Skip to content
Open
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
79 changes: 51 additions & 28 deletions src/mcp/mcp.c
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand All @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 "<root>/<file>" 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
Expand Down Expand Up @@ -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. */
Expand Down
1 change: 1 addition & 0 deletions src/mcp/mcp_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 8 additions & 5 deletions tests/test_mcp.c
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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);
Expand Down
Loading