From e777b268791a77f71b906ab773818689cd016d74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E8=88=AA=E5=AE=87?= <3364451258@qq.com> Date: Tue, 28 Apr 2026 22:21:06 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8DJSON=E5=AF=BC=E5=85=A5?= =?UTF-8?q?=E3=80=81=E5=A4=87=E4=BB=BD=E7=9B=AE=E5=BD=95=E5=88=9B=E5=BB=BA?= =?UTF-8?q?=E5=92=8C=E5=86=85=E5=AD=98=E5=AE=89=E5=85=A8=E7=AD=89=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复JSON导入时转义字符处理不完整的问题,添加对\b、\f等控制字符的转义 改进备份目录创建逻辑,使用SHCreateDirectoryExW递归创建目录 修复内存分配失败处理,避免空指针解引用 修正选项卡标题设置位置,从Dialog改为Tabs控件 增强导入功能,支持TXT文件导入时选择目标变量类型 优化清理无效路径算法,使用标记数组减少内存移动 修复宽字符环境变量设置,使用_wputenv_s替代putenv 添加导入数据初始化,防止未初始化内存访问 改进文件属性检查,使用宽字符API支持Unicode路径 --- include/core/import_export.h | 3 + src/controller/callbacks_io.c | 13 +- src/controller/callbacks_search.c | 5 +- src/controller/callbacks_sys.c | 10 +- src/core/import_export.c | 206 +++++++++++++++++------------- src/core/path_manager.c | 68 ++++++---- src/core/registry_service.c | 62 ++++----- src/main.c | 10 +- src/ui/main_window.c | 10 +- src/utils/os_env.c | 9 +- src/utils/string_ext.c | 18 ++- 11 files changed, 257 insertions(+), 157 deletions(-) diff --git a/include/core/import_export.h b/include/core/import_export.h index e4cc9a4..98fb95e 100644 --- a/include/core/import_export.h +++ b/include/core/import_export.h @@ -6,6 +6,9 @@ #define EXPORT_VERSION "1.0" +// 导出数据结构 +// 注意:此结构体用于导出时是只读的,items 指针指向外部 StringList 的数据 +// 不要对 ExportData 调用 clear_string_list,会破坏原始数据 typedef struct { StringList system; StringList user; diff --git a/src/controller/callbacks_io.c b/src/controller/callbacks_io.c index 9632812..cb5fab0 100644 --- a/src/controller/callbacks_io.c +++ b/src/controller/callbacks_io.c @@ -42,6 +42,8 @@ int btn_import_cb(Ihandle *self) if (filepath) { ExportData imported; + init_string_list(&imported.system); + init_string_list(&imported.user); ErrorCode import_result = import_paths_from_file(filepath, &imported); if (import_result == ERR_OK) { @@ -51,6 +53,8 @@ int btn_import_cb(Ihandle *self) if (!has_system && !has_user) { IupMessage("错误", "文件中没有找到有效的路径!"); + clear_string_list(&imported.system); + clear_string_list(&imported.user); IupDestroy(filedlg); return IUP_DEFAULT; } @@ -63,7 +67,10 @@ int btn_import_cb(Ihandle *self) } else if (has_system) { - choice = 3; + // TXT 文件导入时,让用户选择目标(系统变量或用户变量) + choice = IupAlarm("导入选项", "请选择导入目标:", + "导入到系统变量", "导入到用户变量", NULL); + // IupAlarm 返回 1 或 2,转换为 1(系统) 或 2(用户) } else { @@ -96,6 +103,10 @@ int btn_import_cb(Ihandle *self) total_imported += imported.user.count; } + // 释放导入数据 + clear_string_list(&imported.system); + clear_string_list(&imported.user); + char msg[256]; snprintf(msg, sizeof(msg), "成功导入 %d 个路径!", total_imported); IupMessage("导入成功", msg); diff --git a/src/controller/callbacks_search.c b/src/controller/callbacks_search.c index ccc2ad3..ba2f894 100644 --- a/src/controller/callbacks_search.c +++ b/src/controller/callbacks_search.c @@ -3,6 +3,7 @@ #include "core/app_context.h" #include "core/lua_config.h" #include "utils/string_ext.h" +#include "utils/safe_string.h" #include "utils/ui_constants.h" #include "ui/ui_utils.h" #include @@ -55,7 +56,9 @@ int list_dropfiles_cb(Ihandle *self, const char *filename, int num, int x, int y else return IUP_DEFAULT; - DWORD attr = GetFileAttributesA(filename); + wchar_t *wfilename = utf8_to_wide(filename); + DWORD attr = wfilename ? GetFileAttributesW(wfilename) : INVALID_FILE_ATTRIBUTES; + free(wfilename); if (attr != INVALID_FILE_ATTRIBUTES && (attr & FILE_ATTRIBUTE_DIRECTORY)) { Ihandle *txt_search = IupGetDialogChild(dlg, CTRL_TXT_SEARCH); diff --git a/src/controller/callbacks_sys.c b/src/controller/callbacks_sys.c index 879b014..8994e64 100644 --- a/src/controller/callbacks_sys.c +++ b/src/controller/callbacks_sys.c @@ -28,7 +28,15 @@ int btn_ok_cb(Ihandle *self) return IUP_DEFAULT; } - backup_registry(); + ErrorCode backup_result = backup_registry(); + if (backup_result != ERR_OK) + { + log_error("Backup failed: error code %d", backup_result); + int choice = IupAlarm("警告", "备份失败!是否继续保存?\n(继续保存可能导致无法恢复)", + "继续保存", "取消", NULL); + if (choice != 1) + return IUP_DEFAULT; + } ErrorCode sys_ok = save_system_paths(&ctx->sys_paths); ErrorCode user_ok = save_user_paths(&ctx->user_paths); diff --git a/src/core/import_export.c b/src/core/import_export.c index 7770b21..aa85a74 100644 --- a/src/core/import_export.c +++ b/src/core/import_export.c @@ -11,25 +11,28 @@ static void get_current_datetime(char *buffer, int size) { time_t now = time(NULL); - struct tm *tm_info = localtime(&now); - strftime(buffer, size, "%Y-%m-%d %H:%M:%S", tm_info); + struct tm tm_info; + localtime_s(&tm_info, &now); + strftime(buffer, size, "%Y-%m-%d %H:%M:%S", &tm_info); } -// 转义 JSON 字符串中的特殊字符 +// 转义 JSON 字符串中的特殊字符(符合 RFC 8259 规范) static char *escape_json_string(const char *str) { if (!str) return NULL; int len = strlen(str); - char *result = (char *)malloc(len * 2 + 1); + // 最坏情况:每个字符都需要 \uXXXX 转义(6字节) + char *result = (char *)malloc(len * 6 + 1); if (!result) return NULL; char *p = result; for (int i = 0; i < len; i++) { - switch (str[i]) + unsigned char c = (unsigned char)str[i]; + switch (c) { case '\\': *p++ = '\\'; @@ -51,8 +54,24 @@ static char *escape_json_string(const char *str) *p++ = '\\'; *p++ = 't'; break; + case '\b': + *p++ = '\\'; + *p++ = 'b'; + break; + case '\f': + *p++ = '\\'; + *p++ = 'f'; + break; default: - *p++ = str[i]; + if (c < 0x20) // 其他控制字符 (0x00-0x1F) + { + sprintf(p, "\\u%04x", c); + p += 6; + } + else + { + *p++ = str[i]; + } break; } } @@ -124,6 +143,9 @@ ErrorCode export_paths_to_file(const ExportData *data, const char *filepath) // 移除字符串首尾的空格、制表符、换行符和回车符 static void trim_whitespace(char *str) { + if (!str || *str == '\0') + return; + char *start = str; while (*start == ' ' || *start == '\t') start++; @@ -152,7 +174,20 @@ static int is_comment_or_empty(const char *line) static int is_json_file(const char *filepath) { const char *ext = strrchr(filepath, '.'); - return ext && strcasecmp(ext, ".json") == 0; + return ext && _stricmp(ext, ".json") == 0; +} + +// 检查引号前是否有奇数个连续反斜杠(奇数个表示引号被转义) +static int is_quote_escaped(const char *quote_pos, const char *line_start) +{ + int backslash_count = 0; + const char *p = quote_pos - 1; + while (p >= line_start && *p == '\\') + { + backslash_count++; + p--; + } + return (backslash_count % 2) == 1; // 奇数个反斜杠表示转义 } // 从文件导入 PATH @@ -204,102 +239,97 @@ ErrorCode import_paths_from_file(const char *filepath, ExportData *data) int in_user = 0; int depth = 0; int in_string = 0; - char path_buffer[4096]; - int path_len = 0; + char key_buffer[256] = {0}; + int key_len = 0; while (fgets(buffer, sizeof(buffer), fp)) { char *p = buffer; while (*p) { - if (*p == '"' && (p == buffer || *(p - 1) != '\\')) + // 处理字符串开始/结束 + if (*p == '"') { - in_string = !in_string; - } - else if (in_string && *p == '\\') - { - p++; - if (*p) + if (!in_string) { - if (*p == 'n') - *p = '\n'; - else if (*p == 'r') - *p = '\r'; - else if (*p == 't') - *p = '\t'; + // 字符串开始 + in_string = 1; + key_len = 0; // 开始收集键名或字符串内容 + } + else if (!is_quote_escaped(p, buffer)) + { + // 字符串结束(未转义的引号) + in_string = 0; + + // 在 depth 1 时,检查刚结束的字符串是否是键名 + if (depth == 1) + { + key_buffer[key_len] = '\0'; + if (strcmp(key_buffer, "system") == 0) + { + in_system = 1; + in_user = 0; + } + else if (strcmp(key_buffer, "user") == 0) + { + in_user = 1; + in_system = 0; + } + } + // 在 depth 2 时,如果在 system/user 数组内,提取路径 + else if (depth == 2 && (in_system || in_user)) + { + key_buffer[key_len] = '\0'; + if (key_len > 0) + { + StringList *target = in_system ? &data->system : &data->user; + add_string_list(target, key_buffer); + } + } + } + else + { + // 转义的引号,作为内容的一部分 + if (key_len < (int)sizeof(key_buffer) - 1) + key_buffer[key_len++] = *p; } } - else if (!in_string) + else if (in_string) { + // 在字符串内,收集内容 + if (*p == '\\' && *(p + 1)) + { + // 处理转义序列 + p++; + char ch; + switch (*p) + { + case 'n': ch = '\n'; break; + case 'r': ch = '\r'; break; + case 't': ch = '\t'; break; + case 'b': ch = '\b'; break; + case 'f': ch = '\f'; break; + case '\\': ch = '\\'; break; + case '"': ch = '"'; break; + case '/': ch = '/'; break; + default: ch = *p; break; + } + if (key_len < (int)sizeof(key_buffer) - 1) + key_buffer[key_len++] = ch; + } + else + { + if (key_len < (int)sizeof(key_buffer) - 1) + key_buffer[key_len++] = *p; + } + } + else + { + // 不在字符串内 if (*p == '{' || *p == '[') depth++; else if (*p == '}' || *p == ']') depth--; - else if (depth == 1 && *p == '"') - { - if (strncmp(p, "\"system\"", 8) == 0) - { - in_system = 1; - in_user = 0; - } - else if (strncmp(p, "\"user\"", 6) == 0) - { - in_user = 1; - in_system = 0; - } - } - else if (in_system && depth == 2 && *p == '"') - { - path_len = 0; - p++; - while (*p && path_len < (int)sizeof(path_buffer) - 1) - { - if (*p == '"' && *(p - 1) != '\\') - break; - if (*p == '\\' && *(p + 1)) - { - p++; - if (*p == 'n') - *p = '\n'; - else if (*p == 'r') - *p = '\r'; - else if (*p == 't') - *p = '\t'; - } - path_buffer[path_len++] = *p++; - } - if (path_len > 0) - { - path_buffer[path_len] = '\0'; - add_string_list(&data->system, path_buffer); - } - } - else if (in_user && depth == 2 && *p == '"') - { - path_len = 0; - p++; - while (*p && path_len < (int)sizeof(path_buffer) - 1) - { - if (*p == '"' && *(p - 1) != '\\') - break; - if (*p == '\\' && *(p + 1)) - { - p++; - if (*p == 'n') - *p = '\n'; - else if (*p == 'r') - *p = '\r'; - else if (*p == 't') - *p = '\t'; - } - path_buffer[path_len++] = *p++; - } - if (path_len > 0) - { - path_buffer[path_len] = '\0'; - add_string_list(&data->user, path_buffer); - } - } } p++; } diff --git a/src/core/path_manager.c b/src/core/path_manager.c index 6df9a9f..feb715e 100644 --- a/src/core/path_manager.c +++ b/src/core/path_manager.c @@ -52,44 +52,68 @@ ErrorCode path_manager_move_down(StringList *list, int index) } // 清理无效路径项 +// 算法:先标记需要删除的项,然后从后向前批量删除,减少内存移动 ErrorCode path_manager_clean(StringList *list) { if (!list) return ERR_NULL_PTR; - + if (list->count == 0) return ERR_OK; + + // 分配标记数组 + char *marks = (char *)calloc(list->count, sizeof(char)); + if (!marks) return ERR_OUT_OF_MEMORY; + int removed_count = 0; + // 第一遍:标记无效路径和重复路径 for (int i = list->count - 1; i >= 0; i--) { char *item = list->items[i]; - if (!item) continue; - - int should_remove = 0; + if (!item) + { + marks[i] = 1; + removed_count++; + continue; + } + // 检查路径有效性 if (!is_path_valid(item)) { - should_remove = 1; - } - else - { - for (int j = 0; j < i; j++) - { - char *prev_item = list->items[j]; - if (prev_item && _stricmp(item, prev_item) == 0) - { - should_remove = 1; - break; - } - } + marks[i] = 1; + removed_count++; + continue; } - if (should_remove) + // 检查是否与前面的项重复(只检查未被标记的项) + for (int j = 0; j < i; j++) { - path_manager_remove_at(list, i); - removed_count++; + if (!marks[j] && list->items[j] && _stricmp(item, list->items[j]) == 0) + { + marks[i] = 1; + removed_count++; + break; + } } } - - log_info("Cleaned paths: removed %d invalid/duplicate paths, remaining %d", + + // 第二遍:从后向前删除标记的项,避免多次内存移动 + for (int i = list->count - 1; i >= 0; i--) + { + if (marks[i]) + { + free(list->items[i]); + // 移动后续元素 + for (int j = i; j < list->count - 1; j++) + { + list->items[j] = list->items[j + 1]; + } + list->items[list->count - 1] = NULL; + list->count--; + } + } + + free(marks); + + log_info("Cleaned paths: removed %d invalid/duplicate paths, remaining %d", removed_count, list->count); return ERR_OK; } \ No newline at end of file diff --git a/src/core/registry_service.c b/src/core/registry_service.c index 96f7b5a..6089b12 100644 --- a/src/core/registry_service.c +++ b/src/core/registry_service.c @@ -26,38 +26,40 @@ static ErrorCode load_single_path(HKEY hKeyRoot, const wchar_t *regPath, StringL if (res == ERROR_SUCCESS) { wchar_t *buffer = (wchar_t *)malloc(size + 2); - if (buffer) + if (!buffer) { - memset(buffer, 0, size + 2); - if (RegQueryValueExW(hKey, REG_VALUE, NULL, &type, (LPBYTE)buffer, &size) == ERROR_SUCCESS) - { - wchar_t *current = buffer; - wchar_t *next_semicolon = NULL; - - while (*current) - { - next_semicolon = wcschr(current, L';'); - if (next_semicolon) - *next_semicolon = L'\0'; - - if (wcslen(current) > 0) - { - char *utf8_str = wide_to_utf8(current); - if (utf8_str) - { - add_string_list(list, utf8_str); - free(utf8_str); - } - } - - if (next_semicolon) - current = next_semicolon + 1; - else - break; - } - } - free(buffer); + RegCloseKey(hKey); + return ERR_OUT_OF_MEMORY; } + memset(buffer, 0, size + 2); + if (RegQueryValueExW(hKey, REG_VALUE, NULL, &type, (LPBYTE)buffer, &size) == ERROR_SUCCESS) + { + wchar_t *current = buffer; + wchar_t *next_semicolon = NULL; + + while (*current) + { + next_semicolon = wcschr(current, L';'); + if (next_semicolon) + *next_semicolon = L'\0'; + + if (wcslen(current) > 0) + { + char *utf8_str = wide_to_utf8(current); + if (utf8_str) + { + add_string_list(list, utf8_str); + free(utf8_str); + } + } + + if (next_semicolon) + current = next_semicolon + 1; + else + break; + } + } + free(buffer); } RegCloseKey(hKey); return ERR_OK; diff --git a/src/main.c b/src/main.c index fc3f77a..20073cf 100644 --- a/src/main.c +++ b/src/main.c @@ -50,7 +50,7 @@ int main(int argc, char **argv) log_info("PathEditor starting..."); // 强制设置 UTF8MODE 环境变量,必须在 IupOpen 之前 - putenv("IUP_UTF8MODE=YES"); + _wputenv_s(L"IUP_UTF8MODE", L"YES"); IupOpen(&argc, &argv); IupSetGlobal("UTF8MODE", "YES"); @@ -105,12 +105,16 @@ int main(int argc, char **argv) // 检查管理员权限 if (!check_admin()) { - IupMessage(_("Warning"), _(lua_config_get_string("status", "admin_warning"))); + const char *admin_msg = lua_config_get_string("status", "admin_warning"); + IupMessage(_("Warning"), admin_msg ? _(admin_msg) : "需要管理员权限才能编辑环境变量"); // 设置只读状态提示 Ihandle *lbl_status = IupGetDialogChild(dlg, CTRL_LBL_STATUS); if (lbl_status) - IupSetAttribute(lbl_status, "TITLE", _(lua_config_get_string("status", "readonly"))); + { + const char *readonly_msg = lua_config_get_string("status", "readonly"); + IupSetAttribute(lbl_status, "TITLE", readonly_msg ? _(readonly_msg) : "只读模式"); + } // 禁用所有需要管理员权限的按钮 for (int i = 0; i < ADMIN_DISABLE_COUNT; i++) diff --git a/src/ui/main_window.c b/src/ui/main_window.c index 02b3316..d554ad3 100644 --- a/src/ui/main_window.c +++ b/src/ui/main_window.c @@ -180,9 +180,13 @@ void refresh_main_window_ui(Ihandle *main_dlg) // 设置窗口标题 IupSetAttribute(main_dlg, "TITLE", _(lua_config_get_string("app", "name"))); - // 设置选项卡标题 - IupSetAttribute(main_dlg, "TABTITLE0", _(lua_config_get_string("label", "tab_sys"))); - IupSetAttribute(main_dlg, "TABTITLE1", _(lua_config_get_string("label", "tab_user"))); + // 设置选项卡标题(需要设置在 Tabs 控件上,而非 Dialog) + Ihandle *tabs = IupGetDialogChild(main_dlg, CTRL_TABS_MAIN); + if (tabs) + { + IupSetAttribute(tabs, "TABTITLE0", _(lua_config_get_string("label", "tab_sys"))); + IupSetAttribute(tabs, "TABTITLE1", _(lua_config_get_string("label", "tab_user"))); + } // 辅助函数:设置子控件标题 #define SET_CHILD_TITLE(btn_name, text_key) \ diff --git a/src/utils/os_env.c b/src/utils/os_env.c index de032fb..6f9ed93 100644 --- a/src/utils/os_env.c +++ b/src/utils/os_env.c @@ -56,16 +56,17 @@ ErrorCode backup_registry(void) return ERR_FAILED; } - // 创建备份目录 + // 创建备份目录(递归创建中间目录) wchar_t backup_dir[MAX_PATH]; swprintf(backup_dir, MAX_PATH, L"%s\\PathEditor\\backups", appdata_path); - CreateDirectoryW(backup_dir, NULL); + SHCreateDirectoryExW(NULL, backup_dir, NULL); // 生成时间戳 time_t t = time(NULL); - struct tm *tm_info = localtime(&t); + struct tm tm_info; + localtime_s(&tm_info, &t); wchar_t timestamp[64]; - wcsftime(timestamp, sizeof(timestamp), L"%Y%m%d_%H%M%S", tm_info); + wcsftime(timestamp, sizeof(timestamp) / sizeof(timestamp[0]), L"%Y%m%d_%H%M%S", &tm_info); // 构造备份文件名 wchar_t backup_file[MAX_PATH]; diff --git a/src/utils/string_ext.c b/src/utils/string_ext.c index 6a3d772..fdcfcc8 100644 --- a/src/utils/string_ext.c +++ b/src/utils/string_ext.c @@ -78,10 +78,17 @@ void add_string_list(StringList *list, const char *str) return; if (list->count >= list->capacity) { - list->capacity = (list->capacity == 0) ? 16 : list->capacity * 2; - list->items = (char **)realloc(list->items, list->capacity * sizeof(char *)); + int new_capacity = (list->capacity == 0) ? 16 : list->capacity * 2; + char **new_items = (char **)realloc(list->items, new_capacity * sizeof(char *)); + if (!new_items) + return; // realloc 失败,保留原数据 + list->items = new_items; + list->capacity = new_capacity; } - list->items[list->count] = _strdup(str); // 复制字符串 + char *dup = _strdup(str); + if (!dup) + return; // _strdup 失败,不递增 count + list->items[list->count] = dup; list->count++; } @@ -98,8 +105,11 @@ int string_list_set(StringList *list, int index, const char *str) { if (!list || index < 0 || index >= list->count || !str) return -1; + char *dup = _strdup(str); + if (!dup) + return -1; // _strdup 失败,保留旧数据 free(list->items[index]); - list->items[index] = _strdup(str); + list->items[index] = dup; return 0; }