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:
+29
-11
@@ -270,7 +270,7 @@ static void git_branch(const char *ws, char *out, size_t n) {
|
||||
for (int depth = 0; depth < 8; depth++) {
|
||||
char p[4200], line[512];
|
||||
snprintf(p, sizeof(p), "%s/.git", dir);
|
||||
FILE *fp = fopen(p, "r");
|
||||
FILE *fp = fopen(p, "rb");
|
||||
if (fp) {
|
||||
/* worktree / submodule: ".git" is a file "gitdir: <path>" */
|
||||
char gd[4096] = "";
|
||||
@@ -286,7 +286,7 @@ static void git_branch(const char *ws, char *out, size_t n) {
|
||||
snprintf(p, sizeof(p), "%s/HEAD", gd);
|
||||
else
|
||||
snprintf(p, sizeof(p), "%s/.git/HEAD", dir);
|
||||
fp = fopen(p, "r");
|
||||
fp = fopen(p, "rb");
|
||||
if (fp) {
|
||||
if (fgets(line, sizeof(line), fp)) {
|
||||
line[strcspn(line, "\r\n")] = 0;
|
||||
@@ -308,7 +308,13 @@ static void git_branch(const char *ws, char *out, size_t n) {
|
||||
|
||||
static void os_label(char *out, size_t n) {
|
||||
#ifdef _WIN32
|
||||
snprintf(out, n, "Windows");
|
||||
char ver[64];
|
||||
os_windows_version(ver, sizeof(ver));
|
||||
snprintf(out, n,
|
||||
"%s; run_shell is busybox sh (POSIX syntax and applets; write paths "
|
||||
"as C:/dir or relative, Windows programs like git or python run "
|
||||
"from PATH)",
|
||||
ver);
|
||||
#else
|
||||
struct utsname u;
|
||||
if (uname(&u) == 0)
|
||||
@@ -780,9 +786,14 @@ static void sandbox_escalate(turn_t *t, cst_tool_ctx_t *ctx,
|
||||
free(detail);
|
||||
}
|
||||
if (ans == CST_PERM_DENY) {
|
||||
const char *note = "\n[cst: blocked by the sandbox (writes are limited to "
|
||||
"the workspace and temp dir, no network); the user did "
|
||||
"not allow running it outside]\n";
|
||||
const char *note =
|
||||
cst_sandbox_probe() == CST_SANDBOX_LOWIL
|
||||
? "\n[cst: blocked by the sandbox (writes are limited to the "
|
||||
"workspace and temp dir); the user did not allow running it "
|
||||
"outside]\n"
|
||||
: "\n[cst: blocked by the sandbox (writes are limited to "
|
||||
"the workspace and temp dir, no network); the user did "
|
||||
"not allow running it outside]\n";
|
||||
cst_buf_t b = {0};
|
||||
if (tr->out)
|
||||
cst_buf_append(&b, tr->out, tr->out_len);
|
||||
@@ -924,9 +935,11 @@ static void exec_begin(turn_t *t, pcall_t *pc, exec_t *x) {
|
||||
if (def->perm == CST_TP_WRITE) {
|
||||
ask = 0;
|
||||
} else if (def->perm == CST_TP_SHELL) {
|
||||
if (cst_sandbox_probe() != CST_SANDBOX_NONE) {
|
||||
int sb = cst_sandbox_probe();
|
||||
if (sb != CST_SANDBOX_NONE) {
|
||||
ctx->sandbox = 1;
|
||||
ask = 0;
|
||||
/* the Low-IL sandbox does not stop network access */
|
||||
ask = sb == CST_SANDBOX_LOWIL && cmd && cst_shell_uses_network(cmd);
|
||||
} else if (cmd && cst_shell_is_safe(cmd)) {
|
||||
ask = 0;
|
||||
}
|
||||
@@ -1707,8 +1720,12 @@ void cst_agent_session_id(const cst_agent_t *a, char *out, size_t out_sz) {
|
||||
return;
|
||||
const char *p = a->cfg.session_path;
|
||||
const char *sl = strrchr(p, '/');
|
||||
snprintf(out, out_sz, "%s", sl ? sl + 1 : p);
|
||||
size_t n = strlen(out);
|
||||
const char *base = sl ? sl + 1 : p;
|
||||
size_t n = strlen(base);
|
||||
if (n >= out_sz)
|
||||
n = out_sz - 1;
|
||||
memcpy(out, base, n);
|
||||
out[n] = 0;
|
||||
if (n > 6 && strcmp(out + n - 6, ".jsonl") == 0)
|
||||
out[n - 6] = 0;
|
||||
}
|
||||
@@ -1747,7 +1764,8 @@ void cst_agent_reload_notes(cst_agent_t *a) {
|
||||
fclose(fp);
|
||||
if (b.len && b.buf) {
|
||||
size_t clean_len = 0;
|
||||
a->notes = cst_utf8_sanitize(b.buf, b.len, &clean_len);
|
||||
size_t bom = cst_bom_len(b.buf, b.len);
|
||||
a->notes = cst_utf8_sanitize(b.buf + bom, b.len - bom, &clean_len);
|
||||
}
|
||||
cst_buf_free(&b);
|
||||
return;
|
||||
|
||||
+20
-21
@@ -6,7 +6,6 @@
|
||||
#include "cst_agent.h"
|
||||
#include "os_compat.h"
|
||||
|
||||
#include <dirent.h>
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -45,7 +44,7 @@ int cst_agent_store_ensure(const char *workspace) {
|
||||
char gi[OS_PATH_MAX];
|
||||
if (os_path_join(gi, sizeof(gi), base, ".gitignore") == 0 &&
|
||||
OS_ACCESS(gi, OS_F_OK) != 0) {
|
||||
FILE *fp = fopen(gi, "w");
|
||||
FILE *fp = fopen(gi, "wb");
|
||||
if (fp) {
|
||||
fputs("*\n", fp);
|
||||
fclose(fp);
|
||||
@@ -97,7 +96,7 @@ int cst_agent_store_set_current(const char *workspace, const char *id) {
|
||||
return -1;
|
||||
if (os_path_join(path, sizeof(path), dir, "CURRENT") != 0)
|
||||
return -1;
|
||||
FILE *fp = fopen(path, "w");
|
||||
FILE *fp = fopen(path, "wb");
|
||||
if (!fp)
|
||||
return -1;
|
||||
fprintf(fp, "%s\n", id);
|
||||
@@ -113,7 +112,7 @@ int cst_agent_store_get_current(const char *workspace, char *id_out,
|
||||
return -1;
|
||||
if (os_path_join(path, sizeof(path), dir, "CURRENT") != 0)
|
||||
return -1;
|
||||
FILE *fp = fopen(path, "r");
|
||||
FILE *fp = fopen(path, "rb");
|
||||
if (!fp)
|
||||
return -1;
|
||||
if (!fgets(id_out, (int)id_sz, fp)) {
|
||||
@@ -159,7 +158,7 @@ int cst_agent_store_list(const char *workspace, char ids[][128], int max_ids) {
|
||||
char dir[OS_PATH_MAX];
|
||||
if (cst_agent_store_dir(workspace, dir, sizeof(dir)) != 0)
|
||||
return 0;
|
||||
DIR *d = opendir(dir);
|
||||
os_dir_t *d = os_dir_open(dir);
|
||||
if (!d)
|
||||
return 0;
|
||||
typedef struct {
|
||||
@@ -168,23 +167,23 @@ int cst_agent_store_list(const char *workspace, char ids[][128], int max_ids) {
|
||||
} ent_t;
|
||||
ent_t *ents = calloc((size_t)max_ids * 4, sizeof(ent_t));
|
||||
int n = 0, cap = max_ids * 4;
|
||||
struct dirent *de;
|
||||
while ((de = readdir(d)) != NULL) {
|
||||
size_t len = strlen(de->d_name);
|
||||
if (len < 7 || strcmp(de->d_name + len - 6, ".jsonl") != 0)
|
||||
const os_dirent_t *de;
|
||||
while ((de = os_dir_next(d)) != NULL) {
|
||||
size_t len = strlen(de->name);
|
||||
if (len < 7 || strcmp(de->name + len - 6, ".jsonl") != 0)
|
||||
continue;
|
||||
if (n >= cap)
|
||||
break;
|
||||
snprintf(ents[n].id, sizeof(ents[n].id), "%.*s", (int)(len - 6),
|
||||
de->d_name);
|
||||
de->name);
|
||||
char full[OS_PATH_MAX];
|
||||
if (os_path_join(full, sizeof(full), dir, de->d_name) != 0)
|
||||
if (os_path_join(full, sizeof(full), dir, de->name) != 0)
|
||||
continue;
|
||||
struct stat st;
|
||||
ents[n].mtime = (stat(full, &st) == 0) ? st.st_mtime : 0;
|
||||
n++;
|
||||
}
|
||||
closedir(d);
|
||||
os_dir_close(d);
|
||||
for (int i = 0; i < n; i++) {
|
||||
for (int j = i + 1; j < n; j++) {
|
||||
if (ents[j].mtime > ents[i].mtime) {
|
||||
@@ -218,7 +217,7 @@ int cst_agent_store_title_set(const char *workspace, const char *id,
|
||||
return -1;
|
||||
if (!title || !title[0])
|
||||
return remove(p) == 0 || errno == ENOENT ? 0 : -1;
|
||||
FILE *fp = fopen(p, "w");
|
||||
FILE *fp = fopen(p, "wb");
|
||||
if (!fp)
|
||||
return -1;
|
||||
for (const char *c = title; *c; c++)
|
||||
@@ -255,7 +254,7 @@ int cst_agent_store_info(const char *workspace, const char *id,
|
||||
info->mtime = (long long)st.st_mtime;
|
||||
char tp[OS_PATH_MAX];
|
||||
if (side_path(workspace, id, ".title", tp, sizeof(tp)) == 0) {
|
||||
FILE *tf = fopen(tp, "r");
|
||||
FILE *tf = fopen(tp, "rb");
|
||||
if (tf) {
|
||||
if (fgets(info->title, sizeof(info->title), tf)) {
|
||||
size_t n = strlen(info->title);
|
||||
@@ -265,7 +264,7 @@ int cst_agent_store_info(const char *workspace, const char *id,
|
||||
fclose(tf);
|
||||
}
|
||||
}
|
||||
FILE *fp = fopen(p, "r");
|
||||
FILE *fp = fopen(p, "rb");
|
||||
if (!fp)
|
||||
return 0;
|
||||
size_t cap = 1 << 16;
|
||||
@@ -311,16 +310,16 @@ int cst_agent_store_delete(const char *workspace, const char *id) {
|
||||
remove(p);
|
||||
/* sub-agent transcripts live in <id>/ */
|
||||
if (side_path(workspace, id, "", p, sizeof(p)) == 0) {
|
||||
DIR *d = opendir(p);
|
||||
os_dir_t *d = os_dir_open(p);
|
||||
if (d) {
|
||||
struct dirent *de;
|
||||
while ((de = readdir(d)) != NULL) {
|
||||
const os_dirent_t *de;
|
||||
while ((de = os_dir_next(d)) != NULL) {
|
||||
char f[OS_PATH_MAX];
|
||||
if (strncmp(de->d_name, "sub-", 4) == 0 &&
|
||||
os_path_join(f, sizeof(f), p, de->d_name) == 0)
|
||||
if (strncmp(de->name, "sub-", 4) == 0 &&
|
||||
os_path_join(f, sizeof(f), p, de->name) == 0)
|
||||
remove(f);
|
||||
}
|
||||
closedir(d);
|
||||
os_dir_close(d);
|
||||
rmdir(p);
|
||||
}
|
||||
}
|
||||
|
||||
+18
-10
@@ -12,6 +12,7 @@
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <io.h>
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <fcntl.h>
|
||||
#include <termios.h>
|
||||
@@ -51,7 +52,7 @@ int cst_creds_load(const char *provider, char *out, size_t out_sz) {
|
||||
char path[OS_PATH_MAX];
|
||||
if (credentials_path(path, sizeof(path)) != 0)
|
||||
return -1;
|
||||
FILE *fp = fopen(path, "r");
|
||||
FILE *fp = fopen(path, "rb");
|
||||
if (!fp)
|
||||
return -1;
|
||||
char want[64];
|
||||
@@ -104,7 +105,9 @@ int cst_creds_save(const char *provider, const char *key) {
|
||||
size_t wl = strlen(want);
|
||||
|
||||
#ifdef _WIN32
|
||||
FILE *out = fopen(tmp, "w");
|
||||
FILE *out = fopen(tmp, "wb");
|
||||
if (out)
|
||||
(void)os_chmod_private(tmp); /* label before the key is written */
|
||||
#else
|
||||
/* never exists world-readable, even briefly */
|
||||
int fd = open(tmp, O_WRONLY | O_CREAT | O_TRUNC, 0600);
|
||||
@@ -114,7 +117,7 @@ int cst_creds_save(const char *provider, const char *key) {
|
||||
#endif
|
||||
if (!out)
|
||||
return -1;
|
||||
FILE *in = fopen(path, "r");
|
||||
FILE *in = fopen(path, "rb");
|
||||
if (in) {
|
||||
char line[1024];
|
||||
while (fgets(line, sizeof(line), in)) {
|
||||
@@ -128,10 +131,7 @@ int cst_creds_save(const char *provider, const char *key) {
|
||||
remove(tmp);
|
||||
return -1;
|
||||
}
|
||||
#ifdef _WIN32
|
||||
remove(path);
|
||||
#endif
|
||||
if (rename(tmp, path) != 0) {
|
||||
if (os_rename_replace(tmp, path) != 0) {
|
||||
remove(tmp);
|
||||
return -1;
|
||||
}
|
||||
@@ -167,7 +167,13 @@ int cst_creds_prompt_console(const char *provider, char *out, size_t out_sz) {
|
||||
fprintf(stderr, "API key for %s (input hidden, saved to the config dir): ",
|
||||
provider ? provider : "?");
|
||||
fflush(stderr);
|
||||
#ifndef _WIN32
|
||||
#ifdef _WIN32
|
||||
HANDLE hin = GetStdHandle(STD_INPUT_HANDLE);
|
||||
DWORD old_mode = 0;
|
||||
int tty = GetConsoleMode(hin, &old_mode) != 0;
|
||||
if (tty)
|
||||
SetConsoleMode(hin, old_mode & ~(DWORD)ENABLE_ECHO_INPUT);
|
||||
#else
|
||||
struct termios old, quiet;
|
||||
int tty = isatty(STDIN_FILENO) && tcgetattr(STDIN_FILENO, &old) == 0;
|
||||
if (tty) {
|
||||
@@ -177,12 +183,14 @@ int cst_creds_prompt_console(const char *provider, char *out, size_t out_sz) {
|
||||
}
|
||||
#endif
|
||||
char *got = fgets(out, (int)out_sz, stdin);
|
||||
#ifndef _WIN32
|
||||
if (tty) {
|
||||
#ifdef _WIN32
|
||||
SetConsoleMode(hin, old_mode);
|
||||
#else
|
||||
tcsetattr(STDIN_FILENO, TCSAFLUSH, &old);
|
||||
#endif
|
||||
fputc('\n', stderr);
|
||||
}
|
||||
#endif
|
||||
if (!got) {
|
||||
out[0] = 0;
|
||||
return -1;
|
||||
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
#include "cst_keymap.h"
|
||||
|
||||
#include "cJSON.h"
|
||||
#include "cst_util.h"
|
||||
#include "os_compat.h"
|
||||
|
||||
#include <ctype.h>
|
||||
@@ -491,7 +492,7 @@ int cst_keymap_load(cst_keymap_t *km, char *err, size_t err_sz) {
|
||||
size_t n = fread(buf, 1, sizeof(buf) - 1, fp);
|
||||
fclose(fp);
|
||||
buf[n] = 0;
|
||||
return cst_keymap_apply_json(km, buf, err, err_sz);
|
||||
return cst_keymap_apply_json(km, buf + cst_bom_len(buf, n), err, err_sz);
|
||||
}
|
||||
|
||||
int cst_keymap_lookup(const cst_keymap_t *km, const int *seq, int n,
|
||||
|
||||
+5
-5
@@ -805,11 +805,11 @@ int cst_llm_list_models(const char *api_base, const char *api_key,
|
||||
}
|
||||
CURL *curl = curl_easy_init();
|
||||
if (!curl) {
|
||||
snprintf(err, err_sz, "curl init failed");
|
||||
return -1;
|
||||
}
|
||||
llm_apply_ca_bundle(curl);
|
||||
char url[1024];
|
||||
snprintf(err, err_sz, "curl init failed");
|
||||
return -1;
|
||||
}
|
||||
llm_apply_ca_bundle(curl);
|
||||
char url[1024];
|
||||
size_t bl = strlen(api_base);
|
||||
while (bl && api_base[bl - 1] == '/')
|
||||
bl--;
|
||||
|
||||
@@ -188,6 +188,9 @@ static int demo_ctl_impl(const char *workspace) {
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (os_console_init() != 0)
|
||||
fprintf(stderr, "warning: the process code page is not UTF-8 (Windows 10 "
|
||||
"1903 or later is required); non-ASCII text may break\n");
|
||||
os_set_prog_path(argv[0]);
|
||||
curl_global_init(CURL_GLOBAL_DEFAULT);
|
||||
|
||||
|
||||
@@ -268,6 +268,7 @@ static char *slurp(const char *path, size_t max) {
|
||||
fclose(fp);
|
||||
if (!b.buf)
|
||||
cst_buf_puts(&b, "");
|
||||
cst_strip_bom(b.buf, &b.len);
|
||||
return cst_buf_steal(&b);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -47,7 +47,7 @@ cst_metrics_t *cst_metrics_open(const char *workspace, const char *session_path)
|
||||
snprintf(file, sizeof(file), "%s.jsonl", leaf);
|
||||
os_path_join(m->path, sizeof(m->path), dir, file);
|
||||
}
|
||||
m->fp = fopen(m->path, "a");
|
||||
m->fp = fopen(m->path, "ab");
|
||||
if (!m->fp) {
|
||||
free(m);
|
||||
return NULL;
|
||||
|
||||
+150
-345
@@ -10,6 +10,7 @@
|
||||
#include <stdint.h>
|
||||
#endif
|
||||
#include "os_compat.h"
|
||||
#include "os_internal.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
@@ -26,6 +27,7 @@
|
||||
#include <process.h>
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <dirent.h>
|
||||
#include <fcntl.h>
|
||||
#include <poll.h>
|
||||
#include <pthread.h>
|
||||
@@ -93,26 +95,10 @@ int os_path_join(char *out, size_t out_sz, const char *a, const char *b) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
#ifndef _WIN32
|
||||
int os_realpath(const char *path, char *out, size_t out_sz) {
|
||||
if (!path || !out || out_sz < 2)
|
||||
return -1;
|
||||
#ifdef _WIN32
|
||||
char buf[OS_PATH_MAX];
|
||||
DWORD n = GetFullPathNameA(path, (DWORD)sizeof(buf), buf, NULL);
|
||||
if (n == 0 || n >= sizeof(buf))
|
||||
return -1;
|
||||
/* Prefer forward slashes internally for sandbox compares. */
|
||||
os_path_norm_slashes(buf);
|
||||
if (strlen(buf) + 1 > out_sz)
|
||||
return -1;
|
||||
memcpy(out, buf, strlen(buf) + 1);
|
||||
/* Lowercase drive letter for stable compares */
|
||||
if (out[0] && out[1] == ':') {
|
||||
if (out[0] >= 'A' && out[0] <= 'Z')
|
||||
out[0] = (char)(out[0] - 'A' + 'a');
|
||||
}
|
||||
return 0;
|
||||
#else
|
||||
char *r = realpath(path, NULL);
|
||||
if (!r)
|
||||
return -1;
|
||||
@@ -123,7 +109,6 @@ int os_realpath(const char *path, char *out, size_t out_sz) {
|
||||
memcpy(out, r, strlen(r) + 1);
|
||||
free(r);
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
int os_path_under(const char *root, const char *path) {
|
||||
@@ -132,14 +117,9 @@ int os_path_under(const char *root, const char *path) {
|
||||
size_t rl = strlen(root);
|
||||
if (strncmp(path, root, rl) != 0)
|
||||
return 0;
|
||||
if (path[rl] == '\0')
|
||||
return 1;
|
||||
#ifdef _WIN32
|
||||
return path[rl] == '/' || path[rl] == '\\';
|
||||
#else
|
||||
return path[rl] == '/';
|
||||
#endif
|
||||
return path[rl] == '\0' || path[rl] == '/';
|
||||
}
|
||||
#endif
|
||||
|
||||
int os_mkdir(const char *path, int mode) {
|
||||
if (!path)
|
||||
@@ -172,16 +152,93 @@ int os_mkdir_p(const char *path, int mode) {
|
||||
return os_mkdir(tmp, mode);
|
||||
}
|
||||
|
||||
#ifndef _WIN32
|
||||
struct os_dir {
|
||||
DIR *d;
|
||||
char path[OS_PATH_MAX];
|
||||
os_dirent_t ent;
|
||||
};
|
||||
|
||||
os_dir_t *os_dir_open(const char *path) {
|
||||
if (!path)
|
||||
return NULL;
|
||||
os_dir_t *d = calloc(1, sizeof(*d));
|
||||
if (!d)
|
||||
return NULL;
|
||||
snprintf(d->path, sizeof(d->path), "%s", path);
|
||||
d->d = opendir(path);
|
||||
if (!d->d) {
|
||||
free(d);
|
||||
return NULL;
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
const os_dirent_t *os_dir_next(os_dir_t *d) {
|
||||
struct dirent *de;
|
||||
while (d && (de = readdir(d->d)) != NULL) {
|
||||
if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0)
|
||||
continue;
|
||||
d->ent.name = de->d_name;
|
||||
d->ent.is_dir = d->ent.is_link = 0;
|
||||
#ifdef DT_DIR
|
||||
if (de->d_type != DT_UNKNOWN) {
|
||||
d->ent.is_dir = de->d_type == DT_DIR;
|
||||
d->ent.is_link = de->d_type == DT_LNK;
|
||||
return &d->ent;
|
||||
}
|
||||
#endif
|
||||
char p[OS_PATH_MAX];
|
||||
struct stat st;
|
||||
if (snprintf(p, sizeof(p), "%s/%s", d->path, de->d_name) <
|
||||
(int)sizeof(p) &&
|
||||
lstat(p, &st) == 0) {
|
||||
d->ent.is_dir = S_ISDIR(st.st_mode);
|
||||
d->ent.is_link = S_ISLNK(st.st_mode);
|
||||
}
|
||||
return &d->ent;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void os_dir_close(os_dir_t *d) {
|
||||
if (!d)
|
||||
return;
|
||||
closedir(d->d);
|
||||
free(d);
|
||||
}
|
||||
|
||||
int os_lstat(const char *path, os_stat_t *st) {
|
||||
struct stat s;
|
||||
memset(st, 0, sizeof(*st));
|
||||
if (!path || lstat(path, &s) != 0)
|
||||
return -1;
|
||||
st->is_dir = S_ISDIR(s.st_mode);
|
||||
st->is_reg = S_ISREG(s.st_mode);
|
||||
st->is_link = S_ISLNK(s.st_mode);
|
||||
st->size = (long long)s.st_size;
|
||||
st->mtime = (double)s.st_mtime;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int os_rename_replace(const char *from, const char *to) {
|
||||
return rename(from, to) == 0 ? 0 : -1;
|
||||
}
|
||||
#endif
|
||||
|
||||
int os_config_dir(char *out, size_t out_sz) {
|
||||
if (!out || out_sz < 8)
|
||||
return -1;
|
||||
#ifdef _WIN32
|
||||
const char *base = getenv("APPDATA");
|
||||
const char *base = getenv("XDG_CONFIG_HOME");
|
||||
if (!base || !os_path_is_abs(base))
|
||||
base = getenv("APPDATA");
|
||||
if (!base || !base[0])
|
||||
base = getenv("LOCALAPPDATA");
|
||||
if (!base || !base[0])
|
||||
return -1;
|
||||
int n = snprintf(out, out_sz, "%s\\cst-agent", base);
|
||||
int n = snprintf(out, out_sz, "%s/cst-agent", base);
|
||||
os_path_norm_slashes(out);
|
||||
#else
|
||||
const char *xdg = getenv("XDG_CONFIG_HOME");
|
||||
const char *home = getenv("HOME");
|
||||
@@ -389,7 +446,7 @@ void os_set_run_timeout(int seconds) { g_run_timeout_s = seconds; }
|
||||
|
||||
int os_get_run_timeout(void) { return g_run_timeout_s; }
|
||||
|
||||
static int run_timeout_s(void) {
|
||||
int os_run_timeout_effective(void) {
|
||||
if (g_run_timeout_s > 0)
|
||||
return g_run_timeout_s;
|
||||
const char *e = getenv("CST_SHELL_TIMEOUT");
|
||||
@@ -414,8 +471,8 @@ double os_now_ms(void) {
|
||||
}
|
||||
|
||||
/* Append to capture buffer, keeping at most max_out bytes. */
|
||||
static int cap_append(char **buf, size_t *len, size_t *cap, size_t max_out,
|
||||
const char *data, size_t n, int *truncated) {
|
||||
int os_cap_append(char **buf, size_t *len, size_t *cap, size_t max_out,
|
||||
const char *data, size_t n, int *truncated) {
|
||||
if (*len >= max_out) {
|
||||
*truncated = 1;
|
||||
return 0;
|
||||
@@ -440,7 +497,7 @@ static int cap_append(char **buf, size_t *len, size_t *cap, size_t max_out,
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void cap_note(char **buf, size_t *len, size_t *cap, const char *note) {
|
||||
void os_cap_note(char **buf, size_t *len, size_t *cap, const char *note) {
|
||||
size_t n = strlen(note);
|
||||
if (*len + n + 1 > *cap) {
|
||||
char *nb = realloc(*buf, *len + n + 1);
|
||||
@@ -453,154 +510,7 @@ static void cap_note(char **buf, size_t *len, size_t *cap, const char *note) {
|
||||
*len += n;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
int os_run_capture(const char *cwd, const char *const argv[], size_t max_out,
|
||||
os_run_result_t *r) {
|
||||
if (!argv || !argv[0] || !r)
|
||||
return -1;
|
||||
if (max_out == 0)
|
||||
max_out = 512 * 1024;
|
||||
memset(r, 0, sizeof(*r));
|
||||
|
||||
char cmdline[8192];
|
||||
size_t pos = 0;
|
||||
cmdline[0] = 0;
|
||||
for (int i = 0; argv[i]; i++) {
|
||||
const char *a = argv[i];
|
||||
int need_q = strchr(a, ' ') || strchr(a, '\t') || strchr(a, '"');
|
||||
if (i && pos + 1 < sizeof(cmdline))
|
||||
cmdline[pos++] = ' ';
|
||||
if (need_q) {
|
||||
if (pos + 1 < sizeof(cmdline))
|
||||
cmdline[pos++] = '"';
|
||||
for (const char *p = a; *p && pos + 2 < sizeof(cmdline); p++) {
|
||||
if (*p == '"') {
|
||||
cmdline[pos++] = '\\';
|
||||
cmdline[pos++] = '"';
|
||||
} else {
|
||||
cmdline[pos++] = *p;
|
||||
}
|
||||
}
|
||||
if (pos + 1 < sizeof(cmdline))
|
||||
cmdline[pos++] = '"';
|
||||
} else {
|
||||
size_t L = strlen(a);
|
||||
if (pos + L >= sizeof(cmdline))
|
||||
break;
|
||||
memcpy(cmdline + pos, a, L);
|
||||
pos += L;
|
||||
}
|
||||
cmdline[pos] = 0;
|
||||
}
|
||||
|
||||
SECURITY_ATTRIBUTES sa = {0};
|
||||
sa.nLength = sizeof(sa);
|
||||
sa.bInheritHandle = TRUE;
|
||||
HANDLE rd = NULL, wr = NULL;
|
||||
if (!CreatePipe(&rd, &wr, &sa, 0))
|
||||
return -1;
|
||||
SetHandleInformation(rd, HANDLE_FLAG_INHERIT, 0);
|
||||
|
||||
STARTUPINFOA si = {0};
|
||||
si.cb = sizeof(si);
|
||||
si.dwFlags = STARTF_USESTDHANDLES;
|
||||
si.hStdOutput = wr;
|
||||
si.hStdError = wr;
|
||||
HANDLE nul = CreateFileA("NUL", GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE,
|
||||
&sa, OPEN_EXISTING, 0, NULL);
|
||||
si.hStdInput = nul != INVALID_HANDLE_VALUE ? nul : GetStdHandle(STD_INPUT_HANDLE);
|
||||
PROCESS_INFORMATION pi = {0};
|
||||
|
||||
char *cmd_mut = _strdup(cmdline);
|
||||
if (!cmd_mut) {
|
||||
CloseHandle(rd);
|
||||
CloseHandle(wr);
|
||||
return -1;
|
||||
}
|
||||
BOOL ok = CreateProcessA(NULL, cmd_mut, NULL, NULL, TRUE, 0, NULL,
|
||||
cwd && cwd[0] ? cwd : NULL, &si, &pi);
|
||||
free(cmd_mut);
|
||||
CloseHandle(wr);
|
||||
if (nul != INVALID_HANDLE_VALUE)
|
||||
CloseHandle(nul);
|
||||
if (!ok) {
|
||||
CloseHandle(rd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
size_t cap = 4096, len = 0;
|
||||
char *buf = malloc(cap);
|
||||
if (!buf) {
|
||||
CloseHandle(rd);
|
||||
CloseHandle(pi.hProcess);
|
||||
CloseHandle(pi.hThread);
|
||||
return -1;
|
||||
}
|
||||
buf[0] = 0;
|
||||
int truncated = 0, killed = 0;
|
||||
double deadline = os_now_ms() + run_timeout_s() * 1000.0;
|
||||
for (;;) {
|
||||
DWORD avail = 0;
|
||||
if (!PeekNamedPipe(rd, NULL, 0, NULL, &avail, NULL))
|
||||
break; /* writer closed */
|
||||
if (avail > 0) {
|
||||
char tmp[4096];
|
||||
DWORD n = 0;
|
||||
if (!ReadFile(rd, tmp, sizeof(tmp), &n, NULL) || n == 0)
|
||||
break;
|
||||
cap_append(&buf, &len, &cap, max_out, tmp, n, &truncated);
|
||||
continue;
|
||||
}
|
||||
if (WaitForSingleObject(pi.hProcess, 50) == WAIT_OBJECT_0) {
|
||||
/* drain what is left */
|
||||
while (PeekNamedPipe(rd, NULL, 0, NULL, &avail, NULL) && avail > 0) {
|
||||
char tmp[4096];
|
||||
DWORD n = 0;
|
||||
if (!ReadFile(rd, tmp, sizeof(tmp), &n, NULL) || n == 0)
|
||||
break;
|
||||
cap_append(&buf, &len, &cap, max_out, tmp, n, &truncated);
|
||||
}
|
||||
break;
|
||||
}
|
||||
if ((g_run_cancel && *g_run_cancel) || os_now_ms() > deadline) {
|
||||
killed = (g_run_cancel && *g_run_cancel) ? 2 : 1;
|
||||
TerminateProcess(pi.hProcess, killed == 1 ? 124 : 130);
|
||||
break;
|
||||
}
|
||||
}
|
||||
CloseHandle(rd);
|
||||
WaitForSingleObject(pi.hProcess, INFINITE);
|
||||
DWORD code = 1;
|
||||
GetExitCodeProcess(pi.hProcess, &code);
|
||||
if (truncated)
|
||||
cap_note(&buf, &len, &cap, "\n...(output truncated)\n");
|
||||
if (killed == 1) {
|
||||
char note[96];
|
||||
snprintf(note, sizeof(note), "\n[cst: killed after %ds timeout]\n",
|
||||
run_timeout_s());
|
||||
cap_note(&buf, &len, &cap, note);
|
||||
code = 124;
|
||||
} else if (killed == 2) {
|
||||
cap_note(&buf, &len, &cap, "\n[cst: cancelled]\n");
|
||||
code = 130;
|
||||
}
|
||||
CloseHandle(pi.hProcess);
|
||||
CloseHandle(pi.hThread);
|
||||
buf[len] = 0;
|
||||
r->out = buf;
|
||||
r->out_len = len;
|
||||
r->exit_code = (int)code;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int os_run_capture_ex(const char *cwd, const char *const argv[],
|
||||
size_t max_out, os_run_result_t *r,
|
||||
os_child_hook_fn child_hook, void *ud) {
|
||||
(void)child_hook;
|
||||
(void)ud;
|
||||
return os_run_capture(cwd, argv, max_out, r);
|
||||
}
|
||||
#else
|
||||
#ifndef _WIN32
|
||||
static void cloexec(int fd) {
|
||||
int fl = fcntl(fd, F_GETFD);
|
||||
if (fl >= 0)
|
||||
@@ -685,7 +595,7 @@ int os_run_capture_ex(const char *cwd, const char *const argv[],
|
||||
int truncated = 0, killed = 0;
|
||||
int status = 0, reaped = 0, bg_killed = 0;
|
||||
double reaped_at = 0, killed_at = 0;
|
||||
double deadline = os_now_ms() + run_timeout_s() * 1000.0;
|
||||
double deadline = os_now_ms() + os_run_timeout_effective() * 1000.0;
|
||||
for (;;) {
|
||||
struct pollfd pfd = {pipefd[0], POLLIN, 0};
|
||||
int pr = poll(&pfd, 1, 100);
|
||||
@@ -697,7 +607,7 @@ int os_run_capture_ex(const char *cwd, const char *const argv[],
|
||||
if (n == 0)
|
||||
break; /* every writer closed */
|
||||
if (n > 0)
|
||||
cap_append(&buf, &len, &cap, max_out, tmp, (size_t)n, &truncated);
|
||||
os_cap_append(&buf, &len, &cap, max_out, tmp, (size_t)n, &truncated);
|
||||
else if (errno != EINTR && errno != EAGAIN)
|
||||
break;
|
||||
}
|
||||
@@ -733,9 +643,9 @@ int os_run_capture_ex(const char *cwd, const char *const argv[],
|
||||
waitpid(pid, &status, 0);
|
||||
}
|
||||
if (truncated)
|
||||
cap_note(&buf, &len, &cap, "\n...(output truncated)\n");
|
||||
os_cap_note(&buf, &len, &cap, "\n...(output truncated)\n");
|
||||
if (bg_killed)
|
||||
cap_note(&buf, &len, &cap,
|
||||
os_cap_note(&buf, &len, &cap,
|
||||
"\n[cst: background processes left running were stopped]\n");
|
||||
r->out = buf;
|
||||
r->out_len = len;
|
||||
@@ -743,11 +653,11 @@ int os_run_capture_ex(const char *cwd, const char *const argv[],
|
||||
if (killed == 1) {
|
||||
char note[96];
|
||||
snprintf(note, sizeof(note), "\n[cst: killed after %ds timeout]\n",
|
||||
run_timeout_s());
|
||||
cap_note(&r->out, &r->out_len, &cap, note);
|
||||
os_run_timeout_effective());
|
||||
os_cap_note(&r->out, &r->out_len, &cap, note);
|
||||
r->exit_code = 124;
|
||||
} else if (killed == 2) {
|
||||
cap_note(&r->out, &r->out_len, &cap, "\n[cst: cancelled]\n");
|
||||
os_cap_note(&r->out, &r->out_len, &cap, "\n[cst: cancelled]\n");
|
||||
r->exit_code = 130;
|
||||
}
|
||||
return 0;
|
||||
@@ -759,40 +669,64 @@ int os_run_shell_c(const char *cwd, const char *cmd, size_t max_out,
|
||||
return os_run_shell_ex(cwd, cmd, max_out, r, NULL, NULL);
|
||||
}
|
||||
|
||||
/* busybox: busybox sh -c cmd ; /bin/sh: sh -c cmd. argv needs 5 slots and
|
||||
* points into shell. */
|
||||
static int shell_argv(char *shell, size_t shell_sz, const char *cmd,
|
||||
const char *argv[5]) {
|
||||
if (!cmd || os_resolve_shell(shell, shell_sz) != 0)
|
||||
return -1;
|
||||
const char *base = shell;
|
||||
const char *s = strrchr(shell, '/');
|
||||
#ifdef _WIN32
|
||||
const char *s2 = strrchr(shell, '\\');
|
||||
if (s2 && (!s || s2 > s))
|
||||
s = s2;
|
||||
#endif
|
||||
if (s)
|
||||
base = s + 1;
|
||||
int i = 0;
|
||||
argv[i++] = shell;
|
||||
if (strncmp(base, "busybox", 7) == 0)
|
||||
argv[i++] = "sh";
|
||||
argv[i++] = "-c";
|
||||
argv[i++] = cmd;
|
||||
argv[i] = NULL;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int os_run_shell_ex(const char *cwd, const char *cmd, size_t max_out,
|
||||
os_run_result_t *r, os_child_hook_fn child_hook,
|
||||
void *ud) {
|
||||
if (!cmd || !r)
|
||||
return -1;
|
||||
char shell[OS_PATH_MAX];
|
||||
if (os_resolve_shell(shell, sizeof(shell)) != 0)
|
||||
const char *argv[5];
|
||||
if (!r || shell_argv(shell, sizeof(shell), cmd, argv) != 0)
|
||||
return -1;
|
||||
|
||||
/* busybox: busybox sh -c cmd ; /bin/sh: sh -c cmd */
|
||||
int is_busybox = 0;
|
||||
{
|
||||
const char *base = shell;
|
||||
const char *s = strrchr(shell, '/');
|
||||
#ifdef _WIN32
|
||||
const char *s2 = strrchr(shell, '\\');
|
||||
if (s2 && (!s || s2 > s))
|
||||
s = s2;
|
||||
#endif
|
||||
if (s)
|
||||
base = s + 1;
|
||||
if (strncmp(base, "busybox", 7) == 0)
|
||||
is_busybox = 1;
|
||||
}
|
||||
|
||||
if (is_busybox) {
|
||||
const char *argv[] = {shell, "sh", "-c", cmd, NULL};
|
||||
return os_run_capture_ex(cwd, argv, max_out, r, child_hook, ud);
|
||||
} else {
|
||||
const char *argv[] = {shell, "-c", cmd, NULL};
|
||||
return os_run_capture_ex(cwd, argv, max_out, r, child_hook, ud);
|
||||
}
|
||||
return os_run_capture_ex(cwd, argv, max_out, r, child_hook, ud);
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
int os_run_shell_token(const char *cwd, const char *cmd, size_t max_out,
|
||||
os_run_result_t *r, void *token,
|
||||
const char *const env_add[]) {
|
||||
char shell[OS_PATH_MAX];
|
||||
const char *argv[5];
|
||||
if (!r || shell_argv(shell, sizeof(shell), cmd, argv) != 0)
|
||||
return -1;
|
||||
return os_run_capture_token(cwd, argv, max_out, r, token, env_add);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifndef _WIN32
|
||||
int os_console_init(void) { return 0; }
|
||||
|
||||
char *os_output_to_utf8(const char *s, size_t n, size_t *out_len) {
|
||||
(void)s;
|
||||
(void)n;
|
||||
(void)out_len;
|
||||
return NULL;
|
||||
}
|
||||
#endif
|
||||
|
||||
void os_localtime(const time_t *t, struct tm *out) {
|
||||
if (!t || !out)
|
||||
return;
|
||||
@@ -811,16 +745,13 @@ int os_getpid(void) {
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifndef _WIN32
|
||||
int os_chmod_private(const char *path) {
|
||||
if (!path)
|
||||
return -1;
|
||||
#ifdef _WIN32
|
||||
(void)path;
|
||||
return 0;
|
||||
#else
|
||||
return chmod(path, 0600);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
os_task_fn fn;
|
||||
@@ -905,133 +836,7 @@ int os_cancel_requested(void) { return g_run_cancel && *g_run_cancel; }
|
||||
|
||||
volatile int *os_get_cancel_flag(void) { return g_run_cancel; }
|
||||
|
||||
#ifdef _WIN32
|
||||
static void win_quote_arg(char *out, size_t out_sz, size_t *pos,
|
||||
const char *a) {
|
||||
int need_q = !a[0] || strpbrk(a, " \t\"") != NULL;
|
||||
if (need_q && *pos + 1 < out_sz)
|
||||
out[(*pos)++] = '"';
|
||||
for (const char *p = a; *p && *pos + 2 < out_sz; p++) {
|
||||
if (*p == '"')
|
||||
out[(*pos)++] = '\\';
|
||||
out[(*pos)++] = *p;
|
||||
}
|
||||
if (need_q && *pos + 1 < out_sz)
|
||||
out[(*pos)++] = '"';
|
||||
out[*pos] = 0;
|
||||
}
|
||||
|
||||
int os_proc_spawn(const char *cwd, const char *const argv[],
|
||||
const char *const env_add[], const char *stderr_path,
|
||||
os_proc_t *p) {
|
||||
memset(p, 0, sizeof(*p));
|
||||
if (!argv || !argv[0])
|
||||
return -1;
|
||||
char cmdline[16384];
|
||||
size_t pos = 0;
|
||||
cmdline[0] = 0;
|
||||
for (int i = 0; argv[i]; i++) {
|
||||
if (i && pos + 1 < sizeof(cmdline))
|
||||
cmdline[pos++] = ' ';
|
||||
win_quote_arg(cmdline, sizeof(cmdline), &pos, argv[i]);
|
||||
}
|
||||
for (int i = 0; env_add && env_add[i]; i++)
|
||||
_putenv(env_add[i]); /* inherited by the child */
|
||||
SECURITY_ATTRIBUTES sa = {0};
|
||||
sa.nLength = sizeof(sa);
|
||||
sa.bInheritHandle = TRUE;
|
||||
HANDLE in_r = NULL, in_w = NULL, out_r = NULL, out_w = NULL;
|
||||
if (!CreatePipe(&in_r, &in_w, &sa, 0))
|
||||
return -1;
|
||||
if (!CreatePipe(&out_r, &out_w, &sa, 0)) {
|
||||
CloseHandle(in_r);
|
||||
CloseHandle(in_w);
|
||||
return -1;
|
||||
}
|
||||
SetHandleInformation(in_w, HANDLE_FLAG_INHERIT, 0);
|
||||
SetHandleInformation(out_r, HANDLE_FLAG_INHERIT, 0);
|
||||
HANDLE err = CreateFileA(stderr_path ? stderr_path : "NUL", GENERIC_WRITE,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE, &sa,
|
||||
CREATE_ALWAYS, 0, NULL);
|
||||
STARTUPINFOA si = {0};
|
||||
si.cb = sizeof(si);
|
||||
si.dwFlags = STARTF_USESTDHANDLES;
|
||||
si.hStdInput = in_r;
|
||||
si.hStdOutput = out_w;
|
||||
si.hStdError = err != INVALID_HANDLE_VALUE ? err : out_w;
|
||||
PROCESS_INFORMATION pi = {0};
|
||||
BOOL ok = CreateProcessA(NULL, cmdline, NULL, NULL, TRUE, CREATE_NO_WINDOW,
|
||||
NULL, cwd && cwd[0] ? cwd : NULL, &si, &pi);
|
||||
CloseHandle(in_r);
|
||||
CloseHandle(out_w);
|
||||
if (err != INVALID_HANDLE_VALUE)
|
||||
CloseHandle(err);
|
||||
if (!ok) {
|
||||
CloseHandle(in_w);
|
||||
CloseHandle(out_r);
|
||||
return -1;
|
||||
}
|
||||
CloseHandle(pi.hThread);
|
||||
p->proc = pi.hProcess;
|
||||
p->in_w = in_w;
|
||||
p->out_r = out_r;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int os_proc_write(os_proc_t *p, const char *data, size_t n) {
|
||||
while (n > 0) {
|
||||
DWORD w = 0;
|
||||
if (!p->in_w || !WriteFile((HANDLE)p->in_w, data, (DWORD)n, &w, NULL) ||
|
||||
w == 0)
|
||||
return -1;
|
||||
data += w;
|
||||
n -= w;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
long os_proc_read(os_proc_t *p, char *buf, size_t n, int timeout_ms) {
|
||||
if (!p->out_r)
|
||||
return -1;
|
||||
double deadline = os_now_ms() + timeout_ms;
|
||||
for (;;) {
|
||||
DWORD avail = 0;
|
||||
if (!PeekNamedPipe((HANDLE)p->out_r, NULL, 0, NULL, &avail, NULL))
|
||||
return GetLastError() == ERROR_BROKEN_PIPE ? 0 : -1;
|
||||
if (avail > 0)
|
||||
break;
|
||||
if (os_now_ms() >= deadline)
|
||||
return -2;
|
||||
Sleep(10);
|
||||
}
|
||||
DWORD r = 0;
|
||||
if (!ReadFile((HANDLE)p->out_r, buf, (DWORD)n, &r, NULL))
|
||||
return GetLastError() == ERROR_BROKEN_PIPE ? 0 : -1;
|
||||
return (long)r;
|
||||
}
|
||||
|
||||
void os_proc_close_stdin(os_proc_t *p) {
|
||||
if (p->in_w) {
|
||||
CloseHandle((HANDLE)p->in_w);
|
||||
p->in_w = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
void os_proc_kill(os_proc_t *p) {
|
||||
os_proc_close_stdin(p);
|
||||
if (p->proc) {
|
||||
if (WaitForSingleObject((HANDLE)p->proc, 500) != WAIT_OBJECT_0)
|
||||
TerminateProcess((HANDLE)p->proc, 1);
|
||||
WaitForSingleObject((HANDLE)p->proc, 2000);
|
||||
CloseHandle((HANDLE)p->proc);
|
||||
p->proc = NULL;
|
||||
}
|
||||
if (p->out_r) {
|
||||
CloseHandle((HANDLE)p->out_r);
|
||||
p->out_r = NULL;
|
||||
}
|
||||
}
|
||||
#else
|
||||
#ifndef _WIN32
|
||||
int os_proc_spawn(const char *cwd, const char *const argv[],
|
||||
const char *const env_add[], const char *stderr_path,
|
||||
os_proc_t *p) {
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/* Shared between os_compat.c and os_win.c; not part of the public API. */
|
||||
#ifndef CST_OS_INTERNAL_H
|
||||
#define CST_OS_INTERNAL_H
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
/* CST_SHELL_TIMEOUT / os_set_run_timeout / 120 s */
|
||||
int os_run_timeout_effective(void);
|
||||
/* Append to a capture buffer, keeping at most max_out bytes. */
|
||||
int os_cap_append(char **buf, size_t *len, size_t *cap, size_t max_out,
|
||||
const char *data, size_t n, int *truncated);
|
||||
void os_cap_note(char **buf, size_t *len, size_t *cap, const char *note);
|
||||
|
||||
#endif
|
||||
+934
@@ -0,0 +1,934 @@
|
||||
/* Windows side of os_compat: every string crossing the OS boundary is UTF-8
|
||||
* inside cst-agent and UTF-16 at the Win32 W APIs. The embedded manifest also
|
||||
* sets the process code page to UTF-8, so CRT calls (fopen, getenv, argv)
|
||||
* see UTF-8 too. */
|
||||
#ifdef _WIN32
|
||||
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#include "os_compat.h"
|
||||
#include "cst_util.h"
|
||||
#include "os_internal.h"
|
||||
|
||||
#include <locale.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <windows.h>
|
||||
|
||||
#include <aclapi.h>
|
||||
#include <sddl.h>
|
||||
|
||||
wchar_t *os_utf8_to_wide(const char *s) {
|
||||
if (!s)
|
||||
return NULL;
|
||||
int n = MultiByteToWideChar(CP_UTF8, 0, s, -1, NULL, 0);
|
||||
if (n <= 0)
|
||||
return NULL;
|
||||
wchar_t *w = malloc((size_t)n * sizeof(wchar_t));
|
||||
if (w && MultiByteToWideChar(CP_UTF8, 0, s, -1, w, n) <= 0) {
|
||||
free(w);
|
||||
return NULL;
|
||||
}
|
||||
return w;
|
||||
}
|
||||
|
||||
char *os_wide_to_utf8(const wchar_t *w) {
|
||||
if (!w)
|
||||
return NULL;
|
||||
int n = WideCharToMultiByte(CP_UTF8, 0, w, -1, NULL, 0, NULL, NULL);
|
||||
if (n <= 0)
|
||||
return NULL;
|
||||
char *s = malloc((size_t)n);
|
||||
if (s && WideCharToMultiByte(CP_UTF8, 0, w, -1, s, n, NULL, NULL) <= 0) {
|
||||
free(s);
|
||||
return NULL;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
static UINT g_old_cp, g_old_out_cp;
|
||||
|
||||
static void console_restore(void) {
|
||||
if (g_old_cp)
|
||||
SetConsoleCP(g_old_cp);
|
||||
if (g_old_out_cp)
|
||||
SetConsoleOutputCP(g_old_out_cp);
|
||||
}
|
||||
|
||||
int os_console_init(void) {
|
||||
static int done;
|
||||
if (done)
|
||||
return GetACP() == CP_UTF8 ? 0 : -1;
|
||||
done = 1;
|
||||
g_old_cp = GetConsoleCP();
|
||||
g_old_out_cp = GetConsoleOutputCP();
|
||||
if (g_old_cp || g_old_out_cp) {
|
||||
SetConsoleCP(CP_UTF8);
|
||||
SetConsoleOutputCP(CP_UTF8);
|
||||
atexit(console_restore);
|
||||
}
|
||||
setlocale(LC_ALL, ".UTF-8");
|
||||
/* children inherit these: Python otherwise writes the ANSI code page to pipes */
|
||||
if (!getenv("PYTHONUTF8"))
|
||||
_putenv_s("PYTHONUTF8", "1");
|
||||
if (!getenv("PYTHONIOENCODING"))
|
||||
_putenv_s("PYTHONIOENCODING", "utf-8");
|
||||
return GetACP() == CP_UTF8 ? 0 : -1;
|
||||
}
|
||||
|
||||
/* The system locale's code page. GetOEMCP()/GetACP() say 65001 once the
|
||||
* manifest makes this process UTF-8, but children without one (cmd, most
|
||||
* console tools) still write the system's OEM code page, e.g. 936. */
|
||||
static UINT system_cp(LCTYPE which) {
|
||||
wchar_t b[8];
|
||||
if (!GetLocaleInfoW(LOCALE_SYSTEM_DEFAULT, which, b, 8))
|
||||
return 0;
|
||||
return (UINT)wcstoul(b, NULL, 10);
|
||||
}
|
||||
|
||||
char *os_output_to_utf8(const char *s, size_t n, size_t *out_len) {
|
||||
if (!s || cst_utf8_valid(s, n))
|
||||
return NULL;
|
||||
UINT cps[] = {GetConsoleOutputCP(), system_cp(LOCALE_IDEFAULTCODEPAGE),
|
||||
system_cp(LOCALE_IDEFAULTANSICODEPAGE), GetOEMCP(), GetACP()};
|
||||
for (size_t i = 0; i < sizeof(cps) / sizeof(cps[0]); i++) {
|
||||
UINT cp = cps[i];
|
||||
if (!cp || cp == CP_UTF8)
|
||||
continue;
|
||||
int wn = MultiByteToWideChar(cp, MB_ERR_INVALID_CHARS, s, (int)n, NULL, 0);
|
||||
if (wn <= 0)
|
||||
continue;
|
||||
wchar_t *w = malloc((size_t)wn * sizeof(wchar_t));
|
||||
if (!w)
|
||||
return NULL;
|
||||
MultiByteToWideChar(cp, 0, s, (int)n, w, wn);
|
||||
int un = WideCharToMultiByte(CP_UTF8, 0, w, wn, NULL, 0, NULL, NULL);
|
||||
char *u = un > 0 ? malloc((size_t)un + 1) : NULL;
|
||||
if (u) {
|
||||
WideCharToMultiByte(CP_UTF8, 0, w, wn, u, un, NULL, NULL);
|
||||
u[un] = 0;
|
||||
if (out_len)
|
||||
*out_len = (size_t)un;
|
||||
}
|
||||
free(w);
|
||||
return u;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* --- paths --------------------------------------------------------------- */
|
||||
|
||||
/* UTF-8 path -> wide, with the \\?\ prefix once it gets near MAX_PATH so
|
||||
* deep trees work without the LongPathsEnabled policy. */
|
||||
static wchar_t *wpath(const char *p) {
|
||||
wchar_t *w = os_utf8_to_wide(p);
|
||||
if (!w || wcslen(w) < MAX_PATH - 12)
|
||||
return w;
|
||||
DWORD n = GetFullPathNameW(w, 0, NULL, NULL);
|
||||
wchar_t *f = n ? malloc(n * sizeof(wchar_t)) : NULL;
|
||||
wchar_t *out = n ? malloc((n + 8) * sizeof(wchar_t)) : NULL;
|
||||
if (!f || !out || GetFullPathNameW(w, n, f, NULL) == 0) {
|
||||
free(f);
|
||||
free(out);
|
||||
return w;
|
||||
}
|
||||
free(w);
|
||||
if (wcsncmp(f, L"\\\\?\\", 4) == 0) {
|
||||
wcscpy(out, f);
|
||||
} else if (f[0] == L'\\' && f[1] == L'\\') { /* \\srv\share -> \\?\UNC\srv\share */
|
||||
wcscpy(out, L"\\\\?\\UNC");
|
||||
wcscat(out, f + 1);
|
||||
} else {
|
||||
wcscpy(out, L"\\\\?\\");
|
||||
wcscat(out, f);
|
||||
}
|
||||
free(f);
|
||||
return out;
|
||||
}
|
||||
|
||||
/* Wide path from the OS -> UTF-8 with '/' separators, no \\?\ prefix and a
|
||||
* lowercase drive letter (stable for prefix compares). */
|
||||
static int path_out(const wchar_t *w, char *out, size_t out_sz) {
|
||||
const char *lead = "";
|
||||
if (wcsncmp(w, L"\\\\?\\UNC\\", 8) == 0) {
|
||||
lead = "\\\\"; /* \\?\UNC\srv\share -> \\srv\share */
|
||||
w += 8;
|
||||
} else if (wcsncmp(w, L"\\\\?\\", 4) == 0) {
|
||||
w += 4;
|
||||
}
|
||||
char *u = os_wide_to_utf8(w);
|
||||
if (!u)
|
||||
return -1;
|
||||
int n = snprintf(out, out_sz, "%s%s", lead, u);
|
||||
free(u);
|
||||
if (n < 0 || (size_t)n >= out_sz)
|
||||
return -1;
|
||||
os_path_norm_slashes(out);
|
||||
if (out[0] >= 'A' && out[0] <= 'Z' && out[1] == ':')
|
||||
out[0] = (char)(out[0] - 'A' + 'a');
|
||||
return 0;
|
||||
}
|
||||
|
||||
int os_realpath(const char *path, char *out, size_t out_sz) {
|
||||
if (!path || !path[0] || !out || out_sz < 2)
|
||||
return -1;
|
||||
wchar_t *w = wpath(path);
|
||||
if (!w)
|
||||
return -1;
|
||||
HANDLE h = CreateFileW(w, 0,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
|
||||
NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
|
||||
free(w);
|
||||
if (h == INVALID_HANDLE_VALUE)
|
||||
return -1;
|
||||
wchar_t stackbuf[MAX_PATH + 16], *buf = stackbuf;
|
||||
DWORD n = GetFinalPathNameByHandleW(h, buf, MAX_PATH + 16,
|
||||
FILE_NAME_NORMALIZED | VOLUME_NAME_DOS);
|
||||
if (n >= MAX_PATH + 16) {
|
||||
buf = malloc((n + 1) * sizeof(wchar_t));
|
||||
n = buf ? GetFinalPathNameByHandleW(h, buf, n + 1,
|
||||
FILE_NAME_NORMALIZED | VOLUME_NAME_DOS)
|
||||
: 0;
|
||||
}
|
||||
CloseHandle(h);
|
||||
int rc = n ? path_out(buf, out, out_sz) : -1;
|
||||
if (buf != stackbuf)
|
||||
free(buf);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* NTFS names are case-insensitive: compare the prefix the way the OS does. */
|
||||
int os_path_under(const char *root, const char *path) {
|
||||
if (!root || !path)
|
||||
return 0;
|
||||
wchar_t *wr = os_utf8_to_wide(root), *wp = os_utf8_to_wide(path);
|
||||
int ok = 0;
|
||||
if (wr && wp) {
|
||||
size_t rl = wcslen(wr);
|
||||
if (wcslen(wp) >= rl &&
|
||||
(rl == 0 || CompareStringOrdinal(wr, (int)rl, wp, (int)rl, TRUE) ==
|
||||
CSTR_EQUAL)) {
|
||||
wchar_t c = wp[rl];
|
||||
ok = c == 0 || c == L'/' || c == L'\\' ||
|
||||
(rl && (wr[rl - 1] == L'/' || wr[rl - 1] == L'\\'));
|
||||
}
|
||||
}
|
||||
free(wr);
|
||||
free(wp);
|
||||
return ok;
|
||||
}
|
||||
|
||||
static int reparse_is_link(DWORD attrs, DWORD tag) {
|
||||
return (attrs & FILE_ATTRIBUTE_REPARSE_POINT) &&
|
||||
(tag == IO_REPARSE_TAG_SYMLINK || tag == IO_REPARSE_TAG_MOUNT_POINT);
|
||||
}
|
||||
|
||||
/* --- directories ------------------------------------------------------- */
|
||||
|
||||
struct os_dir {
|
||||
HANDLE h;
|
||||
int pending; /* fd holds an entry not yet returned */
|
||||
WIN32_FIND_DATAW fd;
|
||||
char name[MAX_PATH * 4];
|
||||
os_dirent_t ent;
|
||||
};
|
||||
|
||||
os_dir_t *os_dir_open(const char *path) {
|
||||
if (!path || !path[0])
|
||||
return NULL;
|
||||
size_t L = strlen(path);
|
||||
char *pat = malloc(L + 3);
|
||||
if (!pat)
|
||||
return NULL;
|
||||
memcpy(pat, path, L);
|
||||
if (path[L - 1] != '/' && path[L - 1] != '\\')
|
||||
pat[L++] = '\\';
|
||||
pat[L++] = '*';
|
||||
pat[L] = 0;
|
||||
wchar_t *w = wpath(pat);
|
||||
free(pat);
|
||||
os_dir_t *d = w ? calloc(1, sizeof(*d)) : NULL;
|
||||
if (!d) {
|
||||
free(w);
|
||||
return NULL;
|
||||
}
|
||||
d->h = FindFirstFileExW(w, FindExInfoBasic, &d->fd, FindExSearchNameMatch,
|
||||
NULL, FIND_FIRST_EX_LARGE_FETCH);
|
||||
free(w);
|
||||
if (d->h == INVALID_HANDLE_VALUE) {
|
||||
DWORD e = GetLastError();
|
||||
if (e != ERROR_FILE_NOT_FOUND) { /* an empty drive root has no entries */
|
||||
free(d);
|
||||
return NULL;
|
||||
}
|
||||
} else {
|
||||
d->pending = 1;
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
const os_dirent_t *os_dir_next(os_dir_t *d) {
|
||||
if (!d || d->h == INVALID_HANDLE_VALUE)
|
||||
return NULL;
|
||||
for (;;) {
|
||||
if (!d->pending && !FindNextFileW(d->h, &d->fd))
|
||||
return NULL;
|
||||
d->pending = 0;
|
||||
const wchar_t *n = d->fd.cFileName;
|
||||
if (wcscmp(n, L".") == 0 || wcscmp(n, L"..") == 0)
|
||||
continue;
|
||||
if (WideCharToMultiByte(CP_UTF8, 0, n, -1, d->name, (int)sizeof(d->name),
|
||||
NULL, NULL) <= 0)
|
||||
continue;
|
||||
DWORD a = d->fd.dwFileAttributes;
|
||||
d->ent.name = d->name;
|
||||
d->ent.is_link = reparse_is_link(a, d->fd.dwReserved0);
|
||||
d->ent.is_dir = !d->ent.is_link && (a & FILE_ATTRIBUTE_DIRECTORY);
|
||||
return &d->ent;
|
||||
}
|
||||
}
|
||||
|
||||
void os_dir_close(os_dir_t *d) {
|
||||
if (!d)
|
||||
return;
|
||||
if (d->h != INVALID_HANDLE_VALUE)
|
||||
FindClose(d->h);
|
||||
free(d);
|
||||
}
|
||||
|
||||
static double filetime_secs(FILETIME ft) {
|
||||
ULONGLONG t = ((ULONGLONG)ft.dwHighDateTime << 32) | ft.dwLowDateTime;
|
||||
return (double)t / 1e7 - 11644473600.0;
|
||||
}
|
||||
|
||||
int os_lstat(const char *path, os_stat_t *st) {
|
||||
memset(st, 0, sizeof(*st));
|
||||
wchar_t *w = path ? wpath(path) : NULL;
|
||||
if (!w)
|
||||
return -1;
|
||||
WIN32_FILE_ATTRIBUTE_DATA fa;
|
||||
if (!GetFileAttributesExW(w, GetFileExInfoStandard, &fa)) {
|
||||
free(w);
|
||||
return -1;
|
||||
}
|
||||
DWORD a = fa.dwFileAttributes;
|
||||
if (a & FILE_ATTRIBUTE_REPARSE_POINT) {
|
||||
WIN32_FIND_DATAW fd;
|
||||
HANDLE h = FindFirstFileW(w, &fd);
|
||||
if (h != INVALID_HANDLE_VALUE) {
|
||||
st->is_link = reparse_is_link(a, fd.dwReserved0);
|
||||
FindClose(h);
|
||||
}
|
||||
}
|
||||
free(w);
|
||||
st->is_dir = !st->is_link && (a & FILE_ATTRIBUTE_DIRECTORY);
|
||||
st->is_reg = !st->is_link && !(a & FILE_ATTRIBUTE_DIRECTORY);
|
||||
st->size = (long long)(((ULONGLONG)fa.nFileSizeHigh << 32) | fa.nFileSizeLow);
|
||||
st->mtime = filetime_secs(fa.ftLastWriteTime);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int os_rename_replace(const char *from, const char *to) {
|
||||
wchar_t *wf = wpath(from), *wt = wpath(to);
|
||||
int ok = wf && wt &&
|
||||
MoveFileExW(wf, wt, MOVEFILE_REPLACE_EXISTING | MOVEFILE_COPY_ALLOWED);
|
||||
free(wf);
|
||||
free(wt);
|
||||
return ok ? 0 : -1;
|
||||
}
|
||||
|
||||
/* --- processes ----------------------------------------------------------- */
|
||||
|
||||
/* One argument the way CommandLineToArgvW and the CRT split it again. */
|
||||
static void cmdline_arg(cst_buf_t *b, const char *a) {
|
||||
if (a[0] && !strpbrk(a, " \t\n\v\"")) {
|
||||
cst_buf_puts(b, a);
|
||||
return;
|
||||
}
|
||||
cst_buf_puts(b, "\"");
|
||||
for (const char *p = a;; p++) {
|
||||
size_t bs = 0;
|
||||
while (*p == '\\') {
|
||||
bs++;
|
||||
p++;
|
||||
}
|
||||
if (!*p) {
|
||||
for (size_t i = 0; i < bs * 2; i++)
|
||||
cst_buf_puts(b, "\\");
|
||||
break;
|
||||
}
|
||||
for (size_t i = 0; i < (*p == '"' ? bs * 2 + 1 : bs); i++)
|
||||
cst_buf_puts(b, "\\");
|
||||
cst_buf_append(b, p, 1);
|
||||
}
|
||||
cst_buf_puts(b, "\"");
|
||||
}
|
||||
|
||||
static wchar_t *build_cmdline(const char *const argv[]) {
|
||||
cst_buf_t b = {0};
|
||||
for (int i = 0; argv[i]; i++) {
|
||||
if (i)
|
||||
cst_buf_puts(&b, " ");
|
||||
cmdline_arg(&b, argv[i]);
|
||||
}
|
||||
wchar_t *w = b.buf ? os_utf8_to_wide(b.buf) : NULL;
|
||||
cst_buf_free(&b);
|
||||
return w;
|
||||
}
|
||||
|
||||
static size_t env_name_len(const wchar_t *e) {
|
||||
const wchar_t *eq = wcschr(e + 1, L'='); /* "=C:=C:\x" hides a leading '=' */
|
||||
return eq ? (size_t)(eq - e) : wcslen(e);
|
||||
}
|
||||
|
||||
static int env_cmp(const void *a, const void *b) {
|
||||
const wchar_t *x = *(const wchar_t *const *)a, *y = *(const wchar_t *const *)b;
|
||||
return CompareStringOrdinal(x, (int)env_name_len(x), y, (int)env_name_len(y),
|
||||
TRUE) -
|
||||
CSTR_EQUAL;
|
||||
}
|
||||
|
||||
/* The inherited environment with env_add ("KEY=VALUE") layered on top, as a
|
||||
* sorted CREATE_UNICODE_ENVIRONMENT block; NULL = inherit unchanged. */
|
||||
static wchar_t *env_block(const char *const env_add[]) {
|
||||
if (!env_add || !env_add[0])
|
||||
return NULL;
|
||||
size_t nadd = 0, ncur = 0;
|
||||
while (env_add[nadd])
|
||||
nadd++;
|
||||
wchar_t *cur = GetEnvironmentStringsW();
|
||||
for (const wchar_t *e = cur; e && *e; e += wcslen(e) + 1)
|
||||
ncur++;
|
||||
wchar_t **add = calloc(nadd, sizeof(*add));
|
||||
wchar_t **v = calloc(ncur + nadd, sizeof(*v));
|
||||
wchar_t *blk = NULL;
|
||||
size_t n = 0, total = 1;
|
||||
if (!add || !v)
|
||||
goto out;
|
||||
for (size_t i = 0; i < nadd; i++)
|
||||
if (!(add[i] = os_utf8_to_wide(env_add[i])))
|
||||
goto out;
|
||||
for (wchar_t *e = cur; e && *e; e += wcslen(e) + 1) {
|
||||
int shadowed = 0;
|
||||
for (size_t i = 0; i < nadd && !shadowed; i++)
|
||||
shadowed = env_cmp(&e, &add[i]) == 0;
|
||||
if (!shadowed)
|
||||
v[n++] = e;
|
||||
}
|
||||
for (size_t i = 0; i < nadd; i++) {
|
||||
int dup = 0; /* a later entry for the same name wins */
|
||||
for (size_t k = i + 1; k < nadd && !dup; k++)
|
||||
dup = env_cmp(&add[i], &add[k]) == 0;
|
||||
if (!dup)
|
||||
v[n++] = add[i];
|
||||
}
|
||||
qsort(v, n, sizeof(*v), env_cmp);
|
||||
for (size_t i = 0; i < n; i++)
|
||||
total += wcslen(v[i]) + 1;
|
||||
blk = malloc(total * sizeof(wchar_t));
|
||||
if (blk) {
|
||||
wchar_t *o = blk;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
size_t l = wcslen(v[i]) + 1;
|
||||
memcpy(o, v[i], l * sizeof(wchar_t));
|
||||
o += l;
|
||||
}
|
||||
*o = 0;
|
||||
}
|
||||
out:
|
||||
for (size_t i = 0; add && i < nadd; i++)
|
||||
free(add[i]);
|
||||
free(add);
|
||||
free(v);
|
||||
if (cur)
|
||||
FreeEnvironmentStringsW(cur);
|
||||
return blk;
|
||||
}
|
||||
|
||||
static HANDLE new_job(void) {
|
||||
HANDLE job = CreateJobObjectW(NULL, NULL);
|
||||
if (!job)
|
||||
return NULL;
|
||||
JOBOBJECT_EXTENDED_LIMIT_INFORMATION li;
|
||||
memset(&li, 0, sizeof(li));
|
||||
li.BasicLimitInformation.LimitFlags =
|
||||
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE |
|
||||
JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION;
|
||||
SetInformationJobObject(job, JobObjectExtendedLimitInformation, &li,
|
||||
sizeof(li));
|
||||
return job;
|
||||
}
|
||||
|
||||
static DWORD job_active(HANDLE job) {
|
||||
JOBOBJECT_BASIC_ACCOUNTING_INFORMATION ai;
|
||||
if (!job || !QueryInformationJobObject(job,
|
||||
JobObjectBasicAccountingInformation,
|
||||
&ai, sizeof(ai), NULL))
|
||||
return 0;
|
||||
return ai.ActiveProcesses;
|
||||
}
|
||||
|
||||
/* Children still running after the shell exited. A child started without
|
||||
* our console (the sandbox) has its own conhost.exe, which leaves the job a
|
||||
* moment after its client, so give the job a short while to drain. */
|
||||
static int job_lingers(HANDLE job) {
|
||||
for (int i = 0; i < 50 && job_active(job) > 0; i++)
|
||||
Sleep(10);
|
||||
return job_active(job) > 0;
|
||||
}
|
||||
|
||||
/* Start argv suspended, put it in a fresh kill-on-close job, then resume.
|
||||
* Only in/out/err are inherited (PROC_THREAD_ATTRIBUTE_HANDLE_LIST), so a
|
||||
* concurrent spawn on another thread never leaks into this child. */
|
||||
static int spawn(const char *cwd, const char *const argv[],
|
||||
const char *const env_add[], HANDLE token, HANDLE in,
|
||||
HANDLE out, HANDLE err, HANDLE *proc, HANDLE *job,
|
||||
DWORD *error) {
|
||||
*proc = *job = NULL;
|
||||
*error = ERROR_OUTOFMEMORY;
|
||||
wchar_t *cmd = build_cmdline(argv);
|
||||
wchar_t *wcwd = cwd && cwd[0] ? os_utf8_to_wide(cwd) : NULL;
|
||||
wchar_t *env = env_block(env_add);
|
||||
HANDLE inherit[3];
|
||||
int ninh = 0;
|
||||
HANDLE std3[3] = {in, out, err};
|
||||
for (int i = 0; i < 3; i++) {
|
||||
int seen = !std3[i] || std3[i] == INVALID_HANDLE_VALUE;
|
||||
for (int k = 0; k < ninh && !seen; k++)
|
||||
seen = inherit[k] == std3[i];
|
||||
if (!seen)
|
||||
inherit[ninh++] = std3[i];
|
||||
}
|
||||
SIZE_T asz = 0;
|
||||
InitializeProcThreadAttributeList(NULL, 1, 0, &asz);
|
||||
LPPROC_THREAD_ATTRIBUTE_LIST attrs = malloc(asz);
|
||||
int ok = 0;
|
||||
if (cmd && (!cwd || !cwd[0] || wcwd) && (!env_add || !env_add[0] || env) &&
|
||||
attrs && InitializeProcThreadAttributeList(attrs, 1, 0, &asz)) {
|
||||
if (UpdateProcThreadAttribute(attrs, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
|
||||
inherit, (SIZE_T)ninh * sizeof(HANDLE), NULL,
|
||||
NULL)) {
|
||||
STARTUPINFOEXW si;
|
||||
memset(&si, 0, sizeof(si));
|
||||
si.StartupInfo.cb = sizeof(si);
|
||||
si.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
|
||||
si.StartupInfo.hStdInput = in;
|
||||
si.StartupInfo.hStdOutput = out;
|
||||
si.StartupInfo.hStdError = err;
|
||||
si.lpAttributeList = attrs;
|
||||
/* Share our console when there is one (children then see its UTF-8
|
||||
* code page); the sandbox token cannot attach to it. */
|
||||
DWORD flags = CREATE_SUSPENDED | CREATE_UNICODE_ENVIRONMENT |
|
||||
CREATE_NEW_PROCESS_GROUP | EXTENDED_STARTUPINFO_PRESENT;
|
||||
if (token || !GetConsoleWindow())
|
||||
flags |= CREATE_NO_WINDOW;
|
||||
PROCESS_INFORMATION pi;
|
||||
memset(&pi, 0, sizeof(pi));
|
||||
ok = token ? CreateProcessAsUserW(token, NULL, cmd, NULL, NULL, TRUE,
|
||||
flags, env, wcwd, &si.StartupInfo, &pi)
|
||||
: CreateProcessW(NULL, cmd, NULL, NULL, TRUE, flags, env, wcwd,
|
||||
&si.StartupInfo, &pi);
|
||||
if (ok) {
|
||||
*job = new_job();
|
||||
if (*job && !AssignProcessToJobObject(*job, pi.hProcess)) {
|
||||
CloseHandle(*job);
|
||||
*job = NULL;
|
||||
}
|
||||
ResumeThread(pi.hThread);
|
||||
CloseHandle(pi.hThread);
|
||||
*proc = pi.hProcess;
|
||||
} else {
|
||||
*error = GetLastError();
|
||||
}
|
||||
}
|
||||
DeleteProcThreadAttributeList(attrs);
|
||||
}
|
||||
free(attrs);
|
||||
free(cmd);
|
||||
free(wcwd);
|
||||
free(env);
|
||||
return ok ? 0 : -1;
|
||||
}
|
||||
|
||||
int os_run_capture_token(const char *cwd, const char *const argv[],
|
||||
size_t max_out, os_run_result_t *r, void *token,
|
||||
const char *const env_add[]) {
|
||||
if (!argv || !argv[0] || !r)
|
||||
return -1;
|
||||
if (max_out == 0)
|
||||
max_out = 512 * 1024;
|
||||
memset(r, 0, sizeof(*r));
|
||||
SECURITY_ATTRIBUTES sa = {sizeof(sa), NULL, TRUE};
|
||||
HANDLE rd = NULL, wr = NULL;
|
||||
if (!CreatePipe(&rd, &wr, &sa, 64 * 1024))
|
||||
return -1;
|
||||
SetHandleInformation(rd, HANDLE_FLAG_INHERIT, 0);
|
||||
HANDLE nul = CreateFileW(L"NUL", GENERIC_READ,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE, &sa,
|
||||
OPEN_EXISTING, 0, NULL);
|
||||
HANDLE proc, job;
|
||||
DWORD err;
|
||||
int rc = spawn(cwd, argv, env_add, (HANDLE)token, nul, wr, wr, &proc, &job,
|
||||
&err);
|
||||
CloseHandle(wr);
|
||||
if (nul != INVALID_HANDLE_VALUE)
|
||||
CloseHandle(nul);
|
||||
if (rc != 0) {
|
||||
CloseHandle(rd);
|
||||
if (err != ERROR_FILE_NOT_FOUND && err != ERROR_PATH_NOT_FOUND)
|
||||
return -1;
|
||||
cst_buf_t b = {0};
|
||||
cst_buf_printf(&b, "%s: command not found\n", argv[0]);
|
||||
r->out_len = b.len;
|
||||
r->out = cst_buf_steal(&b);
|
||||
r->exit_code = 127;
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t cap = 4096, len = 0;
|
||||
char *buf = malloc(cap);
|
||||
if (!buf) {
|
||||
TerminateJobObject(job, 1);
|
||||
TerminateProcess(proc, 1);
|
||||
CloseHandle(rd);
|
||||
CloseHandle(proc);
|
||||
if (job)
|
||||
CloseHandle(job);
|
||||
return -1;
|
||||
}
|
||||
buf[0] = 0;
|
||||
int truncated = 0, killed = 0, reaped = 0, bg_killed = 0;
|
||||
double reaped_at = 0, killed_at = 0;
|
||||
double deadline = os_now_ms() + os_run_timeout_effective() * 1000.0;
|
||||
int idle = 0; /* empty polls in a row: wait longer once output stalls */
|
||||
for (;;) {
|
||||
DWORD avail = 0;
|
||||
if (!PeekNamedPipe(rd, NULL, 0, NULL, &avail, NULL))
|
||||
break; /* every writer closed */
|
||||
if (avail) {
|
||||
char tmp[65536];
|
||||
DWORD n = 0;
|
||||
if (!ReadFile(rd, tmp, avail < sizeof(tmp) ? avail : sizeof(tmp), &n,
|
||||
NULL) ||
|
||||
n == 0)
|
||||
break;
|
||||
os_cap_append(&buf, &len, &cap, max_out, tmp, n, &truncated);
|
||||
idle = 0;
|
||||
} else if (reaped) {
|
||||
Sleep(idle++ < 50 ? 1 : 20);
|
||||
}
|
||||
DWORD wait = avail ? 0 : idle++ < 50 ? 1 : 20;
|
||||
if (!reaped && WaitForSingleObject(proc, wait) == WAIT_OBJECT_0) {
|
||||
reaped = 1;
|
||||
reaped_at = os_now_ms();
|
||||
}
|
||||
double now = os_now_ms();
|
||||
/* The shell exited but a background child still holds the pipe
|
||||
* (`server &`): give it a moment, then end the tree. */
|
||||
if (reaped && !killed && !bg_killed && now - reaped_at > 1000) {
|
||||
TerminateJobObject(job, 1);
|
||||
bg_killed = 1;
|
||||
killed_at = now;
|
||||
}
|
||||
if (!reaped && !killed && (os_cancel_requested() || now > deadline)) {
|
||||
killed = os_cancel_requested() ? 2 : 1;
|
||||
killed_at = now;
|
||||
TerminateJobObject(job, killed == 1 ? 124 : 130);
|
||||
if (!job)
|
||||
TerminateProcess(proc, killed == 1 ? 124 : 130);
|
||||
}
|
||||
/* a writer outside the job can keep the pipe open */
|
||||
if ((killed || bg_killed) && now - killed_at > 3000)
|
||||
break;
|
||||
}
|
||||
CloseHandle(rd);
|
||||
WaitForSingleObject(proc, killed ? 5000 : INFINITE);
|
||||
DWORD code = 1;
|
||||
GetExitCodeProcess(proc, &code);
|
||||
if (!bg_killed && !killed && job_lingers(job))
|
||||
bg_killed = 1; /* closing the job below ends them */
|
||||
CloseHandle(proc);
|
||||
if (job)
|
||||
CloseHandle(job);
|
||||
|
||||
size_t ulen = 0;
|
||||
char *u = os_output_to_utf8(buf, len, &ulen);
|
||||
if (u) {
|
||||
free(buf);
|
||||
buf = u;
|
||||
len = ulen;
|
||||
cap = ulen + 1;
|
||||
}
|
||||
if (truncated)
|
||||
os_cap_note(&buf, &len, &cap, "\n...(output truncated)\n");
|
||||
if (bg_killed && !killed)
|
||||
os_cap_note(&buf, &len, &cap,
|
||||
"\n[cst: background processes left running were stopped]\n");
|
||||
if (killed == 1) {
|
||||
char note[96];
|
||||
snprintf(note, sizeof(note), "\n[cst: killed after %ds timeout]\n",
|
||||
os_run_timeout_effective());
|
||||
os_cap_note(&buf, &len, &cap, note);
|
||||
code = 124;
|
||||
} else if (killed == 2) {
|
||||
os_cap_note(&buf, &len, &cap, "\n[cst: cancelled]\n");
|
||||
code = 130;
|
||||
}
|
||||
r->out = buf;
|
||||
r->out_len = len;
|
||||
r->exit_code = (int)code;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int os_run_capture(const char *cwd, const char *const argv[], size_t max_out,
|
||||
os_run_result_t *r) {
|
||||
return os_run_capture_token(cwd, argv, max_out, r, NULL, NULL);
|
||||
}
|
||||
|
||||
int os_run_capture_ex(const char *cwd, const char *const argv[],
|
||||
size_t max_out, os_run_result_t *r,
|
||||
os_child_hook_fn child_hook, void *ud) {
|
||||
(void)child_hook;
|
||||
(void)ud;
|
||||
return os_run_capture_token(cwd, argv, max_out, r, NULL, NULL);
|
||||
}
|
||||
|
||||
int os_proc_spawn(const char *cwd, const char *const argv[],
|
||||
const char *const env_add[], const char *stderr_path,
|
||||
os_proc_t *p) {
|
||||
memset(p, 0, sizeof(*p));
|
||||
if (!argv || !argv[0])
|
||||
return -1;
|
||||
SECURITY_ATTRIBUTES sa = {sizeof(sa), NULL, TRUE};
|
||||
HANDLE in_r = NULL, in_w = NULL, out_r = NULL, out_w = NULL;
|
||||
if (!CreatePipe(&in_r, &in_w, &sa, 64 * 1024))
|
||||
return -1;
|
||||
if (!CreatePipe(&out_r, &out_w, &sa, 64 * 1024)) {
|
||||
CloseHandle(in_r);
|
||||
CloseHandle(in_w);
|
||||
return -1;
|
||||
}
|
||||
SetHandleInformation(in_w, HANDLE_FLAG_INHERIT, 0);
|
||||
SetHandleInformation(out_r, HANDLE_FLAG_INHERIT, 0);
|
||||
wchar_t *werr = stderr_path ? wpath(stderr_path) : NULL;
|
||||
HANDLE err = CreateFileW(werr ? werr : L"NUL", GENERIC_WRITE,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE, &sa,
|
||||
werr ? CREATE_ALWAYS : OPEN_EXISTING, 0, NULL);
|
||||
free(werr);
|
||||
HANDLE proc, job;
|
||||
DWORD e;
|
||||
int rc = spawn(cwd, argv, env_add, NULL, in_r, out_w,
|
||||
err != INVALID_HANDLE_VALUE ? err : out_w, &proc, &job, &e);
|
||||
CloseHandle(in_r);
|
||||
CloseHandle(out_w);
|
||||
if (err != INVALID_HANDLE_VALUE)
|
||||
CloseHandle(err);
|
||||
if (rc != 0) {
|
||||
CloseHandle(in_w);
|
||||
CloseHandle(out_r);
|
||||
return -1;
|
||||
}
|
||||
p->proc = proc;
|
||||
p->job = job;
|
||||
p->in_w = in_w;
|
||||
p->out_r = out_r;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int os_proc_write(os_proc_t *p, const char *data, size_t n) {
|
||||
while (n > 0) {
|
||||
DWORD w = 0;
|
||||
if (!p->in_w || !WriteFile((HANDLE)p->in_w, data, (DWORD)n, &w, NULL) ||
|
||||
w == 0)
|
||||
return -1;
|
||||
data += w;
|
||||
n -= w;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
long os_proc_read(os_proc_t *p, char *buf, size_t n, int timeout_ms) {
|
||||
if (!p->out_r)
|
||||
return -1;
|
||||
double deadline = os_now_ms() + timeout_ms;
|
||||
DWORD avail = 0;
|
||||
for (;;) {
|
||||
if (!PeekNamedPipe((HANDLE)p->out_r, NULL, 0, NULL, &avail, NULL))
|
||||
return GetLastError() == ERROR_BROKEN_PIPE ? 0 : -1;
|
||||
if (avail > 0)
|
||||
break;
|
||||
if (os_now_ms() >= deadline)
|
||||
return -2;
|
||||
Sleep(5);
|
||||
}
|
||||
DWORD r = 0;
|
||||
if (!ReadFile((HANDLE)p->out_r, buf, avail < n ? avail : (DWORD)n, &r, NULL))
|
||||
return GetLastError() == ERROR_BROKEN_PIPE ? 0 : -1;
|
||||
return (long)r;
|
||||
}
|
||||
|
||||
void os_proc_close_stdin(os_proc_t *p) {
|
||||
if (p->in_w) {
|
||||
CloseHandle((HANDLE)p->in_w);
|
||||
p->in_w = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
void os_proc_kill(os_proc_t *p) {
|
||||
os_proc_close_stdin(p);
|
||||
if (p->proc) {
|
||||
/* polite first: most servers exit when stdin closes */
|
||||
if (WaitForSingleObject((HANDLE)p->proc, 500) != WAIT_OBJECT_0) {
|
||||
if (p->job)
|
||||
TerminateJobObject((HANDLE)p->job, 1);
|
||||
else
|
||||
TerminateProcess((HANDLE)p->proc, 1);
|
||||
}
|
||||
WaitForSingleObject((HANDLE)p->proc, 2000);
|
||||
CloseHandle((HANDLE)p->proc);
|
||||
p->proc = NULL;
|
||||
}
|
||||
if (p->job) {
|
||||
CloseHandle((HANDLE)p->job); /* ends anything the server left behind */
|
||||
p->job = NULL;
|
||||
}
|
||||
if (p->out_r) {
|
||||
CloseHandle((HANDLE)p->out_r);
|
||||
p->out_r = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
void os_windows_version(char *out, size_t n) {
|
||||
typedef LONG(WINAPI * rtl_ver_fn)(OSVERSIONINFOW *);
|
||||
OSVERSIONINFOW vi;
|
||||
memset(&vi, 0, sizeof(vi));
|
||||
vi.dwOSVersionInfoSize = sizeof(vi);
|
||||
HMODULE nt = GetModuleHandleW(L"ntdll.dll");
|
||||
rtl_ver_fn fn = nt ? (rtl_ver_fn)(void (*)(void))GetProcAddress(nt, "RtlGetVersion")
|
||||
: NULL;
|
||||
SYSTEM_INFO si;
|
||||
GetNativeSystemInfo(&si);
|
||||
const char *arch = si.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_ARM64
|
||||
? "arm64"
|
||||
: "x86_64";
|
||||
if (fn && fn(&vi) == 0)
|
||||
snprintf(out, n, "Windows %lu.%lu.%lu %s", vi.dwMajorVersion,
|
||||
vi.dwMinorVersion, vi.dwBuildNumber, arch);
|
||||
else
|
||||
snprintf(out, n, "Windows %s", arch);
|
||||
}
|
||||
|
||||
/* Mandatory label from an SDDL SACL such as "S:(ML;;NWNR;;;ME)". */
|
||||
int os_win_set_label(const char *path, const char *sddl) {
|
||||
wchar_t *wp = wpath(path), *ws = os_utf8_to_wide(sddl);
|
||||
PSECURITY_DESCRIPTOR sd = NULL;
|
||||
int rc = -1;
|
||||
if (wp && ws &&
|
||||
ConvertStringSecurityDescriptorToSecurityDescriptorW(ws, SDDL_REVISION_1,
|
||||
&sd, NULL)) {
|
||||
PACL sacl = NULL;
|
||||
BOOL present = FALSE, defaulted = FALSE;
|
||||
if (GetSecurityDescriptorSacl(sd, &present, &sacl, &defaulted) && present &&
|
||||
SetNamedSecurityInfoW(wp, SE_FILE_OBJECT, LABEL_SECURITY_INFORMATION,
|
||||
NULL, NULL, NULL, sacl) == ERROR_SUCCESS)
|
||||
rc = 0;
|
||||
LocalFree(sd);
|
||||
}
|
||||
free(wp);
|
||||
free(ws);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int os_win_get_label(const char *path, char *out, size_t n) {
|
||||
wchar_t *wp = wpath(path);
|
||||
PSECURITY_DESCRIPTOR sd = NULL;
|
||||
wchar_t *ws = NULL;
|
||||
int rc = -1;
|
||||
if (wp && n > 2 &&
|
||||
GetNamedSecurityInfoW(wp, SE_FILE_OBJECT, LABEL_SECURITY_INFORMATION,
|
||||
NULL, NULL, NULL, NULL, &sd) == ERROR_SUCCESS &&
|
||||
ConvertSecurityDescriptorToStringSecurityDescriptorW(
|
||||
sd, SDDL_REVISION_1, LABEL_SECURITY_INFORMATION, &ws, NULL)) {
|
||||
char *s = os_wide_to_utf8(ws);
|
||||
if (s) {
|
||||
/* keep "S:" and the explicit ACEs; inherited ones carry the ID flag */
|
||||
size_t o = 0;
|
||||
out[o++] = 'S';
|
||||
out[o++] = ':';
|
||||
rc = 0;
|
||||
for (const char *p = strchr(s, '('); p; p = strchr(p + 1, '(')) {
|
||||
const char *e = strchr(p, ')');
|
||||
if (!e)
|
||||
break;
|
||||
/* (type;flags;rights;...): flags are two-letter codes */
|
||||
const char *f = strchr(p, ';');
|
||||
int inherited = 0;
|
||||
for (f = f && f < e ? f + 1 : e; f + 1 < e && *f != ';'; f += 2)
|
||||
inherited |= f[0] == 'I' && f[1] == 'D';
|
||||
size_t len = (size_t)(e - p) + 1;
|
||||
if (inherited)
|
||||
continue;
|
||||
if (o + len >= n) {
|
||||
rc = -1;
|
||||
break;
|
||||
}
|
||||
memcpy(out + o, p, len);
|
||||
o += len;
|
||||
}
|
||||
out[o] = 0;
|
||||
free(s);
|
||||
}
|
||||
}
|
||||
if (ws)
|
||||
LocalFree(ws);
|
||||
if (sd)
|
||||
LocalFree(sd);
|
||||
free(wp);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int os_chmod_private(const char *path) {
|
||||
if (!path)
|
||||
return -1;
|
||||
return os_win_set_label(path, "S:(ML;;NWNRNX;;;ME)");
|
||||
}
|
||||
|
||||
/* --- clipboard ----------------------------------------------------------- */
|
||||
|
||||
int os_clipboard_set(const char *utf8, size_t n) {
|
||||
cst_buf_t b = {0}; /* CRLF is what Windows text consumers expect */
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
if (utf8[i] == '\n' && (i == 0 || utf8[i - 1] != '\r'))
|
||||
cst_buf_puts(&b, "\r");
|
||||
cst_buf_append(&b, utf8 + i, 1);
|
||||
}
|
||||
if (!b.buf)
|
||||
cst_buf_puts(&b, "");
|
||||
int wn = MultiByteToWideChar(CP_UTF8, 0, b.buf, (int)b.len, NULL, 0);
|
||||
HGLOBAL h = GlobalAlloc(GMEM_MOVEABLE, ((size_t)wn + 1) * sizeof(wchar_t));
|
||||
wchar_t *w = h ? GlobalLock(h) : NULL;
|
||||
if (!w) {
|
||||
if (h)
|
||||
GlobalFree(h);
|
||||
cst_buf_free(&b);
|
||||
return -1;
|
||||
}
|
||||
MultiByteToWideChar(CP_UTF8, 0, b.buf, (int)b.len, w, wn);
|
||||
w[wn] = 0;
|
||||
GlobalUnlock(h);
|
||||
cst_buf_free(&b);
|
||||
if (!OpenClipboard(NULL)) {
|
||||
GlobalFree(h);
|
||||
return -1;
|
||||
}
|
||||
EmptyClipboard();
|
||||
int ok = SetClipboardData(CF_UNICODETEXT, h) != NULL;
|
||||
if (!ok)
|
||||
GlobalFree(h);
|
||||
CloseClipboard();
|
||||
return ok ? 0 : -1;
|
||||
}
|
||||
|
||||
#endif /* _WIN32 */
|
||||
+69
-1
@@ -7,6 +7,7 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
|
||||
#include "cJSON.h"
|
||||
#include "cst_perms.h"
|
||||
@@ -33,7 +34,7 @@ static void load(cst_perms_t *p) {
|
||||
while ((n = fread(tmp, 1, sizeof(tmp), fp)) > 0 && b.len < 256 * 1024)
|
||||
cst_buf_append(&b, tmp, n);
|
||||
fclose(fp);
|
||||
cJSON *root = b.buf ? cJSON_Parse(b.buf) : NULL;
|
||||
cJSON *root = b.buf ? cJSON_Parse(b.buf + cst_bom_len(b.buf, b.len)) : NULL;
|
||||
cst_buf_free(&b);
|
||||
const cJSON *it = NULL;
|
||||
cJSON_ArrayForEach(it, cJSON_GetObjectItemCaseSensitive(root, "tools")) {
|
||||
@@ -222,6 +223,73 @@ void cst_perms_suggest_prefix(const char *cmd, char *out, size_t out_sz) {
|
||||
out[0] = 0;
|
||||
}
|
||||
|
||||
/* --- network commands ------------------------------------------------------ */
|
||||
|
||||
static int net_word_eq(const char *w, size_t n, const char *name) {
|
||||
size_t k = strlen(name);
|
||||
if (n == k + 4 && strncasecmp(w + k, ".exe", 4) == 0)
|
||||
n = k;
|
||||
return n == k && strncasecmp(w, name, k) == 0;
|
||||
}
|
||||
|
||||
int cst_shell_uses_network(const char *cmd) {
|
||||
static const char *const TOOLS[] = {
|
||||
"curl", "wget", "ssh", "scp", "sftp", "rsync",
|
||||
"ftp", "tftp", "telnet", "nc", "ncat", "netcat",
|
||||
"socat", "npx", "invoke-webrequest", "iwr", "invoke-restmethod",
|
||||
"irm", "start-bitstransfer", "bitsadmin", "certutil"};
|
||||
/* tool + sub-command that downloads or uploads */
|
||||
static const char *const PAIRS[][2] = {
|
||||
{"git", "clone"}, {"git", "fetch"}, {"git", "pull"},
|
||||
{"git", "push"}, {"git", "ls-remote"}, {"git", "submodule"},
|
||||
{"npm", "install"}, {"npm", "i"}, {"npm", "ci"},
|
||||
{"npm", "update"}, {"npm", "publish"}, {"npm", "exec"},
|
||||
{"pnpm", "install"}, {"pnpm", "add"}, {"pnpm", "dlx"},
|
||||
{"yarn", "install"}, {"yarn", "add"}, {"bun", "install"},
|
||||
{"bun", "add"}, {"pip", "install"}, {"pip", "download"},
|
||||
{"pip3", "install"}, {"uv", "pip"}, {"uv", "add"},
|
||||
{"uv", "sync"}, {"cargo", "install"}, {"cargo", "fetch"},
|
||||
{"cargo", "update"}, {"cargo", "publish"}, {"go", "get"},
|
||||
{"go", "install"}, {"go", "mod"}, {"gem", "install"},
|
||||
{"dotnet", "restore"}, {"dotnet", "add"}, {"winget", "install"},
|
||||
{"choco", "install"}, {"scoop", "install"}, {"apt", "install"},
|
||||
{"apt-get", "install"}, {"pkg", "install"}, {"brew", "install"}};
|
||||
if (!cmd)
|
||||
return 0;
|
||||
const char *prev = NULL;
|
||||
size_t prev_n = 0;
|
||||
for (const char *p = cmd; *p;) {
|
||||
while (*p && strchr(" \t\r\n;|&()<>`'\"$=", *p))
|
||||
p++;
|
||||
const char *w = p;
|
||||
while (*p && !strchr(" \t\r\n;|&()<>`'\"$=", *p))
|
||||
p++;
|
||||
size_t n = (size_t)(p - w);
|
||||
if (!n)
|
||||
break;
|
||||
/* /usr/bin/curl, C:\tools\curl.exe */
|
||||
for (size_t i = n; i > 0; i--) {
|
||||
if (w[i - 1] == '/' || w[i - 1] == '\\') {
|
||||
n -= i;
|
||||
w += i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (size_t i = 0; i < sizeof(TOOLS) / sizeof(TOOLS[0]); i++)
|
||||
if (net_word_eq(w, n, TOOLS[i]))
|
||||
return 1;
|
||||
for (size_t i = 0; prev && i < sizeof(PAIRS) / sizeof(PAIRS[0]); i++)
|
||||
if (net_word_eq(prev, prev_n, PAIRS[i][0]) &&
|
||||
net_word_eq(w, n, PAIRS[i][1]))
|
||||
return 1;
|
||||
if (n && w[0] != '-') {
|
||||
prev = w;
|
||||
prev_n = n;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* --- read-only command rules ---------------------------------------------- */
|
||||
|
||||
#define SAFE_MAX_WORDS 64
|
||||
|
||||
+13
-33
@@ -11,10 +11,7 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#ifndef _WIN32
|
||||
#include <dirent.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#include "cJSON.h"
|
||||
#include "cst_mcp.h"
|
||||
@@ -60,6 +57,7 @@ static char *slurp(const char *path, size_t max) {
|
||||
fclose(fp);
|
||||
if (!b.buf)
|
||||
cst_buf_puts(&b, "");
|
||||
cst_strip_bom(b.buf, &b.len);
|
||||
return cst_buf_steal(&b);
|
||||
}
|
||||
|
||||
@@ -101,14 +99,6 @@ static int plugin_run(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||
cst_tool_result_t *r, void *ud) {
|
||||
const ptool_t *pt = ud;
|
||||
r->exit_code = 1;
|
||||
#ifdef _WIN32
|
||||
(void)ctx;
|
||||
(void)c;
|
||||
(void)pt;
|
||||
r->out = strdup("error: plugin tools are not supported on Windows yet");
|
||||
r->out_len = r->out ? strlen(r->out) : 0;
|
||||
return 0;
|
||||
#else
|
||||
const cst_tool_def_t *def = cst_tool_find(pt->tool);
|
||||
char *args = cst_tool_args_json(def, c);
|
||||
char tmp[OS_PATH_MAX];
|
||||
@@ -180,7 +170,6 @@ static int plugin_run(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||
r->out_len = out.len;
|
||||
r->out = cst_buf_steal(&out);
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* argv array -> quoted command line; relative entries found in the plugin
|
||||
@@ -339,21 +328,20 @@ static void load_plugin(const char *dir, const char *dirname,
|
||||
|
||||
static int scan(const char *root, const char *source) {
|
||||
int n = 0;
|
||||
#ifndef _WIN32
|
||||
DIR *d = opendir(root);
|
||||
os_dir_t *d = os_dir_open(root);
|
||||
if (!d)
|
||||
return 0;
|
||||
char *names[CST_PLUGINS_MAX * 2];
|
||||
int nn = 0;
|
||||
struct dirent *de;
|
||||
while (nn < CST_PLUGINS_MAX * 2 && (de = readdir(d)) != NULL) {
|
||||
if (de->d_name[0] == '.')
|
||||
const os_dirent_t *de;
|
||||
while (nn < CST_PLUGINS_MAX * 2 && (de = os_dir_next(d)) != NULL) {
|
||||
if (de->name[0] == '.')
|
||||
continue;
|
||||
names[nn] = strdup(de->d_name);
|
||||
names[nn] = strdup(de->name);
|
||||
if (names[nn])
|
||||
nn++;
|
||||
}
|
||||
closedir(d);
|
||||
os_dir_close(d);
|
||||
for (int i = 1; i < nn; i++)
|
||||
for (int j = i; j > 0 && strcmp(names[j - 1], names[j]) > 0; j--) {
|
||||
char *t = names[j];
|
||||
@@ -371,32 +359,24 @@ static int scan(const char *root, const char *source) {
|
||||
}
|
||||
free(names[i]);
|
||||
}
|
||||
#else
|
||||
(void)root;
|
||||
(void)source;
|
||||
#endif
|
||||
return n;
|
||||
}
|
||||
|
||||
static int count_plugin_dirs(const char *root) {
|
||||
int n = 0;
|
||||
#ifndef _WIN32
|
||||
DIR *d = opendir(root);
|
||||
os_dir_t *d = os_dir_open(root);
|
||||
if (!d)
|
||||
return 0;
|
||||
struct dirent *de;
|
||||
while ((de = readdir(d)) != NULL) {
|
||||
const os_dirent_t *de;
|
||||
while ((de = os_dir_next(d)) != NULL) {
|
||||
char pj[OS_PATH_MAX];
|
||||
if (de->d_name[0] != '.' &&
|
||||
snprintf(pj, sizeof(pj), "%s/%s/plugin.json", root, de->d_name) <
|
||||
if (de->name[0] != '.' &&
|
||||
snprintf(pj, sizeof(pj), "%s/%s/plugin.json", root, de->name) <
|
||||
(int)sizeof(pj) &&
|
||||
is_file(pj))
|
||||
n++;
|
||||
}
|
||||
closedir(d);
|
||||
#else
|
||||
(void)root;
|
||||
#endif
|
||||
os_dir_close(d);
|
||||
return n;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
#elif defined(__APPLE__)
|
||||
#define CST_HAVE_SEATBELT 1
|
||||
#include <stdint.h>
|
||||
#elif defined(_WIN32)
|
||||
#include <windows.h>
|
||||
#include <sddl.h>
|
||||
#endif
|
||||
|
||||
#if defined(__linux__) && (defined(__x86_64__) || defined(__aarch64__))
|
||||
@@ -864,6 +867,341 @@ static int sc_probe(void) {
|
||||
|
||||
#endif /* CST_HAVE_SECCOMP */
|
||||
|
||||
/* --- Windows: Low integrity level ----------------------------------------- */
|
||||
#ifdef _WIN32
|
||||
|
||||
#define LW_LABEL "S:(ML;OICI;NW;;;LW)"
|
||||
#define LW_RECORD "sandbox-labels"
|
||||
|
||||
/* Same user, no admin privileges, Low IL. No LUA_TOKEN: a deny-only
|
||||
* Administrators group cannot open an admin's (SSH) window station and the
|
||||
* child fails in DLL init; the integrity check does not need it. */
|
||||
static HANDLE lw_token(void) {
|
||||
HANDLE self = NULL, tok = NULL;
|
||||
if (!OpenProcessToken(GetCurrentProcess(),
|
||||
TOKEN_DUPLICATE | TOKEN_QUERY | TOKEN_ADJUST_DEFAULT |
|
||||
TOKEN_ASSIGN_PRIMARY,
|
||||
&self))
|
||||
return NULL;
|
||||
BOOL ok = CreateRestrictedToken(self, DISABLE_MAX_PRIVILEGE, 0, NULL, 0,
|
||||
NULL, 0, NULL, &tok);
|
||||
CloseHandle(self);
|
||||
if (!ok)
|
||||
return NULL;
|
||||
PSID low = NULL;
|
||||
if (!ConvertStringSidToSidW(L"S-1-16-4096", &low)) {
|
||||
CloseHandle(tok);
|
||||
return NULL;
|
||||
}
|
||||
TOKEN_MANDATORY_LABEL tml;
|
||||
memset(&tml, 0, sizeof(tml));
|
||||
tml.Label.Sid = low;
|
||||
tml.Label.Attributes = SE_GROUP_INTEGRITY;
|
||||
ok = SetTokenInformation(tok, TokenIntegrityLevel, &tml,
|
||||
(DWORD)(sizeof(tml) + GetLengthSid(low)));
|
||||
LocalFree(low);
|
||||
if (!ok) {
|
||||
CloseHandle(tok);
|
||||
return NULL;
|
||||
}
|
||||
return tok;
|
||||
}
|
||||
|
||||
static int lw_record_path(char *out, size_t n) {
|
||||
char dir[OS_PATH_MAX];
|
||||
if (os_config_dir(dir, sizeof(dir)) != 0)
|
||||
return -1;
|
||||
return snprintf(out, n, "%s/%s", dir, LW_RECORD) < (int)n ? 0 : -1;
|
||||
}
|
||||
|
||||
static char *lw_read_record(void) {
|
||||
char path[OS_PATH_MAX];
|
||||
if (lw_record_path(path, sizeof(path)) != 0)
|
||||
return NULL;
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f)
|
||||
return NULL;
|
||||
size_t cap = 4096, len = 0;
|
||||
char *s = malloc(cap);
|
||||
size_t k;
|
||||
while (s && (k = fread(s + len, 1, cap - len - 1, f)) > 0) {
|
||||
len += k;
|
||||
if (cap - len < 2) {
|
||||
char *g = realloc(s, cap * 2);
|
||||
if (!g) {
|
||||
free(s);
|
||||
s = NULL;
|
||||
break;
|
||||
}
|
||||
s = g;
|
||||
cap *= 2;
|
||||
}
|
||||
}
|
||||
fclose(f);
|
||||
if (s)
|
||||
s[len] = 0;
|
||||
return s;
|
||||
}
|
||||
|
||||
/* dir is a recorded directory or inside one (the label is inherited). */
|
||||
static int lw_recorded(const char *dir) {
|
||||
char *s = lw_read_record();
|
||||
int found = 0;
|
||||
for (char *line = s; line && *line && !found;) {
|
||||
char *nl = strchr(line, '\n');
|
||||
if (nl)
|
||||
*nl = 0;
|
||||
char *tab = strchr(line, '\t');
|
||||
if (tab)
|
||||
*tab = 0;
|
||||
found = line[0] && os_path_under(line, dir);
|
||||
line = nl ? nl + 1 : NULL;
|
||||
}
|
||||
free(s);
|
||||
return found;
|
||||
}
|
||||
|
||||
static int lw_record_add(const char *dir, const char *old_sddl) {
|
||||
char path[OS_PATH_MAX];
|
||||
if (lw_record_path(path, sizeof(path)) != 0)
|
||||
return -1;
|
||||
FILE *f = fopen(path, "ab");
|
||||
if (!f)
|
||||
return -1;
|
||||
fprintf(f, "%s\t%s\n", dir, old_sddl);
|
||||
return fclose(f) == 0 ? 0 : -1;
|
||||
}
|
||||
|
||||
static int lw_same_dir(const char *a, const char *b) {
|
||||
return a && b && a[0] && b[0] && os_path_under(a, b) && os_path_under(b, a);
|
||||
}
|
||||
|
||||
/* An environment directory in the form os_realpath gives the workspace
|
||||
* (long names, '/'), or NULL. */
|
||||
static const char *lw_env_dir(const char *name, char *buf, size_t n) {
|
||||
const char *v = getenv(name);
|
||||
if (!v || !v[0] || os_realpath(v, buf, n) != 0)
|
||||
return NULL;
|
||||
os_path_norm_slashes(buf);
|
||||
return buf;
|
||||
}
|
||||
|
||||
/* Labeling a whole drive, the profile or system folders would open far more
|
||||
* than a project to every sandboxed command. */
|
||||
static int lw_refused(const char *dir) {
|
||||
size_t n = strlen(dir);
|
||||
while (n > 0 && (dir[n - 1] == '/' || dir[n - 1] == '\\'))
|
||||
n--;
|
||||
if (n <= 2)
|
||||
return 1;
|
||||
static const char *const SELF[] = {"USERPROFILE", "APPDATA",
|
||||
"LOCALAPPDATA", "TEMP"};
|
||||
static const char *const TREE[] = {"SystemRoot", "ProgramFiles",
|
||||
"ProgramFiles(x86)", "ProgramData"};
|
||||
char buf[OS_PATH_MAX];
|
||||
for (size_t i = 0; i < sizeof(SELF) / sizeof(SELF[0]); i++)
|
||||
if (lw_same_dir(lw_env_dir(SELF[i], buf, sizeof(buf)), dir))
|
||||
return 1;
|
||||
for (size_t i = 0; i < sizeof(TREE) / sizeof(TREE[0]); i++) {
|
||||
const char *v = lw_env_dir(TREE[i], buf, sizeof(buf));
|
||||
if (v && os_path_under(v, dir))
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int lw_prepare(const char *ws, cst_sandbox_plan_t *p) {
|
||||
static int creds_done, tmp_done;
|
||||
char buf[OS_PATH_MAX + 256];
|
||||
if (!ws || !ws[0])
|
||||
return -1;
|
||||
if (!creds_done) {
|
||||
/* files saved by older versions have no no-read-up label yet */
|
||||
char dir[OS_PATH_MAX];
|
||||
if (os_config_dir(dir, sizeof(dir)) == 0 &&
|
||||
snprintf(buf, sizeof(buf), "%s/credentials", dir) < (int)sizeof(buf)) {
|
||||
os_stat_t st;
|
||||
if (os_lstat(buf, &st) != 0 || os_chmod_private(buf) == 0)
|
||||
creds_done = 1;
|
||||
}
|
||||
}
|
||||
char tmp[OS_PATH_MAX];
|
||||
if (os_temp_dir(tmp, sizeof(tmp)) != 0 ||
|
||||
snprintf(buf, sizeof(buf), "%s/cst-agent-sandbox", tmp) >=
|
||||
(int)sizeof(tmp))
|
||||
return -1;
|
||||
memcpy(tmp, buf, strlen(buf) + 1);
|
||||
if (!tmp_done) {
|
||||
os_mkdir_p(tmp, 0700);
|
||||
if (os_win_set_label(tmp, LW_LABEL) != 0)
|
||||
return -1;
|
||||
tmp_done = 1;
|
||||
}
|
||||
if (!lw_recorded(ws)) {
|
||||
char old[512];
|
||||
if (lw_refused(ws) || os_win_get_label(ws, old, sizeof(old)) != 0 ||
|
||||
os_win_set_label(ws, LW_LABEL) != 0)
|
||||
return -1;
|
||||
lw_record_add(ws, old);
|
||||
snprintf(buf, sizeof(buf),
|
||||
"[cst: sandbox: labeled %s Low integrity so sandboxed commands "
|
||||
"can write there; /sandbox reset-labels undoes it]\n",
|
||||
ws);
|
||||
p->note = strdup(buf);
|
||||
}
|
||||
HANDLE tok = lw_token();
|
||||
if (!tok) {
|
||||
free(p->note);
|
||||
p->note = NULL;
|
||||
return -1;
|
||||
}
|
||||
p->token = tok;
|
||||
for (char *c = tmp; *c; c++)
|
||||
if (*c == '/')
|
||||
*c = '\\';
|
||||
p->tmpdir = strdup(tmp);
|
||||
return p->tmpdir ? 0 : -1;
|
||||
}
|
||||
|
||||
static int lw_run(const char *ws, const char *cmd, size_t max_out,
|
||||
const cst_sandbox_plan_t *p, os_run_result_t *r) {
|
||||
size_t tl = strlen(p->tmpdir) + 8;
|
||||
char *te = malloc(tl), *tm = malloc(tl);
|
||||
int rc = -1;
|
||||
if (te && tm) {
|
||||
snprintf(te, tl, "TEMP=%s", p->tmpdir);
|
||||
snprintf(tm, tl, "TMP=%s", p->tmpdir);
|
||||
const char *env[] = {te, tm, NULL};
|
||||
rc = os_run_shell_token(ws, cmd, max_out, r, p->token, env);
|
||||
}
|
||||
free(te);
|
||||
free(tm);
|
||||
if (rc == 0 && p->note) {
|
||||
size_t nl = strlen(p->note);
|
||||
char *o = malloc(nl + r->out_len + 1);
|
||||
if (o) {
|
||||
memcpy(o, p->note, nl);
|
||||
if (r->out)
|
||||
memcpy(o + nl, r->out, r->out_len);
|
||||
o[nl + r->out_len] = 0;
|
||||
free(r->out);
|
||||
r->out = o;
|
||||
r->out_len += nl;
|
||||
}
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
#endif /* _WIN32 */
|
||||
|
||||
char *cst_sandbox_labels(void) {
|
||||
#ifdef _WIN32
|
||||
char *s = lw_read_record();
|
||||
if (!s)
|
||||
return NULL;
|
||||
size_t o = 0;
|
||||
for (char *line = s; *line;) {
|
||||
char *nl = strchr(line, '\n');
|
||||
size_t len = nl ? (size_t)(nl - line) : strlen(line);
|
||||
size_t path = strcspn(line, "\t\n");
|
||||
if (path) {
|
||||
memmove(s + o, line, path);
|
||||
o += path;
|
||||
s[o++] = '\n';
|
||||
}
|
||||
line += len + (nl ? 1 : 0);
|
||||
}
|
||||
s[o] = 0;
|
||||
if (!o) {
|
||||
free(s);
|
||||
return NULL;
|
||||
}
|
||||
return s;
|
||||
#else
|
||||
return NULL;
|
||||
#endif
|
||||
}
|
||||
|
||||
int cst_sandbox_reset_labels(char **report) {
|
||||
if (report)
|
||||
*report = NULL;
|
||||
#ifdef _WIN32
|
||||
char *s = lw_read_record();
|
||||
size_t cap = 256, len = 0;
|
||||
char *rep = malloc(cap), *keep = NULL;
|
||||
size_t kcap = 0, klen = 0;
|
||||
int ok = 0, bad = 0;
|
||||
if (rep)
|
||||
rep[0] = 0;
|
||||
for (char *line = s; line && *line;) {
|
||||
char *nl = strchr(line, '\n');
|
||||
if (nl)
|
||||
*nl = 0;
|
||||
char *tab = strchr(line, '\t');
|
||||
const char *old = tab ? tab + 1 : "S:";
|
||||
if (tab)
|
||||
*tab = 0;
|
||||
if (line[0]) {
|
||||
os_stat_t st;
|
||||
int gone = os_lstat(line, &st) != 0;
|
||||
int done = gone || os_win_set_label(line, old[0] ? old : "S:") == 0;
|
||||
char msg[OS_PATH_MAX + 64];
|
||||
int m = snprintf(msg, sizeof(msg), "%s %s\n",
|
||||
gone ? "gone " : done ? "restored" : "FAILED ",
|
||||
line);
|
||||
if (m > 0 && rep) {
|
||||
while (len + (size_t)m + 1 > cap) {
|
||||
char *g = realloc(rep, cap *= 2);
|
||||
if (!g)
|
||||
break;
|
||||
rep = g;
|
||||
}
|
||||
if (len + (size_t)m + 1 <= cap) {
|
||||
memcpy(rep + len, msg, (size_t)m + 1);
|
||||
len += (size_t)m;
|
||||
}
|
||||
}
|
||||
if (done) {
|
||||
ok++;
|
||||
} else {
|
||||
bad++;
|
||||
size_t need = strlen(line) + strlen(old) + 3;
|
||||
if (klen + need + 1 > kcap) {
|
||||
char *g = realloc(keep, kcap = (klen + need + 1) * 2);
|
||||
if (g)
|
||||
keep = g;
|
||||
}
|
||||
if (keep && klen + need + 1 <= kcap)
|
||||
klen += (size_t)snprintf(keep + klen, kcap - klen, "%s\t%s\n",
|
||||
line, old);
|
||||
}
|
||||
}
|
||||
line = nl ? nl + 1 : NULL;
|
||||
}
|
||||
free(s);
|
||||
char path[OS_PATH_MAX];
|
||||
if (lw_record_path(path, sizeof(path)) == 0) {
|
||||
if (klen) {
|
||||
FILE *f = fopen(path, "wb");
|
||||
if (f) {
|
||||
fwrite(keep, 1, klen, f);
|
||||
fclose(f);
|
||||
}
|
||||
} else {
|
||||
remove(path);
|
||||
}
|
||||
}
|
||||
free(keep);
|
||||
if (report)
|
||||
*report = rep;
|
||||
else
|
||||
free(rep);
|
||||
return bad ? -1 : ok;
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* --- common --------------------------------------------------------------- */
|
||||
|
||||
int cst_sandbox_probe(void) {
|
||||
@@ -891,6 +1229,15 @@ int cst_sandbox_probe(void) {
|
||||
#endif
|
||||
#ifdef CST_HAVE_SEATBELT
|
||||
kind = CST_SANDBOX_SEATBELT;
|
||||
#endif
|
||||
#ifdef _WIN32
|
||||
if (!e || strcmp(e, "lowil") == 0) {
|
||||
HANDLE tok = lw_token();
|
||||
if (tok) {
|
||||
CloseHandle(tok);
|
||||
kind = CST_SANDBOX_LOWIL;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
(void)want_ll;
|
||||
(void)want_sc;
|
||||
@@ -917,6 +1264,8 @@ const char *cst_sandbox_name(int kind) {
|
||||
return "seatbelt";
|
||||
case CST_SANDBOX_SECCOMP:
|
||||
return "seccomp";
|
||||
case CST_SANDBOX_LOWIL:
|
||||
return "lowil";
|
||||
default:
|
||||
return "none";
|
||||
}
|
||||
@@ -939,6 +1288,10 @@ int cst_sandbox_prepare(const char *workspace, const char *tmpdir,
|
||||
#ifdef CST_HAVE_SECCOMP
|
||||
if (kind == CST_SANDBOX_SECCOMP)
|
||||
rc = sc_prepare(workspace, tmpdir, plan);
|
||||
#endif
|
||||
#ifdef _WIN32
|
||||
if (kind == CST_SANDBOX_LOWIL && (rc = lw_prepare(workspace, plan)) != 0)
|
||||
cst_sandbox_release(plan);
|
||||
#endif
|
||||
(void)workspace;
|
||||
(void)tmpdir;
|
||||
@@ -988,6 +1341,15 @@ void cst_sandbox_release(cst_sandbox_plan_t *plan) {
|
||||
#ifdef CST_HAVE_SECCOMP
|
||||
sc_free(plan->sc);
|
||||
#endif
|
||||
#ifdef _WIN32
|
||||
if (plan->token)
|
||||
CloseHandle(plan->token);
|
||||
#endif
|
||||
plan->token = NULL;
|
||||
free(plan->tmpdir);
|
||||
plan->tmpdir = NULL;
|
||||
free(plan->note);
|
||||
plan->note = NULL;
|
||||
plan->sc = NULL;
|
||||
free(plan->profile);
|
||||
plan->profile = NULL;
|
||||
@@ -1004,6 +1366,12 @@ int cst_sandbox_run_shell(const char *workspace, const char *cmd,
|
||||
if (cst_sandbox_prepare(workspace, tmp, &plan) != 0) {
|
||||
memset(r, 0, sizeof(*r));
|
||||
const char *msg = "[cst: sandbox unavailable; command not run]\n";
|
||||
#ifdef _WIN32
|
||||
if (workspace && !lw_recorded(workspace) && lw_refused(workspace))
|
||||
msg = "[cst: sandbox: will not label a drive root, the profile or a "
|
||||
"system folder writable; command not run. Start cst-agent in a "
|
||||
"project folder]\n";
|
||||
#endif
|
||||
r->out = malloc(strlen(msg) + 1);
|
||||
if (r->out) {
|
||||
memcpy(r->out, msg, strlen(msg) + 1);
|
||||
@@ -1012,8 +1380,12 @@ int cst_sandbox_run_shell(const char *workspace, const char *cmd,
|
||||
r->exit_code = 126;
|
||||
return 0;
|
||||
}
|
||||
#ifdef _WIN32
|
||||
int rc = lw_run(workspace, cmd, max_out, &plan, r);
|
||||
#else
|
||||
int rc = os_run_shell_ex(workspace, cmd, max_out, r, cst_sandbox_enter,
|
||||
&plan);
|
||||
#endif
|
||||
cst_sandbox_release(&plan);
|
||||
return rc;
|
||||
}
|
||||
@@ -1039,6 +1411,11 @@ int cst_sandbox_blocked(const char *out, size_t n, int exit_code) {
|
||||
"权限不够", "不允许的操作", "只读文件系统", "權限不足",
|
||||
"拒絕不符權限的操作", "許可がありません", "Keine Berechtigung",
|
||||
"Permission non accord", "Permiso denegado", "Отказано в доступе",
|
||||
/* Windows ERROR_ACCESS_DENIED from cmd, PowerShell and Win32 tools */
|
||||
"Access is denied", "Access to the path", "UnauthorizedAccess",
|
||||
"拒绝访问", "存取被拒", "Zugriff verweigert", "Accès refusé",
|
||||
/* Cygwin/MSYS2 programs cannot initialize at Low IL */
|
||||
"DLL initialization routine failed", "0xc0000142",
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(SIGNS) / sizeof(SIGNS[0]); i++)
|
||||
if (has(out, n, SIGNS[i]))
|
||||
|
||||
+3
-3
@@ -137,7 +137,7 @@ static const char *jstr(const cJSON *o, const char *key) {
|
||||
}
|
||||
|
||||
static void load_existing(cst_session_t *s) {
|
||||
FILE *rp = fopen(s->path, "r");
|
||||
FILE *rp = fopen(s->path, "rb");
|
||||
if (!rp)
|
||||
return;
|
||||
char *line = NULL;
|
||||
@@ -196,7 +196,7 @@ cst_session_t *cst_session_open(const char *path) {
|
||||
pthread_mutex_init(&s->mu, NULL);
|
||||
snprintf(s->path, sizeof(s->path), "%s", path ? path : "session.jsonl");
|
||||
load_existing(s);
|
||||
s->fp = fopen(s->path, "a+");
|
||||
s->fp = fopen(s->path, "a+b");
|
||||
if (!s->fp || session_grow(s) != 0) {
|
||||
if (s->fp)
|
||||
fclose(s->fp);
|
||||
@@ -295,7 +295,7 @@ static int reopen_truncate(cst_session_t *s) {
|
||||
fclose(s->fp);
|
||||
s->fp = NULL;
|
||||
}
|
||||
s->fp = fopen(s->path, "w+");
|
||||
s->fp = fopen(s->path, "w+b");
|
||||
return s->fp ? 0 : -1;
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
#include "cst_agent.h"
|
||||
#include "cst_util.h"
|
||||
#include "os_compat.h"
|
||||
|
||||
#include "cJSON.h"
|
||||
@@ -31,7 +32,7 @@ int cst_settings_load(cst_settings_t *s) {
|
||||
size_t n = fread(buf, 1, sizeof(buf) - 1, fp);
|
||||
fclose(fp);
|
||||
buf[n] = 0;
|
||||
cJSON *root = cJSON_Parse(buf);
|
||||
cJSON *root = cJSON_Parse(buf + cst_bom_len(buf, n));
|
||||
if (!cJSON_IsObject(root)) {
|
||||
cJSON_Delete(root);
|
||||
return -1;
|
||||
|
||||
+14
-36
@@ -13,10 +13,7 @@
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#ifndef _WIN32
|
||||
#include <dirent.h>
|
||||
#include <pthread.h>
|
||||
#endif
|
||||
|
||||
#include "cst_tools.h"
|
||||
#include "cst_util.h"
|
||||
@@ -166,14 +163,9 @@ static struct {
|
||||
char source[48];
|
||||
} g_roots[EXTRA_ROOTS];
|
||||
static int g_nroots;
|
||||
#ifndef _WIN32
|
||||
static pthread_mutex_t g_mu = PTHREAD_MUTEX_INITIALIZER;
|
||||
#define LOCK() pthread_mutex_lock(&g_mu)
|
||||
#define UNLOCK() pthread_mutex_unlock(&g_mu)
|
||||
#else
|
||||
#define LOCK() ((void)0)
|
||||
#define UNLOCK() ((void)0)
|
||||
#endif
|
||||
|
||||
void cst_skills_add_root(const char *dir, const char *source) {
|
||||
if (!dir || !dir[0])
|
||||
@@ -225,6 +217,7 @@ static char *slurp(const char *path, size_t max, size_t *len) {
|
||||
}
|
||||
if (!b.buf)
|
||||
cst_buf_puts(&b, "");
|
||||
cst_strip_bom(b.buf, &b.len);
|
||||
if (len)
|
||||
*len = b.len;
|
||||
return cst_buf_steal(&b);
|
||||
@@ -243,16 +236,15 @@ static int have(const cst_skill_t *v, int n, const char *name) {
|
||||
|
||||
static int scan_root(const char *root, const char *source, cst_skill_t *out,
|
||||
int n, int max) {
|
||||
#ifndef _WIN32
|
||||
DIR *d = opendir(root);
|
||||
os_dir_t *d = os_dir_open(root);
|
||||
if (!d)
|
||||
return n;
|
||||
struct dirent *de;
|
||||
while (n < max && (de = readdir(d)) != NULL) {
|
||||
if (de->d_name[0] == '.')
|
||||
const os_dirent_t *de;
|
||||
while (n < max && (de = os_dir_next(d)) != NULL) {
|
||||
if (de->name[0] == '.')
|
||||
continue;
|
||||
char dir[OS_PATH_MAX], md[OS_PATH_MAX + 16];
|
||||
if (snprintf(dir, sizeof(dir), "%s/%s", root, de->d_name) >=
|
||||
if (snprintf(dir, sizeof(dir), "%s/%s", root, de->name) >=
|
||||
(int)sizeof(dir))
|
||||
continue;
|
||||
snprintf(md, sizeof(md), "%s/SKILL.md", dir);
|
||||
@@ -262,7 +254,7 @@ static int scan_root(const char *root, const char *source, cst_skill_t *out,
|
||||
cst_skill_t s;
|
||||
memset(&s, 0, sizeof(s));
|
||||
if (!cst_fm_get(text, "name", s.name, sizeof(s.name)) || !s.name[0])
|
||||
snprintf(s.name, sizeof(s.name), "%.63s", de->d_name);
|
||||
snprintf(s.name, sizeof(s.name), "%.63s", de->name);
|
||||
cst_fm_get(text, "description", s.desc, sizeof(s.desc));
|
||||
if (!s.desc[0]) {
|
||||
/* first prose line of the body */
|
||||
@@ -282,13 +274,7 @@ static int scan_root(const char *root, const char *source, cst_skill_t *out,
|
||||
if (!have(out, n, s.name))
|
||||
out[n++] = s;
|
||||
}
|
||||
closedir(d);
|
||||
#else
|
||||
(void)root;
|
||||
(void)source;
|
||||
(void)out;
|
||||
(void)max;
|
||||
#endif
|
||||
os_dir_close(d);
|
||||
return n;
|
||||
}
|
||||
|
||||
@@ -371,25 +357,24 @@ static int find_skill(const char *ws, const char *name, cst_skill_t *out) {
|
||||
|
||||
static void list_files(const char *dir, const char *rel, int depth,
|
||||
cst_buf_t *b, int *count) {
|
||||
#ifndef _WIN32
|
||||
char full[OS_PATH_MAX];
|
||||
if (snprintf(full, sizeof(full), "%s%s%s", dir, rel[0] ? "/" : "", rel) >=
|
||||
(int)sizeof(full))
|
||||
return;
|
||||
DIR *d = opendir(full);
|
||||
os_dir_t *d = os_dir_open(full);
|
||||
if (!d)
|
||||
return;
|
||||
char *names[256];
|
||||
int nn = 0;
|
||||
struct dirent *de;
|
||||
while (nn < 256 && (de = readdir(d)) != NULL) {
|
||||
if (de->d_name[0] == '.')
|
||||
const os_dirent_t *de;
|
||||
while (nn < 256 && (de = os_dir_next(d)) != NULL) {
|
||||
if (de->name[0] == '.')
|
||||
continue;
|
||||
names[nn] = strdup(de->d_name);
|
||||
names[nn] = strdup(de->name);
|
||||
if (names[nn])
|
||||
nn++;
|
||||
}
|
||||
closedir(d);
|
||||
os_dir_close(d);
|
||||
for (int i = 1; i < nn; i++) {
|
||||
for (int j = i; j > 0 && strcmp(names[j - 1], names[j]) > 0; j--) {
|
||||
char *tmp = names[j];
|
||||
@@ -416,13 +401,6 @@ static void list_files(const char *dir, const char *rel, int depth,
|
||||
}
|
||||
for (int i = 0; i < nn; i++)
|
||||
free(names[i]);
|
||||
#else
|
||||
(void)dir;
|
||||
(void)rel;
|
||||
(void)depth;
|
||||
(void)b;
|
||||
(void)count;
|
||||
#endif
|
||||
}
|
||||
|
||||
static char *errf(int *code, const char *fmt, const char *a, const char *b) {
|
||||
|
||||
+23
-40
@@ -14,9 +14,6 @@
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <time.h>
|
||||
#ifndef _WIN32
|
||||
#include <dirent.h>
|
||||
#endif
|
||||
|
||||
#include "cJSON.h"
|
||||
#include "cst_agent.h"
|
||||
@@ -79,6 +76,7 @@ static char *slurp(const char *path, size_t max) {
|
||||
fclose(fp);
|
||||
if (!b.buf)
|
||||
cst_buf_puts(&b, "");
|
||||
cst_strip_bom(b.buf, &b.len);
|
||||
return cst_buf_steal(&b);
|
||||
}
|
||||
|
||||
@@ -118,25 +116,24 @@ static int truthy(const char *v) {
|
||||
|
||||
static void load_dir(const char *dir, const char *source, cst_agent_role_t *v,
|
||||
int *n, int max) {
|
||||
#ifndef _WIN32
|
||||
DIR *d = opendir(dir);
|
||||
os_dir_t *d = os_dir_open(dir);
|
||||
if (!d)
|
||||
return;
|
||||
struct dirent *de;
|
||||
while ((de = readdir(d)) != NULL && *n < max) {
|
||||
size_t len = strlen(de->d_name);
|
||||
if (de->d_name[0] == '.' || len < 4 ||
|
||||
strcmp(de->d_name + len - 3, ".md") != 0)
|
||||
const os_dirent_t *de;
|
||||
while ((de = os_dir_next(d)) != NULL && *n < max) {
|
||||
size_t len = strlen(de->name);
|
||||
if (de->name[0] == '.' || len < 4 ||
|
||||
strcmp(de->name + len - 3, ".md") != 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))
|
||||
continue;
|
||||
char *text = slurp(p, ROLE_FILE_MAX);
|
||||
if (!text)
|
||||
continue;
|
||||
char name[64], val[1024];
|
||||
if (!cst_fm_get(text, "name", name, sizeof(name)) || !name[0])
|
||||
snprintf(name, sizeof(name), "%.*s", (int)(len - 3), de->d_name);
|
||||
snprintf(name, sizeof(name), "%.*s", (int)(len - 3), de->name);
|
||||
char clean[64];
|
||||
cst_tool_sanitize_name(name, clean, sizeof(clean));
|
||||
int at = find_role(v, *n, clean);
|
||||
@@ -166,14 +163,7 @@ static void load_dir(const char *dir, const char *source, cst_agent_role_t *v,
|
||||
r->prompt = strdup(body);
|
||||
free(text);
|
||||
}
|
||||
closedir(d);
|
||||
#else
|
||||
(void)dir;
|
||||
(void)source;
|
||||
(void)v;
|
||||
(void)n;
|
||||
(void)max;
|
||||
#endif
|
||||
os_dir_close(d);
|
||||
}
|
||||
|
||||
int cst_roles_list(const char *ws, cst_agent_role_t *out, int max) {
|
||||
@@ -255,18 +245,16 @@ int cst_subagent_path(const char *ws, const char *parent_id, const char *file,
|
||||
static int next_file(const char *dir, char *file, size_t fsz) {
|
||||
pthread_mutex_lock(&g_seq_mu);
|
||||
int best = 0;
|
||||
#ifndef _WIN32
|
||||
DIR *d = opendir(dir);
|
||||
os_dir_t *d = os_dir_open(dir);
|
||||
if (d) {
|
||||
struct dirent *de;
|
||||
while ((de = readdir(d)) != NULL) {
|
||||
const os_dirent_t *de;
|
||||
while ((de = os_dir_next(d)) != NULL) {
|
||||
int k = 0;
|
||||
if (sscanf(de->d_name, "sub-%d.jsonl", &k) == 1 && k > best)
|
||||
if (sscanf(de->name, "sub-%d.jsonl", &k) == 1 && k > best)
|
||||
best = k;
|
||||
}
|
||||
closedir(d);
|
||||
os_dir_close(d);
|
||||
}
|
||||
#endif
|
||||
int rc = -1;
|
||||
for (int k = best + 1; k < best + 100; k++) {
|
||||
char p[OS_PATH_MAX];
|
||||
@@ -276,7 +264,7 @@ static int next_file(const char *dir, char *file, size_t fsz) {
|
||||
struct stat st;
|
||||
if (stat(p, &st) == 0)
|
||||
continue;
|
||||
FILE *fp = fopen(p, "a");
|
||||
FILE *fp = fopen(p, "ab");
|
||||
if (fp) {
|
||||
fclose(fp);
|
||||
rc = 0;
|
||||
@@ -302,7 +290,7 @@ static void write_meta(const char *dir, const char *file, const char *role,
|
||||
cJSON_AddNumberToObject(o, "ms", ms);
|
||||
char *txt = cJSON_PrintUnformatted(o);
|
||||
cJSON_Delete(o);
|
||||
FILE *fp = txt ? fopen(p, "w") : NULL;
|
||||
FILE *fp = txt ? fopen(p, "wb") : NULL;
|
||||
if (fp) {
|
||||
fputs(txt, fp);
|
||||
fputc('\n', fp);
|
||||
@@ -317,19 +305,18 @@ int cst_subagent_list(const char *ws, const char *parent_id,
|
||||
if (sub_dir(ws, parent_id, dir, sizeof(dir)) != 0)
|
||||
return 0;
|
||||
int n = 0;
|
||||
#ifndef _WIN32
|
||||
DIR *d = opendir(dir);
|
||||
os_dir_t *d = os_dir_open(dir);
|
||||
if (!d)
|
||||
return 0;
|
||||
struct dirent *de;
|
||||
while ((de = readdir(d)) != NULL && n < max) {
|
||||
const os_dirent_t *de;
|
||||
while ((de = os_dir_next(d)) != NULL && n < max) {
|
||||
int k = 0;
|
||||
char tail[8] = "";
|
||||
if (sscanf(de->d_name, "sub-%d.%7s", &k, tail) != 2 ||
|
||||
if (sscanf(de->name, "sub-%d.%7s", &k, tail) != 2 ||
|
||||
strcmp(tail, "json") != 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))
|
||||
continue;
|
||||
char *txt = slurp(p, 64 * 1024);
|
||||
cJSON *o = txt ? cJSON_Parse(txt) : NULL;
|
||||
@@ -358,11 +345,7 @@ int cst_subagent_list(const char *ws, const char *parent_id,
|
||||
r->mtime = (long long)st.st_mtime;
|
||||
n++;
|
||||
}
|
||||
closedir(d);
|
||||
#else
|
||||
(void)out;
|
||||
(void)max;
|
||||
#endif
|
||||
os_dir_close(d);
|
||||
/* newest first: higher number */
|
||||
for (int i = 1; i < n; i++)
|
||||
for (int j = i; j > 0 && atoi(out[j - 1].file + 4) < atoi(out[j].file + 4);
|
||||
|
||||
+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
|
||||
|
||||
+48
-3
@@ -1,4 +1,5 @@
|
||||
#include "cst_agent.h"
|
||||
#include "cst_util.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -129,9 +130,9 @@ static int parse_hunk_header(const char *p, size_t n, long *a, long *b,
|
||||
return -1;
|
||||
}
|
||||
|
||||
int cst_patch_apply_buffer(const char *orig, size_t olen, const char *hunks,
|
||||
size_t hlen, char **out, size_t *out_len,
|
||||
int *nhunks, char *err, size_t err_sz) {
|
||||
static int patch_core(const char *orig, size_t olen, const char *hunks,
|
||||
size_t hlen, char **out, size_t *out_len, int *nhunks,
|
||||
char *err, size_t err_sz) {
|
||||
lines_t file = {0}, hl = {0};
|
||||
lines_t old_seq = {0}, new_seq = {0};
|
||||
int rc = -1;
|
||||
@@ -315,3 +316,47 @@ done:
|
||||
free(new_seq.v);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* A CRLF file is patched as LF (the model's hunks usually are) and written
|
||||
* back as CRLF; a BOM is kept out of the first line's context. */
|
||||
int cst_patch_apply_buffer(const char *orig, size_t olen, const char *hunks,
|
||||
size_t hlen, char **out, size_t *out_len,
|
||||
int *nhunks, char *err, size_t err_sz) {
|
||||
size_t bom = orig ? cst_bom_len(orig, olen) : 0;
|
||||
int crlf = orig && cst_text_is_crlf(orig, olen);
|
||||
if (!bom && !crlf)
|
||||
return patch_core(orig, olen, hunks, hlen, out, out_len, nhunks, err,
|
||||
err_sz);
|
||||
if (out)
|
||||
*out = NULL;
|
||||
char *o = malloc(olen - bom + 1), *h = malloc(hlen + 1), *res = NULL;
|
||||
size_t rlen = 0;
|
||||
int rc = -1;
|
||||
if (!o || !h) {
|
||||
snprintf(err, err_sz, "oom");
|
||||
goto done;
|
||||
}
|
||||
memcpy(o, orig + bom, olen - bom);
|
||||
o[olen - bom] = 0;
|
||||
size_t on = olen - bom, hn = hlen;
|
||||
if (hlen)
|
||||
memcpy(h, hunks, hlen);
|
||||
h[hlen] = 0;
|
||||
if (crlf) {
|
||||
on = cst_crlf_to_lf(o, on);
|
||||
hn = cst_crlf_to_lf(h, hn);
|
||||
}
|
||||
rc = patch_core(o, on, h, hn, &res, &rlen, nhunks, err, err_sz);
|
||||
if (rc == 0) {
|
||||
*out = cst_text_restyle(res, rlen, crlf, bom != 0, out_len);
|
||||
if (!*out) {
|
||||
snprintf(err, err_sz, "oom");
|
||||
rc = -1;
|
||||
}
|
||||
}
|
||||
done:
|
||||
free(res);
|
||||
free(o);
|
||||
free(h);
|
||||
return rc;
|
||||
}
|
||||
|
||||
@@ -6,9 +6,7 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#ifndef _WIN32
|
||||
#include <pthread.h>
|
||||
#endif
|
||||
|
||||
#include "cJSON.h"
|
||||
#include "cst_tools.h"
|
||||
@@ -182,14 +180,9 @@ const char *cst_tool_profile_name(int p) {
|
||||
#define DYN_MAX 256
|
||||
static cst_tool_def_t *g_dyn[DYN_MAX];
|
||||
static atomic_int g_ndyn;
|
||||
#ifndef _WIN32
|
||||
static pthread_mutex_t g_dyn_mu = PTHREAD_MUTEX_INITIALIZER;
|
||||
#define DYN_LOCK() pthread_mutex_lock(&g_dyn_mu)
|
||||
#define DYN_UNLOCK() pthread_mutex_unlock(&g_dyn_mu)
|
||||
#else
|
||||
#define DYN_LOCK() ((void)0)
|
||||
#define DYN_UNLOCK() ((void)0)
|
||||
#endif
|
||||
|
||||
int cst_tool_count(void) { return NTOOLS + atomic_load(&g_ndyn); }
|
||||
|
||||
|
||||
@@ -238,6 +238,22 @@ static int write_content(const ctl_tool_call_t *call, const char **content,
|
||||
return *content ? 0 : -1;
|
||||
}
|
||||
|
||||
/* Overwriting a CRLF and/or BOM file with plain LF text keeps the file's
|
||||
* style: malloc'd restyled copy, or NULL to write content as given. */
|
||||
static char *keep_text_style(const char *oldc, size_t olen, const char *s,
|
||||
size_t n, size_t *out_len) {
|
||||
if (!oldc)
|
||||
return NULL;
|
||||
int has_crlf = 0;
|
||||
for (size_t i = 1; i < n && !has_crlf; i++)
|
||||
has_crlf = s[i] == '\n' && s[i - 1] == '\r';
|
||||
int crlf = !has_crlf && cst_text_is_crlf(oldc, olen);
|
||||
int bom = cst_bom_len(oldc, olen) && !cst_bom_len(s, n);
|
||||
if (!crlf && !bom)
|
||||
return NULL;
|
||||
return cst_text_restyle(s, n, crlf, 1, out_len);
|
||||
}
|
||||
|
||||
int cst_preview_write_file(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||
char **out) {
|
||||
const char *path = tool_arg(c, "path");
|
||||
@@ -252,11 +268,18 @@ int cst_preview_write_file(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||
char *oldc = NULL;
|
||||
if (cst_sandbox_resolve(ctx->workspace_root, path, full, sizeof(full)) == 0)
|
||||
oldc = tool_slurp(full, &olen, 0);
|
||||
size_t sn = 0;
|
||||
char *styled = keep_text_style(oldc, olen, content, n, &sn);
|
||||
if (styled) {
|
||||
content = styled;
|
||||
n = sn;
|
||||
}
|
||||
cst_buf_t b = {0};
|
||||
cst_buf_printf(&b, "%s %s (%zu bytes)\n", oldc ? "overwrite" : "create",
|
||||
path, n);
|
||||
tool_append_diff(&b, path, oldc, olen, content, n);
|
||||
free(oldc);
|
||||
free(styled);
|
||||
*out = cst_buf_steal(&b);
|
||||
return 0;
|
||||
}
|
||||
@@ -276,14 +299,22 @@ int cst_tool_write_file(const char *workspace_root, const ctl_tool_call_t *call,
|
||||
|
||||
size_t olen = 0;
|
||||
char *oldc = tool_slurp(full, &olen, 0);
|
||||
size_t sn = 0;
|
||||
char *styled = keep_text_style(oldc, olen, content, n, &sn);
|
||||
if (styled) {
|
||||
content = styled;
|
||||
n = sn;
|
||||
}
|
||||
if (tool_write_whole(full, content, n) != 0) {
|
||||
free(oldc);
|
||||
free(styled);
|
||||
return tool_set_resultf(r, 1, "error: cannot write file: %.200s", path);
|
||||
}
|
||||
cst_buf_t b = {0};
|
||||
cst_buf_printf(&b, "wrote %s (%zu bytes)\n", path, n);
|
||||
tool_append_diff(&b, path, oldc, olen, content, n);
|
||||
free(oldc);
|
||||
free(styled);
|
||||
return tool_take_buf(r, &b, 0);
|
||||
}
|
||||
|
||||
|
||||
@@ -41,11 +41,12 @@
|
||||
|
||||
static int g_nocolor; /* NO_COLOR */
|
||||
static int g_ascii; /* non-UTF-8 locale or CST_ASCII=1 */
|
||||
static int g_logo_text; /* console font lacks the logo's block glyphs */
|
||||
static int g_utf8 = 1; /* locale can show non-ASCII text */
|
||||
#define HAS_COLOR() (!g_nocolor && has_colors())
|
||||
|
||||
/* Curses: ncursesw on Unix, PDCurses (wide) on Windows. */
|
||||
#if defined(CST_USE_PDCURSES) || defined(_WIN32)
|
||||
/* Curses: ncursesw (Unix and Windows), or PDCurses (wide) on Windows. */
|
||||
#if defined(CST_USE_PDCURSES)
|
||||
#ifndef PDC_WIDE
|
||||
#define PDC_WIDE 1
|
||||
#endif
|
||||
@@ -62,10 +63,23 @@ static int g_utf8 = 1; /* locale can show non-ASCII text */
|
||||
#ifndef KEY_RESIZE
|
||||
#define KEY_RESIZE 0632
|
||||
#endif
|
||||
#define wcwidth cst_wcwidth /* MinGW often lacks wcwidth */
|
||||
#else
|
||||
#include <ncurses.h>
|
||||
#endif
|
||||
#ifdef _WIN32
|
||||
#define wcwidth cst_wcwidth /* MinGW often lacks wcwidth */
|
||||
#endif
|
||||
|
||||
/* The Windows console (conhost/ConPTY) loses wide characters on insert or
|
||||
* delete-character, so curses must repaint cells instead. */
|
||||
static WINDOW *tui_newwin(int rows, int cols, int y, int x) {
|
||||
WINDOW *w = newwin(rows, cols, y, x);
|
||||
#ifdef _WIN32
|
||||
if (w)
|
||||
idcok(w, FALSE);
|
||||
#endif
|
||||
return w;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
static void cst_usleep(unsigned usec) {
|
||||
@@ -1876,10 +1890,10 @@ static void git_branch(const char *dir, char *out, size_t sz) {
|
||||
FILE *f = NULL;
|
||||
for (int depth = 0; depth < 64 && d[0]; depth++) {
|
||||
snprintf(p, sizeof(p), "%s/.git/HEAD", d);
|
||||
if ((f = fopen(p, "r")) != NULL)
|
||||
if ((f = fopen(p, "rb")) != NULL)
|
||||
break;
|
||||
snprintf(p, sizeof(p), "%s/.git", d);
|
||||
FILE *g = fopen(p, "r");
|
||||
FILE *g = fopen(p, "rb");
|
||||
if (g) {
|
||||
int ok = fgets(line, sizeof(line), g) && strncmp(line, "gitdir: ", 8) == 0;
|
||||
fclose(g);
|
||||
@@ -1889,7 +1903,7 @@ static void git_branch(const char *dir, char *out, size_t sz) {
|
||||
? snprintf(p, sizeof(p), "%s/HEAD", line + 8)
|
||||
: snprintf(p, sizeof(p), "%s/%s/HEAD", d, line + 8);
|
||||
if (n > 0 && (size_t)n < sizeof(p))
|
||||
f = fopen(p, "r");
|
||||
f = fopen(p, "rb");
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -2322,7 +2336,7 @@ static void draw_home(tui_t *t) {
|
||||
getmaxyx(t->logw, h, w);
|
||||
int box_top = t->in_y - t->log_y;
|
||||
int box_bot = box_top + t->in_h;
|
||||
if (g_ascii || !g_utf8) {
|
||||
if (g_ascii || !g_utf8 || g_logo_text) {
|
||||
int x = (w - 8) / 2, y = box_top - 2;
|
||||
if (y >= 0) {
|
||||
draw_ansi_at(t->logw, y, x, 3, "cst", CP_LOGO_DIM, 1, 0);
|
||||
@@ -2805,12 +2819,107 @@ static int input_visual_rows(const tui_t *t) {
|
||||
return rows;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
static DWORD g_con_in_mode;
|
||||
static int g_con_in_saved;
|
||||
|
||||
/* QuickEdit makes the console select text itself and swallow the clicks;
|
||||
* it is off while the TUI owns the mouse and restored on exit. */
|
||||
static void win_console_mouse(int on) {
|
||||
HANDLE h = GetStdHandle(STD_INPUT_HANDLE);
|
||||
DWORD m;
|
||||
if (!GetConsoleMode(h, &m))
|
||||
return;
|
||||
if (!g_con_in_saved) {
|
||||
g_con_in_mode = m;
|
||||
g_con_in_saved = 1;
|
||||
}
|
||||
if (on)
|
||||
m = (m & ~(DWORD)ENABLE_QUICK_EDIT_MODE) | ENABLE_MOUSE_INPUT |
|
||||
ENABLE_EXTENDED_FLAGS;
|
||||
else
|
||||
m = g_con_in_mode | ENABLE_EXTENDED_FLAGS;
|
||||
SetConsoleMode(h, m);
|
||||
}
|
||||
|
||||
static CONSOLE_FONT_INFOEX g_con_font;
|
||||
static int g_con_font_saved;
|
||||
|
||||
/* conhost has no glyph fallback: NSimSun, the default on Chinese systems,
|
||||
* draws U+2580 as an empty box. */
|
||||
static int win_font_has(const WCHAR *face, WCHAR ch) {
|
||||
HDC dc = CreateCompatibleDC(NULL);
|
||||
if (!dc)
|
||||
return 1;
|
||||
HFONT font = CreateFontW(16, 0, 0, 0, FW_NORMAL, 0, 0, 0, DEFAULT_CHARSET,
|
||||
0, 0, 0, FF_MODERN, face);
|
||||
WORD idx = 0;
|
||||
int ok = 1;
|
||||
if (font) {
|
||||
HGDIOBJ old = SelectObject(dc, font);
|
||||
if (GetGlyphIndicesW(dc, &ch, 1, &idx, GGI_MARK_NONEXISTING_GLYPHS) == 1)
|
||||
ok = idx != 0xffff;
|
||||
SelectObject(dc, old);
|
||||
DeleteObject(font);
|
||||
}
|
||||
DeleteDC(dc);
|
||||
return ok;
|
||||
}
|
||||
|
||||
/* Raster fonts cannot draw UTF-8 box drawing or CJK; switch the console to
|
||||
* a TrueType font of the same height while the TUI runs. */
|
||||
static void win_console_font(int on) {
|
||||
HANDLE h = GetStdHandle(STD_OUTPUT_HANDLE);
|
||||
if (!on) {
|
||||
if (g_con_font_saved)
|
||||
SetCurrentConsoleFontEx(h, FALSE, &g_con_font);
|
||||
g_con_font_saved = 0;
|
||||
return;
|
||||
}
|
||||
CONSOLE_FONT_INFOEX f = {.cbSize = sizeof f};
|
||||
if (!GetCurrentConsoleFontEx(h, FALSE, &f))
|
||||
return;
|
||||
if (!(f.FontFamily & TMPF_TRUETYPE)) {
|
||||
g_con_font = f;
|
||||
CONSOLE_FONT_INFOEX tt = {.cbSize = sizeof tt};
|
||||
tt.dwFontSize.Y = f.dwFontSize.Y > 0 ? f.dwFontSize.Y : 16;
|
||||
tt.FontFamily = FF_MODERN | TMPF_TRUETYPE;
|
||||
tt.FontWeight = FW_NORMAL;
|
||||
wcscpy(tt.FaceName, GetACP() == 936 ? L"NSimSun" : L"Consolas");
|
||||
if (!SetCurrentConsoleFontEx(h, FALSE, &tt))
|
||||
return;
|
||||
g_con_font_saved = 1;
|
||||
f = tt;
|
||||
}
|
||||
if (!getenv("WT_SESSION") && !win_font_has(f.FaceName, 0x2580))
|
||||
g_logo_text = 1;
|
||||
}
|
||||
#endif
|
||||
|
||||
#define MOUSE_RUN_MAX 16
|
||||
/* getmouse() hands out a run of queued events newest first (the Windows
|
||||
* console driver queues press, motion and release together); returns them
|
||||
* oldest first. */
|
||||
static int get_mouse_run(MEVENT *out, int max) {
|
||||
int n = 0;
|
||||
while (n < max && getmouse(&out[n]) == OK)
|
||||
n++;
|
||||
for (int i = 0; i < n / 2; i++) {
|
||||
MEVENT tmp = out[i];
|
||||
out[i] = out[n - 1 - i];
|
||||
out[n - 1 - i] = tmp;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
static void apply_mouse(const tui_t *t) {
|
||||
mousemask(t->mouse_on ? (mmask_t)(ALL_MOUSE_EVENTS | REPORT_MOUSE_POSITION)
|
||||
: 0,
|
||||
NULL);
|
||||
mouseinterval(0);
|
||||
#ifdef NCURSES_VERSION
|
||||
#ifdef _WIN32
|
||||
win_console_mouse(t->mouse_on);
|
||||
#elif defined(NCURSES_VERSION)
|
||||
/* ncurses only asks for presses and releases (1000). 1002 also reports
|
||||
* motion while a button is held, for drag selection; terminals treat it as
|
||||
* replacing 1000, so it goes after mousemask and is turned off explicitly. */
|
||||
@@ -2869,8 +2978,8 @@ static void layout(tui_t *t) {
|
||||
int top = (t->rows - 1 - block) / 2;
|
||||
t->in_y = clampi(top + LOGO_H + 1, 0, t->rows - 1 - t->in_h);
|
||||
t->log_x = t->log_y = 0;
|
||||
t->logw = newwin(t->rows - 1, t->cols, 0, 0);
|
||||
t->status = newwin(1, t->cols - 2, t->rows - 1, 1);
|
||||
t->logw = tui_newwin(t->rows - 1, t->cols, 0, 0);
|
||||
t->status = tui_newwin(1, t->cols - 2, t->rows - 1, 1);
|
||||
} else {
|
||||
int mw = t->cols - t->side_w;
|
||||
t->in_y = t->rows - 1 - t->in_h;
|
||||
@@ -2885,14 +2994,14 @@ static void layout(tui_t *t) {
|
||||
t->todo_rows = 0;
|
||||
log_h = t->in_y - t->log_y > 0 ? t->in_y - t->log_y : 1;
|
||||
}
|
||||
t->logw = newwin(log_h, mw - 2, t->log_y, 1);
|
||||
t->logw = tui_newwin(log_h, mw - 2, t->log_y, 1);
|
||||
if (t->todo_rows)
|
||||
t->todow = newwin(t->todo_rows, mw - 2, t->log_y + log_h, 1);
|
||||
t->status = newwin(1, mw - 2, t->rows - 1, 1);
|
||||
t->todow = tui_newwin(t->todo_rows, mw - 2, t->log_y + log_h, 1);
|
||||
t->status = tui_newwin(1, mw - 2, t->rows - 1, 1);
|
||||
if (t->side_w)
|
||||
t->side = newwin(t->rows, t->side_w, 0, mw);
|
||||
t->side = tui_newwin(t->rows, t->side_w, 0, mw);
|
||||
}
|
||||
t->input = newwin(t->in_h, t->in_w, t->in_y, t->in_x);
|
||||
t->input = tui_newwin(t->in_h, t->in_w, t->in_y, t->in_x);
|
||||
if (HAS_COLOR()) {
|
||||
wbkgdset(t->logw, COLOR_PAIR(CP_PANEL));
|
||||
wbkgdset(t->status, COLOR_PAIR(CP_PANEL));
|
||||
@@ -3495,7 +3604,7 @@ static int picker_run(tui_t *t, const char *title, char items[][128], int n,
|
||||
redraw_all(t); /* uncover what a larger dialog hid */
|
||||
last_dh = dh;
|
||||
last_dw = dw;
|
||||
dlg = newwin(dh, dw, dy, dx);
|
||||
dlg = tui_newwin(dh, dw, dy, dx);
|
||||
if (!dlg)
|
||||
break;
|
||||
keypad(dlg, TRUE);
|
||||
@@ -3549,9 +3658,14 @@ static int picker_run(tui_t *t, const char *title, char items[][128], int n,
|
||||
layout(t);
|
||||
redraw_all(t);
|
||||
} else if (wch == KEY_MOUSE) {
|
||||
MEVENT ev;
|
||||
if (getmouse(&ev) != OK)
|
||||
MEVENT evs[MOUSE_RUN_MAX], ev;
|
||||
int ne = get_mouse_run(evs, MOUSE_RUN_MAX), k0 = ne - 1;
|
||||
if (ne == 0)
|
||||
continue;
|
||||
for (int i = 0; i < ne; i++)
|
||||
if (evs[i].bstate & (BUTTON1_PRESSED | BUTTON1_CLICKED))
|
||||
k0 = i; /* a click anywhere in the run wins over its release */
|
||||
ev = evs[k0];
|
||||
if (ev.bstate & BUTTON4_PRESSED) {
|
||||
if (sel > 0)
|
||||
sel--;
|
||||
@@ -4301,6 +4415,34 @@ static void cmd_tools(tui_t *t, char *arg, int *quit) {
|
||||
}
|
||||
}
|
||||
|
||||
static void cmd_sandbox(tui_t *t, char *arg, int *quit) {
|
||||
(void)quit;
|
||||
if (arg && strcmp(arg, "reset-labels") == 0) {
|
||||
char *rep = NULL;
|
||||
int n = cst_sandbox_reset_labels(&rep);
|
||||
if (rep && rep[0])
|
||||
push_lines(t, " ", rep);
|
||||
push_system_line(t, n < 0 ? "(some labels could not be restored; "
|
||||
"they stay listed)"
|
||||
: n == 0 ? "(no labeled directories)"
|
||||
: "(labels restored)");
|
||||
free(rep);
|
||||
return;
|
||||
}
|
||||
int sk = cst_sandbox_probe();
|
||||
push_system_line(t, "Shell sandbox: %s",
|
||||
sk == CST_SANDBOX_NONE ? "none (rule-based approval)"
|
||||
: cst_sandbox_name(sk));
|
||||
char *l = cst_sandbox_labels();
|
||||
if (l) {
|
||||
push_system_line(t, "Labeled Low integrity (writable by sandboxed "
|
||||
"commands):");
|
||||
push_lines(t, " ", l);
|
||||
push_system_line(t, "/sandbox reset-labels restores their old labels");
|
||||
free(l);
|
||||
}
|
||||
}
|
||||
|
||||
static void cmd_permissions(tui_t *t, char *arg, int *quit) {
|
||||
(void)quit;
|
||||
cst_perms_t *p = cst_agent_perms(t->agent);
|
||||
@@ -4537,7 +4679,7 @@ static void cmd_export(tui_t *t, char *arg, int *quit) {
|
||||
*sl = 0;
|
||||
os_mkdir_p(dir, 0755);
|
||||
}
|
||||
FILE *fp = fopen(path, "w");
|
||||
FILE *fp = fopen(path, "wb");
|
||||
if (!fp) {
|
||||
push_system_line(t, "Error: cannot write %s", path);
|
||||
return;
|
||||
@@ -4569,9 +4711,19 @@ static void cmd_export(tui_t *t, char *arg, int *quit) {
|
||||
push_system_line(t, "(exported to %s)", path);
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
#define COPY_VIA "clipboard"
|
||||
#else
|
||||
#define COPY_VIA "OSC 52"
|
||||
#endif
|
||||
|
||||
/* Puts text on the terminal's clipboard; returns the bytes sent, 0 on
|
||||
* failure. */
|
||||
static size_t osc52_copy(const char *text, size_t n) {
|
||||
#ifdef _WIN32
|
||||
if (os_clipboard_set(text, n) == 0)
|
||||
return n; /* conhost has no OSC 52 */
|
||||
#endif
|
||||
static const char b64[] =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
if (n > 75000)
|
||||
@@ -4614,8 +4766,8 @@ static void cmd_copy(tui_t *t, char *arg, int *quit) {
|
||||
}
|
||||
size_t n = osc52_copy(text, strlen(text));
|
||||
if (n)
|
||||
push_system_line(t, "(copied %zu bytes of the last reply via OSC 52 — the "
|
||||
"terminal must allow clipboard access)", n);
|
||||
push_system_line(t, "(copied %zu bytes of the last reply via " COPY_VIA
|
||||
" — the terminal must allow clipboard access)", n);
|
||||
}
|
||||
|
||||
static void cmd_init(tui_t *t, char *arg, int *quit) {
|
||||
@@ -4945,6 +5097,8 @@ static const slash_cmd_t SLASH[] = {
|
||||
cmd_tools},
|
||||
{"permissions", "perms", "[reset]", "show saved always-allow rules", 1,
|
||||
cmd_permissions},
|
||||
{"sandbox", "", "[reset-labels]", "shell sandbox and labeled folders", 1,
|
||||
cmd_sandbox},
|
||||
{"approval", "", "[ask|auto|yolo]", "how tools get approved (Shift+Tab)",
|
||||
1, cmd_approval},
|
||||
{"yolo", "", "", "toggle approving everything", 1, cmd_yolo},
|
||||
@@ -5530,7 +5684,7 @@ static void sel_copy(tui_t *t) {
|
||||
for (size_t i = 0; i < b.n; i++)
|
||||
chars += ((unsigned char)b.s[i] & 0xC0) != 0x80;
|
||||
char msg[96];
|
||||
snprintf(msg, sizeof(msg), "Copied %zu characters (OSC 52)", chars);
|
||||
snprintf(msg, sizeof(msg), "Copied %zu characters (" COPY_VIA ")", chars);
|
||||
set_status(t, msg);
|
||||
draw_status(t);
|
||||
}
|
||||
@@ -6187,6 +6341,21 @@ static void insert_newline(tui_t *t) {
|
||||
}
|
||||
|
||||
/* Paste mode: keep newlines/tabs literally; redraw once at the end. */
|
||||
#ifdef _WIN32
|
||||
static int win_key_pending(void) {
|
||||
INPUT_RECORD rec[32];
|
||||
DWORD n = 0;
|
||||
HANDLE h = GetStdHandle(STD_INPUT_HANDLE);
|
||||
if (!PeekConsoleInputW(h, rec, 32, &n))
|
||||
return 0;
|
||||
for (DWORD i = 0; i < n; i++)
|
||||
if (rec[i].EventType == KEY_EVENT && rec[i].Event.KeyEvent.bKeyDown &&
|
||||
rec[i].Event.KeyEvent.uChar.UnicodeChar)
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
static void paste_char(tui_t *t, wint_t wch) {
|
||||
if (wch == 13) {
|
||||
input_put_byte(t, '\n');
|
||||
@@ -6207,20 +6376,31 @@ static void paste_char(tui_t *t, wint_t wch) {
|
||||
}
|
||||
|
||||
int cst_tui_run(cst_agent_t *agent) {
|
||||
#ifdef _WIN32
|
||||
setlocale(LC_ALL, ".UTF-8");
|
||||
#else
|
||||
if (!setlocale(LC_ALL, ""))
|
||||
setlocale(LC_ALL, "C.UTF-8");
|
||||
#endif
|
||||
#ifndef _WIN32
|
||||
if (!getenv("ESCDELAY"))
|
||||
setenv("ESCDELAY", "25", 0);
|
||||
#endif
|
||||
|
||||
#ifdef _WIN32
|
||||
win_console_font(1);
|
||||
#endif
|
||||
initscr();
|
||||
#ifdef _WIN32
|
||||
idcok(stdscr, FALSE);
|
||||
#endif
|
||||
raw(); /* Ctrl+C/Ctrl+Z reach us as keys instead of signals */
|
||||
noecho();
|
||||
keypad(stdscr, TRUE);
|
||||
curs_set(1);
|
||||
theme_init();
|
||||
#ifdef NCURSES_VERSION
|
||||
#if defined(NCURSES_VERSION) && !defined(_WIN32)
|
||||
/* the Windows console driver reads key events, not VT sequences */
|
||||
define_key("\033[200~", KEY_PASTE_BEGIN);
|
||||
define_key("\033[201~", KEY_PASTE_END);
|
||||
printf("\033[?2004h");
|
||||
@@ -6375,6 +6555,18 @@ int cst_tui_run(cst_agent_t *agent) {
|
||||
if (t.sel_state == SEL_SHOWN &&
|
||||
!(rc == KEY_CODE_YES && (wch == KEY_MOUSE || wch == KEY_RESIZE)))
|
||||
sel_clear(&t);
|
||||
#ifdef _WIN32
|
||||
/* The console has no bracketed paste: an Enter with more keys already
|
||||
* queued is a pasted line break, not a send. */
|
||||
if (rc == OK && !t.in_paste && (wch == 10 || wch == 13) &&
|
||||
win_key_pending()) {
|
||||
paste_char(&t, wch);
|
||||
input_changed(&t);
|
||||
after_edit(&t);
|
||||
draw_input(&t);
|
||||
continue;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!t.in_paste &&
|
||||
!(rc == KEY_CODE_YES &&
|
||||
@@ -6453,12 +6645,13 @@ int cst_tui_run(cst_agent_t *agent) {
|
||||
continue;
|
||||
}
|
||||
if (wch == KEY_MOUSE) {
|
||||
MEVENT ev;
|
||||
if (getmouse(&ev) == OK) {
|
||||
if (on_mouse(&t, &ev, busy) == -2)
|
||||
MEVENT evs[MOUSE_RUN_MAX];
|
||||
int ne = get_mouse_run(evs, MOUSE_RUN_MAX);
|
||||
for (int i = 0; i < ne; i++)
|
||||
if (on_mouse(&t, &evs[i], busy) == -2)
|
||||
goto out;
|
||||
if (ne)
|
||||
draw_input(&t); /* keep the terminal cursor in the box */
|
||||
}
|
||||
continue;
|
||||
}
|
||||
int wdir;
|
||||
@@ -6667,7 +6860,11 @@ out:
|
||||
usleep(100000); /* let a /models probe finish before curl cleanup */
|
||||
|
||||
tui_free(&t);
|
||||
#ifdef NCURSES_VERSION
|
||||
#ifdef _WIN32
|
||||
if (g_con_in_saved)
|
||||
win_console_mouse(0);
|
||||
win_console_font(0);
|
||||
#elif defined(NCURSES_VERSION)
|
||||
kitty_restore();
|
||||
printf("\033[?1002l\033[?2004l");
|
||||
fflush(stdout);
|
||||
|
||||
+24
-42
@@ -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
|
||||
|
||||
+209
@@ -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++) {
|
||||
|
||||
Reference in New Issue
Block a user