diff --git a/src/discover/discover.c b/src/discover/discover.c index a43b44be3..96b66144d 100644 --- a/src/discover/discover.c +++ b/src/discover/discover.c @@ -245,7 +245,7 @@ static bool expand_git_path(const char *path, char *out, size_t out_sz) { } static bool read_core_excludes_file(const char *config_path, char *out, size_t out_sz) { - FILE *f = fopen(config_path, "r"); + FILE *f = cbm_fopen(config_path, "r"); if (!f) { return false; } diff --git a/src/discover/gitignore.c b/src/discover/gitignore.c index 57b1e1581..d390c637f 100644 --- a/src/discover/gitignore.c +++ b/src/discover/gitignore.c @@ -12,6 +12,7 @@ * - patterns with / are rooted (anchored to base) */ #include "foundation/constants.h" +#include "foundation/compat_fs.h" enum { GI_INIT_CAP = 16, GI_CHAR_IDX1 = 1, GI_CHAR_IDX2 = 2, GI_SKIP3 = 3 }; #include "discover/discover.h" @@ -287,7 +288,7 @@ cbm_gitignore_t *cbm_gitignore_load(const char *path) { return NULL; } - FILE *f = fopen(path, "r"); + FILE *f = cbm_fopen(path, "r"); if (!f) { return NULL; } diff --git a/src/discover/language.c b/src/discover/language.c index a0254306c..917651d13 100644 --- a/src/discover/language.c +++ b/src/discover/language.c @@ -11,6 +11,7 @@ #include "cbm.h" // CBMLanguage, CBM_LANG_* #include "foundation/constants.h" +#include "foundation/compat_fs.h" enum { LANG_SCAN_PASSES = 2 }; #define SLEN(s) (sizeof(s) - 1) @@ -998,7 +999,7 @@ CBMLanguage cbm_disambiguate_m(const char *path) { return CBM_LANG_MATLAB; } - FILE *f = fopen(path, "r"); + FILE *f = cbm_fopen(path, "r"); if (!f) { return CBM_LANG_MATLAB; } diff --git a/src/discover/userconfig.c b/src/discover/userconfig.c index 63ba4885c..7c606f912 100644 --- a/src/discover/userconfig.c +++ b/src/discover/userconfig.c @@ -13,6 +13,7 @@ #include "cbm.h" /* CBMLanguage, CBM_LANG_* */ #include "foundation/constants.h" #include "foundation/platform.h" /* cbm_safe_getenv */ +#include "foundation/compat_fs.h" enum { MAX_CONFIG_SIZE = 65536 }; #include "foundation/log.h" @@ -231,7 +232,7 @@ static int parse_extra_extensions(yyjson_val *root, cbm_userext_t **entries, int * Returns 0 on success (or absent file), -1 on alloc failure. */ static int load_config_file(const char *path, cbm_userext_t **entries, int *count) { - FILE *f = fopen(path, "rb"); + FILE *f = cbm_fopen(path, "rb"); if (!f) { return 0; /* file absent — silently ignore */ } diff --git a/src/foundation/compat_fs.c b/src/foundation/compat_fs.c index f77ad9a89..886f5c459 100644 --- a/src/foundation/compat_fs.c +++ b/src/foundation/compat_fs.c @@ -130,6 +130,22 @@ int cbm_pclose(FILE *f) { return _pclose(f); } +FILE *cbm_fopen(const char *path, const char *mode) { + wchar_t *wpath = cbm_utf8_to_wide(path); + if (!wpath) { + return NULL; + } + wchar_t *wmode = cbm_utf8_to_wide(mode); + if (!wmode) { + free(wpath); + return NULL; + } + FILE *f = _wfopen(wpath, wmode); + free(wpath); + free(wmode); + return f; +} + bool cbm_mkdir_p(const char *path, int mode) { (void)mode; wchar_t *wpath = cbm_utf8_to_wide(path); @@ -262,6 +278,10 @@ int cbm_pclose(FILE *f) { return pclose(f); } +FILE *cbm_fopen(const char *path, const char *mode) { + return fopen(path, mode); +} + bool cbm_mkdir_p(const char *path, int mode) { /* Try direct mkdir first */ if (mkdir(path, (mode_t)mode) == 0) { diff --git a/src/foundation/compat_fs.h b/src/foundation/compat_fs.h index 285ad555b..224e2b9a4 100644 --- a/src/foundation/compat_fs.h +++ b/src/foundation/compat_fs.h @@ -50,10 +50,16 @@ int cbm_unlink(const char *path); /* Delete an empty directory. Returns 0 on success. */ int cbm_rmdir(const char *path); +/* Open a file by UTF-8 path. + * On Windows, converts to wide-char and calls _wfopen so paths with + * non-ASCII characters (accents, CJK, etc.) are handled correctly. + * On POSIX, delegates to fopen. mode must be an ASCII string. */ +FILE *cbm_fopen(const char *path, const char *mode); + /* Execute a command without shell interpretation. * argv is a NULL-terminated array: {"cmd", "arg1", "arg2", NULL}. * Returns the process exit code, or -1 on fork/exec failure. - * POSIX: fork() + execvp(). Windows: _spawnvp(). */ + * POSIX: fork() + execvp(). Windows: CreateProcess with proper quoting. */ int cbm_exec_no_shell(const char *const *argv); #endif /* CBM_COMPAT_FS_H */ diff --git a/src/mcp/mcp.c b/src/mcp/mcp.c index 5235aaab5..84bc08492 100644 --- a/src/mcp/mcp.c +++ b/src/mcp/mcp.c @@ -2782,7 +2782,7 @@ static void free_node_contents(cbm_node_t *n) { /* ── Helper: read lines [start, end] from a file ─────────────── */ static char *read_file_lines(const char *path, int start, int end) { - FILE *fp = fopen(path, "r"); + FILE *fp = cbm_fopen(path, "r"); if (!fp) { return NULL; } @@ -4639,7 +4639,7 @@ static char *adr_read_legacy_file(const char *root_path) { } char adr_path[CBM_SZ_4K]; snprintf(adr_path, sizeof(adr_path), "%s/.codebase-memory/adr.md", root_path); - FILE *fp = fopen(adr_path, "r"); + FILE *fp = cbm_fopen(adr_path, "r"); if (!fp) { return NULL; } diff --git a/src/pipeline/artifact.c b/src/pipeline/artifact.c index 720c9d237..014f32b23 100644 --- a/src/pipeline/artifact.c +++ b/src/pipeline/artifact.c @@ -122,7 +122,7 @@ static bool artifact_path(char *buf, size_t bufsz, const char *repo_path, const /* Read entire file into malloc'd buffer. Sets *out_len. Returns NULL on error. */ static char *read_file_alloc(const char *path, size_t *out_len) { - FILE *fp = fopen(path, "rb"); + FILE *fp = cbm_fopen(path, "rb"); if (!fp) { return NULL; } diff --git a/src/pipeline/pass_calls.c b/src/pipeline/pass_calls.c index 2e27adc18..5c0f978e3 100644 --- a/src/pipeline/pass_calls.c +++ b/src/pipeline/pass_calls.c @@ -22,6 +22,7 @@ enum { PC_RING = 4, PC_RING_MASK = 3, PC_SIG_SCAN = 15, PC_REGEX_GRP = 2 }; #include "graph_buffer/graph_buffer.h" #include "foundation/log.h" #include "foundation/compat.h" +#include "foundation/compat_fs.h" #include "foundation/str_util.h" #include "cbm.h" #include "service_patterns.h" @@ -43,7 +44,7 @@ static bool pc_module_is_dir(CBMLanguage lang) { /* Read entire file into heap-allocated buffer. Caller must free(). */ static char *read_file(const char *path, int *out_len) { - FILE *f = fopen(path, "rb"); + FILE *f = cbm_fopen(path, "rb"); if (!f) { return NULL; } diff --git a/src/pipeline/pass_definitions.c b/src/pipeline/pass_definitions.c index f0816068c..767703d34 100644 --- a/src/pipeline/pass_definitions.c +++ b/src/pipeline/pass_definitions.c @@ -22,6 +22,7 @@ enum { PD_JSON_FIELD_OVERHEAD = 6 }; #include "graph_buffer/graph_buffer.h" #include "foundation/log.h" #include "foundation/compat.h" +#include "foundation/compat_fs.h" #include "cbm.h" #include "simhash/minhash.h" #include "semantic/ast_profile.h" @@ -33,7 +34,7 @@ enum { PD_JSON_FIELD_OVERHEAD = 6 }; /* Read entire file into heap-allocated buffer. Returns NULL on error. * Caller must free(). Sets *out_len to byte count. */ static char *read_file(const char *path, int *out_len) { - FILE *f = fopen(path, "rb"); + FILE *f = cbm_fopen(path, "rb"); if (!f) { return NULL; } diff --git a/src/pipeline/pass_envscan.c b/src/pipeline/pass_envscan.c index f2a6ae606..766392ba7 100644 --- a/src/pipeline/pass_envscan.c +++ b/src/pipeline/pass_envscan.c @@ -316,7 +316,7 @@ static int scan_line(const char *line, file_type_t ft, char *key_out, size_t key /* Scan a single file for env URL bindings. Returns number of bindings added. */ static int scan_env_file(const char *full_path, const char *rel, file_type_t ft, cbm_env_binding_t *out, int max_out) { - FILE *f = fopen(full_path, "r"); + FILE *f = cbm_fopen(full_path, "r"); if (!f) { return 0; } diff --git a/src/pipeline/pass_k8s.c b/src/pipeline/pass_k8s.c index 0a99443f5..494e13895 100644 --- a/src/pipeline/pass_k8s.c +++ b/src/pipeline/pass_k8s.c @@ -19,6 +19,7 @@ #include "discover/discover.h" #include "foundation/log.h" #include "foundation/compat.h" +#include "foundation/compat_fs.h" #include "cbm.h" #include @@ -30,7 +31,7 @@ /* Read entire file into heap-allocated buffer. Returns NULL on error. * Caller must free(). Sets *out_len to byte count. */ static char *k8s_read_file(const char *path, int *out_len) { - FILE *f = fopen(path, "rb"); + FILE *f = cbm_fopen(path, "rb"); if (!f) { return NULL; } diff --git a/src/pipeline/pass_lsp_cross.c b/src/pipeline/pass_lsp_cross.c index 31a7500aa..d2ad23079 100644 --- a/src/pipeline/pass_lsp_cross.c +++ b/src/pipeline/pass_lsp_cross.c @@ -28,6 +28,7 @@ #include "foundation/constants.h" #include "foundation/hash_table.h" #include "foundation/log.h" +#include "foundation/compat_fs.h" #include #include @@ -67,7 +68,7 @@ static bool pxc_module_is_dir(CBMLanguage lang) { * read_file helper in pass_calls.c / pass_parallel.c (kept local so the * pipeline doesn't grow a public read-file API just for this pass). */ static char *pxc_read_file(const char *path, int *out_len) { - FILE *f = fopen(path, "rb"); + FILE *f = cbm_fopen(path, "rb"); if (!f) return NULL; (void)fseek(f, 0, SEEK_END); diff --git a/src/pipeline/pass_parallel.c b/src/pipeline/pass_parallel.c index fefcf736a..93656b5ec 100644 --- a/src/pipeline/pass_parallel.c +++ b/src/pipeline/pass_parallel.c @@ -56,6 +56,7 @@ enum { PP_CSHARP_M_PREFIX_LEN = 2 }; #include "helpers.h" /* cbm_kind_in_set_free_cache — per-worker-thread cache teardown */ #include "pipeline/worker_pool.h" #include "foundation/compat.h" +#include "foundation/compat_fs.h" #include "foundation/compat_thread.h" #include "graph_buffer/graph_buffer.h" #include "service_patterns.h" @@ -88,7 +89,7 @@ static uint64_t extract_now_ns(void) { /* Read file into a malloc'd buffer (= mimalloc in production). */ static char *read_file(const char *path, int *out_len) { - FILE *f = fopen(path, "rb"); + FILE *f = cbm_fopen(path, "rb"); if (!f) { return NULL; } diff --git a/src/pipeline/pass_pkgmap.c b/src/pipeline/pass_pkgmap.c index 5b2ce8e5a..97dfb2ad0 100644 --- a/src/pipeline/pass_pkgmap.c +++ b/src/pipeline/pass_pkgmap.c @@ -33,7 +33,7 @@ /* Read an entire file into a malloc'd buffer. Returns NULL on failure. */ static char *pkgmap_read_file(const char *path, int *out_len) { - FILE *f = fopen(path, "rb"); + FILE *f = cbm_fopen(path, "rb"); if (!f) { return NULL; } diff --git a/src/pipeline/pass_semantic.c b/src/pipeline/pass_semantic.c index 3c3c76da7..2b9c03b46 100644 --- a/src/pipeline/pass_semantic.c +++ b/src/pipeline/pass_semantic.c @@ -19,6 +19,7 @@ #include "graph_buffer/graph_buffer.h" #include "foundation/log.h" #include "foundation/compat.h" +#include "foundation/compat_fs.h" #include "cbm.h" #include @@ -34,7 +35,7 @@ static bool ps_module_is_dir(CBMLanguage lang) { } static char *read_file(const char *path, int *out_len) { - FILE *f = fopen(path, "rb"); + FILE *f = cbm_fopen(path, "rb"); if (!f) { return NULL; } diff --git a/src/pipeline/pass_usages.c b/src/pipeline/pass_usages.c index 7f9c72c82..ec01a0ab6 100644 --- a/src/pipeline/pass_usages.c +++ b/src/pipeline/pass_usages.c @@ -18,6 +18,7 @@ #include "graph_buffer/graph_buffer.h" #include "foundation/log.h" #include "foundation/compat.h" +#include "foundation/compat_fs.h" #include "cbm.h" #include @@ -33,7 +34,7 @@ static bool pu_module_is_dir(CBMLanguage lang) { /* Read file into heap buffer. Caller must free(). */ static char *read_file(const char *path, int *out_len) { - FILE *f = fopen(path, "rb"); + FILE *f = cbm_fopen(path, "rb"); if (!f) { return NULL; } diff --git a/src/pipeline/path_alias.c b/src/pipeline/path_alias.c index 0c4cc3cf6..79e26b08b 100644 --- a/src/pipeline/path_alias.c +++ b/src/pipeline/path_alias.c @@ -123,7 +123,7 @@ static int cmp_scope_by_specificity(const void *a, const void *b) { * (e.g. "apps/manager", or "" for repo root). Returns NULL if the file is * missing, malformed, or has neither a usable paths block nor a baseUrl. */ static cbm_path_alias_map_t *load_tsconfig_file(const char *abs_path, const char *dir_prefix) { - FILE *f = fopen(abs_path, "r"); + FILE *f = cbm_fopen(abs_path, "r"); if (!f) { return NULL; } @@ -346,7 +346,7 @@ static void find_alias_files(const char *abs_dir, const char *rel_dir, alias_con for (int i = 0; i < TS_CONFIG_NAMES_COUNT && *count < max_count; i++) { char check[CBM_SZ_512]; snprintf(check, sizeof(check), "%s/%s", abs_dir, TS_CONFIG_NAMES[i]); - FILE *f = fopen(check, "r"); + FILE *f = cbm_fopen(check, "r"); if (f) { fclose(f); snprintf(out[*count].abs, sizeof(out[*count].abs), "%s", check);