Files
Joseph MattielloandClaude Fable 5 0230c9e676 fix(trace): analyzer CLI fixes -- overflow-safe trace_diff alloc, REGSGPU/DSP FLAGS label, field_diff blank-line desync
Task 6 review found three Critical issues in the analyzer CLIs added
in 93c7f26; fixes all three:

- trace_diff.c: load_filtered() trusted the VJTR header's hdr.count
  for a malloc(count * sizeof(vjtrace_ev)) with no overflow check. A
  crafted header claiming ~2^59 records wrapped that size_t multiply
  into a tiny allocation, and the read loop then wrote real trailing
  records past its end (reviewer reproduced SIGABRT/exit 133 instead
  of exit 2). Now derives the real record count from the file's
  actual length (ftell to end, minus header size, divided by
  ev_size), requires it to match hdr.count exactly before allocating
  anything sized by it, plus a defense-in-depth SIZE_MAX guard on the
  multiply itself. trace_dump.c was already unaffected (streams
  record-by-record).

- trace_memdiff.c / vjtrace.h: REGSGPU/REGSDSP slot 33 was labeled
  CTRL per a literal reading of the task-6 brief's ambiguity
  resolution, but the writer (GPUGetFlags()/DSPGetFlags() in
  vjtrace_snapshot()) and vjtrace.h's own format comment both say
  it's the flags register, not G_CTRL/D_CTRL (a different register at
  a different address, not captured by this format at all). Relabeled
  to FLAGS in trace_memdiff.c; tightened vjtrace.h's REGSDSP wording
  to match REGSGPU's already-clear "+ GPU_FLAGS" and added an
  explicit note on the CTRL/FLAGS distinction.

- field_diff.c: the row loop skipped any row where EITHER side's line
  was blank, not just a trailing blank on both -- silently dropping
  the comparison when one file has a blank line and the other has
  real data at that position (reviewer reproduced two genuinely
  different CSVs reported identical, exit 0). Now: blank on both
  sides is tolerated (never desyncs alignment, since both cursors
  always advance together); blank on exactly one side is compared as
  an all-empty-fields row against the real row, surfacing as an
  ordinary per-column mismatch with exit 1.

Re-verified the full malformed-input matrix (bad magic, truncated
header, mid-record/mid-section truncation, crafted huge count,
blank-line-desync CSV) on all four tools plus real-data behavior on
t1/t2 rings, f1/f2 CSVs, and the snapshot pairs from the original
report -- all unchanged from before the fix except the three bugs
themselves. c89-lint clean. Fix report appended to
.superpowers/sdd/2026-08-11-vjtrace-flight-recorder/task-6-report.md.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-11 22:33:07 -04:00

236 lines
7.9 KiB
C

/* test/tools/field_diff.c -- row/column diff of two per-field CSVs
* written by test/harness/trace_probe.c's --field-csv option.
*
* Standalone C99, no harness dependency. Does not hardcode a column
* list: column names come from each file's own header row, and the
* two headers must name the same columns in the same order (this
* tool refuses to guess an alignment for a schema mismatch -- that is
* a malformed-input condition, not a diff).
*
* Build:
* cc -O2 -Wall -std=c99 -I. -o test/tools/field_diff test/tools/field_diff.c
*
* Usage:
* field_diff A.csv B.csv
*
* Exit: 0 = identical (same header, same row count, every field
* equal), 1 = a difference was found, 2 = usage error, missing file,
* empty file, or a ragged/malformed row (column count not matching
* the header).
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#define MAXCOLS 64
#define LINEBUF 8192
/* Splits `line` in place on ',' (trailing \n/\r stripped first),
* filling out[] with pointers into `line` and returning the field
* count, or -1 if there are more than MAXCOLS fields. */
static int split_csv(char *line, char *out[], int maxcols)
{
int n = 0;
char *p = line;
size_t len = strlen(line);
while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) {
line[--len] = '\0';
}
if (len == 0)
return 0;
out[n++] = p;
while (*p) {
if (*p == ',') {
*p = '\0';
if (n >= maxcols)
return -1;
out[n++] = p + 1;
}
p++;
}
return n;
}
int main(int argc, char **argv)
{
const char *pathA, *pathB;
FILE *fa, *fb;
char lineA[LINEBUF], lineB[LINEBUF];
char *colsA[MAXCOLS], *colsB[MAXCOLS];
char *fieldsA[MAXCOLS], *fieldsB[MAXCOLS];
char names[MAXCOLS][64];
int ncols, i;
long row;
long mismatch_count[MAXCOLS];
long row_row = -1;
int row_col = -1;
char row_a[128], row_b[128];
int have_first = 0;
long total_rows_a = 0, total_rows_b = 0;
int extra_a = 0, extra_b = 0;
if (argc != 3) {
fprintf(stderr, "usage: field_diff A.csv B.csv\n");
return 2;
}
pathA = argv[1];
pathB = argv[2];
fa = fopen(pathA, "r");
if (!fa) {
fprintf(stderr, "field_diff: cannot open '%s': %s\n", pathA, strerror(errno));
return 2;
}
fb = fopen(pathB, "r");
if (!fb) {
fprintf(stderr, "field_diff: cannot open '%s': %s\n", pathB, strerror(errno));
fclose(fa);
return 2;
}
if (!fgets(lineA, sizeof(lineA), fa)) {
fprintf(stderr, "field_diff: '%s' is empty (no header)\n", pathA);
fclose(fa); fclose(fb);
return 2;
}
if (!fgets(lineB, sizeof(lineB), fb)) {
fprintf(stderr, "field_diff: '%s' is empty (no header)\n", pathB);
fclose(fa); fclose(fb);
return 2;
}
{
int ncolsA = split_csv(lineA, colsA, MAXCOLS);
int ncolsB = split_csv(lineB, colsB, MAXCOLS);
if (ncolsA < 0 || ncolsB < 0) {
fprintf(stderr, "field_diff: header has more than %d columns\n", MAXCOLS);
fclose(fa); fclose(fb);
return 2;
}
if (ncolsA != ncolsB) {
fprintf(stderr,
"field_diff: header column count mismatch ('%s' has %d, "
"'%s' has %d) -- different --field-csv schema, cannot align\n",
pathA, ncolsA, pathB, ncolsB);
fclose(fa); fclose(fb);
return 2;
}
ncols = ncolsA;
for (i = 0; i < ncols; i++) {
if (strcmp(colsA[i], colsB[i]) != 0) {
fprintf(stderr,
"field_diff: header column %d name mismatch ('%s' vs '%s') "
"-- cannot align columns\n", i, colsA[i], colsB[i]);
fclose(fa); fclose(fb);
return 2;
}
strncpy(names[i], colsA[i], sizeof(names[i]) - 1);
names[i][sizeof(names[i]) - 1] = '\0';
mismatch_count[i] = 0;
}
}
row = 0;
for (;;) {
char *gotA = fgets(lineA, sizeof(lineA), fa);
char *gotB = fgets(lineB, sizeof(lineB), fb);
int nA, nB, k;
if (!gotA && !gotB)
break;
if (gotA && !gotB) { extra_a = 1; break; }
if (!gotA && gotB) { extra_b = 1; break; }
row++;
nA = split_csv(lineA, fieldsA, MAXCOLS);
nB = split_csv(lineB, fieldsB, MAXCOLS);
if (nA < 0 || nB < 0) {
fprintf(stderr, "field_diff: row %ld has more than %d columns\n", row, MAXCOLS);
fclose(fa); fclose(fb);
return 2;
}
if (nA == 0 && nB == 0) {
/* Blank line at the same position on both sides -- most
* commonly a trailing blank at EOF. Both cursors already
* advanced together this iteration, so tolerating this
* never desyncs row alignment; just don't count it as a
* compared data row. */
row--;
continue;
}
/* Exactly one side blank: NOT a skip. Skipping here was the
* bug -- it silently swallowed a row where one file has real
* content and the other doesn't, which is a genuine
* difference, not noise. Compare it like any other row by
* treating the blank side as all-empty-string fields, so a
* blank-vs-real-data row surfaces as a normal per-column
* mismatch instead of vanishing. */
if (nA == 0) {
for (k = 0; k < ncols; k++) fieldsA[k] = (char *)"";
nA = ncols;
}
if (nB == 0) {
for (k = 0; k < ncols; k++) fieldsB[k] = (char *)"";
nB = ncols;
}
if (nA != ncols || nB != ncols) {
fprintf(stderr,
"field_diff: row %ld is ragged ('%s' has %d fields, '%s' has "
"%d, header has %d)\n", row, pathA, nA, pathB, nB, ncols);
fclose(fa); fclose(fb);
return 2;
}
total_rows_a++;
total_rows_b++;
for (i = 0; i < ncols; i++) {
if (strcmp(fieldsA[i], fieldsB[i]) != 0) {
mismatch_count[i]++;
if (!have_first) {
have_first = 1;
row_row = row;
row_col = i;
strncpy(row_a, fieldsA[i], sizeof(row_a) - 1); row_a[sizeof(row_a)-1] = '\0';
strncpy(row_b, fieldsB[i], sizeof(row_b) - 1); row_b[sizeof(row_b)-1] = '\0';
}
}
}
}
/* count any remaining rows on the longer side (for the row-count
* note) -- the line that triggered extra_a/extra_b is already in
* lineA/lineB, so count it before fetching further. */
if (extra_a) {
if (split_csv(lineA, fieldsA, MAXCOLS) > 0) total_rows_a++;
while (fgets(lineA, sizeof(lineA), fa))
if (split_csv(lineA, fieldsA, MAXCOLS) > 0) total_rows_a++;
}
if (extra_b) {
if (split_csv(lineB, fieldsB, MAXCOLS) > 0) total_rows_b++;
while (fgets(lineB, sizeof(lineB), fb))
if (split_csv(lineB, fieldsB, MAXCOLS) > 0) total_rows_b++;
}
fclose(fa);
fclose(fb);
if (!have_first && total_rows_a == total_rows_b) {
printf("field_diff: %ld rows, %d columns, identical\n", total_rows_a, ncols);
return 0;
}
if (have_first)
printf("field_diff: first diff at row=%ld col=%s a=%s b=%s\n",
row_row, names[row_col], row_a, row_b);
if (total_rows_a != total_rows_b)
printf("field_diff: row count mismatch -- %s has %ld rows, %s has %ld rows\n",
pathA, total_rows_a, pathB, total_rows_b);
printf("field_diff: per-column mismatch summary (over %ld compared rows):\n",
row);
for (i = 0; i < ncols; i++)
if (mismatch_count[i] > 0)
printf(" %-16s %ld/%ld mismatches\n", names[i], mismatch_count[i], row);
return 1;
}