From f1e36b583d3a21664b373f854466c676897a5560 Mon Sep 17 00:00:00 2001 From: H4M4CHi-ttr <42773515+H4M4CHi-ttr@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:49:39 +0900 Subject: [PATCH] fix(watcher): inspect repository and cache paths as UTF-8 Use existing UTF-8 filesystem inspection for watcher roots, .git markers, and cached graph databases. Preserve symlink following and distinguish confirmed absence from unavailable inspection before pruning projects. Cover an existing Unicode root with zero prune grace and capture the blocked-git test environment through the existing UTF-8 runtime helper. Split the independently applicable bug fix from the original certified EmbeddingGemma 2 contribution without changing its file contents. Signed-off-by: H4M4CHi-ttr <42773515+H4M4CHi-ttr@users.noreply.github.com> --- src/daemon/application.c | 4 ++-- src/watcher/watcher.c | 36 +++++++++++++++++++++++++--------- tests/test_watcher.c | 42 ++++++++++++++++++++++++++++++++++++---- 3 files changed, 67 insertions(+), 15 deletions(-) diff --git a/src/daemon/application.c b/src/daemon/application.c index ee121b5c6..581d435a5 100644 --- a/src/daemon/application.c +++ b/src/daemon/application.c @@ -448,8 +448,8 @@ static bool application_regular_db_exists(const char *project) { if (written <= 0 || (size_t)written >= sizeof(path)) { return false; } - struct stat status; - return stat(path, &status) == 0 && S_ISREG(status.st_mode); + cbm_path_info_t status; + return cbm_path_info_utf8(path, &status) == CBM_PATH_INFO_OK && status.is_regular; } static bool application_canonical_directory_exists(const char *path) { diff --git a/src/watcher/watcher.c b/src/watcher/watcher.c index f8d04c4c6..ac7c47b7f 100644 --- a/src/watcher/watcher.c +++ b/src/watcher/watcher.c @@ -522,14 +522,28 @@ static watcher_git_status_t git_repo_status(cbm_watcher_t *w, project_state_t *s * a `.git` file (the gitlink form used by linked worktrees and initialized * submodules). Deliberately a filesystem check, not a git invocation — the whole * point is to learn something `rev-parse` cannot tell us, because it walks up. */ +/* Preserve stat's link-following behavior while inspecting UTF-8 paths. */ +static int watcher_path_info(const char *path, cbm_path_info_t *info) { + int status = cbm_path_info_utf8(path, info); + if (status == CBM_PATH_INFO_OK && info->is_symlink) { + char canonical[CBM_SZ_4K]; + if (!cbm_canonical_path(path, canonical, sizeof(canonical))) { + return CBM_PATH_INFO_UNAVAILABLE; + } + status = cbm_path_info_utf8(canonical, info); + } + return status; +} + static bool git_has_own_dot_git(const char *root_path) { char path[CBM_SZ_4K]; int written = snprintf(path, sizeof(path), "%s/.git", root_path); if (written <= 0 || (size_t)written >= sizeof(path)) { return false; } - struct stat st; - return stat(path, &st) == 0 && (S_ISDIR(st.st_mode) || S_ISREG(st.st_mode)); + cbm_path_info_t info; + return watcher_path_info(path, &info) == CBM_PATH_INFO_OK && + (info.is_directory || info.is_regular); } /* Does the ancestor repository actually track anything inside this directory? @@ -990,13 +1004,17 @@ static root_status_t root_status(const char *root_path, int *out_errno) { if (!root_path) { return ROOT_UNCERTAIN; } - struct stat st; - if (stat(root_path, &st) == 0) { - /* Exists but is no longer a directory → the root directory is gone. */ - return S_ISDIR(st.st_mode) ? ROOT_PRESENT : ROOT_MISSING; + cbm_path_info_t info; + int status = watcher_path_info(root_path, &info); + if (status == CBM_PATH_INFO_OK) { + return info.is_directory ? ROOT_PRESENT : ROOT_MISSING; } +#ifdef _WIN32 + *out_errno = status == CBM_PATH_INFO_ABSENT ? ENOENT : EIO; +#else *out_errno = errno; - return cbm_watcher_root_missing_errno(errno) ? ROOT_MISSING : ROOT_UNCERTAIN; +#endif + return status == CBM_PATH_INFO_ABSENT ? ROOT_MISSING : ROOT_UNCERTAIN; } /* Sustained-absence window (seconds) before a missing root may be pruned. @@ -1338,8 +1356,8 @@ static void publish_status(project_state_t *s, bool scanned) { /* Init baseline for a project: check if git, get HEAD, count files */ static bool init_baseline(cbm_watcher_t *w, project_state_t *s) { - struct stat st; - if (stat(s->root_path, &st) != 0) { + cbm_path_info_t info; + if (watcher_path_info(s->root_path, &info) != CBM_PATH_INFO_OK || !info.is_directory) { cbm_log_warn("watcher.root_gone", "project", s->project_name, "path", s->root_path); s->baseline_done = true; s->is_git = false; diff --git a/tests/test_watcher.c b/tests/test_watcher.c index e0b78cffc..8be483ca8 100644 --- a/tests/test_watcher.c +++ b/tests/test_watcher.c @@ -1103,8 +1103,8 @@ TEST(watcher_stop_and_unwatch_cancel_blocked_git_without_backstop) { watcher_windows_image_identity_t expected_git; ASSERT_TRUE(watcher_windows_capture_image_identity(fake_git, &expected_git)); - const char *old_path_raw = getenv("PATH"); - const char *old_marker_raw = getenv(WATCHER_TEST_BLOCKING_GIT_MARKER_ENV); + const char *old_path_raw = cbm_runtime_getenv("PATH"); + const char *old_marker_raw = cbm_runtime_getenv(WATCHER_TEST_BLOCKING_GIT_MARKER_ENV); char *old_path = old_path_raw ? cbm_strdup(old_path_raw) : NULL; char *old_marker = old_marker_raw ? cbm_strdup(old_marker_raw) : NULL; size_t path_size = strlen(bin) + 2 + (old_path ? strlen(old_path) : 0); @@ -1238,8 +1238,8 @@ TEST(watcher_stop_and_unwatch_cancel_blocked_git_without_backstop) { ASSERT_TRUE(watcher_test_self_image(self)); ASSERT_EQ(symlink(self, fake_git), 0); - const char *old_path_raw = getenv("PATH"); - const char *old_marker_raw = getenv(WATCHER_TEST_BLOCKING_GIT_MARKER_ENV); + const char *old_path_raw = cbm_runtime_getenv("PATH"); + const char *old_marker_raw = cbm_runtime_getenv(WATCHER_TEST_BLOCKING_GIT_MARKER_ENV); char *old_path = old_path_raw ? strdup(old_path_raw) : NULL; char *old_marker = old_marker_raw ? strdup(old_marker_raw) : NULL; size_t path_size = strlen(bin) + 2 + (old_path ? strlen(old_path) : 0); @@ -3995,7 +3995,41 @@ TEST(watcher_null_watch_count) { * SUITE * ══════════════════════════════════════════════════════════════════ */ +TEST(watcher_unicode_root_is_not_pruned_as_missing) { + char *parent = th_mktempdir("cbm_watch_utf8"); + ASSERT_NOT_NULL(parent); + char root[CBM_SZ_4K]; + snprintf(root, sizeof(root), "%s/\xE6\x97\xA5\xE6\x9C\xAC\xE8\xAA\x9E", parent); + ASSERT_EQ(th_mkdir_p(root), 0); + const char *saved = cbm_runtime_getenv("CBM_WATCHER_PRUNE_GRACE_S"); + char *saved_copy = saved ? strdup(saved) : NULL; + cbm_setenv("CBM_WATCHER_PRUNE_GRACE_S", "0", 1); + cbm_store_t *store = cbm_store_open_memory(); + cbm_store_upsert_project(store, "unicode-root", root); + cbm_watcher_t *watcher = cbm_watcher_new(store, NULL, NULL); + cbm_watcher_watch(watcher, "unicode-root", root); + for (int i = 0; i < 8; i++) { + cbm_watcher_poll_once(watcher); + } + cbm_project_t info = {0}; + bool kept = cbm_watcher_watch_count(watcher) == 1 && + cbm_store_get_project(store, "unicode-root", &info) == CBM_STORE_OK; + cbm_project_free_fields(&info); + cbm_watcher_free(watcher); + cbm_store_close(store); + if (saved_copy) { + cbm_setenv("CBM_WATCHER_PRUNE_GRACE_S", saved_copy, 1); + } else { + cbm_unsetenv("CBM_WATCHER_PRUNE_GRACE_S"); + } + free(saved_copy); + ASSERT_EQ(th_rmtree(parent), 0); + ASSERT_TRUE(kept); + PASS(); +} + SUITE(watcher) { + RUN_TEST(watcher_unicode_root_is_not_pruned_as_missing); /* Adaptive interval */ RUN_TEST(poll_interval_base); RUN_TEST(poll_interval_scaling);