mirror of
https://github.com/libretro/virtualjaguar-libretro.git
synced 2026-11-09 17:30:53 -10:00
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]>
236 lines
7.9 KiB
C
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;
|
|
}
|