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:
+48
-77
@@ -12,12 +12,7 @@
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#ifndef _WIN32
|
||||
#include <dirent.h>
|
||||
#include <fnmatch.h>
|
||||
#include <regex.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#include "cst_tools.h"
|
||||
#include "cst_util.h"
|
||||
@@ -154,6 +149,9 @@ int cst_tool_read_file2(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||
FILE *fp = fopen(full, "rb");
|
||||
if (!fp)
|
||||
return tool_set_resultf(r, 1, "error: cannot open file: %.200s", path);
|
||||
char bom[3];
|
||||
if (fread(bom, 1, 3, fp) != 3 || !cst_bom_len(bom, 3))
|
||||
rewind(fp);
|
||||
|
||||
long offset = tool_arg_long(c, "offset", 0);
|
||||
long limit = tool_arg_long(c, "limit", 0);
|
||||
@@ -178,6 +176,8 @@ int cst_tool_read_file2(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||
return tool_set_resultf(r, 1, "error: %.200s is a binary file (%zu bytes)",
|
||||
path, n);
|
||||
}
|
||||
cst_strip_bom(buf, &n);
|
||||
n = cst_crlf_to_lf(buf, n); /* edits match LF text and keep CRLF */
|
||||
cst_tool_result_free(r);
|
||||
r->out = buf;
|
||||
r->out_len = n;
|
||||
@@ -217,6 +217,9 @@ int cst_tool_read_file2(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||
}
|
||||
if (numbers)
|
||||
cst_buf_printf(&b, "%6ld|", lineno);
|
||||
if (line.len >= 2 && line.buf[line.len - 2] == '\r' &&
|
||||
line.buf[line.len - 1] == '\n')
|
||||
line.buf[--line.len - 1] = '\n';
|
||||
cst_buf_append(&b, line.buf, line.len);
|
||||
shown++;
|
||||
}
|
||||
@@ -442,6 +445,14 @@ static int plan_edit(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||
nwn = n2n;
|
||||
}
|
||||
}
|
||||
} else if (cnt && memchr(nw, '\n', nwn) &&
|
||||
cst_text_is_crlf(pl->oldc, pl->olen)) {
|
||||
size_t n2n;
|
||||
n2 = to_crlf(nw, nwn, &n2n);
|
||||
if (n2) {
|
||||
nw = n2;
|
||||
nwn = n2n;
|
||||
}
|
||||
}
|
||||
int rc = 0;
|
||||
if (cnt == 0) {
|
||||
@@ -539,7 +550,6 @@ int cst_preview_edit_file(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||
/* --- delete_file / move_file --------------------------------------------- */
|
||||
|
||||
static int reject_symlink(const cst_tool_ctx_t *ctx, const char *path) {
|
||||
#ifndef _WIN32
|
||||
char root[OS_PATH_MAX], joined[OS_PATH_MAX];
|
||||
if (os_realpath(ctx->workspace_root, root, sizeof(root)) != 0)
|
||||
return 1;
|
||||
@@ -547,13 +557,9 @@ static int reject_symlink(const cst_tool_ctx_t *ctx, const char *path) {
|
||||
snprintf(joined, sizeof(joined), "%s", path);
|
||||
else if (os_path_join(joined, sizeof(joined), root, path) != 0)
|
||||
return 1;
|
||||
struct stat st;
|
||||
if (lstat(joined, &st) == 0 && S_ISLNK(st.st_mode))
|
||||
os_stat_t st;
|
||||
if (os_lstat(joined, &st) == 0 && st.is_link)
|
||||
return 1;
|
||||
#else
|
||||
(void)ctx;
|
||||
(void)path;
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -672,8 +678,6 @@ int cst_tool_move_file(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||
|
||||
/* --- directory walking (list_dir / glob / grep) -------------------------- */
|
||||
|
||||
#ifndef _WIN32
|
||||
|
||||
static const char *const SKIP_DIRS[] = {
|
||||
".git", ".hg", ".svn", "node_modules", ".agent", "__pycache__",
|
||||
".venv", ".cache", ".idea", ".vscode", ".mypy_cache", ".pytest_cache"};
|
||||
@@ -703,7 +707,7 @@ static void ignore_load(const char *root, ignore_t *ig) {
|
||||
char p[OS_PATH_MAX];
|
||||
if (os_path_join(p, sizeof(p), root, ".gitignore") != 0)
|
||||
return;
|
||||
FILE *fp = fopen(p, "r");
|
||||
FILE *fp = fopen(p, "rb");
|
||||
if (!fp)
|
||||
return;
|
||||
char line[512];
|
||||
@@ -749,7 +753,7 @@ static int ignored(const ignore_t *ig, const char *rel, const char *name,
|
||||
const ign_rule_t *r = &ig->v[i];
|
||||
if (r->dir_only && !isdir)
|
||||
continue;
|
||||
if (fnmatch(r->pat, r->anchored ? rel : name, 0) == 0)
|
||||
if (cst_fnmatch(r->pat, r->anchored ? rel : name, 0) == 0)
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
@@ -758,7 +762,7 @@ static int ignored(const ignore_t *ig, const char *rel, const char *name,
|
||||
typedef struct walker walker_t;
|
||||
/* Return non-zero to stop the walk. */
|
||||
typedef int (*walk_fn)(walker_t *w, const char *rel, const char *full,
|
||||
const struct stat *st, int depth);
|
||||
const os_stat_t *st, int depth);
|
||||
|
||||
struct walker {
|
||||
const char *root; /* workspace realpath */
|
||||
@@ -781,15 +785,13 @@ static void walk_dir(walker_t *w, const char *rel, int depth) {
|
||||
snprintf(dirfull, sizeof(dirfull), "%s/%s", w->root, rel);
|
||||
else
|
||||
snprintf(dirfull, sizeof(dirfull), "%s", w->root);
|
||||
DIR *d = opendir(dirfull);
|
||||
os_dir_t *d = os_dir_open(dirfull);
|
||||
if (!d)
|
||||
return;
|
||||
char **names = NULL;
|
||||
int n = 0, cap = 0;
|
||||
struct dirent *de;
|
||||
while ((de = readdir(d)) != NULL) {
|
||||
if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0)
|
||||
continue;
|
||||
const os_dirent_t *de;
|
||||
while ((de = os_dir_next(d)) != NULL) {
|
||||
if (n == cap) {
|
||||
cap = cap ? cap * 2 : 32;
|
||||
char **nv = realloc(names, (size_t)cap * sizeof(char *));
|
||||
@@ -797,11 +799,11 @@ static void walk_dir(walker_t *w, const char *rel, int depth) {
|
||||
break;
|
||||
names = nv;
|
||||
}
|
||||
names[n] = strdup(de->d_name);
|
||||
names[n] = strdup(de->name);
|
||||
if (names[n])
|
||||
n++;
|
||||
}
|
||||
closedir(d);
|
||||
os_dir_close(d);
|
||||
qsort(names, (size_t)n, sizeof(char *), name_cmp);
|
||||
for (int i = 0; i < n && !w->stop; i++) {
|
||||
char crel[OS_PATH_MAX], cfull[OS_PATH_MAX];
|
||||
@@ -810,10 +812,10 @@ static void walk_dir(walker_t *w, const char *rel, int depth) {
|
||||
int k2 = snprintf(cfull, sizeof(cfull), "%s/%s", w->root, crel);
|
||||
if (k1 >= (int)sizeof(crel) || k2 >= (int)sizeof(cfull))
|
||||
continue; /* path too long */
|
||||
struct stat st;
|
||||
if (lstat(cfull, &st) != 0)
|
||||
os_stat_t st;
|
||||
if (os_lstat(cfull, &st) != 0)
|
||||
continue;
|
||||
int isdir = S_ISDIR(st.st_mode);
|
||||
int isdir = st.is_dir;
|
||||
if (ignored(&w->ig, crel, names[i], isdir))
|
||||
continue;
|
||||
if (w->fn(w, crel, cfull, &st, depth))
|
||||
@@ -861,21 +863,18 @@ typedef struct {
|
||||
|
||||
/* Entries directly inside a directory (for the depth boundary). */
|
||||
static int dir_entries(const char *full) {
|
||||
DIR *d = opendir(full);
|
||||
os_dir_t *d = os_dir_open(full);
|
||||
if (!d)
|
||||
return -1;
|
||||
int n = 0;
|
||||
struct dirent *de;
|
||||
while ((de = readdir(d)) != NULL) {
|
||||
if (strcmp(de->d_name, ".") != 0 && strcmp(de->d_name, "..") != 0)
|
||||
n++;
|
||||
}
|
||||
closedir(d);
|
||||
while (os_dir_next(d))
|
||||
n++;
|
||||
os_dir_close(d);
|
||||
return n;
|
||||
}
|
||||
|
||||
static int list_cb(walker_t *w, const char *rel, const char *full,
|
||||
const struct stat *st, int depth) {
|
||||
const os_stat_t *st, int depth) {
|
||||
list_ud_t *u = w->ud;
|
||||
if (u->count >= LIST_MAX) {
|
||||
if (++u->more >= LIST_COUNT_MAX) {
|
||||
@@ -888,7 +887,7 @@ static int list_cb(walker_t *w, const char *rel, const char *full,
|
||||
name = name ? name + 1 : rel;
|
||||
for (int i = 0; i < depth; i++)
|
||||
cst_buf_puts(&u->b, " ");
|
||||
if (S_ISDIR(st->st_mode)) {
|
||||
if (st->is_dir) {
|
||||
int k = -1;
|
||||
if (skip_dir_name(name))
|
||||
cst_buf_printf(&u->b, "%s/ (skipped)\n", name);
|
||||
@@ -897,10 +896,10 @@ static int list_cb(walker_t *w, const char *rel, const char *full,
|
||||
k == 1 ? "y" : "ies");
|
||||
else
|
||||
cst_buf_printf(&u->b, "%s/%s\n", name, k == 0 ? " (empty)" : "");
|
||||
} else if (S_ISLNK(st->st_mode))
|
||||
} else if (st->is_link)
|
||||
cst_buf_printf(&u->b, "%s@\n", name);
|
||||
else
|
||||
cst_buf_printf(&u->b, "%s (%lld B)\n", name, (long long)st->st_size);
|
||||
cst_buf_printf(&u->b, "%s (%lld B)\n", name, st->size);
|
||||
u->count++;
|
||||
return 0;
|
||||
}
|
||||
@@ -974,7 +973,7 @@ static int seg_match(const char *pat, const char *path) {
|
||||
pseg[pl] = 0;
|
||||
memcpy(sseg, path, sl);
|
||||
sseg[sl] = 0;
|
||||
if (fnmatch(pseg, sseg, FNM_PERIOD) != 0)
|
||||
if (cst_fnmatch(pseg, sseg, CST_FNM_PERIOD) != 0)
|
||||
return 0;
|
||||
if (!ps)
|
||||
return !ss;
|
||||
@@ -994,11 +993,11 @@ typedef struct {
|
||||
} glob_ud_t;
|
||||
|
||||
static int glob_cb(walker_t *w, const char *rel, const char *full,
|
||||
const struct stat *st, int depth) {
|
||||
const os_stat_t *st, int depth) {
|
||||
(void)full;
|
||||
(void)depth;
|
||||
glob_ud_t *u = w->ud;
|
||||
if (!S_ISREG(st->st_mode))
|
||||
if (!st->is_reg)
|
||||
return 0;
|
||||
const char *sub = rel;
|
||||
size_t bl = strlen(u->base);
|
||||
@@ -1009,7 +1008,7 @@ static int glob_cb(walker_t *w, const char *rel, const char *full,
|
||||
}
|
||||
const char *name = strrchr(sub, '/');
|
||||
name = name ? name + 1 : sub;
|
||||
int ok = u->basename_only ? fnmatch(u->pattern, name, FNM_PERIOD) == 0
|
||||
int ok = u->basename_only ? cst_fnmatch(u->pattern, name, CST_FNM_PERIOD) == 0
|
||||
: seg_match(u->pattern, sub);
|
||||
if (!ok)
|
||||
return 0;
|
||||
@@ -1025,7 +1024,7 @@ static int glob_cb(walker_t *w, const char *rel, const char *full,
|
||||
return 1;
|
||||
}
|
||||
u->hits[u->n] = strdup(rel);
|
||||
u->mtimes[u->n] = (double)st->st_mtime;
|
||||
u->mtimes[u->n] = st->mtime;
|
||||
if (u->hits[u->n])
|
||||
u->n++;
|
||||
if (u->n >= GLOB_MAX * 20) {
|
||||
@@ -1109,11 +1108,11 @@ typedef struct {
|
||||
} files_ud_t;
|
||||
|
||||
static int files_cb(walker_t *w, const char *rel, const char *full,
|
||||
const struct stat *st, int depth) {
|
||||
const os_stat_t *st, int depth) {
|
||||
(void)full;
|
||||
(void)depth;
|
||||
files_ud_t *u = w->ud;
|
||||
if (!S_ISREG(st->st_mode))
|
||||
if (!st->is_reg)
|
||||
return 0;
|
||||
if (u->n >= u->max)
|
||||
return 1;
|
||||
@@ -1275,16 +1274,16 @@ static void grep_file(grep_ud_t *u, const char *rel, const char *full) {
|
||||
}
|
||||
|
||||
static int grep_cb(walker_t *w, const char *rel, const char *full,
|
||||
const struct stat *st, int depth) {
|
||||
const os_stat_t *st, int depth) {
|
||||
(void)depth;
|
||||
grep_ud_t *u = w->ud;
|
||||
if (!S_ISREG(st->st_mode) || st->st_size > READ_FILE_MAX)
|
||||
if (!st->is_reg || st->size > READ_FILE_MAX)
|
||||
return 0;
|
||||
if (u->glob) {
|
||||
const char *name = strrchr(rel, '/');
|
||||
name = name ? name + 1 : rel;
|
||||
int ok = strchr(u->glob, '/') ? fnmatch(u->glob, rel, 0) == 0
|
||||
: fnmatch(u->glob, name, 0) == 0;
|
||||
int ok = strchr(u->glob, '/') ? cst_fnmatch(u->glob, rel, 0) == 0
|
||||
: cst_fnmatch(u->glob, name, 0) == 0;
|
||||
if (!ok)
|
||||
return 0;
|
||||
}
|
||||
@@ -1391,32 +1390,4 @@ int cst_tool_grep(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||
return tool_take_buf(r, &out, 0);
|
||||
}
|
||||
|
||||
#else /* _WIN32: directory tools are not implemented natively */
|
||||
|
||||
int cst_tool_list_dir(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||
cst_tool_result_t *r) {
|
||||
(void)ctx;
|
||||
(void)c;
|
||||
return tool_set_result(r, "error: list_dir is not available on Windows; "
|
||||
"use run_shell", 1);
|
||||
}
|
||||
int cst_workspace_files(const char *ws, char ***out, int max) {
|
||||
(void)ws;
|
||||
(void)max;
|
||||
*out = NULL;
|
||||
return 0;
|
||||
}
|
||||
int cst_tool_glob(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||
cst_tool_result_t *r) {
|
||||
(void)ctx;
|
||||
(void)c;
|
||||
return tool_set_result(r, "error: glob is not available on Windows", 1);
|
||||
}
|
||||
int cst_tool_grep(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||
cst_tool_result_t *r) {
|
||||
(void)ctx;
|
||||
(void)c;
|
||||
return tool_set_result(r, "error: grep is not available on Windows", 1);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user