Images - vision gate: text-only models get a note instead of image parts and no view_image tool; unknown models retry once without images on HTTP 400 - paste: Ctrl+V and /attach paste read the clipboard (wl-paste, xclip, pngpaste/osascript, Win32 PNG or CF_DIB converted to PNG); pasting image file paths attaches them - MCP image content is stored in the session and sent with the tool result - attach: rel path validation, base64 decode, /retry keeps images Model registry - built-in src/models_builtin.json embedded at build time, merged with <config>/models.json and cached GET /models metadata (context, vision, pricing); most specific rule wins per field - /model labels, /status source, --list-models [provider] Cache and cost - stable request prefix (frozen tools, notes as messages), DeepSeek prefix completion, OpenAI/Anthropic cache hints, /cost and hit rate in the status
903 lines
26 KiB
C
903 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) {
|
|
/* parent dirs do not exist yet; the write would create them */
|
|
snprintf(pl->full[i], OS_PATH_MAX, "%s", target);
|
|
} 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;
|
|
}
|