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
This commit is contained in:
@@ -0,0 +1,238 @@
|
||||
/* 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;
|
||||
}
|
||||
Reference in New Issue
Block a user