#ifndef _DEFAULT_SOURCE #define _DEFAULT_SOURCE #endif #include #include #include #include #include #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: \n" */ 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; }