D511 - Two formatter bugs in one run, both in the padding nobody tested

measured - 2026-09-03 (twenty-three sprintf cases against glibc 2.39)

sprintf had no differential coverage. Twenty-three cases later, two conformance bugs, both in the same three lines:

sprintf/width-zero-negative: buffer is 30302d3432…, expected 2d30303432…
sprintf/precision:           returned 0x2, glibc returned 0x3

%05d of -42 rendered 00-42

The padding was applied uniformly after rendering, so a negative number got its zero fill in front of its sign. ISO C 7.21.6.1 puts the 0 flag's fill after any sign, which is what makes -0042 and not 00-42.

Nothing here could have noticed. The string is the right length, the digits are right, and it is only wrong in the order of two characters - a guest parsing it back gets a number it cannot read, somewhere with no connection to the formatting.

%.3d of 42 rendered 42

Precision was read by read_specifier, applied to %s - where it truncates - and to nothing else. For d i o u x X it is the minimum number of digits, zero-filled on the left, so 42 must render as 042 and the return must be 3.

Two further rules came with it and are implemented rather than noticed later: a specified precision makes the 0 flag ignored, and a precision of zero with a value of zero renders no characters at all.

Why the cases found it and the previous ones did not

snprintf has had cases since the differential was built, and every one of them formats %d with no flags. The interesting failures in a formatter are not in any one conversion, they are in the specifier parser - a width read as a precision, a 0 dropped for a left-justified field, a %% that consumes an argument.

So the format string is a case input here rather than a family of helpers, and the twenty- three cases sweep the conversions and the flags rather than the values. That shape is the finding, more than either bug.

Both were watched failing, separately, by reverting each half of the fix on its own - a single break would have left the other half unproven.

And the dispatch was consolidated because it tripped the lint twice in a day

run in the differential test grew an if block per shape and hit the 100-line ceiling twice while these cases were being added. It is one match naming which replay each function takes, so adding a shape is a line rather than a block, and a reader asking how memcpy is replayed has a list rather than a chain to walk.