fix(daemon): keep one separator spelling for the session root on Windows

On Windows cbm_canonical_path answers in the platform's native backslash
form, and the daemon stored that spelling verbatim for the client's
session_root and allowed_root (cbm_mcp_server_set_session_context) and for
the auto-index job's repo_path (application.c canonicalizes with the bare
call at three sites). Every tool handler, however, normalizes separators
after canonicalizing. One directory therefore had two names, and the exact
comparison in application_index_args_equal never matched: an explicit
index_repository request for the session root was refused as
OPTIONS_CONFLICT instead of joining the auto-index job already running for
it.

Normalize the separator spelling of both session roots in
set_session_context, and canonicalize plus normalize at the three
application.c sites through one helper, application_canonical_root().
Separators fold on every platform, so the new test
daemon_session_context_keeps_one_spelling_of_a_root binds wherever the
suite runs; it fails on main with "C:\repos\cbm" != "C:/repos/cbm".

Distilled from #1726 with co-author credit.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Co-authored-by: 刘冲 <mail@liuchong.dev>
This commit is contained in:
Martin Vogel
2026-09-06 02:48:16 +02:00
co-authored by 刘冲
parent fe85a6b236
commit c1b9c451c7
3 changed files with 53 additions and 5 deletions
+18 -3
View File
@@ -379,6 +379,20 @@ static bool application_canonical_directory_exists(const char *path) {
return cbm_path_info_utf8(path, &info) == 0 && info.is_directory;
}
/* The daemon's one spelling of a repository root. cbm_canonical_path answers in
* the platform's native form (backslashes on Windows) while every tool handler
* normalizes the separators of the paths it canonicalizes. A root kept native
* is a second name for one directory, and the exact comparison downstream -
* index-args equality when a request joins the job already running for that
* root - never matches it. */
static bool application_canonical_root(const char *path, char *out, size_t out_size) {
if (!cbm_canonical_path(path, out, out_size)) {
return false;
}
cbm_normalize_path_sep(out);
return true;
}
static cbm_daemon_application_watch_t *application_find_watch_locked(
cbm_daemon_application_t *application, const char *project) {
for (cbm_daemon_application_watch_t *watch = application->watches; watch; watch = watch->next) {
@@ -2355,9 +2369,10 @@ static cbm_daemon_runtime_application_status_t application_set_context(
}
char canonical_root[APPLICATION_PATH_CAP] = {0};
char canonical_allowed[APPLICATION_PATH_CAP] = {0};
bool canonical = cbm_canonical_path(root, canonical_root, sizeof(canonical_root));
bool canonical = application_canonical_root(root, canonical_root, sizeof(canonical_root));
if (canonical && allowed_present) {
canonical = cbm_canonical_path(allowed, canonical_allowed, sizeof(canonical_allowed));
canonical =
application_canonical_root(allowed, canonical_allowed, sizeof(canonical_allowed));
}
canonical = canonical && application_canonical_directory_exists(canonical_root);
bool set =
@@ -3372,7 +3387,7 @@ static int application_background_index(cbm_daemon_application_t *application,
return -1;
}
char canonical_root[APPLICATION_PATH_CAP];
if (!cbm_canonical_path(root_path, canonical_root, sizeof(canonical_root)) ||
if (!application_canonical_root(root_path, canonical_root, sizeof(canonical_root)) ||
!application_canonical_directory_exists(canonical_root)) {
return -1;
}
+14 -2
View File
@@ -1765,7 +1765,18 @@ bool cbm_mcp_server_set_session_context(cbm_mcp_server_t *srv, const char *sessi
return false;
}
char *project = cbm_project_name_from_path(session_root);
/* Keep both roots in the one spelling every tool handler produces. Each
* handler normalizes separators after canonicalizing, so a root stored in
* the platform's native form (backslashes on Windows) is a second name for
* the same directory - and the daemon compares roots exactly. An explicit
* index_repository request for this root was refused as an options
* conflict instead of joining the auto-index job already running for it,
* because the job's repo_path never matched the request's. */
char root[sizeof(srv->session_root)];
snprintf(root, sizeof(root), "%s", session_root);
cbm_normalize_path_sep(root);
char *project = cbm_project_name_from_path(root);
if (!project || project[0] == '\0' || strlen(project) >= sizeof(srv->session_project)) {
free(project);
return false;
@@ -1776,8 +1787,9 @@ bool cbm_mcp_server_set_session_context(cbm_mcp_server_t *srv, const char *sessi
free(project);
return false;
}
cbm_normalize_path_sep(allowed_copy);
snprintf(srv->session_root, sizeof(srv->session_root), "%s", session_root);
snprintf(srv->session_root, sizeof(srv->session_root), "%s", root);
snprintf(srv->session_project, sizeof(srv->session_project), "%s", project);
free(project);
+21
View File
@@ -467,6 +467,26 @@ TEST(daemon_sessions_keep_distinct_roots_and_allowed_root_policy) {
PASS();
}
/* The daemon canonicalizes a client's root through the platform API, which on
* Windows answers in backslash form, while every tool handler normalizes the
* separators of the paths it canonicalizes. Stored in its native form, one
* directory had two names, and an explicit index_repository request was
* refused as an options conflict instead of joining the auto-index job already
* running for that root. Separators fold on every platform, so the check binds
* wherever the suite runs. */
TEST(daemon_session_context_keeps_one_spelling_of_a_root) {
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
ASSERT_NOT_NULL(srv);
cbm_mcp_server_set_background_tasks(srv, false);
ASSERT_TRUE(cbm_mcp_server_set_session_context(srv, "C:\\repos\\cbm", "C:\\repos"));
ASSERT_STR_EQ(cbm_mcp_server_session_root(srv), "C:/repos/cbm");
ASSERT_STR_EQ(cbm_mcp_server_allowed_root(srv), "C:/repos");
cbm_mcp_server_free(srv);
PASS();
}
SUITE(daemon) {
RUN_TEST(daemon_client_ids_are_connection_bound);
RUN_TEST(daemon_shared_job_survives_until_final_subscriber_disconnects);
@@ -480,4 +500,5 @@ SUITE(daemon) {
RUN_TEST(daemon_bridge_rejects_embedded_nul_body);
RUN_TEST(daemon_bridge_rejects_oversized_headers);
RUN_TEST(daemon_sessions_keep_distinct_roots_and_allowed_root_policy);
RUN_TEST(daemon_session_context_keeps_one_spelling_of_a_root);
}