The invariant
Ifd::set_sub_ifd (crates/gamut-ifd/src/entry.rs) says:
The pointer field (a LONG/LONG8 array of the children's offsets) is synthesised by the
writer, so tag must not also be set as a regular field.
Nothing checks it. set and set_sub_ifd write to two independent stores, so
ifd.set_sub_ifd(tags::EXIF_IFD, vec![child]);
ifd.set(tags::EXIF_IFD, Value::Long(vec![8])); // accepted
leaves a directory that violates the documented contract, and write then resolves the conflict
silently in whichever direction its emit order happens to pick.
Why it matters beyond the setter
The half a consumer can see is worse than the half it cannot. A reader decides "this tag is a
pointer" from a directory's fields (ifd.get(tag) with a LONG/IFD/LONG8/IFD8 value),
because that is all a file carries; a caller building a directory in memory expresses the same
thing as a group. A crate that validates only the groups it was handed is therefore blind to
exactly the shape a reader misreads — which is the defect PR #520 repaired in gamut-tiff
(TiffMetadata::check inspected sub_ifds() while resolve_pointers inspects get(tag), so a
standard pointer tag carried as a plain LONG encoded cleanly and then failed the crate's own
reader with read out of bounds or sub-IFD pointer loop).
gamut-tiff now guards its own subtree, and gamut-exif uses the group setter correctly, so no
in-workspace path is currently broken. But the guard is per-consumer, and the invariant belongs to
the type that documents it: every present and future consumer of gamut-ifd has to rediscover it,
and a hand-built or third-party-parsed directory reaches all of them.
Options
- Make it unrepresentable.
set refuses (or debug_asserts against) a tag that already has a
group, and set_sub_ifd removes any field under that tag — the cheapest fix, and it makes the
doc sentence true by construction. set is currently infallible, so refusing means either a
fallible variant or silent precedence; removing on set_sub_ifd and documenting the precedence
is the smaller change.
- Expose the check. A
pub fn — or an entry in the existing invariants module — that
answers "does this directory carry a pointer tag as both a field and a group", so every consumer
validates identically rather than each writing its own.
- Validate in
write. Turn the conflict into a typed error at the one place the ambiguity
actually has to be resolved. Catches every consumer, but only at the last moment and only for
directories that get written.
(2) composes best with how gamut-tiff already validates its Exif subtree; (1) is the one that
makes the class impossible.
Where
crates/gamut-ifd/src/entry.rs — Ifd::set, Ifd::set_sub_ifd; crates/gamut-ifd/src/writer.rs.
Found while reviewing #446 / PR #520; gamut-ifd is frozen base territory for that lane, so the
repair was made in the consumer and the question is raised here.
The invariant
Ifd::set_sub_ifd(crates/gamut-ifd/src/entry.rs) says:Nothing checks it.
setandset_sub_ifdwrite to two independent stores, soleaves a directory that violates the documented contract, and
writethen resolves the conflictsilently in whichever direction its emit order happens to pick.
Why it matters beyond the setter
The half a consumer can see is worse than the half it cannot. A reader decides "this tag is a
pointer" from a directory's fields (
ifd.get(tag)with aLONG/IFD/LONG8/IFD8value),because that is all a file carries; a caller building a directory in memory expresses the same
thing as a group. A crate that validates only the groups it was handed is therefore blind to
exactly the shape a reader misreads — which is the defect PR #520 repaired in
gamut-tiff(
TiffMetadata::checkinspectedsub_ifds()whileresolve_pointersinspectsget(tag), so astandard pointer tag carried as a plain
LONGencoded cleanly and then failed the crate's ownreader with
read out of boundsorsub-IFD pointer loop).gamut-tiffnow guards its own subtree, andgamut-exifuses the group setter correctly, so noin-workspace path is currently broken. But the guard is per-consumer, and the invariant belongs to
the type that documents it: every present and future consumer of
gamut-ifdhas to rediscover it,and a hand-built or third-party-parsed directory reaches all of them.
Options
setrefuses (ordebug_asserts against) a tag that already has agroup, and
set_sub_ifdremoves any field under that tag — the cheapest fix, and it makes thedoc sentence true by construction.
setis currently infallible, so refusing means either afallible variant or silent precedence; removing on
set_sub_ifdand documenting the precedenceis the smaller change.
pub fn— or an entry in the existinginvariantsmodule — thatanswers "does this directory carry a pointer tag as both a field and a group", so every consumer
validates identically rather than each writing its own.
write. Turn the conflict into a typed error at the one place the ambiguityactually has to be resolved. Catches every consumer, but only at the last moment and only for
directories that get written.
(2) composes best with how
gamut-tiffalready validates its Exif subtree; (1) is the one thatmakes the class impossible.
Where
crates/gamut-ifd/src/entry.rs—Ifd::set,Ifd::set_sub_ifd;crates/gamut-ifd/src/writer.rs.Found while reviewing #446 / PR #520;
gamut-ifdis frozen base territory for that lane, so therepair was made in the consumer and the question is raised here.