Skip to content

test(ifd): do not hoist the two-reader comparison into invariants — it is a tautology (supersedes #566) #592

Description

@justin13888

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions