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,208 @@
|
||||
/* Portable helpers for the test programs: scratch dirs, env, recursive
|
||||
* delete, symlinks and (with CST_TEST_SOCKETS) a loopback socket layer. */
|
||||
#ifndef CST_TEST_UTIL_H
|
||||
#define CST_TEST_UTIL_H
|
||||
|
||||
#include "os_compat.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#ifdef CST_TEST_SOCKETS
|
||||
#include <winsock2.h> /* before windows.h */
|
||||
#include <ws2tcpip.h>
|
||||
#endif
|
||||
#include <direct.h>
|
||||
#include <io.h>
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#if defined(__GNUC__)
|
||||
#define CST_TEST_UNUSED __attribute__((unused))
|
||||
#else
|
||||
#define CST_TEST_UNUSED
|
||||
#endif
|
||||
|
||||
/* mkdir(path, mode) that also builds on Windows. */
|
||||
#define test_mkdir(p, mode) os_mkdir((p), (mode))
|
||||
|
||||
/* Fresh empty directory "<tmp>/<tag>-<pid>-<n>"; 0 on success. */
|
||||
static CST_TEST_UNUSED int test_tmpdir(char *out, size_t n, const char *tag) {
|
||||
static int seq;
|
||||
char base[1024];
|
||||
if (os_temp_dir(base, sizeof(base)) != 0)
|
||||
snprintf(base, sizeof(base), ".");
|
||||
for (int i = 0; i < 100; i++) {
|
||||
int k = snprintf(out, n, "%s/%s-%d-%d", base, tag, os_getpid(), seq++);
|
||||
if (k < 0 || (size_t)k >= n)
|
||||
return -1;
|
||||
#ifdef _WIN32
|
||||
int rc = _mkdir(out);
|
||||
#else
|
||||
int rc = mkdir(out, 0755);
|
||||
#endif
|
||||
if (rc == 0)
|
||||
return 0;
|
||||
if (errno != EEXIST)
|
||||
return -1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* rm -rf without a shell; links are removed, never followed. */
|
||||
static CST_TEST_UNUSED int test_rmrf(const char *path) {
|
||||
os_stat_t st;
|
||||
if (os_lstat(path, &st) != 0)
|
||||
return 0;
|
||||
if (st.is_link) {
|
||||
#ifdef _WIN32
|
||||
if (RemoveDirectoryA(path) || DeleteFileA(path))
|
||||
return 0;
|
||||
return -1;
|
||||
#else
|
||||
return unlink(path);
|
||||
#endif
|
||||
}
|
||||
if (!st.is_dir) {
|
||||
#ifdef _WIN32
|
||||
SetFileAttributesA(path, FILE_ATTRIBUTE_NORMAL);
|
||||
#endif
|
||||
return remove(path);
|
||||
}
|
||||
os_dir_t *d = os_dir_open(path);
|
||||
if (d) {
|
||||
const os_dirent_t *e;
|
||||
while ((e = os_dir_next(d))) {
|
||||
if (!strcmp(e->name, ".") || !strcmp(e->name, ".."))
|
||||
continue;
|
||||
size_t L = strlen(path) + strlen(e->name) + 2;
|
||||
char *c = malloc(L);
|
||||
if (!c)
|
||||
break;
|
||||
snprintf(c, L, "%s/%s", path, e->name);
|
||||
test_rmrf(c);
|
||||
free(c);
|
||||
}
|
||||
os_dir_close(d);
|
||||
}
|
||||
#ifdef _WIN32
|
||||
return _rmdir(path);
|
||||
#else
|
||||
return rmdir(path);
|
||||
#endif
|
||||
}
|
||||
|
||||
/* 0 on success; -1 when the platform or account cannot make links (Windows
|
||||
* without Developer Mode or admin rights): callers skip those checks. */
|
||||
static CST_TEST_UNUSED int test_symlink(const char *target, const char *link) {
|
||||
#ifdef _WIN32
|
||||
wchar_t *wt = os_utf8_to_wide(target), *wl = os_utf8_to_wide(link);
|
||||
int ok = 0;
|
||||
if (wt && wl) {
|
||||
for (wchar_t *p = wt; *p; p++)
|
||||
if (*p == L'/')
|
||||
*p = L'\\';
|
||||
DWORD attr = GetFileAttributesW(wt);
|
||||
if (attr == INVALID_FILE_ATTRIBUTES) {
|
||||
/* relative targets resolve against the link's directory */
|
||||
wchar_t *slash = wcsrchr(wl, L'/');
|
||||
if (!slash)
|
||||
slash = wcsrchr(wl, L'\\');
|
||||
if (slash && wt[0] != L'\\' && !(wt[0] && wt[1] == L':')) {
|
||||
size_t dl = (size_t)(slash - wl) + 1, tl = wcslen(wt);
|
||||
wchar_t *full = malloc((dl + tl + 1) * sizeof(wchar_t));
|
||||
if (full) {
|
||||
memcpy(full, wl, dl * sizeof(wchar_t));
|
||||
memcpy(full + dl, wt, (tl + 1) * sizeof(wchar_t));
|
||||
attr = GetFileAttributesW(full);
|
||||
free(full);
|
||||
}
|
||||
}
|
||||
}
|
||||
DWORD flags = 0x2; /* SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE */
|
||||
if (attr != INVALID_FILE_ATTRIBUTES && (attr & FILE_ATTRIBUTE_DIRECTORY))
|
||||
flags |= SYMBOLIC_LINK_FLAG_DIRECTORY;
|
||||
ok = CreateSymbolicLinkW(wl, wt, flags) != 0;
|
||||
}
|
||||
free(wt);
|
||||
free(wl);
|
||||
return ok ? 0 : -1;
|
||||
#else
|
||||
return symlink(target, link);
|
||||
#endif
|
||||
}
|
||||
|
||||
/* rand_r replacement (same sequence everywhere). */
|
||||
static CST_TEST_UNUSED int test_rand(unsigned *seed) {
|
||||
*seed = *seed * 1103515245u + 12345u;
|
||||
return (int)((*seed >> 16) & 0x7fff);
|
||||
}
|
||||
|
||||
/* getline replacement: -1 at EOF. */
|
||||
static CST_TEST_UNUSED long test_getline(char **line, size_t *cap, FILE *fp) {
|
||||
if (!*line || *cap == 0) {
|
||||
*cap = 256;
|
||||
*line = malloc(*cap);
|
||||
if (!*line)
|
||||
return -1;
|
||||
}
|
||||
size_t n = 0;
|
||||
int c;
|
||||
while ((c = fgetc(fp)) != EOF) {
|
||||
if (n + 2 > *cap) {
|
||||
size_t nc = *cap * 2;
|
||||
char *nl = realloc(*line, nc);
|
||||
if (!nl)
|
||||
return -1;
|
||||
*line = nl;
|
||||
*cap = nc;
|
||||
}
|
||||
(*line)[n++] = (char)c;
|
||||
if (c == '\n')
|
||||
break;
|
||||
}
|
||||
if (n == 0)
|
||||
return -1;
|
||||
(*line)[n] = 0;
|
||||
return (long)n;
|
||||
}
|
||||
|
||||
static CST_TEST_UNUSED void test_sleep_ms(int ms) {
|
||||
#ifdef _WIN32
|
||||
Sleep((DWORD)ms);
|
||||
#else
|
||||
usleep((useconds_t)ms * 1000);
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef CST_TEST_SOCKETS
|
||||
#ifdef _WIN32
|
||||
typedef SOCKET test_sock_t;
|
||||
#define TEST_BAD_SOCK INVALID_SOCKET
|
||||
#define test_sock_close closesocket
|
||||
#ifndef MSG_NOSIGNAL
|
||||
#define MSG_NOSIGNAL 0
|
||||
#endif
|
||||
static CST_TEST_UNUSED void test_sock_init(void) {
|
||||
WSADATA w;
|
||||
WSAStartup(MAKEWORD(2, 2), &w);
|
||||
}
|
||||
#else
|
||||
#include <arpa/inet.h>
|
||||
#include <netinet/in.h>
|
||||
#include <signal.h>
|
||||
#include <sys/socket.h>
|
||||
typedef int test_sock_t;
|
||||
#define TEST_BAD_SOCK (-1)
|
||||
#define test_sock_close close
|
||||
static CST_TEST_UNUSED void test_sock_init(void) { signal(SIGPIPE, SIG_IGN); }
|
||||
#endif
|
||||
#endif /* CST_TEST_SOCKETS */
|
||||
|
||||
#endif
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include "cst_test_util.h"
|
||||
|
||||
static int nfail;
|
||||
|
||||
@@ -180,12 +181,12 @@ static void test_tool_multi_file(void) {
|
||||
int main(void) {
|
||||
test_buffer_basic();
|
||||
test_buffer_edge();
|
||||
snprintf(g_ws, sizeof(g_ws), "/tmp/cst-patch-test-%d", (int)getpid());
|
||||
mkdir(g_ws, 0755);
|
||||
if (test_tmpdir(g_ws, sizeof(g_ws), "cst-patch-test") != 0) {
|
||||
perror("tmpdir");
|
||||
return 1;
|
||||
}
|
||||
test_tool_multi_file();
|
||||
char cmd[300];
|
||||
snprintf(cmd, sizeof(cmd), "rm -rf %s /tmp/escape.c", g_ws);
|
||||
if (system(cmd) != 0)
|
||||
if (test_rmrf(g_ws) != 0)
|
||||
fprintf(stderr, "warning: cleanup failed\n");
|
||||
if (nfail) {
|
||||
fprintf(stderr, "test_apply_patch: %d failed\n", nfail);
|
||||
|
||||
+26
-7
@@ -9,6 +9,11 @@
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include "cst_test_util.h"
|
||||
#ifdef _WIN32
|
||||
#include <aclapi.h>
|
||||
#include <sddl.h>
|
||||
#endif
|
||||
|
||||
#define CHECK(c) \
|
||||
do { \
|
||||
@@ -23,7 +28,7 @@ static char g_dir[256];
|
||||
static void write_file(const char *name, const char *text) {
|
||||
char path[512];
|
||||
snprintf(path, sizeof(path), "%s/cst-agent/%s", g_dir, name);
|
||||
FILE *fp = fopen(path, "w");
|
||||
FILE *fp = fopen(path, "wb");
|
||||
CHECK(fp);
|
||||
fputs(text, fp);
|
||||
fclose(fp);
|
||||
@@ -57,9 +62,26 @@ static void test_creds(void) {
|
||||
|
||||
char path[512];
|
||||
snprintf(path, sizeof(path), "%s/cst-agent/credentials", g_dir);
|
||||
#ifdef _WIN32
|
||||
{
|
||||
wchar_t *wp = os_utf8_to_wide(path);
|
||||
PSECURITY_DESCRIPTOR sd = NULL;
|
||||
LPWSTR s = NULL;
|
||||
CHECK(wp && GetNamedSecurityInfoW(wp, SE_FILE_OBJECT,
|
||||
LABEL_SECURITY_INFORMATION, NULL, NULL,
|
||||
NULL, NULL, &sd) == ERROR_SUCCESS);
|
||||
CHECK(ConvertSecurityDescriptorToStringSecurityDescriptorW(
|
||||
sd, SDDL_REVISION_1, LABEL_SECURITY_INFORMATION, &s, NULL));
|
||||
CHECK(wcsstr(s, L"NR") && wcsstr(s, L";ME)")); /* Low-IL cannot read */
|
||||
LocalFree(s);
|
||||
LocalFree(sd);
|
||||
free(wp);
|
||||
}
|
||||
#else
|
||||
struct stat st;
|
||||
CHECK(stat(path, &st) == 0);
|
||||
CHECK((st.st_mode & 0077) == 0);
|
||||
#endif
|
||||
|
||||
CHECK(cst_creds_resolve("deepseek", key, sizeof(key), &from) == 0);
|
||||
CHECK(strcmp(from, "saved") == 0);
|
||||
@@ -124,23 +146,20 @@ static void test_effort(void) {
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
snprintf(g_dir, sizeof(g_dir), "/tmp/cst-config-XXXXXX");
|
||||
CHECK(mkdtemp(g_dir));
|
||||
CHECK(test_tmpdir(g_dir, sizeof(g_dir), "cst-config") == 0);
|
||||
setenv("XDG_CONFIG_HOME", g_dir, 1);
|
||||
unsetenv("DEEPSEEK_API_KEY");
|
||||
unsetenv("MINIMAX_API_KEY");
|
||||
unsetenv("OPENAI_API_KEY");
|
||||
char sub[300];
|
||||
snprintf(sub, sizeof(sub), "%s/cst-agent", g_dir);
|
||||
CHECK(mkdir(sub, 0700) == 0);
|
||||
CHECK(test_mkdir(sub, 0755) == 0);
|
||||
|
||||
test_creds();
|
||||
test_settings();
|
||||
test_effort();
|
||||
|
||||
char cmd[400];
|
||||
snprintf(cmd, sizeof(cmd), "rm -rf '%s'", g_dir);
|
||||
CHECK(system(cmd) == 0);
|
||||
CHECK(test_rmrf(g_dir) == 0);
|
||||
printf("test_config: OK\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
+14
-7
@@ -9,6 +9,7 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include "cst_test_util.h"
|
||||
|
||||
static int nfail;
|
||||
|
||||
@@ -30,6 +31,11 @@ static void put(const char *p, const char *s) {
|
||||
|
||||
/* GNU diff output, or NULL when diff is not installed. */
|
||||
static char *gnu_diff(const char *a, const char *b) {
|
||||
#ifdef _WIN32
|
||||
(void)a;
|
||||
(void)b;
|
||||
return NULL; /* no GNU diff to compare against */
|
||||
#else
|
||||
char pa[] = "/tmp/cstdiffaXXXXXX", pb[] = "/tmp/cstdiffbXXXXXX";
|
||||
int fa = mkstemp(pa), fb = mkstemp(pb);
|
||||
if (fa < 0 || fb < 0)
|
||||
@@ -67,6 +73,7 @@ static char *gnu_diff(const char *a, const char *b) {
|
||||
unlink(pa);
|
||||
unlink(pb);
|
||||
return out;
|
||||
#endif
|
||||
}
|
||||
|
||||
static int have_gnu_diff(void) {
|
||||
@@ -130,11 +137,11 @@ static char *random_text(unsigned *seed, int lines) {
|
||||
size_t cap = (size_t)lines * 16 + 2, n = 0;
|
||||
char *s = malloc(cap);
|
||||
for (int i = 0; i < lines; i++) {
|
||||
const char *w = words[rand_r(seed) % 8];
|
||||
n += (size_t)snprintf(s + n, cap - n, "%s%d\n", w, rand_r(seed) % 3);
|
||||
const char *w = words[test_rand(seed) % 8];
|
||||
n += (size_t)snprintf(s + n, cap - n, "%s%d\n", w, test_rand(seed) % 3);
|
||||
}
|
||||
s[n] = 0;
|
||||
if (n && rand_r(seed) % 5 == 0)
|
||||
if (n && test_rand(seed) % 5 == 0)
|
||||
s[n - 1] = 0; /* no newline at end of file */
|
||||
return s;
|
||||
}
|
||||
@@ -146,15 +153,15 @@ static char *mutate(unsigned *seed, const char *a) {
|
||||
while (*p) {
|
||||
const char *e = strchr(p, '\n');
|
||||
size_t len = e ? (size_t)(e - p) + 1 : strlen(p);
|
||||
int r = rand_r(seed) % 10;
|
||||
int r = test_rand(seed) % 10;
|
||||
if (r == 0) {
|
||||
/* drop the line */
|
||||
} else if (r == 1) {
|
||||
o += (size_t)snprintf(b + o, cap - o, "inserted%d\n", rand_r(seed) % 9);
|
||||
o += (size_t)snprintf(b + o, cap - o, "inserted%d\n", test_rand(seed) % 9);
|
||||
memcpy(b + o, p, len);
|
||||
o += len;
|
||||
} else if (r == 2) {
|
||||
o += (size_t)snprintf(b + o, cap - o, "changed%d\n", rand_r(seed) % 9);
|
||||
o += (size_t)snprintf(b + o, cap - o, "changed%d\n", test_rand(seed) % 9);
|
||||
} else {
|
||||
memcpy(b + o, p, len);
|
||||
o += len;
|
||||
@@ -198,7 +205,7 @@ int main(void) {
|
||||
unsigned seed = 12345;
|
||||
int ok = 0, total = 400;
|
||||
for (int i = 0; i < total; i++) {
|
||||
char *a = random_text(&seed, 1 + (int)(rand_r(&seed) % 60));
|
||||
char *a = random_text(&seed, 1 + (int)(test_rand(&seed) % 60));
|
||||
char *b = mutate(&seed, a);
|
||||
ok += round_trip(a, b);
|
||||
free(a);
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
/* Windows Low-IL shell sandbox: writes only in the workspace and the
|
||||
* sandbox TEMP, the credentials file is unreadable, timeouts end the whole
|
||||
* process tree, and /sandbox reset-labels restores the workspace label.
|
||||
* Elsewhere it only checks that the kind is never reported. */
|
||||
#ifndef _GNU_SOURCE
|
||||
#define _GNU_SOURCE
|
||||
#endif
|
||||
|
||||
#include "cst_sandbox.h"
|
||||
#include "os_compat.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)
|
||||
|
||||
#ifdef _WIN32
|
||||
static int run(const char *ws, const char *cmd, os_run_result_t *r) {
|
||||
memset(r, 0, sizeof(*r));
|
||||
if (cst_sandbox_run_shell(ws, cmd, 0, r) != 0)
|
||||
return -1;
|
||||
return r->exit_code;
|
||||
}
|
||||
|
||||
static int has(const os_run_result_t *r, const char *s) {
|
||||
return r->out && strstr(r->out, s);
|
||||
}
|
||||
|
||||
static void write_file(const char *path, const char *text) {
|
||||
FILE *f = fopen(path, "wb");
|
||||
if (f) {
|
||||
fputs(text, f);
|
||||
fclose(f);
|
||||
}
|
||||
}
|
||||
|
||||
static void confined(void) {
|
||||
char root[1024], cfg[1100], ws[1100], out[1100], cred[1300], cmd[4096];
|
||||
char wsr[1100];
|
||||
if (test_tmpdir(root, sizeof(root), "cst-lowil") != 0) {
|
||||
EXPECT(0, "tmpdir");
|
||||
return;
|
||||
}
|
||||
snprintf(cfg, sizeof(cfg), "%s/cfg", root);
|
||||
snprintf(ws, sizeof(ws), "%s/项目 ws", root);
|
||||
snprintf(out, sizeof(out), "%s/outside", root);
|
||||
test_mkdir(cfg, 0700);
|
||||
test_mkdir(ws, 0755);
|
||||
test_mkdir(out, 0755);
|
||||
setenv("XDG_CONFIG_HOME", cfg, 1);
|
||||
char cdir[1200];
|
||||
EXPECT(os_config_dir(cdir, sizeof(cdir)) == 0, "config dir");
|
||||
os_mkdir_p(cdir, 0700);
|
||||
snprintf(cred, sizeof(cred), "%s/credentials", cdir);
|
||||
write_file(cred, "deepseek=sk-test-secret\n");
|
||||
EXPECT(os_chmod_private(cred) == 0, "label credentials");
|
||||
EXPECT(os_realpath(ws, wsr, sizeof(wsr)) == 0, "realpath ws");
|
||||
os_path_norm_slashes(wsr);
|
||||
|
||||
os_run_result_t r;
|
||||
EXPECT(run(wsr, "echo hi > in.txt && cat in.txt", &r) == 0,
|
||||
"write inside the workspace");
|
||||
EXPECT(has(&r, "hi"), "output captured");
|
||||
EXPECT(has(&r, "labeled"), "first run says the workspace was labeled");
|
||||
os_run_result_free(&r);
|
||||
|
||||
EXPECT(run(wsr, "echo again > in.txt && cat in.txt", &r) == 0,
|
||||
"second run");
|
||||
EXPECT(!has(&r, "labeled"), "labeled only once");
|
||||
EXPECT(!has(&r, "background"), "conhost is not a background process");
|
||||
os_run_result_free(&r);
|
||||
|
||||
char *labels = cst_sandbox_labels();
|
||||
EXPECT(labels && strstr(labels, wsr), "workspace recorded");
|
||||
free(labels);
|
||||
|
||||
snprintf(cmd, sizeof(cmd), "echo x > '%s/o.txt'", out);
|
||||
int rc = run(wsr, cmd, &r);
|
||||
EXPECT(rc != 0, "write outside the workspace fails");
|
||||
EXPECT(cst_sandbox_blocked(r.out, r.out_len, rc), "outside write blocked");
|
||||
os_run_result_free(&r);
|
||||
snprintf(cmd, sizeof(cmd), "%s/o.txt", out);
|
||||
os_stat_t st;
|
||||
EXPECT(os_lstat(cmd, &st) != 0, "nothing written outside");
|
||||
|
||||
snprintf(cmd, sizeof(cmd), "cat '%s'", cred);
|
||||
rc = run(wsr, cmd, &r);
|
||||
EXPECT(rc != 0 && !has(&r, "sk-test-secret"), "credentials unreadable");
|
||||
EXPECT(cst_sandbox_blocked(r.out, r.out_len, rc), "credential read blocked");
|
||||
os_run_result_free(&r);
|
||||
|
||||
EXPECT(run(wsr, "echo t > \"$TEMP/t.txt\" && cat \"$TEMP/t.txt\"", &r) == 0,
|
||||
"TEMP is writable");
|
||||
EXPECT(has(&r, "t"), "TEMP read back");
|
||||
os_run_result_free(&r);
|
||||
|
||||
/* System32's whoami: busybox has an applet and MSYS2's cannot start at
|
||||
* Low IL */
|
||||
char sys[MAX_PATH];
|
||||
UINT sl = GetSystemDirectoryA(sys, sizeof(sys));
|
||||
for (UINT i = 0; i < sl; i++)
|
||||
if (sys[i] == '\\')
|
||||
sys[i] = '/';
|
||||
snprintf(cmd, sizeof(cmd), "'%s/whoami.exe' /groups | grep S-1-16-4096",
|
||||
sys);
|
||||
rc = run(wsr, cmd, &r);
|
||||
EXPECT(rc == 0, "child runs at Low integrity");
|
||||
if (rc != 0)
|
||||
fprintf(stderr, "whoami: exit %d: %.*s\n", rc, (int)r.out_len,
|
||||
r.out ? r.out : "");
|
||||
os_run_result_free(&r);
|
||||
|
||||
/* timeout ends background children too */
|
||||
os_set_run_timeout(2);
|
||||
double t0 = os_now_ms();
|
||||
rc = run(wsr, "sleep 30 & sleep 30; echo late", &r);
|
||||
double ms = os_now_ms() - t0;
|
||||
EXPECT(rc == 124, "timeout exit code");
|
||||
EXPECT(ms < 10000, "process tree killed promptly");
|
||||
EXPECT(!has(&r, "late"), "command did not finish");
|
||||
os_run_result_free(&r);
|
||||
os_set_run_timeout(0);
|
||||
|
||||
const char *prof = getenv("USERPROFILE");
|
||||
char profr[1100];
|
||||
if (prof && os_realpath(prof, profr, sizeof(profr)) == 0) {
|
||||
os_path_norm_slashes(profr);
|
||||
rc = run(profr, "echo no", &r);
|
||||
EXPECT(rc == 126 && has(&r, "will not label"), "profile is not labeled");
|
||||
EXPECT(cst_sandbox_blocked(r.out, r.out_len, rc), "offers a rerun");
|
||||
os_run_result_free(&r);
|
||||
}
|
||||
rc = run("C:/", "echo no", &r);
|
||||
EXPECT(rc == 126 && has(&r, "will not label"), "drive root is not labeled");
|
||||
os_run_result_free(&r);
|
||||
|
||||
char *rep = NULL;
|
||||
EXPECT(cst_sandbox_reset_labels(&rep) >= 1, "reset-labels restored");
|
||||
EXPECT(rep && strstr(rep, "restored"), "reset report");
|
||||
free(rep);
|
||||
labels = cst_sandbox_labels();
|
||||
EXPECT(!labels, "record cleared");
|
||||
free(labels);
|
||||
char sddl[256];
|
||||
EXPECT(os_win_get_label(wsr, sddl, sizeof(sddl)) == 0 &&
|
||||
strcmp(sddl, "S:") == 0,
|
||||
"workspace has no explicit label again");
|
||||
|
||||
unsetenv("XDG_CONFIG_HOME");
|
||||
test_rmrf(root);
|
||||
}
|
||||
#endif
|
||||
|
||||
int main(void) {
|
||||
int kind = cst_sandbox_probe();
|
||||
printf("test_lowil: sandbox %s\n", cst_sandbox_name(kind));
|
||||
EXPECT(strcmp(cst_sandbox_name(CST_SANDBOX_LOWIL), "lowil") == 0, "name");
|
||||
#ifdef _WIN32
|
||||
EXPECT(kind == CST_SANDBOX_LOWIL, "lowil available");
|
||||
if (kind == CST_SANDBOX_LOWIL)
|
||||
confined();
|
||||
#else
|
||||
EXPECT(kind != CST_SANDBOX_LOWIL, "lowil is Windows only");
|
||||
char *rep = NULL;
|
||||
EXPECT(cst_sandbox_reset_labels(&rep) == 0 && !rep, "nothing to reset");
|
||||
EXPECT(!cst_sandbox_labels(), "no labels");
|
||||
#endif
|
||||
if (nfail) {
|
||||
fprintf(stderr, "test_lowil: %d failure(s)\n", nfail);
|
||||
return 1;
|
||||
}
|
||||
puts("test_lowil: OK");
|
||||
return 0;
|
||||
}
|
||||
+8
-3
@@ -14,7 +14,10 @@
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "cst_test_util.h"
|
||||
|
||||
static int nfail;
|
||||
static char g_tmp[OS_PATH_MAX];
|
||||
|
||||
#define CHECK(cond, msg) \
|
||||
do { \
|
||||
@@ -159,7 +162,7 @@ static int call(const char *tool, const char *k, const char *v,
|
||||
}
|
||||
cst_tool_ctx_t ctx;
|
||||
memset(&ctx, 0, sizeof(ctx));
|
||||
ctx.workspace_root = "/tmp";
|
||||
ctx.workspace_root = g_tmp;
|
||||
memset(r, 0, sizeof(*r));
|
||||
return cst_tool_run(&ctx, &c, r);
|
||||
}
|
||||
@@ -181,6 +184,8 @@ int main(int argc, char **argv) {
|
||||
fprintf(stderr, "cannot resolve %s\n", argv[0]);
|
||||
return 1;
|
||||
}
|
||||
if (os_temp_dir(g_tmp, sizeof(g_tmp)) != 0)
|
||||
snprintf(g_tmp, sizeof(g_tmp), ".");
|
||||
setenv("CST_TEST_SECRET", "s3cret", 1);
|
||||
char cfg[OS_PATH_MAX + 128];
|
||||
snprintf(cfg, sizeof(cfg),
|
||||
@@ -206,7 +211,7 @@ int main(int argc, char **argv) {
|
||||
cst_mcp_counts(&ready, &total);
|
||||
CHECK(ready == 0 && total == 3, "counts before start (disabled excluded)");
|
||||
|
||||
cst_mcp_start_all("/tmp");
|
||||
cst_mcp_start_all(g_tmp);
|
||||
cst_mcp_wait_ready(10000);
|
||||
|
||||
cst_mcp_info_t mi[CST_MCP_MAX];
|
||||
@@ -281,7 +286,7 @@ int main(int argc, char **argv) {
|
||||
|
||||
/* a live server stops cleanly too */
|
||||
CHECK(cst_mcp_add_config_json("fake2", cfg, NULL, "user") == 0, "re-add");
|
||||
cst_mcp_start_all("/tmp");
|
||||
cst_mcp_start_all(g_tmp);
|
||||
cst_mcp_wait_ready(10000);
|
||||
CHECK(cst_tool_find("mcp__fake2__echo"), "second server ready");
|
||||
t0 = os_now_ms();
|
||||
|
||||
+14
-20
@@ -8,18 +8,10 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
int main(void) {
|
||||
puts("test_models: skipped on Windows");
|
||||
return 0;
|
||||
}
|
||||
#else
|
||||
#define CST_TEST_SOCKETS
|
||||
#include "cst_test_util.h"
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <netinet/in.h>
|
||||
#include <pthread.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define CHECK(c) \
|
||||
do { \
|
||||
@@ -32,35 +24,38 @@ int main(void) {
|
||||
/* One canned HTTP response per accepted connection. */
|
||||
static const char *g_responses[8];
|
||||
static int g_nresp;
|
||||
static int g_listen = -1;
|
||||
static test_sock_t g_listen = TEST_BAD_SOCK;
|
||||
static char g_request[4096];
|
||||
|
||||
static void *serve(void *arg) {
|
||||
(void)arg;
|
||||
for (int i = 0; i < g_nresp; i++) {
|
||||
int c = accept(g_listen, NULL, NULL);
|
||||
if (c < 0)
|
||||
test_sock_t c = accept(g_listen, NULL, NULL);
|
||||
if (c == TEST_BAD_SOCK)
|
||||
return NULL;
|
||||
ssize_t n = recv(c, g_request, sizeof(g_request) - 1, 0);
|
||||
long n = recv(c, g_request, sizeof(g_request) - 1, 0);
|
||||
g_request[n > 0 ? n : 0] = 0;
|
||||
const char *r = g_responses[i];
|
||||
ssize_t off = 0, len = (ssize_t)strlen(r);
|
||||
long off = 0, len = (long)strlen(r);
|
||||
while (off < len) {
|
||||
ssize_t w = send(c, r + off, (size_t)(len - off), 0);
|
||||
long w = send(c, r + off, (int)(len - off), 0);
|
||||
if (w <= 0)
|
||||
break;
|
||||
off += w;
|
||||
}
|
||||
close(c);
|
||||
test_sock_close(c);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int start_server(void) {
|
||||
test_sock_init();
|
||||
g_listen = socket(AF_INET, SOCK_STREAM, 0);
|
||||
CHECK(g_listen >= 0);
|
||||
CHECK(g_listen != TEST_BAD_SOCK);
|
||||
#ifndef _WIN32
|
||||
int one = 1;
|
||||
setsockopt(g_listen, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
|
||||
#endif
|
||||
struct sockaddr_in a;
|
||||
memset(&a, 0, sizeof(a));
|
||||
a.sin_family = AF_INET;
|
||||
@@ -118,7 +113,7 @@ int main(void) {
|
||||
CHECK(n == -1 && strstr(err, "500") != NULL);
|
||||
|
||||
pthread_join(th, NULL);
|
||||
close(g_listen);
|
||||
test_sock_close(g_listen);
|
||||
|
||||
/* nothing listening: network error, not an auth failure */
|
||||
n = cst_llm_list_models(base, "sk-test", ids, 8, err, sizeof(err));
|
||||
@@ -127,4 +122,3 @@ int main(void) {
|
||||
puts("test_models: ok");
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
/* cst_fnmatch and the os_dir / os_lstat layer that replaced the POSIX-only
|
||||
* directory code; runs on every platform. */
|
||||
#ifndef _GNU_SOURCE
|
||||
#define _GNU_SOURCE
|
||||
#endif
|
||||
|
||||
#include "cst_util.h"
|
||||
#include "os_compat.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 void fnm(const char *pat, const char *s, int flags, int want) {
|
||||
int got = cst_fnmatch(pat, s, flags) == 0;
|
||||
if (got != want) {
|
||||
fprintf(stderr, "FAIL fnmatch(\"%s\", \"%s\", %d) = %d\n", pat, s, flags,
|
||||
got);
|
||||
nfail++;
|
||||
}
|
||||
}
|
||||
|
||||
static void test_fnmatch(void) {
|
||||
fnm("*.c", "main.c", 0, 1);
|
||||
fnm("*.c", "main.h", 0, 0);
|
||||
fnm("*.c", "src/main.c", 0, 1); /* '*' crosses '/' */
|
||||
fnm("?.txt", "a.txt", 0, 1);
|
||||
fnm("?.txt", "ab.txt", 0, 0);
|
||||
fnm("?.txt", "中.txt", 0, 1); /* one code point */
|
||||
fnm("[abc]x", "bx", 0, 1);
|
||||
fnm("[!abc]x", "bx", 0, 0);
|
||||
fnm("[a-c]x", "cx", 0, 1);
|
||||
fnm("[[:digit:]]*", "7up", 0, 1);
|
||||
fnm("[[:digit:]]*", "up", 0, 0);
|
||||
fnm("\\*", "*", 0, 1);
|
||||
fnm("\\*", "a", 0, 0);
|
||||
fnm("*", ".hidden", CST_FNM_PERIOD, 0);
|
||||
fnm(".*", ".hidden", CST_FNM_PERIOD, 1);
|
||||
fnm("*", ".hidden", 0, 1);
|
||||
fnm("*文件*", "我的文件.txt", 0, 1);
|
||||
fnm("README*", "readme.md", CST_FNM_CASEFOLD, 1);
|
||||
#ifdef _WIN32
|
||||
fnm("README*", "readme.md", 0, 1); /* Windows names are case-insensitive */
|
||||
#else
|
||||
fnm("README*", "readme.md", 0, 0);
|
||||
#endif
|
||||
fnm("", "", 0, 1);
|
||||
fnm("a", "", 0, 0);
|
||||
}
|
||||
|
||||
static void touch(const char *dir, const char *name) {
|
||||
char p[1200];
|
||||
snprintf(p, sizeof(p), "%s/%s", dir, name);
|
||||
FILE *f = fopen(p, "wb");
|
||||
if (f) {
|
||||
fputs("x", f);
|
||||
fclose(f);
|
||||
}
|
||||
}
|
||||
|
||||
static void test_dir(void) {
|
||||
char root[1024], sub[1100], p[1200];
|
||||
if (test_tmpdir(root, sizeof(root), "cst-osdir") != 0) {
|
||||
EXPECT(0, "tmpdir");
|
||||
return;
|
||||
}
|
||||
snprintf(sub, sizeof(sub), "%s/子目录", root);
|
||||
EXPECT(test_mkdir(sub, 0755) == 0, "mkdir with a Chinese name");
|
||||
touch(root, "a.txt");
|
||||
touch(root, "文件 一.md");
|
||||
touch(root, ".hidden");
|
||||
|
||||
int files = 0, dirs = 0, seen_cn = 0, seen_hidden = 0, dots = 0;
|
||||
os_dir_t *d = os_dir_open(root);
|
||||
EXPECT(d != NULL, "os_dir_open");
|
||||
const os_dirent_t *e;
|
||||
while (d && (e = os_dir_next(d)) != NULL) {
|
||||
if (!strcmp(e->name, ".") || !strcmp(e->name, ".."))
|
||||
dots++;
|
||||
if (e->is_dir)
|
||||
dirs++;
|
||||
else
|
||||
files++;
|
||||
seen_cn |= strcmp(e->name, "文件 一.md") == 0 && !e->is_dir;
|
||||
seen_hidden |= strcmp(e->name, ".hidden") == 0;
|
||||
if (!strcmp(e->name, "子目录"))
|
||||
EXPECT(e->is_dir && !e->is_link, "subdir flags");
|
||||
}
|
||||
os_dir_close(d);
|
||||
EXPECT(dots == 0, "no . or .. entries");
|
||||
EXPECT(files == 3 && dirs == 1, "entry counts");
|
||||
EXPECT(seen_cn, "UTF-8 file name listed");
|
||||
EXPECT(seen_hidden, "dot files listed");
|
||||
d = os_dir_open(sub);
|
||||
EXPECT(d != NULL && os_dir_next(d) == NULL, "empty Chinese subdir");
|
||||
if (d)
|
||||
os_dir_close(d);
|
||||
|
||||
os_stat_t st;
|
||||
snprintf(p, sizeof(p), "%s/文件 一.md", root);
|
||||
EXPECT(os_lstat(p, &st) == 0 && st.is_reg && !st.is_dir && st.size == 1,
|
||||
"os_lstat on a UTF-8 file");
|
||||
EXPECT(os_lstat(sub, &st) == 0 && st.is_dir, "os_lstat on a dir");
|
||||
snprintf(p, sizeof(p), "%s/missing", root);
|
||||
EXPECT(os_lstat(p, &st) != 0, "os_lstat on a missing path");
|
||||
EXPECT(os_dir_open(p) == NULL, "os_dir_open on a missing path");
|
||||
|
||||
char real[1200], rr[1200];
|
||||
EXPECT(os_realpath(sub, real, sizeof(real)) == 0 &&
|
||||
os_realpath(root, rr, sizeof(rr)) == 0,
|
||||
"os_realpath");
|
||||
EXPECT(os_path_under(rr, real), "realpath of subdir is under root");
|
||||
EXPECT(!os_path_under(real, rr), "root is not under subdir");
|
||||
|
||||
snprintf(p, sizeof(p), "%s/link", root);
|
||||
int lk = test_symlink(sub, p);
|
||||
if (lk == 0) {
|
||||
EXPECT(os_lstat(p, &st) == 0 && st.is_link, "symlink is reported as link");
|
||||
} else {
|
||||
printf("test_os_dir: symlinks not permitted, link checks skipped\n");
|
||||
}
|
||||
test_rmrf(root);
|
||||
EXPECT(os_lstat(root, &st) != 0, "test_rmrf removed everything");
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
os_console_init();
|
||||
test_fnmatch();
|
||||
test_dir();
|
||||
if (nfail) {
|
||||
fprintf(stderr, "test_os_dir: %d failure(s)\n", nfail);
|
||||
return 1;
|
||||
}
|
||||
puts("test_os_dir: OK");
|
||||
return 0;
|
||||
}
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include "cst_test_util.h"
|
||||
|
||||
static int nfail;
|
||||
|
||||
@@ -33,11 +34,11 @@ static void put(const char *rel, const char *text) {
|
||||
for (char *q = p + strlen(g_root) + 1; *q; q++) {
|
||||
if (*q == '/') {
|
||||
*q = 0;
|
||||
mkdir(p, 0755);
|
||||
test_mkdir(p, 0755);
|
||||
*q = '/';
|
||||
}
|
||||
}
|
||||
FILE *fp = fopen(p, "w");
|
||||
FILE *fp = fopen(p, "wb");
|
||||
if (fp) {
|
||||
fputs(text, fp);
|
||||
fclose(fp);
|
||||
@@ -70,8 +71,10 @@ static void run(const char *ws, const char *tool, const char *k1,
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
snprintf(g_root, sizeof(g_root), "/tmp/cst-plugins-%d", (int)getpid());
|
||||
mkdir(g_root, 0755);
|
||||
if (test_tmpdir(g_root, sizeof(g_root), "cst-plugins") != 0) {
|
||||
perror("tmpdir");
|
||||
return 1;
|
||||
}
|
||||
char ws[300], cfgdir[300];
|
||||
snprintf(ws, sizeof(ws), "%s/ws", g_root);
|
||||
snprintf(cfgdir, sizeof(cfgdir), "%s/cfg", g_root);
|
||||
@@ -206,9 +209,7 @@ int main(void) {
|
||||
CHECK(!cst_tool_find("skill") && !cst_tool_find("echo_args"),
|
||||
"extensions unload");
|
||||
|
||||
char cmd[400];
|
||||
snprintf(cmd, sizeof(cmd), "rm -rf %s", g_root);
|
||||
if (system(cmd) != 0)
|
||||
if (test_rmrf(g_root) != 0)
|
||||
fprintf(stderr, "note: cleanup failed\n");
|
||||
if (nfail) {
|
||||
fprintf(stderr, "test_plugins: %d failure(s)\n", nfail);
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include "cst_test_util.h"
|
||||
|
||||
/*
|
||||
* Truncation experiment on recorded model streams (tests/fixtures/sse).
|
||||
@@ -79,7 +80,7 @@ static int load_trace(const char *path, trace_t *t) {
|
||||
char *line = NULL;
|
||||
size_t cap = 0;
|
||||
ssize_t n;
|
||||
while ((n = getline(&line, &cap, fp)) > 0) {
|
||||
while ((n = test_getline(&line, &cap, fp)) > 0) {
|
||||
if (strncmp(line, "data:", 5) != 0)
|
||||
continue;
|
||||
cJSON *j = cJSON_Parse(line + 5);
|
||||
|
||||
@@ -73,6 +73,43 @@ static void test_blocked(void) {
|
||||
const char *d = "curl: (7) Failed to connect to example.com port 443 after 9 "
|
||||
"ms: Could not connect to server\n";
|
||||
CHECK(cst_sandbox_blocked(d, strlen(d), 7));
|
||||
const char *e = "Access is denied.\r\n";
|
||||
CHECK(cst_sandbox_blocked(e, strlen(e), 1));
|
||||
const char *f = "拒绝访问。\r\n";
|
||||
CHECK(cst_sandbox_blocked(f, strlen(f), 1));
|
||||
const char *g = "sh: C:/msys64/usr/bin/ls.exe: A dynamic link library (DLL) "
|
||||
"initialization routine failed. Error 0xc0000142\n";
|
||||
CHECK(cst_sandbox_blocked(g, strlen(g), 1));
|
||||
}
|
||||
|
||||
static void test_network_rules(void) {
|
||||
static const char *const NET[] = {
|
||||
"curl -fsSL https://example.com",
|
||||
"cd x && wget http://a/b",
|
||||
"/usr/bin/ssh host ls",
|
||||
"C:\\tools\\curl.exe -O x",
|
||||
"git push origin main",
|
||||
"git --no-pager fetch",
|
||||
"npm install",
|
||||
"python -m pip install requests",
|
||||
"go get example.com/x",
|
||||
"cargo install ripgrep",
|
||||
"Invoke-WebRequest -Uri x",
|
||||
};
|
||||
static const char *const LOCAL[] = {
|
||||
"ls -la", "git status", "git commit -m push", "npm run build",
|
||||
"make -j8", "pip list", "echo curly", "cat curl.txt",
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(NET) / sizeof(NET[0]); i++) {
|
||||
if (!cst_shell_uses_network(NET[i]))
|
||||
fprintf(stderr, "expected network: %s\n", NET[i]);
|
||||
CHECK(cst_shell_uses_network(NET[i]));
|
||||
}
|
||||
for (size_t i = 0; i < sizeof(LOCAL) / sizeof(LOCAL[0]); i++) {
|
||||
if (cst_shell_uses_network(LOCAL[i]))
|
||||
fprintf(stderr, "expected local: %s\n", LOCAL[i]);
|
||||
CHECK(!cst_shell_uses_network(LOCAL[i]));
|
||||
}
|
||||
}
|
||||
|
||||
#if !defined(_WIN32)
|
||||
@@ -314,6 +351,7 @@ static void test_confined(int kind) {
|
||||
int main(void) {
|
||||
test_rules();
|
||||
test_blocked();
|
||||
test_network_rules();
|
||||
int kind = cst_sandbox_probe();
|
||||
printf("test_sandbox: sandbox %s\n", cst_sandbox_name(kind));
|
||||
#if !defined(_WIN32)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "cJSON.h"
|
||||
#include "cst_agent.h"
|
||||
#include "os_compat.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -76,8 +77,11 @@ static void check_messages(cst_session_t *s) {
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
char path[256];
|
||||
snprintf(path, sizeof(path), "/tmp/cst-session-test-%d.jsonl", (int)getpid());
|
||||
char path[1024], tmp[900];
|
||||
if (os_temp_dir(tmp, sizeof(tmp)) != 0)
|
||||
snprintf(tmp, sizeof(tmp), ".");
|
||||
snprintf(path, sizeof(path), "%s/cst-session-test-%d.jsonl", tmp,
|
||||
os_getpid());
|
||||
remove(path);
|
||||
|
||||
cst_session_t *s = cst_session_open(path);
|
||||
@@ -129,7 +133,7 @@ int main(void) {
|
||||
cst_session_close(s);
|
||||
|
||||
/* legacy format: tool_calls_json stored as a string */
|
||||
fp = fopen(path, "w");
|
||||
fp = fopen(path, "wb");
|
||||
fprintf(fp, "{\"role\":\"assistant\",\"content\":\"\",\"tool_calls_json\":"
|
||||
"\"[{\\\"id\\\":\\\"c9\\\",\\\"type\\\":\\\"function\\\","
|
||||
"\\\"function\\\":{\\\"name\\\":\\\"run_shell\\\","
|
||||
|
||||
+6
-5
@@ -8,6 +8,7 @@
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include "cst_test_util.h"
|
||||
|
||||
/* run_shell must never hang the agent: timeout kills the whole process
|
||||
* group, stdin is empty, huge output is capped. */
|
||||
@@ -41,8 +42,10 @@ static int sh(const char *cmd, cst_tool_result_t *r, double *ms) {
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
snprintf(g_ws, sizeof(g_ws), "/tmp/cst-shell-test-%d", (int)getpid());
|
||||
mkdir(g_ws, 0755);
|
||||
if (test_tmpdir(g_ws, sizeof(g_ws), "cst-shell-test") != 0) {
|
||||
perror("tmpdir");
|
||||
return 1;
|
||||
}
|
||||
cst_tool_result_t r;
|
||||
double ms;
|
||||
|
||||
@@ -81,9 +84,7 @@ int main(void) {
|
||||
EXPECT(ms < 20000, "large output does not hang");
|
||||
cst_tool_result_free(&r);
|
||||
|
||||
char cmd[300];
|
||||
snprintf(cmd, sizeof(cmd), "rm -rf %s", g_ws);
|
||||
if (system(cmd) != 0)
|
||||
if (test_rmrf(g_ws) != 0)
|
||||
fprintf(stderr, "warning: cleanup failed\n");
|
||||
if (nfail) {
|
||||
fprintf(stderr, "test_shell: %d failed\n", nfail);
|
||||
|
||||
+10
-9
@@ -11,6 +11,7 @@
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include "cst_test_util.h"
|
||||
|
||||
static int nfail;
|
||||
|
||||
@@ -30,11 +31,11 @@ static void put(const char *rel, const char *text) {
|
||||
for (char *q = p + strlen(g_root) + 1; *q; q++) {
|
||||
if (*q == '/') {
|
||||
*q = 0;
|
||||
mkdir(p, 0755);
|
||||
test_mkdir(p, 0755);
|
||||
*q = '/';
|
||||
}
|
||||
}
|
||||
FILE *fp = fopen(p, "w");
|
||||
FILE *fp = fopen(p, "wb");
|
||||
if (fp) {
|
||||
fputs(text, fp);
|
||||
fclose(fp);
|
||||
@@ -69,8 +70,10 @@ static void frontmatter(void) {
|
||||
int main(void) {
|
||||
frontmatter();
|
||||
|
||||
snprintf(g_root, sizeof(g_root), "/tmp/cst-skills-%d", (int)getpid());
|
||||
mkdir(g_root, 0755);
|
||||
if (test_tmpdir(g_root, sizeof(g_root), "cst-skills") != 0) {
|
||||
perror("tmpdir");
|
||||
return 1;
|
||||
}
|
||||
char ws[300], cfgdir[300];
|
||||
snprintf(ws, sizeof(ws), "%s/ws", g_root);
|
||||
snprintf(cfgdir, sizeof(cfgdir), "%s/cfg", g_root);
|
||||
@@ -94,8 +97,8 @@ int main(void) {
|
||||
char link[512], target[512];
|
||||
snprintf(link, sizeof(link), "%s/ws/.agent/skills/forms/leak", g_root);
|
||||
snprintf(target, sizeof(target), "%s/ws/secret.txt", g_root);
|
||||
if (symlink(target, link) != 0)
|
||||
perror("symlink");
|
||||
if (test_symlink(target, link) != 0)
|
||||
fprintf(stderr, "note: no symlinks here, escape check is partial\n");
|
||||
}
|
||||
|
||||
cst_skill_t v[CST_SKILLS_MAX];
|
||||
@@ -180,9 +183,7 @@ int main(void) {
|
||||
CHECK(sp && !strstr(sp, "Skills ("), "minimal prompt unchanged");
|
||||
free(sp);
|
||||
|
||||
char cmd[400];
|
||||
snprintf(cmd, sizeof(cmd), "rm -rf %s", g_root);
|
||||
if (system(cmd) != 0)
|
||||
if (test_rmrf(g_root) != 0)
|
||||
fprintf(stderr, "note: cleanup failed\n");
|
||||
if (nfail) {
|
||||
fprintf(stderr, "test_skills: %d failure(s)\n", nfail);
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include "cst_test_util.h"
|
||||
|
||||
/* Drives cst_llm_chat_stream through CST_REPLAY_SSE with synthetic and
|
||||
* recorded (tests/fixtures/sse) streams, at several network chunk sizes. */
|
||||
@@ -279,18 +280,14 @@ static void test_recorded(void) {
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
snprintf(g_dir, sizeof(g_dir), "/tmp/cst-sse-test-%d", (int)getpid());
|
||||
char cmd[300];
|
||||
snprintf(cmd, sizeof(cmd), "mkdir -p %s", g_dir);
|
||||
if (system(cmd) != 0)
|
||||
if (test_tmpdir(g_dir, sizeof(g_dir), "cst-sse-test") != 0)
|
||||
return 1;
|
||||
test_text_reasoning_usage();
|
||||
test_parallel_calls();
|
||||
test_same_index_new_id();
|
||||
test_invalid_args_and_error();
|
||||
test_recorded();
|
||||
snprintf(cmd, sizeof(cmd), "rm -rf %s", g_dir);
|
||||
if (system(cmd) != 0)
|
||||
if (test_rmrf(g_dir) != 0)
|
||||
fprintf(stderr, "warning: cleanup failed\n");
|
||||
if (nfail) {
|
||||
fprintf(stderr, "test_sse_decode: %d failed\n", nfail);
|
||||
|
||||
+27
-32
@@ -15,19 +15,16 @@
|
||||
#include "cst_util.h"
|
||||
#include "os_compat.h"
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#define CST_TEST_SOCKETS
|
||||
#include "cst_test_util.h"
|
||||
|
||||
#include <curl/curl.h>
|
||||
#include <netinet/in.h>
|
||||
#include <pthread.h>
|
||||
#include <signal.h>
|
||||
#include <stdatomic.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static int nfail;
|
||||
|
||||
@@ -45,9 +42,9 @@ static atomic_int g_child_active, g_child_max, g_child_reqs;
|
||||
static atomic_int g_child_saw_task, g_child_saw_write, g_child_role_prompt;
|
||||
static atomic_int g_parent_saw_task;
|
||||
|
||||
static void send_all(int fd, const char *s, size_t n) {
|
||||
static void send_all(test_sock_t fd, const char *s, size_t n) {
|
||||
while (n > 0) {
|
||||
ssize_t w = send(fd, s, n, MSG_NOSIGNAL);
|
||||
long w = send(fd, s, (int)n, MSG_NOSIGNAL);
|
||||
if (w <= 0)
|
||||
return;
|
||||
s += w;
|
||||
@@ -55,7 +52,7 @@ static void send_all(int fd, const char *s, size_t n) {
|
||||
}
|
||||
}
|
||||
|
||||
static void sse(int fd, cJSON *delta, const char *finish) {
|
||||
static void sse(test_sock_t fd, cJSON *delta, const char *finish) {
|
||||
cJSON *o = cJSON_CreateObject();
|
||||
cJSON *ch = cJSON_AddArrayToObject(o, "choices");
|
||||
cJSON *c0 = cJSON_CreateObject();
|
||||
@@ -80,7 +77,7 @@ static void sse(int fd, cJSON *delta, const char *finish) {
|
||||
cJSON_Delete(o);
|
||||
}
|
||||
|
||||
static void reply_text(int fd, const char *text) {
|
||||
static void reply_text(test_sock_t fd, const char *text) {
|
||||
cJSON *d = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(d, "role", "assistant");
|
||||
cJSON_AddStringToObject(d, "content", text);
|
||||
@@ -90,7 +87,7 @@ static void reply_text(int fd, const char *text) {
|
||||
}
|
||||
|
||||
/* calls: {name, args_json} pairs */
|
||||
static void reply_calls(int fd, const char *const *calls, int n) {
|
||||
static void reply_calls(test_sock_t fd, const char *const *calls, int n) {
|
||||
for (int i = 0; i < n; i++) {
|
||||
cJSON *d = cJSON_CreateObject();
|
||||
cJSON *tc = cJSON_AddArrayToObject(d, "tool_calls");
|
||||
@@ -121,7 +118,7 @@ static int has_tool(const cJSON *body, const char *name) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void handle(int fd, const char *body_txt) {
|
||||
static void handle(test_sock_t fd, const char *body_txt) {
|
||||
cJSON *body = cJSON_Parse(body_txt);
|
||||
const cJSON *msgs = cJSON_GetObjectItemCaseSensitive(body, "messages");
|
||||
int nm = cJSON_GetArraySize(msgs);
|
||||
@@ -155,9 +152,9 @@ static void handle(int fd, const char *body_txt) {
|
||||
if (strstr(sys->valuestring, "Role instructions:\nYou only read."))
|
||||
atomic_store(&g_child_role_prompt, 1);
|
||||
if (strstr(user, "Slow")) {
|
||||
usleep(4000 * 1000); /* cancelled before this ends */
|
||||
test_sleep_ms(4000); /* cancelled before this ends */
|
||||
} else {
|
||||
usleep(300 * 1000);
|
||||
test_sleep_ms(300);
|
||||
}
|
||||
if (strcmp(last_role, "user") == 0) {
|
||||
const char *calls[] = {"read_file", "{\"path\":\"a.txt\"}"};
|
||||
@@ -206,13 +203,13 @@ static void handle(int fd, const char *body_txt) {
|
||||
}
|
||||
|
||||
static void *conn_main(void *p) {
|
||||
int fd = (int)(intptr_t)p;
|
||||
test_sock_t fd = (test_sock_t)(intptr_t)p;
|
||||
cst_buf_t req = {0};
|
||||
char tmp[8192];
|
||||
long need = -1;
|
||||
size_t hdr_end = 0;
|
||||
for (;;) {
|
||||
ssize_t n = recv(fd, tmp, sizeof(tmp), 0);
|
||||
long n = recv(fd, tmp, sizeof(tmp), 0);
|
||||
if (n <= 0)
|
||||
break;
|
||||
cst_buf_append(&req, tmp, (size_t)n);
|
||||
@@ -230,23 +227,23 @@ static void *conn_main(void *p) {
|
||||
if (hdr_end)
|
||||
handle(fd, req.buf + hdr_end);
|
||||
cst_buf_free(&req);
|
||||
close(fd);
|
||||
test_sock_close(fd);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int g_lfd;
|
||||
static test_sock_t g_lfd;
|
||||
|
||||
static void *server_main(void *p) {
|
||||
(void)p;
|
||||
for (;;) {
|
||||
int fd = accept(g_lfd, NULL, NULL);
|
||||
if (fd < 0)
|
||||
test_sock_t fd = accept(g_lfd, NULL, NULL);
|
||||
if (fd == TEST_BAD_SOCK)
|
||||
return NULL;
|
||||
pthread_t th;
|
||||
if (pthread_create(&th, NULL, conn_main, (void *)(intptr_t)fd) == 0)
|
||||
pthread_detach(th);
|
||||
else
|
||||
close(fd);
|
||||
test_sock_close(fd);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,10 +279,10 @@ static void put(const char *root, const char *rel, const char *text) {
|
||||
for (char *q = p + strlen(root) + 1; *q; q++)
|
||||
if (*q == '/') {
|
||||
*q = 0;
|
||||
mkdir(p, 0755);
|
||||
test_mkdir(p, 0755);
|
||||
*q = '/';
|
||||
}
|
||||
FILE *fp = fopen(p, "w");
|
||||
FILE *fp = fopen(p, "wb");
|
||||
if (fp) {
|
||||
fputs(text, fp);
|
||||
fclose(fp);
|
||||
@@ -293,13 +290,13 @@ static void put(const char *root, const char *rel, const char *text) {
|
||||
}
|
||||
|
||||
static void *cancel_later(void *p) {
|
||||
usleep(800 * 1000);
|
||||
test_sleep_ms(800);
|
||||
cst_agent_request_cancel(p);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
test_sock_init();
|
||||
curl_global_init(CURL_GLOBAL_DEFAULT);
|
||||
g_lfd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
struct sockaddr_in addr;
|
||||
@@ -316,10 +313,10 @@ int main(void) {
|
||||
pthread_t srv;
|
||||
pthread_create(&srv, NULL, server_main, NULL);
|
||||
|
||||
char root[256], ws[300];
|
||||
snprintf(root, sizeof(root), "/tmp/cst-sub-%d", (int)getpid());
|
||||
char root[600], ws[700];
|
||||
if (test_tmpdir(root, sizeof(root), "cst-sub") != 0)
|
||||
return 1;
|
||||
snprintf(ws, sizeof(ws), "%s/ws", root);
|
||||
mkdir(root, 0755);
|
||||
setenv("XDG_CONFIG_HOME", root, 1);
|
||||
put(root, "ws/a.txt", "alpha\n");
|
||||
put(root, "ws/.agent/agents/reviewer.md",
|
||||
@@ -470,11 +467,9 @@ int main(void) {
|
||||
CHECK(cst_subagent_list(ws, "parent1", recs, 8) == 0,
|
||||
"delete removes sub-agent transcripts");
|
||||
|
||||
char cmd[400];
|
||||
snprintf(cmd, sizeof(cmd), "rm -rf %s", root);
|
||||
if (system(cmd) != 0)
|
||||
if (test_rmrf(root) != 0)
|
||||
fprintf(stderr, "note: cleanup failed\n");
|
||||
close(g_lfd);
|
||||
test_sock_close(g_lfd);
|
||||
if (nfail) {
|
||||
fprintf(stderr, "test_subagent: %d failure(s)\n", nfail);
|
||||
return 1;
|
||||
|
||||
+12
-10
@@ -12,6 +12,8 @@
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "cst_test_util.h"
|
||||
|
||||
static int nfail;
|
||||
|
||||
#define EXPECT(cond, msg) \
|
||||
@@ -98,17 +100,19 @@ static int run(cst_tool_ctx_t *ctx, ctl_tool_call_t c, cst_tool_result_t *r) {
|
||||
static void reset(cst_tool_result_t *r) { cst_tool_result_free(r); }
|
||||
|
||||
int main(void) {
|
||||
snprintf(g_ws, sizeof(g_ws), "/tmp/cst-tools-test-%d", (int)getpid());
|
||||
mkdir(g_ws, 0755);
|
||||
if (test_tmpdir(g_ws, sizeof(g_ws), "cst-tools-test") != 0) {
|
||||
perror("tmpdir");
|
||||
return 1;
|
||||
}
|
||||
char p[512];
|
||||
snprintf(p, sizeof(p), "%s/src", g_ws);
|
||||
mkdir(p, 0755);
|
||||
test_mkdir(p, 0755);
|
||||
snprintf(p, sizeof(p), "%s/src/deep", g_ws);
|
||||
mkdir(p, 0755);
|
||||
test_mkdir(p, 0755);
|
||||
snprintf(p, sizeof(p), "%s/node_modules", g_ws);
|
||||
mkdir(p, 0755);
|
||||
test_mkdir(p, 0755);
|
||||
snprintf(p, sizeof(p), "%s/build", g_ws);
|
||||
mkdir(p, 0755);
|
||||
test_mkdir(p, 0755);
|
||||
wfile("src/main.c", "#include <stdio.h>\nint main(void) {\n int x = 42;\n"
|
||||
" printf(\"%d\\n\", x);\n return 0;\n}\n");
|
||||
wfile("src/deep/util.c", "int add(int a, int b) { return a + b; }\n"
|
||||
@@ -119,7 +123,7 @@ int main(void) {
|
||||
wfile(".gitignore", "build/\n*.log\n");
|
||||
wfile("run.log", "int add in log\n");
|
||||
{
|
||||
FILE *fp = fopen(strcat(strcpy(p, g_ws), "/lines.txt"), "w");
|
||||
FILE *fp = fopen(strcat(strcpy(p, g_ws), "/lines.txt"), "wb");
|
||||
for (int i = 1; i <= 100; i++)
|
||||
fprintf(fp, "line %d\n", i);
|
||||
fclose(fp);
|
||||
@@ -410,9 +414,7 @@ int main(void) {
|
||||
"patch targets");
|
||||
}
|
||||
|
||||
char cmd[400];
|
||||
snprintf(cmd, sizeof(cmd), "rm -rf %s", g_ws);
|
||||
if (system(cmd) != 0)
|
||||
if (test_rmrf(g_ws) != 0)
|
||||
fprintf(stderr, "note: cleanup failed\n");
|
||||
if (nfail) {
|
||||
fprintf(stderr, "test_tools_fs: %d failure(s)\n", nfail);
|
||||
|
||||
+6
-8
@@ -10,6 +10,7 @@
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include "cst_test_util.h"
|
||||
|
||||
static int nfail;
|
||||
|
||||
@@ -81,9 +82,9 @@ static void test_undo(void) {
|
||||
EXPECT(cst_undo_snapshot(g_ws, sid, "t3", "d/e/f.txt") == 0, "snap nested");
|
||||
char p[512];
|
||||
snprintf(p, sizeof(p), "%s/d", g_ws);
|
||||
mkdir(p, 0755);
|
||||
test_mkdir(p, 0755);
|
||||
snprintf(p, sizeof(p), "%s/d/e", g_ws);
|
||||
mkdir(p, 0755);
|
||||
test_mkdir(p, 0755);
|
||||
wfile("d/e/f.txt", "x");
|
||||
EXPECT(cst_undo_restore_last(g_ws, sid, rep, sizeof(rep)) == 0, "undo t3");
|
||||
EXPECT(rfile_eq("d/e/f.txt", NULL), "nested file removed");
|
||||
@@ -146,7 +147,7 @@ static void test_store(void) {
|
||||
EXPECT(cst_agent_store_path_for_id(g_ws, "20260101-000000-abcd", path,
|
||||
sizeof(path)) == 0,
|
||||
"path for id");
|
||||
FILE *fp = fopen(path, "w");
|
||||
FILE *fp = fopen(path, "wb");
|
||||
EXPECT(fp != NULL, "create session file");
|
||||
if (fp) {
|
||||
fputs("{\"role\":\"system\",\"content\":\"sys\"}\n", fp);
|
||||
@@ -174,17 +175,14 @@ static void test_store(void) {
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
snprintf(g_ws, sizeof(g_ws), "/tmp/cst_test_undo_XXXXXX");
|
||||
if (!mkdtemp(g_ws)) {
|
||||
if (test_tmpdir(g_ws, sizeof(g_ws), "cst_test_undo") != 0) {
|
||||
perror("mkdtemp");
|
||||
return 1;
|
||||
}
|
||||
test_undo();
|
||||
test_perms();
|
||||
test_store();
|
||||
char cmd[512];
|
||||
snprintf(cmd, sizeof(cmd), "rm -rf '%s'", g_ws);
|
||||
if (system(cmd) != 0)
|
||||
if (test_rmrf(g_ws) != 0)
|
||||
fprintf(stderr, "warning: cleanup failed\n");
|
||||
if (nfail) {
|
||||
fprintf(stderr, "test_undo: %d failure(s)\n", nfail);
|
||||
|
||||
@@ -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