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:
WHC
2026-10-06 23:08:14 +08:00
parent 9862c29f58
commit 2a43ae6622
84 changed files with 9784 additions and 1083 deletions
+24 -42
View File
@@ -9,10 +9,7 @@
#include <string.h>
#include <sys/stat.h>
#ifndef _WIN32
#include <dirent.h>
#include <unistd.h>
#endif
#include "cst_agent.h"
#include "cst_undo.h"
@@ -75,7 +72,7 @@ static int put(const char *p, const char *d, size_t n) {
}
static int manifest_has(const char *mf, const char *rel) {
FILE *fp = fopen(mf, "r");
FILE *fp = fopen(mf, "rb");
if (!fp)
return 0;
char line[1400];
@@ -120,7 +117,7 @@ int cst_undo_snapshot(const char *ws, const char *sid, const char *tag,
return 0;
int idx = 0;
{
FILE *fp = fopen(mf, "r");
FILE *fp = fopen(mf, "rb");
if (fp) {
int ch;
while ((ch = fgetc(fp)) != EOF)
@@ -144,7 +141,7 @@ int cst_undo_snapshot(const char *ws, const char *sid, const char *tag,
free(data);
}
}
FILE *fp = fopen(mf, "a");
FILE *fp = fopen(mf, "ab");
if (!fp)
return -1;
fprintf(fp, "%d\t%s\t%s\n", idx, state, rel);
@@ -152,35 +149,34 @@ int cst_undo_snapshot(const char *ws, const char *sid, const char *tag,
return 0;
}
#ifndef _WIN32
static int latest_tag(const char *root, char *tag, size_t n) {
DIR *d = opendir(root);
os_dir_t *d = os_dir_open(root);
if (!d)
return -1;
struct dirent *de;
const os_dirent_t *de;
tag[0] = 0;
while ((de = readdir(d)) != NULL) {
if (de->d_name[0] == '.')
while ((de = os_dir_next(d)) != NULL) {
if (de->name[0] == '.')
continue;
if (strcmp(de->d_name, tag) > 0)
snprintf(tag, n, "%s", de->d_name);
if (strcmp(de->name, tag) > 0)
snprintf(tag, n, "%s", de->name);
}
closedir(d);
os_dir_close(d);
return tag[0] ? 0 : -1;
}
static void rm_dir(const char *dir) {
DIR *d = opendir(dir);
os_dir_t *d = os_dir_open(dir);
if (d) {
struct dirent *de;
while ((de = readdir(d)) != NULL) {
if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0)
const os_dirent_t *de;
while ((de = os_dir_next(d)) != NULL) {
if (strcmp(de->name, ".") == 0 || strcmp(de->name, "..") == 0)
continue;
char p[OS_PATH_MAX];
if (snprintf(p, sizeof(p), "%s/%s", dir, de->d_name) < (int)sizeof(p))
if (snprintf(p, sizeof(p), "%s/%s", dir, de->name) < (int)sizeof(p))
unlink(p);
}
closedir(d);
os_dir_close(d);
}
rmdir(dir);
}
@@ -189,14 +185,14 @@ int cst_undo_count(const char *ws, const char *sid) {
char root[OS_PATH_MAX];
if (undo_root(ws, sid, root, sizeof(root)) != 0)
return 0;
DIR *d = opendir(root);
os_dir_t *d = os_dir_open(root);
if (!d)
return 0;
int n = 0;
struct dirent *de;
while ((de = readdir(d)) != NULL)
n += de->d_name[0] != '.';
closedir(d);
const os_dirent_t *de;
while ((de = os_dir_next(d)) != NULL)
n += de->name[0] != '.';
os_dir_close(d);
return n;
}
@@ -214,7 +210,7 @@ int cst_undo_restore_last(const char *ws, const char *sid, char *report,
snprintf(report, report_sz, "undo path too long");
return -1;
}
FILE *fp = fopen(mf, "r");
FILE *fp = fopen(mf, "rb");
cst_buf_t rep = {0};
int restored = 0, failed = 0;
if (fp) {
@@ -231,10 +227,10 @@ int cst_undo_restore_last(const char *ws, const char *sid, char *report,
*t3++ = 0;
const char *rel = t3, *state = t2;
char full[OS_PATH_MAX];
struct stat lst;
os_stat_t lst;
if (!rel_ok(rel) || os_path_join(full, sizeof(full), ws, rel) != 0)
continue;
if (lstat(full, &lst) == 0 && S_ISLNK(lst.st_mode)) {
if (os_lstat(full, &lst) == 0 && lst.is_link) {
cst_buf_printf(&rep, "skipped %s (now a symlink)\n", rel);
failed++;
continue;
@@ -280,17 +276,3 @@ int cst_undo_restore_last(const char *ws, const char *sid, char *report,
cst_buf_free(&rep);
return failed && !restored ? -1 : 0;
}
#else
int cst_undo_count(const char *ws, const char *sid) {
(void)ws;
(void)sid;
return 0;
}
int cst_undo_restore_last(const char *ws, const char *sid, char *report,
size_t report_sz) {
(void)ws;
(void)sid;
snprintf(report, report_sz, "undo is not available on Windows");
return -1;
}
#endif