mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-08 08:53:43 +08:00
Merge pull request #1816 from z2250389/codex-z2250389/fix-windows-unicode-git-root
fix(windows): support Unicode repository paths
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "foundation/compat_fs.h"
|
||||
#include "foundation/constants.h"
|
||||
#include "foundation/platform.h"
|
||||
#include "foundation/str_util.h"
|
||||
|
||||
#include <ctype.h>
|
||||
@@ -9,7 +10,6 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
enum {
|
||||
GIT_CMD_MAX = 1024,
|
||||
@@ -254,8 +254,7 @@ int cbm_git_context_resolve(const char *path, cbm_git_context_t *out) {
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
|
||||
struct stat st;
|
||||
out->root_exists = (stat(path, &st) == 0);
|
||||
out->root_exists = cbm_is_dir(path);
|
||||
if (!out->root_exists) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -715,7 +715,6 @@ static void handle_processes(cbm_http_conn_t *c) {
|
||||
|
||||
/* ── Directory browser ────────────────────────────────────────── */
|
||||
|
||||
#include <dirent.h>
|
||||
|
||||
static void append_roots_json(char *buf, size_t bufsz, int *pos) {
|
||||
http_appendf(buf, bufsz, pos, ",\"roots\":[");
|
||||
@@ -770,7 +769,7 @@ static void handle_browse(cbm_http_conn_t *c, const cbm_http_req_t *req) {
|
||||
return;
|
||||
}
|
||||
|
||||
DIR *dir = opendir(path);
|
||||
cbm_dir_t *dir = cbm_opendir(path);
|
||||
if (!dir) {
|
||||
cbm_http_replyf(c, 403, g_cors_json, "{\"error\":\"cannot open directory\"}");
|
||||
return;
|
||||
@@ -781,16 +780,16 @@ static void handle_browse(cbm_http_conn_t *c, const cbm_http_req_t *req) {
|
||||
int pos = 0;
|
||||
http_appendf(buf, sizeof(buf), &pos, "{\"path\":\"%s\",\"dirs\":[", path);
|
||||
|
||||
struct dirent *ent;
|
||||
cbm_dirent_t *ent;
|
||||
int count = 0;
|
||||
while ((ent = readdir(dir)) != NULL) {
|
||||
while ((ent = cbm_readdir(dir)) != NULL) {
|
||||
/* Skip hidden dirs and . / .. */
|
||||
if (ent->d_name[0] == '.')
|
||||
if (ent->name[0] == '.')
|
||||
continue;
|
||||
|
||||
/* Check if it's actually a directory */
|
||||
char full[2048];
|
||||
snprintf(full, sizeof(full), "%s/%s", path, ent->d_name);
|
||||
snprintf(full, sizeof(full), "%s/%s", path, ent->name);
|
||||
if (!cbm_is_dir(full))
|
||||
continue;
|
||||
|
||||
@@ -799,7 +798,7 @@ static void handle_browse(cbm_http_conn_t *c, const cbm_http_req_t *req) {
|
||||
/* Escape directory name to prevent XSS (e.g., names with quotes/angle brackets) */
|
||||
{
|
||||
char esc[512];
|
||||
cbm_json_escape(esc, (int)sizeof(esc), ent->d_name);
|
||||
cbm_json_escape(esc, (int)sizeof(esc), ent->name);
|
||||
http_appendf(buf, sizeof(buf), &pos, "\"%s\"", esc);
|
||||
}
|
||||
if (pos >= (int)sizeof(buf)) {
|
||||
@@ -810,7 +809,7 @@ static void handle_browse(cbm_http_conn_t *c, const cbm_http_req_t *req) {
|
||||
if (count >= 200)
|
||||
break; /* safety limit */
|
||||
}
|
||||
closedir(dir);
|
||||
cbm_closedir(dir);
|
||||
|
||||
/* Parent path — escape to prevent injection */
|
||||
char parent[1024];
|
||||
|
||||
@@ -1859,6 +1859,53 @@ static bool app_env_backup_restore(app_env_backup_t *backup) {
|
||||
return status == 0;
|
||||
}
|
||||
|
||||
/* The Graph UI enters through the daemon's background coordinator rather than
|
||||
* the MCP tool path. Keep that path UTF-8-safe as well. */
|
||||
TEST(daemon_application_ui_index_accepts_non_ascii_directory) {
|
||||
app_env_backup_t cache_environment;
|
||||
bool cache_saved = app_env_backup_capture(&cache_environment, "CBM_CACHE_DIR");
|
||||
char root[APP_TEST_PATH_CAP];
|
||||
char cache[APP_TEST_PATH_CAP];
|
||||
(void)snprintf(root, sizeof(root),
|
||||
"%s/cbm-app-ui-index-\xE6\x97\xA5\xE6\x9C\xAC\xE8\xAA\x9E-XXXXXX", cbm_tmpdir());
|
||||
(void)snprintf(cache, sizeof(cache), "%s/cbm-app-ui-index-cache-XXXXXX", cbm_tmpdir());
|
||||
bool dirs_created = cbm_mkdtemp(root) != NULL && cbm_mkdtemp(cache) != NULL;
|
||||
bool cache_set = cache_saved && dirs_created && cbm_setenv("CBM_CACHE_DIR", cache, 1) == 0;
|
||||
|
||||
app_fake_worker_context_t fake;
|
||||
app_fake_worker_context_init(&fake);
|
||||
atomic_store(&fake.allow_completion, true);
|
||||
cbm_daemon_application_worker_ops_t worker_ops = {
|
||||
.context = &fake,
|
||||
.start = app_fake_worker_start,
|
||||
.poll = app_fake_worker_poll,
|
||||
.cancel = app_fake_worker_cancel,
|
||||
.log_path = app_fake_worker_log_path,
|
||||
.destroy = app_fake_worker_destroy,
|
||||
};
|
||||
cbm_daemon_application_config_t config = {.worker_ops = &worker_ops};
|
||||
cbm_daemon_application_t *application = cache_set ? cbm_daemon_application_new(&config) : NULL;
|
||||
int result = application ? cbm_daemon_application_index(application, "", root) : -1;
|
||||
bool stopped = application && cbm_daemon_application_shutdown(application, APP_TEST_TIMEOUT_MS);
|
||||
cbm_daemon_application_free(application);
|
||||
bool root_cleaned = th_rmtree(root) == 0;
|
||||
bool cache_cleaned = th_rmtree(cache) == 0;
|
||||
bool cache_restored = app_env_backup_restore(&cache_environment);
|
||||
|
||||
ASSERT_TRUE(cache_saved);
|
||||
ASSERT_TRUE(dirs_created);
|
||||
ASSERT_TRUE(cache_set);
|
||||
ASSERT_NOT_NULL(application);
|
||||
ASSERT_EQ(result, 0);
|
||||
ASSERT_EQ(atomic_load(&fake.starts), 1);
|
||||
ASSERT_EQ(atomic_load(&fake.destroys), 1);
|
||||
ASSERT_TRUE(stopped);
|
||||
ASSERT_TRUE(root_cleaned);
|
||||
ASSERT_TRUE(cache_cleaned);
|
||||
ASSERT_TRUE(cache_restored);
|
||||
PASS();
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
atomic_int starts;
|
||||
atomic_int start_failures_remaining;
|
||||
@@ -5652,6 +5699,7 @@ SUITE(daemon_application) {
|
||||
RUN_TEST(daemon_application_initialize_coalesces_auto_index_for_full_sessions);
|
||||
RUN_TEST(daemon_application_sensitive_root_blocks_auto_index_but_preserves_controls);
|
||||
RUN_TEST(daemon_application_sensitive_root_blocks_watch_but_preserves_controls);
|
||||
RUN_TEST(daemon_application_ui_index_accepts_non_ascii_directory);
|
||||
RUN_TEST(daemon_application_index_args_compare_repo_path_separator_equivalently);
|
||||
RUN_TEST(daemon_application_programmatic_index_injects_resource_policy);
|
||||
RUN_TEST(daemon_application_auto_index_honors_tracked_file_limit);
|
||||
|
||||
@@ -8,8 +8,9 @@
|
||||
* (input_path), not to worktree_root. Joining it with worktree_root and then
|
||||
* string-stripping "/.git" left unresolved ".." components in the result.
|
||||
*
|
||||
* These tests shell out to `git`, so they SKIP_PLATFORM on Windows (the CI
|
||||
* shell there cannot init a repo via system()).
|
||||
* The canonical_root tests shell out to `git`, so they SKIP_PLATFORM on
|
||||
* Windows (the CI shell there cannot init a repo via system()). The root-path
|
||||
* existence test runs on every platform.
|
||||
*
|
||||
* Reproduce-first guard: canonical_root_subdir is the genuine RED-without-the-fix
|
||||
* guard — a repo indexed from a subdirectory yields a relative --git-common-dir
|
||||
@@ -34,9 +35,29 @@
|
||||
#include <limits.h>
|
||||
#endif
|
||||
|
||||
/* These helpers shell out to git and are only used by the non-Windows test
|
||||
* bodies below; on Windows every test SKIP_PLATFORMs, so guard them here too or
|
||||
* they'd be unused-static functions and fail the -Werror build. */
|
||||
/* The Windows narrow stat() API interprets UTF-8 paths through the active ANSI
|
||||
* code page. That made an existing Unicode repository root appear absent and
|
||||
* stopped Git discovery before git was invoked (#1240). */
|
||||
TEST(root_exists_non_ascii_directory) {
|
||||
/* UTF-8 for "Japanese language", written as bytes to keep the source ASCII. */
|
||||
char *tmp = th_mktempdir("cbm_gitctx_\xE6\x97\xA5\xE6\x9C\xAC\xE8\xAA\x9E");
|
||||
if (!tmp)
|
||||
FAIL("th_mktempdir returned NULL for non-ASCII path");
|
||||
|
||||
cbm_git_context_t ctx = {0};
|
||||
int rc = cbm_git_context_resolve(tmp, &ctx);
|
||||
bool root_exists = ctx.root_exists;
|
||||
cbm_git_context_free(&ctx);
|
||||
th_rmtree(tmp);
|
||||
|
||||
ASSERT_EQ(rc, 0);
|
||||
ASSERT_TRUE(root_exists);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* These helpers shell out to git and are only used by the non-Windows
|
||||
* canonical_root test bodies below; guard them here too or they'd be
|
||||
* unused-static functions and fail the -Werror build on Windows. */
|
||||
#ifndef _WIN32
|
||||
/* Run a git command inside dir, return 0 on success. */
|
||||
static int git_run(const char *dir, const char *args) {
|
||||
@@ -235,6 +256,7 @@ TEST(canonical_root_linked_worktree) {
|
||||
/* ── Suite ──────────────────────────────────────────────────────── */
|
||||
|
||||
SUITE(git_context) {
|
||||
RUN_TEST(root_exists_non_ascii_directory);
|
||||
RUN_TEST(canonical_root_repo_root);
|
||||
RUN_TEST(canonical_root_subdir);
|
||||
RUN_TEST(canonical_root_linked_worktree);
|
||||
|
||||
@@ -2150,6 +2150,59 @@ TEST(ui_server_rejects_non_loopback_host) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* The browser sends UTF-8 repository paths in a percent-encoded query. On
|
||||
* Windows, the handler's narrow opendir/readdir calls accepted the ASCII parent
|
||||
* but could neither list nor open a non-ASCII child, even though cbm_is_dir's
|
||||
* wide-path check had already accepted it. Exercise both user-visible steps:
|
||||
* discover the Unicode directory from its parent, then browse into it. */
|
||||
TEST(ui_server_browse_non_ascii_directory) {
|
||||
static const char utf8_name[] = "\xE9\x81\x93\xE5\x85\xB7\xE7\xAE\xB1"; /* toolbox */
|
||||
char *created = th_mktempdir("cbm_browse_utf8");
|
||||
if (!created)
|
||||
FAIL("mktempdir");
|
||||
|
||||
char base[512];
|
||||
snprintf(base, sizeof(base), "%s", created);
|
||||
cbm_normalize_path_sep(base);
|
||||
|
||||
char unicode_dir[768];
|
||||
char child[1024];
|
||||
int unicode_len = snprintf(unicode_dir, sizeof(unicode_dir), "%s/%s", base, utf8_name);
|
||||
int child_len = snprintf(child, sizeof(child), "%s/Percy", unicode_dir);
|
||||
if (unicode_len <= 0 || (size_t)unicode_len >= sizeof(unicode_dir) || child_len <= 0 ||
|
||||
(size_t)child_len >= sizeof(child) || th_mkdir_p(child) != 0) {
|
||||
th_cleanup(base);
|
||||
FAIL("failed to create non-ASCII browse fixture");
|
||||
}
|
||||
|
||||
th_server_t ts;
|
||||
if (th_server_start(&ts) != 0) {
|
||||
th_cleanup(base);
|
||||
FAIL("server start");
|
||||
}
|
||||
|
||||
int port = cbm_http_server_port(ts.srv);
|
||||
char request[1536];
|
||||
char response[8192];
|
||||
snprintf(request, sizeof(request),
|
||||
"GET /api/browse?path=%s/%%E9%%81%%93%%E5%%85%%B7%%E7%%AE%%B1 HTTP/1.1\r\n\r\n", base);
|
||||
int n = th_http(port, request, response, sizeof(response));
|
||||
bool child_browse_ok =
|
||||
n > 0 && th_status(response) == 200 && strstr(response, "\"Percy\"") != NULL;
|
||||
|
||||
snprintf(request, sizeof(request), "GET /api/browse?path=%s HTTP/1.1\r\n\r\n", base);
|
||||
n = th_http(port, request, response, sizeof(response));
|
||||
bool parent_lists_unicode =
|
||||
n > 0 && th_status(response) == 200 && strstr(response, utf8_name) != NULL;
|
||||
|
||||
th_server_stop(&ts);
|
||||
th_cleanup(base);
|
||||
|
||||
ASSERT_TRUE(parent_lists_unicode);
|
||||
ASSERT_TRUE(child_browse_ok);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* The directory browser formats readdir() entries into a fixed 32 KB response
|
||||
* buffer. The per-entry loop is clamped, but the trailing "parent"/"roots"
|
||||
* appends were not — once the entries filled the buffer, pos ran past the end
|
||||
@@ -2367,6 +2420,7 @@ TEST(ui_server_index_status_long_paths_no_overflow) {
|
||||
/* ── Suite ────────────────────────────────────────────────────── */
|
||||
|
||||
SUITE(httpd) {
|
||||
RUN_TEST(ui_server_browse_non_ascii_directory);
|
||||
RUN_TEST(ui_server_browse_wide_dir_no_overflow);
|
||||
RUN_TEST(ui_server_logs_escape_dense_no_overflow);
|
||||
RUN_TEST(ui_server_index_status_long_paths_no_overflow);
|
||||
|
||||
Reference in New Issue
Block a user