Files
Hermit/src/tools/tools.c
T
WHC 50abcf1a91
ci / tcc-check (push) Successful in 1m9s
ci / linux (aarch64, 0) (push) Failing after 58m44s
ci / linux (x86, 1) (push) Successful in 13m47s
ci / linux (x86_64, 1) (push) Failing after 1h0m2s
ci / windows (push) Successful in 10m46s
build: finish cross-platform release tooling
2026-10-10 16:21:00 +08:00

904 lines
26 KiB
C

#ifndef _DEFAULT_SOURCE
#define _DEFAULT_SOURCE
#endif
#include <errno.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "cJSON.h"
#include "cst_agent.h"
#include "cst_attach.h"
#include "cst_sandbox.h"
#include "cst_util.h"
#include "os_compat.h"
#include "tools_internal.h"
#define SHELL_OUT_MAX (512 * 1024)
#define DIFF_OUT_CAP (48 * 1024)
/* --- helpers ------------------------------------------------------------- */
void cst_tool_result_free(cst_tool_result_t *r) {
if (!r)
return;
free(r->out);
r->out = NULL;
r->out_len = 0;
cst_attachments_free(r->attach, r->nattach);
r->attach = NULL;
r->nattach = 0;
}
void cst_tool_call_clear(ctl_tool_call_t *c) {
if (!c)
return;
for (int i = 0; i < c->nargs; i++) {
free(c->args[i].value);
c->args[i].value = NULL;
}
c->nargs = 0;
}
const char *tool_arg_n(const ctl_tool_call_t *call, const char *key,
size_t *len) {
for (int i = 0; i < call->nargs; i++) {
if (strcmp(call->args[i].key, key) == 0) {
if (len)
*len = call->args[i].value ? call->args[i].value_len : 0;
return call->args[i].value ? call->args[i].value : "";
}
}
if (len)
*len = 0;
return NULL;
}
const char *tool_arg(const ctl_tool_call_t *call, const char *key) {
return tool_arg_n(call, key, NULL);
}
long tool_arg_long(const ctl_tool_call_t *call, const char *key, long def) {
const char *v = tool_arg(call, key);
if (!v)
return def;
while (*v == ' ' || *v == '"')
v++;
if (!*v)
return def;
char *end = NULL;
long x = strtol(v, &end, 10);
return end == v ? def : x;
}
int tool_arg_bool(const ctl_tool_call_t *call, const char *key, int def) {
const char *v = tool_arg(call, key);
if (!v || !*v)
return def;
while (*v == ' ' || *v == '"')
v++;
if (*v == 't' || *v == 'T' || *v == 'y' || *v == 'Y' || *v == '1')
return 1;
if (*v == 'f' || *v == 'F' || *v == 'n' || *v == 'N' || *v == '0')
return 0;
return def;
}
int tool_set_result(cst_tool_result_t *r, const char *s, int code) {
cst_tool_result_free(r);
size_t n = s ? strlen(s) : 0;
r->out = malloc(n + 1);
if (!r->out)
return -1;
if (n)
memcpy(r->out, s, n);
r->out[n] = 0;
r->out_len = n;
r->exit_code = code;
return 0;
}
int tool_set_resultf(cst_tool_result_t *r, int code, const char *fmt, ...) {
char msg[1024];
va_list ap;
va_start(ap, fmt);
vsnprintf(msg, sizeof(msg), fmt, ap);
va_end(ap);
return tool_set_result(r, msg, code);
}
int tool_take_buf(cst_tool_result_t *r, cst_buf_t *b, int code) {
cst_tool_result_free(r);
r->out_len = b->len;
r->out = cst_buf_steal(b);
r->exit_code = code;
return r->out ? 0 : -1;
}
char *tool_slurp(const char *full, size_t *out_len, size_t max) {
*out_len = 0;
FILE *fp = fopen(full, "rb");
if (!fp)
return NULL;
if (fseek(fp, 0, SEEK_END) != 0) {
fclose(fp);
return NULL;
}
long sz = ftell(fp);
if (sz < 0 || (max && (size_t)sz > max)) {
fclose(fp);
return NULL;
}
rewind(fp);
char *buf = malloc((size_t)sz + 1);
if (!buf) {
fclose(fp);
return NULL;
}
size_t n = fread(buf, 1, (size_t)sz, fp);
fclose(fp);
buf[n] = 0;
*out_len = n;
return buf;
}
int tool_write_whole(const char *full, const char *data, size_t n) {
FILE *fp = fopen(full, "wb");
if (!fp)
return -1;
if (n && fwrite(data, 1, n, fp) != n) {
fclose(fp);
return -1;
}
return fclose(fp) == 0 ? 0 : -1;
}
int tool_is_binary(const char *s, size_t n) {
size_t lim = n < 8192 ? n : 8192;
return s && memchr(s, 0, lim) != NULL;
}
const char *tool_rel(const char *workspace_real, const char *full) {
size_t n = strlen(workspace_real);
if (strncmp(full, workspace_real, n) == 0) {
if (full[n] == '/')
return full + n + 1;
if (full[n] == 0)
return ".";
}
return full;
}
void tool_append_diff(cst_buf_t *b, const char *relpath, const char *oldc,
size_t olen, const char *newc, size_t nlen) {
if (tool_is_binary(oldc, olen) || tool_is_binary(newc, nlen)) {
cst_buf_puts(b, "(binary content — no textual diff)\n");
return;
}
char *diff = cst_diff_unified(relpath && relpath[0] ? relpath : "file",
oldc ? oldc : "", olen, newc ? newc : "",
nlen);
if (!diff) {
cst_buf_puts(b, "(no textual diff — out of memory)\n");
return;
}
if (!diff[0]) {
cst_buf_puts(b, "(no textual diff — identical)\n");
} else if (strlen(diff) > DIFF_OUT_CAP) {
size_t keep = cst_utf8_floor(diff, DIFF_OUT_CAP);
cst_buf_append(b, diff, keep);
cst_buf_puts(b, "\n...(diff truncated)\n");
} else {
cst_buf_puts(b, diff);
}
free(diff);
}
/* Create missing parent directories of a workspace-relative path. */
void tool_ensure_parent_dirs(const char *workspace_root, const char *rel) {
if (!rel || !rel[0] || os_path_is_abs(rel))
return;
char root_real[OS_PATH_MAX];
if (os_realpath(workspace_root, root_real, sizeof(root_real)) != 0)
return;
os_path_norm_slashes(root_real);
char tmp[OS_PATH_MAX];
snprintf(tmp, sizeof(tmp), "%s", rel);
os_path_norm_slashes(tmp);
if (strstr(tmp, "..") != NULL)
return;
for (char *p = tmp; *p; p++) {
if (*p != '/' || p == tmp)
continue;
*p = 0;
char dir[OS_PATH_MAX];
if (os_path_join(dir, sizeof(dir), root_real, tmp) == 0) {
os_mkdir(dir, 0755);
char real[OS_PATH_MAX];
if (os_realpath(dir, real, sizeof(real)) != 0)
return;
os_path_norm_slashes(real);
if (!os_path_under(root_real, real))
return; /* symlink pointing outside: stop creating */
}
*p = '/';
}
}
/* --- read_file / write_file / run_shell ---------------------------------- */
int cst_tool_read_file(const char *workspace_root, const ctl_tool_call_t *call,
cst_tool_result_t *r) {
cst_tool_ctx_t ctx = {0};
ctx.workspace_root = workspace_root;
return cst_tool_read_file2(&ctx, call, r);
}
static int write_content(const ctl_tool_call_t *call, const char **content,
size_t *n) {
*content = tool_arg_n(call, "content", n);
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");
const char *content;
size_t n;
if (!path || !path[0] || write_content(c, &content, &n) != 0) {
*out = strdup("error: missing path or content");
return -1;
}
char full[OS_PATH_MAX];
size_t olen = 0;
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;
}
int cst_tool_write_file(const char *workspace_root, const ctl_tool_call_t *call,
cst_tool_result_t *r) {
const char *path = tool_arg(call, "path");
const char *content;
size_t n;
if (!path || !path[0] || write_content(call, &content, &n) != 0)
return tool_set_result(r, "error: missing path or content", 1);
tool_ensure_parent_dirs(workspace_root, path);
char full[OS_PATH_MAX];
if (cst_sandbox_resolve(workspace_root, path, full, sizeof(full)) != 0)
return tool_set_resultf(r, 1, "error: path outside workspace: %.200s",
path);
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);
}
int cst_tool_run_shell(const char *workspace_root, const ctl_tool_call_t *call,
cst_tool_result_t *r) {
return cst_tool_run_shell_ex(workspace_root, call, r, 0);
}
int cst_tool_run_shell_ex(const char *workspace_root,
const ctl_tool_call_t *call, cst_tool_result_t *r,
int sandboxed) {
const char *cmd = tool_arg(call, "cmd");
if (!cmd)
cmd = tool_arg(call, "command");
if (!cmd || !cmd[0])
return tool_set_result(r, "error: missing cmd", 1);
char root_real[OS_PATH_MAX];
if (os_realpath(workspace_root, root_real, sizeof(root_real)) != 0)
return tool_set_result(r, "error: bad workspace", 1);
os_run_result_t rr = {0};
int rc = sandboxed
? cst_sandbox_run_shell(root_real, cmd, SHELL_OUT_MAX, &rr)
: os_run_shell_c(root_real, cmd, SHELL_OUT_MAX, &rr);
if (rc != 0) {
os_run_result_free(&rr);
return tool_set_result(r, "error: shell exec failed", 1);
}
cst_tool_result_free(r);
r->out = rr.out;
r->out_len = rr.out_len;
r->exit_code = rr.exit_code;
rr.out = NULL;
if (!r->out)
tool_set_result(r, "", rr.exit_code);
return 0;
}
#define SHC_LINE_MAX 2000
#define SHC_HEAD 80
#define SHC_TAIL 120
#define SHC_BYTES_MAX (40 * 1024)
/* One output line: escapes removed and only what follows the last lone
* `\r` kept (what a terminal would finally show). */
static void shc_clean_line(const char *s, size_t n, cst_buf_t *out) {
size_t start = out->len;
for (size_t i = 0; i < n; i++) {
unsigned char ch = (unsigned char)s[i];
if (ch == 0x1b && i + 1 < n) {
unsigned char k = (unsigned char)s[i + 1];
if (k == '[') {
i += 2;
while (i < n && ((unsigned char)s[i] < 0x40 || (unsigned char)s[i] > 0x7e))
i++;
} else if (k == ']') {
i += 2;
while (i < n && s[i] != 0x07 &&
!(s[i] == 0x1b && i + 1 < n && s[i + 1] == '\\'))
i++;
if (i < n && s[i] == 0x1b)
i++;
} else {
i++;
}
continue;
}
if (ch == '\r') {
if (i + 1 < n)
out->len = start;
continue;
}
if (ch == '\b') {
if (out->len > start)
out->len--;
continue;
}
if (ch < 0x20 && ch != '\t')
continue;
cst_buf_append(out, (const char *)&s[i], 1);
}
}
typedef struct {
size_t off, len;
} shc_line_t;
static void shc_put(cst_buf_t *b, const char *s, size_t n) {
if (n > SHC_LINE_MAX) {
size_t keep = cst_utf8_floor(s, SHC_LINE_MAX);
cst_buf_append(b, s, keep);
cst_buf_printf(b, " … [+%zu bytes]\n", n - keep);
return;
}
cst_buf_append(b, s, n);
cst_buf_puts(b, "\n");
}
char *cst_shell_compact(const char *s, size_t n, size_t *out_len) {
cst_buf_t clean = {0};
shc_line_t *lines = NULL;
size_t nl = 0, cap = 0;
for (size_t i = 0; i < n;) {
const char *e = memchr(s + i, '\n', n - i);
size_t end = e ? (size_t)(e - s) : n;
size_t ln = end - i;
if (ln && s[i + ln - 1] == '\r')
ln--;
if (nl == cap) {
cap = cap ? cap * 2 : 256;
shc_line_t *nv = realloc(lines, cap * sizeof(*nv));
if (!nv)
break;
lines = nv;
}
size_t off = clean.len;
shc_clean_line(s + i, ln, &clean);
lines[nl].off = off;
lines[nl].len = clean.len - off;
nl++;
i = e ? end + 1 : n;
}
/* Collapse runs of identical lines: keep one plus a count. */
size_t m = 0;
int *rep = calloc(nl ? nl : 1, sizeof(int));
for (size_t i = 0; rep && i < nl;) {
size_t j = i + 1;
while (j < nl && lines[j].len == lines[i].len &&
memcmp(clean.buf + lines[j].off, clean.buf + lines[i].off,
lines[i].len) == 0)
j++;
lines[m] = lines[i];
rep[m] = (int)(j - i);
m++;
if (j - i == 2) {
lines[m] = lines[i];
rep[m - 1] = rep[m] = 1;
m++;
}
i = j;
}
if (!rep)
m = nl;
size_t total = 0;
for (size_t i = 0; i < m; i++)
total += (lines[i].len < SHC_LINE_MAX ? lines[i].len : SHC_LINE_MAX) + 1;
int cut = m > SHC_HEAD + SHC_TAIL + 20 || total > SHC_BYTES_MAX;
size_t head = m, tail_from = m;
if (cut) {
head = m < SHC_HEAD ? m : SHC_HEAD;
tail_from = m > SHC_TAIL ? m - SHC_TAIL : 0;
if (tail_from < head)
tail_from = head;
/* byte budget: shrink the head first, keep the tail (errors live there) */
size_t tb = 0;
for (size_t i = tail_from; i < m; i++)
tb += (lines[i].len < SHC_LINE_MAX ? lines[i].len : SHC_LINE_MAX) + 1;
while (tb > SHC_BYTES_MAX * 3 / 4 && tail_from + 1 < m) {
tb -= (lines[tail_from].len < SHC_LINE_MAX ? lines[tail_from].len
: SHC_LINE_MAX) + 1;
tail_from++;
}
size_t hb = 0, h = 0;
while (h < head) {
size_t l = (lines[h].len < SHC_LINE_MAX ? lines[h].len : SHC_LINE_MAX) + 1;
if (hb + l + tb > SHC_BYTES_MAX)
break;
hb += l;
h++;
}
head = h;
}
cst_buf_t b = {0};
for (size_t i = 0; i < m; i++) {
if (cut && i == head && head < tail_from) {
size_t omitted = 0;
for (size_t k = head; k < tail_from; k++)
omitted += rep ? (size_t)rep[k] : 1;
cst_buf_printf(&b, "… %zu lines omitted …\n", omitted);
i = tail_from;
if (i >= m)
break;
}
shc_put(&b, clean.buf ? clean.buf + lines[i].off : "", lines[i].len);
if (rep && rep[i] > 2)
cst_buf_printf(&b, "… (previous line repeated %d more times)\n",
rep[i] - 1);
}
/* Keep "no trailing newline" outputs that way. */
if (n && s[n - 1] != '\n' && b.len && b.buf[b.len - 1] == '\n')
b.len--;
if (!b.buf)
cst_buf_puts(&b, "");
else
b.buf[b.len] = 0;
free(rep);
free(lines);
cst_buf_free(&clean);
*out_len = b.len;
return cst_buf_steal(&b);
}
/* --- apply_patch --------------------------------------------------------- */
typedef struct {
char old_path[1024];
char new_path[1024];
const char *body; /* first line after +++ */
size_t body_len;
} patch_file_t;
static void header_path(const char *p, size_t n, char *out, size_t out_sz) {
while (n > 0 && (*p == ' ' || *p == '\t')) {
p++;
n--;
}
/* strip trailing timestamp ("\t2024-...") and CR */
for (size_t i = 0; i < n; i++) {
if (p[i] == '\t' || p[i] == '\r') {
n = i;
break;
}
}
while (n > 0 && p[n - 1] == ' ')
n--;
if (n >= 2 && (p[0] == 'a' || p[0] == 'b') && p[1] == '/') {
p += 2;
n -= 2;
}
if (n >= out_sz)
n = out_sz - 1;
memcpy(out, p, n);
out[n] = 0;
}
static const char *next_line(const char *p, const char *end) {
const char *nl = memchr(p, '\n', (size_t)(end - p));
return nl ? nl + 1 : end;
}
static int is_header_at(const char *p, const char *end) {
if (end - p < 4 || memcmp(p, "--- ", 4) != 0)
return 0;
const char *q = next_line(p, end);
return end - q >= 4 && memcmp(q, "+++ ", 4) == 0;
}
/* Split a multi-file unified diff. Returns number of files or -1. */
static int split_patch(const char *patch, size_t plen, patch_file_t *files,
int max_files) {
const char *p = patch, *end = patch + plen;
int n = 0;
while (p < end) {
if (!is_header_at(p, end)) {
p = next_line(p, end);
continue;
}
if (n >= max_files)
return -1;
patch_file_t *f = &files[n];
const char *l1 = p, *l2 = next_line(p, end);
const char *l1e = memchr(l1, '\n', (size_t)(end - l1));
const char *l2e = memchr(l2, '\n', (size_t)(end - l2));
if (!l1e)
l1e = end;
if (!l2e)
l2e = end;
header_path(l1 + 4, (size_t)(l1e - l1 - 4), f->old_path,
sizeof(f->old_path));
header_path(l2 + 4, (size_t)(l2e - l2 - 4), f->new_path,
sizeof(f->new_path));
f->body = next_line(l2, end);
const char *q = f->body;
while (q < end && !is_header_at(q, end) &&
!(end - q >= 11 && memcmp(q, "diff --git ", 11) == 0))
q = next_line(q, end);
f->body_len = (size_t)(q - f->body);
n++;
p = q;
}
return n;
}
#define PATCH_MAX_FILES 16
typedef struct {
int nf;
patch_file_t files[PATCH_MAX_FILES];
char *full[PATCH_MAX_FILES];
char *oldc[PATCH_MAX_FILES];
size_t olen[PATCH_MAX_FILES];
char *newc[PATCH_MAX_FILES];
size_t nlen[PATCH_MAX_FILES];
int del[PATCH_MAX_FILES];
int nh[PATCH_MAX_FILES];
} patch_plan_t;
static void plan_free(patch_plan_t *pl) {
for (int i = 0; i < pl->nf; i++) {
free(pl->full[i]);
free(pl->oldc[i]);
free(pl->newc[i]);
}
}
static const char *plan_target(const patch_plan_t *pl, int i) {
return pl->del[i] ? pl->files[i].old_path : pl->files[i].new_path;
}
/* Phase 1: compute every new file in memory. mkdirs only when !dry. */
static int plan_patch(const char *ws, const char *patch, patch_plan_t *pl,
int dry, char *err, size_t err_sz) {
memset(pl, 0, sizeof(*pl));
int nf = split_patch(patch, strlen(patch), pl->files, PATCH_MAX_FILES);
if (nf < 0) {
snprintf(err, err_sz, "error: too many files in one patch");
return -1;
}
if (nf == 0) {
snprintf(err, err_sz,
"error: missing ---/+++ file header (use a unified diff: "
"--- a/path, +++ b/path, then @@ -a,b +c,d @@ hunks)");
return -1;
}
pl->nf = nf;
for (int i = 0; i < nf; i++) {
patch_file_t *f = &pl->files[i];
int is_new = strcmp(f->old_path, "/dev/null") == 0;
pl->del[i] = strcmp(f->new_path, "/dev/null") == 0;
const char *target = plan_target(pl, i);
if (!target[0]) {
snprintf(err, err_sz, "error: empty path in file header");
return -1;
}
if (!pl->del[i] && !dry)
tool_ensure_parent_dirs(ws, target);
pl->full[i] = malloc(OS_PATH_MAX);
if (!pl->full[i] ||
cst_sandbox_resolve(ws, target, pl->full[i], OS_PATH_MAX) != 0) {
if (dry && pl->full[i] && !pl->del[i] && is_new &&
strlen(target) < OS_PATH_MAX) {
/* parent dirs do not exist yet; the write would create them */
memcpy(pl->full[i], target, strlen(target) + 1);
} else {
snprintf(err, err_sz, "error: path outside workspace: %.200s",
target);
return -1;
}
}
if (!is_new) {
pl->oldc[i] = tool_slurp(pl->full[i], &pl->olen[i], 0);
if (!pl->oldc[i]) {
snprintf(err, err_sz,
"error: %.200s does not exist (use --- /dev/null for new "
"files)",
target);
return -1;
}
}
if (pl->del[i])
continue;
char perr[400];
if (cst_patch_apply_buffer(pl->oldc[i] ? pl->oldc[i] : "", pl->olen[i],
f->body, f->body_len, &pl->newc[i],
&pl->nlen[i], &pl->nh[i], perr,
sizeof(perr)) != 0) {
snprintf(err, err_sz, "error: %.200s: %.280s (nothing was written)",
target, perr);
return -1;
}
}
return 0;
}
static void update_file_call(ctl_tool_call_t *fake, const char *patch,
const char *nl, char *path, size_t path_sz) {
size_t plen = (size_t)(nl - (patch + 17));
if (plen >= path_sz)
plen = path_sz - 1;
memcpy(path, patch + 17, plen);
path[plen] = 0;
memset(fake, 0, sizeof(*fake));
snprintf(fake->name, sizeof(fake->name), "write_file");
fake->nargs = 2;
snprintf(fake->args[0].key, sizeof(fake->args[0].key), "path");
fake->args[0].value = path;
fake->args[0].value_len = strlen(path);
snprintf(fake->args[1].key, sizeof(fake->args[1].key), "content");
fake->args[1].value = (char *)(nl + 1);
fake->args[1].value_len = strlen(nl + 1);
}
int cst_preview_apply_patch(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
char **out) {
const char *patch = tool_arg(c, "patch");
if (!patch || !patch[0]) {
*out = strdup("error: missing patch");
return -1;
}
if (strncmp(patch, "*** Update File: ", 17) == 0) {
const char *nl = strchr(patch, '\n');
if (!nl) {
*out = strdup("error: incomplete Update File header");
return -1;
}
ctl_tool_call_t fake;
char path[1024];
update_file_call(&fake, patch, nl, path, sizeof(path));
return cst_preview_write_file(ctx, &fake, out);
}
patch_plan_t *pl = malloc(sizeof(*pl));
if (!pl) {
*out = strdup("error: oom");
return -1;
}
char err[512];
if (plan_patch(ctx->workspace_root, patch, pl, 1, err, sizeof(err)) != 0) {
plan_free(pl);
free(pl);
*out = strdup(err);
return -1;
}
cst_buf_t b = {0};
for (int i = 0; i < pl->nf; i++) {
const char *target = plan_target(pl, i);
if (pl->del[i]) {
cst_buf_printf(&b, "delete %s\n", target);
continue;
}
tool_append_diff(&b, target, pl->oldc[i], pl->olen[i], pl->newc[i],
pl->nlen[i]);
}
plan_free(pl);
free(pl);
*out = cst_buf_steal(&b);
return 0;
}
int cst_tool_apply_patch(const char *workspace_root, const ctl_tool_call_t *call,
cst_tool_result_t *r) {
const char *patch = tool_arg(call, "patch");
if (!patch || !patch[0])
return tool_set_result(r, "error: missing patch", 1);
/* Full-file replace shorthand: "*** Update File: <path>\n<content>" */
if (strncmp(patch, "*** Update File: ", 17) == 0) {
const char *nl = strchr(patch, '\n');
if (!nl)
return tool_set_result(r, "error: incomplete Update File header", 1);
if ((size_t)(nl - (patch + 17)) >= 1024)
return tool_set_result(r, "error: path too long", 1);
ctl_tool_call_t fake;
char path[1024];
update_file_call(&fake, patch, nl, path, sizeof(path));
return cst_tool_write_file(workspace_root, &fake, r);
}
patch_plan_t *pl = malloc(sizeof(*pl));
if (!pl)
return tool_set_result(r, "error: oom", 1);
char err[512];
err[0] = 0;
int rc = plan_patch(workspace_root, patch, pl, 0, err, sizeof(err));
/* Phase 2: write everything. */
cst_buf_t out = {0};
if (rc == 0) {
for (int i = 0; i < pl->nf; i++) {
const char *target = plan_target(pl, i);
if (pl->del[i]) {
if (remove(pl->full[i]) != 0) {
snprintf(err, sizeof(err), "error: cannot delete %.200s", target);
rc = -1;
break;
}
cst_buf_printf(&out, "deleted %s\n", target);
continue;
}
if (tool_write_whole(pl->full[i], pl->newc[i], pl->nlen[i]) != 0) {
snprintf(err, sizeof(err), "error: cannot write %.200s", target);
rc = -1;
break;
}
cst_buf_printf(&out, "applied patch → %s (%d hunk%s)\n", target,
pl->nh[i], pl->nh[i] == 1 ? "" : "s");
tool_append_diff(&out, target, pl->oldc[i], pl->olen[i], pl->newc[i],
pl->nlen[i]);
}
}
plan_free(pl);
free(pl);
if (rc != 0) {
cst_buf_free(&out);
return tool_set_result(r, err, 1);
}
return tool_take_buf(r, &out, 0);
}
/* --- native arguments ---------------------------------------------------- */
int cst_tool_call_from_native(ctl_tool_call_t *out, const char *id,
const char *name, const char *args_json) {
if (!out || !name || !name[0])
return -1;
memset(out, 0, sizeof(*out));
if (id)
snprintf(out->id, sizeof(out->id), "%s", id);
snprintf(out->name, sizeof(out->name), "%s", name);
if (!args_json)
return -1;
const char *p = args_json;
while (*p == ' ' || *p == '\n' || *p == '\r' || *p == '\t')
p++;
if (!*p)
return 0; /* "" == no arguments */
cJSON *root = cJSON_Parse(args_json);
if (!root || !cJSON_IsObject(root)) {
cJSON_Delete(root);
return -1;
}
cJSON *it = NULL;
cJSON_ArrayForEach(it, root) {
if (out->nargs >= CTL_MAX_ARGS || !it->string ||
strlen(it->string) >= CTL_MAX_KEY)
continue;
char *val = NULL;
if (cJSON_IsString(it))
val = strdup(it->valuestring ? it->valuestring : "");
else if (cJSON_IsNull(it))
continue;
else
val = cJSON_PrintUnformatted(it);
if (!val)
continue;
ctl_arg_t *a = &out->args[out->nargs++];
snprintf(a->key, sizeof(a->key), "%s", it->string);
a->value = val;
a->value_len = strlen(val);
}
cJSON_Delete(root);
return 0;
}
/* Paths a write call may change, for /undo snapshots. */
int cst_tool_targets(const ctl_tool_call_t *call, char paths[][1024],
int max) {
int n = 0;
static const char *const keys[] = {"path", "from", "to"};
if (strcmp(call->name, "apply_patch") != 0) {
for (size_t k = 0; k < sizeof(keys) / sizeof(keys[0]) && n < max; k++) {
const char *v = tool_arg(call, keys[k]);
if (v && v[0])
snprintf(paths[n++], 1024, "%s", v);
}
return n;
}
const char *patch = tool_arg(call, "patch");
if (!patch)
return 0;
if (strncmp(patch, "*** Update File: ", 17) == 0) {
const char *nl = strchr(patch, '\n');
size_t len = nl ? (size_t)(nl - patch - 17) : strlen(patch + 17);
if (len >= 1024 || max < 1)
return 0;
memcpy(paths[0], patch + 17, len);
paths[0][len] = 0;
return 1;
}
patch_file_t files[PATCH_MAX_FILES];
int nf = split_patch(patch, strlen(patch), files, PATCH_MAX_FILES);
for (int i = 0; i < nf && n < max; i++) {
if (strcmp(files[i].old_path, "/dev/null") != 0 &&
snprintf(paths[n], 1024, "%s", files[i].old_path) < 1024)
n++;
if (n < max && strcmp(files[i].new_path, "/dev/null") != 0 &&
strcmp(files[i].new_path, files[i].old_path) != 0 &&
snprintf(paths[n], 1024, "%s", files[i].new_path) < 1024)
n++;
}
return n;
}