Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions src/daemon/application.c
Original file line number Diff line number Diff line change
Expand Up @@ -1588,6 +1588,33 @@ static bool application_index_args_normalize_defaults(yyjson_mut_val *root) {
return true;
}

/* One directory is one root. The auto-index job spells repo_path the way the
* session policy holds it - the platform's native form, backslashes on
* Windows - while an explicit index_repository request arrives in the
* handler's forward-slash spelling. Compared byte-exact the two never matched
* on Windows, and the request was refused as an options conflict instead of
* joining the job already running for its root. The policy keeps its
* spelling: the sensitive-root and allowed-root containment checks match it
* byte-exact against HOME and the granted roots, and respelling it there
* admitted $HOME. So the fold happens here, on this comparison's private copy,
* and nothing the daemon stores changes. */
static bool application_index_args_fold_repo_path(yyjson_mut_doc *document) {
yyjson_mut_val *root = yyjson_mut_doc_get_root(document);
yyjson_mut_val *repo_path = yyjson_mut_obj_get(root, "repo_path");
if (!repo_path || !yyjson_mut_is_str(repo_path)) {
return true;
}
char *folded = strdup(yyjson_mut_get_str(repo_path));
if (!folded) {
return false;
}
cbm_normalize_path_sep(folded);
yyjson_mut_val *key = yyjson_mut_str(document, "repo_path");
yyjson_mut_val *value = yyjson_mut_strcpy(document, folded);
free(folded);
return key && value && yyjson_mut_obj_replace(root, key, value);
}

static bool application_index_args_equal(const char *left, const char *right) {
if (!left || !right) {
return false;
Expand All @@ -1600,6 +1627,8 @@ static bool application_index_args_equal(const char *left, const char *right) {
yyjson_mut_val *right_root = right_copy ? yyjson_mut_doc_get_root(right_copy) : NULL;
bool equal = application_index_args_normalize_defaults(left_root) &&
application_index_args_normalize_defaults(right_root) &&
application_index_args_fold_repo_path(left_copy) &&
application_index_args_fold_repo_path(right_copy) &&
yyjson_mut_equals(left_root, right_root);
yyjson_mut_doc_free(left_copy);
yyjson_mut_doc_free(right_copy);
Expand All @@ -1608,6 +1637,10 @@ static bool application_index_args_equal(const char *left, const char *right) {
return equal;
}

bool cbm_daemon_application_index_args_equal_for_test(const char *left, const char *right) {
return application_index_args_equal(left, right);
}

/* Caller holds application->mutex. Keeping watcher ownership validation and
* this admission in the same critical section closes the unwatch race. */
static cbm_daemon_application_job_t *application_job_subscribe_locked(
Expand Down
6 changes: 6 additions & 0 deletions src/daemon/application_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,10 @@ int cbm_daemon_application_busy_queue_waits_for_test(void);
char *cbm_daemon_application_framable_response_for_test(char *response,
const cbm_jsonrpc_request_t *request);

/* The job registry's index-argument equality: the running job's args against
* a request's. Exposed because the two repo_path spellings it folds - the
* session policy's native root and a handler's forward-slash one - only differ
* on Windows, while the fold itself runs on every platform. */
bool cbm_daemon_application_index_args_equal_for_test(const char *left, const char *right);

#endif /* CBM_DAEMON_APPLICATION_INTERNAL_H */
21 changes: 21 additions & 0 deletions tests/test_daemon.c
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,26 @@ TEST(daemon_sessions_keep_distinct_roots_and_allowed_root_policy) {
PASS();
}

/* The session policy keeps a root in the spelling the daemon canonicalized it
* to - the platform's native form, backslashes on Windows. The sensitive-root
* and allowed-root containment checks compare that spelling byte-exact against
* HOME and the granted roots, so a root respelled with forward slashes on the
* way in stopped matching them, and $HOME was admitted for auto-index and
* watch on Windows. One directory being one root for the job registry is
* folded at that comparison, never by respelling the policy. */
TEST(daemon_session_context_keeps_the_policy_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);
Expand All @@ -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_the_policy_spelling_of_a_root);
}
25 changes: 25 additions & 0 deletions tests/test_daemon_application.c
Original file line number Diff line number Diff line change
Expand Up @@ -5382,6 +5382,30 @@ TEST(daemon_application_oversized_reply_is_a_jsonrpc_error_not_a_death) {
PASS();
}

/* One directory is one root for the job registry. The auto-index job spells
* repo_path the way the session policy holds it - the platform's native form,
* backslashes on Windows - while an explicit index_repository request arrives
* in the handler's forward-slash spelling. Compared byte-exact the two never
* matched on Windows, and the request was refused as an options conflict
* instead of joining the job already running for its root. The fold runs on
* every platform, so this binds wherever the suite runs; every other option
* stays exact. */
TEST(daemon_application_index_args_compare_repo_path_separator_equivalently) {
ASSERT_TRUE(cbm_daemon_application_index_args_equal_for_test(
"{\"repo_path\":\"C:\\\\repos\\\\cbm\"}", "{\"repo_path\":\"C:/repos/cbm\"}"));
ASSERT_TRUE(cbm_daemon_application_index_args_equal_for_test(
"{\"repo_path\":\"C:\\\\repos\\\\cbm\",\"mode\":\"full\"}",
"{\"mode\":\"full\",\"repo_path\":\"C:/repos/cbm\"}"));
ASSERT_FALSE(cbm_daemon_application_index_args_equal_for_test(
"{\"repo_path\":\"C:\\\\repos\\\\cbm\"}", "{\"repo_path\":\"C:/repos/cbm2\"}"));
ASSERT_FALSE(cbm_daemon_application_index_args_equal_for_test(
"{\"repo_path\":\"C:\\\\repos\\\\cbm\"}", "{\"repo_path\":\"C:/repos/cbm/sub\"}"));
ASSERT_FALSE(cbm_daemon_application_index_args_equal_for_test(
"{\"repo_path\":\"C:\\\\repos\\\\cbm\",\"mode\":\"incremental\"}",
"{\"repo_path\":\"C:/repos/cbm\"}"));
PASS();
}

SUITE(daemon_application) {
RUN_TEST(daemon_application_oversized_reply_is_a_jsonrpc_error_not_a_death);
RUN_TEST(daemon_application_new_session_does_not_retain_initial_store);
Expand All @@ -5400,6 +5424,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_index_args_compare_repo_path_separator_equivalently);
RUN_TEST(daemon_application_auto_index_honors_tracked_file_limit);
RUN_TEST(daemon_application_auto_index_file_count_handles_literal_metacharacter_path);
RUN_TEST(daemon_application_auto_index_file_count_supports_non_git_roots);
Expand Down
Loading