Skip to content

Commit 93740ce

Browse files
committed
fix(branch): restore files clobbered by a stale-base soft reset
The previous commit accidentally staged a stale-main vs current-main delta (a git reset --soft against an outdated origin/main ref), reverting the DeusData#1028 wide-canonicalization work and DeusData#1027's regression test. Restore every touched file to current main; the net branch diff is now exactly the intended two-file DeusData#898 fix (pass_calls.c + tests/test_mcp.c). Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
1 parent 51921d9 commit 93740ce

6 files changed

Lines changed: 89 additions & 28 deletions

File tree

‎src/foundation/compat_fs.c‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -714,6 +714,51 @@ int cbm_exec_no_shell(const char *const *argv) {
714714

715715
#endif /* _WIN32 */
716716

717+
/* Canonicalize an EXISTING path (collapse `..`, resolve per-OS): realpath on
718+
* POSIX; on Windows a wide-path GetFileAttributesW existence check +
719+
* GetFullPathNameW. The previous callers used the ANSI CRT (_access/
720+
* _fullpath) on UTF-8 input — locale-dependent by construction: on a CJK
721+
* system codepage (e.g. Big5) the UTF-8 bytes of a CJK path re-decode into
722+
* different characters and canonicalization corrupts the path (#973).
723+
* Returns 0 when the path does not exist or cannot be resolved. */
724+
int cbm_canonical_path(const char *path, char *out, size_t out_sz) {
725+
if (!path || !out || out_sz == 0) {
726+
return 0;
727+
}
728+
#ifdef _WIN32
729+
wchar_t *wpath = cbm_utf8_to_wide(path);
730+
if (!wpath) {
731+
return 0;
732+
}
733+
if (GetFileAttributesW(wpath) == INVALID_FILE_ATTRIBUTES) {
734+
free(wpath);
735+
return 0;
736+
}
737+
enum { CANON_WIDE_MAX = 4096 };
738+
wchar_t wfull[CANON_WIDE_MAX];
739+
DWORD n = GetFullPathNameW(wpath, CANON_WIDE_MAX, wfull, NULL);
740+
free(wpath);
741+
if (n == 0 || n >= CANON_WIDE_MAX) {
742+
return 0;
743+
}
744+
char *utf8 = cbm_wide_to_utf8(wfull);
745+
if (!utf8) {
746+
return 0;
747+
}
748+
size_t len = strlen(utf8);
749+
if (len >= out_sz) {
750+
free(utf8);
751+
return 0;
752+
}
753+
memcpy(out, utf8, len + 1);
754+
free(utf8);
755+
return 1;
756+
#else
757+
/* Callers pass >= 4K buffers (>= PATH_MAX on our platforms). */
758+
return realpath(path, out) != NULL;
759+
#endif
760+
}
761+
717762
/* rename() with overwrite semantics on every platform: POSIX rename already
718763
* replaces atomically; Windows rename fails with EEXIST when the target
719764
* exists, so use MoveFileExW(MOVEFILE_REPLACE_EXISTING) there (wide paths —

‎src/foundation/compat_fs.h‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ void cbm_remove_db_sidecars(const char *db_path);
5353
/* rename() that replaces an existing destination on every platform
5454
* (Windows rename fails with EEXIST; this uses MoveFileExW there). */
5555
int cbm_rename_replace(const char *src, const char *dst);
56+
/* Canonicalize an EXISTING path (realpath / wide GetFullPathNameW). Locale-
57+
* independent on Windows — never routes UTF-8 through the ANSI CRT (#973).
58+
* out must be >= 4096 bytes. Returns 1 on success, 0 otherwise. */
59+
int cbm_canonical_path(const char *path, char *out, size_t out_sz);
5660

5761
/* Delete an empty directory. Returns 0 on success. */
5862
int cbm_rmdir(const char *path);

‎src/mcp/mcp.c‎

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -811,25 +811,17 @@ static char *canonicalize_repo_path_if_exists(char *repo_path) {
811811
}
812812

813813
char real[CBM_SZ_4K];
814-
#ifdef _WIN32
815-
if (_access(repo_path, 0) == 0 && _fullpath(real, repo_path, sizeof(real))) {
814+
/* Wide-path canonicalization: the old _access/_fullpath pair decoded the
815+
* UTF-8 repo_path through the ANSI codepage and corrupted CJK paths on
816+
* CJK-locale systems (#973). */
817+
if (cbm_canonical_path(repo_path, real, sizeof(real))) {
816818
cbm_normalize_path_sep(real);
817819
char *canonical = heap_strdup(real);
818820
if (canonical) {
819821
free(repo_path);
820822
return canonical;
821823
}
822824
}
823-
#else
824-
if (realpath(repo_path, real)) {
825-
cbm_normalize_path_sep(real);
826-
char *canonical = heap_strdup(real);
827-
if (canonical) {
828-
free(repo_path);
829-
return canonical;
830-
}
831-
}
832-
#endif
833825

834826
return repo_path;
835827
}
@@ -5056,16 +5048,17 @@ static yyjson_doc *enrich_node_properties(yyjson_mut_doc *doc, yyjson_mut_val *o
50565048
* across preprocessor branches, which defeats source-level tooling that parses
50575049
* without the preprocessor (and left this function unindexed in the graph). */
50585050
static bool resolve_canonical_path(const char *path, char *out, size_t out_sz) {
5059-
#ifdef _WIN32
5060-
if (!_fullpath(out, path, out_sz)) {
5051+
/* cbm_canonical_path: realpath on POSIX; wide existence check +
5052+
* GetFullPathNameW on Windows (the old bare _fullpath was ANSI —
5053+
* CJK-locale corruption, #973 — and, unlike POSIX realpath, resolved
5054+
* nonexistent paths too; requiring existence aligns the platforms). */
5055+
if (!cbm_canonical_path(path, out, out_sz)) {
50615056
return false;
50625057
}
5058+
#ifdef _WIN32
50635059
cbm_normalize_path_sep(out);
5064-
return true;
5065-
#else
5066-
(void)out_sz;
5067-
return realpath(path, out) != NULL;
50685060
#endif
5061+
return true;
50695062
}
50705063

50715064
bool cbm_path_within_root(const char *root_path, const char *abs_path) {

‎src/pipeline/fqn.c‎

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* Handles Python __init__.py, JS/TS index.{js,ts}, path separators.
66
*/
77
#include "pipeline/pipeline.h"
8+
#include "foundation/compat_fs.h"
89
#include "foundation/constants.h"
910
#include "foundation/platform.h"
1011

@@ -410,17 +411,12 @@ char *cbm_project_name_from_path(const char *abs_path) {
410411

411412
char real[CBM_SZ_4K];
412413
const char *name_path = abs_path;
413-
#ifdef _WIN32
414-
if (_access(abs_path, 0) == 0 && _fullpath(real, abs_path, sizeof(real))) {
414+
/* Wide-path canonicalization — the ANSI _access/_fullpath pair corrupted
415+
* CJK paths on CJK-locale Windows (#973). */
416+
if (cbm_canonical_path(abs_path, real, sizeof(real))) {
415417
cbm_normalize_path_sep(real);
416418
name_path = real;
417419
}
418-
#else
419-
if (realpath(abs_path, real)) {
420-
cbm_normalize_path_sep(real);
421-
name_path = real;
422-
}
423-
#endif
424420

425421
/* Work on mutable copy */
426422
char *path = strdup(name_path);

‎tests/test_mcp.c‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -964,8 +964,9 @@ TEST(mcp_discovery_methods_return_empty_lists) {
964964
};
965965
for (int i = 0; i < 3; i++) {
966966
char reqbuf[256];
967-
snprintf(reqbuf, sizeof(reqbuf), "{\"jsonrpc\":\"2.0\",\"id\":%d,\"method\":\"%s\"}",
968-
100 + i, cases[i].method);
967+
snprintf(reqbuf, sizeof(reqbuf),
968+
"{\"jsonrpc\":\"2.0\",\"id\":%d,\"method\":\"%s\"}", 100 + i,
969+
cases[i].method);
969970
char *resp = cbm_mcp_server_handle(srv, reqbuf);
970971
ASSERT_NOT_NULL(resp);
971972
ASSERT_NULL(strstr(resp, "Method not found"));

‎tests/windows/test_cli_non_ascii_arg.py‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,28 @@ def main():
9898
print("non-ASCII argv (supervised): rc=%d" % p.returncode)
9999
print(" stdout: %s" % out[:200].replace("\n", " "))
100100
print(" stderr: %s" % err[-200:].replace("\n", " "))
101+
# #973 variant: the reporter's exact shape — Traditional-Chinese dir,
102+
# FLAG-form --repo-path + --mode fast, supervised. This adds coverage
103+
# of the flag->JSON converter and the repo-path canonicalization,
104+
# which used ANSI _access/_fullpath (locale-dependent — corrupted CJK
105+
# paths on CJK-codepage systems) before the cbm_canonical_path fix.
106+
cjk_repo = os.path.join(work, "\u96f7\u9054\u6e2c\u8a66")
107+
make_fixture(cjk_repo)
108+
env3 = dict(os.environ)
109+
env3["CBM_CACHE_DIR"] = os.path.join(work, "cache_cjk")
110+
env3.pop("CBM_INDEX_SUPERVISOR", None)
111+
p2 = subprocess.run([binary, "cli", "index_repository",
112+
"--repo-path", cjk_repo, "--mode", "fast"],
113+
capture_output=True, timeout=120, env=env3)
114+
out2 = (p2.stdout or b"").decode("utf-8", "replace")
115+
cjk_ok = '"nodes"' in out2 and '"nodes":0' not in out2.replace(" ", "")
116+
print("CJK flag-form (--repo-path, supervised, fast): rc=%d" % p2.returncode)
117+
print(" stdout: %s" % out2[:200].replace("\n", " "))
118+
if not cjk_ok:
119+
print("\nRED: CJK flag-form repo_path was not indexed (#973 — "
120+
"canonicalization or spawn boundary mangled the path).")
121+
return 1
122+
101123
if honored:
102124
print("\nGREEN: CLI honored the non-ASCII repo_path (argv + worker spawn).")
103125
return 0

0 commit comments

Comments
 (0)