The working file. Wrong turns, compiler errors, and the fix that worked. Kept because on re-read these are worth more than the polished README.
gcc is not on PATH by default here — it lives in MSYS2's UCRT64 tree, installed for the
COBOL track:
export PATH="/c/msys64/ucrt64/bin:$PATH"
gcc -std=c99 -Wall -Wextra -O2 -o build/logagg.exe logagg.c
Clean first time with -Wall -Wextra, which is not usually how this goes.
UCRT64 and not MINGW64. The two MSYS2 environments link against different Microsoft C
runtimes — MSVCRT is the old one and is not C99-compatible; UCRT is what Visual Studio
itself uses (the MSYS2 docs explain the split).
The concrete difference here: MSVCRT's printf does not handle %lld correctly — it wants
%I64d instead — while UCRT follows the standard. Getting this wrong would have produced
garbage in every numeric column, and the failure would have been silent.
Every \n printed becomes \r\n. The whole diff fails and the failure is invisible in a
terminal, which renders both identically.
#ifdef _WIN32
# include <io.h>
# include <fcntl.h>
_setmode(_fileno(stdout), _O_BINARY);
#endif
There is no portable way to express this — it is a real platform difference. All three
tracks hit it and all three solve it differently (Java writes the bytes itself, Python uses
sys.stdout.buffer). That comparison is more instructive than any of the three fixes.
verify.py has a dedicated check for it, because the symptom is so unhelpful:
if b"\r" in actual and b"\r" not in expected:
return "output contains CR -- stdout is in text mode (SPEC 4 requires LF)"
%-20s pads but does not truncateSPEC 4 says the service column is 20 wide and truncated at 20. printf("%-20s", name)
pads a short name and happily runs long on a 25-character one, shifting every subsequent
column. The precision field is what truncates:
printf("%-20.20s%9lld...", nd->service, ...);
No service in the fixture is over 20 characters, so this would have passed the diff and sat there as a latent bug. Caught by reading the spec rather than by testing — the lesson is uncomfortable but real.
strcmp, not strcollSPEC 3 wants byte-wise ordering. strcoll sorts by the current locale's collation, so the
answer would change with LC_COLLATE. Same trap as Java's Collator and, in a different
costume, as Python's default str.lower() on Turkish input. Anything the machine reads
must not have the user's locale in the path.
realloc — the one-line leakchar *p = realloc(l->buf, cap);
if (!p) return 0;
l->buf = p;
Writing l->buf = realloc(l->buf, cap) directly is the classic C memory bug. On failure
realloc returns NULL and leaves the original block allocated — so the assignment
overwrites the only pointer to it:
correct: p = realloc(l->buf, cap) → NULL
l->buf still points at the old block; caller frees it later at line_free
wrong: l->buf = realloc(l->buf, cap) → NULL
l->buf is now NULL; the old block is allocated, unreachable, leaked
The correct form needs a temporary at every call site. The behaviour is documented — the
realloc man page states the
original block is left untouched on failure — and CERT C's memory-management (MEM) rules
cover the pattern, because the failure is a leak on the error path: exactly the path
nobody tests.
split_fields writes NUL bytes into the line buffer and returns pointers into it.
Nothing is allocated and nothing survives the next read_line. Which means:
n->service = malloc(strlen(service) + 1);
strcpy(n->service, service);
The table must copy its keys. Store the borrowed pointer instead and every key in the hash table silently becomes the last line of the file — a bug that produces a plausible-looking report with completely wrong numbers. The README's Q3 diagram draws the two pointer kinds side by side.
Nothing in the type system distinguishes "pointer I own" from "pointer I am borrowing". The distinction lives in a comment. Rust's borrow checker is that same distinction moved into the compiler, and it is worth maintaining the comment version once before reading the enforced version.
--shard N (SPEC 6): per-thread Table, merged after pthread_join — the shape is
in the README's Q6 diagram. Want to also write the shared-table-plus-mutex version
and measure the difference.-fsanitize=address,undefined and confirm clean. The manual reasoning says
it is; the reasoning is not the evidence.valgrind --leak-check=full — needs a Linux box or WSL; not available in MSYS2.logagg reads a file and writes stdout, which is exactly the shape
libFuzzer/afl++ are built for — wire the parser to a buffer and let it run.-O0 and -O2 and diff. No UB is believed to be present, so
they should agree; confirming that is cheap and the failure mode is instructive.