/* Per-turn file snapshots under /.agent/undo/// so the * file changes of the latest turn can be rolled back (/undo). */ #ifndef _DEFAULT_SOURCE #define _DEFAULT_SOURCE #endif #include #include #include #include #include #include "cst_agent.h" #include "cst_undo.h" #include "cst_util.h" #include "os_compat.h" #define UNDO_FILE_MAX (8 * 1024 * 1024) static int undo_root(const char *ws, const char *sid, char *out, size_t n) { int k = snprintf(out, n, "%s/.agent/undo/%s", ws, sid && sid[0] ? sid : "_"); return k < 0 || (size_t)k >= n ? -1 : 0; } /* Workspace-relative, no "..", not absolute: undo may delete these paths. */ static int rel_ok(const char *rel) { if (!rel || !rel[0] || rel[0] == '/' || rel[0] == '\\' || (rel[0] && rel[1] == ':')) return 0; const char *p = rel; while (*p) { size_t n = strcspn(p, "/\\"); if (n == 2 && p[0] == '.' && p[1] == '.') return 0; p += n; while (*p == '/' || *p == '\\') p++; } return strchr(rel, '\n') == NULL && strchr(rel, '\t') == NULL; } static char *slurp(const char *p, size_t *len, size_t max) { *len = 0; FILE *fp = fopen(p, "rb"); if (!fp) return NULL; fseek(fp, 0, SEEK_END); long sz = ftell(fp); if (sz < 0 || (size_t)sz > max) { fclose(fp); return NULL; } rewind(fp); char *b = malloc((size_t)sz + 1); if (!b) { fclose(fp); return NULL; } *len = fread(b, 1, (size_t)sz, fp); b[*len] = 0; fclose(fp); return b; } static int put(const char *p, const char *d, size_t n) { FILE *fp = fopen(p, "wb"); if (!fp) return -1; int ok = n == 0 || fwrite(d, 1, n, fp) == n; return fclose(fp) == 0 && ok ? 0 : -1; } static int manifest_has(const char *mf, const char *rel) { FILE *fp = fopen(mf, "rb"); if (!fp) return 0; char line[1400]; int found = 0; while (!found && fgets(line, sizeof(line), fp)) { char *t2 = strchr(line, '\t'); char *t3 = t2 ? strchr(t2 + 1, '\t') : NULL; if (!t3) continue; t3++; size_t n = strlen(t3); if (n && t3[n - 1] == '\n') t3[--n] = 0; found = strcmp(t3, rel) == 0; } fclose(fp); return found; } int cst_undo_snapshot(const char *ws, const char *sid, const char *tag, const char *rel) { if (!ws || !tag || !rel_ok(rel)) return -1; char full[OS_PATH_MAX]; if (cst_sandbox_resolve(ws, rel, full, sizeof(full)) != 0) { /* parent missing: the file is new; record it as absent */ snprintf(full, sizeof(full), "%s", ""); } char dir[OS_PATH_MAX], mf[OS_PATH_MAX]; if (undo_root(ws, sid, dir, sizeof(dir)) != 0) return -1; size_t dl = strlen(dir); int k = snprintf(dir + dl, sizeof(dir) - dl, "/%s", tag); if (k < 0 || (size_t)k >= sizeof(dir) - dl) return -1; if (os_mkdir_p(dir, 0755) != 0) return -1; k = snprintf(mf, sizeof(mf), "%s/MANIFEST", dir); if (k < 0 || (size_t)k >= sizeof(mf)) return -1; if (manifest_has(mf, rel)) return 0; int idx = 0; { FILE *fp = fopen(mf, "rb"); if (fp) { int ch; while ((ch = fgetc(fp)) != EOF) idx += ch == '\n'; fclose(fp); } } const char *state = "absent"; struct stat st; if (full[0] && stat(full, &st) == 0 && S_ISREG(st.st_mode)) { size_t n = 0; char *data = slurp(full, &n, UNDO_FILE_MAX); if (!data) { state = "toolarge"; } else { char bak[OS_PATH_MAX + 16]; int bl = snprintf(bak, sizeof(bak), "%s/%d.bak", dir, idx); state = bl > 0 && (size_t)bl < sizeof(bak) && put(bak, data, n) == 0 ? "file" : "toolarge"; free(data); } } FILE *fp = fopen(mf, "ab"); if (!fp) return -1; fprintf(fp, "%d\t%s\t%s\n", idx, state, rel); fclose(fp); return 0; } static int latest_tag(const char *root, char *tag, size_t n) { os_dir_t *d = os_dir_open(root); if (!d) return -1; const os_dirent_t *de; tag[0] = 0; while ((de = os_dir_next(d)) != NULL) { if (de->name[0] == '.') continue; if (strcmp(de->name, tag) > 0) snprintf(tag, n, "%s", de->name); } os_dir_close(d); return tag[0] ? 0 : -1; } static void rm_dir(const char *dir) { os_dir_t *d = os_dir_open(dir); if (d) { const os_dirent_t *de; while ((de = os_dir_next(d)) != NULL) { if (strcmp(de->name, ".") == 0 || strcmp(de->name, "..") == 0) continue; char p[OS_PATH_MAX]; if (snprintf(p, sizeof(p), "%s/%s", dir, de->name) < (int)sizeof(p)) unlink(p); } os_dir_close(d); } rmdir(dir); } int cst_undo_count(const char *ws, const char *sid) { char root[OS_PATH_MAX]; if (undo_root(ws, sid, root, sizeof(root)) != 0) return 0; os_dir_t *d = os_dir_open(root); if (!d) return 0; int n = 0; const os_dirent_t *de; while ((de = os_dir_next(d)) != NULL) n += de->name[0] != '.'; os_dir_close(d); return n; } int cst_undo_restore_last(const char *ws, const char *sid, char *report, size_t report_sz) { report[0] = 0; char root[OS_PATH_MAX], tag[256], dir[OS_PATH_MAX], mf[OS_PATH_MAX]; if (undo_root(ws, sid, root, sizeof(root)) != 0 || latest_tag(root, tag, sizeof(tag)) != 0) { snprintf(report, report_sz, "nothing to undo"); return -1; } if (snprintf(dir, sizeof(dir), "%s/%s", root, tag) >= (int)sizeof(dir) || snprintf(mf, sizeof(mf), "%s/MANIFEST", dir) >= (int)sizeof(mf)) { snprintf(report, report_sz, "undo path too long"); return -1; } FILE *fp = fopen(mf, "rb"); cst_buf_t rep = {0}; int restored = 0, failed = 0; if (fp) { char line[1400]; while (fgets(line, sizeof(line), fp)) { size_t n = strlen(line); if (n && line[n - 1] == '\n') line[--n] = 0; char *t2 = strchr(line, '\t'); char *t3 = t2 ? strchr(t2 + 1, '\t') : NULL; if (!t3) continue; *t2++ = 0; *t3++ = 0; const char *rel = t3, *state = t2; char full[OS_PATH_MAX]; os_stat_t lst; if (!rel_ok(rel) || os_path_join(full, sizeof(full), ws, rel) != 0) continue; if (os_lstat(full, &lst) == 0 && lst.is_link) { cst_buf_printf(&rep, "skipped %s (now a symlink)\n", rel); failed++; continue; } if (strcmp(state, "file") == 0) { char bak[OS_PATH_MAX]; if (snprintf(bak, sizeof(bak), "%s/%s.bak", dir, line) >= (int)sizeof(bak)) bak[0] = 0; size_t len = 0; char *data = slurp(bak, &len, UNDO_FILE_MAX); char parent[OS_PATH_MAX]; snprintf(parent, sizeof(parent), "%s", full); char *sl = strrchr(parent, '/'); if (sl) { *sl = 0; os_mkdir_p(parent, 0755); } if (data && put(full, data, len) == 0) { cst_buf_printf(&rep, "restored %s\n", rel); restored++; } else { cst_buf_printf(&rep, "FAILED to restore %s\n", rel); failed++; } free(data); } else if (strcmp(state, "absent") == 0) { if (unlink(full) == 0) { cst_buf_printf(&rep, "removed %s (did not exist before)\n", rel); restored++; } } else { cst_buf_printf(&rep, "skipped %s (too large to snapshot)\n", rel); failed++; } } fclose(fp); } rm_dir(dir); if (!rep.buf) cst_buf_puts(&rep, "nothing changed\n"); snprintf(report, report_sz, "%s", rep.buf); cst_buf_free(&rep); return failed && !restored ? -1 : 0; }