mirror of
https://github.com/LHY0125/PathEditor.git
synced 2026-06-29 18:15:55 +08:00
feat: CSV 导入导出、导入撤销支持及多项 bug 修复
功能: - 新增 CSV 格式导入导出支持(含 BOM 处理、引号转义、智能标题行检测) - 导入操作支持撤销/重做 - 保存时 PATH 长度检查与警告 - 深色模式状态持久化(darkmode.txt) - 提取 get_current_target/push_record 为共享函数,消除控制器层重复代码 - 新增 string_list_insert_at,修复撤销删除时的索引恢复 - 新增 undo_redo、error_code、import_export 单元测试 Bug 修复: - 修复备份目录对话框和失败原因的硬编码中文字符串 - 提取 get_exe_dir 到 os_env 消除 i18n.c/ui_utils.c 重复定义 - 修复导入撤销 old_sys/old_user 内存管理(push 后置 NULL 防止重复释放) - 修复 CSV 导出转义与导入解析不一致(移除反斜杠转义,依赖 CSV 引号机制) - 修正 PATH 长度 8191 限制描述为 "command line safe limit"
This commit is contained in:
+137
-12
@@ -5,6 +5,7 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
#include <time.h>
|
||||
|
||||
// 获取当前日期时间
|
||||
@@ -103,14 +104,6 @@ static char *escape_csv_field(const char *str)
|
||||
*p++ = '"';
|
||||
*p++ = '"';
|
||||
break;
|
||||
case '\n':
|
||||
*p++ = '\\';
|
||||
*p++ = 'n';
|
||||
break;
|
||||
case '\r':
|
||||
*p++ = '\\';
|
||||
*p++ = 'r';
|
||||
break;
|
||||
default:
|
||||
*p++ = str[i];
|
||||
break;
|
||||
@@ -169,7 +162,7 @@ static ErrorCode export_paths_to_json(const ExportData *data, FILE *fp)
|
||||
|
||||
// 导出 PATH 到 CSV 文件
|
||||
// 格式:type,path
|
||||
// type: "system" 或 "user"
|
||||
// type: system 或 user
|
||||
static ErrorCode export_paths_to_csv(const ExportData *data, FILE *fp)
|
||||
{
|
||||
// 写入 UTF-8 BOM
|
||||
@@ -186,7 +179,7 @@ static ErrorCode export_paths_to_csv(const ExportData *data, FILE *fp)
|
||||
char *escaped = escape_csv_field(data->system.items[i]);
|
||||
if (escaped)
|
||||
{
|
||||
fprintf(fp, "\"system\",\"%s\"\n", escaped);
|
||||
fprintf(fp, "system,%s\n", escaped);
|
||||
free(escaped);
|
||||
}
|
||||
}
|
||||
@@ -200,7 +193,7 @@ static ErrorCode export_paths_to_csv(const ExportData *data, FILE *fp)
|
||||
char *escaped = escape_csv_field(data->user.items[i]);
|
||||
if (escaped)
|
||||
{
|
||||
fprintf(fp, "\"user\",\"%s\"\n", escaped);
|
||||
fprintf(fp, "user,%s\n", escaped);
|
||||
free(escaped);
|
||||
}
|
||||
}
|
||||
@@ -263,7 +256,7 @@ static void trim_whitespace(char *str)
|
||||
start++;
|
||||
|
||||
char *end = str + strlen(str) - 1;
|
||||
while (end > start && (*end == ' ' || *end == '\t' || *end == '\n' || *end == '\r'))
|
||||
while (end >= start && (*end == ' ' || *end == '\t' || *end == '\n' || *end == '\r'))
|
||||
*end-- = '\0';
|
||||
|
||||
if (start != str)
|
||||
@@ -289,6 +282,133 @@ static int is_json_file(const char *filepath)
|
||||
return ext && _stricmp(ext, ".json") == 0;
|
||||
}
|
||||
|
||||
// 检查文件是否为 CSV 格式(通过扩展名)
|
||||
static int is_csv_file(const char *filepath)
|
||||
{
|
||||
const char *ext = strrchr(filepath, '.');
|
||||
return ext && _stricmp(ext, ".csv") == 0;
|
||||
}
|
||||
|
||||
// 解析 CSV 字段(处理引号包围的字段)
|
||||
// 返回值:指向下一个字段的指针,或 NULL
|
||||
static const char *parse_csv_field(const char *line, char *field, int field_size)
|
||||
{
|
||||
if (!line || !field || field_size <= 0)
|
||||
return NULL;
|
||||
|
||||
const char *p = line;
|
||||
char *out = field;
|
||||
char *end = field + field_size - 1;
|
||||
|
||||
if (*p == '"')
|
||||
{
|
||||
// 引号包围的字段
|
||||
p++; // 跳过开始引号
|
||||
while (*p && out < end)
|
||||
{
|
||||
if (*p == '"')
|
||||
{
|
||||
if (*(p + 1) == '"')
|
||||
{
|
||||
// 转义的引号 ""
|
||||
*out++ = '"';
|
||||
p += 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 结束引号
|
||||
p++; // 跳过结束引号
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
*out++ = *p++;
|
||||
}
|
||||
}
|
||||
// 跳过逗号分隔符
|
||||
if (*p == ',') p++;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 非引号字段(按逗号分隔)
|
||||
while (*p && *p != ',' && out < end)
|
||||
{
|
||||
*out++ = *p++;
|
||||
}
|
||||
if (*p == ',') p++;
|
||||
}
|
||||
|
||||
*out = '\0';
|
||||
return p;
|
||||
}
|
||||
|
||||
// 导入 CSV 格式的 PATH 文件
|
||||
static ErrorCode import_paths_from_csv(const char *filepath, ExportData *data)
|
||||
{
|
||||
FILE *fp = fopen(filepath, "rb");
|
||||
if (!fp)
|
||||
{
|
||||
log_error("Failed to open file for import: %s", filepath);
|
||||
return ERR_FILE_NOT_FOUND;
|
||||
}
|
||||
|
||||
char line[4096];
|
||||
int line_num = 0;
|
||||
int header_skipped = 0;
|
||||
|
||||
while (fgets(line, sizeof(line), fp))
|
||||
{
|
||||
line_num++;
|
||||
trim_whitespace(line);
|
||||
|
||||
if (line[0] == '\0')
|
||||
continue;
|
||||
|
||||
// 跳过 UTF-8 BOM
|
||||
const char *start = line;
|
||||
if ((unsigned char)start[0] == 0xEF &&
|
||||
(unsigned char)start[1] == 0xBB &&
|
||||
(unsigned char)start[2] == 0xBF)
|
||||
{
|
||||
start += 3;
|
||||
}
|
||||
|
||||
// 智能检测标题行:第一行包含 "type" 和 "path" 则视为标题
|
||||
if (!header_skipped)
|
||||
{
|
||||
header_skipped = 1;
|
||||
char lower[256];
|
||||
strncpy(lower, start, sizeof(lower) - 1);
|
||||
lower[sizeof(lower) - 1] = '\0';
|
||||
for (char *p = lower; *p; p++) *p = (char)tolower((unsigned char)*p);
|
||||
if (strstr(lower, "type") && strstr(lower, "path"))
|
||||
continue;
|
||||
}
|
||||
|
||||
char type[32] = {0};
|
||||
char path[4096] = {0};
|
||||
|
||||
start = parse_csv_field(start, type, sizeof(type));
|
||||
if (!start)
|
||||
continue;
|
||||
parse_csv_field(start, path, sizeof(path));
|
||||
|
||||
if (path[0] == '\0')
|
||||
continue;
|
||||
|
||||
if (_stricmp(type, "system") == 0)
|
||||
add_string_list(&data->system, path);
|
||||
else if (_stricmp(type, "user") == 0)
|
||||
add_string_list(&data->user, path);
|
||||
}
|
||||
|
||||
fclose(fp);
|
||||
log_info("Imported paths from CSV file: sys=%d, user=%d, file=%s",
|
||||
data->system.count, data->user.count, filepath);
|
||||
return ERR_OK;
|
||||
}
|
||||
|
||||
// 检查引号前是否有奇数个连续反斜杠(奇数个表示引号被转义)
|
||||
static int is_quote_escaped(const char *quote_pos, const char *line_start)
|
||||
{
|
||||
@@ -311,6 +431,11 @@ ErrorCode import_paths_from_file(const char *filepath, ExportData *data)
|
||||
init_string_list(&data->system);
|
||||
init_string_list(&data->user);
|
||||
|
||||
if (is_csv_file(filepath))
|
||||
{
|
||||
return import_paths_from_csv(filepath, data);
|
||||
}
|
||||
|
||||
if (!is_json_file(filepath))
|
||||
{
|
||||
FILE *fp = fopen(filepath, "rb");
|
||||
|
||||
+5
-18
@@ -149,11 +149,11 @@ int undo(UndoRedoManager *mgr, StringList *sys_paths, StringList *user_paths)
|
||||
break;
|
||||
|
||||
case OP_DELETE:
|
||||
// 撤销删除:恢复被删除的路径
|
||||
// 撤销删除:恢复被删除的路径到原始位置
|
||||
for (int i = 0; i < rec->count; i++)
|
||||
{
|
||||
if (rec->old_paths[i])
|
||||
add_string_list(target, rec->old_paths[i]);
|
||||
string_list_insert_at(target, rec->index + i, rec->old_paths[i]);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -221,23 +221,10 @@ int redo(UndoRedoManager *mgr, StringList *sys_paths, StringList *user_paths)
|
||||
break;
|
||||
|
||||
case OP_DELETE:
|
||||
// 重做删除:重新删除路径
|
||||
for (int i = 0; i < rec->count; i++)
|
||||
// 重做删除:从高索引到低索引删除,避免索引偏移
|
||||
for (int i = rec->count - 1; i >= 0; i--)
|
||||
{
|
||||
// 找到并删除对应路径
|
||||
for (int j = 0; j < target->count; j++)
|
||||
{
|
||||
if (target->items[j] && rec->old_paths[i] &&
|
||||
strcmp(target->items[j], rec->old_paths[i]) == 0)
|
||||
{
|
||||
free(target->items[j]);
|
||||
// 移动后面的元素
|
||||
for (int k = j; k < target->count - 1; k++)
|
||||
target->items[k] = target->items[k + 1];
|
||||
target->count--;
|
||||
break;
|
||||
}
|
||||
}
|
||||
path_manager_remove_at(target, rec->index + i);
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user