Issue #566 proposes hoisting gamut-ifd's "two-reader differential" into the crate's
invariants module, so that both the pinned-seed property tier and the fuzz tier drive one copy
of it. The comparison it names is not a differential, and invariants is the wrong home for
it. This issue records why, so nobody implements #566 as written. (Filed rather than commented
on #566, because the run that found this does not edit existing issues.)
The comparison cannot fail
crates/gamut-ifd/src/reader.rs defines the slice entry points as the streaming ones:
pub fn read(data: &[u8]) -> Result<TiffFile> {
crate::IfdReader::open(data)?.read_file()
}
pub fn read_tree(data: &[u8], pointer_tags: &[u16]) -> Result<TiffFile> {
crate::IfdReader::open(data)?.read_tree(pointer_tags)
}
The "other door" in the comparison — IfdReader::open(data).and_then(|mut r| r.read_file()) — is
character-for-character the body of the first door. src/stream.rs's module docs say so outright:
"This module is the parser: the slice functions are thin wrappers over an IfdReader<&[u8]>,
so there is exactly one directory-body walk."
Falsifier, executed before filing: read_file was modified to drop the last directory of a
multi-directory chain, and a hand-built two-directory file was run through the comparison. No
disagreement was reported — both sides dropped the directory, because both sides are the same
call.
Why invariants specifically is the wrong home
docs/testing.md gives invariants a precise job: a law is written once there and driven from
both a pinned-seed proptest and the fuzz tier, because "a property is the specification a fuzzer
checks". A function that returns the same answer for every input is not a specification of
anything — putting it in invariants would make the repository's strongest testing construct
assert a tautology, and would spend fuzz budget searching an empty space. gamut-ifd's
ifd_read fuzz target measured the cost directly: dropping the two duplicate parses took it from
roughly 12 000 exec/s to roughly 20 000.
What the claim actually is, and where it already lives
The claim worth keeping is a structure pin: read must go on delegating rather than growing a
second directory walk with a second set of hostile-input guards to drift. That is a statement about
two function bodies, not about any input, so it needs one bounded run, not a search. It already
has one — crates/gamut-ifd/tests/robustness.rs's survives helper drives both doors over the
exhaustive truncation and single-byte-overwrite corpus — and PR #568 relabels it there as a
structure pin rather than a differential.
What is left of #566, and is worth doing
The second half of #566 stands on its own and is unaffected by the above: tests/robustness.rs
compares two parses with assert_eq! on TiffFile, which derives PartialEq, not Eq. A
FLOAT/DOUBLE field holds f32/f64, and NaN != NaN makes the comparison non-reflexive, so
two identical parses can report a disagreement. The fixtures there happen never to produce a
NaN; a fuzz target reached one within a minute. That is a real latent defect in a test and should
be fixed — by comparing a total rendering (the Debug form) as PR #568's target did before the
comparison was dropped, or by making the fixture's float fields explicit.
Suggested disposition
Refs #566, #264, #568.
Issue #566 proposes hoisting
gamut-ifd's "two-reader differential" into the crate'sinvariantsmodule, so that both the pinned-seed property tier and the fuzz tier drive one copyof it. The comparison it names is not a differential, and
invariantsis the wrong home forit. This issue records why, so nobody implements #566 as written. (Filed rather than commented
on #566, because the run that found this does not edit existing issues.)
The comparison cannot fail
crates/gamut-ifd/src/reader.rsdefines the slice entry points as the streaming ones:The "other door" in the comparison —
IfdReader::open(data).and_then(|mut r| r.read_file())— ischaracter-for-character the body of the first door.
src/stream.rs's module docs say so outright:"This module is the parser: the slice functions are thin wrappers over an
IfdReader<&[u8]>,so there is exactly one directory-body walk."
Falsifier, executed before filing:
read_filewas modified to drop the last directory of amulti-directory chain, and a hand-built two-directory file was run through the comparison. No
disagreement was reported — both sides dropped the directory, because both sides are the same
call.
Why
invariantsspecifically is the wrong homedocs/testing.mdgivesinvariantsa precise job: a law is written once there and driven fromboth a pinned-seed
proptestand the fuzz tier, because "a property is the specification a fuzzerchecks". A function that returns the same answer for every input is not a specification of
anything — putting it in
invariantswould make the repository's strongest testing constructassert a tautology, and would spend fuzz budget searching an empty space.
gamut-ifd'sifd_readfuzz target measured the cost directly: dropping the two duplicate parses took it fromroughly 12 000 exec/s to roughly 20 000.
What the claim actually is, and where it already lives
The claim worth keeping is a structure pin:
readmust go on delegating rather than growing asecond directory walk with a second set of hostile-input guards to drift. That is a statement about
two function bodies, not about any input, so it needs one bounded run, not a search. It already
has one —
crates/gamut-ifd/tests/robustness.rs'ssurviveshelper drives both doors over theexhaustive truncation and single-byte-overwrite corpus — and PR #568 relabels it there as a
structure pin rather than a differential.
What is left of #566, and is worth doing
The second half of #566 stands on its own and is unaffected by the above:
tests/robustness.rscompares two parses with
assert_eq!onTiffFile, which derivesPartialEq, notEq. AFLOAT/DOUBLEfield holdsf32/f64, andNaN != NaNmakes the comparison non-reflexive, sotwo identical parses can report a disagreement. The fixtures there happen never to produce a
NaN; a fuzz target reached one within a minute. That is a real latent defect in a test and shouldbe fixed — by comparing a total rendering (the
Debugform) as PR #568's target did before thecomparison was dropped, or by making the fixture's float fields explicit.
Suggested disposition
invariantshoist. Close test(ifd): hoist the two-reader differential into invariants, and stop comparing parses with PartialEq #566 as superseded, or reduce it to thePartialEq/NaNfix alone.crates/gamut-ifd/tests/robustness.rs, named as a structure pin.Refs #566, #264, #568.