diff --git a/Makefile b/Makefile index 28629f02..ab0779eb 100755 --- a/Makefile +++ b/Makefile @@ -239,7 +239,7 @@ test-make-var: echo "HOME: $(HOME)"; \ fi -# 这两个测试文件都用 DEBUG mode +# 这些测试文件都用 DEBUG mode test-xy: CFLAGS += $(CFLAGS_debug) test-xy: @$(CC) test/xy.c $(CFLAGS) $(LDLIBS_win) -o xy diff --git a/justfile b/justfile index cb0e066c..8e174f7d 100644 --- a/justfile +++ b/justfile @@ -139,7 +139,7 @@ debug: build-in-debug-mode test: test-xy test-fw -# 这两个测试文件都用 DEBUG mode +# 这些测试文件都用 DEBUG mode test-xy: @{{CC}} test/xy.c {{CFLAGS_debug_mode}} -o xy @{{BIN_xy}} diff --git a/lib/xy.h b/lib/xy.h index bba59158..5bcdf6fc 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-11-11> * * * xy: 襄阳、咸阳 @@ -56,8 +56,11 @@ #include #include #include -#include -#include // opendir() closedir() +#ifndef _WIN32 + #include + #include // opendir() closedir() + #include +#endif #if defined(__STDC__) && __STDC_VERSION__ >= 202311 #define XY_Deprecate_This(msg) [[deprecated(msg)]] @@ -108,6 +111,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 @@ -962,44 +970,6 @@ xy_file_remove (const char *path) } -/** - * @brief 路径是否存在(文件或目录) - */ -static bool -xy_path_exist (const char *path) -{ -#ifdef XY_Build_On_Windows - wchar_t *wpath = xy_utf8_to_wstr (path); - bool exist = (NULL != wpath) && (INVALID_FILE_ATTRIBUTES != GetFileAttributesW (wpath)); - free (wpath); - return exist; -#else - return 0 == access (path, 0); /* F_OK */ -#endif -} - - -/** - * @brief 路径是否为目录 - */ -static bool -xy_path_is_dir (const char *path) -{ -#ifdef XY_Build_On_Windows - wchar_t *wpath = xy_utf8_to_wstr (path); - DWORD attr = wpath ? GetFileAttributesW (wpath) : INVALID_FILE_ATTRIBUTES; - free (wpath); - return (INVALID_FILE_ATTRIBUTES != attr) && (attr & FILE_ATTRIBUTE_DIRECTORY); -#else - DIR *d = opendir (path); - if (!d) - return false; - closedir (d); - return true; -#endif -} - - /** * @brief 执行命令 * @@ -1022,7 +992,7 @@ xy_system (const char *cmd) /** * @brief 以管道执行命令 * - * @return 同 popen(),需要以 pclose() 关闭 + * @return 同 popen(),需要以 xy_pclose() 关闭 */ static FILE * xy_popen (const char *cmd, const char *mode) @@ -1040,6 +1010,17 @@ xy_popen (const char *cmd, const char *mode) } +static int +xy_pclose (FILE *stream) +{ +#ifdef XY_Build_On_Windows + return _pclose (stream); +#else + return pclose (stream); +#endif +} + + /** * @brief 读取环境变量 * @@ -1362,7 +1343,7 @@ xy_run_iter_lines (const char *cmd, unsigned long n, bool (*func) (const char } free (buf); - pclose (stream); + xy_pclose (stream); return ret; } @@ -1438,7 +1419,7 @@ xy_run_get_stdout (const char *cmd, char **output) } buf[size] = '\0'; - int status = pclose (stream); + int status = xy_pclose (stream); if (output) *output = buf; @@ -1567,50 +1548,244 @@ _xy_win_powershellv5_profile () #define xy_bashrc "~/.bashrc" #define xy_fishrc "~/.config/fish/config.fish" +static char * +xy_normalize_path (const char *path); + + +enum _XyStat +{ + _XY_Path_Error, + _XY_Path_Missing, + _XY_Path_File, + _XY_Path_Dir, + _XY_Path_Other +}; + + +enum XyStatError +{ + XY_Stat_Ok, + XY_Stat_Not_Found, + XY_Stat_Not_Directory, + XY_Stat_Not_File, + XY_Stat_Invalid_Path, + XY_Stat_Access_Denied, + XY_Stat_Link_Loop, + XY_Stat_Name_Too_Long, + XY_Stat_IO_Error, + XY_Stat_Busy, + XY_Stat_Resource_Limit, + XY_Stat_Query_Error +}; + + +static enum XyStatError +_xy_stat_error (unsigned long code) +{ + switch (code) + { +#ifdef XY_Build_On_Windows + case ERROR_FILE_NOT_FOUND: + case ERROR_PATH_NOT_FOUND: return XY_Stat_Not_Found; + case ERROR_DIRECTORY: return XY_Stat_Not_Directory; + case ERROR_INVALID_NAME: + case ERROR_BAD_PATHNAME: + case ERROR_INVALID_PARAMETER: + case ERROR_NO_UNICODE_TRANSLATION: return XY_Stat_Invalid_Path; + case ERROR_ACCESS_DENIED: + case ERROR_PRIVILEGE_NOT_HELD: return XY_Stat_Access_Denied; + case ERROR_CANT_RESOLVE_FILENAME: return XY_Stat_Link_Loop; + case ERROR_FILENAME_EXCED_RANGE: return XY_Stat_Name_Too_Long; + case ERROR_IO_DEVICE: + case ERROR_READ_FAULT: + case ERROR_GEN_FAILURE: return XY_Stat_IO_Error; + case ERROR_SHARING_VIOLATION: + case ERROR_LOCK_VIOLATION: return XY_Stat_Busy; + case ERROR_NOT_ENOUGH_MEMORY: + case ERROR_OUTOFMEMORY: + case ERROR_TOO_MANY_OPEN_FILES: return XY_Stat_Resource_Limit; +#else + case ENOENT: return XY_Stat_Not_Found; + case ENOTDIR: return XY_Stat_Not_Directory; + case EINVAL: return XY_Stat_Invalid_Path; + case EACCES: + case EPERM: return XY_Stat_Access_Denied; + case ELOOP: return XY_Stat_Link_Loop; + case ENAMETOOLONG: return XY_Stat_Name_Too_Long; + case EIO: return XY_Stat_IO_Error; + case EBUSY: return XY_Stat_Busy; + case ENOMEM: + case EMFILE: + case ENFILE: return XY_Stat_Resource_Limit; +#endif + default: return XY_Stat_Query_Error; + } +} + + /** - * @note Windows上,`path` 不要夹带变量名,因为最终不会帮你转换 + * @internal 规范化路径后跟随链接查询目标类型,区分不存在与查询错误 * * @memory SAFE */ -static bool -xy_file_exist (const char *path) +static enum _XyStat +xy_stat (const char *path, enum XyStatError *error) { - char *expanded_path = NULL; - const char *check_path = path; + if (error) + *error = XY_Stat_Invalid_Path; + if (!path) + return _XY_Path_Error; - if (xy_str_start_with (path, "~")) + char *normalized_path = xy_normalize_path (path); + enum _XyStat result = _XY_Path_Error; + if (!normalized_path[0]) { - expanded_path = xy_2strcat (xy_os_home, path + 1); - check_path = expanded_path; + free (normalized_path); + return result; } - bool result = xy_path_exist (check_path); - if (expanded_path) free (expanded_path); + enum XyStatError status = XY_Stat_Ok; +#ifdef XY_Build_On_Windows + wchar_t *wpath = xy_utf8_to_wstr (normalized_path); + if (wpath) + { + /* 不请求读写权限;允许目录,且不使用 OPEN_REPARSE_POINT,以跟随链接和 junction。 */ + HANDLE handle = CreateFileW (wpath, 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL); + if (INVALID_HANDLE_VALUE == handle) + status = _xy_stat_error (GetLastError ()); + else + { + BY_HANDLE_FILE_INFORMATION info; + if (GetFileInformationByHandle (handle, &info)) + result = (info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) ? _XY_Path_Dir : _XY_Path_File; + else + { + status = _xy_stat_error (GetLastError ()); + SetLastError (ERROR_SUCCESS); + DWORD type = GetFileType (handle); + /* 磁盘元数据查询失败不能被后续成功的类型查询掩盖。 */ + if (FILE_TYPE_DISK != type && (FILE_TYPE_UNKNOWN != type || ERROR_SUCCESS == GetLastError ())) + { + result = _XY_Path_Other; + status = XY_Stat_Ok; + } + } + CloseHandle (handle); + } + free (wpath); + } + else + status = XY_Stat_Invalid_Path; +#else + struct stat info; + if (0 == stat (normalized_path, &info)) + { + if (S_ISDIR (info.st_mode)) + result = _XY_Path_Dir; + else if (S_ISREG (info.st_mode)) + result = _XY_Path_File; + else + result = _XY_Path_Other; + } + else + status = _xy_stat_error (errno); +#endif + + if (XY_Stat_Not_Found == status || XY_Stat_Not_Directory == status) + result = _XY_Path_Missing; + if (error) + *error = status; + free (normalized_path); return result; } + /** - * @note `xy_file_exist()` 和 `xy_dir_exist()` 两个函数在所有平台默认都支持使用 '~', - * 但实现中都没有调用 `xy_normalize_path()`,以防万一,调用前可能需要用户手动调用它 + * @brief 路径指向的对象是否存在,包括目录、普通文件及 socket 等特殊文件 + * + * @note 先调用 xy_normalize_path(),再跟随符号链接(Windows 上包括 junction)查询目标。 + * 断链、NULL 或规范化后的空路径返回 false。 + * 权限不足等查询错误也可能返回 false;需要区分时可直接调用 xy_stat()。 + * 本函数不保证目标可读、可写或可执行。 * * @memory SAFE */ static bool -xy_dir_exist (const char *path) +xy_path_exist (const char *path) { - char *allocated_dir = NULL; - const char *dir = path; + enum _XyStat type = xy_stat (path, NULL); + return _XY_Path_Error != type && _XY_Path_Missing != type; +} - if (xy_str_start_with (path, "~")) - { - allocated_dir = xy_2strcat (xy_os_home, path + 1); - dir = allocated_dir; - } - bool result = xy_path_is_dir (dir); +/** + * @brief 路径指向的对象是否为目录,仅判断类型,不检查目录能否读取或写入 + * + * @param[out] error 可传 NULL;返回 true 时置为 XY_Stat_Ok,否则返回具体的 XyStatError。 + * + * @note 先调用 xy_normalize_path(),再跟随符号链接(Windows 上包括 junction)查询目标。 + * 不存在、类型不符或查询出错时返回 false;类型和错误信息来自同一次查询。 + * + * @memory SAFE + */ +static bool +xy_stat_is_dir (const char *path, enum XyStatError *error) +{ + enum _XyStat type = xy_stat (path, error); + if (error && (_XY_Path_File == type || _XY_Path_Other == type)) + *error = XY_Stat_Not_Directory; + return _XY_Path_Dir == type; +} + - if (allocated_dir) free (allocated_dir); - return result; +/** + * @brief 路径指向的对象是否为普通文件,仅判断类型,不检查文件能否读取、写入或执行 + * + * @param[out] error 可传 NULL;错误语义同 xy_stat_is_dir()。 + * + * @note 先调用 xy_normalize_path(),再跟随符号链接查询目标。 + * 目录、socket、FIFO、设备等均不是普通文件。 + * 不存在、类型不符或查询出错时返回 false;类型和错误信息来自同一次查询。 + * + * @memory SAFE + */ +static bool +xy_stat_is_file (const char *path, enum XyStatError *error) +{ + enum _XyStat type = xy_stat (path, error); + if (error && (_XY_Path_Dir == type || _XY_Path_Other == type)) + *error = XY_Stat_Not_File; + return _XY_Path_File == type; +} + + +/** + * @brief xy_stat_is_file() 的语法糖,判断普通文件是否存在 + * + * @note 一次查询同时确认存在性和类型,先规范化路径并跟随链接;目录和特殊文件返回 false。 + * 权限不足等查询错误也可能返回 false;需要区分时可使用 xy_stat_is_file() 的 error 参数。 + * + * @memory SAFE + */ +static bool +xy_file_exist (const char *path) +{ + return xy_stat_is_file (path, NULL); +} + + +/** + * @brief xy_stat_is_dir() 的语法糖,先规范化路径并跟随链接,仅判断目标是否为目录 + * + * @note 权限不足等查询错误也可能返回 false;需要区分时可使用 xy_stat_is_dir() 的 error 参数。 + * + * @memory SAFE + */ +static bool +xy_dir_exist (const char *path) +{ + return xy_stat_is_dir (path, NULL); } /** @@ -1620,8 +1795,9 @@ xy_dir_exist (const char *path) * 1. 删除路径左右两边多出来的空白符 * - 防止通过间接方式得到的路径包含了空白字符 (如 grep 出来的结果) * - 防止维护者多写了空白字符 - * 2. 将 ~ 转换为绝对路径 - * 3. 在Windows上,使用标准的 \ 作为路径分隔符 + * 2. 将开头的 ~ 转换为当前用户的 HOME/USERPROFILE + * 3. Windows 上展开 %NAME% 环境变量 + * 4. 在Windows上,使用标准的 \ 作为路径分隔符 * * @memory SAFE * return caller-free @@ -1631,17 +1807,44 @@ xy_normalize_path (const char *path) { char *new = xy_str_strip (path); - if (xy_str_start_with (new, "~")) + if ('~' == new[0] && ('\0' == new[1] || '/' == new[1] || '\\' == new[1])) { - char *tmp = xy_str_delete_prefix (new, "~"); - char *joined = xy_2strcat (xy_os_home, tmp); - free (tmp); - xy_ptr_replace (&new, joined); + const char *home = xy_os_home; + if (home) + xy_ptr_replace (&new, xy_2strcat (home, new + 1)); } +#ifdef XY_Build_On_Windows + /* '%' 是 ASCII 字符,可直接在 UTF-8 中检查;普通路径无需进行编码转换。 */ + if (strchr (new, '%')) + { + wchar_t *wpath = xy_utf8_to_wstr (new); + if (wpath) + { + DWORD needed = ExpandEnvironmentStringsW (wpath, NULL, 0); + if (needed > 0) + { + wchar_t *wexpanded = xy_malloc0 ((size_t) needed * sizeof (wchar_t)); + DWORD written = ExpandEnvironmentStringsW (wpath, wexpanded, needed); + if (written > 0 && written <= needed) + { + char *utf8 = xy_wstr_to_utf8 (wexpanded); + if (utf8) + xy_ptr_replace (&new, utf8); + } + free (wexpanded); + } + free (wpath); + } + } +#endif + if (xy.on_windows) { - xy_ptr_replace (&new, xy_str_gsub (new, "/", "\\")); + /* 分隔符替换不改变长度,直接复用规范化过程分配的缓冲区。 */ + for (char *p = new; *p; p++) + if ('/' == *p) + *p = '\\'; } return new; } @@ -1720,29 +1923,24 @@ 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"; +#else + +#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"); @@ -1796,7 +1994,7 @@ _xy_detect_os () { char buf[256]; fgets (buf, sizeof (buf), fp); - pclose (fp); + xy_pclose (fp); if (strstr (buf, "BSD") != NULL) { xy.on_bsd = true; @@ -1818,6 +2016,7 @@ _xy_detect_os () if (!(xy.on_windows || xy.on_linux || xy.on_android || xy.on_macos || xy.on_bsd)) xy_throw ("Unknown operating system"); +#endif } diff --git a/src/framework/OS.c b/src/framework/OS.c index 23535664..6265e0f7 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 * ------------------------------------------------------------*/ @@ -1227,7 +1227,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; @@ -1282,7 +1282,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/recipe/ware/Nix.c b/src/recipe/ware/Nix.c index 982b0bef..bfb24281 100644 --- a/src/recipe/ware/Nix.c +++ b/src/recipe/ware/Nix.c @@ -12,7 +12,7 @@ def_dish(wr_nix, "nix"); bool wr_nix_is_multi_user_installation_mode () { - if (xy_file_exist ("/nix/var/nix/daemon-socket/socket")) + if (xy_path_exist ("/nix/var/nix/daemon-socket/socket")) { return true; } diff --git a/test/xy.c b/test/xy.c index 37b3ee98..659c2936 100644 --- a/test/xy.c +++ b/test/xy.c @@ -5,7 +5,7 @@ * Test Authors : 曾奥然 * Contributors : Mikachu2333 * Created On : <2023-08-30> - * Last Modified : <2026-08-13> + * Last Modified : <2026-11-11> * * Test xy.h * ------------------------------------------------------------*/ @@ -130,6 +130,81 @@ main (int argc, char const *argv[]) assert (xy_file_exist ("./doc/image/chsrc.png")); assert (xy_dir_exist ("~")); + assert (!xy_file_exist (NULL)); + assert (!xy_dir_exist (NULL)); + assert (!xy_path_exist (NULL)); + assert (!xy_stat_is_file (NULL, NULL)); + assert (!xy_stat_is_dir (NULL, NULL)); + + enum XyStatError error = XY_Stat_Ok; + assert (!xy_stat_is_file (NULL, &error) && XY_Stat_Invalid_Path == error); + assert (xy_stat_is_file ("./doc/image/chsrc.png", &error) && XY_Stat_Ok == error); + assert (!xy_stat_is_dir (NULL, &error) && XY_Stat_Invalid_Path == error); + assert (xy_stat_is_dir ("~", &error) && XY_Stat_Ok == error); + + assert (!xy_stat_is_file ("~", &error) && XY_Stat_Not_File == error); + assert (!xy_stat_is_dir ("./doc/image/chsrc.png", &error) && XY_Stat_Not_Directory == error); + assert (xy_stat_is_file (" ./doc/image/chsrc.png \t", &error) && XY_Stat_Ok == error); + assert (!xy_stat_is_file ("~", NULL)); + assert (!xy_stat_is_dir ("./doc/image/chsrc.png", NULL)); + + assert (!xy_stat_is_file ("", &error) && XY_Stat_Invalid_Path == error); + assert (!xy_stat_is_dir ("", &error) && XY_Stat_Invalid_Path == error); + assert (!xy_stat_is_file (" \t\n", &error) && XY_Stat_Invalid_Path == error); + assert (!xy_stat_is_dir (" \t\n", &error) && XY_Stat_Invalid_Path == error); + assert (!xy_stat_is_file ("./doc/image/xy-stat-nonexistent", &error) && XY_Stat_Not_Found == error); + assert (!xy_stat_is_dir ("./doc/image/xy-stat-nonexistent", &error) && XY_Stat_Not_Found == error); + assert (!xy_stat_is_file ("./doc/image/chsrc.png/missing", &error)); +#ifdef XY_Build_On_Windows + assert (XY_Stat_Not_Found == error || XY_Stat_Not_Directory == error); +#else + assert (XY_Stat_Not_Directory == error); +#endif + enum XyStatError component_error = error; + assert (!xy_stat_is_dir ("./doc/image/chsrc.png/missing", &error) && component_error == error); + +#ifdef XY_Build_On_Windows + assert (xy_path_exist ("NUL")); + assert (!xy_stat_is_file ("NUL", &error) && XY_Stat_Not_File == error); + assert (!xy_stat_is_dir ("NUL", &error) && XY_Stat_Not_Directory == error); + assert (!xy_stat_is_file ("invalid