#ifndef _DEFAULT_SOURCE #define _DEFAULT_SOURCE #endif #ifndef _XOPEN_SOURCE #define _XOPEN_SOURCE 700 #endif #include #include #include #include #include #include #ifndef _WIN32 #include #include #include #include #endif #include "cst_tools.h" #include "cst_util.h" #include "os_compat.h" #include "tools_internal.h" #define EDIT_FILE_MAX (8 * 1024 * 1024) #define RANGE_OUT_MAX (256 * 1024) #define RANGE_DEFAULT_LINES 2000 #define LIST_MAX 500 #define GLOB_MAX 200 #define GREP_MAX_MATCHES 200 #define GREP_MAX_FILES 20000 #define GREP_LINE_MAX 300 static int resolve(const cst_tool_ctx_t *ctx, const char *path, char *full, size_t n) { return cst_sandbox_resolve(ctx->workspace_root, path, full, n); } static int is_dir(const char *full) { struct stat st; return stat(full, &st) == 0 && S_ISDIR(st.st_mode); } static int is_regular(const char *full) { struct stat st; return stat(full, &st) == 0 && S_ISREG(st.st_mode); } /* --- read_file ----------------------------------------------------------- */ #define READ_LINE_MAX 2000 /* Default-profile read: "path · lines a-b of N" header, `N|` numbers, * long lines cut, and the next offset when more remains. */ static int read_compact(FILE *fp, const char *path, long offset, long limit, int numbers, cst_tool_result_t *r) { if (offset <= 0) offset = 1; if (limit <= 0) limit = RANGE_DEFAULT_LINES; cst_buf_t b = {0}, line = {0}; long lineno = 0, shown = 0, cut_lines = 0; int full = 0, binary = 0, ch; for (;;) { line.len = 0; int got = 0, cr = 0; size_t len = 0; while ((ch = fgetc(fp)) != EOF) { got = 1; if (ch == 0) binary = 1; if (ch == '\n') break; len++; cr = ch == '\r'; if (line.len < READ_LINE_MAX + 8) { char cc = (char)ch; cst_buf_append(&line, &cc, 1); } } if (!got || binary) break; lineno++; if (lineno < offset || shown >= limit || full) continue; len -= (size_t)cr; if (b.len + (len < READ_LINE_MAX ? len : READ_LINE_MAX) > RANGE_OUT_MAX) { full = 1; continue; } if (numbers) cst_buf_printf(&b, "%ld|", lineno); if (len > READ_LINE_MAX) { size_t keep = cst_utf8_floor(line.buf, READ_LINE_MAX); cst_buf_append(&b, line.buf, keep); cst_buf_printf(&b, " … [line cut, +%zu bytes]\n", len - keep); cut_lines++; } else { if (len) cst_buf_append(&b, line.buf, len); cst_buf_puts(&b, "\n"); } shown++; } cst_buf_free(&line); if (binary) { cst_buf_free(&b); return tool_set_resultf(r, 1, "error: %.200s is a binary file; inspect it with " "run_shell (file, xxd | head)", path); } long last = offset + shown - 1; cst_buf_t out = {0}; if (lineno == 0) cst_buf_printf(&out, "%s · empty file\n", path); else if (shown == 0) cst_buf_printf(&out, "%s · %ld line%s; offset %ld is past the end\n", path, lineno, lineno == 1 ? "" : "s", offset); else if (offset == 1 && last == lineno) cst_buf_printf(&out, "%s · %ld line%s\n", path, lineno, lineno == 1 ? "" : "s"); else cst_buf_printf(&out, "%s · lines %ld-%ld of %ld\n", path, offset, last, lineno); if (b.buf) cst_buf_append(&out, b.buf, b.len); cst_buf_free(&b); if (shown && last < lineno) cst_buf_printf(&out, "… %ld more line%s; continue with offset=%ld\n", lineno - last, lineno - last == 1 ? "" : "s", last + 1); if (cut_lines) cst_buf_printf(&out, "(%ld long line%s cut at %d bytes)\n", cut_lines, cut_lines == 1 ? "" : "s", READ_LINE_MAX); return tool_take_buf(r, &out, 0); } int cst_tool_read_file2(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c, cst_tool_result_t *r) { const char *path = tool_arg(c, "path"); if (!path || !path[0]) return tool_set_result(r, "error: missing path", 1); char full[OS_PATH_MAX]; if (resolve(ctx, path, full, sizeof(full)) != 0) return tool_set_resultf(r, 1, "error: path outside workspace or missing parent: " "%.200s", path); if (is_dir(full)) return tool_set_resultf(r, 1, "error: %.200s is a directory (use list_dir)", path); FILE *fp = fopen(full, "rb"); if (!fp) return tool_set_resultf(r, 1, "error: cannot open file: %.200s", path); long offset = tool_arg_long(c, "offset", 0); long limit = tool_arg_long(c, "limit", 0); int numbers = tool_arg_bool(c, "line_numbers", ctx->compact); if (ctx->compact) { int rc = read_compact(fp, path, offset, limit, numbers, r); fclose(fp); return rc; } if (offset <= 0 && limit <= 0 && !numbers) { fclose(fp); size_t n = 0; char *buf = tool_slurp(full, &n, READ_FILE_MAX); if (!buf) return tool_set_resultf(r, 1, "error: file too large (>2MiB) or unreadable: " "%.200s (read a slice with offset/limit)", path); if (tool_is_binary(buf, n)) { free(buf); return tool_set_resultf(r, 1, "error: %.200s is a binary file (%zu bytes)", path, n); } cst_tool_result_free(r); r->out = buf; r->out_len = n; r->exit_code = 0; return 0; } if (offset <= 0) offset = 1; if (limit <= 0) limit = RANGE_DEFAULT_LINES; cst_buf_t b = {0}; long lineno = 0, shown = 0; int truncated = 0, binary = 0; cst_buf_t line = {0}; int ch; for (;;) { line.len = 0; int got = 0; while ((ch = fgetc(fp)) != EOF) { got = 1; if (ch == 0) binary = 1; char cc = (char)ch; cst_buf_append(&line, &cc, 1); if (ch == '\n') break; } if (!got || binary) break; lineno++; if (lineno < offset || shown >= limit || truncated) continue; if (b.len + line.len > RANGE_OUT_MAX) { truncated = 1; continue; } if (numbers) cst_buf_printf(&b, "%6ld|", lineno); cst_buf_append(&b, line.buf, line.len); shown++; } fclose(fp); cst_buf_free(&line); if (binary) { cst_buf_free(&b); return tool_set_resultf(r, 1, "error: %.200s is a binary file", path); } if (b.len && b.buf[b.len - 1] != '\n') cst_buf_puts(&b, "\n"); long last = offset + shown - 1; if (shown == 0) cst_buf_printf(&b, "(no lines: file has %ld line%s)\n", lineno, lineno == 1 ? "" : "s"); else if (last < lineno) cst_buf_printf(&b, "... (lines %ld-%ld of %ld; next offset=%ld)\n", offset, last, lineno, last + 1); return tool_take_buf(r, &b, 0); } /* --- edit_file ----------------------------------------------------------- */ static size_t count_occ(const char *hay, size_t hn, const char *needle, size_t nn) { size_t cnt = 0; if (!nn) return 0; for (size_t i = 0; i + nn <= hn;) { const char *p = memchr(hay + i, needle[0], hn - i - nn + 1); if (!p) break; i = (size_t)(p - hay); if (memcmp(p, needle, nn) == 0) { cnt++; i += nn; } else { i++; } } return cnt; } /* "12, 40, 77" — line numbers of the first few occurrences. */ static void occ_lines(const char *hay, size_t hn, const char *nd, size_t nn, char *out, size_t out_sz) { out[0] = 0; size_t used = 0; long line = 1; size_t last = 0; int k = 0; for (size_t i = 0; nn && i + nn <= hn && k < 6;) { const char *p = memchr(hay + i, nd[0], hn - i - nn + 1); if (!p) break; i = (size_t)(p - hay); if (memcmp(p, nd, nn) != 0) { i++; continue; } for (size_t j = last; j < i; j++) line += hay[j] == '\n'; last = i; int w = snprintf(out + used, out_sz - used, "%s%ld", k ? ", " : "", line); if (w < 0 || (size_t)w >= out_sz - used) break; used += (size_t)w; k++; i += nn; } } static int trimmed_eq(const char *a, size_t an, const char *b, size_t bn) { while (an && isspace((unsigned char)*a)) a++, an--; while (an && isspace((unsigned char)a[an - 1])) an--; while (bn && isspace((unsigned char)*b)) b++, bn--; while (bn && isspace((unsigned char)b[bn - 1])) bn--; return an && an == bn && memcmp(a, b, an) == 0; } /* old_string was not found: lines whose trimmed text equals old_string's * first non-blank line, as "42, 97" (empty when none). */ static void near_lines(const char *hay, size_t hn, const char *old, size_t on, char *out, size_t out_sz) { out[0] = 0; const char *f = old; size_t fl = 0; for (size_t i = 0; i < on;) { const char *e = memchr(old + i, '\n', on - i); size_t end = e ? (size_t)(e - old) : on; int blank = 1; for (size_t j = i; j < end; j++) if (!isspace((unsigned char)old[j])) blank = 0; if (!blank) { f = old + i; fl = end - i; break; } i = end + 1; } if (!fl) return; size_t used = 0; long line = 1; int k = 0; for (size_t i = 0; i < hn && k < 5; line++) { const char *e = memchr(hay + i, '\n', hn - i); size_t end = e ? (size_t)(e - hay) : hn; if (trimmed_eq(hay + i, end - i, f, fl)) { int w = snprintf(out + used, out_sz - used, "%s%ld", k ? ", " : "", line); if (w < 0 || (size_t)w >= out_sz - used) break; used += (size_t)w; k++; } i = end + 1; } } static char *replace_all(const char *hay, size_t hn, const char *old, size_t on, const char *nw, size_t nwn, int all, size_t *out_len) { cst_buf_t b = {0}; size_t i = 0; int done = 0; while (i < hn) { if (!(all || !done) || i + on > hn) { cst_buf_append(&b, hay + i, hn - i); break; } const char *p = NULL; for (size_t j = i; j + on <= hn; j++) { if (hay[j] == old[0] && memcmp(hay + j, old, on) == 0) { p = hay + j; break; } } if (!p) { cst_buf_append(&b, hay + i, hn - i); break; } cst_buf_append(&b, hay + i, (size_t)(p - (hay + i))); cst_buf_append(&b, nw, nwn); i = (size_t)(p - hay) + on; done = 1; } if (!b.buf) cst_buf_puts(&b, ""); *out_len = b.len; return cst_buf_steal(&b); } /* LF → CRLF copy (for files with Windows line endings). */ static char *to_crlf(const char *s, size_t n, size_t *out_len) { cst_buf_t b = {0}; for (size_t i = 0; i < n; i++) { if (s[i] == '\n' && (i == 0 || s[i - 1] != '\r')) cst_buf_append(&b, "\r", 1); cst_buf_append(&b, s + i, 1); } if (!b.buf) cst_buf_puts(&b, ""); *out_len = b.len; return cst_buf_steal(&b); } typedef struct { char full[OS_PATH_MAX]; char *oldc, *newc; size_t olen, nlen; size_t count; } edit_plan_t; static int plan_edit(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c, edit_plan_t *pl, char *err, size_t err_sz) { memset(pl, 0, sizeof(*pl)); const char *path = tool_arg(c, "path"); size_t on = 0, nwn = 0; const char *old = tool_arg_n(c, "old_string", &on); const char *nw = tool_arg_n(c, "new_string", &nwn); int all = tool_arg_bool(c, "replace_all", 0); if (!path || !path[0] || !old || !nw) { snprintf(err, err_sz, "error: need path, old_string and new_string"); return -1; } if (on == 0) { snprintf(err, err_sz, "error: old_string is empty (use write_file to create a file)"); return -1; } if (on == nwn && memcmp(old, nw, on) == 0) { snprintf(err, err_sz, "error: old_string and new_string are identical"); return -1; } if (resolve(ctx, path, pl->full, sizeof(pl->full)) != 0 || !is_regular(pl->full)) { snprintf(err, err_sz, "error: no such file: %.200s", path); return -1; } pl->oldc = tool_slurp(pl->full, &pl->olen, EDIT_FILE_MAX); if (!pl->oldc) { snprintf(err, err_sz, "error: cannot read %.200s (or larger than 8MiB)", path); return -1; } char *o2 = NULL, *n2 = NULL; size_t cnt = count_occ(pl->oldc, pl->olen, old, on); if (cnt == 0 && memchr(old, '\n', on) && strstr(pl->oldc, "\r\n")) { size_t o2n, n2n; o2 = to_crlf(old, on, &o2n); n2 = to_crlf(nw, nwn, &n2n); if (o2 && n2) { cnt = count_occ(pl->oldc, pl->olen, o2, o2n); if (cnt) { old = o2; on = o2n; nw = n2; nwn = n2n; } } } int rc = 0; if (cnt == 0) { char near[64]; near_lines(pl->oldc, pl->olen, old, on, near, sizeof(near)); if (near[0]) snprintf(err, err_sz, "error: old_string not found in %.200s, but its first line " "appears (whitespace aside) at line %s — read_file around " "there and copy the exact text, indentation included", path, near); else snprintf(err, err_sz, "error: old_string not found in %.200s; it must match the file " "exactly (whitespace and indentation included) — re-read the " "file (or grep for a distinctive word) and copy the text", path); rc = -1; } else if (cnt > 1 && !all) { char where[64]; occ_lines(pl->oldc, pl->olen, old, on, where, sizeof(where)); snprintf(err, err_sz, "error: old_string matches %zu places in %.200s (lines %s%s); " "include more surrounding lines to make it unique, or set " "replace_all=true", cnt, path, where, cnt > 6 ? ", …" : ""); rc = -1; } else { pl->newc = replace_all(pl->oldc, pl->olen, old, on, nw, nwn, all, &pl->nlen); pl->count = all ? cnt : 1; if (!pl->newc) { snprintf(err, err_sz, "error: oom"); rc = -1; } } free(o2); free(n2); return rc; } static void edit_free(edit_plan_t *pl) { free(pl->oldc); free(pl->newc); } int cst_tool_edit_file(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c, cst_tool_result_t *r) { edit_plan_t *pl = malloc(sizeof(*pl)); if (!pl) return tool_set_result(r, "error: oom", 1); char err[512]; if (plan_edit(ctx, c, pl, err, sizeof(err)) != 0) { edit_free(pl); free(pl); return tool_set_result(r, err, 1); } const char *path = tool_arg(c, "path"); if (tool_write_whole(pl->full, pl->newc, pl->nlen) != 0) { edit_free(pl); free(pl); return tool_set_resultf(r, 1, "error: cannot write %.200s", path); } cst_buf_t b = {0}; cst_buf_printf(&b, "edited %s (%zu replacement%s)\n", path, pl->count, pl->count == 1 ? "" : "s"); tool_append_diff(&b, path, pl->oldc, pl->olen, pl->newc, pl->nlen); edit_free(pl); free(pl); return tool_take_buf(r, &b, 0); } int cst_preview_edit_file(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c, char **out) { edit_plan_t *pl = malloc(sizeof(*pl)); if (!pl) { *out = strdup("error: oom"); return -1; } char err[512]; int rc = plan_edit(ctx, c, pl, err, sizeof(err)); if (rc != 0) { *out = strdup(err); } else { cst_buf_t b = {0}; tool_append_diff(&b, tool_arg(c, "path"), pl->oldc, pl->olen, pl->newc, pl->nlen); *out = cst_buf_steal(&b); } edit_free(pl); free(pl); return rc; } /* --- 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; if (os_path_is_abs(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)) return 1; #else (void)ctx; (void)path; #endif return 0; } int cst_preview_delete_file(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c, char **out) { const char *path = tool_arg(c, "path"); char full[OS_PATH_MAX]; if (!path || resolve(ctx, path, full, sizeof(full)) != 0 || !is_regular(full)) { *out = strdup("error: no such file"); return -1; } size_t n = 0; char *old = tool_slurp(full, &n, READ_FILE_MAX); cst_buf_t b = {0}; cst_buf_printf(&b, "delete %s (%zu bytes)\n", path, n); if (old) tool_append_diff(&b, path, old, n, "", 0); free(old); *out = cst_buf_steal(&b); return 0; } int cst_tool_delete_file(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c, cst_tool_result_t *r) { const char *path = tool_arg(c, "path"); char full[OS_PATH_MAX]; if (!path || !path[0]) return tool_set_result(r, "error: missing path", 1); if (reject_symlink(ctx, path)) return tool_set_resultf(r, 1, "error: %.200s is a symlink; not deleted", path); if (resolve(ctx, path, full, sizeof(full)) != 0) return tool_set_resultf(r, 1, "error: path outside workspace: %.200s", path); if (is_dir(full)) return tool_set_resultf(r, 1, "error: %.200s is a directory (delete_file only " "removes files)", path); if (!is_regular(full)) return tool_set_resultf(r, 1, "error: no such file: %.200s", path); size_t n = 0; char *old = tool_slurp(full, &n, READ_FILE_MAX); if (remove(full) != 0) { free(old); return tool_set_resultf(r, 1, "error: cannot delete %.200s: %s", path, strerror(errno)); } cst_buf_t b = {0}; cst_buf_printf(&b, "deleted %s (%zu bytes)\n", path, n); if (old) tool_append_diff(&b, path, old, n, "", 0); free(old); return tool_take_buf(r, &b, 0); } static int plan_move(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c, char *src, char *dst, char *err, size_t err_sz, int mkdirs) { const char *from = tool_arg(c, "from"); const char *to = tool_arg(c, "to"); if (!from || !from[0] || !to || !to[0]) { snprintf(err, err_sz, "error: need from and to"); return -1; } if (reject_symlink(ctx, from)) { snprintf(err, err_sz, "error: %.200s is a symlink", from); return -1; } if (resolve(ctx, from, src, OS_PATH_MAX) != 0 || !is_regular(src)) { snprintf(err, err_sz, "error: no such file: %.200s", from); return -1; } if (mkdirs) tool_ensure_parent_dirs(ctx->workspace_root, to); if (resolve(ctx, to, dst, OS_PATH_MAX) != 0) { if (!mkdirs) { snprintf(dst, OS_PATH_MAX, "%s", to); return 0; } snprintf(err, err_sz, "error: destination outside workspace: %.200s", to); return -1; } struct stat st; if (stat(dst, &st) == 0) { snprintf(err, err_sz, "error: destination exists: %.200s", to); return -1; } return 0; } int cst_preview_move_file(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c, char **out) { char src[OS_PATH_MAX], dst[OS_PATH_MAX], err[512]; if (plan_move(ctx, c, src, dst, err, sizeof(err), 0) != 0) { *out = strdup(err); return -1; } cst_buf_t b = {0}; cst_buf_printf(&b, "move %s → %s\n", tool_arg(c, "from"), tool_arg(c, "to")); *out = cst_buf_steal(&b); return 0; } int cst_tool_move_file(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c, cst_tool_result_t *r) { char src[OS_PATH_MAX], dst[OS_PATH_MAX], err[512]; if (plan_move(ctx, c, src, dst, err, sizeof(err), 1) != 0) return tool_set_result(r, err, 1); if (rename(src, dst) != 0) return tool_set_resultf(r, 1, "error: cannot move: %s", strerror(errno)); return tool_set_resultf(r, 0, "moved %.300s → %.300s", tool_arg(c, "from"), tool_arg(c, "to")); } /* --- 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"}; static int skip_dir_name(const char *name) { for (size_t i = 0; i < sizeof(SKIP_DIRS) / sizeof(SKIP_DIRS[0]); i++) { if (strcmp(name, SKIP_DIRS[i]) == 0) return 1; } return 0; } /* Root .gitignore, simple rules only: `name`, `*.o`, `dir/`, `/path`. */ typedef struct { char pat[256]; int dir_only; int anchored; /* contains '/': match the relative path */ } ign_rule_t; typedef struct { ign_rule_t *v; int n; } ignore_t; static void ignore_load(const char *root, ignore_t *ig) { memset(ig, 0, sizeof(*ig)); char p[OS_PATH_MAX]; if (os_path_join(p, sizeof(p), root, ".gitignore") != 0) return; FILE *fp = fopen(p, "r"); if (!fp) return; char line[512]; int cap = 0; while (fgets(line, sizeof(line), fp)) { size_t n = strlen(line); while (n && (line[n - 1] == '\n' || line[n - 1] == '\r' || line[n - 1] == ' ')) line[--n] = 0; if (!n || line[0] == '#' || line[0] == '!') continue; if (ig->n == cap) { cap = cap ? cap * 2 : 16; ign_rule_t *nv = realloc(ig->v, (size_t)cap * sizeof(*nv)); if (!nv) break; ig->v = nv; } ign_rule_t *r = &ig->v[ig->n]; memset(r, 0, sizeof(*r)); char *s = line; if (n && s[n - 1] == '/') { r->dir_only = 1; s[--n] = 0; } if (s[0] == '/') { s++; r->anchored = 1; } if (strchr(s, '/')) r->anchored = 1; if (!s[0]) continue; snprintf(r->pat, sizeof(r->pat), "%.255s", s); ig->n++; } fclose(fp); } static int ignored(const ignore_t *ig, const char *rel, const char *name, int isdir) { for (int i = 0; i < ig->n; i++) { const ign_rule_t *r = &ig->v[i]; if (r->dir_only && !isdir) continue; if (fnmatch(r->pat, r->anchored ? rel : name, 0) == 0) return 1; } return 0; } 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); struct walker { const char *root; /* workspace realpath */ ignore_t ig; walk_fn fn; void *ud; int max_depth; int stop; }; static int name_cmp(const void *a, const void *b) { return strcmp(*(char *const *)a, *(char *const *)b); } static void walk_dir(walker_t *w, const char *rel, int depth) { if (w->stop) return; char dirfull[OS_PATH_MAX]; if (rel[0]) snprintf(dirfull, sizeof(dirfull), "%s/%s", w->root, rel); else snprintf(dirfull, sizeof(dirfull), "%s", w->root); DIR *d = opendir(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; if (n == cap) { cap = cap ? cap * 2 : 32; char **nv = realloc(names, (size_t)cap * sizeof(char *)); if (!nv) break; names = nv; } names[n] = strdup(de->d_name); if (names[n]) n++; } closedir(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]; int k1 = rel[0] ? snprintf(crel, sizeof(crel), "%s/%s", rel, names[i]) : snprintf(crel, sizeof(crel), "%s", names[i]); 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) continue; int isdir = S_ISDIR(st.st_mode); if (ignored(&w->ig, crel, names[i], isdir)) continue; if (w->fn(w, crel, cfull, &st, depth)) w->stop = 1; if (isdir && !skip_dir_name(names[i]) && depth + 1 < w->max_depth) walk_dir(w, crel, depth + 1); } for (int i = 0; i < n; i++) free(names[i]); free(names); } /* Resolve an optional directory argument to a workspace-relative prefix. */ static int start_rel(const cst_tool_ctx_t *ctx, const char *path, const char *root, char *rel, size_t rel_sz, char *err, size_t err_sz) { rel[0] = 0; if (!path || !path[0] || strcmp(path, ".") == 0 || strcmp(path, "./") == 0) return 0; char full[OS_PATH_MAX]; if (resolve(ctx, path, full, sizeof(full)) != 0) { snprintf(err, err_sz, "error: path outside workspace: %.200s", path); return -1; } struct stat st; if (stat(full, &st) != 0) { snprintf(err, err_sz, "error: no such path: %.200s", path); return -1; } snprintf(rel, rel_sz, "%s", tool_rel(root, full)); if (strcmp(rel, ".") == 0) rel[0] = 0; return S_ISDIR(st.st_mode) ? 0 : 1; } /* list_dir */ #define LIST_COUNT_MAX 100000 typedef struct { cst_buf_t b; int count; int more; /* entries past LIST_MAX (counted, not shown) */ int truncated; /* stopped counting */ } list_ud_t; /* Entries directly inside a directory (for the depth boundary). */ static int dir_entries(const char *full) { DIR *d = opendir(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); return n; } static int list_cb(walker_t *w, const char *rel, const char *full, const struct stat *st, int depth) { list_ud_t *u = w->ud; if (u->count >= LIST_MAX) { if (++u->more >= LIST_COUNT_MAX) { u->truncated = 1; return 1; } return 0; } const char *name = strrchr(rel, '/'); name = name ? name + 1 : rel; for (int i = 0; i < depth; i++) cst_buf_puts(&u->b, " "); if (S_ISDIR(st->st_mode)) { int k = -1; if (skip_dir_name(name)) cst_buf_printf(&u->b, "%s/ (skipped)\n", name); else if (depth + 1 >= w->max_depth && (k = dir_entries(full)) > 0) cst_buf_printf(&u->b, "%s/ (%d entr%s)\n", name, k, k == 1 ? "y" : "ies"); else cst_buf_printf(&u->b, "%s/%s\n", name, k == 0 ? " (empty)" : ""); } else if (S_ISLNK(st->st_mode)) cst_buf_printf(&u->b, "%s@\n", name); else cst_buf_printf(&u->b, "%s (%lld B)\n", name, (long long)st->st_size); u->count++; return 0; } int cst_tool_list_dir(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c, cst_tool_result_t *r) { char root[OS_PATH_MAX], rel[OS_PATH_MAX], err[512]; if (os_realpath(ctx->workspace_root, root, sizeof(root)) != 0) return tool_set_result(r, "error: bad workspace", 1); int k = start_rel(ctx, tool_arg(c, "path"), root, rel, sizeof(rel), err, sizeof(err)); if (k < 0) return tool_set_result(r, err, 1); if (k == 1) return tool_set_resultf(r, 1, "error: %.200s is a file (use read_file)", rel); long depth = tool_arg_long(c, "depth", 2); if (depth < 1) depth = 1; if (depth > 6) depth = 6; walker_t w; memset(&w, 0, sizeof(w)); w.root = root; ignore_load(root, &w.ig); list_ud_t u; memset(&u, 0, sizeof(u)); w.fn = list_cb; w.ud = &u; w.max_depth = (int)depth; cst_buf_printf(&u.b, "%s/\n", rel[0] ? rel : "."); walk_dir(&w, rel, 0); free(w.ig.v); if (u.count == 0) cst_buf_puts(&u.b, "(empty)\n"); if (u.more) cst_buf_printf(&u.b, "… %s%d more entries not shown (list a subdirectory " "or use a smaller depth)\n", u.truncated ? "over " : "", u.more); return tool_take_buf(r, &u.b, 0); } /* glob */ static int seg_match(const char *pat, const char *path) { /* pat and path are '/'-separated; "**" spans any number of segments */ if (!*pat) return !*path; const char *ps = strchr(pat, '/'); size_t pl = ps ? (size_t)(ps - pat) : strlen(pat); if (pl == 2 && pat[0] == '*' && pat[1] == '*') { const char *rest = ps ? ps + 1 : ""; if (!*rest) return 1; for (const char *p = path;; ) { if (seg_match(rest, p)) return 1; const char *sl = strchr(p, '/'); if (!sl) return 0; p = sl + 1; } } if (!*path) return 0; const char *ss = strchr(path, '/'); size_t sl = ss ? (size_t)(ss - path) : strlen(path); char pseg[256], sseg[256]; if (pl >= sizeof(pseg) || sl >= sizeof(sseg)) return 0; memcpy(pseg, pat, pl); pseg[pl] = 0; memcpy(sseg, path, sl); sseg[sl] = 0; if (fnmatch(pseg, sseg, FNM_PERIOD) != 0) return 0; if (!ps) return !ss; if (!ss) return 0; return seg_match(ps + 1, ss + 1); } typedef struct { const char *pattern; const char *base; /* rel prefix of the search dir ("" = root) */ int basename_only; char **hits; double *mtimes; int n, cap; int truncated; } glob_ud_t; static int glob_cb(walker_t *w, const char *rel, const char *full, const struct stat *st, int depth) { (void)full; (void)depth; glob_ud_t *u = w->ud; if (!S_ISREG(st->st_mode)) return 0; const char *sub = rel; size_t bl = strlen(u->base); if (bl) { if (strncmp(rel, u->base, bl) != 0 || rel[bl] != '/') return 0; sub = rel + bl + 1; } const char *name = strrchr(sub, '/'); name = name ? name + 1 : sub; int ok = u->basename_only ? fnmatch(u->pattern, name, FNM_PERIOD) == 0 : seg_match(u->pattern, sub); if (!ok) return 0; if (u->n == u->cap) { u->cap = u->cap ? u->cap * 2 : 64; char **nh = realloc(u->hits, (size_t)u->cap * sizeof(char *)); double *nm = realloc(u->mtimes, (size_t)u->cap * sizeof(double)); if (nh) u->hits = nh; if (nm) u->mtimes = nm; if (!nh || !nm) return 1; } u->hits[u->n] = strdup(rel); u->mtimes[u->n] = (double)st->st_mtime; if (u->hits[u->n]) u->n++; if (u->n >= GLOB_MAX * 20) { u->truncated = 1; return 1; } return 0; } typedef struct { const char *path; double mtime; } glob_hit_t; static int hit_by_mtime(const void *a, const void *b) { const glob_hit_t *x = a, *y = b; if (x->mtime != y->mtime) return x->mtime < y->mtime ? 1 : -1; return strcmp(x->path, y->path); } int cst_tool_glob(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c, cst_tool_result_t *r) { const char *pattern = tool_arg(c, "pattern"); if (!pattern || !pattern[0]) return tool_set_result(r, "error: missing pattern", 1); char root[OS_PATH_MAX], rel[OS_PATH_MAX], err[512]; if (os_realpath(ctx->workspace_root, root, sizeof(root)) != 0) return tool_set_result(r, "error: bad workspace", 1); int k = start_rel(ctx, tool_arg(c, "path"), root, rel, sizeof(rel), err, sizeof(err)); if (k < 0) return tool_set_result(r, err, 1); while (pattern[0] == '.' && pattern[1] == '/') pattern += 2; walker_t w; memset(&w, 0, sizeof(w)); w.root = root; ignore_load(root, &w.ig); glob_ud_t u; memset(&u, 0, sizeof(u)); u.pattern = pattern; u.base = rel; u.basename_only = strchr(pattern, '/') == NULL; w.fn = glob_cb; w.ud = &u; w.max_depth = 64; walk_dir(&w, rel, 0); free(w.ig.v); cst_buf_t b = {0}; glob_hit_t *hs = malloc((size_t)(u.n ? u.n : 1) * sizeof(*hs)); if (hs) { for (int i = 0; i < u.n; i++) { hs[i].path = u.hits[i]; hs[i].mtime = u.mtimes[i]; } qsort(hs, (size_t)u.n, sizeof(*hs), hit_by_mtime); for (int i = 0; i < u.n && i < GLOB_MAX; i++) cst_buf_printf(&b, "%s\n", hs[i].path); } free(hs); if (u.n == 0) cst_buf_printf(&b, "(no files match %s)\n", pattern); else if (u.n > GLOB_MAX || u.truncated) cst_buf_printf(&b, "… %s%d more files (showing the %d newest of %s%d; " "narrow the pattern or path)\n", u.truncated ? "over " : "", u.n - GLOB_MAX, GLOB_MAX, u.truncated ? "over " : "", u.n); for (int i = 0; i < u.n; i++) free(u.hits[i]); free(u.hits); free(u.mtimes); return tool_take_buf(r, &b, 0); } /* Workspace file list for UI completion (same ignore rules as glob). */ typedef struct { char **v; int n, max; } files_ud_t; static int files_cb(walker_t *w, const char *rel, const char *full, const struct stat *st, int depth) { (void)full; (void)depth; files_ud_t *u = w->ud; if (!S_ISREG(st->st_mode)) return 0; if (u->n >= u->max) return 1; u->v[u->n] = strdup(rel); if (u->v[u->n]) u->n++; return 0; } int cst_workspace_files(const char *ws, char ***out, int max) { *out = NULL; char root[OS_PATH_MAX]; if (max <= 0 || os_realpath(ws, root, sizeof(root)) != 0) return 0; files_ud_t u = {calloc((size_t)max, sizeof(char *)), 0, max}; if (!u.v) return 0; walker_t w; memset(&w, 0, sizeof(w)); w.root = root; ignore_load(root, &w.ig); w.fn = files_cb; w.ud = &u; w.max_depth = 16; walk_dir(&w, "", 0); free(w.ig.v); *out = u.v; return u.n; } /* grep: matches grouped by file, "N matches in F files" summary. */ #define GREP_PER_FILE 20 #define GREP_COUNT_MAX 5000 typedef struct { regex_t re; const char *glob; int context; cst_buf_t b; int matches; /* counted (up to GREP_COUNT_MAX) */ int shown; int files; /* text files searched */ int hit_files; /* files with a match */ int truncated; /* stopped counting */ } grep_ud_t; static void put_line(cst_buf_t *b, long ln, char sep, const char *s, size_t n) { if (n && s[n - 1] == '\n') n--; if (n && s[n - 1] == '\r') n--; int cut = 0; if (n > GREP_LINE_MAX) { n = cst_utf8_floor(s, GREP_LINE_MAX); cut = 1; } cst_buf_printf(b, "%ld%c", ln, sep); cst_buf_append(b, s, n); cst_buf_puts(b, cut ? " …\n" : "\n"); } static int line_hit(grep_ud_t *u, const char *s, size_t n) { char tmp[GREP_LINE_MAX * 8]; size_t m = n < sizeof(tmp) - 1 ? n : sizeof(tmp) - 1; memcpy(tmp, s, m); tmp[m] = 0; if (m && tmp[m - 1] == '\n') tmp[--m] = 0; if (m && tmp[m - 1] == '\r') tmp[--m] = 0; return regexec(&u->re, tmp, 0, NULL, 0) == 0; } static void grep_file(grep_ud_t *u, const char *rel, const char *full) { size_t n = 0; char *buf = tool_slurp(full, &n, READ_FILE_MAX); if (!buf) return; if (tool_is_binary(buf, n)) { free(buf); return; } u->files++; /* line table */ int nl = 0, cap = 256; const char **ls = malloc((size_t)cap * sizeof(char *)); size_t *ln = malloc((size_t)cap * sizeof(size_t)); if (!ls || !ln) { free(ls); free(ln); free(buf); return; } for (size_t i = 0; i < n;) { const char *e = memchr(buf + i, '\n', n - i); size_t end = e ? (size_t)(e - buf) + 1 : n; if (nl == cap) { cap *= 2; const char **a = realloc(ls, (size_t)cap * sizeof(char *)); size_t *bb = realloc(ln, (size_t)cap * sizeof(size_t)); if (a) ls = a; if (bb) ln = bb; if (!a || !bb) break; } ls[nl] = buf + i; ln[nl] = end - i; nl++; i = end; } cst_buf_t fb = {0}; int fm = 0, fshown = 0, last_printed = -1; for (int i = 0; i < nl; i++) { if (!line_hit(u, ls[i], ln[i])) continue; fm++; if (++u->matches >= GREP_COUNT_MAX) u->truncated = 1; if (fshown < GREP_PER_FILE && u->shown < GREP_MAX_MATCHES) { int from = i - u->context < 0 ? 0 : i - u->context; if (from <= last_printed) from = last_printed + 1; else if (u->context && last_printed >= 0) cst_buf_puts(&fb, "--\n"); for (int k = from; k < i; k++) put_line(&fb, k + 1, '-', ls[k], ln[k]); put_line(&fb, i + 1, ':', ls[i], ln[i]); last_printed = i; for (int k = i + 1; k <= i + u->context && k < nl; k++) { if (line_hit(u, ls[k], ln[k])) break; /* printed by the main loop */ put_line(&fb, k + 1, '-', ls[k], ln[k]); last_printed = k; } fshown++; u->shown++; } if (u->truncated) break; } if (fm) { u->hit_files++; if (fm == 1) cst_buf_printf(&u->b, "%s\n", rel); else cst_buf_printf(&u->b, "%s (%d matches)\n", rel, fm); if (fb.buf) cst_buf_append(&u->b, fb.buf, fb.len); if (fm > fshown) cst_buf_printf(&u->b, "… %d more in this file\n", fm - fshown); } cst_buf_free(&fb); free(ls); free(ln); free(buf); } static int grep_cb(walker_t *w, const char *rel, const char *full, const struct stat *st, int depth) { (void)depth; grep_ud_t *u = w->ud; if (!S_ISREG(st->st_mode) || st->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; if (!ok) return 0; } grep_file(u, rel, full); return u->truncated || u->files >= GREP_MAX_FILES; } /* Translate common PCRE shorthands models use into POSIX ERE. */ static char *pcre_to_ere(const char *p) { cst_buf_t b = {0}; for (size_t i = 0; p[i]; i++) { if (p[i] == '\\' && p[i + 1]) { const char *rep = NULL; switch (p[i + 1]) { case 'd': rep = "[0-9]"; break; case 'D': rep = "[^0-9]"; break; case 'w': rep = "[A-Za-z0-9_]"; break; case 'W': rep = "[^A-Za-z0-9_]"; break; case 's': rep = "[[:space:]]"; break; case 'S': rep = "[^[:space:]]"; break; default: break; } if (rep) { cst_buf_puts(&b, rep); i++; continue; } cst_buf_append(&b, p + i, 2); i++; continue; } cst_buf_append(&b, p + i, 1); } if (!b.buf) cst_buf_puts(&b, ""); return cst_buf_steal(&b); } int cst_tool_grep(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c, cst_tool_result_t *r) { const char *pattern = tool_arg(c, "pattern"); if (!pattern || !pattern[0]) return tool_set_result(r, "error: missing pattern", 1); char root[OS_PATH_MAX], rel[OS_PATH_MAX], err[512]; if (os_realpath(ctx->workspace_root, root, sizeof(root)) != 0) return tool_set_result(r, "error: bad workspace", 1); int k = start_rel(ctx, tool_arg(c, "path"), root, rel, sizeof(rel), err, sizeof(err)); if (k < 0) return tool_set_result(r, err, 1); grep_ud_t u; memset(&u, 0, sizeof(u)); char *ere = pcre_to_ere(pattern); int flags = REG_EXTENDED | REG_NOSUB; if (tool_arg_bool(c, "ignore_case", 0)) flags |= REG_ICASE; int rc = ere ? regcomp(&u.re, ere, flags) : -1; free(ere); if (rc != 0) { char msg[200] = "out of memory"; if (rc > 0) regerror(rc, &u.re, msg, sizeof(msg)); return tool_set_resultf(r, 1, "error: bad regex: %s", msg); } const char *g = tool_arg(c, "glob"); u.glob = g && g[0] ? g : NULL; long ctxl = tool_arg_long(c, "context", 0); u.context = ctxl < 0 ? 0 : ctxl > 5 ? 5 : (int)ctxl; if (k == 1) { char full[OS_PATH_MAX]; if (snprintf(full, sizeof(full), "%s/%s", root, rel) < (int)sizeof(full)) grep_file(&u, rel, full); } else { walker_t w; memset(&w, 0, sizeof(w)); w.root = root; ignore_load(root, &w.ig); w.fn = grep_cb; w.ud = &u; w.max_depth = 64; walk_dir(&w, rel, 0); free(w.ig.v); } regfree(&u.re); if (u.matches == 0) { cst_buf_free(&u.b); cst_buf_printf(&u.b, "(no matches in %d file%s)\n", u.files, u.files == 1 ? "" : "s"); return tool_take_buf(r, &u.b, 0); } cst_buf_t out = {0}; cst_buf_printf(&out, "%s%d match%s in %d file%s", u.truncated ? "over " : "", u.matches, u.matches == 1 ? "" : "es", u.hit_files, u.hit_files == 1 ? "" : "s"); if (u.shown < u.matches) cst_buf_printf(&out, " (showing %d; narrow with path/glob or a tighter " "pattern)", u.shown); cst_buf_puts(&out, "\n"); cst_buf_append(&out, u.b.buf, u.b.len); cst_buf_free(&u.b); 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