Files
Hermit/tests/test_win_utf8.c
WHC 2a43ae6622
ci / linux (clang) (push) Canceled after 0s
ci / linux (gcc) (push) Canceled after 0s
ci / sanitizers (push) Canceled after 0s
ci / musl (push) Canceled after 0s
ci / macos (push) Canceled after 0s
Windows: official port -- UCRT static build, UTF-8 throughout, Low IL sandbox, ncurses TUI, installer; v0.6.0
Build
  * cross build switches to pkgsCross.ucrt64 (shell-win.nix); curl (Schannel),
    winpthreads and ncurses 6.6 are linked statically, so the release is
    cst-agent.exe + busybox.exe (busybox-w32 Unicode) with no extra DLLs
  * windres embeds a manifest with activeCodePage=UTF-8 and a version resource
  * ncurses 6.6 Windows console driver patches (mouse button state, A_DIM,
    wide-char declarations): scripts/patches/
  * scripts/bootstrap_msys2.sh builds the same release natively in MSYS2
    UCRT64; PR #1's MINGW64 notes now point there
  * vendor/tre-regex (musl's TRE) provides <regex.h> on MinGW

UTF-8
  * paths cross the system call boundary as UTF-16 (*W APIs); console code
    pages set to 65001 and restored on exit; files are read and written in
    binary mode
  * child output that is not valid UTF-8 is decoded from the console, OEM or
    ANSI system code page
  * read_file hides CR; edit_file, apply_patch and write_file keep CRLF line
    endings and a UTF-8 BOM

OS layer and processes
  * os_dir_* and cst_fnmatch replace the POSIX-only directory walks in
    subagent, plugins, skills, undo and fs_tools; realpath / path_under and
    symlink rejection work on Windows
  * every child goes through CreateProcessW with its own environment block
    inside a Job Object, so timeouts and Esc end the whole process tree

Sandbox (auto mode, "auto · lowil")
  * run_shell runs with a restricted Low integrity token; the workspace gets
    an inheritable Low label once, the sandbox TEMP is labeled Low, and the
    credentials file gets a no-read-up label
  * drive roots, the profile and system folders are never labeled
  * network commands still ask, since Low IL does not block the network;
    Access denied and MSYS2's 0xc0000142 offer a rerun outside the sandbox
  * /sandbox lists labeled folders, /sandbox reset-labels restores them

TUI
  * raster console fonts are switched to Consolas / NSimSun while running
  * the home logo falls back to text when the console font lacks U+2580
  * mouse drag copy goes to the Windows clipboard

Distribution
  * scripts/install_windows.ps1 installs a Gitea release into
    %LOCALAPPDATA%\Programs\cst-agent and adds it to the user PATH;
    cst-agent-update.cmd reruns it; scripts/release_windows.sh uploads the
    zip with the token taken from the environment
  * scripts/run_windows.bat is gone; docs/README-windows.md is rewritten

Tests
  * new test_lowil, test_win_utf8 and test_os_dir; tests made portable
    (cst_test_util.h)
  * scripts/win_test.sh and scripts/winvm/ run the suite on English and
    Chinese Windows Server VMs as Administrator, a standard user and a
    Chinese user name: 29/29 on all six
2026-10-06 23:08:14 +08:00

239 lines
8.2 KiB
C

/* UTF-8 everywhere: Chinese workspace and file names through the file
* tools and the shell, CRLF and BOM files edited by apply_patch, write_file
* and edit_file without losing their style, and (Windows) child output in
* the OEM code page turned into UTF-8. */
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include "cst_agent.h"
#include "cst_util.h"
#include "os_compat.h"
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "cst_test_util.h"
static int nfail;
#define EXPECT(cond, msg) \
do { \
if (!(cond)) { \
fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, msg); \
nfail++; \
} \
} while (0)
static char g_ws[1100];
/* tool(name, key, value, key, value, ..., NULL) */
static int tool(cst_tool_result_t *r, const char *name, ...) {
ctl_tool_call_t c;
memset(&c, 0, sizeof(c));
snprintf(c.id, sizeof(c.id), "t");
snprintf(c.name, sizeof(c.name), "%s", name);
va_list ap;
va_start(ap, name);
const char *k;
while ((k = va_arg(ap, const char *)) != NULL) {
const char *v = va_arg(ap, const char *);
snprintf(c.args[c.nargs].key, sizeof(c.args[c.nargs].key), "%s", k);
c.args[c.nargs].value = (char *)v;
c.args[c.nargs].value_len = strlen(v);
c.nargs++;
}
va_end(ap);
memset(r, 0, sizeof(*r));
cst_tool_dispatch(g_ws, &c, r);
return r->exit_code;
}
static char *slurp(const char *rel, size_t *n) {
char p[1200];
snprintf(p, sizeof(p), "%s/%s", g_ws, rel);
FILE *f = fopen(p, "rb");
if (!f)
return NULL;
char *b = malloc(1 << 16);
*n = b ? fread(b, 1, (1 << 16) - 1, f) : 0;
if (b)
b[*n] = 0;
fclose(f);
return b;
}
static void spit(const char *rel, const char *s) {
char p[1200];
snprintf(p, sizeof(p), "%s/%s", g_ws, rel);
FILE *f = fopen(p, "wb");
if (f) {
fputs(s, f);
fclose(f);
}
}
static int file_is(const char *rel, const char *want) {
size_t n = 0;
char *b = slurp(rel, &n);
int ok = b && n == strlen(want) && memcmp(b, want, n) == 0;
if (!ok)
fprintf(stderr, " %s is \"%s\"\n", rel, b ? b : "(missing)");
free(b);
return ok;
}
static void test_patch_buffer(void) {
char *out = NULL, err[256];
size_t n = 0;
int nh = 0;
const char *crlf = "a\r\nb\r\nc\r\n";
const char *hunk = "@@ -1,3 +1,3 @@\n a\n-b\n+B\n c\n";
EXPECT(cst_patch_apply_buffer(crlf, strlen(crlf), hunk, strlen(hunk), &out,
&n, &nh, err, sizeof(err)) == 0,
"LF hunk applies to a CRLF file");
EXPECT(out && strcmp(out, "a\r\nB\r\nc\r\n") == 0, "CRLF kept");
free(out);
const char *bom = "\xEF\xBB\xBF" "first\nsecond\n";
const char *h2 = "@@ -1,2 +1,2 @@\n-first\n+FIRST\n second\n";
EXPECT(cst_patch_apply_buffer(bom, strlen(bom), h2, strlen(h2), &out, &n,
&nh, err, sizeof(err)) == 0,
"first line matches past the BOM");
EXPECT(out && strcmp(out, "\xEF\xBB\xBF" "FIRST\nsecond\n") == 0,
"BOM kept");
free(out);
const char *lf = "x\ny\n";
const char *h3 = "@@ -1,2 +1,3 @@\n x\n+中文\n y\n";
EXPECT(cst_patch_apply_buffer(lf, strlen(lf), h3, strlen(h3), &out, &n, &nh,
err, sizeof(err)) == 0 &&
out && strcmp(out, "x\n中文\ny\n") == 0,
"LF file stays LF");
free(out);
EXPECT(cst_text_is_crlf("a\r\nb\r\n", 6), "is_crlf");
EXPECT(!cst_text_is_crlf("a\r\nb\n", 5), "mixed is not crlf");
EXPECT(!cst_text_is_crlf("ab", 2), "no newline is not crlf");
}
static void test_tools(void) {
cst_tool_result_t r;
/* Chinese names through write/read/list/glob/grep */
EXPECT(tool(&r, "write_file", "path", "目录/文件.txt", "content",
"你好,世界\n第二行\n", NULL) == 0,
"write_file with a Chinese path");
cst_tool_result_free(&r);
EXPECT(tool(&r, "read_file", "path", "目录/文件.txt", NULL) == 0 && r.out &&
strstr(r.out, "你好,世界"),
"read_file with a Chinese path");
cst_tool_result_free(&r);
EXPECT(tool(&r, "list_dir", "path", "目录", NULL) == 0 && r.out &&
strstr(r.out, "文件.txt"),
"list_dir shows the Chinese name");
cst_tool_result_free(&r);
EXPECT(tool(&r, "glob", "pattern", "**/*.txt", NULL) == 0 && r.out &&
strstr(r.out, "目录/文件.txt"),
"glob finds the Chinese name");
cst_tool_result_free(&r);
EXPECT(tool(&r, "grep", "pattern", "世界", NULL) == 0 && r.out &&
strstr(r.out, "文件.txt"),
"grep matches Chinese text");
cst_tool_result_free(&r);
/* CRLF + BOM file */
spit("win.txt", "\xEF\xBB\xBFone\r\ntwo\r\nthree\r\n");
EXPECT(tool(&r, "read_file", "path", "win.txt", NULL) == 0 && r.out &&
strcmp(r.out, "one\ntwo\nthree\n") == 0,
"read_file hides BOM and CR");
cst_tool_result_free(&r);
EXPECT(tool(&r, "edit_file", "path", "win.txt", "old_string", "two",
"new_string", "二\n2", NULL) == 0,
"edit_file single-line match");
cst_tool_result_free(&r);
EXPECT(file_is("win.txt", "\xEF\xBB\xBFone\r\n二\r\n2\r\nthree\r\n"),
"edit_file inserts CRLF");
EXPECT(tool(&r, "edit_file", "path", "win.txt", "old_string", "one\n二",
"new_string", "壹\n贰", NULL) == 0,
"edit_file multi-line LF match");
cst_tool_result_free(&r);
EXPECT(file_is("win.txt", "\xEF\xBB\xBF壹\r\n贰\r\n2\r\nthree\r\n"),
"edit_file keeps CRLF and BOM");
EXPECT(tool(&r, "apply_patch", "patch",
"--- a/win.txt\n+++ b/win.txt\n@@ -3,2 +3,2 @@\n 2\n-three\n"
"+叁\n",
NULL) == 0,
"apply_patch on a CRLF file");
if (r.exit_code)
fprintf(stderr, " apply_patch: %s\n", r.out ? r.out : "");
cst_tool_result_free(&r);
EXPECT(file_is("win.txt", "\xEF\xBB\xBF壹\r\n贰\r\n2\r\n叁\r\n"),
"apply_patch keeps CRLF and BOM");
EXPECT(tool(&r, "write_file", "path", "win.txt", "content", "new\ntext\n",
NULL) == 0,
"write_file over a CRLF file");
cst_tool_result_free(&r);
EXPECT(file_is("win.txt", "\xEF\xBB\xBFnew\r\ntext\r\n"),
"write_file keeps CRLF and BOM");
EXPECT(tool(&r, "write_file", "path", "new.txt", "content", "lf\n", NULL) ==
0 &&
file_is("new.txt", "lf\n"),
"new files are LF without BOM");
cst_tool_result_free(&r);
/* the shell sees and prints UTF-8 */
EXPECT(tool(&r, "run_shell", "cmd", "cat 目录/文件.txt && echo 完成", NULL) ==
0 &&
r.out && strstr(r.out, "你好,世界") && strstr(r.out, "完成"),
"run_shell with Chinese arguments and output");
if (!r.out || !strstr(r.out, "完成"))
fprintf(stderr, " run_shell: %s\n", r.out ? r.out : "");
cst_tool_result_free(&r);
}
#ifdef _WIN32
static void test_oem_output(void) {
/* "中文" in GBK */
const char gbk[] = "\xD6\xD0\xCE\xC4\r\n";
size_t n = 0;
char *u = os_output_to_utf8(gbk, sizeof(gbk) - 1, &n);
wchar_t b[8] = L"0";
GetLocaleInfoW(LOCALE_SYSTEM_DEFAULT, LOCALE_IDEFAULTCODEPAGE, b, 8);
unsigned oem = (unsigned)wcstoul(b, NULL, 10);
if (oem == 936)
EXPECT(u && strstr(u, "中文"), "GBK output decoded on a 936 system");
else
EXPECT(u != NULL, "non-UTF-8 output is decoded");
free(u);
EXPECT(os_output_to_utf8("中文", strlen("中文"), &n) == NULL,
"UTF-8 output left alone");
printf("test_win_utf8: ACP %u, OEM %u, system OEM %u, console %u\n",
GetACP(), GetOEMCP(), oem, GetConsoleOutputCP());
EXPECT(GetACP() == CP_UTF8, "manifest makes UTF-8 the process code page");
}
#endif
int main(void) {
os_console_init();
char base[1024];
if (test_tmpdir(base, sizeof(base), "cst-utf8") != 0) {
perror("tmpdir");
return 1;
}
snprintf(g_ws, sizeof(g_ws), "%s/项目 空格", base);
test_mkdir(g_ws, 0755);
test_patch_buffer();
test_tools();
#ifdef _WIN32
test_oem_output();
#endif
test_rmrf(base);
if (nfail) {
fprintf(stderr, "test_win_utf8: %d failure(s)\n", nfail);
return 1;
}
puts("test_win_utf8: OK");
return 0;
}