diff --git a/src/mcp/mcp.c b/src/mcp/mcp.c index 04316627b..deae6514c 100644 --- a/src/mcp/mcp.c +++ b/src/mcp/mcp.c @@ -8930,6 +8930,36 @@ bool cbm_search_code_file_pattern_can_prefilter(const char *file_pattern) { return true; } +bool cbm_search_code_windows_path_matches_prefilter(const char *path, const char *file_pattern) { + if (!path || !cbm_search_code_file_pattern_can_prefilter(file_pattern)) { + return false; + } + + const char *suffix = file_pattern + 1; + size_t path_len = strlen(path); + size_t suffix_len = strlen(suffix); + if (path_len < suffix_len) { + return false; + } + + const unsigned char *candidate = (const unsigned char *)path + path_len - suffix_len; + const unsigned char *expected = (const unsigned char *)suffix; + for (size_t i = 0; i < suffix_len; i++) { + unsigned char left = candidate[i]; + unsigned char right = expected[i]; + if (left >= 'A' && left <= 'Z') { + left = (unsigned char)(left - 'A' + 'a'); + } + if (right >= 'A' && right <= 'Z') { + right = (unsigned char)(right - 'A' + 'a'); + } + if (left != right) { + return false; + } + } + return true; +} + /* Build the grep/search command string based on scoped vs recursive mode. * On Windows, uses PowerShell Select-String with tab-delimited output. * On POSIX, uses grep with colon-delimited output. */ @@ -8950,30 +8980,16 @@ void cbm_search_code_build_grep_cmd(char *cmd, size_t cmd_sz, bool use_regex, bo const char *sm = use_regex ? "" : " -SimpleMatch"; if (scoped) { if (file_pattern) { - if (cbm_search_code_file_pattern_can_prefilter(file_pattern)) { - snprintf( - cmd, cmd_sz, - "powershell -Command \"" CBM_PS_UTF8_PRELUDE - "$pat = Get-Content -Encoding UTF8 -LiteralPath '%s'; " - "Get-Content -Encoding UTF8 -LiteralPath '%s'" - " | Where-Object { $_ -like '%s' }" - " | ForEach-Object { Select-String -LiteralPath $_ -Pattern $pat%s " - "-ErrorAction SilentlyContinue }" - " | Where-Object { $_.Path -like '*%s' }" - " | ForEach-Object { $_.Path + [char]9 + $_.LineNumber + [char]9 + $_.Line }\"", - tmpfile, filelist, file_pattern, sm, file_pattern); - } else { - snprintf( - cmd, cmd_sz, - "powershell -Command \"" CBM_PS_UTF8_PRELUDE - "$pat = Get-Content -Encoding UTF8 -LiteralPath '%s'; " - "Get-Content -Encoding UTF8 -LiteralPath '%s' | ForEach-Object { Select-String " - "-LiteralPath $_ -Pattern $pat%s " - "-ErrorAction SilentlyContinue }" - " | Where-Object { $_.Path -like '*%s' }" - " | ForEach-Object { $_.Path + [char]9 + $_.LineNumber + [char]9 + $_.Line }\"", - tmpfile, filelist, sm, file_pattern); - } + snprintf( + cmd, cmd_sz, + "powershell -Command \"" CBM_PS_UTF8_PRELUDE + "$pat = Get-Content -Encoding UTF8 -LiteralPath '%s'; " + "Get-Content -Encoding UTF8 -LiteralPath '%s' | ForEach-Object { Select-String " + "-LiteralPath $_ -Pattern $pat%s " + "-ErrorAction SilentlyContinue }" + " | Where-Object { $_.Path -like '*%s' }" + " | ForEach-Object { $_.Path + [char]9 + $_.LineNumber + [char]9 + $_.Line }\"", + tmpfile, filelist, sm, file_pattern); } else { snprintf( cmd, cmd_sz, @@ -9611,8 +9627,8 @@ static void classify_all_grep_hits(grep_match_t *gm, int gm_count, cbm_store_t * * created inside the private scratch directory; this function never opens or * closes it, so the list is never reachable through a predictable pathname. */ static bool write_scoped_filelist(cbm_mcp_server_t *srv, const char *project, const char *root_path, - FILE *fl, bool has_path_filter, cbm_regex_t *path_regex, - int *out_written) { + FILE *fl, const char *file_pattern, bool has_path_filter, + cbm_regex_t *path_regex, int *out_written) { *out_written = 0; cbm_store_t *pre_store = resolve_store(srv, project); if (!pre_store) { @@ -9644,6 +9660,12 @@ static bool write_scoped_filelist(cbm_mcp_server_t *srv, const char *project, co continue; } } +#ifdef _WIN32 + if (cbm_search_code_file_pattern_can_prefilter(file_pattern) && + !cbm_search_code_windows_path_matches_prefilter(indexed_files[fi], file_pattern)) { + continue; + } +#endif /* Write "/" piece-by-piece (no fixed-size buffer, so an * arbitrarily long absolute path cannot overflow). Forward slash join * so xargs doesn't treat Windows backslashes as escapes; binary mode @@ -10046,8 +10068,9 @@ static char *handle_search_code(cbm_mcp_server_t *srv, const char *args) { int scoped_written = 0; uint64_t scope_t0 = metrics.include_phase_timings ? cbm_now_ms() : 0; - scoped = write_scoped_filelist(srv, project, root_path, scratch.filelist, has_path_filter, - has_path_filter ? &path_regex : NULL, &scoped_written); + scoped = write_scoped_filelist(srv, project, root_path, scratch.filelist, file_pattern, + has_path_filter, has_path_filter ? &path_regex : NULL, + &scoped_written); /* Close before grep runs: this is what flushes the records the helper wrote * through the descriptor. Clearing the field hands ownership to * search_scratch_close, which still unlinks the file itself. */ diff --git a/src/mcp/mcp_internal.h b/src/mcp/mcp_internal.h index d9321c595..f7f8d21dc 100644 --- a/src/mcp/mcp_internal.h +++ b/src/mcp/mcp_internal.h @@ -56,6 +56,7 @@ bool cbm_detect_node_in_hunks(const cbm_node_t *node, const cbm_changed_hunk_t * * PowerShell -like contract. Exposed for * direct boundary tests only. */ bool cbm_search_code_file_pattern_can_prefilter(const char *file_pattern); +bool cbm_search_code_windows_path_matches_prefilter(const char *path, const char *file_pattern); /* Internal command builder exposed so tests can pin the PowerShell pipeline * ordering without diff --git a/tests/test_mcp.c b/tests/test_mcp.c index 278d33e68..030c5a7d7 100644 --- a/tests/test_mcp.c +++ b/tests/test_mcp.c @@ -4944,22 +4944,25 @@ TEST(search_code_file_pattern_prefilter_boundaries) { ASSERT_FALSE(cbm_search_code_file_pattern_can_prefilter("src\\*.pas")); ASSERT_FALSE(cbm_search_code_file_pattern_can_prefilter("*.c++")); ASSERT_FALSE(cbm_search_code_file_pattern_can_prefilter("*R&D*.go")); + + ASSERT_TRUE(cbm_search_code_windows_path_matches_prefilter("src/UnitMain.PAS", "*.pas")); + ASSERT_TRUE(cbm_search_code_windows_path_matches_prefilter("types/index.D.TS", "*.d.ts")); + ASSERT_FALSE(cbm_search_code_windows_path_matches_prefilter("src/UnitMain.pas.bak", "*.pas")); + ASSERT_FALSE(cbm_search_code_windows_path_matches_prefilter("types/index.ts", "*.d.ts")); PASS(); } -TEST(search_code_windows_prefilter_precedes_content_scan) { +TEST(search_code_windows_scope_prefilter_removes_pipeline_filter) { #ifdef _WIN32 char command[CBM_SZ_4K]; cbm_search_code_build_grep_cmd(command, sizeof(command), false, true, "*.go", "C:/tmp/pattern", "C:/tmp/filelist", "C:/tmp/root"); - const char *prefilter = strstr(command, "Where-Object { $_ -like '*.go' }"); const char *content_scan = strstr(command, "ForEach-Object { Select-String"); const char *postfilter = strstr(command, "Where-Object { $_.Path -like '**.go' }"); - ASSERT_NOT_NULL(prefilter); + ASSERT_NULL(strstr(command, "Where-Object { $_ -like '*.go' }")); ASSERT_NOT_NULL(content_scan); ASSERT_NOT_NULL(postfilter); - ASSERT_TRUE(prefilter < content_scan); ASSERT_TRUE(content_scan < postfilter); cbm_search_code_build_grep_cmd(command, sizeof(command), false, true, "*handler*.go", @@ -11380,7 +11383,7 @@ SUITE(mcp) { RUN_TEST(search_code_path_filter_prefilter_keeps_matches); RUN_TEST(search_code_path_filter_matches_nothing); RUN_TEST(search_code_file_pattern_prefilter_boundaries); - RUN_TEST(search_code_windows_prefilter_precedes_content_scan); + RUN_TEST(search_code_windows_scope_prefilter_removes_pipeline_filter); RUN_TEST(search_code_windows_cancel_cleans_supervised_scan); RUN_TEST(search_code_windows_output_limit_fails_closed_and_cleans_scan); RUN_TEST(search_code_windows_scan_pins_utf8_output);