diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index f30a2fe79..839307a21 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -26,7 +26,7 @@ just test / test-cli / fastcheck ## 架构速览 - `src/chsrc-main.c` — 入口:`main()`、CLI 解析、输出显示 -- `src/framework/` — 核心骨架:`struct.h`(`Dish_t`/`Source_t`/`MirrorSite_t`/Chef DSL 宏定义)、`core.c`(全局状态、测速)、`chef.c`(Chef DSL 实现)、`helper.c`(`hp_*` 辅助函数)、`mirror.c`(通用镜像站) +- `src/framework/` — 核心骨架:`struct.h`(`Dish_t`/`Source_t`/`MirrorSite_t`/Chef DSL 宏定义)、`base.c`/`OS.c`/`source.c`/`waiter.c`(全局状态、系统与文件操作、测速与命令分派)、`env.c`(跨平台环境变量幂等持久化读写:`chsrc_set_env`/`chsrc_set_env_block`/`chsrc_get_env`)、`chef.c`(Chef DSL 实现)、`helper.c`(`hp_*` 辅助函数)、`mirror.c`(通用镜像站) - `src/recipe/{lang,os,ware}/` — 按类别(`pl`/`os`/`wr`)组织的 recipe;新 recipe 须在 `src/recipe/menu.c` 中注册(`#include` + `add()`)才会出现在菜单中 - `lib/xy.h` — 独立 C11 工具库(MIT):字符串、系统检测、日志、数据结构、文件 I/O。约定 `return caller-free` 表示调用方必须释放返回值 - 执行流程:`main()` → `chsrc_init_framework()` → 解析 CLI → 在 `pl`/`os`/`wr` 菜单中匹配 dish → `preparefn()` → 按操作分派 `getfn`/`setfn`/`resetfn` diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 000000000..4582d6ef2 --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,7 @@ +{ + "MD012": false, + "MD013": false, + "MD033": false, + "MD041": false, + "MD045": false +} diff --git a/Makefile b/Makefile index eaaa5d424..c0a1a61b2 100755 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ # | @Mikachu2333 # | # Created On : <2023-08-28> -# Last Modified : <2026-08-14> +# Last Modified : <2026-10-28> # # 请阅读 ./doc/01-开发与构建.md 来使用 # -------------------------------------------------------------- @@ -66,6 +66,11 @@ ifeq ($(On-Windows), 1) # 非 MSYS2 环境 (独立 LLVM) 下回退到 x86_64 Windows-Clang-Arch = $(if $(MSYSTEM_CARCH),$(MSYSTEM_CARCH),x86_64) CFLAGS_clang = -target $(Windows-Clang-Arch)-pc-windows-gnu + + # 注意: Android 交叉编译同样在 Windows/MSYS2 下进行, 必须排除 Windows 导入库 + ifneq ($(CROSS_BUILD_WINDOWS_FOR_ANDROID), 1) + LDLIBS_win = -ladvapi32 -lkernel32 -luser32 -lshell32 + endif endif ifeq ($(CC), clang) @@ -161,13 +166,13 @@ c: clean #============ Target 定义开始 ================ # 编译开始结束时的提示信息 -Starting-Echo-Info = echo Starting: Build in ${1} mode: \'$(CC)\' $(CFLAGS) -o ${2} +Starting-Echo-Info = echo Starting: Build in ${1} mode: \'$(CC)\' $(CFLAGS) $(LDLIBS_win) -o ${2} Finished-Echo-Info = echo Finished: Build in ${1} mode # 真正的编译命令 -Build-Command = $(CC) src/chsrc-main.c $(CFLAGS) $(CFLAGS_warning) -o $(1) +Build-Command = $(CC) src/chsrc-main.c $(CFLAGS) $(CFLAGS_warning) $(LDLIBS_win) -o $(1) -Build-Command-For-Release = $(CC) src/chsrc-main.c ${Windows-Res-File} $(CFLAGS) $(CFLAGS_warning) -o $(1) +Build-Command-For-Release = $(CC) src/chsrc-main.c ${Windows-Res-File} $(CFLAGS) $(CFLAGS_warning) $(LDLIBS_win) -o $(1) @@ -237,12 +242,12 @@ test-make-var: # 这两个测试文件都用 DEBUG mode test-xy: CFLAGS += $(CFLAGS_debug) test-xy: - @$(CC) test/xy.c $(CFLAGS) -o xy + @$(CC) test/xy.c $(CFLAGS) $(LDLIBS_win) -o xy @./xy test-fw: CFLAGS += $(CFLAGS_debug) test-fw: - @$(CC) test/fw.c $(CFLAGS) -o fw + @$(CC) test/fw.c $(CFLAGS) $(LDLIBS_win) -o fw @./fw check: test diff --git "a/doc/10-\345\246\202\344\275\225\347\274\226\345\206\231recipe.md" "b/doc/10-\345\246\202\344\275\225\347\274\226\345\206\231recipe.md" index 3e51606b2..1851908af 100644 --- "a/doc/10-\345\246\202\344\275\225\347\274\226\345\206\231recipe.md" +++ "b/doc/10-\345\246\202\344\275\225\347\274\226\345\206\231recipe.md" @@ -7,7 +7,7 @@ ! Contributors : @Mikachu2333 ! | ! Created On : <2024-08-19> - ! Last Modified : <2026-09-01> + ! Last Modified : <2026-10-28> ! ---------------------------------------------------------- --> # Write A Recipe Even If You Don't Know C @@ -82,6 +82,7 @@ 1. 看一眼就能上手的参照物是 [PHP recipe](../src/recipe/lang/PHP.c) 2. 最好的参照物是 [Ruby recipe](../src/recipe/lang/Ruby/Ruby.c) 3. 组换源参照物是 [JS combo recipe](../src/recipe/lang/JavaScript/JavaScript.c) + 4. 环境变量型换源参照物是单变量的 [Pub recipe](../src/recipe/lang/Dart/Pub.c) 与多变量的 [rustup recipe](../src/recipe/lang/Rust/rustup.c) 5. 在 [Wiki] 中记录的镜像站中寻找可用源;可以额外补充镜像站 @@ -95,6 +96,11 @@ 1. `framework/*.c` 中以 `chsrc_`, `chef_`, `hp_` 开头的所有函数或宏 2. `xy.h` 中以 `xy_` 开头的所有函数或宏 + 3. 若菜品通过**环境变量**持久化换源,请统一使用 `framework/env.c` 提供的跨平台幂等接口,**切勿**手工向 `.bashrc`/`.zshrc` 追加字符串或直接调用 Windows `setx`: + - `chsrc_set_env (name, value)`:写入或更新单个持久化环境变量(传入 `NULL` 表示删除该变量,常用于 `_resetsrc()`) + - `chsrc_set_env_block (name1, value1, ..., NULL)`:原子写入或删除多个环境变量(一次备份、一次落盘/广播、一次提示) + - `chsrc_get_env (var1, ..., NULL)`:读取并打印已持久化的环境变量值(持久化缺失时回退当前终端值;两者同时存在且不一致时会提示) + - 作用域声明上,若某作用域官方支持但 `chsrc` 仅输出手工配置指引,可标记为 `ScopeCap_Able_But_Just_Docs` 一个简单的方法是,在 VS Code 中按快捷键 `Ctrl-T` 搜索上述三种前缀 diff --git a/justfile b/justfile index 2554a5439..ba99e586f 100644 --- a/justfile +++ b/justfile @@ -8,7 +8,7 @@ # | @NewbieXvwu # | # Created On : <2025-06-18> -# Last Modified : <2026-07-25> +# Last Modified : <2026-10-28> # # 该文件主要用于在原生Windows上执行项目的基本任务,而不借助于 # GNU make 以及相应的 MSYS2、Cygwin 环境 @@ -81,10 +81,14 @@ CFLAGS_chk_static := if STATIC == '1' { CFLAGS_optimization := "-O2" +LDLIBS_win := if os() == 'windows' { + '-ladvapi32 -lkernel32 -luser32 -lshell32' +} else { '' } -CFLAGS_dev_mode_prompt := CFLAGS_base -CFLAGS_debug_mode_prompt := CFLAGS_base + ' ' + CFLAGS_debug -CFLAGS_release_mode_prompt := CFLAGS_base + ' ' + CFLAGS_optimization + ' ' + CFLAGS_chk_static + +CFLAGS_dev_mode_prompt := CFLAGS_base + ' ' + LDLIBS_win +CFLAGS_debug_mode_prompt := CFLAGS_base + ' ' + CFLAGS_debug + ' ' + LDLIBS_win +CFLAGS_release_mode_prompt := CFLAGS_base + ' ' + CFLAGS_optimization + ' ' + CFLAGS_chk_static + ' ' + LDLIBS_win CFLAGS_dev_mode := CFLAGS_dev_mode_prompt + ' ' + CFLAGS_warn CFLAGS_debug_mode := CFLAGS_debug_mode_prompt + ' ' + CFLAGS_warn diff --git a/lib/xy.h b/lib/xy.h index 7e4286c85..b5a128763 100644 --- a/lib/xy.h +++ b/lib/xy.h @@ -10,7 +10,7 @@ * | AnonTokio * | * Created On : <2023-08-28> - * Last Modified : <2026-08-13> + * Last Modified : <2026-10-28> * * * xy: 襄阳、咸阳 @@ -108,6 +108,11 @@ xy = +/** + * 平台判定约定: + * 1. 编译期 `#ifdef XY_Build_On_Windows` 仅用于包裹 Win32 SDK 专有头文件、类型与 API 调用 + * 2. 运行期 `if (xy.on_windows)` 用于纯 C 跨平台分支逻辑,二者真值严格一致 + */ #ifdef _WIN32 #define XY_Build_On_Windows 1 @@ -1393,50 +1398,32 @@ xy_dir_exist (const char *path) { char *allocated_dir = NULL; const char *dir = path; - if (xy.on_windows) + + if (xy_str_start_with (path, "~")) { - if (xy_str_start_with (path, "~")) - { - allocated_dir = xy_2strcat (xy_os_home, path + 1); - dir = allocated_dir; - } + allocated_dir = xy_2strcat (xy_os_home, path + 1); + dir = allocated_dir; } - if (xy.on_windows) - { -#ifdef XY_Build_On_Windows - // 也可以用 opendir() #include - DWORD attr = GetFileAttributesA (dir); + bool result = false; - bool result = false; - if (attr == INVALID_FILE_ATTRIBUTES) - { - // Q: 我们应该报错吗? - result = false; - } - else if (attr & FILE_ATTRIBUTE_DIRECTORY) - { - result = true; - } - else - { - result = false; - } - if (allocated_dir) free (allocated_dir); - return result; -#endif +#ifdef XY_Build_On_Windows + DWORD attr = GetFileAttributesA (dir); + if (attr != INVALID_FILE_ATTRIBUTES && (attr & FILE_ATTRIBUTE_DIRECTORY)) + { + result = true; } - else +#else + DIR *d = opendir (dir); + if (d) { - char *tmp_cmd = xy_2strcat ("test -d ", dir); - int status = system (tmp_cmd); - free (tmp_cmd); - bool result = (0==status); - if (allocated_dir) free (allocated_dir); - return result; + closedir (d); + result = true; } +#endif - return false; + if (allocated_dir) free (allocated_dir); + return result; } /** @@ -1546,29 +1533,25 @@ xy_parent_dir (const char *path) void _xy_detect_os () { +#ifdef XY_Build_On_Windows /** - * 首先判断是否为 Windows - * - * SystemDrive 为 C: + * Windows 原生编译目标 (_WIN32) + * 使运行期 xy.on_windows 与编译期 XY_Build_On_Windows 严格等价 */ - char *drive = getenv ("SystemDrive"); - if (drive) - { - char path[256]; - /** - * C:\Users 目录都可能不存在! - * https://github.com/RubyMetric/chsrc/issues/361 - */ - snprintf (path, sizeof (path), "%s\\Windows", drive); - DIR *d = opendir (path); - if (d) - { - xy.on_windows = true; - xy.os_family = "windows"; - closedir (d); - return; - } - } + xy.on_windows = true; + xy.os_family = "windows"; + return; +#endif + +#if defined(__CYGWIN__) || defined(__MSYS__) + /** + * Cygwin / MSYS2 POSIX 运行时未定义 _WIN32,无 Win32 SDK 头与注册表约定, + * 归入 Unix 分支处理 + */ + xy.on_linux = true; + xy.os_family = "unix"; + return; +#endif /* 判断 Linux */ FILE *fp = fopen ("/proc/version", "r"); diff --git a/src/chsrc-main.c b/src/chsrc-main.c index ec9679edf..035fd3910 100644 --- a/src/chsrc-main.c +++ b/src/chsrc-main.c @@ -36,6 +36,7 @@ #include "framework/version.h" #include "framework/base.c" #include "framework/OS.c" +#include "framework/env.c" #include "framework/dish.c" #include "framework/chef.c" #include "framework/source.c" @@ -160,16 +161,24 @@ main (int argc, char const *argv[]) : " -local is deprecated, please use -scope=project"; chsrc_warn (msg); } - else if (xy_str_start_with (argv[i], "-scope")) + else if ( xy_streql (argv[i], "-scope") + || xy_str_start_with (argv[i], "-scope=")) { const char *scope = NULL; if (xy_streql (argv[i], "-scope")) { - scope = argv[i+1]; + if (i + 1 > argc) + { + char *msg = ENGLISH ? "Option -scope requires an argument. Valid scopes are: default, project, user, system" + : "-scope 选项缺少参数。有效的换源作用域为: default, project, user, system"; + chsrc_error (msg); + return Exit_Unknown; + } + scope = argv[++i]; cli_arg_Dish_pos++; cli_arg_Mirror_pos++; } - else if (xy_str_start_with (argv[i], "-scope=")) + else { scope = argv[i] + 7; } @@ -224,8 +233,8 @@ main (int argc, char const *argv[]) command = "help"; /* 交到下方处理 */ } else if ( xy_streql (argv[i], "-v") - || xy_streql (command, "-version") - || xy_streql (command, "--version")) + || xy_streql (argv[i], "-version") + || xy_streql (argv[i], "--version")) { command = "version"; /* 交到下方处理 */ } diff --git a/src/framework/OS.c b/src/framework/OS.c index c4033a38a..e280d5cc0 100644 --- a/src/framework/OS.c +++ b/src/framework/OS.c @@ -10,7 +10,7 @@ * | @NewbieXvwu * | * Created On : <2023-08-29> - * Last Modified : <2026-08-19> + * Last Modified : <2026-10-28> * * chsrc OS operations * ------------------------------------------------------------*/ @@ -19,6 +19,12 @@ #define NoMark "x" #define HalfYesMark "⍻" +#ifdef XY_Build_On_Windows +#include +#include +#include +#endif + static void log_for_file_write (const char *filename, bool is_overwrite) { @@ -276,26 +282,49 @@ chsrc_check_file (char *path) +bool +chsrc_is_root (void) +{ +#ifdef XY_Build_On_Windows + BOOL is_admin = FALSE; + SID_IDENTIFIER_AUTHORITY nt_authority = SECURITY_NT_AUTHORITY; + PSID admin_group = NULL; + + if (AllocateAndInitializeSid (&nt_authority, 2, + SECURITY_BUILTIN_DOMAIN_RID, + DOMAIN_ALIAS_RID_ADMINS, + 0, 0, 0, 0, 0, 0, &admin_group)) + { + if (!CheckTokenMembership (NULL, admin_group, &is_admin)) + { + is_admin = FALSE; + } + FreeSid (admin_group); + } + return (is_admin != FALSE); +#else + return (0 == geteuid ()); +#endif +} + + void chsrc_ensure_root () { - char *euid = getenv ("$EUID"); - if (NULL==euid) + if (chsrc_is_root ()) + return; + + char *msg = NULL; + if (xy.on_windows) { - char *buf = xy_run ("id -u", 0); - if (0!=xy_str2int(buf)) goto not_root; - else return; + msg = ENGLISH ? "Please run the terminal as Administrator to ensure the necessary permissions" + : "请以管理员身份运行终端来保证必要的权限"; } else { - if (0!=xy_str2int(euid)) goto not_root; - else return; + msg = ENGLISH ? "Use sudo before the command or switch to root to ensure the necessary permissions" + : "请在命令前使用 sudo 或切换为root用户来保证必要的权限"; } - - char *msg = NULL; -not_root: - msg = ENGLISH ? "Use sudo before the command or switch to root to ensure the necessary permissions" - : "请在命令前使用 sudo 或切换为root用户来保证必要的权限"; chsrc_error (msg); exit (Exit_UserCause); } @@ -388,6 +417,37 @@ chsrc_run_directly (const char *cmd) } +#ifdef XY_Build_On_Windows +/** + * @brief 生成十六进制随机串(CSPRNG) + * + * @param[out] out 输出缓冲区,需要 2 * nbytes + 1 字节 + * @param[in] nbytes 随机字节数,上限 64 + * + * @return 成功与否 + */ +static bool +_chsrc_random_hex (char *out, size_t nbytes) +{ + unsigned char rnd[64]; + if (nbytes > sizeof (rnd)) + return false; + + if (!RtlGenRandom (rnd, (ULONG) nbytes)) + return false; + + static const char hex_digits[] = "0123456789abcdef"; + for (size_t i = 0; i < nbytes; i++) + { + out[2 * i] = hex_digits[rnd[i] >> 4]; + out[2 * i + 1] = hex_digits[rnd[i] & 0x0F]; + } + out[2 * nbytes] = '\0'; + return true; +} +#endif + + /** * @brief 在本目录创建一个临时文件 * @@ -406,46 +466,102 @@ chsrc_make_tmpfile (char *filename, char *postfix, bool loud, char **tmpfilepath #ifdef XY_Build_On_Windows /** - * Windows 上使用 GetTempPath 和 GetTempFileName 创建临时文件 - * 这是 Windows API 推荐的标准方法 - * - * 由于 GetTempFileName 不支持自定义后缀,我们需要: - * 1. 使用 GetTempFileName 生成唯一的临时文件名 - * 2. 将其重命名为带有正确后缀的文件名(PowerShell 需要 .ps1 后缀) + * Windows 上用宽字符 API 组装最终文件名,并以 CREATE_NEW 原子地独占创建: + * 名字由 CSPRNG 生成,重名换名重试;不经过"先创建再删除"的中间文件, + * 因此不存在名字可预测或被 symlink 预先占位的窗口,也支持非 ASCII 的临时目录 */ - char temp_path[MAX_PATH]; - char temp_filename[MAX_PATH]; - - /* 获取系统临时目录 */ - DWORD ret = GetTempPathA (MAX_PATH, temp_path); - if (ret == 0 || ret > MAX_PATH) + WCHAR temp_dir[MAX_PATH + 16]; + DWORD tlen = GetTempPathW (MAX_PATH + 1, temp_dir); + if (0 == tlen || tlen > MAX_PATH + 1) { char *msg = CHINESE ? "无法获取系统临时目录" : "Unable to get system temp directory"; chsrc_error2 (msg); exit (Exit_ExternalError); } - /* 生成唯一的临时文件名 (会自动创建文件) */ - ret = GetTempFileNameA (temp_path, "chsrc_", 0, temp_filename); - if (ret == 0) + bool has_sep = (L'\\' == temp_dir[tlen - 1] || L'/' == temp_dir[tlen - 1]); + + for (int attempt = 0; attempt < 8 && !f; attempt++) { - char *msg = CHINESE ? "无法生成临时文件名" : "Unable to generate temporary filename"; - chsrc_error2 (msg); - exit (Exit_ExternalError); - } + char hex[13]; + if (!_chsrc_random_hex (hex, 6)) + { + char *msg = CHINESE ? "无法生成随机临时文件名" : "Unable to generate a random temporary filename"; + chsrc_error2 (msg); + exit (Exit_ExternalError); + } + + char *name = xy_strcat (5, "chsrc_", hex, "_", filename, postfix); - tmpfile = xy_strcat (4, temp_filename, "_", filename, postfix); + int wname_len = MultiByteToWideChar (CP_UTF8, 0, name, -1, NULL, 0); + if (wname_len <= 0) + { + free (name); + chsrc_error2 (CHINESE ? "无法转换临时文件名" : "Unable to convert the temporary filename"); + exit (Exit_ExternalError); + } + + WCHAR *wname = xy_malloc0 ((size_t) wname_len * sizeof (WCHAR)); + MultiByteToWideChar (CP_UTF8, 0, name, -1, wname, wname_len); + free (name); + + size_t wpath_cap = (size_t) tlen + 1 + (size_t) wname_len; + WCHAR *wpath = xy_malloc0 (wpath_cap * sizeof (WCHAR)); + + size_t wl = 0; + for (DWORD i = 0; i < tlen; i++) + wpath[wl++] = temp_dir[i]; + if (!has_sep) + wpath[wl++] = L'\\'; + for (int i = 0; wname[i]; i++) + wpath[wl++] = wname[i]; + wpath[wl] = L'\0'; + free (wname); + + HANDLE h = CreateFileW (wpath, GENERIC_READ | GENERIC_WRITE, 0, NULL, + CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL); + if (INVALID_HANDLE_VALUE == h) + { + DWORD err = GetLastError (); + free (wpath); - /* 删除 GetTempFileName 自动创建的文件 */ - DeleteFileA (temp_filename); + if (ERROR_FILE_EXISTS == err || ERROR_ALREADY_EXISTS == err) + continue; - /* 创建带有正确后缀的文件 */ - f = fopen (tmpfile, "w+"); + char *err_s = xy_int2str ((int) err); + char *msg = CHINESE ? "无法创建临时文件,错误码: " : "Unable to create temporary file, error: "; + msg = xy_2strcat (msg, err_s); + chsrc_error2 (msg); + exit (Exit_ExternalError); + } + + int fd = _open_osfhandle ((intptr_t) h, _O_RDWR | _O_TEXT); + if (-1 == fd) + { + CloseHandle (h); + free (wpath); + chsrc_error2 (CHINESE ? "无法创建临时文件" : "Unable to create temporary file"); + exit (Exit_ExternalError); + } + + f = _fdopen (fd, "w+"); + if (!f) + { + _close (fd); + free (wpath); + chsrc_error2 (CHINESE ? "无法打开临时文件" : "Unable to open temporary file"); + exit (Exit_ExternalError); + } + + int u8_len = WideCharToMultiByte (CP_UTF8, 0, wpath, -1, NULL, 0, NULL, NULL); + tmpfile = xy_malloc0 ((size_t) u8_len); + WideCharToMultiByte (CP_UTF8, 0, wpath, -1, tmpfile, u8_len, NULL, NULL); + free (wpath); + } if (!f) { - char *msg = CHINESE ? "无法创建临时文件: " : "Unable to create temporary file: "; - msg = xy_2strcat (msg, tmpfile); + char *msg = CHINESE ? "无法创建具有唯一名称的临时文件" : "Unable to create a uniquely named temporary file"; chsrc_error2 (msg); exit (Exit_ExternalError); } @@ -507,6 +623,172 @@ chsrc_make_tmpfile (char *filename, char *postfix, bool loud, char **tmpfilepath } +/** + * @brief 在系统临时目录下创建一个本进程独占、名字不可预测的目录 + * + * @details + * Windows 上用 RtlGenRandom 生成名字,POSIX 上用 mkdtemp;无论哪个平台,目录都必须由本进程 + * 创建成功(重名换名重试),绝不复用已存在的目录。可预测的名字(如 PID、操作序号)可能被 + * 提前占位,或以 junction / symlink 重定向,使后续写入落到攻击者指定的位置 + * + * @param[in] prefix 目录名前缀(UTF-8) + * @param[out] out_err 失败原因,可为 NULL + * + * @return 目录路径,调用者需要 free;失败返回 NULL + */ +char * +chsrc_make_tmpdir (const char *prefix, char **out_err) +{ +#ifdef XY_Build_On_Windows + WCHAR temp_dir[MAX_PATH + 16]; + DWORD tlen = GetTempPathW (MAX_PATH + 1, temp_dir); + if (0 == tlen || tlen > MAX_PATH + 1) + { + if (out_err) + { + *out_err = ENGLISH + ? xy_strdup ("Failed to locate the system temp directory (GetTempPathW)") + : xy_strdup ("无法获取系统临时目录路径"); + } + return NULL; + } + + bool has_sep = (L'\\' == temp_dir[tlen - 1] || L'/' == temp_dir[tlen - 1]); + + for (int attempt = 0; attempt < 8; attempt++) + { + char hex[17]; + if (!_chsrc_random_hex (hex, 8)) + { + if (out_err) + { + *out_err = ENGLISH + ? xy_strdup ("Failed to generate a random temporary directory name (RtlGenRandom)") + : xy_strdup ("无法生成临时目录的随机名称 (RtlGenRandom)"); + } + return NULL; + } + + char name[64]; + size_t n = 0; + for (const char *p = prefix; *p && n < 40; p++) + name[n++] = *p; + name[n++] = '-'; + for (size_t i = 0; hex[i]; i++) + name[n++] = hex[i]; + name[n] = '\0'; + + WCHAR wname[64]; + if (0 == MultiByteToWideChar (CP_UTF8, 0, name, -1, wname, + (int) (sizeof (wname) / sizeof (WCHAR)))) + { + if (out_err) + { + *out_err = ENGLISH + ? xy_strdup ("Failed to convert the temporary directory name to UTF-16") + : xy_strdup ("无法将临时目录名转换为 UTF-16"); + } + return NULL; + } + + WCHAR wpath[MAX_PATH + 160]; + DWORD wl = 0; + for (DWORD i = 0; i < tlen; i++) + wpath[wl++] = temp_dir[i]; + if (!has_sep) + wpath[wl++] = L'\\'; + for (size_t i = 0; wname[i]; i++) + wpath[wl++] = wname[i]; + wpath[wl] = L'\0'; + + if (CreateDirectoryW (wpath, NULL)) + { + int u8_len = WideCharToMultiByte (CP_UTF8, 0, wpath, -1, NULL, 0, NULL, NULL); + if (u8_len <= 0) + { + if (out_err) + { + *out_err = ENGLISH + ? xy_strdup ("Failed to convert the temporary directory path to UTF-8") + : xy_strdup ("无法将临时目录路径转换为 UTF-8"); + } + return NULL; + } + char *u8 = xy_malloc0 ((size_t) u8_len); + WideCharToMultiByte (CP_UTF8, 0, wpath, -1, u8, u8_len, NULL, NULL); + return u8; + } + + DWORD err = GetLastError (); + if (ERROR_ALREADY_EXISTS != err) + { + char *err_s = xy_int2str ((int) err); + if (out_err) + { + *out_err = ENGLISH + ? xy_strcat (4, "Failed to create temporary directory `", name, "` (Win32 error ", err_s) + : xy_strcat (4, "无法创建临时目录 `", name, "`,错误码: ", err_s); + } + free (err_s); + return NULL; + } + + /* 名字已被占位(预先放置的目录或 junction),换一个随机名重试 */ + } + + if (out_err) + { + *out_err = ENGLISH + ? xy_strdup ("Failed to create a uniquely named temporary directory") + : xy_strdup ("无法创建具有唯一名称的临时目录"); + } + return NULL; +#else + const char *tmp = getenv ("TMPDIR"); + if (!tmp || '\0' == tmp[0]) + tmp = "/tmp"; + + char *tmpl = xy_strcat (4, tmp, "/", prefix, "-XXXXXX"); + if (!mkdtemp (tmpl)) + { + if (out_err) + { + *out_err = ENGLISH + ? xy_strcat (3, "Failed to create temporary directory `", tmpl, "`") + : xy_strcat (3, "无法创建临时目录 `", tmpl, "`"); + } + free (tmpl); + return NULL; + } + return tmpl; +#endif +} + + +/** + * @brief 删除文件,路径按 UTF-8 处理 + * + * @note Windows 上 CRT 的 remove() 走 ANSI 代码页,非 ASCII 路径会失败, + * 因此改用 DeleteFileW + */ +void +chsrc_remove_file (const char *path) +{ +#ifdef XY_Build_On_Windows + int wlen = MultiByteToWideChar (CP_UTF8, 0, path, -1, NULL, 0); + if (wlen <= 0) + return; + + WCHAR *wpath = xy_malloc0 ((size_t) wlen * sizeof (WCHAR)); + MultiByteToWideChar (CP_UTF8, 0, path, -1, wpath, wlen); + DeleteFileW (wpath); + free (wpath); +#else + remove (path); +#endif +} + + /** * 以 bash file.bash 的形式运行脚本内容 */ @@ -523,7 +805,7 @@ chsrc_run_as_bash_file (const char *script_content) println (faint(script_content)); char *cmd = xy_2strcat ("bash ", tmpfile); chsrc_run (cmd, RunOpt_Dont_Abort_On_Failure); - remove (tmpfile); + chsrc_remove_file (tmpfile); free (tmpfile); /* 释放 tmpfile 路径内存 */ } @@ -544,7 +826,7 @@ chsrc_run_as_sh_file (const char *script_content) println (faint(script_content)); char *cmd = xy_2strcat ("sh ", tmpfile); chsrc_run (cmd, RunOpt_Dont_Abort_On_Failure); - remove (tmpfile); + chsrc_remove_file (tmpfile); free (tmpfile); } @@ -564,7 +846,7 @@ chsrc_run_as_pwsh_file (const char *script_content) println (faint(script_content)); char *cmd = xy_2strcat ("pwsh ", tmpfile); chsrc_run (cmd, RunOpt_Dont_Abort_On_Failure); - remove (tmpfile); + chsrc_remove_file (tmpfile); free (tmpfile); } @@ -585,7 +867,7 @@ chsrc_run_as_powershellv5_file (const char *script_content) // -ExecutionPolicy Bypass char *cmd = xy_2strcat ("powershell -File ", tmpfile); chsrc_run (cmd, RunOpt_Dont_Abort_On_Failure); - remove (tmpfile); + chsrc_remove_file (tmpfile); free (tmpfile); } @@ -638,65 +920,6 @@ chsrc_run_in_inline_pwsh_shell (const char *cmdline) } -static void -chsrc_view_env (const char *var1, ...) -{ - char *cmd = NULL; - const char *var = var1; - - va_list vars; - va_start (vars, var1); - - bool first = true; - while (var) - { - if (xy.on_windows) - { - if (first) - { - cmd = xy_strcat (3, "set ", var, " "); - first = false; - } - else - { - cmd = xy_strcat (4, cmd, "& set ", var, " "); - } - } - else - { - if (first) - { - cmd = xy_strcat (5, "echo ", var, "=$", var, " "); - first = false; - } - else - { - cmd = xy_strcat (6, cmd, "; echo ", var, "=$", var, " "); - } - } - var = va_arg (vars, const char *); - } - - va_end (vars); - - if (var1) - { - /** - * 不用 chsrc_run(),因为在Windows上,set在遇到环境变量未定义时会返回非0,导致 chsrc_run() 报告运行失败 - * 这个错误过于醒目。我们应该像在 sh 一样,默默地没有输出即可,而不是报错 - */ - // chsrc_run (cmd, RunOpt_Dont_Notify_On_Success|RunOpt_No_Last_New_Line|RunOpt_Dont_Abort_On_Failure); - int status = system (cmd); - if (status!=0) { xy_noop(); } - } - else - { - /* 必须给一个参数 */ - xy_unreached(); - } -} - - static void chsrc_view_file (const char *path) { @@ -717,10 +940,11 @@ chsrc_view_file (const char *path) static void chsrc_ensure_dir (const char *dir) { - dir = xy_normalize_path (dir); + char *norm_dir = xy_normalize_path (dir); - if (xy_dir_exist (dir)) + if (xy_dir_exist (norm_dir)) { + free (norm_dir); return; } @@ -734,26 +958,32 @@ chsrc_ensure_dir (const char *dir) { mkdir_cmd = "mkdir -p "; } - char *cmd = xy_2strcat (mkdir_cmd, dir); - cmd = xy_quiet_cmd (cmd); + char *raw_cmd = xy_strcat (4, mkdir_cmd, "\"", norm_dir, "\""); + char *cmd = xy_quiet_cmd (raw_cmd); + free (raw_cmd); chsrc_run_as_a_service (cmd); + free (cmd); char *msg = ENGLISH ? "Directory doesn't exist, created automatically " : "目录不存在,已自动创建 "; - chsrc_alert2 (xy_2strcat (msg, dir)); + chsrc_alert2 (xy_2strcat (msg, norm_dir)); + free (norm_dir); } static void chsrc_append_to_file (const char *str, const char *filename) { + xy_cant_be_null (str); + xy_cant_be_null (filename); + if (in_dry_run_mode()) { goto log_anyway; } char *file = xy_normalize_path (filename); - char *dir = xy_parent_dir (file); + char *dir = xy_parent_dir (file); chsrc_ensure_dir (dir); FILE *f = fopen (file, "a"); @@ -766,34 +996,26 @@ chsrc_append_to_file (const char *str, const char *filename) } size_t len = strlen (str); - - size_t ret = fwrite (str, len, 1, f); - if (ret != 1) + if (len > 0) { - char *msg = ENGLISH ? xy_2strcat ("Write failed to ", file) - : xy_2strcat ("写入文件失败: ", file); - chsrc_error2 (msg); - exit (Exit_UserCause); + size_t ret = fwrite (str, 1, len, f); + if (ret != len) + { + fclose (f); + char *msg = ENGLISH ? xy_2strcat ("Write failed to ", file) + : xy_2strcat ("写入文件失败: ", file); + chsrc_error2 (msg); + exit (Exit_UserCause); + } } fclose (f); + free (dir); + free (file); log_anyway: /* 输出recipe指定的文件名 */ log_for_file_write (filename, false); - - /* - char *cmd = NULL; - if (xy.on_windows) - { - cmd = xy_strcat (4, "echo ", str, " >> ", file); - } - else - { - cmd = xy_strcat (4, "echo '", str, "' >> ", file); - } - chsrc_run_a_service (cmd); - */ } /** @@ -802,86 +1024,133 @@ chsrc_append_to_file (const char *str, const char *filename) static void chsrc_prepend_to_file (const char *str, const char *filename) { + xy_cant_be_null (str); + xy_cant_be_null (filename); + if (in_dry_run_mode()) { goto log_anyway; } char *file = xy_normalize_path (filename); + char *dir = xy_parent_dir (file); + chsrc_ensure_dir (dir); char *file_content = xy_file_read (file); - char *content = xy_2strcat (str, file_content); + char *content = xy_2strcat (str, file_content); FILE *f = fopen (file, "w"); - - if (f) + if (NULL==f) { - fwrite (content, 1, strlen (content), f); - fclose (f); + char *msg = ENGLISH ? xy_2strcat ("Unable to open file to write: ", file) + : xy_2strcat ("无法打开文件以写入: ", file); + chsrc_error2 (msg); + exit (Exit_UserCause); } - else + + size_t len = strlen (content); + if (len > 0) { - chsrc_error2 ("文件打开失败"); - exit (Exit_UserCause); + size_t ret = fwrite (content, 1, len, f); + if (ret != len) + { + fclose (f); + char *msg = ENGLISH ? xy_2strcat ("Write failed to ", file) + : xy_2strcat ("写入文件失败: ", file); + chsrc_error2 (msg); + exit (Exit_UserCause); + } } + fclose (f); + free (content); + free (file_content); + free (dir); + free (file); + log_anyway: /* 输出recipe指定的文件名 */ log_for_file_write (filename, false); } -static void -chsrc_overwrite_file (const char *str, const char *filename) +/** + * @brief 尝试覆写文件,失败时返回 false 而不直接 exit,供底层模块错误码处理与单测使用 + */ +static bool +chsrc_try_overwrite_file (const char *str, const char *filename) { + xy_cant_be_null (str); + xy_cant_be_null (filename); + if (in_dry_run_mode()) { - goto log_anyway; + log_for_file_write (filename, true); + return true; } char *file = xy_normalize_path (filename); - char *dir = xy_parent_dir (file); + char *dir = xy_parent_dir (file); chsrc_ensure_dir (dir); FILE *f = fopen (file, "w"); if (NULL==f) { - char *msg = ENGLISH ? xy_2strcat ("Unable to open file to overwrite: ", file) - : xy_2strcat ("无法打开文件以覆盖: ", file); - chsrc_error2 (msg); - exit (Exit_UserCause); + free (dir); + free (file); + return false; } size_t len = strlen (str); - size_t ret = fwrite (str, len, 1, f); - if (ret != 1) + if (len > 0) { - fclose (f); - char *msg = ENGLISH ? xy_2strcat ("Write failed to ", file) - : xy_2strcat ("写入文件失败: ", file); - chsrc_error2 (msg); - exit (Exit_UserCause); + size_t ret = fwrite (str, 1, len, f); + if (ret != len) + { + fclose (f); + free (dir); + free (file); + return false; + } } fclose (f); + free (dir); + free (file); -log_anyway: /* 输出recipe指定的文件名 */ log_for_file_write (filename, true); + return true; +} + +static void +chsrc_overwrite_file (const char *str, const char *filename) +{ + if (!chsrc_try_overwrite_file (str, filename)) + { + char *file = xy_normalize_path (filename); + char *msg = ENGLISH ? xy_2strcat ("Unable to open or write file: ", file) + : xy_2strcat ("无法打开或写入文件: ", file); + chsrc_error2 (msg); + exit (Exit_UserCause); + } } static void chsrc_backup (const char *path) { + xy_cant_be_null (path); + if (in_dry_run_mode()) { goto log_anyway; } - char *cmd = NULL; - bool exist = xy_file_exist (path); + char *norm_path = xy_normalize_path (path); + bool exist = xy_file_exist (norm_path); if (!exist) { + free (norm_path); char *msg = ENGLISH ? "File doesn't exist, skip backup: " : "文件不存在,跳过备份: "; chsrc_alert2 (xy_2strcat (msg, path)); return; @@ -896,17 +1165,21 @@ chsrc_backup (const char *path) for (size_t i=0; i < xy_seq_len(backed_up_paths); i++) { - if (xy_streql (xy_seq_at (backed_up_paths, i), path)) - goto log_anyway; + if (xy_streql (xy_seq_at (backed_up_paths, i), norm_path)) + { + free (norm_path); + goto log_anyway; + } } - xy_seq_push (backed_up_paths, xy_strdup (path)); + xy_seq_push (backed_up_paths, xy_strdup (norm_path)); } + char *cmd = NULL; if (xy.on_bsd || xy.on_macos) { /* BSD 和 macOS 的 cp 不支持 --backup 选项 */ - cmd = xy_strcat (5, "cp -f ", path, " ", path, ".bak"); + cmd = xy_strcat (5, "cp -f \"", norm_path, "\" \"", norm_path, ".bak\""); } else if (xy.on_windows) { @@ -914,7 +1187,7 @@ chsrc_backup (const char *path) * @note /Y 表示覆盖 * @note 默认情况下会输出一个 "已复制 1个文件" */ - cmd = xy_strcat (5, "copy /Y ", path, " ", path, ".bak 1>nul"); + cmd = xy_strcat (5, "copy /Y \"", norm_path, "\" \"", norm_path, ".bak\" 1>nul"); } else { @@ -927,18 +1200,21 @@ chsrc_backup (const char *path) */ char *ver = xy_run ("cp --version 2>&1", 1); /* cp (GNU coreutils) 9.4 */ - if (strstr (ver, "GNU coreutils")) + if (ver && strstr (ver, "GNU coreutils")) { - cmd = xy_strcat (5, "cp ", path, " ", path, ".bak --backup='t'"); + cmd = xy_strcat (5, "cp \"", norm_path, "\" \"", norm_path, ".bak\" --backup='t'"); } else { /* 非 GNU 的 cp 可能不支持 --backup ,如 busybox cp */ - cmd = xy_strcat (5, "cp -f ", path, " ", path, ".bak"); + cmd = xy_strcat (5, "cp -f \"", norm_path, "\" \"", norm_path, ".bak\""); } + if (ver) free (ver); } chsrc_run_as_a_service (cmd); + free (cmd); + free (norm_path); log_anyway: log_for_file_backup_op (path); @@ -954,7 +1230,7 @@ chsrc_get_cpuarch () char *ret; char *msg; -#if XY_Build_On_Windows +#ifdef XY_Build_On_Windows SYSTEM_INFO info; GetNativeSystemInfo (&info); WORD num = info.wProcessorArchitecture; @@ -1009,7 +1285,7 @@ chsrc_get_cpucore () { int cores = 2; -#if XY_Build_On_Windows +#ifdef XY_Build_On_Windows SYSTEM_INFO info; GetSystemInfo (&info); DWORD num = info.dwNumberOfProcessors; diff --git a/src/framework/chef.c b/src/framework/chef.c index b8dfb4d13..4ce127c09 100644 --- a/src/framework/chef.c +++ b/src/framework/chef.c @@ -4,8 +4,9 @@ * File Name : chef.c * File Authors : @ccmywish * Contributors : @BingChunMoLi + * | @Mikachu2333 * Created On : <2025-08-09> - * Last Modified : <2026-09-01> + * Last Modified : <2026-10-28> * * 本文件的函数能够帮助 chefs 为一个 dish 定义 recipe * ------------------------------------------------------------*/ @@ -390,7 +391,7 @@ chef_set_default_scope (Dish_t *dish, Scope_t scope) } /* 防止 chef 们写错 */ - if (cap != ScopeCap_Able_And_Implemented) + if (!scope_cap_is_runnable (cap)) { chsrc_breakdown ("该作用域未被明确支持,无法设置为默认作用域"); } diff --git a/src/framework/env.c b/src/framework/env.c new file mode 100644 index 000000000..cf157431d --- /dev/null +++ b/src/framework/env.c @@ -0,0 +1,3067 @@ +/** ------------------------------------------------------------ + * SPDX-License-Identifier: GPL-3.0-or-later + * ------------------------------------------------------------- + * File Name : env.c + * File Authors : @Mikachu2333 + * Contributors : @ccmywish + * | + * Created On : <2026-10-28> + * Last Modified : <2026-10-28> + * + * 跨平台环境变量持久化管理模块 + * ------------------------------------------------------------*/ + +typedef enum EnvOp_t +{ + Env_OK, /* 实际发生写入或删除 */ + Env_Unchanged, /* 所有目标持久化值已与期望一致,未备份、未写盘 */ + Env_NoPermission, /* SystemScope 权限不足,未做任何修改 */ + Env_TargetUnwritable, /* 目标文件/目录/注册表键不可写或备份创建失败 */ + Env_BadName, /* 变量名不匹配 ^[A-Za-z_][A-Za-z0-9_]*$ */ + Env_BadValue, /* 值含目标方言危险字符(引号/$/`/换行等) */ + Env_AmbiguousTarget, /* 未知 shell、多文件值冲突或现有行含复杂动态语法 */ + Env_ScopeUnsupported /* ProjectScope 等不支持的作用域(防御性) */ +} +EnvOp_t; + +typedef struct EnvKV_t +{ + const char *name; + const char *value; /* NULL 表示删除该变量;"" 表示设为空串 */ +} +EnvKV_t; + +typedef enum PosixShell_t +{ + PosixShell_Unknown = 0, + PosixShell_Bash = 1, + PosixShell_Zsh = 2, + PosixShell_Fish = 3 +} +PosixShell_t; + +typedef enum EnvDialect_t +{ + EnvDialect_PosixShell, + EnvDialect_Fish, + EnvDialect_EtcEnv, + EnvDialect_WinRegistry +} +EnvDialect_t; + +typedef enum EnvLineMatch_t +{ + EnvLine_NoMatch, + EnvLine_SafeMatch, + EnvLine_ComplexMatch +} +EnvLineMatch_t; + +/* 仅供 test/fw.c 单元测试注入使用,生产环境保持默认值 */ +static int Env_Test_Force_Shell = -1; +static const char *Env_Test_Etc_Env_Path = NULL; + + +/* ============================================================ + * 动态字节缓冲与字符串切片辅助 + * ============================================================ */ + +static char * +env_strndup (const char *s, size_t n) +{ + char *out = xy_malloc0 (n + 1); + if (n > 0) + memcpy (out, s, n); + out[n] = '\0'; + return out; +} + +typedef struct EnvBuf_t +{ + char *data; + size_t len; + size_t cap; +} +EnvBuf_t; + +static void +env_buf_init (EnvBuf_t *b) +{ + b->cap = 256; + b->len = 0; + b->data = xy_malloc0 (b->cap); +} + +static void +env_buf_reserve (EnvBuf_t *b, size_t extra) +{ + size_t needed = b->len + extra + 1; + if (needed <= b->cap) + return; + + size_t new_cap = b->cap; + while (new_cap < needed) + { + new_cap *= 2; + } + + char *new_data = realloc (b->data, new_cap); + if (!new_data) + { + xy_throw ("Memory allocation failed in env_buf_reserve"); + } + b->data = new_data; + b->cap = new_cap; +} + +static void +env_buf_append_mem (EnvBuf_t *b, const char *s, size_t n) +{ + if (0 == n) + return; + env_buf_reserve (b, n); + memcpy (b->data + b->len, s, n); + b->len += n; + b->data[b->len] = '\0'; +} + +static void +env_buf_append_str (EnvBuf_t *b, const char *s) +{ + if (!s) + return; + env_buf_append_mem (b, s, strlen (s)); +} + +static void +env_buf_append_char (EnvBuf_t *b, char c) +{ + env_buf_reserve (b, 1); + b->data[b->len++] = c; + b->data[b->len] = '\0'; +} + +static char * +env_buf_take (EnvBuf_t *b) +{ + char *ret = b->data; + b->data = NULL; + b->len = 0; + b->cap = 0; + return ret; +} + +static void +env_buf_free (EnvBuf_t *b) +{ + if (b->data) + { + free (b->data); + b->data = NULL; + } + b->len = 0; + b->cap = 0; +} + + +/* ============================================================ + * 变量名与变量值合法性校验(防注入) + * ============================================================ */ + +static bool +env_is_ident_start (char c) +{ + return ( ('A' <= c && c <= 'Z') + || ('a' <= c && c <= 'z') + || ('_' == c)); +} + +static bool +env_is_ident_char (char c) +{ + return ( env_is_ident_start (c) + || ('0' <= c && c <= '9')); +} + +static bool +env_validate_name (const char *name) +{ + if (!name || '\0' == name[0]) + return false; + + if (!env_is_ident_start (name[0])) + return false; + + for (size_t i = 1; name[i] != '\0'; i++) + { + if (!env_is_ident_char (name[i])) + return false; + } + return true; +} + +static bool +env_validate_value (const char *value, EnvDialect_t dialect, char *out_bad_ch) +{ + if (NULL == value) + return true; /* NULL 表示删除变量 */ + + for (size_t i = 0; value[i] != '\0'; i++) + { + unsigned char uc = (unsigned char) value[i]; + char c = value[i]; + + /* 所有平台均禁止换行与回车及控制字符(除制表符视方言而定) */ + if ('\r' == c || '\n' == c || (uc < 0x20 && '\t' != c) || 0x7F == uc) + { + if (out_bad_ch) *out_bad_ch = c; + return false; + } + + if (EnvDialect_WinRegistry == dialect) + { + continue; + } + + /* POSIX Shell / fish / /etc/environment 共同危险字符 */ + if ('$' == c || '`' == c || '"' == c || '\\' == c) + { + if (out_bad_ch) *out_bad_ch = c; + return false; + } + + /* /etc/environment 使用无引号 NAME=value,额外禁止空白符、# 与单引号 */ + if (EnvDialect_EtcEnv == dialect) + { + if (' ' == c || '\t' == c || '#' == c || '\'' == c) + { + if (out_bad_ch) *out_bad_ch = c; + return false; + } + } + } + + return true; +} + + +/* ============================================================ + * POSIX 行级语法精确识别与改写引擎 + * ============================================================ */ + +static const char * +env_skip_ws (const char *p) +{ + while (' ' == *p || '\t' == *p) + { + p++; + } + return p; +} + +/** + * 检查值结尾后是否仅剩空白或合法行尾注释 + * @param require_ws_before_hash POSIX shell 中引号/裸值后必须有空白才能将 # 视为注释 + */ +static bool +env_valid_line_trailer (const char *p, bool require_ws_before_hash) +{ + if ('\0' == *p) + return true; + + if (require_ws_before_hash && (' ' != *p && '\t' != *p)) + return false; + + p = env_skip_ws (p); + return ('\0' == *p || '#' == *p); +} + +static bool +env_is_unquoted_bad_shell_char (char c) +{ + return ( '$' == c || '`' == c || '"' == c || '\'' == c || '\\' == c + || ';' == c || '&' == c || '|' == c || '<' == c || '>' == c + || '(' == c || ')' == c || '*' == c || '?' == c || '[' == c + || ']' == c || '~' == c); +} + +/** + * 解析 NAME= 右侧字面值(供 PosixShell 与 EtcEnv 共用) + */ +static EnvLineMatch_t +env_parse_posix_rhs (const char *rhs, EnvDialect_t dialect, char **out_val) +{ + if ('\0' == *rhs) + { + if (out_val) *out_val = xy_strdup (""); + return EnvLine_SafeMatch; + } + + /* NAME= 后直接跟空白:仅当后面为空或 # 注释时才表示空串,否则如 `NAME= cmd` 属于命令前缀 */ + if (' ' == *rhs || '\t' == *rhs) + { + const char *after_ws = env_skip_ws (rhs); + if ('\0' == *after_ws || '#' == *after_ws) + { + if (out_val) *out_val = xy_strdup (""); + return EnvLine_SafeMatch; + } + return EnvLine_ComplexMatch; + } + + if ('#' == *rhs) + { + /* `NAME=#foo` 在 shell 中并非注释而是字面 `#foo`,但对 /etc/environment 存在歧义 */ + if (EnvDialect_EtcEnv == dialect) + return EnvLine_ComplexMatch; + } + + if ('"' == *rhs) + { + const char *start = rhs + 1; + const char *p = start; + while ('\0' != *p && '"' != *p) + { + if ('$' == *p || '`' == *p || '\\' == *p) + return EnvLine_ComplexMatch; + p++; + } + if ('"' != *p) + return EnvLine_ComplexMatch; /* 未闭合双引号 */ + + if (!env_valid_line_trailer (p + 1, true)) + return EnvLine_ComplexMatch; + + if (out_val) *out_val = env_strndup (start, (size_t) (p - start)); + return EnvLine_SafeMatch; + } + + if ('\'' == *rhs) + { + const char *start = rhs + 1; + const char *p = start; + while ('\0' != *p && '\'' != *p) + { + /* 若单引号内含有双引号写出时不安全的字符,拒绝自动改写以防语义变化 */ + if ('$' == *p || '`' == *p || '"' == *p || '\\' == *p) + return EnvLine_ComplexMatch; + p++; + } + if ('\'' != *p) + return EnvLine_ComplexMatch; /* 未闭合单引号 */ + + if (!env_valid_line_trailer (p + 1, true)) + return EnvLine_ComplexMatch; + + if (out_val) *out_val = env_strndup (start, (size_t) (p - start)); + return EnvLine_SafeMatch; + } + + /* 裸值形式(如 export NVM_NODEJS_ORG_MIRROR=https://... 或 /etc/environment 的 K=V) */ + const char *start = rhs; + const char *p = start; + while ('\0' != *p && ' ' != *p && '\t' != *p) + { + if (env_is_unquoted_bad_shell_char (*p)) + return EnvLine_ComplexMatch; + if (EnvDialect_EtcEnv == dialect && '#' == *p) + return EnvLine_ComplexMatch; + p++; + } + + if (!env_valid_line_trailer (p, true)) + return EnvLine_ComplexMatch; + + if (out_val) *out_val = env_strndup (start, (size_t) (p - start)); + return EnvLine_SafeMatch; +} + +/** + * 检查一行中在 export 之后是否以任意形式出现了目标变量名(用于捕获多变量同行定义,如 `export A=1 TARGET=2`) + */ +static bool +env_export_line_mentions_name (const char *p, const char *name, size_t name_len) +{ + bool in_squote = false; + bool in_dquote = false; + + while ('\0' != *p) + { + if (!in_squote && !in_dquote && '#' == *p) + break; + + if ('\'' == *p && !in_dquote) + { + in_squote = !in_squote; + p++; + continue; + } + if ('"' == *p && !in_squote) + { + in_dquote = !in_dquote; + p++; + continue; + } + if ('\\' == *p && !in_squote && '\0' != p[1]) + { + p += 2; + continue; + } + + if (!in_squote && !in_dquote && env_is_ident_start (*p)) + { + const char *id_start = p; + while (env_is_ident_char (*p)) + p++; + size_t id_len = (size_t) (p - id_start); + if ( id_len == name_len + && 0 == memcmp (id_start, name, name_len) + && ('=' == *p || ' ' == *p || '\t' == *p || '\0' == *p || ';' == *p)) + { + return true; + } + continue; + } + + p++; + } + return false; +} + +static EnvLineMatch_t +env_classify_posix_or_etc_line (const char *line, size_t line_len, + const char *name, EnvDialect_t dialect, + char **out_val) +{ + /* 行内含有嵌入 NUL 或孤立 CR 视为异常,仅在疑似提及时拒绝,否则忽略 */ + if (memchr (line, '\0', line_len) || memchr (line, '\r', line_len)) + { + char *copy = xy_malloc0 (line_len + 1); + for (size_t i = 0; i < line_len; i++) + copy[i] = ('\0' == line[i]) ? ' ' : line[i]; + bool mentions = (NULL != strstr (copy, name)); + free (copy); + return mentions ? EnvLine_ComplexMatch : EnvLine_NoMatch; + } + + char *buf = env_strndup (line, line_len); + const char *p = env_skip_ws (buf); + size_t name_len = strlen (name); + + if ('\0' == *p || '#' == *p) + { + free (buf); + return EnvLine_NoMatch; + } + + bool had_export = false; + if (0 == strncmp (p, "export", 6) && (' ' == p[6] || '\t' == p[6])) + { + had_export = true; + p = env_skip_ws (p + 6); + } + + /* 直接以 NAME= 开头 */ + if (0 == strncmp (p, name, name_len) && '=' == p[name_len]) + { + EnvLineMatch_t res = env_parse_posix_rhs (p + name_len + 1, dialect, out_val); + free (buf); + return res; + } + + /* `export NAME`(无 `=`)或 `export OTHER=1 NAME=2` 等复杂形式 */ + if (had_export && env_export_line_mentions_name (p, name, name_len)) + { + free (buf); + return EnvLine_ComplexMatch; + } + + free (buf); + return EnvLine_NoMatch; +} + +static EnvLineMatch_t +env_classify_fish_line (const char *line, size_t line_len, + const char *name, char **out_val) +{ + if (memchr (line, '\0', line_len) || memchr (line, '\r', line_len)) + { + char *copy = xy_malloc0 (line_len + 1); + for (size_t i = 0; i < line_len; i++) + copy[i] = ('\0' == line[i]) ? ' ' : line[i]; + bool mentions = (NULL != strstr (copy, name)); + free (copy); + return mentions ? EnvLine_ComplexMatch : EnvLine_NoMatch; + } + + char *buf = env_strndup (line, line_len); + const char *p = env_skip_ws (buf); + size_t name_len = strlen (name); + + if ('\0' == *p || '#' == *p) + { + free (buf); + return EnvLine_NoMatch; + } + + /* 必须是独立的 `set` 命令 */ + if (0 != strncmp (p, "set", 3) || (' ' != p[3] && '\t' != p[3])) + { + free (buf); + return EnvLine_NoMatch; + } + + p = env_skip_ws (p + 3); + + bool has_export = false; + bool has_bad_flag = false; + + while ('-' == *p) + { + if ('-' == p[1] && (' ' == p[2] || '\t' == p[2] || '\0' == p[2])) + { + p = env_skip_ws (p + 2); + break; + } + else if ('-' == p[1]) + { + /* 长选项 */ + const char *opt_start = p; + while ('\0' != *p && ' ' != *p && '\t' != *p) + p++; + size_t opt_len = (size_t) (p - opt_start); + if (8 == opt_len && 0 == memcmp (opt_start, "--export", 8)) + has_export = true; + else if (8 == opt_len && 0 == memcmp (opt_start, "--global", 8)) + xy_noop (); + else + has_bad_flag = true; + } + else + { + /* 短选项组合,如 -x, -gx, -xg, -Ux, -e */ + p++; + if ('\0' == *p || ' ' == *p || '\t' == *p) + { + has_bad_flag = true; + } + while ('\0' != *p && ' ' != *p && '\t' != *p) + { + if ('x' == *p) + has_export = true; + else if ('g' == *p) + xy_noop (); + else + has_bad_flag = true; + p++; + } + } + p = env_skip_ws (p); + } + + /* 读取变量名 token */ + const char *var_start = p; + while ('\0' != *p && ' ' != *p && '\t' != *p && ';' != *p && '#' != *p) + p++; + size_t var_len = (size_t) (p - var_start); + + if (0 == var_len) + { + free (buf); + return EnvLine_NoMatch; + } + + bool matched_var = (var_len == name_len && 0 == memcmp (var_start, name, name_len)); + if (!matched_var) + { + /* 如 `set -e OTHER TARGET` 一类多变量操作 */ + if (has_bad_flag && env_export_line_mentions_name (p, name, name_len)) + { + free (buf); + return EnvLine_ComplexMatch; + } + free (buf); + return EnvLine_NoMatch; + } + + /* 命中了变量名,但选项不是纯导出(如 -Ux universal、-e erase、无 -x 等),拒绝自动覆写 */ + if (!has_export || has_bad_flag) + { + free (buf); + return EnvLine_ComplexMatch; + } + + if (';' == *p) + { + free (buf); + return EnvLine_ComplexMatch; + } + + p = env_skip_ws (p); + if ('\0' == *p || '#' == *p) + { + /* `set -x NAME` 无参数在 fish 中为空列表而非空串,视为复杂语法保护 */ + free (buf); + return EnvLine_ComplexMatch; + } + + /* 解析单个值参数 */ + if ('"' == *p) + { + const char *vstart = p + 1; + const char *vp = vstart; + while ('\0' != *vp && '"' != *vp) + { + if ('$' == *vp || '`' == *vp || '\\' == *vp) + { + free (buf); + return EnvLine_ComplexMatch; + } + vp++; + } + if ('"' != *vp || !env_valid_line_trailer (vp + 1, true)) + { + free (buf); + return EnvLine_ComplexMatch; + } + if (out_val) *out_val = env_strndup (vstart, (size_t) (vp - vstart)); + free (buf); + return EnvLine_SafeMatch; + } + + if ('\'' == *p) + { + const char *vstart = p + 1; + const char *vp = vstart; + while ('\0' != *vp && '\'' != *vp) + { + if ('$' == *vp || '`' == *vp || '"' == *vp || '\\' == *vp) + { + free (buf); + return EnvLine_ComplexMatch; + } + vp++; + } + if ('\'' != *vp || !env_valid_line_trailer (vp + 1, true)) + { + free (buf); + return EnvLine_ComplexMatch; + } + if (out_val) *out_val = env_strndup (vstart, (size_t) (vp - vstart)); + free (buf); + return EnvLine_SafeMatch; + } + + const char *vstart = p; + const char *vp = vstart; + while ('\0' != *vp && ' ' != *vp && '\t' != *vp) + { + if (env_is_unquoted_bad_shell_char (*vp) || '#' == *vp) + { + free (buf); + return EnvLine_ComplexMatch; + } + vp++; + } + if (!env_valid_line_trailer (vp, true)) + { + free (buf); + return EnvLine_ComplexMatch; + } + + if (out_val) *out_val = env_strndup (vstart, (size_t) (vp - vstart)); + free (buf); + return EnvLine_SafeMatch; +} + +static EnvLineMatch_t +env_classify_line (const char *line, size_t line_len, + const char *name, EnvDialect_t dialect, + char **out_val) +{ + if (out_val) *out_val = NULL; + + switch (dialect) + { + case EnvDialect_PosixShell: + case EnvDialect_EtcEnv: + return env_classify_posix_or_etc_line (line, line_len, name, dialect, out_val); + case EnvDialect_Fish: + return env_classify_fish_line (line, line_len, name, out_val); + default: + xy_unreached (); + return EnvLine_NoMatch; + } +} + +static char * +env_format_canonical_line (const char *name, const char *value, EnvDialect_t dialect) +{ + switch (dialect) + { + case EnvDialect_PosixShell: + return xy_strcat (5, "export ", name, "=\"", value, "\""); + case EnvDialect_Fish: + return xy_strcat (5, "set -x ", name, " \"", value, "\""); + case EnvDialect_EtcEnv: + return xy_strcat (3, name, "=", value); + default: + xy_unreached (); + return NULL; + } +} + + +/* ============================================================ + * 文件读写与内容转换 + * ============================================================ */ + +static bool +env_read_file_bytes (const char *norm_path, char **out_buf, size_t *out_len) +{ + *out_buf = NULL; + *out_len = 0; + + FILE *fp = fopen (norm_path, "rb"); + if (!fp) + return false; + + EnvBuf_t b; + env_buf_init (&b); + + char chunk[4096]; + size_t n = 0; + while ((n = fread (chunk, 1, sizeof (chunk), fp)) > 0) + { + env_buf_append_mem (&b, chunk, n); + } + + if (ferror (fp)) + { + fclose (fp); + env_buf_free (&b); + return false; + } + + fclose (fp); + *out_len = b.len; + *out_buf = env_buf_take (&b); + return true; +} + +/** + * 在内存中对单个配置文件内容应用 kvs 修改: + * - 第 1 处安全匹配原位替换为标准格式; + * - 同文件后续重复安全匹配行剔除; + * - value == NULL 删除所有安全匹配行; + * - 未出现且 value != NULL 的变量追加在文件末尾(确保前导换行); + * - 遇到复杂动态行立即返回 Env_AmbiguousTarget 并携带精确文件路径与行号。 + */ +static EnvOp_t +env_transform_content (const char *old_content, + size_t old_len, + EnvDialect_t dialect, + const EnvKV_t *kvs, + size_t kv_count, + const char *display_path, + char **out_new_content, + size_t *out_new_len, + bool *out_changed, + char **out_err) +{ + *out_new_content = NULL; + *out_new_len = 0; + *out_changed = false; + + bool *seen = xy_malloc0 (kv_count > 0 ? kv_count * sizeof (bool) : 1); + EnvBuf_t out; + env_buf_init (&out); + + size_t pos = 0; + size_t lineno = 0; + + while (pos < old_len) + { + lineno++; + const char *nl = memchr (old_content + pos, '\n', old_len - pos); + size_t raw_len = nl ? (size_t) (nl - (old_content + pos) + 1) : (old_len - pos); + const char *raw_line = old_content + pos; + + size_t body_len = raw_len; + const char *eol = "\n"; + size_t eol_len = 1; + + if (body_len > 0 && '\n' == raw_line[body_len - 1]) + { + body_len--; + if (body_len > 0 && '\r' == raw_line[body_len - 1]) + { + body_len--; + eol = "\r\n"; + eol_len = 2; + } + } + + int matched_idx = -1; + for (size_t k = 0; k < kv_count; k++) + { + EnvLineMatch_t m = env_classify_line (raw_line, body_len, kvs[k].name, dialect, NULL); + if (EnvLine_ComplexMatch == m) + { + char *line_str = xy_int2str ((int) lineno); + if (out_err) + { + *out_err = ENGLISH + ? xy_strcat (7, "Complex or dynamic syntax for `", kvs[k].name, + "` in ", display_path, ":", line_str, + "; refusing automatic modification to protect your configuration. Please edit it manually.") + : xy_strcat (7, "配置文件 ", display_path, ":", line_str, + " 处的变量 `", kvs[k].name, + "` 含有动态展开或复杂语法,为保护您的配置已拒绝自动修改,请手工编辑"); + } + free (line_str); + free (seen); + env_buf_free (&out); + return Env_AmbiguousTarget; + } + if (EnvLine_SafeMatch == m && matched_idx < 0) + { + matched_idx = (int) k; + } + } + + if (matched_idx < 0) + { + /* 无关行(含注释、历史 # ------ chsrc BLOCK 注释等)原样保留 */ + env_buf_append_mem (&out, raw_line, raw_len); + } + else + { + size_t k = (size_t) matched_idx; + if (NULL == kvs[k].value) + { + /* 删除该行 */ + xy_noop (); + } + else if (!seen[k]) + { + seen[k] = true; + char *canon = env_format_canonical_line (kvs[k].name, kvs[k].value, dialect); + env_buf_append_str (&out, canon); + env_buf_append_mem (&out, eol, eol_len); + free (canon); + } + else + { + /* 同文件后续重复定义直接折叠剔除 */ + xy_noop (); + } + } + + pos += raw_len; + } + + /* 追加文件中尚未出现且非删除的变量 */ + for (size_t k = 0; k < kv_count; k++) + { + if (NULL != kvs[k].value && !seen[k]) + { + if (out.len > 0 && '\n' != out.data[out.len - 1]) + { + env_buf_append_char (&out, '\n'); + } + char *canon = env_format_canonical_line (kvs[k].name, kvs[k].value, dialect); + env_buf_append_str (&out, canon); + env_buf_append_char (&out, '\n'); + free (canon); + } + } + + free (seen); + + bool changed = (out.len != old_len) || (old_len > 0 && 0 != memcmp (old_content, out.data, old_len)); + *out_changed = changed; + *out_new_len = out.len; + *out_new_content = env_buf_take (&out); + return Env_OK; +} + + +/* ============================================================ + * POSIX Shell 检测与配置文件集合 + * ============================================================ */ + +#ifndef XY_Build_On_Windows +#include + +static PosixShell_t +env_shell_from_name (const char *raw_name, bool *out_known_unsupported) +{ + if (out_known_unsupported) *out_known_unsupported = false; + if (!raw_name || '\0' == raw_name[0]) + return PosixShell_Unknown; + + /* 取路径 basename */ + const char *base = strrchr (raw_name, '/'); + base = base ? (base + 1) : raw_name; + + /* 登录 shell 可能带有前导 '-',如 -zsh、-bash */ + while ('-' == *base) + base++; + + if (xy_streql (base, "bash")) + return PosixShell_Bash; + if (xy_streql (base, "zsh")) + return PosixShell_Zsh; + if (xy_streql (base, "fish")) + return PosixShell_Fish; + + if ( xy_streql (base, "csh") || xy_streql (base, "tcsh") + || xy_streql (base, "nu") || xy_streql (base, "nushell") + || xy_streql (base, "pwsh") || xy_streql (base, "powershell") + || xy_streql (base, "elvish") || xy_streql (base, "xonsh") + || xy_streql (base, "ksh") || xy_streql (base, "mksh")) + { + if (out_known_unsupported) *out_known_unsupported = true; + } + + return PosixShell_Unknown; +} + +static char * +env_get_parent_comm (void) +{ + pid_t ppid = getppid (); + if (ppid <= 1) + return NULL; + + /* Linux 优先直读 /proc//comm,零子进程开销且安静 */ + char proc_path[64]; + snprintf (proc_path, sizeof (proc_path), "/proc/%d/comm", (int) ppid); + FILE *fp = fopen (proc_path, "r"); + if (fp) + { + char line[128]; + if (fgets (line, sizeof (line), fp)) + { + fclose (fp); + return xy_str_strip (line); + } + fclose (fp); + } + + /* macOS / BSD 回退至 ps */ + char ps_cmd[96]; + snprintf (ps_cmd, sizeof (ps_cmd), "ps -p %d -o comm= 2>/dev/null", (int) ppid); + char *out = xy_run (ps_cmd, 1); + if (out) + { + char *stripped = xy_str_strip (out); + free (out); + if ('\0' == stripped[0]) + { + free (stripped); + return NULL; + } + return stripped; + } + return NULL; +} + +static PosixShell_t +env_detect_posix_shell (void) +{ + if (Env_Test_Force_Shell >= 0) + return (PosixShell_t) Env_Test_Force_Shell; + + /* 1. 优先检查直接父进程名(支持用户在 bash 登录下临时进入交互式 zsh/fish/nu) */ + char *ppid_comm = env_get_parent_comm (); + if (ppid_comm) + { + bool unsupported_interactive = false; + PosixShell_t s = env_shell_from_name (ppid_comm, &unsupported_interactive); + free (ppid_comm); + if (PosixShell_Unknown != s) + return s; + if (unsupported_interactive) + return PosixShell_Unknown; + } + + /* 2. 父进程为 sh/dash 包装器或脚本解释器时,回退精确匹配 $SHELL */ + const char *shell_env = getenv ("SHELL"); + if (shell_env && '\0' != shell_env[0]) + { + return env_shell_from_name (shell_env, NULL); + } + + return PosixShell_Unknown; +} + +static bool +env_is_migrated_legacy_fish_var (const char *name) +{ + static const char *const legacy_vars[] = { + "RUSTUP_DIST_SERVER", + "RUSTUP_UPDATE_ROOT", + "PUB_HOSTED_URL", + "FLUTTER_STORAGE_BASE_URL", + "NVM_NODEJS_ORG_MIRROR", + "HOMEBREW_API_DOMAIN", + "HOMEBREW_BOTTLE_DOMAIN", + "HOMEBREW_BREW_GIT_REMOTE", + "HOMEBREW_CORE_GIT_REMOTE" + }; + + for (size_t i = 0; i < sizeof (legacy_vars) / sizeof (legacy_vars[0]); i++) + { + if (xy_streql (name, legacy_vars[i])) + return true; + } + return false; +} + +static char * +env_build_unknown_shell_error (const EnvKV_t *kvs, size_t kv_count) +{ + const char *shell_env = getenv ("SHELL"); + const char *shown = (shell_env && '\0' != shell_env[0]) ? shell_env : "(unset)"; + + EnvBuf_t hint; + env_buf_init (&hint); + + if (ENGLISH) + { + env_buf_append_str (&hint, "Unable to confirm current shell (SHELL="); + env_buf_append_str (&hint, shown); + env_buf_append_str (&hint, "; supported shells: bash, zsh, fish). Refusing to guess target rc file."); + if (kvs && kv_count > 0) + { + env_buf_append_str (&hint, " Please configure the following variable(s) manually in your shell profile:"); + for (size_t i = 0; i < kv_count; i++) + { + env_buf_append_str (&hint, "\n "); + if (kvs[i].value) + { + env_buf_append_str (&hint, kvs[i].name); + env_buf_append_str (&hint, "=\""); + env_buf_append_str (&hint, kvs[i].value); + env_buf_append_str (&hint, "\""); + } + else + { + env_buf_append_str (&hint, "unset "); + env_buf_append_str (&hint, kvs[i].name); + } + } + } + } + else + { + env_buf_append_str (&hint, "无法精确确认当前 Shell 类型 (SHELL="); + env_buf_append_str (&hint, shown); + env_buf_append_str (&hint, ",当前仅自动支持 bash、zsh、fish),已拒绝猜测写入配置文件。"); + if (kvs && kv_count > 0) + { + env_buf_append_str (&hint, "请手工在您的 Shell 配置文件中设置以下环境变量:"); + for (size_t i = 0; i < kv_count; i++) + { + env_buf_append_str (&hint, "\n "); + if (kvs[i].value) + { + env_buf_append_str (&hint, kvs[i].name); + env_buf_append_str (&hint, "=\""); + env_buf_append_str (&hint, kvs[i].value); + env_buf_append_str (&hint, "\""); + } + else + { + env_buf_append_str (&hint, "unset "); + env_buf_append_str (&hint, kvs[i].name); + } + } + } + } + + return env_buf_take (&hint); +} + +typedef struct PosixFileSpec_t +{ + const char *display_path; + EnvDialect_t dialect; + bool create_if_missing; +} +PosixFileSpec_t; + +typedef struct PreparedFileOp_t +{ + const char *display_path; + char *norm_path; + bool existed; + bool changed; + char *new_content; +} +PreparedFileOp_t; + +static void +env_free_prepared_ops (PreparedFileOp_t *ops, size_t count) +{ + if (!ops) + return; + for (size_t i = 0; i < count; i++) + { + if (ops[i].norm_path) free (ops[i].norm_path); + if (ops[i].new_content) free (ops[i].new_content); + } + free (ops); +} + +static bool +env_file_mentions_any_kv (const char *display_path, EnvDialect_t dialect, + const EnvKV_t *kvs, size_t kv_count) +{ + char *norm = xy_normalize_path (display_path); + if (!xy_file_exist (norm)) + { + free (norm); + return false; + } + + char *content = NULL; + size_t len = 0; + if (!env_read_file_bytes (norm, &content, &len)) + { + free (norm); + return false; + } + free (norm); + + bool found = false; + size_t pos = 0; + while (pos < len && !found) + { + const char *nl = memchr (content + pos, '\n', len - pos); + size_t raw_len = nl ? (size_t) (nl - (content + pos) + 1) : (len - pos); + size_t body_len = raw_len; + if (body_len > 0 && '\n' == content[pos + body_len - 1]) + { + body_len--; + if (body_len > 0 && '\r' == content[pos + body_len - 1]) + body_len--; + } + + for (size_t k = 0; k < kv_count; k++) + { + if (EnvLine_NoMatch != env_classify_line (content + pos, body_len, kvs[k].name, dialect, NULL)) + { + found = true; + break; + } + } + pos += raw_len; + } + + free (content); + return found; +} + +static EnvOp_t +env_apply_posix (const EnvKV_t *kvs, size_t kv_count, PosixShell_t *out_shell, char **out_err) +{ + if (out_shell) *out_shell = PosixShell_Unknown; + + /* SystemScope -> /etc/environment */ + if (chsrc_in_system_scope_mode ()) + { + const char *etc_path = Env_Test_Etc_Env_Path ? Env_Test_Etc_Env_Path : "/etc/environment"; + + if (!Env_Test_Etc_Env_Path && !chsrc_is_root () && !in_dry_run_mode ()) + { + if (out_err) + { + *out_err = ENGLISH + ? xy_strdup ("Use sudo before the command or switch to root to ensure the necessary permissions") + : xy_strdup ("请在命令前使用 sudo 或切换为root用户来保证必要的权限"); + } + return Env_NoPermission; + } + + for (size_t k = 0; k < kv_count; k++) + { + if (!env_validate_value (kvs[k].value, EnvDialect_EtcEnv, NULL)) + { + if (out_err) + { + *out_err = ENGLISH + ? xy_strcat (3, "Value for `", kvs[k].name, + "` contains unsafe characters (whitespace, quotes, `#`, `$`, `` ` ``, `\\`, or newline) for /etc/environment") + : xy_strcat (3, "变量 `", kvs[k].name, + "` 的值含有 /etc/environment 不安全字符(空白符、引号、`#`、`$`、反引号、反斜杠或换行)"); + } + return Env_BadValue; + } + } + + char *norm_path = xy_normalize_path (etc_path); + bool existed = xy_file_exist (norm_path); + char *old_content = NULL; + size_t old_len = 0; + + if (existed) + { + if (!env_read_file_bytes (norm_path, &old_content, &old_len)) + { + if (out_err) + { + *out_err = ENGLISH ? xy_2strcat ("Unable to read file: ", norm_path) + : xy_2strcat ("无法读取文件: ", norm_path); + } + free (norm_path); + return Env_TargetUnwritable; + } + } + else + { + old_content = xy_strdup (""); + old_len = 0; + } + + char *new_content = NULL; + size_t new_len = 0; + bool changed = false; + + EnvOp_t tr = env_transform_content (old_content, old_len, EnvDialect_EtcEnv, + kvs, kv_count, etc_path, + &new_content, &new_len, &changed, out_err); + free (old_content); + if (Env_OK != tr) + { + free (norm_path); + return tr; + } + + if (!changed || (!existed && 0 == new_len)) + { + free (new_content); + free (norm_path); + return Env_Unchanged; + } + + if (existed) + chsrc_backup (etc_path); + + if (in_dry_run_mode ()) + { + log_for_file_write (etc_path, true); + } + else if (!chsrc_try_overwrite_file (new_content, etc_path)) + { + if (out_err) + { + *out_err = ENGLISH ? xy_2strcat ("Unable to open or write file: ", norm_path) + : xy_2strcat ("无法打开或写入文件: ", norm_path); + } + free (new_content); + free (norm_path); + return Env_TargetUnwritable; + } + + free (new_content); + free (norm_path); + return Env_OK; + } + + /* UserScope */ + PosixShell_t shell = env_detect_posix_shell (); + if (PosixShell_Unknown == shell) + { + if (out_err) + *out_err = env_build_unknown_shell_error (kvs, kv_count); + return Env_AmbiguousTarget; + } + if (out_shell) *out_shell = shell; + + EnvDialect_t user_dialect = (PosixShell_Fish == shell) ? EnvDialect_Fish : EnvDialect_PosixShell; + for (size_t k = 0; k < kv_count; k++) + { + if (!env_validate_value (kvs[k].value, user_dialect, NULL)) + { + if (out_err) + { + *out_err = ENGLISH + ? xy_strcat (3, "Value for `", kvs[k].name, + "` contains unsafe shell characters (`\"`, `\\`, `$`, `` ` ``, or newline)") + : xy_strcat (3, "变量 `", kvs[k].name, + "` 的值含有不安全字符(`\"`、`\\`、`$`、反引号或换行)"); + } + return Env_BadValue; + } + } + + PosixFileSpec_t write_specs[4]; + size_t write_count = 0; + PosixFileSpec_t check_specs[5]; + size_t check_count = 0; + + if (PosixShell_Bash == shell) + { + write_specs[write_count++] = (PosixFileSpec_t) { "~/.bashrc", EnvDialect_PosixShell, false }; + write_specs[write_count++] = (PosixFileSpec_t) { "~/.bash_profile", EnvDialect_PosixShell, false }; + write_specs[write_count++] = (PosixFileSpec_t) { "~/.profile", EnvDialect_PosixShell, true }; + + check_specs[check_count++] = (PosixFileSpec_t) { "~/.zshrc", EnvDialect_PosixShell, false }; + check_specs[check_count++] = (PosixFileSpec_t) { "~/.zprofile", EnvDialect_PosixShell, false }; + check_specs[check_count++] = (PosixFileSpec_t) { "~/.config/fish/conf.d/chsrc.fish", EnvDialect_Fish, false }; + check_specs[check_count++] = (PosixFileSpec_t) { "~/.config/fish/config.fish", EnvDialect_Fish, false }; + } + else if (PosixShell_Zsh == shell) + { + write_specs[write_count++] = (PosixFileSpec_t) { "~/.zshrc", EnvDialect_PosixShell, false }; + write_specs[write_count++] = (PosixFileSpec_t) { "~/.zprofile", EnvDialect_PosixShell, false }; + write_specs[write_count++] = (PosixFileSpec_t) { "~/.profile", EnvDialect_PosixShell, true }; + + check_specs[check_count++] = (PosixFileSpec_t) { "~/.bashrc", EnvDialect_PosixShell, false }; + check_specs[check_count++] = (PosixFileSpec_t) { "~/.bash_profile", EnvDialect_PosixShell, false }; + check_specs[check_count++] = (PosixFileSpec_t) { "~/.config/fish/conf.d/chsrc.fish", EnvDialect_Fish, false }; + check_specs[check_count++] = (PosixFileSpec_t) { "~/.config/fish/config.fish", EnvDialect_Fish, false }; + } + else if (PosixShell_Fish == shell) + { + write_specs[write_count++] = (PosixFileSpec_t) { "~/.config/fish/conf.d/chsrc.fish", EnvDialect_Fish, true }; + + check_specs[check_count++] = (PosixFileSpec_t) { "~/.bashrc", EnvDialect_PosixShell, false }; + check_specs[check_count++] = (PosixFileSpec_t) { "~/.zshrc", EnvDialect_PosixShell, false }; + check_specs[check_count++] = (PosixFileSpec_t) { "~/.profile", EnvDialect_PosixShell, false }; + } + + /* 阶段 1:全部读取并在内存完成转换校验,任一文件遇复杂语法则零落盘中止 */ + PreparedFileOp_t *ops = xy_malloc0 ((write_count + 1) * sizeof (PreparedFileOp_t)); + size_t op_count = 0; + + for (size_t i = 0; i < write_count; i++) + { + char *norm = xy_normalize_path (write_specs[i].display_path); + bool existed = xy_file_exist (norm); + + if (!existed && !write_specs[i].create_if_missing) + { + free (norm); + continue; + } + + char *old_content = NULL; + size_t old_len = 0; + if (existed) + { + if (!env_read_file_bytes (norm, &old_content, &old_len)) + { + if (out_err) + { + *out_err = ENGLISH ? xy_2strcat ("Unable to read file: ", norm) + : xy_2strcat ("无法读取文件: ", norm); + } + free (norm); + env_free_prepared_ops (ops, op_count); + return Env_TargetUnwritable; + } + } + else + { + old_content = xy_strdup (""); + old_len = 0; + } + + char *new_content = NULL; + size_t new_len = 0; + bool changed = false; + + EnvOp_t tr = env_transform_content (old_content, old_len, write_specs[i].dialect, + kvs, kv_count, write_specs[i].display_path, + &new_content, &new_len, &changed, out_err); + free (old_content); + if (Env_OK != tr) + { + free (norm); + env_free_prepared_ops (ops, op_count); + return tr; + } + + /* 若文件原本不存在且转换后仍为空(例如仅做删除操作),不创建空文件 */ + if (!existed && 0 == new_len) + { + changed = false; + } + + ops[op_count].display_path = write_specs[i].display_path; + ops[op_count].norm_path = norm; + ops[op_count].existed = existed; + ops[op_count].changed = changed; + ops[op_count].new_content = new_content; + op_count++; + } + + /* Fish 一次性迁移清理:从 ~/.config/fish/config.fish 精确移除 5 个迁移 dish 的历史同名行 */ + EnvKV_t *non_legacy_fish_kvs = NULL; + size_t non_legacy_count = 0; + + if (PosixShell_Fish == shell) + { + EnvKV_t *cleanup_kvs = xy_malloc0 ((kv_count > 0 ? kv_count : 1) * sizeof (EnvKV_t)); + size_t cleanup_count = 0; + non_legacy_fish_kvs = xy_malloc0 ((kv_count > 0 ? kv_count : 1) * sizeof (EnvKV_t)); + + for (size_t k = 0; k < kv_count; k++) + { + if (env_is_migrated_legacy_fish_var (kvs[k].name)) + { + cleanup_kvs[cleanup_count++] = (EnvKV_t) { kvs[k].name, NULL }; + } + else + { + non_legacy_fish_kvs[non_legacy_count++] = kvs[k]; + } + } + + if (cleanup_count > 0) + { + const char *fishrc_disp = "~/.config/fish/config.fish"; + char *fishrc_norm = xy_normalize_path (fishrc_disp); + if (xy_file_exist (fishrc_norm)) + { + char *old_content = NULL; + size_t old_len = 0; + if (!env_read_file_bytes (fishrc_norm, &old_content, &old_len)) + { + if (out_err) + { + *out_err = ENGLISH ? xy_2strcat ("Unable to read file: ", fishrc_norm) + : xy_2strcat ("无法读取文件: ", fishrc_norm); + } + free (fishrc_norm); + free (cleanup_kvs); + free (non_legacy_fish_kvs); + env_free_prepared_ops (ops, op_count); + return Env_TargetUnwritable; + } + + char *new_content = NULL; + size_t new_len = 0; + bool changed = false; + + EnvOp_t tr = env_transform_content (old_content, old_len, EnvDialect_Fish, + cleanup_kvs, cleanup_count, fishrc_disp, + &new_content, &new_len, &changed, out_err); + free (old_content); + if (Env_OK != tr) + { + free (fishrc_norm); + free (cleanup_kvs); + free (non_legacy_fish_kvs); + env_free_prepared_ops (ops, op_count); + return tr; + } + + ops[op_count].display_path = fishrc_disp; + ops[op_count].norm_path = fishrc_norm; + ops[op_count].existed = true; + ops[op_count].changed = changed; + ops[op_count].new_content = new_content; + op_count++; + } + else + { + free (fishrc_norm); + } + } + free (cleanup_kvs); + } + + bool any_changed = false; + for (size_t i = 0; i < op_count; i++) + { + if (ops[i].changed) + { + any_changed = true; + break; + } + } + + if (!any_changed) + { + if (non_legacy_fish_kvs) free (non_legacy_fish_kvs); + env_free_prepared_ops (ops, op_count); + return Env_Unchanged; + } + + /* 阶段 2:仅对内容实际发生变更的文件执行备份与写入 */ + for (size_t i = 0; i < op_count; i++) + { + if (!ops[i].changed) + continue; + + if (ops[i].existed) + chsrc_backup (ops[i].display_path); + + if (in_dry_run_mode ()) + { + log_for_file_write (ops[i].display_path, true); + } + else if (!chsrc_try_overwrite_file (ops[i].new_content, ops[i].display_path)) + { + if (out_err) + { + *out_err = ENGLISH ? xy_2strcat ("Unable to open or write file: ", ops[i].norm_path) + : xy_2strcat ("无法打开或写入文件: ", ops[i].norm_path); + } + if (non_legacy_fish_kvs) free (non_legacy_fish_kvs); + env_free_prepared_ops (ops, op_count); + return Env_TargetUnwritable; + } + } + + /* 检查集提醒:若其他 Shell 配置文件中也定义了相同变量,仅提示不越界修改 */ + for (size_t i = 0; i < check_count; i++) + { + if (env_file_mentions_any_kv (check_specs[i].display_path, check_specs[i].dialect, kvs, kv_count)) + { + char *msg = ENGLISH + ? xy_strcat (3, "Detected matching variable definition in ", check_specs[i].display_path, + "; switch to that shell if you also want chsrc to update it") + : xy_strcat (3, "检测到 ", check_specs[i].display_path, + " 中也存在同名环境变量定义,若您同时使用该 Shell 可切换后同步更新"); + chsrc_note2 (msg); + free (msg); + } + } + + if (PosixShell_Fish == shell && non_legacy_count > 0) + { + if (env_file_mentions_any_kv ("~/.config/fish/config.fish", EnvDialect_Fish, + non_legacy_fish_kvs, non_legacy_count)) + { + char *msg = ENGLISH + ? xy_strdup ("Detected matching variable definition in ~/.config/fish/config.fish; verify whether it overrides conf.d/chsrc.fish") + : xy_strdup ("检测到 ~/.config/fish/config.fish 中也存在同名环境变量定义,请留意其加载顺序是否覆盖 conf.d/chsrc.fish"); + chsrc_note2 (msg); + free (msg); + } + } + + if (non_legacy_fish_kvs) free (non_legacy_fish_kvs); + env_free_prepared_ops (ops, op_count); + return Env_OK; +} + +/** + * 在单个文件中查询持久化变量值(文件内多次安全赋值以最后一次为准,与 Shell 顺序执行一致) + */ +static EnvOp_t +env_query_single_posix_file (const char *display_path, EnvDialect_t dialect, + const char *name, bool *out_found, char **out_val, + char **out_err) +{ + *out_found = false; + *out_val = NULL; + + char *norm = xy_normalize_path (display_path); + if (!xy_file_exist (norm)) + { + free (norm); + return Env_OK; + } + + char *content = NULL; + size_t len = 0; + if (!env_read_file_bytes (norm, &content, &len)) + { + if (out_err) + { + *out_err = ENGLISH ? xy_2strcat ("Unable to read file: ", norm) + : xy_2strcat ("无法读取文件: ", norm); + } + free (norm); + return Env_TargetUnwritable; + } + free (norm); + + size_t pos = 0; + size_t lineno = 0; + + while (pos < len) + { + lineno++; + const char *nl = memchr (content + pos, '\n', len - pos); + size_t raw_len = nl ? (size_t) (nl - (content + pos) + 1) : (len - pos); + size_t body_len = raw_len; + if (body_len > 0 && '\n' == content[pos + body_len - 1]) + { + body_len--; + if (body_len > 0 && '\r' == content[pos + body_len - 1]) + body_len--; + } + + char *parsed = NULL; + EnvLineMatch_t m = env_classify_line (content + pos, body_len, name, dialect, &parsed); + if (EnvLine_ComplexMatch == m) + { + char *line_str = xy_int2str ((int) lineno); + if (out_err) + { + *out_err = ENGLISH + ? xy_strcat (6, "Complex or dynamic syntax for `", name, + "` in ", display_path, ":", line_str) + : xy_strcat (6, "配置文件 ", display_path, ":", line_str, + " 处的变量 `", name, "` 含有动态展开或复杂语法,无法精确确认持久化值"); + } + free (line_str); + free (content); + if (*out_val) + { + free (*out_val); + *out_val = NULL; + *out_found = false; + } + return Env_AmbiguousTarget; + } + else if (EnvLine_SafeMatch == m) + { + if (*out_val) free (*out_val); + *out_val = parsed; + *out_found = true; + } + + pos += raw_len; + } + + free (content); + return Env_OK; +} + +static EnvOp_t +env_query_persisted_posix (const char *name, bool *out_found, char **out_val, char **out_err) +{ + *out_found = false; + *out_val = NULL; + + if (chsrc_in_system_scope_mode ()) + { + const char *etc_path = Env_Test_Etc_Env_Path ? Env_Test_Etc_Env_Path : "/etc/environment"; + return env_query_single_posix_file (etc_path, EnvDialect_EtcEnv, name, out_found, out_val, out_err); + } + + PosixShell_t shell = env_detect_posix_shell (); + if (PosixShell_Unknown == shell) + { + if (out_err) + *out_err = env_build_unknown_shell_error (NULL, 0); + return Env_AmbiguousTarget; + } + + PosixFileSpec_t specs[4]; + size_t spec_count = 0; + + if (PosixShell_Bash == shell) + { + specs[spec_count++] = (PosixFileSpec_t) { "~/.bashrc", EnvDialect_PosixShell, false }; + specs[spec_count++] = (PosixFileSpec_t) { "~/.bash_profile", EnvDialect_PosixShell, false }; + specs[spec_count++] = (PosixFileSpec_t) { "~/.profile", EnvDialect_PosixShell, false }; + } + else if (PosixShell_Zsh == shell) + { + specs[spec_count++] = (PosixFileSpec_t) { "~/.zshrc", EnvDialect_PosixShell, false }; + specs[spec_count++] = (PosixFileSpec_t) { "~/.zprofile", EnvDialect_PosixShell, false }; + specs[spec_count++] = (PosixFileSpec_t) { "~/.profile", EnvDialect_PosixShell, false }; + } + else if (PosixShell_Fish == shell) + { + specs[spec_count++] = (PosixFileSpec_t) { "~/.config/fish/conf.d/chsrc.fish", EnvDialect_Fish, false }; + specs[spec_count++] = (PosixFileSpec_t) { "~/.config/fish/config.fish", EnvDialect_Fish, false }; + } + + const char *first_file = NULL; + char *agreed_val = NULL; + bool any_found = false; + + for (size_t i = 0; i < spec_count; i++) + { + bool file_found = false; + char *file_val = NULL; + + EnvOp_t rc = env_query_single_posix_file (specs[i].display_path, specs[i].dialect, + name, &file_found, &file_val, out_err); + if (Env_OK != rc) + { + if (agreed_val) free (agreed_val); + return rc; + } + + if (!file_found) + continue; + + if (!any_found) + { + any_found = true; + agreed_val = file_val; + first_file = specs[i].display_path; + } + else + { + if (!xy_streql (agreed_val, file_val)) + { + if (out_err) + { + *out_err = ENGLISH + ? xy_strcat (9, "Conflicting values for `", name, "` between ", + first_file, " (`", agreed_val, "`) and ", + specs[i].display_path, " — please resolve the conflict manually") + : xy_strcat (9, "变量 `", name, "` 在 ", + first_file, " (`", agreed_val, "`) 与 ", + specs[i].display_path, " 中存在不同值冲突,请先手工统一后再查看"); + } + free (agreed_val); + free (file_val); + return Env_AmbiguousTarget; + } + free (file_val); + } + } + + *out_found = any_found; + *out_val = agreed_val; + return Env_OK; +} + +#endif /* !XY_Build_On_Windows */ + + +/* ============================================================ + * UTF-8 <-> UTF-16LE 编解码与 .reg 备份序列化(纯 C 跨平台可单测) + * ============================================================ */ + +static void +env_u16_push (uint16_t **buf, size_t *len, size_t *cap, uint16_t unit) +{ + if (*len + 2 > *cap) + { + *cap *= 2; + uint16_t *grown = realloc (*buf, (*cap) * sizeof (uint16_t)); + if (!grown) + xy_throw ("Memory allocation failed in env_u16_push"); + *buf = grown; + } + (*buf)[(*len)++] = unit; +} + +static void +env_utf8_to_utf16le (const char *utf8, uint16_t **out_u16, size_t *out_u16_len) +{ + size_t cap = 64; + size_t len = 0; + uint16_t *buf = xy_malloc0 (cap * sizeof (uint16_t)); + + const unsigned char *s = (const unsigned char *) (utf8 ? utf8 : ""); + size_t i = 0; + + while ('\0' != s[i]) + { + uint32_t cp = 0; + unsigned char b0 = s[i]; + + if (b0 < 0x80) + { + cp = b0; + i += 1; + } + else if ((b0 & 0xE0) == 0xC0 && (s[i + 1] & 0xC0) == 0x80) + { + cp = ((uint32_t) (b0 & 0x1F) << 6) | (uint32_t) (s[i + 1] & 0x3F); + if (cp < 0x80) + cp = 0xFFFD; + i += 2; + } + else if ( (b0 & 0xF0) == 0xE0 + && (s[i + 1] & 0xC0) == 0x80 + && (s[i + 2] & 0xC0) == 0x80) + { + cp = ((uint32_t) (b0 & 0x0F) << 12) + | ((uint32_t) (s[i + 1] & 0x3F) << 6) + | (uint32_t) (s[i + 2] & 0x3F); + if (cp < 0x800 || (cp >= 0xD800 && cp <= 0xDFFF)) + cp = 0xFFFD; + i += 3; + } + else if ( (b0 & 0xF8) == 0xF0 + && (s[i + 1] & 0xC0) == 0x80 + && (s[i + 2] & 0xC0) == 0x80 + && (s[i + 3] & 0xC0) == 0x80) + { + cp = ((uint32_t) (b0 & 0x07) << 18) + | ((uint32_t) (s[i + 1] & 0x3F) << 12) + | ((uint32_t) (s[i + 2] & 0x3F) << 6) + | (uint32_t) (s[i + 3] & 0x3F); + if (cp < 0x10000 || cp > 0x10FFFF) + cp = 0xFFFD; + i += 4; + } + else + { + cp = 0xFFFD; + i += 1; + } + + if (cp <= 0xFFFF) + { + env_u16_push (&buf, &len, &cap, (uint16_t) cp); + } + else + { + uint32_t v = cp - 0x10000; + env_u16_push (&buf, &len, &cap, (uint16_t) (0xD800 + (v >> 10))); + env_u16_push (&buf, &len, &cap, (uint16_t) (0xDC00 + (v & 0x3FF))); + } + } + + buf[len] = 0; + *out_u16 = buf; + *out_u16_len = len; +} + +static char * +env_utf16le_to_utf8 (const uint16_t *u16, size_t u16_len) +{ + while (u16_len > 0 && 0 == u16[u16_len - 1]) + { + u16_len--; + } + + EnvBuf_t out; + env_buf_init (&out); + + size_t i = 0; + while (i < u16_len) + { + uint32_t cp = u16[i++]; + if (cp >= 0xD800 && cp <= 0xDBFF) + { + if (i < u16_len && u16[i] >= 0xDC00 && u16[i] <= 0xDFFF) + { + uint32_t low = u16[i++]; + cp = 0x10000 + (((cp - 0xD800) << 10) | (low - 0xDC00)); + } + else + { + cp = 0xFFFD; + } + } + else if (cp >= 0xDC00 && cp <= 0xDFFF) + { + cp = 0xFFFD; + } + + if (cp < 0x80) + { + env_buf_append_char (&out, (char) cp); + } + else if (cp < 0x800) + { + env_buf_append_char (&out, (char) (0xC0 | (cp >> 6))); + env_buf_append_char (&out, (char) (0x80 | (cp & 0x3F))); + } + else if (cp < 0x10000) + { + env_buf_append_char (&out, (char) (0xE0 | (cp >> 12))); + env_buf_append_char (&out, (char) (0x80 | ((cp >> 6) & 0x3F))); + env_buf_append_char (&out, (char) (0x80 | (cp & 0x3F))); + } + else + { + env_buf_append_char (&out, (char) (0xF0 | (cp >> 18))); + env_buf_append_char (&out, (char) (0x80 | ((cp >> 12) & 0x3F))); + env_buf_append_char (&out, (char) (0x80 | ((cp >> 6) & 0x3F))); + env_buf_append_char (&out, (char) (0x80 | (cp & 0x3F))); + } + } + + return env_buf_take (&out); +} + +typedef struct EnvRegSnapshot_t +{ + const char *name; + bool existed; + uint32_t reg_type; /* 1 = REG_SZ, 2 = REG_EXPAND_SZ 等 */ + char *utf8_sz_val; /* REG_SZ / REG_EXPAND_SZ 解码文本 */ + unsigned char *raw_bytes; /* 原始字节快照 */ + size_t raw_len; +} +EnvRegSnapshot_t; + +static void +env_append_reg_escaped_str (EnvBuf_t *b, const char *s) +{ + if (!s) + return; + for (size_t i = 0; '\0' != s[i]; i++) + { + if ('\\' == s[i] || '"' == s[i]) + env_buf_append_char (b, '\\'); + env_buf_append_char (b, s[i]); + } +} + +/** + * 生成带 UTF-16LE BOM (0xFF 0xFE) 的标准 Windows Registry Editor Version 5.00 备份字节流 + */ +static unsigned char * +env_build_reg_backup_bytes (const char *key_path, + const EnvRegSnapshot_t *snaps, + size_t snap_count, + size_t *out_byte_len) +{ + EnvBuf_t txt; + env_buf_init (&txt); + + env_buf_append_str (&txt, "Windows Registry Editor Version 5.00\r\n\r\n["); + env_buf_append_str (&txt, key_path); + env_buf_append_str (&txt, "]\r\n"); + + for (size_t i = 0; i < snap_count; i++) + { + env_buf_append_char (&txt, '"'); + env_append_reg_escaped_str (&txt, snaps[i].name); + env_buf_append_str (&txt, "\"="); + + if (!snaps[i].existed) + { + env_buf_append_str (&txt, "-\r\n"); + } + else if (1 == snaps[i].reg_type && NULL != snaps[i].utf8_sz_val) + { + env_buf_append_char (&txt, '"'); + env_append_reg_escaped_str (&txt, snaps[i].utf8_sz_val); + env_buf_append_str (&txt, "\"\r\n"); + } + else + { + char hdr[32]; + snprintf (hdr, sizeof (hdr), "hex(%x):", (unsigned int) snaps[i].reg_type); + env_buf_append_str (&txt, hdr); + for (size_t b = 0; b < snaps[i].raw_len; b++) + { + char hx[8]; + snprintf (hx, sizeof (hx), "%s%02x", (b > 0 ? "," : ""), snaps[i].raw_bytes[b]); + env_buf_append_str (&txt, hx); + } + env_buf_append_str (&txt, "\r\n"); + } + } + + env_buf_append_str (&txt, "\r\n"); + + uint16_t *u16 = NULL; + size_t u16_len = 0; + env_utf8_to_utf16le (txt.data, &u16, &u16_len); + env_buf_free (&txt); + + size_t total_bytes = 2 + u16_len * 2; + unsigned char *bytes = xy_malloc0 (total_bytes + 2); + + /* UTF-16LE BOM */ + bytes[0] = 0xFF; + bytes[1] = 0xFE; + + for (size_t i = 0; i < u16_len; i++) + { + bytes[2 + i * 2] = (unsigned char) (u16[i] & 0xFF); + bytes[2 + i * 2 + 1] = (unsigned char) ((u16[i] >> 8) & 0xFF); + } + + free (u16); + *out_byte_len = total_bytes; + return bytes; +} + + +/* ============================================================ + * Windows 注册表后端(Reg*W + .reg 备份与失败自动回滚 + WM_SETTINGCHANGE) + * ============================================================ */ + +#ifdef XY_Build_On_Windows + +#define ENV_WIN_HKCU_SUBKEY L"Environment" +#define ENV_WIN_HKLM_SUBKEY L"SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment" +#define ENV_WIN_HKCU_REGPATH "HKEY_CURRENT_USER\\Environment" +#define ENV_WIN_HKLM_REGPATH "HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment" + +static unsigned int Env_Win_Backup_Seq = 0; +static bool Env_Win_Combo_HKCU_Saved = false; +static bool Env_Win_Combo_HKLM_Saved = false; +static int Env_Win_Last_Combo_Depth = 0; +static char *Env_Win_Backup_Dir = NULL; /* 进程级备份目录,随进程结束回收 */ + +static void +env_free_reg_snapshots (EnvRegSnapshot_t *snaps, size_t count) +{ + if (!snaps) + return; + for (size_t i = 0; i < count; i++) + { + if (snaps[i].utf8_sz_val) free (snaps[i].utf8_sz_val); + if (snaps[i].raw_bytes) free (snaps[i].raw_bytes); + } + free (snaps); +} + +static EnvOp_t +env_read_win_snapshot_one (HKEY hkey, const char *name, EnvRegSnapshot_t *snap, char **out_err) +{ + snap->name = name; + snap->existed = false; + snap->reg_type = 0; + snap->utf8_sz_val = NULL; + snap->raw_bytes = NULL; + snap->raw_len = 0; + + if (!hkey) + return Env_OK; + + uint16_t *wname = NULL; + size_t wname_len = 0; + env_utf8_to_utf16le (name, &wname, &wname_len); + (void) wname_len; + + DWORD reg_type = 0; + DWORD cb_data = 0; + LONG st = RegQueryValueExW (hkey, (LPCWSTR) wname, NULL, ®_type, NULL, &cb_data); + + if (ERROR_FILE_NOT_FOUND == st) + { + free (wname); + return Env_OK; + } + if (ERROR_SUCCESS != st) + { + free (wname); + char *code_str = xy_int2str ((int) st); + if (out_err) + { + *out_err = ENGLISH + ? xy_strcat (4, "Failed to query Windows Registry value `", name, "` (error ", code_str) + : xy_strcat (4, "读取 Windows 注册表变量 `", name, "` 失败,错误码: ", code_str); + } + free (code_str); + return Env_TargetUnwritable; + } + + unsigned char *raw = xy_malloc0 ((size_t) cb_data + sizeof (WCHAR)); + DWORD cb_read = cb_data; + st = RegQueryValueExW (hkey, (LPCWSTR) wname, NULL, ®_type, (LPBYTE) raw, &cb_read); + free (wname); + + if (ERROR_SUCCESS != st) + { + free (raw); + char *code_str = xy_int2str ((int) st); + if (out_err) + { + *out_err = ENGLISH + ? xy_strcat (4, "Failed to read Windows Registry value `", name, "` (error ", code_str) + : xy_strcat (4, "读取 Windows 注册表变量 `", name, "` 内容失败,错误码: ", code_str); + } + free (code_str); + return Env_TargetUnwritable; + } + + snap->existed = true; + snap->reg_type = (uint32_t) reg_type; + snap->raw_bytes = raw; + snap->raw_len = (size_t) cb_read; + + if (REG_SZ == reg_type || REG_EXPAND_SZ == reg_type) + { + size_t u16_units = (size_t) cb_read / sizeof (WCHAR); + uint16_t *u16_aligned = xy_malloc0 ((u16_units + 1) * sizeof (uint16_t)); + for (size_t u = 0; u < u16_units; u++) + { + u16_aligned[u] = (uint16_t) raw[u * 2] + | ((uint16_t) raw[u * 2 + 1] << 8); + } + snap->utf8_sz_val = env_utf16le_to_utf8 (u16_aligned, u16_units); + free (u16_aligned); + } + + return Env_OK; +} + +/** + * 将整个当前注册表键的原有值连同本次新增变量的删除标记 ("NAME"=-) 导出到 %TEMP%\chsrc-<随机名>\env-hkcu|hklm-.reg + */ +static bool +env_write_win_reg_backup (HKEY hkey_read, + bool is_system, + const EnvRegSnapshot_t *snaps, + size_t snap_count, + char **out_reg_utf8_path, + char **out_err) +{ + *out_reg_utf8_path = NULL; + + /* Combo dish 同一根键去重:若同一轮 combo 已完整导出过该根键原始镜像,无需重复覆盖 */ + if (chsrc_in_dish_group_mode ()) + { + if (ProgStatus.ComboStackDepth != Env_Win_Last_Combo_Depth) + { + Env_Win_Last_Combo_Depth = ProgStatus.ComboStackDepth; + Env_Win_Combo_HKCU_Saved = false; + Env_Win_Combo_HKLM_Saved = false; + } + if (!is_system && Env_Win_Combo_HKCU_Saved) + return true; + if (is_system && Env_Win_Combo_HKLM_Saved) + return true; + } + else + { + Env_Win_Combo_HKCU_Saved = false; + Env_Win_Combo_HKLM_Saved = false; + } + + if (NULL == Env_Win_Backup_Dir) + { + Env_Win_Backup_Dir = chsrc_make_tmpdir ("chsrc", out_err); + if (NULL == Env_Win_Backup_Dir) + return false; + } + + Env_Win_Backup_Seq++; + char file_name[64]; + snprintf (file_name, sizeof (file_name), "env-%s-%u.reg", + is_system ? "hklm" : "hkcu", Env_Win_Backup_Seq); + + char *reg_utf8 = xy_strcat (3, Env_Win_Backup_Dir, "\\", file_name); + + /* 若处于 combo 模式,尽量把该键下全部现有值也一并纳入首次完整快照 */ + EnvRegSnapshot_t *export_snaps = NULL; + size_t export_count = 0; + bool own_export = false; + + if (chsrc_in_dish_group_mode () && NULL != hkey_read) + { + DWORD val_count = 0; + DWORD max_name_len = 0; + DWORD max_data_len = 0; + if (ERROR_SUCCESS == RegQueryInfoKeyW (hkey_read, NULL, NULL, NULL, NULL, NULL, NULL, + &val_count, &max_name_len, &max_data_len, NULL, NULL)) + { + export_snaps = xy_malloc0 ((val_count + snap_count + 1) * sizeof (EnvRegSnapshot_t)); + own_export = true; + + WCHAR *wname_buf = xy_malloc0 ((max_name_len + 4) * sizeof (WCHAR)); + for (DWORD idx = 0; idx < val_count; idx++) + { + DWORD cch_name = max_name_len + 2; + LONG est = RegEnumValueW (hkey_read, idx, wname_buf, &cch_name, + NULL, NULL, NULL, NULL); + if (ERROR_SUCCESS != est) + continue; + char *ename = env_utf16le_to_utf8 ((const uint16_t *) wname_buf, (size_t) cch_name); + if (Env_OK == env_read_win_snapshot_one (hkey_read, ename, &export_snaps[export_count], NULL)) + { + export_snaps[export_count].name = ename; + export_count++; + } + else + { + free (ename); + } + } + free (wname_buf); + + /* 追加本次将写入但原先不存在的变量(导出为 "NAME"=-) */ + for (size_t s = 0; s < snap_count; s++) + { + if (!snaps[s].existed) + { + export_snaps[export_count].name = xy_strdup (snaps[s].name); + export_snaps[export_count].existed = false; + export_count++; + } + } + } + } + + const EnvRegSnapshot_t *src_snaps = own_export ? export_snaps : snaps; + size_t src_count = own_export ? export_count : snap_count; + + size_t reg_byte_len = 0; + unsigned char *reg_bytes = env_build_reg_backup_bytes ( + is_system ? ENV_WIN_HKLM_REGPATH : ENV_WIN_HKCU_REGPATH, + src_snaps, src_count, ®_byte_len); + + if (own_export) + { + for (size_t i = 0; i < export_count; i++) + { + free ((void *) export_snaps[i].name); + } + env_free_reg_snapshots (export_snaps, export_count); + } + + uint16_t *reg_w = NULL; + size_t reg_w_len = 0; + env_utf8_to_utf16le (reg_utf8, ®_w, ®_w_len); + (void) reg_w_len; + + HANDLE hf = CreateFileW ((LPCWSTR) reg_w, GENERIC_WRITE, 0, NULL, + CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + free (reg_w); + + if (INVALID_HANDLE_VALUE == hf) + { + DWORD err = GetLastError (); + char *err_s = xy_int2str ((int) err); + if (out_err) + { + *out_err = ENGLISH + ? xy_strcat (4, "Failed to create Registry backup file `", reg_utf8, "` (Win32 error ", err_s) + : xy_strcat (4, "无法创建注册表备份文件 `", reg_utf8, "`(请检查磁盘空间与权限),错误码: ", err_s); + } + free (err_s); + free (reg_bytes); + free (reg_utf8); + return false; + } + + DWORD written = 0; + BOOL wok = WriteFile (hf, reg_bytes, (DWORD) reg_byte_len, &written, NULL); + DWORD werr = wok ? 0 : GetLastError (); + CloseHandle (hf); + free (reg_bytes); + + if (!wok || written != (DWORD) reg_byte_len) + { + char *err_s = xy_int2str ((int) werr); + if (out_err) + { + *out_err = ENGLISH + ? xy_strcat (4, "Failed to write Registry backup file `", reg_utf8, "` (Win32 error ", err_s) + : xy_strcat (4, "写入注册表备份文件 `", reg_utf8, "` 失败(磁盘可能已满),错误码: ", err_s); + } + free (err_s); + free (reg_utf8); + return false; + } + + if (chsrc_in_dish_group_mode ()) + { + if (is_system) Env_Win_Combo_HKLM_Saved = true; + else Env_Win_Combo_HKCU_Saved = true; + } + + char *bk_msg = ENGLISH + ? xy_2strcat ("Backed up Windows Registry environment to: ", reg_utf8) + : xy_2strcat ("已备份 Windows 注册表环境变量至: ", reg_utf8); + chsrc_log2 (bk_msg); + free (bk_msg); + + *out_reg_utf8_path = reg_utf8; + return true; +} + +static void +env_rollback_win_registry (HKEY hkey_write, const EnvRegSnapshot_t *snaps, size_t count) +{ + for (size_t i = 0; i < count; i++) + { + uint16_t *wname = NULL; + size_t wname_len = 0; + env_utf8_to_utf16le (snaps[i].name, &wname, &wname_len); + (void) wname_len; + + if (!snaps[i].existed) + { + RegDeleteValueW (hkey_write, (LPCWSTR) wname); + } + else + { + RegSetValueExW (hkey_write, (LPCWSTR) wname, 0, + (DWORD) snaps[i].reg_type, + (const BYTE *) snaps[i].raw_bytes, + (DWORD) snaps[i].raw_len); + } + free (wname); + } +} + +static EnvOp_t +env_apply_windows (const EnvKV_t *kvs, size_t kv_count, char **out_err) +{ + bool is_system = chsrc_in_system_scope_mode (); + + if (is_system && !chsrc_is_root () && !in_dry_run_mode ()) + { + if (out_err) + { + *out_err = ENGLISH + ? xy_strdup ("Please run the terminal as Administrator to ensure the necessary permissions") + : xy_strdup ("请以管理员身份运行终端来保证必要的权限"); + } + return Env_NoPermission; + } + + for (size_t k = 0; k < kv_count; k++) + { + if (!env_validate_value (kvs[k].value, EnvDialect_WinRegistry, NULL)) + { + if (out_err) + { + *out_err = ENGLISH + ? xy_strcat (3, "Value for `", kvs[k].name, + "` contains invalid control characters or newlines for Windows Registry") + : xy_strcat (3, "变量 `", kvs[k].name, + "` 的值含有换行或非法控制字符,无法写入 Windows 注册表"); + } + return Env_BadValue; + } + } + + HKEY root_key = is_system ? HKEY_LOCAL_MACHINE : HKEY_CURRENT_USER; + LPCWSTR sub_key = is_system ? ENV_WIN_HKLM_SUBKEY : ENV_WIN_HKCU_SUBKEY; + + HKEY hkey_read = NULL; + LONG st = RegOpenKeyExW (root_key, sub_key, 0, KEY_QUERY_VALUE, &hkey_read); + if (ERROR_FILE_NOT_FOUND == st) + { + hkey_read = NULL; + } + else if (ERROR_ACCESS_DENIED == st) + { + if (out_err) + { + *out_err = ENGLISH + ? xy_strdup ("Access denied when opening Windows Registry Environment key") + : xy_strdup ("打开 Windows 注册表环境变量键时权限不足"); + } + return Env_NoPermission; + } + else if (ERROR_SUCCESS != st) + { + char *code_s = xy_int2str ((int) st); + if (out_err) + { + *out_err = ENGLISH + ? xy_2strcat ("Failed to open Windows Registry Environment key, error: ", code_s) + : xy_2strcat ("打开 Windows 注册表环境变量键失败,错误码: ", code_s); + } + free (code_s); + return Env_TargetUnwritable; + } + + EnvRegSnapshot_t *snaps = xy_malloc0 (kv_count * sizeof (EnvRegSnapshot_t)); + for (size_t k = 0; k < kv_count; k++) + { + EnvOp_t qrc = env_read_win_snapshot_one (hkey_read, kvs[k].name, &snaps[k], out_err); + if (Env_OK != qrc) + { + if (hkey_read) RegCloseKey (hkey_read); + env_free_reg_snapshots (snaps, k); + return qrc; + } + } + + /* 幂等判定:检查是否所有目标变量在注册表中已完全一致 */ + bool any_changed = false; + for (size_t k = 0; k < kv_count; k++) + { + if (NULL == kvs[k].value) + { + if (snaps[k].existed) + { + any_changed = true; + break; + } + } + else + { + if ( !snaps[k].existed + || REG_SZ != snaps[k].reg_type + || NULL == snaps[k].utf8_sz_val + || !xy_streql (snaps[k].utf8_sz_val, kvs[k].value)) + { + any_changed = true; + break; + } + } + } + + if (!any_changed) + { + if (hkey_read) RegCloseKey (hkey_read); + env_free_reg_snapshots (snaps, kv_count); + return Env_Unchanged; + } + + /* Dry-run 模式不创建 .reg 文件、不修改注册表、不发送广播 */ + if (in_dry_run_mode ()) + { + if (hkey_read) RegCloseKey (hkey_read); + env_free_reg_snapshots (snaps, kv_count); + const char *reg_disp = is_system ? ENV_WIN_HKLM_REGPATH : ENV_WIN_HKCU_REGPATH; + log_for_file_write (reg_disp, true); + return Env_OK; + } + + char *backup_path = NULL; + if (!env_write_win_reg_backup (hkey_read, is_system, snaps, kv_count, &backup_path, out_err)) + { + if (hkey_read) RegCloseKey (hkey_read); + env_free_reg_snapshots (snaps, kv_count); + return Env_TargetUnwritable; + } + + if (hkey_read) + { + RegCloseKey (hkey_read); + hkey_read = NULL; + } + + HKEY hkey_write = NULL; + DWORD disp = 0; + st = RegCreateKeyExW (root_key, sub_key, 0, NULL, REG_OPTION_NON_VOLATILE, + KEY_QUERY_VALUE | KEY_SET_VALUE, NULL, &hkey_write, &disp); + if (ERROR_ACCESS_DENIED == st) + { + if (backup_path) free (backup_path); + env_free_reg_snapshots (snaps, kv_count); + if (out_err) + { + *out_err = ENGLISH + ? xy_strdup ("Access denied when opening Windows Registry Environment key for writing") + : xy_strdup ("以写入权限打开 Windows 注册表环境变量键被拒绝"); + } + return Env_NoPermission; + } + if (ERROR_SUCCESS != st) + { + char *code_s = xy_int2str ((int) st); + if (out_err) + { + *out_err = ENGLISH + ? xy_2strcat ("Failed to open Windows Registry Environment key for writing, error: ", code_s) + : xy_2strcat ("打开 Windows 注册表写入句柄失败,错误码: ", code_s); + } + free (code_s); + if (backup_path) free (backup_path); + env_free_reg_snapshots (snaps, kv_count); + return Env_TargetUnwritable; + } + + for (size_t k = 0; k < kv_count; k++) + { + uint16_t *wname = NULL; + size_t wname_len = 0; + env_utf8_to_utf16le (kvs[k].name, &wname, &wname_len); + (void) wname_len; + + LONG op_st = ERROR_SUCCESS; + if (NULL == kvs[k].value) + { + op_st = RegDeleteValueW (hkey_write, (LPCWSTR) wname); + if (ERROR_FILE_NOT_FOUND == op_st) + op_st = ERROR_SUCCESS; + } + else + { + uint16_t *wval = NULL; + size_t wval_len = 0; + env_utf8_to_utf16le (kvs[k].value, &wval, &wval_len); + DWORD cb = (DWORD) ((wval_len + 1) * sizeof (WCHAR)); + op_st = RegSetValueExW (hkey_write, (LPCWSTR) wname, 0, REG_SZ, + (const BYTE *) wval, cb); + free (wval); + } + free (wname); + + if (ERROR_SUCCESS != op_st) + { + /* 立即回滚已修改的前序键值 */ + env_rollback_win_registry (hkey_write, snaps, k); + RegCloseKey (hkey_write); + + char *code_s = xy_int2str ((int) op_st); + const char *b = backup_path ? backup_path : "(combo backup)"; + if (out_err) + { + *out_err = ENGLISH + ? xy_strcat (6, "Failed to update Registry value `", kvs[k].name, + "` (error ", code_s, + "); rolled back previous changes. Backup saved at: ", b) + : xy_strcat (6, "更新注册表变量 `", kvs[k].name, + "` 失败(错误码 ", code_s, + "),已自动回滚前序修改。原始备份位于: ", b); + } + free (code_s); + if (backup_path) free (backup_path); + env_free_reg_snapshots (snaps, kv_count); + return Env_TargetUnwritable; + } + } + + RegCloseKey (hkey_write); + if (backup_path) free (backup_path); + env_free_reg_snapshots (snaps, kv_count); + + const char *reg_disp = is_system ? ENV_WIN_HKLM_REGPATH : ENV_WIN_HKCU_REGPATH; + log_for_file_write (reg_disp, true); + + /* 广播 WM_SETTINGCHANGE,通知 Explorer 与现代终端刷新环境块(超时或失败静默忽略) */ + DWORD_PTR bc_res = 0; + SendMessageTimeoutW (HWND_BROADCAST, WM_SETTINGCHANGE, 0, + (LPARAM) L"Environment", SMTO_ABORTIFHUNG, + 1500, &bc_res); + + return Env_OK; +} + +static EnvOp_t +env_query_persisted_windows (const char *name, bool *out_found, char **out_val, char **out_err) +{ + *out_found = false; + *out_val = NULL; + + bool is_system = chsrc_in_system_scope_mode (); + HKEY root_key = is_system ? HKEY_LOCAL_MACHINE : HKEY_CURRENT_USER; + LPCWSTR sub_key = is_system ? ENV_WIN_HKLM_SUBKEY : ENV_WIN_HKCU_SUBKEY; + + HKEY hkey = NULL; + LONG st = RegOpenKeyExW (root_key, sub_key, 0, KEY_QUERY_VALUE, &hkey); + if (ERROR_FILE_NOT_FOUND == st) + return Env_OK; + if (ERROR_ACCESS_DENIED == st) + { + if (out_err) + { + *out_err = ENGLISH + ? xy_strdup ("Access denied when reading Windows Registry Environment key") + : xy_strdup ("读取 Windows 注册表环境变量键权限不足"); + } + return Env_NoPermission; + } + if (ERROR_SUCCESS != st) + { + char *code_s = xy_int2str ((int) st); + if (out_err) + { + *out_err = ENGLISH + ? xy_2strcat ("Failed to open Windows Registry Environment key, error: ", code_s) + : xy_2strcat ("打开 Windows 注册表环境变量键失败,错误码: ", code_s); + } + free (code_s); + return Env_TargetUnwritable; + } + + EnvRegSnapshot_t snap; + EnvOp_t qrc = env_read_win_snapshot_one (hkey, name, &snap, out_err); + RegCloseKey (hkey); + if (Env_OK != qrc) + return qrc; + + if (!snap.existed) + return Env_OK; + + if (REG_SZ != snap.reg_type && REG_EXPAND_SZ != snap.reg_type) + { + if (snap.raw_bytes) free (snap.raw_bytes); + if (snap.utf8_sz_val) free (snap.utf8_sz_val); + if (out_err) + { + *out_err = ENGLISH + ? xy_strcat (3, "Windows Registry value `", name, "` has unsupported non-string type") + : xy_strcat (3, "Windows 注册表变量 `", name, "` 为非字符串类型,无法作为镜像地址解析"); + } + return Env_AmbiguousTarget; + } + + *out_found = true; + *out_val = snap.utf8_sz_val; + if (snap.raw_bytes) free (snap.raw_bytes); + return Env_OK; +} + +#endif /* XY_Build_On_Windows */ + + +/* ============================================================ + * 跨平台统一内部入口(供公开 API 与 test/fw.c 共用) + * ============================================================ */ + +/** + * @brief 持久化写入引擎的内部入口,供公开 API 与 `test/fw.c` 单元测试共用 + * + * @details 与 `chsrc_set_env()` / `chsrc_set_env_block()` 的区别仅在于错误处理策略: + * 本函数**不打印、不退出**,而是把完整的 `EnvOp_t` 状态原样返回, + * 以便单元测试覆盖全部失败分支(公开 API 依赖它"遇错即退出"的行为) + * + * @param raw_kvs (name, value) 数组;`value == NULL` 表示删除 + * @param raw_count 数组元素个数。为 0 或 `raw_kvs == NULL` 时直接返回 `Env_Unchanged` + * @param out_shell [out] 实际使用的 POSIX shell,仅 UserScope 且 shell 被成功识别时赋值; + * SystemScope、失败路径以及 Windows 构建下保持 `PosixShell_Unknown`。 + * 可为 `NULL`。调用方需在返回 `Env_OK` 后把它交给重启提示逻辑 + * @param out_err [out] 失败原因文案,**caller-free**;仅在非 `Env_OK`/`Env_Unchanged` + * 时被赋值,成功时为 `NULL`。可为 `NULL` + * + * @return 完整的 `EnvOp_t`,包括 `Env_NoPermission` 这类公开 API 会转成退出的状态 + * + * @note 入参数组与其中的字符串由调用方持有;本函数内部只做同名去重 + * (后者覆盖前者的值,位置保持首次出现),不产生所有权转移 + */ +static EnvOp_t +env_apply_kvs (const EnvKV_t *raw_kvs, size_t raw_count, + PosixShell_t *out_shell, char **out_err) +{ + if (out_err) *out_err = NULL; + if (out_shell) *out_shell = PosixShell_Unknown; + + if (chsrc_in_project_scope_mode ()) + { + if (out_err) + { + *out_err = ENGLISH + ? xy_strdup ("Project scope is not supported for environment variable recipes") + : xy_strdup ("环境变量换源不支持项目级作用域 (ProjectScope)"); + } + return Env_ScopeUnsupported; + } + + if (0 == raw_count || NULL == raw_kvs) + return Env_Unchanged; + + /* 1. 校验所有变量名 */ + for (size_t i = 0; i < raw_count; i++) + { + if (!env_validate_name (raw_kvs[i].name)) + { + const char *bad = raw_kvs[i].name ? raw_kvs[i].name : "(null)"; + if (out_err) + { + *out_err = ENGLISH + ? xy_strcat (3, "Invalid environment variable name: `", bad, "`") + : xy_strcat (3, "非法的环境变量名: `", bad, "`"); + } + return Env_BadName; + } + } + + /* 2. 去重:同名变量保留最后一次指定的值,保持首次出现的顺序 */ + EnvKV_t *kvs = xy_malloc0 (raw_count * sizeof (EnvKV_t)); + size_t kv_count = 0; + + for (size_t i = 0; i < raw_count; i++) + { + bool updated = false; + for (size_t j = 0; j < kv_count; j++) + { + if (xy_streql (kvs[j].name, raw_kvs[i].name)) + { + kvs[j].value = raw_kvs[i].value; + updated = true; + break; + } + } + if (!updated) + { + kvs[kv_count++] = raw_kvs[i]; + } + } + +#ifdef XY_Build_On_Windows + EnvOp_t rc = env_apply_windows (kvs, kv_count, out_err); + free (kvs); + return rc; +#else + EnvOp_t rc = env_apply_posix (kvs, kv_count, out_shell, out_err); + free (kvs); + return rc; +#endif +} + +/** + * @brief 查询单个变量持久化值的内部入口,供 `chsrc_get_env()` 与单元测试共用 + * + * @param name 变量名,必须匹配 `^[A-Za-z_][A-Za-z0-9_]*$` + * @param out_found [out] 是否在持久化目标中找到该变量;未找到时 `*out_val` 保持 `NULL`。可为 `NULL` + * @param out_val [out] 解析出的值,**caller-free**。值为空串时返回的也是空串而非 `NULL`。可为 `NULL` + * @param out_err [out] 失败原因文案,**caller-free**;仅在非 `Env_OK` 时被赋值。可为 `NULL` + * + * @return 完整 `EnvOp_t`,不打印、不退出 + * @retval Env_OK 查询完成(`*out_found` 区分"已设置"与"未设置") + * @retval Env_AmbiguousTarget 无法唯一定位:shell 未识别、多文件值冲突、行含复杂语法或注册表非字符串类型 + * @retval Env_TargetUnwritable 目标文件无法读取 + * @retval Env_NoPermission 注册表键读取权限不足 + * @retval Env_BadName 变量名非法 + * @retval Env_ScopeUnsupported 当前为 `ProjectScope` + * + * @note 同一文件内多次安全赋值时以**最后一次**为准,与 shell 顺序执行语义一致 + */ +static EnvOp_t +env_query_persisted (const char *name, bool *out_found, char **out_val, char **out_err) +{ + if (out_err) *out_err = NULL; + if (out_found) *out_found = false; + if (out_val) *out_val = NULL; + + if (chsrc_in_project_scope_mode ()) + { + if (out_err) + { + *out_err = ENGLISH + ? xy_strdup ("Project scope is not supported for environment variable recipes") + : xy_strdup ("环境变量换源不支持项目级作用域 (ProjectScope)"); + } + return Env_ScopeUnsupported; + } + + if (!env_validate_name (name)) + { + const char *bad = name ? name : "(null)"; + if (out_err) + { + *out_err = ENGLISH + ? xy_strcat (3, "Invalid environment variable name: `", bad, "`") + : xy_strcat (3, "非法的环境变量名: `", bad, "`"); + } + return Env_BadName; + } + +#ifdef XY_Build_On_Windows + return env_query_persisted_windows (name, out_found, out_val, out_err); +#else + return env_query_persisted_posix (name, out_found, out_val, out_err); +#endif +} + +static void +env_emit_post_write_hint (PosixShell_t shell) +{ + if (in_dry_run_mode ()) + return; + +#ifdef XY_Build_On_Windows + (void) shell; + char *msg = ENGLISH + ? "Environment variables updated in Windows Registry. Please restart your terminal for changes to take effect." + : "已更新 Windows 注册表环境变量,请重启终端使新环境变量生效"; + chsrc_note2 (msg); +#else + if (chsrc_in_system_scope_mode ()) + { + char *msg = ENGLISH + ? "Updated /etc/environment. Please re-login or start a new session for system-wide changes to take effect." + : "已更新 /etc/environment,请重新登录系统或开启新会话使全局环境变量生效"; + chsrc_note2 (msg); + return; + } + + const char *reload_cmd = "source ~/.profile"; + if (PosixShell_Bash == shell) + { + reload_cmd = xy_file_exist ("~/.bashrc") ? "source ~/.bashrc" : "source ~/.profile"; + } + else if (PosixShell_Zsh == shell) + { + reload_cmd = xy_file_exist ("~/.zshrc") ? "source ~/.zshrc" : "source ~/.profile"; + } + else if (PosixShell_Fish == shell) + { + reload_cmd = "source ~/.config/fish/conf.d/chsrc.fish"; + } + + char *msg = ENGLISH + ? xy_strcat (3, "Please restart your terminal or run `", reload_cmd, "` to apply environment variable changes immediately") + : xy_strcat (3, "请重启终端或执行 `", reload_cmd, "` 以立即生效环境变量更改"); + chsrc_note2 (msg); + free (msg); +#endif +} + +static void +env_abort_on_error (EnvOp_t op, char *err_msg) +{ + if (err_msg) + { + chsrc_error2 (err_msg); + free (err_msg); + } + else + { + chsrc_error2 (ENGLISH ? "Environment variable operation failed" : "环境变量操作失败"); + } + + if (Env_ScopeUnsupported == op) + exit (Exit_Unsupported); + else + exit (Exit_UserCause); +} + + +/* ============================================================ + * Recipe 公开 API + * ============================================================ */ + +/** + * @brief 持久化写入或删除**单个**环境变量 + * + * @details 写入目标由当前 `ProgMode.Scope` 与平台共同决定: + * - Windows:`HKCU\Environment`(UserScope)或 + * `HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment`(SystemScope) + * - POSIX UserScope:精确识别出的当前 shell 对应的一组 rc 文件 + * - POSIX SystemScope:`/etc/environment` + * + * @note 绝不修改当前进程的环境块(不调用 `setenv`/`_putenv`), + * 因此写入后必须由用户重启终端或重新加载配置文件才会生效 + * + * @note **幂等**:若所有目标的持久化值已经等于期望值,则不备份、不写盘、不广播、 + * 不打印重启提示,直接返回 `Env_Unchanged` + * + * @note 实际发生写入时(`Env_OK`),本函数会在末尾输出且仅输出 1 次重启终端提示; + * Windows 侧还会广播 `WM_SETTINGCHANGE` 通知 Explorer 与现代终端刷新 + * + * @param name 变量名,必须匹配 `^[A-Za-z_][A-Za-z0-9_]*$` + * @param value 变量值。`NULL` 表示**删除**该变量;`""` 表示设置为空串。 + * 含目标方言的危险字符(POSIX 双引号内的 `"` `\\` `$` `` ` ``、 + * 以及所有平台的 `\r` `\n` 与控制字符)会被拒绝 + * + * @return 只会是下面两种之一,其余状态不返回 + * @retval Env_OK 实际发生了写入或删除 + * @retval Env_Unchanged 目标值已与期望一致 + * + * @warning 其余 `EnvOp_t` 状态(权限不足、目标不可写、非法名/值、无法确认 shell、 + * 多文件冲突或行含复杂语法、ProjectScope)会先打印中英双语错误, + * 再 `exit (Exit_UserCause)`(ProjectScope 为 `Exit_Unsupported`), + * 以防止 recipe 漏检返回值而打印假的"换源成功"结论 + */ +static EnvOp_t +chsrc_set_env (const char *name, const char *value) +{ + EnvKV_t kv = { name, value }; + PosixShell_t shell = PosixShell_Unknown; + char *err = NULL; + + EnvOp_t op = env_apply_kvs (&kv, 1, &shell, &err); + if (Env_OK == op) + { + if (err) free (err); + env_emit_post_write_hint (shell); + return Env_OK; + } + if (Env_Unchanged == op) + { + if (err) free (err); + return Env_Unchanged; + } + + env_abort_on_error (op, err); + return op; +} + +/** + * @brief 一次性持久化写入或删除**多个**环境变量,供一个 dish 需要同时设置若干变量的场景使用 + * + * @details 语义上等价于对每个变量调用一次 `chsrc_set_env()`,但代价只有一份: + * 单次备份、单次落盘、Windows 侧单次注册表写入序列与单次 `WM_SETTINGCHANGE` + * 广播,且最多输出 1 次重启终端提示。同一 dish 内分多次调用本函数 + * 也不会互相抹掉对方已写入的变量(按变量名增量合并/删除)。 + * + * @param name1 第 1 个变量名。传 `NULL` 表示空操作,直接返回 `Env_Unchanged` + * @param ... 紧跟 `name1` 的是它的值,之后交替出现 `(name, value)`, + * 并以**一个额外的 `NULL`** 结束(即结束符占用的是 name 槽位) + * + * @note **参数总数必须是奇数**。`name` 与 `value` 成对,末尾单独一个 `NULL` 作为哨兵: + * @code + * chsrc_set_env_block ("FOO", "1", NULL); // 写 1 个 + * chsrc_set_env_block ("FOO", "1", "BAR", NULL, NULL); // 写 FOO,删 BAR + * @endcode + * 若误传成偶数个参数,最后的 `NULL` 会被当成某个变量的值消费掉, + * 随后 `va_arg()` 将越过参数表末尾读取,属未定义行为 + * + * @note 同一名字出现多次时,**最后一次**的值生效,但其在参数表中的位置保持首次出现的位置 + * + * @note 校验阶段是"全有或全无":任一变量名/值非法,或任一目标文件里的同名行含 + * 复杂动态语法,则在任何落盘动作之前中止;Windows 侧若写入中途失败, + * 会自动把已改过的值回滚为原状态并给出 `.reg` 备份路径 + * + * @return 同 `chsrc_set_env()`,只会是 `Env_OK` 或 `Env_Unchanged`; + * 其余状态同样打印双语错误后 `exit`,详见 `chsrc_set_env()` 的 `@warning` + */ +static EnvOp_t +chsrc_set_env_block (const char *name1, ...) +{ + if (NULL == name1) + return Env_Unchanged; + + size_t cap = 8; + size_t count = 0; + EnvKV_t *kvs = xy_malloc0 (cap * sizeof (EnvKV_t)); + + va_list ap; + va_start (ap, name1); + + const char *cur_name = name1; + while (NULL != cur_name) + { + const char *cur_val = va_arg (ap, const char *); + if (count >= cap) + { + cap *= 2; + EnvKV_t *grown = realloc (kvs, cap * sizeof (EnvKV_t)); + if (!grown) + { + va_end (ap); + free (kvs); + xy_throw ("Memory allocation failed in chsrc_set_env_block"); + } + kvs = grown; + } + kvs[count++] = (EnvKV_t) { cur_name, cur_val }; + cur_name = va_arg (ap, const char *); + } + + va_end (ap); + + PosixShell_t shell = PosixShell_Unknown; + char *err = NULL; + + EnvOp_t op = env_apply_kvs (kvs, count, &shell, &err); + free (kvs); + + if (Env_OK == op) + { + if (err) free (err); + env_emit_post_write_hint (shell); + return Env_OK; + } + if (Env_Unchanged == op) + { + if (err) free (err); + return Env_Unchanged; + } + + env_abort_on_error (op, err); + return op; +} + +/** + * @brief 读取并打印若干个环境变量的值,优先取持久化配置,缺失时回退到当前终端会话 + * + * @details 这是 `chsrc get` 对环境变量型 dish 的展示入口。它先按当前平台与作用域解析 + * 真正的持久化落点(POSIX 逐行解析 shell rc 文件与 `/etc/environment`, + * Windows 读注册表),因此打印出的就是重启终端之后才会生效的值; + * 若持久化配置中没有该变量,则以当前进程环境(`getenv`)为准, + * 方便用户在首次 `chsrc set` 之前查看机器上已有的值。 + * + * @param var1 第 1 个要查看的变量名,不可为 `NULL` + * @param ... 后续变量名,以 `NULL` 结束:`chsrc_get_env ("A", "B", NULL);` + * + * @note 只读,不改文件,不生成备份 + * + * @note 每个变量只输出一行 `NAME=value`: + * - 持久化配置中存在 → 输出持久化值 + * - 持久化配置中不存在 → 输出当前终端会话中的值;会话中也没有则为 `NAME=` + * + * @note 仅当**持久化值与会话值同时存在且不一致**时才追加一条提示,说明需重启终端 + * 或重新加载配置文件才会同步;只有单侧存在时不做任何额外提示 + * + * @important + * 本函数 **只打印、不返回值** ;若 recipe 需要在内存中取用当前进程的环境变量, + * 请直接调用 `getenv()`,不要把本函数当作取值接口 + * + * @warning 所有变量先完成读取校验,任何一个无法唯一定位持久化值时**不输出半截结果**, + * 而是打印双语错误后 `exit`。拒绝的情形包括:无法确认当前 shell、 + * 多个配置文件对同一变量给出不同值、同名行含动态展开或复杂语法、 + * 注册表值不是字符串类型、`ProjectScope`、变量名非法 + */ +static void +chsrc_get_env (const char *var1, ...) +{ + if (!var1) + { + xy_unreached (); + return; + } + + size_t cap = 8; + size_t count = 0; + const char **vars = xy_malloc0 (cap * sizeof (const char *)); + + va_list ap; + va_start (ap, var1); + const char *cur = var1; + while (cur) + { + if (count >= cap) + { + cap *= 2; + const char **grown = realloc (vars, cap * sizeof (const char *)); + if (!grown) + { + va_end (ap); + free (vars); + xy_throw ("Memory allocation failed in chsrc_get_env"); + } + vars = grown; + } + vars[count++] = cur; + cur = va_arg (ap, const char *); + } + va_end (ap); + + /* 先对全部待查变量完成持久化读取校验,遇到未知 Shell / 冲突 / 复杂语法立即中止,避免输出半截结果 */ + bool *founds = xy_malloc0 (count * sizeof (bool)); + char **vals = xy_malloc0 (count * sizeof (char *)); + + for (size_t i = 0; i < count; i++) + { + char *err = NULL; + EnvOp_t rc = env_query_persisted (vars[i], &founds[i], &vals[i], &err); + if (Env_OK != rc) + { + for (size_t j = 0; j < i; j++) + { + if (vals[j]) free (vals[j]); + } + free (founds); + free (vals); + free (vars); + env_abort_on_error (rc, err); + return; + } + if (err) free (err); + } + + for (size_t i = 0; i < count; i++) + { + const char *proc_val = getenv (vars[i]); + + /* 持久化配置中没有该项时以当前进程环境为准,不做额外提示 */ + const char *shown = founds[i] ? vals[i] : (proc_val ? proc_val : ""); + printf ("%s=%s\n", vars[i], shown); + + /* 仅当持久化值与当前会话值同时存在且不一致时才提示 */ + if (founds[i] && proc_val && !xy_streql (vals[i], proc_val)) + { + char *note = ENGLISH + ? xy_strcat (5, "Current terminal session has a different value for `", vars[i], + "` (`", proc_val, "`); restart terminal or reload shell profile to sync") + : xy_strcat (5, "当前终端会话中的 `", vars[i], + "` 值为 `", proc_val, "`(与已持久化配置不同),重启终端或重新加载配置文件后同步"); + chsrc_note2 (note); + free (note); + } + + if (vals[i]) free (vals[i]); + } + + free (founds); + free (vals); + free (vars); +} diff --git a/src/framework/source.c b/src/framework/source.c index 50bb6b0d6..05b37a526 100644 --- a/src/framework/source.c +++ b/src/framework/source.c @@ -10,7 +10,7 @@ * | @JIAFALSEDREAM * | * Created On : <2023-08-29> - * Last Modified : <2026-08-20> + * Last Modified : <2026-10-28> * * 源的确定 * ------------------------------------------------------------*/ @@ -790,18 +790,12 @@ chsrc_conclude (Source_t *source) /** * @brief 检测该 dish 是否实现了用户所指定的 scope 能力 - * - * @note 此函数目前只支持中文 */ void chsrc_check_scope_capability (Dish_t *dish) { ScopeCapability_t cap = ScopeCap_Unknown; - char *msg1 = "不支持"; - char *msg2 = "换源,请使用 chsrc ls "; - char *msg3 = " 查看支持的作用域以及默认作用域"; - char *aliases = dish->aliases; char *scope_name = NULL; @@ -809,10 +803,11 @@ chsrc_check_scope_capability (Dish_t *dish) { cap = dish->scope_caps[ScopeCap_Slot_Project]; - if (cap != ScopeCap_Able_And_Implemented) + if (!scope_cap_is_runnable (cap)) { - scope_name = "项目级"; - char* msg = xy_strcat (5, msg1, scope_name, msg2, aliases, msg3); + scope_name = CHINESE ? "项目级" : "Project"; + char *msg = CHINESE ? xy_strcat (5, "不支持", scope_name, "换源,请使用 chsrc ls ", aliases, " 查看支持的作用域以及默认作用域") + : xy_strcat (4, scope_name, " scope is not supported. Use `chsrc ls ", aliases, "` to view supported scopes and default scope"); chsrc_error (msg); exit (Exit_UserCause); } @@ -821,10 +816,11 @@ chsrc_check_scope_capability (Dish_t *dish) { cap = dish->scope_caps[ScopeCap_Slot_User]; - if (cap != ScopeCap_Able_And_Implemented) + if (!scope_cap_is_runnable (cap)) { - scope_name = "用户级"; - char* msg = xy_strcat (5, msg1, scope_name, msg2, aliases, msg3); + scope_name = CHINESE ? "用户级" : "User"; + char *msg = CHINESE ? xy_strcat (5, "不支持", scope_name, "换源,请使用 chsrc ls ", aliases, " 查看支持的作用域以及默认作用域") + : xy_strcat (4, scope_name, " scope is not supported. Use `chsrc ls ", aliases, "` to view supported scopes and default scope"); chsrc_error (msg); exit (Exit_UserCause); } @@ -833,10 +829,11 @@ chsrc_check_scope_capability (Dish_t *dish) { cap = dish->scope_caps[ScopeCap_Slot_System]; - if (cap != ScopeCap_Able_And_Implemented) + if (!scope_cap_is_runnable (cap)) { - scope_name = "系统级"; - char* msg = xy_strcat (5, msg1, scope_name, msg2, aliases, msg3); + scope_name = CHINESE ? "系统级" : "System"; + char *msg = CHINESE ? xy_strcat (5, "不支持", scope_name, "换源,请使用 chsrc ls ", aliases, " 查看支持的作用域以及默认作用域") + : xy_strcat (4, scope_name, " scope is not supported. Use `chsrc ls ", aliases, "` to view supported scopes and default scope"); chsrc_error (msg); exit (Exit_UserCause); } diff --git a/src/framework/struct.h b/src/framework/struct.h index bae3ec905..db9bf7195 100644 --- a/src/framework/struct.h +++ b/src/framework/struct.h @@ -5,9 +5,10 @@ * File Authors : @ccmywish * | @G_I_Y * Contributors : @livelycode36 + * | @Mikachu2333 * | * Created On : <2023-08-29> - * Last Modified : <2026-09-01> + * Last Modified : <2026-10-28> * * chsrc struct * ------------------------------------------------------------*/ @@ -142,10 +143,18 @@ typedef enum ScopeCapability_t ScopeCap_Unknown, /* 未知,缺乏对该 dish 的细致了解 */ ScopeCap_Unable, /* 不支持该作用域 */ ScopeCap_Able_But_Not_Implemented, /* 支持但chsrc尚未实现 */ + ScopeCap_Able_But_Just_Docs, /* 支持但chsrc仅提供文档指引 */ ScopeCap_Able_And_Implemented /* 支持且chsrc已经实现 */ } ScopeCapability_t; +static inline bool +scope_cap_is_runnable (ScopeCapability_t cap) +{ + return ( cap == ScopeCap_Able_And_Implemented + || cap == ScopeCap_Able_But_Just_Docs); +} + typedef enum Capability_t { CanNot, diff --git a/src/framework/waiter.c b/src/framework/waiter.c index a96f92bc8..a2009f31f 100644 --- a/src/framework/waiter.c +++ b/src/framework/waiter.c @@ -4,10 +4,10 @@ * File Name : source.c * File Authors : @ccmywish * | @G_I_Y - * Contributors : Nul None + * Contributors : @Mikachu2333 * | * Created On : <2023-08-29> - * Last Modified : <2026-09-07> + * Last Modified : <2026-10-28> * * 由 chsrc-main.c 拆分而来 * ------------------------------------------------------------*/ @@ -250,20 +250,29 @@ cli_print_dish_features (Dish_t *dish, const char *input_dish_name) switch (cap) { case ScopeCap_Unknown: - msg = xy_strcat (6, " ", bdred(NoMark), scope_msg, " (", "是否支持该作用域尚不了解,欢迎贡献", ")"); + msg = xy_strcat (6, " ", bdred(NoMark), scope_msg, " (", + CHINESE ? "是否支持该作用域尚不了解,欢迎贡献" : "unknown, welcome to contribute", ")"); puts (msg); break; case ScopeCap_Unable: - msg = xy_strcat (6, " ", bdred(NoMark), scope_msg, " (", "不支持", ")"); + msg = xy_strcat (6, " ", bdred(NoMark), scope_msg, " (", + CHINESE ? "不支持" : "unsupported", ")"); puts (msg); break; case ScopeCap_Able_But_Not_Implemented: - msg = xy_strcat (6, " ", bdyellow(HalfYesMark), scope_msg, " (", "支持但未实现", ")"); + msg = xy_strcat (6, " ", bdyellow(HalfYesMark), scope_msg, " (", + CHINESE ? "支持但未实现" : "supported, but not implemented yet", ")"); + puts (msg); + break; + case ScopeCap_Able_But_Just_Docs: + msg = xy_strcat (6, " ", bdyellow(HalfYesMark), scope_msg, " (", + CHINESE ? "支持但仅提供文档指引" : "supported, docs guidance only", ")"); puts (msg); break; case ScopeCap_Able_And_Implemented: msg = xy_strcat (3, " ", bdgreen(YesMark), - purple (xy_strcat (4, scope_msg, " (", "支持且已实现", ")"))); + purple (xy_strcat (4, scope_msg, " (", + CHINESE ? "支持且已实现" : "supported and implemented", ")"))); puts (msg); break; default: @@ -289,8 +298,10 @@ cli_print_dish_features (Dish_t *dish, const char *input_dish_name) default: xy_unreached(); } + char *default_prefix = CHINESE ? "默认作用域 | chsrc set -scope=default " + : "Default Scope | chsrc set -scope=default "; char *msg = xy_strcat (2, bdblue (" = "), - purple (xy_strcat (5, "默认作用域 | chsrc set -scope=default ", input_dish_name, " (= ", default_scope_name ,")"))); + purple (xy_strcat (5, default_prefix, input_dish_name, " (= ", default_scope_name, ")"))); puts (msg); br(); } @@ -900,6 +911,7 @@ waiter_handle_Reset_Source (Dish_t *dish, const char *input, char *option) if (dish->resetfn) { + chsrc_check_scope_capability (dish); dish->resetfn(option); } else diff --git a/src/recipe/lang/Dart/Flutter.c b/src/recipe/lang/Dart/Flutter.c index 513d9dd0f..9d69588ce 100644 --- a/src/recipe/lang/Dart/Flutter.c +++ b/src/recipe/lang/Dart/Flutter.c @@ -10,25 +10,25 @@ pl_dart_flutter_prepare (void) chef_prep_this_dish (pl_dart_flutter, gsr); chef_set_recipe_created_on (this, "2023-09-10"); - chef_set_recipe_last_updated (this, "2025-07-11"); + chef_set_recipe_last_updated (this, "2026-10-28"); chef_set_chefs (this, 1, "@ccmywish"); - chef_set_sauciers (this, 2, "@czyt", "@xrgzs"); + chef_set_sauciers (this, 3, "@czyt", "@xrgzs", "@Mikachu2333"); - chef_set_scope_cap (this, ProjectScope, ScopeCap_Unknown); + chef_set_scope_cap (this, ProjectScope, ScopeCap_Unable); chef_set_scope_cap (this, UserScope, ScopeCap_Able_And_Implemented); - chef_set_scope_cap (this, SystemScope, ScopeCap_Unknown); + chef_set_scope_cap (this, SystemScope, ScopeCap_Able_And_Implemented); chef_set_default_scope (this, UserScope); - chef_allow_english(this); - chef_allow_user_define(this); + chef_allow_english (this); + chef_allow_user_define (this); def_sources_begin() - {&UpstreamProvider, "https://storage.googleapis.com", FeedByPrepare}, - {&FlutterCN, "https://storage.flutter-io.cn", FeedByPrepare}, - {&Sjtug_Zhiyuan, "https://mirror.sjtu.edu.cn", FeedByPrepare }, /* 官方文档也没有给后缀,怀疑是否存在问题 */ + {&UpstreamProvider, "https://storage.googleapis.com", FeedByPrepare}, + {&FlutterCN, "https://storage.flutter-io.cn", FeedByPrepare}, + {&Sjtug_Zhiyuan, "https://mirror.sjtu.edu.cn", FeedByPrepare}, /* 官方文档也没有给后缀,怀疑是否存在问题 */ {&Tuna, "https://mirrors.tuna.tsinghua.edu.cn/flutter", FeedByPrepare}, - {&Nju, "https://mirror.nju.edu.cn/flutter", FeedByPrepare} + {&Nju, "https://mirror.nju.edu.cn/flutter", FeedByPrepare} def_sources_end() chef_set_rest_smURL_with_postfix (this, "/flutter_infra_release/releases/stable/linux/flutter_linux_v1.0.0-stable.tar.xz"); @@ -41,7 +41,7 @@ pl_dart_flutter_prepare (void) void pl_dart_flutter_getsrc (char *option) { - chsrc_view_env ("FLUTTER_STORAGE_BASE_URL", NULL); + chsrc_get_env ("FLUTTER_STORAGE_BASE_URL", NULL); } @@ -55,28 +55,9 @@ pl_dart_flutter_setsrc (char *option) { chsrc_use_this_source (pl_dart_flutter); - char *w = NULL; - char *cmd = NULL; - if (xy.on_windows) - { - cmd = xy_strcat (3, "setx FLUTTER_STORAGE_BASE_URL \"", source.url, "\""); - chsrc_run (cmd, RunOpt_No_Last_New_Line); - } - else - { - char *zshrc = xy_zshrc; - char *bashrc = xy_bashrc; - - chsrc_backup (zshrc); - w = xy_strcat (3, "export FLUTTER_STORAGE_BASE_URL=\"", source.url, "\"\n"); - chsrc_append_to_file (w, zshrc); - - if (xy_file_exist (bashrc)) - { - chsrc_backup (bashrc); - chsrc_append_to_file (w, bashrc); - } - } + char *url = hp_remove_trailing_slash (source.url); + chsrc_set_env ("FLUTTER_STORAGE_BASE_URL", url); + free (url); chsrc_determine_chgtype (ChgType_Auto); chsrc_conclude (&source); @@ -86,5 +67,8 @@ pl_dart_flutter_setsrc (char *option) void pl_dart_flutter_resetsrc (char *option) { - pl_dart_flutter_setsrc (option); + chsrc_set_env ("FLUTTER_STORAGE_BASE_URL", NULL); + + chsrc_determine_chgtype (ChgType_Reset); + chsrc_conclude (NULL); } diff --git a/src/recipe/lang/Dart/Pub.c b/src/recipe/lang/Dart/Pub.c index dff5e1014..fa7b542c2 100644 --- a/src/recipe/lang/Dart/Pub.c +++ b/src/recipe/lang/Dart/Pub.c @@ -10,25 +10,25 @@ pl_dart_prepare (void) chef_prep_this_dish (pl_dart, gsr); chef_set_recipe_created_on (this, "2023-09-10"); - chef_set_recipe_last_updated (this, "2025-07-11"); + chef_set_recipe_last_updated (this, "2026-10-28"); chef_set_chefs (this, 1, "@ccmywish"); - chef_set_sauciers (this, 2, "@czyt", "@xrgzs"); + chef_set_sauciers (this, 3, "@czyt", "@xrgzs", "@Mikachu2333"); - chef_set_scope_cap (this, ProjectScope, ScopeCap_Unknown); + chef_set_scope_cap (this, ProjectScope, ScopeCap_Unable); chef_set_scope_cap (this, UserScope, ScopeCap_Able_And_Implemented); - chef_set_scope_cap (this, SystemScope, ScopeCap_Unknown); + chef_set_scope_cap (this, SystemScope, ScopeCap_Able_And_Implemented); chef_set_default_scope (this, UserScope); - chef_allow_english(this); - chef_allow_user_define(this); + chef_allow_english (this); + chef_allow_user_define (this); def_sources_begin() - {&UpstreamProvider, "https://pub.dev", FeedByPrepare}, - {&FlutterCN, "https://pub.flutter-io.cn", FeedByPrepare}, - {&Sjtug_Zhiyuan, "https://mirror.sjtu.edu.cn/dart-pub", FeedByPrepare}, + {&UpstreamProvider, "https://pub.dev", FeedByPrepare}, + {&FlutterCN, "https://pub.flutter-io.cn", FeedByPrepare}, + {&Sjtug_Zhiyuan, "https://mirror.sjtu.edu.cn/dart-pub", FeedByPrepare}, {&Tuna, "https://mirrors.tuna.tsinghua.edu.cn/dart-pub", FeedByPrepare}, - {&Nju, "https://mirror.nju.edu.cn/dart-pub", FeedByPrepare} + {&Nju, "https://mirror.nju.edu.cn/dart-pub", FeedByPrepare} def_sources_end() chef_set_rest_smURL_with_postfix (this, "/packages/flutter_vision/versions/1.1.4.tar.gz"); @@ -38,9 +38,10 @@ pl_dart_prepare (void) void pl_dart_getsrc (char *option) { - chsrc_view_env ("PUB_HOSTED_URL", NULL); + chsrc_get_env ("PUB_HOSTED_URL", NULL); } + /** * @consult https://mirrors.tuna.tsinghua.edu.cn/help/dart-pub/ * @consult https://mirror.sjtu.edu.cn/docs/dart-pub @@ -51,29 +52,9 @@ pl_dart_setsrc (char *option) { chsrc_use_this_source (pl_dart); - char *w = NULL; - char *cmd = NULL; - if (xy.on_windows) - { - cmd = xy_strcat (3, "setx PUB_HOSTED_URL \"", source.url, "\""); - chsrc_run (cmd, RunOpt_No_Last_New_Line); - } - else - { - char *zshrc = xy_zshrc; - char *bashrc = xy_bashrc; - - chsrc_backup (zshrc); - w = xy_strcat (3, "export PUB_HOSTED_URL=\"", source.url, "\"\n"); - - chsrc_append_to_file (w, zshrc); - - if (xy_file_exist (bashrc)) - { - chsrc_backup (bashrc); - chsrc_append_to_file (w, bashrc); - } - } + char *url = hp_remove_trailing_slash (source.url); + chsrc_set_env ("PUB_HOSTED_URL", url); + free (url); chsrc_determine_chgtype (ChgType_Auto); chsrc_conclude (&source); @@ -83,5 +64,8 @@ pl_dart_setsrc (char *option) void pl_dart_resetsrc (char *option) { - pl_dart_setsrc (option); + chsrc_set_env ("PUB_HOSTED_URL", NULL); + + chsrc_determine_chgtype (ChgType_Reset); + chsrc_conclude (NULL); } diff --git a/src/recipe/lang/JavaScript/nvm.c b/src/recipe/lang/JavaScript/nvm.c index ca642a588..ac08f935b 100644 --- a/src/recipe/lang/JavaScript/nvm.c +++ b/src/recipe/lang/JavaScript/nvm.c @@ -4,26 +4,41 @@ def_dish(pl_nvm, "nvm"); +static void +pl_nvm_check_platform (void) +{ + if (xy.on_windows) + { + char *msg = ENGLISH + ? "Official nvm (nvm-sh) is only supported on POSIX systems; on Windows (nvm-windows), use `nvm node_mirror ` instead" + : "官方 nvm (nvm-sh) 仅支持 POSIX 环境;Windows 下的 nvm-windows 机制不同,请使用 `nvm node_mirror ` 命令"; + chsrc_error (msg); + exit (Exit_Unsupported); + } +} + void pl_nvm_prepare (void) { chef_prep_this_dish (pl_nvm, gsr); chef_set_recipe_created_on (this, "2024-09-23"); - chef_set_recipe_last_updated (this, "2025-06-19"); + chef_set_recipe_last_updated (this, "2026-10-28"); chef_set_chefs (this, 1, "@ccmywish"); - chef_set_sauciers (this, 0); + chef_set_sauciers (this, 1, "@Mikachu2333"); - chef_set_scope_cap (this, ProjectScope, ScopeCap_Unknown); + chef_set_scope_cap (this, ProjectScope, ScopeCap_Unable); chef_set_scope_cap (this, UserScope, ScopeCap_Able_And_Implemented); - chef_set_scope_cap (this, SystemScope, ScopeCap_Unable); + chef_set_scope_cap (this, SystemScope, ScopeCap_Able_And_Implemented); chef_set_default_scope (this, UserScope); - chef_allow_english(this); - chef_allow_user_define(this); + chef_allow_english (this); + chef_allow_user_define (this); - chef_set_note (this, "nvm 不支持 Fish shell", "nvm does not support Fish"); + chef_set_note (this, + "官方 nvm (nvm-sh) 原生不支持 Fish,Fish 下需配合 bass 或 nvm.fish 等插件使用;Windows 下的 nvm-windows 机制不同,暂不支持", + "Official nvm (nvm-sh) does not natively support Fish (use with bass or nvm.fish); nvm-windows on Windows uses a different mechanism and is unsupported"); chef_use_other_dish_sources (this, &pl_nodejs_binary_dish); } @@ -32,7 +47,8 @@ pl_nvm_prepare (void) void pl_nvm_getsrc (char *option) { - chsrc_view_env ("NVM_NODEJS_ORG_MIRROR", NULL); + pl_nvm_check_platform (); + chsrc_get_env ("NVM_NODEJS_ORG_MIRROR", NULL); } @@ -40,23 +56,17 @@ pl_nvm_getsrc (char *option) * @consult https://github.com/nvm-sh/nvm?tab=readme-ov-file#use-a-mirror-of-node-binaries * @consult https://mirrors.tuna.tsinghua.edu.cn/help/nodejs-release/ * @issue https://github.com/RubyMetric/chsrc/issues/81 - * - * @note nvm 不支持 Fish */ void pl_nvm_setsrc (char *option) { - Source_t source = chsrc_yield_source_and_confirm (&pl_nodejs_binary_dish, option); - - char *w = xy_strcat (3, "export NVM_NODEJS_ORG_MIRROR=", source.url, "\n"); + pl_nvm_check_platform (); - char *zshrc = xy_zshrc; - char *bashrc = xy_bashrc; - - chsrc_append_to_file (w, bashrc); + Source_t source = chsrc_yield_source_and_confirm (&pl_nodejs_binary_dish, option); - if (xy_file_exist (zshrc)) - chsrc_append_to_file (w, zshrc); + char *url = hp_remove_trailing_slash (source.url); + chsrc_set_env ("NVM_NODEJS_ORG_MIRROR", url); + free (url); chsrc_determine_chgtype (ChgType_Auto); chsrc_conclude (&source); @@ -66,7 +76,10 @@ pl_nvm_setsrc (char *option) void pl_nvm_resetsrc (char *option) { - // pl_nvm_setsrc (ChgType_Reset); - chsrc_error ("暂不支持对 nvm 重置"); - exit (Exit_Unsupported); + pl_nvm_check_platform (); + + chsrc_set_env ("NVM_NODEJS_ORG_MIRROR", NULL); + + chsrc_determine_chgtype (ChgType_Reset); + chsrc_conclude (NULL); } diff --git a/src/recipe/lang/Rust/rustup.c b/src/recipe/lang/Rust/rustup.c index 1418951a8..a676a8ace 100644 --- a/src/recipe/lang/Rust/rustup.c +++ b/src/recipe/lang/Rust/rustup.c @@ -13,18 +13,18 @@ pl_rust_rustup_prepare (void) chef_prep_this_dish (pl_rust_rustup, gsr); chef_set_recipe_created_on (this, "2024-10-02"); - chef_set_recipe_last_updated (this, "2026-09-06"); + chef_set_recipe_last_updated (this, "2026-10-28"); chef_set_chefs (this, 1, "@ccmywish"); chef_set_sauciers (this, 2, "@Yangmoooo", "@Mikachu2333"); chef_set_scope_cap (this, ProjectScope, ScopeCap_Unable); chef_set_scope_cap (this, UserScope, ScopeCap_Able_And_Implemented); - chef_set_scope_cap (this, SystemScope, ScopeCap_Unable); + chef_set_scope_cap (this, SystemScope, ScopeCap_Able_And_Implemented); chef_set_default_scope (this, UserScope); - chef_deny_english (this); - chef_allow_user_define(this); + chef_allow_english (this); + chef_allow_user_define (this); def_sources_begin() {&UpstreamProvider, "https://static.rust-lang.org", FeedByPrepare}, @@ -39,7 +39,6 @@ pl_rust_rustup_prepare (void) //{&Iscas, "https://mirror.iscas.ac.cn/rustup", FeedByPrepare}, //{&Sjtug_Zhiyuan, "https://mirror.sjtu.edu.cn/rust-static", FeedByPrepare}, //{&Ali, "https://mirrors.aliyun.com/rustup", FeedByPrepare}, - def_sources_end() // 40MB大小 @@ -47,11 +46,10 @@ pl_rust_rustup_prepare (void) } - void pl_rust_rustup_getsrc (char *option) { - chsrc_view_env ("RUSTUP_UPDATE_ROOT", "RUSTUP_DIST_SERVER", NULL); + chsrc_get_env ("RUSTUP_UPDATE_ROOT", "RUSTUP_DIST_SERVER", NULL); } @@ -63,57 +61,30 @@ pl_rust_rustup_setsrc (char *option) { chsrc_use_this_source (pl_rust_rustup); -#ifdef XY_Build_On_Windows - - char *cmd1 = xy_strcat (3, "setx RUSTUP_DIST_SERVER \"", source.url, "\""); - char *cmd2 = xy_strcat (3, "setx RUSTUP_UPDATE_ROOT \"", source.url, "/rustup\""); - - char *cmd = xy_strcat (3, cmd1, " & ", cmd2); - chsrc_run (cmd, RunOpt_Dont_Notify_On_Success|RunOpt_No_Last_New_Line); - -#else - - char *w1 = xy_strcat (3, "export RUSTUP_DIST_SERVER=\"", source.url, "\"\n"); - char *w2 = xy_strcat (3, "export RUSTUP_UPDATE_ROOT=\"", source.url, "/rustup\"\n"); + char *dist_server = hp_remove_trailing_slash (source.url); + char *update_root = xy_2strcat (dist_server, "/rustup"); - char *w = xy_2strcat (w1, w2); + chsrc_set_env_block ( + "RUSTUP_DIST_SERVER", dist_server, + "RUSTUP_UPDATE_ROOT", update_root, + NULL); - char *bashrc = xy_bashrc; - if (xy_file_exist (bashrc)) - { - chsrc_backup (bashrc); - chsrc_append_to_file (w, bashrc); - } - - char *zshrc = xy_zshrc; - if (xy_file_exist (zshrc)) - { - chsrc_backup (zshrc); - chsrc_append_to_file (w, zshrc); - } - - char *fishrc = xy_fishrc; - if (xy_file_exist (fishrc)) - { - char *w1 = xy_strcat (3, "set -x RUSTUP_DIST_SERVER ", source.url, "\n"); - char *w2 = xy_strcat (3, "set -x RUSTUP_UPDATE_ROOT ", source.url, "/rustup\n"); - - char *w = xy_2strcat (w1, w2); - - chsrc_backup (fishrc); - chsrc_append_to_file (w, fishrc); - } -#endif + free (update_root); + free (dist_server); chsrc_determine_chgtype (ChgType_Auto); chsrc_conclude (&source); - chsrc_alert2 ("请重启终端使rustup环境变量生效"); } - void pl_rust_rustup_resetsrc (char *option) { - pl_rust_rustup_setsrc (option); + chsrc_set_env_block ( + "RUSTUP_DIST_SERVER", NULL, + "RUSTUP_UPDATE_ROOT", NULL, + NULL); + + chsrc_determine_chgtype (ChgType_Reset); + chsrc_conclude (NULL); } diff --git a/src/recipe/recipe-template.c b/src/recipe/recipe-template.c index dee6f96f7..7d9b20a43 100644 --- a/src/recipe/recipe-template.c +++ b/src/recipe/recipe-template.c @@ -6,7 +6,7 @@ * Contributors : @Mikachu2333 * | * Created On : <2024-08-09> - * Last Modified : <2026-08-14> + * Last Modified : <2026-10-28> * ------------------------------------------------------------- * 本文件作为一个通用模板: * @@ -68,8 +68,10 @@ void // 做了贡献?将自己的信息加在这里! chef_set_sauciers (this, 2, "@nulnone", "@someone"); + /* 可选枚举: ScopeCap_Unknown | ScopeCap_Unable | ScopeCap_Able_But_Not_Implemented + * ScopeCap_Able_But_Just_Docs | ScopeCap_Able_And_Implemented */ chef_set_scope_cap (this, ProjectScope, ScopeCap_Able_And_Implemented); - chef_set_scope_cap (this, UserScope, ScopeCap_Able_But_NotImplemented); + chef_set_scope_cap (this, UserScope, ScopeCap_Able_But_Not_Implemented); chef_set_scope_cap (this, SystemScope, ScopeCap_Unable); chef_set_default_scope (this, UserScope); @@ -100,6 +102,7 @@ void __getsrc (char *option) { // chsrc get + // 若为环境变量型 recipe,可调用: chsrc_get_env ("VAR_NAME", NULL); } @@ -118,7 +121,7 @@ void /* 你可能想要指定不同的 dish 来使用它的源 */ // Source_t source = chsrc_yield_source_and_confirm (&pl_pypi_dish, option); - /* 具体的换源步骤,如调用第三方命令... */ + /* 具体的换源步骤,如调用第三方命令,或使用 chsrc_set_env / chsrc_set_env_block 持久化环境变量... */ /* 最后总结输出 */ chsrc_determine_chgtype (ChgType_xxx); diff --git a/src/recipe/ware/Homebrew/Homebrew.c b/src/recipe/ware/Homebrew/Homebrew.c index 010b80c1b..42dcdb261 100644 --- a/src/recipe/ware/Homebrew/Homebrew.c +++ b/src/recipe/ware/Homebrew/Homebrew.c @@ -2,38 +2,50 @@ * SPDX-License-Identifier: GPL-3.0-or-later * ------------------------------------------------------------*/ -#include "rawstr4c.h" - def_dish(wr_homebrew, "brew/homebrew"); +static void +wr_homebrew_check_platform (void) +{ + if (xy.on_windows) + { + char *msg = ENGLISH + ? "Homebrew is only supported on macOS and Linux, not native Windows" + : "Homebrew 仅支持 macOS 与 Linux,不支持原生 Windows"; + chsrc_error (msg); + exit (Exit_Unsupported); + } +} + void -wr_homebrew_prepare () +wr_homebrew_prepare (void) { - chef_prep_this_dish (wr_homebrew, gs); + chef_prep_this_dish (wr_homebrew, gsr); chef_set_recipe_created_on (this, "2023-09-10"); - chef_set_recipe_last_updated (this, "2025-09-12"); + chef_set_recipe_last_updated (this, "2026-10-28"); chef_set_chefs (this, 1, "@ccmywish"); - chef_set_sauciers (this, 1, "@Word2VecT"); + chef_set_sauciers (this, 2, "@Word2VecT", "@Mikachu2333"); chef_set_scope_cap (this, ProjectScope, ScopeCap_Unable); chef_set_scope_cap (this, UserScope, ScopeCap_Able_And_Implemented); - chef_set_scope_cap (this, SystemScope, ScopeCap_Unable); + chef_set_scope_cap (this, SystemScope, ScopeCap_Able_And_Implemented); chef_set_default_scope (this, UserScope); - chef_allow_english(this); - chef_deny_user_define(this); + chef_allow_english (this); + chef_deny_user_define (this); - chef_set_note (this, "该换源通过写入环境变量实现,若多次换源,请手动清理profile文件", - "This source switching is implemented by writing environment variables. If switching sources multiple times, please manually clean the profile file"); + chef_set_note (this, + "Homebrew 仅支持 macOS 与 Linux,通过持久化环境变量实现幂等换源", + "Homebrew is supported on macOS and Linux only, configured idempotently via persistent environment variables"); def_sources_begin() - {&UpstreamProvider, "https://github.com/Homebrew/brew.git", DelegateToUpstream}, + {&UpstreamProvider, "https://github.com/Homebrew/brew.git", DelegateToUpstream}, {&Tuna, "https://mirrors.tuna.tsinghua.edu.cn/", DelegateToMirror}, - {&Bfsu, "https://mirrors.bfsu.edu.cn/", DelegateToMirror}, - {&Nju, "https://mirror.nju.edu.cn/", DelegateToMirror}, - {&Nyist, "https://mirror.nyist.edu.cn/", DelegateToMirror} + {&Bfsu, "https://mirrors.bfsu.edu.cn/", DelegateToMirror}, + {&Nju, "https://mirror.nju.edu.cn/", DelegateToMirror}, + {&Nyist, "https://mirror.nyist.edu.cn/", DelegateToMirror} def_sources_end() } @@ -41,44 +53,78 @@ wr_homebrew_prepare () void wr_homebrew_getsrc (char *option) { - int status = xy_run_get_status (RAWSTR_wr_homebrew_read_config_cmd); - if (status != 0) { xy_noop(); } + wr_homebrew_check_platform (); + chsrc_get_env ( + "HOMEBREW_API_DOMAIN", + "HOMEBREW_BOTTLE_DOMAIN", + "HOMEBREW_BREW_GIT_REMOTE", + "HOMEBREW_CORE_GIT_REMOTE", + NULL); } + /** * @consult https://mirrors.tuna.tsinghua.edu.cn/help/homebrew/ * - * 自brew 4.0.0 (2023 年 2 月 16日) 起, + * 自 brew 4.0.0 (2023 年 2 月 16 日) 起, * HOMEBREW_INSTALL_FROM_API 会成为默认行为,无需设置。大部分用户无需再克隆 homebrew-core 仓库,故无需设置 HOMEBREW_CORE_GIT_REMOTE 环境变量; * 但是为了以防万一,我们还是为用户设置该环境变量 */ void wr_homebrew_setsrc (char *option) { - chsrc_use_this_source (wr_homebrew); - - char *w = xy_str_gsub (RAWSTR_wr_homebrew_config_in_bash, "@1@", source.url); + wr_homebrew_check_platform (); - char *zshrc = xy_zshrc; - chsrc_backup (zshrc); - chsrc_append_to_file (w, zshrc); + chsrc_use_this_source (wr_homebrew); - char *bashrc = xy_bashrc; - if (xy_file_exist (bashrc)) + if (source_is_upstream (&source)) { - chsrc_backup (bashrc); - chsrc_append_to_file (w, bashrc); + chsrc_set_env_block ( + "HOMEBREW_BREW_GIT_REMOTE", NULL, + "HOMEBREW_CORE_GIT_REMOTE", NULL, + "HOMEBREW_API_DOMAIN", NULL, + "HOMEBREW_BOTTLE_DOMAIN", NULL, + NULL); } - - char *fishrc = xy_fishrc; - if (xy_file_exist (fishrc)) + else { - char *w = xy_str_gsub (RAWSTR_wr_homebrew_config_in_fish, "@1@", source.url); - chsrc_backup (fishrc); - chsrc_append_to_file (w, fishrc); + char *base = hp_remove_trailing_slash (source.url); + char *brew_git = xy_2strcat (base, "/git/homebrew/brew.git"); + char *core_git = xy_2strcat (base, "/git/homebrew/homebrew-core.git"); + char *api_domain = xy_2strcat (base, "/homebrew-bottles/api"); + char *bottle_dom = xy_2strcat (base, "/homebrew-bottles"); + + chsrc_set_env_block ( + "HOMEBREW_BREW_GIT_REMOTE", brew_git, + "HOMEBREW_CORE_GIT_REMOTE", core_git, + "HOMEBREW_API_DOMAIN", api_domain, + "HOMEBREW_BOTTLE_DOMAIN", bottle_dom, + NULL); + + free (bottle_dom); + free (api_domain); + free (core_git); + free (brew_git); + free (base); } chsrc_determine_chgtype (ChgType_Auto); chsrc_conclude (&source); - chsrc_alert2 ("请重启终端使Homebrew环境变量生效"); +} + + +void +wr_homebrew_resetsrc (char *option) +{ + wr_homebrew_check_platform (); + + chsrc_set_env_block ( + "HOMEBREW_BREW_GIT_REMOTE", NULL, + "HOMEBREW_CORE_GIT_REMOTE", NULL, + "HOMEBREW_API_DOMAIN", NULL, + "HOMEBREW_BOTTLE_DOMAIN", NULL, + NULL); + + chsrc_determine_chgtype (ChgType_Reset); + chsrc_conclude (NULL); } diff --git a/src/recipe/ware/Homebrew/rawstr4c.h b/src/recipe/ware/Homebrew/rawstr4c.h deleted file mode 100644 index 68c435bca..000000000 --- a/src/recipe/ware/Homebrew/rawstr4c.h +++ /dev/null @@ -1,12 +0,0 @@ -#pragma once - -/** - * Generated by rawstr4c v1.0.0-2025/08/09 - */ - -char RAWSTR_wr_homebrew_read_config_cmd[] = "echo HOMEBREW_API_DOMAIN=$HOMEBREW_API_DOMAIN;\necho HOMEBREW_BOTTLE_DOMAIN=$HOMEBREW_BOTTLE_DOMAIN;\necho HOMEBREW_BREW_GIT_REMOTE=$HOMEBREW_BREW_GIT_REMOTE;\necho HOMEBREW_CORE_GIT_REMOTE=$HOMEBREW_CORE_GIT_REMOTE;"; - -char RAWSTR_wr_homebrew_config_in_bash[] = "# ------ chsrc BLOCK BEGIN for Homebrew ------\nexport HOMEBREW_BREW_GIT_REMOTE=\"@1@/git/homebrew/brew.git\"\nexport HOMEBREW_CORE_GIT_REMOTE=\"@1@/git/homebrew/homebrew-core.git\"\n# For Bottles\nexport HOMEBREW_API_DOMAIN=\"@1@/homebrew-bottles/api\"\nexport HOMEBREW_BOTTLE_DOMAIN=\"@1@/homebrew-bottles\"\n# ------ chsrc BLOCK ENDIN for Homebrew ------"; - -char RAWSTR_wr_homebrew_config_in_fish[] = "# ------ chsrc BLOCK BEGIN for Homebrew ------\nset -x HOMEBREW_BREW_GIT_REMOTE \"@1@/git/homebrew/brew.git\"\nset -x HOMEBREW_CORE_GIT_REMOTE \"@1@/git/homebrew/homebrew-core.git\"\n# For Bottles\nset -x HOMEBREW_API_DOMAIN \"@1@/homebrew-bottles/api\"\nset -x HOMEBREW_BOTTLE_DOMAIN \"@1@/homebrew-bottles\"\n# ------ chsrc BLOCK ENDIN for Homebrew ------"; - diff --git a/src/recipe/ware/Homebrew/rawstr4c.md b/src/recipe/ware/Homebrew/rawstr4c.md deleted file mode 100644 index 80d0f4a5d..000000000 --- a/src/recipe/ware/Homebrew/rawstr4c.md +++ /dev/null @@ -1,78 +0,0 @@ - - -# rawstr4c input - -该文件尽可能测试 `rawstr4c` 的多个功能,以下三个变量的输出,将分别为: - -1. `RAWSTR_wr_homebrew_read_config_cmd` -2. `RAWSTR_wr_homebrew_config_in_bash` -3. `RAWSTR_wr_homebrew_config_in_fish` - -若不同,则生成有问题。 - -- prefix = `RAWSTR_wr_homebrew` -- postfix = `:use-language` - -- translate = `:escape` -- output = `:global-variable-only-header` - -## Homebrew - -### read user env - -下面的命令对于 `Bash`、`Zsh` 和 `Fish` 都适用 - -- name = `RAWSTR_wr_homebrew_read_config_cmd` -- name-literally = `true` - -测试一下 name-literally 功能 - -```bash -echo HOMEBREW_API_DOMAIN=$HOMEBREW_API_DOMAIN; -echo HOMEBREW_BOTTLE_DOMAIN=$HOMEBREW_BOTTLE_DOMAIN; -echo HOMEBREW_BREW_GIT_REMOTE=$HOMEBREW_BREW_GIT_REMOTE; -echo HOMEBREW_CORE_GIT_REMOTE=$HOMEBREW_CORE_GIT_REMOTE; -``` - -### Bash config - -- no-prefix = `false` -- no-postfix = `true` -- name = `config_in_bash` - -测试一下 no-postfix 功能 - -```bash -# ------ chsrc BLOCK BEGIN for Homebrew ------ -export HOMEBREW_BREW_GIT_REMOTE="@1@/git/homebrew/brew.git" -export HOMEBREW_CORE_GIT_REMOTE="@1@/git/homebrew/homebrew-core.git" -# For Bottles -export HOMEBREW_API_DOMAIN="@1@/homebrew-bottles/api" -export HOMEBREW_BOTTLE_DOMAIN="@1@/homebrew-bottles" -# ------ chsrc BLOCK ENDIN for Homebrew ------ -``` - -### Fish config - -- name = `config` -- language = `fish` - -`Fish` 没有高亮,所以我们这里的 code block 用 `bash` 指示 ,我们刚好测试一下 language 功能 - -```bash -# ------ chsrc BLOCK BEGIN for Homebrew ------ -set -x HOMEBREW_BREW_GIT_REMOTE "@1@/git/homebrew/brew.git" -set -x HOMEBREW_CORE_GIT_REMOTE "@1@/git/homebrew/homebrew-core.git" -# For Bottles -set -x HOMEBREW_API_DOMAIN "@1@/homebrew-bottles/api" -set -x HOMEBREW_BOTTLE_DOMAIN "@1@/homebrew-bottles" -# ------ chsrc BLOCK ENDIN for Homebrew ------ -``` diff --git a/test/cli.pl b/test/cli.pl index 2f47b243d..691174293 100644 --- a/test/cli.pl +++ b/test/cli.pl @@ -2,9 +2,9 @@ # --------------------------------------------------------------- # Test File : cli.pl # Test Authors : @ccmywish -# Contributors : Nil Null +# Contributors : @Mikachu2333 # Created On : <2024-06-05> -# Last Modified : <2026-08-20> +# Last Modified : <2026-10-28> # # 测试 chsrc 可执行文件 # @@ -42,6 +42,7 @@ use v5.42; use utf8; # v5.38 还不用强制 utf8,现在代码里(包括注释)只要有其他字符,都必须强制使用 utf8 +use File::Temp qw(tempdir); use Test::More; # `` 执行命令以后,返回的全部是字节流,而不是字符串, # chsrc 已经输出的是 UTF-8 字符串了,所以我们在这里需要 @@ -75,6 +76,7 @@ like `$CHSRC -v -en`, $version_str_en, 'chsrc -v -en'; like `$CHSRC -en -v`, $version_str_en, 'chsrc -en -v'; like `$CHSRC --version -en`, $version_str_en, 'chsrc --version -en'; +like `$CHSRC -en --version`, $version_str_en, 'chsrc -en --version'; like `$CHSRC version -en`, $version_str_en, 'chsrc version -en'; my $help_str = qr/^ (help|list|get|set|reset)/m; @@ -141,6 +143,102 @@ like `$CHSRC list -no-color js`, $list_js, 'chsrc list -no-color js'; +=begin comment + +测试 -scope 参数校验与环境变量型 recipes (rustup / pub / flutter / nvm / brew) + +=end comment +=cut + +like `$CHSRC set -scope 2>&1`, qr/缺少参数/, 'chsrc set -scope missing arg'; +like `$CHSRC set -scope=project rustup 2>&1`, qr/不支持项目级换源/, 'chsrc set -scope=project rustup rejected'; +like `$CHSRC reset -scope=project rustup 2>&1`,qr/不支持项目级换源/, 'chsrc reset -scope=project rustup rejected'; +like `$CHSRC ls -en -no-color rustup`, qr/system scope.*supported and implemented/, 'chsrc ls -en rustup shows system scope'; + +if ($^O ne 'MSWin32') { + my $tmp_home = tempdir (CLEANUP => 1); + local $ENV{HOME} = $tmp_home; + + # 未知 shell 精确拒绝执行 + { + local $ENV{SHELL} = '/usr/bin/nu'; + like `$CHSRC set rustup tuna 2>&1`, qr/无法精确确认当前 Shell 类型/, 'unknown shell set rejected'; + like `$CHSRC get rustup 2>&1`, qr/无法精确确认当前 Shell 类型/, 'unknown shell get rejected'; + } + + # Bash 下 5 个迁移 recipe 的幂等写入、无双斜杠、持久化查看与重置 + { + local $ENV{SHELL} = '/bin/bash'; + open my $fh, '>', "$tmp_home/.bashrc" or die $!; + print {$fh} "# user header\n"; + close $fh; + + like `$CHSRC set -en -no-color rustup tuna`, qr/Thanks to the mirror site.*TUNA/s, 'rustup set tuna'; + open my $rc1, '<', "$tmp_home/.bashrc" or die $!; + my $content1 = do { local $/; <$rc1> }; + close $rc1; + + # 重复执行幂等,文件内容与重启提示不重复产生 + my $out2 = `$CHSRC set -en -no-color rustup tuna`; + unlike $out2, qr/source ~\/\.bashrc/, 'rustup repeat set skips restart hint'; + open my $rc2, '<', "$tmp_home/.bashrc" or die $!; + my $content2 = do { local $/; <$rc2> }; + close $rc2; + is $content2, $content1, 'rustup repeat set is byte-for-byte idempotent'; + + like `$CHSRC get -no-color rustup`, qr|RUSTUP_DIST_SERVER=https://mirrors\.tuna\.tsinghua\.edu\.cn/rustup|, 'rustup get persisted'; + + # Homebrew URL 无双斜杠 + like `$CHSRC set -en -no-color brew tuna`, qr/Thanks to the mirror site.*TUNA/s, 'brew set tuna'; + my $brew_get = `$CHSRC get -no-color brew`; + like $brew_get, qr|HOMEBREW_BREW_GIT_REMOTE=https://mirrors\.tuna\.tsinghua\.edu\.cn/git/homebrew/brew\.git|, 'brew url clean single slash'; + unlike $brew_get, qr|tsinghua\.edu\.cn//|, 'brew url has no double slash'; + + # Pub / Flutter / nvm 基本闭环 + like `$CHSRC set -en -no-color pub tuna`, qr/TUNA/, 'pub set tuna'; + like `$CHSRC set -en -no-color flutter tuna`, qr/TUNA/, 'flutter set tuna'; + like `$CHSRC set -en -no-color nvm tuna`, qr/TUNA/, 'nvm set tuna'; + like `$CHSRC get -no-color nvm`, qr|NVM_NODEJS_ORG_MIRROR=https://mirrors\.tuna\.tsinghua\.edu\.cn/nodejs-release|, 'nvm get persisted'; + + # 危险字符注入拦截 + like `$CHSRC set rustup 'https://evil.org/\$inj' 2>&1`, qr/不安全字符/, 'dangerous char in custom url rejected'; + + # 重置全部变量并确认清空 + like `$CHSRC reset -en -no-color rustup`, qr/reset to the upstream/, 'rustup reset'; + like `$CHSRC reset -en -no-color brew`, qr/reset to the upstream/, 'brew reset'; + like `$CHSRC reset -en -no-color pub`, qr/reset to the upstream/, 'pub reset'; + like `$CHSRC reset -en -no-color flutter`, qr/reset to the upstream/, 'flutter reset'; + like `$CHSRC reset -en -no-color nvm`, qr/reset to the upstream/, 'nvm reset'; + # 断言"持久化已清空"时,同时清掉父进程可能继承来的同名变量, + # 否则 chsrc_get_env 的 getenv 回退会把会话值打印出来,使结果依赖运行环境 + { + local $ENV{RUSTUP_UPDATE_ROOT} = undef; + local $ENV{RUSTUP_DIST_SERVER} = undef; + like `$CHSRC get -no-color rustup`, qr/^RUSTUP_UPDATE_ROOT=\nRUSTUP_DIST_SERVER=$/m, 'rustup empty after reset'; + } + } + + # Fish 下 conf.d/chsrc.fish 写入与 config.fish 旧配置一次性清理 + { + local $ENV{SHELL} = '/usr/bin/fish'; + mkdir "$tmp_home/.config"; + mkdir "$tmp_home/.config/fish"; + open my $ffh, '>', "$tmp_home/.config/fish/config.fish" or die $!; + print {$ffh} "# keep comment\nset -x PUB_HOSTED_URL https://old.mirror\n"; + close $ffh; + + like `$CHSRC set -en -no-color pub tuna`, qr/TUNA/, 'fish pub set tuna'; + ok -f "$tmp_home/.config/fish/conf.d/chsrc.fish", 'fish conf.d/chsrc.fish created'; + + open my $oldf, '<', "$tmp_home/.config/fish/config.fish" or die $!; + my $old_fish_txt = do { local $/; <$oldf> }; + close $oldf; + unlike $old_fish_txt, qr/PUB_HOSTED_URL/, 'legacy PUB_HOSTED_URL cleaned from config.fish'; + like $old_fish_txt, qr/# keep comment/, 'unrelated comment preserved in config.fish'; + } +} + + diff --git a/test/fw.c b/test/fw.c index bcceb18b9..301975250 100644 --- a/test/fw.c +++ b/test/fw.c @@ -4,7 +4,7 @@ * File Authors : @ccmywish * Contributors : @Mikachu2333 * Created On : <2024-12-14> - * Last Modified : <2026-08-20> + * Last Modified : <2026-10-28> * * 该文件最好启用 DEBUG mode 编译 * ------------------------------------------------------------*/ @@ -13,6 +13,290 @@ #include "../src/framework/base.c" #include "../src/framework/OS.c" +#include "../src/framework/env.c" + +static void +test_env_utf16_and_reg_serializer (void) +{ + /* 1. UTF-8 <-> UTF-16LE 往返测试(覆盖 ASCII、中文与代理对 Emoji) */ + const char *sample = "C:\\Users\\张三\\Mirror🦀Test"; + uint16_t *u16 = NULL; + size_t u16_n = 0; + env_utf8_to_utf16le (sample, &u16, &u16_n); + assert (NULL != u16); + assert (u16_n > 0); + + char *roundtrip = env_utf16le_to_utf8 (u16, u16_n); + assert_str (roundtrip, sample); + free (roundtrip); + free (u16); + + /* 2. .reg UTF-16LE + BOM 序列化校验(覆盖 REG_SZ 转义、删除标记与 hex(2)) */ + unsigned char expand_raw[] = { '%', 0x00, 'T', 0x00, '%', 0x00, 0x00, 0x00 }; + EnvRegSnapshot_t snaps[3] = { + { "RUSTUP_DIST_SERVER", true, 1, "https://a.org/path\\\"q", NULL, 0 }, + { "NEW_ADDED_VAR", false, 0, NULL, NULL, 0 }, + { "EXPAND_VAR", true, 2, NULL, expand_raw, sizeof (expand_raw) } + }; + + size_t reg_len = 0; + unsigned char *reg_bytes = env_build_reg_backup_bytes ( + "HKEY_CURRENT_USER\\Environment", snaps, 3, ®_len); + + assert (reg_len >= 4); + assert (0xFF == reg_bytes[0] && 0xFE == reg_bytes[1]); + + size_t units = (reg_len - 2) / 2; + uint16_t *dec = xy_malloc0 ((units + 1) * sizeof (uint16_t)); + for (size_t i = 0; i < units; i++) + { + dec[i] = (uint16_t) reg_bytes[2 + i * 2] | ((uint16_t) reg_bytes[2 + i * 2 + 1] << 8); + } + char *reg_txt = env_utf16le_to_utf8 (dec, units); + free (dec); + free (reg_bytes); + + assert (NULL != strstr (reg_txt, "Windows Registry Editor Version 5.00\r\n\r\n[HKEY_CURRENT_USER\\Environment]\r\n")); + assert (NULL != strstr (reg_txt, "\"RUSTUP_DIST_SERVER\"=\"https://a.org/path\\\\\\\"q\"\r\n")); + assert (NULL != strstr (reg_txt, "\"NEW_ADDED_VAR\"=-\r\n")); + assert (NULL != strstr (reg_txt, "\"EXPAND_VAR\"=hex(2):25,00,54,00,25,00,00,00\r\n")); + free (reg_txt); +} + +#ifndef XY_Build_On_Windows +static void +test_env_posix_backend (void) +{ + char tmpl[] = "/tmp/chsrc_fw_env_XXXXXX"; + char *tmp_home = mkdtemp (tmpl); + assert (NULL != tmp_home); + + const char *orig_home = getenv ("HOME"); + char *saved_home = orig_home ? xy_strdup (orig_home) : NULL; + Scope_t saved_scope = ProgMode.Scope; + bool saved_dry = ProgMode.DryRunMode; + + setenv ("HOME", tmp_home, 1); + ProgMode.Scope = UserScope; + + /* 1. 变量名与危险字符校验 */ + Env_Test_Force_Shell = PosixShell_Bash; + char *err = NULL; + + EnvKV_t bad_name1 = { "1BAD", "ok" }; + assert (Env_BadName == env_apply_kvs (&bad_name1, 1, NULL, &err)); + free (err); err = NULL; + + EnvKV_t bad_name2 = { "BAD-NAME", "ok" }; + assert (Env_BadName == env_apply_kvs (&bad_name2, 1, NULL, &err)); + free (err); err = NULL; + + const char *bad_vals[] = { + "https://a.org/$(id)", + "https://a.org/`id`", + "https://a.org/\"inj", + "https://a.org/\\esc", + "https://a.org/\nexport X=1", + "https://a.org/\r" + }; + for (size_t i = 0; i < sizeof (bad_vals) / sizeof (bad_vals[0]); i++) + { + EnvKV_t bad_v = { "TEST_URL", bad_vals[i] }; + assert (Env_BadValue == env_apply_kvs (&bad_v, 1, NULL, &err)); + free (err); err = NULL; + } + + /* 2. ProjectScope 拒绝与非 root SystemScope 权限拒绝 */ + ProgMode.Scope = ProjectScope; + EnvKV_t ok_kv = { "TEST_URL", "https://example.org" }; + assert (Env_ScopeUnsupported == env_apply_kvs (&ok_kv, 1, NULL, &err)); + free (err); err = NULL; + + if (!chsrc_is_root ()) + { + ProgMode.Scope = SystemScope; + assert (Env_NoPermission == env_apply_kvs (&ok_kv, 1, NULL, &err)); + free (err); err = NULL; + } + ProgMode.Scope = UserScope; + + /* 3. 未知 Shell 在 set 与 query 时均精确拒绝,不污染目录 */ + Env_Test_Force_Shell = PosixShell_Unknown; + assert (Env_AmbiguousTarget == env_apply_kvs (&ok_kv, 1, NULL, &err)); + assert (NULL != err); + free (err); err = NULL; + + bool q_found = false; + char *q_val = NULL; + assert (Env_AmbiguousTarget == env_query_persisted ("TEST_URL", &q_found, &q_val, &err)); + free (err); err = NULL; + assert (!xy_file_exist ("~/.profile")); + + /* 4. Bash UserScope:保留注释、前缀边界隔离、重复行折叠、无尾换行补齐、幂等与删除 */ + Env_Test_Force_Shell = PosixShell_Bash; + char *bashrc_path = xy_normalize_path ("~/.bashrc"); + char *profile_path = xy_normalize_path ("~/.profile"); + + chsrc_overwrite_file ( + "# user comment\n" + "# ------ chsrc BLOCK BEGIN legacy ------\n" + "export RUSTUP_DIST_SERVER_EXTRA=\"keep_me\"\n" + "export RUSTUP_DIST_SERVER=\"https://old.one\"\n" + "export NVM_NODEJS_ORG_MIRROR=https://old.nvm\n" + "export RUSTUP_DIST_SERVER=\"https://old.duplicate\"\n" + "# ------ chsrc BLOCK ENDIN legacy ------", + bashrc_path); + + EnvKV_t rust_kvs[2] = { + { "RUSTUP_DIST_SERVER", "https://mirrors.tuna.tsinghua.edu.cn/rustup" }, + { "RUSTUP_UPDATE_ROOT", "https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup" } + }; + assert (Env_OK == env_apply_kvs (rust_kvs, 2, NULL, &err)); + assert (NULL == err); + assert (xy_file_exist (profile_path)); + assert (!xy_file_exist ("~/.bash_profile")); + + /* 第二次相同写入必须返回 Env_Unchanged */ + assert (Env_Unchanged == env_apply_kvs (rust_kvs, 2, NULL, &err)); + + assert (Env_OK == env_query_persisted ("RUSTUP_DIST_SERVER", &q_found, &q_val, &err)); + assert (q_found); + assert_str (q_val, "https://mirrors.tuna.tsinghua.edu.cn/rustup"); + free (q_val); q_val = NULL; + + assert (Env_OK == env_query_persisted ("RUSTUP_DIST_SERVER_EXTRA", &q_found, &q_val, &err)); + assert (q_found); + assert_str (q_val, "keep_me"); + free (q_val); q_val = NULL; + + char *bashrc_after = xy_file_read (bashrc_path); + assert (NULL != strstr (bashrc_after, "# user comment\n")); + assert (NULL != strstr (bashrc_after, "# ------ chsrc BLOCK BEGIN legacy ------\n")); + assert (NULL == strstr (bashrc_after, "old.duplicate")); + free (bashrc_after); + + /* Dry-run 不落盘 */ + ProgMode.DryRunMode = true; + EnvKV_t dry_kv = { "DRY_VAR", "https://dry.org" }; + assert (Env_OK == env_apply_kvs (&dry_kv, 1, NULL, &err)); + ProgMode.DryRunMode = false; + assert (Env_OK == env_query_persisted ("DRY_VAR", &q_found, &q_val, &err)); + assert (!q_found); + + /* 删除变量幂等验证 */ + EnvKV_t del_kvs[2] = { + { "RUSTUP_DIST_SERVER", NULL }, + { "RUSTUP_UPDATE_ROOT", NULL } + }; + assert (Env_OK == env_apply_kvs (del_kvs, 2, NULL, &err)); + assert (Env_Unchanged == env_apply_kvs (del_kvs, 2, NULL, &err)); + assert_str (xy_file_read (profile_path), ""); + + /* 5. 多文件不同值冲突检测 */ + chsrc_overwrite_file ("export CONFLICT_VAR=\"https://one\"\n", bashrc_path); + chsrc_overwrite_file ("export CONFLICT_VAR=\"https://two\"\n", profile_path); + assert (Env_AmbiguousTarget == env_query_persisted ("CONFLICT_VAR", &q_found, &q_val, &err)); + assert (NULL != err && NULL != strstr (err, ".bashrc") && NULL != strstr (err, ".profile")); + free (err); err = NULL; + + /* 6. 复杂动态语法行保护(带行号且零修改) */ + const char *complex_rc = "# line 1\nexport SAFE=\"1\"\nexport DYN_VAR=\"$HOME/mirror\"\n"; + chsrc_overwrite_file (complex_rc, bashrc_path); + chsrc_overwrite_file ("", profile_path); + EnvKV_t dyn_kv = { "DYN_VAR", "https://override.org" }; + assert (Env_AmbiguousTarget == env_apply_kvs (&dyn_kv, 1, NULL, &err)); + assert (NULL != err && NULL != strstr (err, ":3")); + free (err); err = NULL; + assert_str (xy_file_read (bashrc_path), complex_rc); + + /* 7. Fish conf.d/chsrc.fish 写入与 config.fish 一次性精确清理 */ + Env_Test_Force_Shell = PosixShell_Fish; + char *fish_cfg = xy_normalize_path ("~/.config/fish/config.fish"); + char *fish_conf = xy_normalize_path ("~/.config/fish/conf.d/chsrc.fish"); + + chsrc_overwrite_file ( + "# fish comment\n" + "# ------ chsrc BLOCK BEGIN for Homebrew ------\n" + "set -x RUSTUP_DIST_SERVER https://legacy.fish\n" + "set -gx PUB_HOSTED_URL \"https://legacy.pub\"\n" + "set -x MY_FISH_VAR \"stay_here\"\n", + fish_cfg); + + EnvKV_t fish_kvs[2] = { + { "RUSTUP_DIST_SERVER", "https://rsproxy.cn" }, + { "PUB_HOSTED_URL", "https://pub.flutter-io.cn" } + }; + assert (Env_OK == env_apply_kvs (fish_kvs, 2, NULL, &err)); + assert (Env_Unchanged == env_apply_kvs (fish_kvs, 2, NULL, &err)); + + char *fish_conf_txt = xy_file_read (fish_conf); + assert_str (fish_conf_txt, + "set -x RUSTUP_DIST_SERVER \"https://rsproxy.cn\"\n" + "set -x PUB_HOSTED_URL \"https://pub.flutter-io.cn\"\n"); + free (fish_conf_txt); + + char *fish_cfg_txt = xy_file_read (fish_cfg); + assert (NULL == strstr (fish_cfg_txt, "RUSTUP_DIST_SERVER")); + assert (NULL == strstr (fish_cfg_txt, "PUB_HOSTED_URL")); + assert (NULL != strstr (fish_cfg_txt, "MY_FISH_VAR")); + assert (NULL != strstr (fish_cfg_txt, "# ------ chsrc BLOCK BEGIN for Homebrew ------")); + free (fish_cfg_txt); + + /* Fish universal 变量 (-Ux) 视为复杂行拒绝覆写 */ + chsrc_overwrite_file ("set -Ux UNI_VAR \"abc\"\n", fish_conf); + EnvKV_t uni_kv = { "UNI_VAR", "def" }; + assert (Env_AmbiguousTarget == env_apply_kvs (&uni_kv, 1, NULL, &err)); + free (err); err = NULL; + + /* 8. /etc/environment 方言验证(通过 Env_Test_Etc_Env_Path 指向临时文件) */ + char *etc_mock = xy_2strcat (tmp_home, "/mock_etc_environment"); + Env_Test_Etc_Env_Path = etc_mock; + ProgMode.Scope = SystemScope; + + EnvKV_t etc_space_bad = { "PUB_HOSTED_URL", "https://has space.org" }; + assert (Env_BadValue == env_apply_kvs (&etc_space_bad, 1, NULL, &err)); + free (err); err = NULL; + + EnvKV_t etc_ok = { "PUB_HOSTED_URL", "https://pub.flutter-io.cn" }; + assert (Env_OK == env_apply_kvs (&etc_ok, 1, NULL, &err)); + assert (Env_Unchanged == env_apply_kvs (&etc_ok, 1, NULL, &err)); + assert_str (xy_file_read (etc_mock), "PUB_HOSTED_URL=https://pub.flutter-io.cn\n"); + + assert (Env_OK == env_query_persisted ("PUB_HOSTED_URL", &q_found, &q_val, &err)); + assert (q_found); + assert_str (q_val, "https://pub.flutter-io.cn"); + free (q_val); q_val = NULL; + + EnvKV_t etc_del = { "PUB_HOSTED_URL", NULL }; + assert (Env_OK == env_apply_kvs (&etc_del, 1, NULL, &err)); + assert_str (xy_file_read (etc_mock), ""); + + /* 清理临时目录并恢复全局状态 */ + Env_Test_Etc_Env_Path = NULL; + Env_Test_Force_Shell = -1; + ProgMode.Scope = saved_scope; + ProgMode.DryRunMode = saved_dry; + if (saved_home) + { + setenv ("HOME", saved_home, 1); + free (saved_home); + } + else + { + unsetenv ("HOME"); + } + + char *rm_cmd = xy_strcat (3, "rm -rf \"", tmp_home, "\""); + int st = system (rm_cmd); + if (st != 0) xy_noop (); + free (rm_cmd); + free (etc_mock); + free (fish_conf); + free (fish_cfg); + free (profile_path); + free (bashrc_path); +} +#endif int main (int argc, char const *argv[]) @@ -76,6 +360,9 @@ main (int argc, char const *argv[]) chsrc_overwrite_file ("Line999 \nLine998\nLine997\n", tmpfile); assert_str (xy_file_read (tmpfile), "Line999 \nLine998\nLine997\n"); + chsrc_overwrite_file ("", tmpfile); + assert_str (xy_file_read (tmpfile), ""); + chsrc_log (xy_2strcat ("CPU arch = ", chsrc_get_cpuarch ())); print ("chsrc: CPU cores = "); println (chsrc_get_cpucore ()); @@ -84,19 +371,21 @@ main (int argc, char const *argv[]) // chsrc_run_in_inline_pwsh_shell ("Write-Host \"Hello from inline PowerShell\""); - if (xy.on_windows) - { - chsrc_view_env ("OS", "windir", NULL); - chsrc_run_as_pwsh_file ("Write-Host \"Hello from PowerShell (v7) file\""); - chsrc_run_as_powershellv5_file ("Write-Host \"Hello from PowerShell v5 file\""); - chsrc_run_as_powershell_file ("Write-Host \"Hello from PowerShell file\""); - } - else - { - chsrc_view_env ("PWD", NULL); - chsrc_run_as_sh_file ("echo Hello from sh file"); - chsrc_run_as_bash_file ("echo Hello from Bash file"); - } +#ifdef XY_Build_On_Windows + chsrc_get_env ("OS", "windir", NULL); + chsrc_run_as_pwsh_file ("Write-Host \"Hello from PowerShell (v7) file\""); + chsrc_run_as_powershellv5_file ("Write-Host \"Hello from PowerShell v5 file\""); + chsrc_run_as_powershell_file ("Write-Host \"Hello from PowerShell file\""); +#else + Env_Test_Force_Shell = PosixShell_Bash; + chsrc_get_env ("PWD", NULL); + Env_Test_Force_Shell = -1; + chsrc_run_as_sh_file ("echo Hello from sh file"); + chsrc_run_as_bash_file ("echo Hello from Bash file"); + test_env_posix_backend (); +#endif + + test_env_utf16_and_reg_serializer (); chsrc_run ("echo " Chsrc_Version " test pass!", RunOpt_Dont_Notify_On_Success); return 0;