mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 18:58:00 +00:00
fix(windows): pin the search_code PowerShell pipe to UTF-8
PowerShell 5.1 encodes stdout for a native-process pipe in the console OEM codepage, so raw search content containing characters the inherited CP cannot carry (Cyrillic under CP437/850, ...) reached collect_grep_matches as '?' — and whether it degraded depended entirely on which console the server happened to inherit. That surfaced as the intermittent test_mcp raw-Русский mojibake on the windows CI leg and means real Windows users in a default console get '?' for all non-ASCII search_code raw content. Every generated command now pins [Console]::OutputEncoding to UTF-8, so the pipe is codepage-independent by construction. The read side needs no pin: Select-String decodes BOM-less UTF-8 via .NET StreamReader defaults. A Windows-side builder test asserts all five command variants carry the prelude. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
1 parent
773bb037dc
commit
4dd099679d
2 files changed
+84
-42
No files matched your search
+20
-5
@@ -8848,6 +8848,16 @@ bool cbm_search_code_file_pattern_can_prefilter(const char *file_pattern) {
|
||||
/* 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. */
|
||||
/* Windows PowerShell 5.1 encodes stdout for a native-process pipe in the
|
||||
* console OEM codepage, so any character the inherited CP cannot carry
|
||||
* (Cyrillic under CP437/850, etc.) degrades to '?' before it ever reaches
|
||||
* collect_grep_matches — and WHETHER it degrades depends on the console the
|
||||
* server happened to inherit. Pin the pipe to UTF-8 inside every generated
|
||||
* command so raw search content is codepage-independent. (The read side is
|
||||
* already safe: Select-String decodes BOM-less UTF-8 via .NET StreamReader
|
||||
* defaults.) */
|
||||
#define CBM_PS_UTF8_PRELUDE "[Console]::OutputEncoding=[System.Text.Encoding]::UTF8; "
|
||||
|
||||
void cbm_search_code_build_grep_cmd(char *cmd, size_t cmd_sz, bool use_regex, bool scoped,
|
||||
const char *file_pattern, const char *tmpfile,
|
||||
const char *filelist, const char *root_path) {
|
||||
@@ -8858,7 +8868,8 @@ void cbm_search_code_build_grep_cmd(char *cmd, size_t cmd_sz, bool use_regex, bo
|
||||
if (cbm_search_code_file_pattern_can_prefilter(file_pattern)) {
|
||||
snprintf(
|
||||
cmd, cmd_sz,
|
||||
"powershell -Command \"$pat = Get-Content -Encoding UTF8 -LiteralPath '%s'; "
|
||||
"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 "
|
||||
@@ -8869,7 +8880,8 @@ void cbm_search_code_build_grep_cmd(char *cmd, size_t cmd_sz, bool use_regex, bo
|
||||
} else {
|
||||
snprintf(
|
||||
cmd, cmd_sz,
|
||||
"powershell -Command \"$pat = Get-Content -Encoding UTF8 -LiteralPath '%s'; "
|
||||
"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 }"
|
||||
@@ -8880,7 +8892,8 @@ void cbm_search_code_build_grep_cmd(char *cmd, size_t cmd_sz, bool use_regex, bo
|
||||
} else {
|
||||
snprintf(
|
||||
cmd, cmd_sz,
|
||||
"powershell -Command \"$pat = Get-Content -Encoding UTF8 -LiteralPath '%s'; "
|
||||
"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 }"
|
||||
@@ -8891,7 +8904,8 @@ void cbm_search_code_build_grep_cmd(char *cmd, size_t cmd_sz, bool use_regex, bo
|
||||
if (file_pattern) {
|
||||
snprintf(
|
||||
cmd, cmd_sz,
|
||||
"powershell -Command \"Get-ChildItem -Recurse -Path '%s\\*' -Include '%s' -File "
|
||||
"powershell -Command \"" CBM_PS_UTF8_PRELUDE
|
||||
"Get-ChildItem -Recurse -Path '%s\\*' -Include '%s' -File "
|
||||
"-ErrorAction SilentlyContinue"
|
||||
" | Select-String -Pattern (Get-Content -Encoding UTF8 -LiteralPath '%s')%s "
|
||||
"-ErrorAction SilentlyContinue"
|
||||
@@ -8900,7 +8914,8 @@ void cbm_search_code_build_grep_cmd(char *cmd, size_t cmd_sz, bool use_regex, bo
|
||||
} else {
|
||||
snprintf(
|
||||
cmd, cmd_sz,
|
||||
"powershell -Command \"Get-ChildItem -Recurse -Path '%s\\*' -File -ErrorAction "
|
||||
"powershell -Command \"" CBM_PS_UTF8_PRELUDE
|
||||
"Get-ChildItem -Recurse -Path '%s\\*' -File -ErrorAction "
|
||||
"SilentlyContinue"
|
||||
" | Select-String -Pattern (Get-Content -Encoding UTF8 -LiteralPath '%s')%s "
|
||||
"-ErrorAction SilentlyContinue"
|
||||
|
||||
+64
-37
@@ -2120,7 +2120,7 @@ TEST(tool_search_graph_includes_node_properties) {
|
||||
ASSERT_NOT_NULL(strstr(inner, "func HandleRequest")); /* its value */
|
||||
ASSERT_NOT_NULL(strstr(inner, "\"base_classes\""));
|
||||
ASSERT_NOT_NULL(strstr(inner, "[\"HandlerBase\",\"Audited\"]"));
|
||||
ASSERT_NULL(strstr(inner, "is_exported")); /* blob never spills */
|
||||
ASSERT_NULL(strstr(inner, "is_exported")); /* blob never spills */
|
||||
free(inner);
|
||||
free(resp);
|
||||
|
||||
@@ -2680,15 +2680,13 @@ TEST(tool_check_index_coverage_accepts_truncated_ignored_catalog_for_fresh_path_
|
||||
((int64_t)source_stat.st_mtimespec.tv_sec * (int64_t)CBM_NSEC_PER_SEC) +
|
||||
(int64_t)source_stat.st_mtimespec.tv_nsec;
|
||||
#elif defined(_WIN32)
|
||||
int64_t source_mtime_ns =
|
||||
(int64_t)source_stat.st_mtime * (int64_t)CBM_NSEC_PER_SEC;
|
||||
int64_t source_mtime_ns = (int64_t)source_stat.st_mtime * (int64_t)CBM_NSEC_PER_SEC;
|
||||
#else
|
||||
int64_t source_mtime_ns =
|
||||
((int64_t)source_stat.st_mtim.tv_sec * (int64_t)CBM_NSEC_PER_SEC) +
|
||||
(int64_t)source_stat.st_mtim.tv_nsec;
|
||||
int64_t source_mtime_ns = ((int64_t)source_stat.st_mtim.tv_sec * (int64_t)CBM_NSEC_PER_SEC) +
|
||||
(int64_t)source_stat.st_mtim.tv_nsec;
|
||||
#endif
|
||||
ASSERT_EQ(cbm_store_upsert_file_hash(store, "test-project", "main.go", "",
|
||||
source_mtime_ns, source_stat.st_size),
|
||||
ASSERT_EQ(cbm_store_upsert_file_hash(store, "test-project", "main.go", "", source_mtime_ns,
|
||||
source_stat.st_size),
|
||||
CBM_STORE_OK);
|
||||
cbm_project_t project = {0};
|
||||
ASSERT_EQ(cbm_store_get_project(store, "test-project", &project), CBM_STORE_OK);
|
||||
@@ -2702,8 +2700,7 @@ TEST(tool_check_index_coverage_accepts_truncated_ignored_catalog_for_fresh_path_
|
||||
.coverage_version = 1,
|
||||
.hash_records_complete = true,
|
||||
};
|
||||
ASSERT_EQ(cbm_store_coverage_replace_ex(store, "test-project", NULL, 0, &meta),
|
||||
CBM_STORE_OK);
|
||||
ASSERT_EQ(cbm_store_coverage_replace_ex(store, "test-project", NULL, 0, &meta), CBM_STORE_OK);
|
||||
cbm_project_free_fields(&project);
|
||||
|
||||
char *response = cbm_mcp_handle_tool(
|
||||
@@ -4351,12 +4348,12 @@ TEST(search_code_full_preserves_utf8_source) {
|
||||
.end_line = 2};
|
||||
ASSERT_GT(cbm_store_upsert_node(st, §ion), 0);
|
||||
|
||||
char *resp = cbm_mcp_server_handle(
|
||||
srv, "{\"jsonrpc\":\"2.0\",\"id\":97,\"method\":\"tools/call\","
|
||||
"\"params\":{\"name\":\"search_code\",\"arguments\":{"
|
||||
"\"project\":\"utf8-search\",\"pattern\":\"accounting-design\","
|
||||
"\"file_pattern\":\"*.md\",\"path_filter\":\"^design/\","
|
||||
"\"mode\":\"full\",\"format\":\"json\",\"limit\":5}}}");
|
||||
char *resp =
|
||||
cbm_mcp_server_handle(srv, "{\"jsonrpc\":\"2.0\",\"id\":97,\"method\":\"tools/call\","
|
||||
"\"params\":{\"name\":\"search_code\",\"arguments\":{"
|
||||
"\"project\":\"utf8-search\",\"pattern\":\"accounting-design\","
|
||||
"\"file_pattern\":\"*.md\",\"path_filter\":\"^design/\","
|
||||
"\"mode\":\"full\",\"format\":\"json\",\"limit\":5}}}");
|
||||
ASSERT_NOT_NULL(resp);
|
||||
char *inner = extract_text_content(resp);
|
||||
ASSERT_NOT_NULL(inner);
|
||||
@@ -4408,11 +4405,11 @@ TEST(search_code_raw_match_preserves_utf8_content) {
|
||||
.end_line = 1};
|
||||
ASSERT_GT(cbm_store_upsert_node(st, &node), 0);
|
||||
|
||||
char *resp = cbm_mcp_server_handle(
|
||||
srv, "{\"jsonrpc\":\"2.0\",\"id\":98,\"method\":\"tools/call\","
|
||||
"\"params\":{\"name\":\"search_code\",\"arguments\":{"
|
||||
"\"project\":\"test-project\",\"pattern\":\"raw-\","
|
||||
"\"file_pattern\":\"*.md\",\"format\":\"json\",\"limit\":5}}}");
|
||||
char *resp =
|
||||
cbm_mcp_server_handle(srv, "{\"jsonrpc\":\"2.0\",\"id\":98,\"method\":\"tools/call\","
|
||||
"\"params\":{\"name\":\"search_code\",\"arguments\":{"
|
||||
"\"project\":\"test-project\",\"pattern\":\"raw-\","
|
||||
"\"file_pattern\":\"*.md\",\"format\":\"json\",\"limit\":5}}}");
|
||||
ASSERT_NOT_NULL(resp);
|
||||
char *inner = extract_text_content(resp);
|
||||
ASSERT_NOT_NULL(inner);
|
||||
@@ -4459,11 +4456,11 @@ TEST(search_code_context_preserves_utf8_context) {
|
||||
.end_line = 3};
|
||||
ASSERT_GT(cbm_store_upsert_node(st, &node), 0);
|
||||
|
||||
char *resp = cbm_mcp_server_handle(
|
||||
srv, "{\"jsonrpc\":\"2.0\",\"id\":99,\"method\":\"tools/call\","
|
||||
"\"params\":{\"name\":\"search_code\",\"arguments\":{"
|
||||
"\"project\":\"test-project\",\"pattern\":\"context-needle\","
|
||||
"\"format\":\"json\",\"context\":1,\"limit\":5}}}");
|
||||
char *resp =
|
||||
cbm_mcp_server_handle(srv, "{\"jsonrpc\":\"2.0\",\"id\":99,\"method\":\"tools/call\","
|
||||
"\"params\":{\"name\":\"search_code\",\"arguments\":{"
|
||||
"\"project\":\"test-project\",\"pattern\":\"context-needle\","
|
||||
"\"format\":\"json\",\"context\":1,\"limit\":5}}}");
|
||||
ASSERT_NOT_NULL(resp);
|
||||
char *inner = extract_text_content(resp);
|
||||
ASSERT_NOT_NULL(inner);
|
||||
@@ -4507,8 +4504,8 @@ TEST(search_code_invalid_utf8_still_returns_valid_json) {
|
||||
char invalid_path[512];
|
||||
snprintf(invalid_path, sizeof(invalid_path), "%s/project/invalid.md", tmp);
|
||||
static const unsigned char invalid_source[] = {
|
||||
'i', 'n', 'v', 'a', 'l', 'i', 'd', '-', 'n', 'e', 'e', 'd', 'l', 'e', '\n',
|
||||
'c', 'o', 'n', 't', 'e', 'x', 't', ' ', 0xFF, '\n',
|
||||
'i', 'n', 'v', 'a', 'l', 'i', 'd', '-', 'n', 'e', 'e', 'd', 'l',
|
||||
'e', '\n', 'c', 'o', 'n', 't', 'e', 'x', 't', ' ', 0xFF, '\n',
|
||||
};
|
||||
FILE *fp = cbm_fopen(invalid_path, "wb");
|
||||
ASSERT_NOT_NULL(fp);
|
||||
@@ -4526,11 +4523,11 @@ TEST(search_code_invalid_utf8_still_returns_valid_json) {
|
||||
.end_line = 2};
|
||||
ASSERT_GT(cbm_store_upsert_node(st, &node), 0);
|
||||
|
||||
char *resp = cbm_mcp_server_handle(
|
||||
srv, "{\"jsonrpc\":\"2.0\",\"id\":100,\"method\":\"tools/call\","
|
||||
"\"params\":{\"name\":\"search_code\",\"arguments\":{"
|
||||
"\"project\":\"test-project\",\"pattern\":\"invalid-needle\","
|
||||
"\"mode\":\"full\",\"format\":\"json\",\"limit\":5}}}");
|
||||
char *resp =
|
||||
cbm_mcp_server_handle(srv, "{\"jsonrpc\":\"2.0\",\"id\":100,\"method\":\"tools/call\","
|
||||
"\"params\":{\"name\":\"search_code\",\"arguments\":{"
|
||||
"\"project\":\"test-project\",\"pattern\":\"invalid-needle\","
|
||||
"\"mode\":\"full\",\"format\":\"json\",\"limit\":5}}}");
|
||||
ASSERT_NOT_NULL(resp);
|
||||
char *inner = extract_text_content(resp);
|
||||
ASSERT_NOT_NULL(inner);
|
||||
@@ -4921,6 +4918,35 @@ TEST(search_code_windows_prefilter_precedes_content_scan) {
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Windows raw scans must pin the PowerShell pipe to UTF-8: PS 5.1 otherwise
|
||||
* emits stdout in the console OEM codepage and non-ASCII content degrades to
|
||||
* '?' depending on the inherited console CP (seen as test_mcp raw-Русский
|
||||
* mojibake on CI). Every builder variant must carry the prelude. */
|
||||
TEST(search_code_windows_scan_pins_utf8_output) {
|
||||
#ifdef _WIN32
|
||||
static const char prelude[] =
|
||||
"powershell -Command \"[Console]::OutputEncoding=[System.Text.Encoding]::UTF8; ";
|
||||
char command[CBM_SZ_4K];
|
||||
struct {
|
||||
bool scoped;
|
||||
const char *file_pattern;
|
||||
} cases[] = {{true, "*.go"}, /* scoped + prefilterable pattern */
|
||||
{true, "*x*z*.go"}, /* scoped + non-prefilterable pattern */
|
||||
{true, NULL}, /* scoped, no pattern */
|
||||
{false, "*.go"}, /* unscoped + pattern */
|
||||
{false, NULL}}; /* unscoped, no pattern */
|
||||
for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
|
||||
cbm_search_code_build_grep_cmd(command, sizeof(command), false, cases[i].scoped,
|
||||
cases[i].file_pattern, "C:/tmp/pattern", "C:/tmp/filelist",
|
||||
"C:/tmp/root");
|
||||
ASSERT_TRUE(strncmp(command, prelude, sizeof(prelude) - 1) == 0);
|
||||
}
|
||||
PASS();
|
||||
#else
|
||||
SKIP_PLATFORM("PowerShell scan encoding applies on Windows");
|
||||
#endif
|
||||
}
|
||||
|
||||
/* issue #283: search_code with regex=true and a syntactically invalid pattern
|
||||
* must return an explicit error, not an empty result indistinguishable from a
|
||||
* legitimate no-match. */
|
||||
@@ -8769,10 +8795,10 @@ TEST(tool_resolve_store_by_internal_name_issue704) {
|
||||
ASSERT_NULL(strstr(list, "ghost704")); /* 0-byte ghost filtered (RED before) */
|
||||
free(list);
|
||||
|
||||
char *page = cbm_mcp_server_handle(
|
||||
srv, "{\"jsonrpc\":\"2.0\",\"id\":11,\"method\":\"tools/call\","
|
||||
"\"params\":{\"name\":\"list_projects\","
|
||||
"\"arguments\":{\"offset\":0,\"limit\":1}}}");
|
||||
char *page =
|
||||
cbm_mcp_server_handle(srv, "{\"jsonrpc\":\"2.0\",\"id\":11,\"method\":\"tools/call\","
|
||||
"\"params\":{\"name\":\"list_projects\","
|
||||
"\"arguments\":{\"offset\":0,\"limit\":1}}}");
|
||||
ASSERT_NOT_NULL(page);
|
||||
ASSERT_NOT_NULL(strstr(page, "\\\"total\\\":3"));
|
||||
ASSERT_NOT_NULL(strstr(page, "\\\"limit\\\":1"));
|
||||
@@ -11223,6 +11249,7 @@ SUITE(mcp) {
|
||||
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_scan_pins_utf8_output);
|
||||
RUN_TEST(search_code_invalid_regex_errors_issue283);
|
||||
RUN_TEST(search_code_literal_pipe_warns_issue282);
|
||||
RUN_TEST(search_code_reports_phase_timings_only_in_debug_mode);
|
||||
|
||||
Reference in new issue
Block a user