Skip to content

Commit 9fbdac2

Browse files
authored
Merge pull request DeusData#813 from DeusData/distill/702-installer-utf8
fix(windows): unswallow installer failures, quote CreateProcessW args, document MotW
2 parents 5f6461e + 138bd86 commit 9fbdac2

5 files changed

Lines changed: 348 additions & 7 deletions

File tree

‎README.md‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,16 @@ Invoke-WebRequest -Uri https://raw.githubusercontent.com/DeusData/codebase-memor
5959
# 2. (Optional but recommended) Inspect the script
6060
notepad install.ps1
6161
62-
# 3. Run it
62+
# 3. Unblock the downloaded file (removes Mark-of-the-Web restriction added by browsers/Invoke-WebRequest)
63+
Unblock-File .\install.ps1
64+
65+
# 4. Run it
6366
.\install.ps1
6467
6568
```
6669

70+
> **Note:** If you see a script execution policy error, run `Set-ExecutionPolicy -Scope Process Bypass` first, or invoke with `PowerShell -ExecutionPolicy Bypass -File .\install.ps1`.
71+
6772
Options: `--ui` (graph visualization), `--skip-config` (binary only, no agent setup), `--dir=<path>` (custom location).
6873

6974
Restart your coding agent. Say **"Index this project"** — done.
@@ -86,6 +91,7 @@ Restart your coding agent. Say **"Index this project"** — done.
8691
Windows (PowerShell):
8792
```powershell
8893
Expand-Archive codebase-memory-mcp-windows-amd64.zip -DestinationPath .
94+
Unblock-File .\install.ps1
8995
.\install.ps1
9096
```
9197

‎install.ps1‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -131,11 +131,13 @@ if ($SkipConfig) {
131131
} else {
132132
Write-Host ""
133133
Write-Host "Configuring coding agents..."
134-
try {
135-
& $Dest install -y 2>&1 | Write-Host
136-
} catch {
137-
Write-Host "Agent configuration failed (non-fatal)."
138-
Write-Host "Run manually: codebase-memory-mcp install"
134+
& $Dest install -y 2>&1 | Write-Host
135+
if ($LASTEXITCODE -ne 0) {
136+
Write-Host ""
137+
Write-Host "error: agent configuration failed (exit code $LASTEXITCODE)" -ForegroundColor Red
138+
Write-Host "The binary was installed, but no coding agents were configured."
139+
Write-Host "Run manually to configure: `"$Dest`" install"
140+
exit 1
139141
}
140142
}
141143

‎src/foundation/compat_fs.c‎

Lines changed: 130 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77
#include "foundation/constants.h"
88
#include "foundation/compat_fs.h"
9+
#include "foundation/compat_fs_internal.h"
910

1011
#include <stdio.h>
1112
#include <stdlib.h>
@@ -197,11 +198,139 @@ int cbm_rmdir(const char *path) {
197198
return ret;
198199
}
199200

201+
/* Build a properly-quoted Windows command line from an argv array.
202+
* Returns a heap-allocated wide string, or NULL on allocation failure.
203+
* Quoting follows the MSVC CRT convention: arguments containing spaces,
204+
* tabs, or double-quotes are wrapped in double-quotes, with backslashes
205+
* before a closing quote doubled and the quote itself escaped. Argument
206+
* bytes are treated as UTF-8 and converted to wide via cbm_utf8_to_wide,
207+
* so non-ASCII arguments (e.g. a non-ASCII %USERPROFILE%) survive intact.
208+
* Declared in compat_fs_internal.h so the test suite can drive it. */
209+
wchar_t *cbm_build_cmdline(const char *const *argv) {
210+
/* First pass: compute required buffer size. */
211+
size_t total = 1; /* NUL terminator */
212+
for (int i = 0; argv[i]; i++) {
213+
const char *arg = argv[i];
214+
bool needs_quote = (arg[0] == '\0');
215+
for (const char *p = arg; *p; p++) {
216+
if (*p == ' ' || *p == '\t' || *p == '"') {
217+
needs_quote = true;
218+
}
219+
}
220+
if (i > 0) {
221+
total++; /* space separator */
222+
}
223+
if (needs_quote) {
224+
total += 2; /* opening and closing quote */
225+
size_t backslashes = 0;
226+
for (const char *p = arg; *p; p++) {
227+
if (*p == '\\') {
228+
backslashes++;
229+
} else if (*p == '"') {
230+
total += backslashes + 1; /* double backslashes + escape backslash */
231+
backslashes = 0;
232+
} else {
233+
backslashes = 0;
234+
}
235+
total++;
236+
}
237+
/* Trailing backslashes before closing quote must be doubled. */
238+
total += backslashes;
239+
} else {
240+
total += strlen(arg);
241+
}
242+
}
243+
244+
/* Build the quoted command line in UTF-8 first, then widen it as a
245+
* whole via cbm_utf8_to_wide. Every character the quoting logic acts
246+
* on (space, tab, '"', '\\') is ASCII and, by UTF-8's design, never
247+
* appears inside a multibyte sequence, so operating on raw bytes here
248+
* is safe and keeps multibyte argument bytes intact for conversion. */
249+
char *buf = (char *)malloc(total);
250+
if (!buf) {
251+
return NULL;
252+
}
253+
254+
/* Second pass: write the command line bytes. */
255+
char *w = buf;
256+
for (int i = 0; argv[i]; i++) {
257+
const char *arg = argv[i];
258+
bool needs_quote = (arg[0] == '\0');
259+
for (const char *p = arg; *p; p++) {
260+
if (*p == ' ' || *p == '\t' || *p == '"') {
261+
needs_quote = true;
262+
break;
263+
}
264+
}
265+
if (i > 0) {
266+
*w++ = ' ';
267+
}
268+
if (needs_quote) {
269+
*w++ = '"';
270+
size_t backslashes = 0;
271+
for (const char *p = arg; *p; p++) {
272+
if (*p == '\\') {
273+
backslashes++;
274+
*w++ = '\\';
275+
} else if (*p == '"') {
276+
/* Double the preceding backslashes, then escape the quote. */
277+
for (size_t b = 0; b < backslashes; b++) {
278+
*w++ = '\\';
279+
}
280+
*w++ = '\\';
281+
*w++ = '"';
282+
backslashes = 0;
283+
} else {
284+
backslashes = 0;
285+
*w++ = *p;
286+
}
287+
}
288+
/* Double trailing backslashes before the closing quote. */
289+
for (size_t b = 0; b < backslashes; b++) {
290+
*w++ = '\\';
291+
}
292+
*w++ = '"';
293+
} else {
294+
for (const char *p = arg; *p; p++) {
295+
*w++ = *p;
296+
}
297+
}
298+
}
299+
*w = '\0';
300+
301+
wchar_t *out = cbm_utf8_to_wide(buf);
302+
free(buf);
303+
return out;
304+
}
305+
200306
int cbm_exec_no_shell(const char *const *argv) {
201307
if (!argv || !argv[0]) {
202308
return CBM_NOT_FOUND;
203309
}
204-
return (int)_spawnvp(_P_WAIT, argv[0], argv);
310+
311+
wchar_t *cmdline = cbm_build_cmdline(argv);
312+
if (!cmdline) {
313+
return CBM_NOT_FOUND;
314+
}
315+
316+
STARTUPINFOW si;
317+
PROCESS_INFORMATION pi;
318+
memset(&si, 0, sizeof(si));
319+
memset(&pi, 0, sizeof(pi));
320+
si.cb = sizeof(si);
321+
322+
if (!CreateProcessW(NULL, cmdline, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)) {
323+
free(cmdline);
324+
return CBM_NOT_FOUND;
325+
}
326+
free(cmdline);
327+
328+
WaitForSingleObject(pi.hProcess, INFINITE);
329+
DWORD exit_code = (DWORD)CBM_NOT_FOUND;
330+
GetExitCodeProcess(pi.hProcess, &exit_code);
331+
CloseHandle(pi.hProcess);
332+
CloseHandle(pi.hThread);
333+
return (int)exit_code;
205334
}
206335

207336
#else /* POSIX */
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* compat_fs_internal.h — Internal helpers exposed for testing.
3+
*
4+
* These functions are implementation details of compat_fs.c; they are
5+
* declared here only so that the test suite can drive them directly.
6+
* Production code outside compat_fs.c should use the public APIs in
7+
* compat_fs.h instead.
8+
*/
9+
#ifndef CBM_FOUNDATION_COMPAT_FS_INTERNAL_H
10+
#define CBM_FOUNDATION_COMPAT_FS_INTERNAL_H
11+
12+
#ifdef _WIN32
13+
14+
#include <wchar.h>
15+
16+
/*
17+
* Build a properly-quoted Windows command line from a NULL-terminated
18+
* argv array. This is the quoting step underlying cbm_exec_no_shell on
19+
* Windows: it is what turns {"taskkill", "/FI", "IMAGENAME eq foo.exe"}
20+
* into `taskkill /FI "IMAGENAME eq foo.exe"` rather than three bare
21+
* tokens (the #697 regression).
22+
*
23+
* Quoting follows the MSVC/CommandLineToArgvW convention: an argument is
24+
* wrapped in double-quotes when it is empty or contains a space, tab, or
25+
* double-quote; backslashes immediately before a quote (literal or the
26+
* closing one) are doubled, and embedded double-quotes are escaped with a
27+
* backslash.
28+
*
29+
* Returns a heap-allocated wide string the caller must free(), or NULL on
30+
* allocation failure.
31+
*/
32+
wchar_t *cbm_build_cmdline(const char *const *argv);
33+
34+
#endif /* _WIN32 */
35+
36+
#endif /* CBM_FOUNDATION_COMPAT_FS_INTERNAL_H */

0 commit comments

Comments
 (0)