Windows: official port -- UCRT static build, UTF-8 throughout, Low IL sandbox, ncurses TUI, installer; v0.6.0
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

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:
WHC
2026-10-06 23:08:14 +08:00
parent 9862c29f58
commit 2a43ae6622
84 changed files with 9784 additions and 1083 deletions
+209
View File
@@ -4,6 +4,8 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <wchar.h>
#include <wctype.h>
static int buf_reserve(cst_buf_t *b, size_t extra) {
if (b->len + extra + 1 <= b->cap)
@@ -153,6 +155,213 @@ char *cst_utf8_sanitize(const char *s, size_t n, size_t *out_len) {
return out;
}
int cst_utf8_valid(const char *s, size_t n) {
const unsigned char *p = (const unsigned char *)s;
for (size_t i = 0; i < n;) {
size_t k = utf8_seq_ok(p + i, n - i);
if (k == 0)
return 0;
i += k;
}
return 1;
}
size_t cst_bom_len(const char *s, size_t n) {
return n >= 3 && memcmp(s, "\xEF\xBB\xBF", 3) == 0 ? 3 : 0;
}
void cst_strip_bom(char *s, size_t *len) {
if (!s)
return;
size_t n = len ? *len : strlen(s);
if (!cst_bom_len(s, n))
return;
memmove(s, s + 3, n - 3 + 1);
if (len)
*len = n - 3;
}
size_t cst_crlf_to_lf(char *s, size_t n) {
size_t o = 0;
for (size_t i = 0; i < n; i++)
if (!(s[i] == '\r' && i + 1 < n && s[i + 1] == '\n'))
s[o++] = s[i];
if (o < n)
s[o] = 0;
return o;
}
int cst_text_is_crlf(const char *s, size_t n) {
int crlf = 0;
for (size_t i = 0; s && i < n; i++) {
if (s[i] != '\n')
continue;
if (i == 0 || s[i - 1] != '\r')
return 0;
crlf = 1;
}
return crlf;
}
char *cst_text_restyle(const char *s, size_t n, int crlf, int bom,
size_t *out_len) {
size_t skip = cst_bom_len(s, n), extra = bom ? 3 : 0;
for (size_t i = skip; crlf && i < n; i++)
extra += s[i] == '\n' && (i == skip || s[i - 1] != '\r');
char *o = malloc(n - skip + extra + 1);
if (!o)
return NULL;
size_t k = 0;
if (bom) {
memcpy(o, "\xEF\xBB\xBF", 3);
k = 3;
}
for (size_t i = skip; i < n; i++) {
if (crlf && s[i] == '\n' && (i == skip || s[i - 1] != '\r'))
o[k++] = '\r';
o[k++] = s[i];
}
o[k] = 0;
if (out_len)
*out_len = k;
return o;
}
/* Next code point of a UTF-8 string; a stray byte stands for itself. */
static int utf8_next(const char **ps) {
const unsigned char *p = (const unsigned char *)*ps;
size_t left = 0;
while (left < 4 && p[left])
left++;
size_t k = utf8_seq_ok(p, left);
if (k <= 1) {
*ps += 1;
return p[0];
}
int c = p[0] & (0x7F >> k);
for (size_t i = 1; i < k; i++)
c = (c << 6) | (p[i] & 0x3F);
*ps += k;
return c;
}
static int fold_cp(int c) {
if (c >= 'A' && c <= 'Z')
return c + 32;
if (c > 0x7F && c <= 0xFFFF)
return (int)towlower((wint_t)c);
return c;
}
/* p points just past '['. 1/0 = (no) match with *pp moved past ']';
* -1 = no closing ']' (the '[' is then a literal). */
static int fnm_bracket(const char **pp, int c, int fold) {
const char *p = *pp;
int neg = 0, hit = 0;
if (*p == '!' || *p == '^') {
neg = 1;
p++;
}
int fc = fold ? fold_cp(c) : c;
for (int first = 1; *p && (*p != ']' || first); first = 0) {
if (p[0] == '[' && p[1] == ':') {
const char *e = strstr(p + 2, ":]");
if (e) {
char name[16];
size_t nl = (size_t)(e - (p + 2));
if (nl < sizeof(name)) {
memcpy(name, p + 2, nl);
name[nl] = 0;
wctype_t t = wctype(name);
if (t && c <= 0xFFFF && iswctype((wint_t)c, t))
hit = 1;
}
p = e + 2;
continue;
}
}
if (*p == '\\' && p[1])
p++;
int lo = utf8_next(&p), hi = lo;
if (p[0] == '-' && p[1] && p[1] != ']') {
p++;
if (*p == '\\' && p[1])
p++;
hi = utf8_next(&p);
}
if ((c >= lo && c <= hi) ||
(fold && fc >= fold_cp(lo) && fc <= fold_cp(hi)))
hit = 1;
}
if (*p != ']')
return -1;
*pp = p + 1;
return hit != neg;
}
int cst_fnmatch(const char *pat, const char *str, int flags) {
#ifdef _WIN32
flags |= CST_FNM_CASEFOLD;
#endif
int fold = (flags & CST_FNM_CASEFOLD) != 0;
const char *p = pat, *s = str;
const char *star_p = NULL, *star_s = NULL;
if ((flags & CST_FNM_PERIOD) && s[0] == '.' && p[0] != '.' &&
!(p[0] == '\\' && p[1] == '.'))
return 1;
while (*s) {
if (*p == '*') {
while (*p == '*')
p++;
if (!*p)
return 0;
star_p = p;
star_s = s;
continue;
}
const char *sn = s;
int c = utf8_next(&sn);
if (*p == '?') {
p++;
s = sn;
continue;
}
if (*p == '[') {
const char *q = p + 1;
int r = fnm_bracket(&q, c, fold);
if (r == 1) {
p = q;
s = sn;
continue;
}
if (r == 0)
goto backtrack;
}
{
const char *q = p;
if (*q == '\\' && q[1])
q++;
if (*q) {
int pc = utf8_next(&q);
if (pc == c || (fold && fold_cp(pc) == fold_cp(c))) {
p = q;
s = sn;
continue;
}
}
}
backtrack:
if (!star_p)
return 1;
utf8_next(&star_s);
s = star_s;
p = star_p;
}
while (*p == '*')
p++;
return *p ? 1 : 0;
}
uint64_t cst_fnv1a64(uint64_t h, const void *data, size_t n) {
const unsigned char *p = data;
for (size_t i = 0; i < n; i++) {