You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Exif 3.0 §4.6.9.2 Table 21 (Tag Support Levels (5) — 1st IFD TIFF Tag) keys every 1st IFD tag's
support level on four columns, selected by the thumbnail's Compression (0x0103) value: Uncompressed Chunky, Uncompressed Planar, YCC, and Compressed. For the thumbnail pointer
pair the row reads (extracted from references/exif/exif-3.0-dc-008-translation-2023.pdf):
Field name
Tag
Unc. Chunky
Unc. Planar
YCC
Compressed
JPEGInterchangeFormat
513 / 0x201
N
N
N
M
JPEGInterchangeFormatLength
514 / 0x202
N
N
N
M
N is "It is not allowed to record"; M is "Mandatory (shall be recorded)". So the pair is
mandatory only for a compressed thumbnail, and forbidden outright for an uncompressed one.
What gamut-exif does
ExifReader's thumbnail path (crates/gamut-exif/src/stream.rs, read_thumbnail) never reads Compression. Its rule is unconditional in both directions:
JPEGInterchangeFormat present, JPEGInterchangeFormatLength absent → strict rejects with BadThumbnail, lenient records a drop.
JPEGInterchangeFormatLength present, JPEGInterchangeFormat absent → accepted silently in both
modes (nothing is addressed, so nothing was dropped).
PR #522 narrowed the documentation so nothing claims Table 21 as the authority for that rule —
the rule now stands on the structural fact that an offset with nothing to size it addresses an
unreadable range. The behavioural question is deliberately left open here, because changing it
is an accept-to-reject or reject-to-accept move that wants its own review round and its own
equivalence sweep.
The two open questions
Should the strict rule be conditioned on Compression? Today an uncompressed thumbnail
(Compression = 1) that records only a JPEGInterchangeFormat is rejected. Table 21 forbids
recording that tag there at all, so the blob is malformed either way — but the reason gamut-exif gives is "the length is missing", and the length is a tag the same table forbids.
Options: (a) leave it unconditional and keep the message purely structural (today, after feat(exif): streaming ReadAt entry point + a report of what a lenient parse dropped #522);
(b) reject an uncompressed thumbnail carrying either JPEG tag, as N says; (c) only apply the
pair rule when Compression = 6.
Should compressed-with-only-a-length also be rejected? For Compression = 6 both tags are M, so a 1st IFD carrying only JPEGInterchangeFormatLength is exactly as malformed as one
carrying only the offset — yet strict accepts it. PR feat(exif): streaming ReadAt entry point + a report of what a lenient parse dropped #522's decision 12 rejects an alternative
on the grounds that "half a mandatory pair is malformed", which argues for symmetry; the
counter-argument is that a length alone addresses nothing, so no bytes were lost and there is
nothing to name in a report. Rejecting it would need a DroppedRegion/DropReason pairing for
a region with no address, or a strict-only error with no lenient counterpart.
Neither direction regresses master: on master both cases parse silently, and #522 already
changed one of them. Changing the other, or conditioning either on a tag the reader does not read
today, is a behavioural change in strict mode that needs its own equivalence sweep against a
corpus — the same measurement #522 ran for the change it did make (12 of 3 144 sweep cases). Doing
it inside a round that had otherwise converged would have reopened the review.
Where to look
crates/gamut-exif/src/stream.rs — read_thumbnail, the (Some(_), None) and (None, _) arms.
crates/gamut-exif/src/report.rs — DropReason::ThumbnailLengthMissing, named for exactly this
one site.
crates/gamut-exif/tests/report.rs — a_thumbnail_offset_without_a_length_is_named and a_thumbnail_with_no_jpeg_range_reports_nothing, which pin both directions as they stand.
What Table 21 actually says
Exif 3.0 §4.6.9.2 Table 21 (Tag Support Levels (5) — 1st IFD TIFF Tag) keys every 1st IFD tag's
support level on four columns, selected by the thumbnail's
Compression(0x0103) value:Uncompressed Chunky, Uncompressed Planar, YCC, and Compressed. For the thumbnail pointer
pair the row reads (extracted from
references/exif/exif-3.0-dc-008-translation-2023.pdf):JPEGInterchangeFormat0x201JPEGInterchangeFormatLength0x202Nis "It is not allowed to record";Mis "Mandatory (shall be recorded)". So the pair ismandatory only for a compressed thumbnail, and forbidden outright for an uncompressed one.
What
gamut-exifdoesExifReader's thumbnail path (crates/gamut-exif/src/stream.rs,read_thumbnail) never readsCompression. Its rule is unconditional in both directions:JPEGInterchangeFormatpresent,JPEGInterchangeFormatLengthabsent → strict rejects withBadThumbnail, lenient records a drop.JPEGInterchangeFormatLengthpresent,JPEGInterchangeFormatabsent → accepted silently in bothmodes (nothing is addressed, so nothing was dropped).
PR #522 narrowed the documentation so nothing claims Table 21 as the authority for that rule —
the rule now stands on the structural fact that an offset with nothing to size it addresses an
unreadable range. The behavioural question is deliberately left open here, because changing it
is an accept-to-reject or reject-to-accept move that wants its own review round and its own
equivalence sweep.
The two open questions
Compression? Today an uncompressed thumbnail(
Compression = 1) that records only aJPEGInterchangeFormatis rejected. Table 21 forbidsrecording that tag there at all, so the blob is malformed either way — but the reason
gamut-exifgives is "the length is missing", and the length is a tag the same table forbids.Options: (a) leave it unconditional and keep the message purely structural (today, after feat(exif): streaming ReadAt entry point + a report of what a lenient parse dropped #522);
(b) reject an uncompressed thumbnail carrying either JPEG tag, as
Nsays; (c) only apply thepair rule when
Compression = 6.Compression = 6both tags areM, so a 1st IFD carrying onlyJPEGInterchangeFormatLengthis exactly as malformed as onecarrying only the offset — yet strict accepts it. PR feat(exif): streaming ReadAt entry point + a report of what a lenient parse dropped #522's decision 12 rejects an alternative
on the grounds that "half a mandatory pair is malformed", which argues for symmetry; the
counter-argument is that a length alone addresses nothing, so no bytes were lost and there is
nothing to name in a report. Rejecting it would need a
DroppedRegion/DropReasonpairing fora region with no address, or a strict-only error with no lenient counterpart.
Why it is filed rather than decided in #522
Neither direction regresses
master: onmasterboth cases parse silently, and #522 alreadychanged one of them. Changing the other, or conditioning either on a tag the reader does not read
today, is a behavioural change in
strictmode that needs its own equivalence sweep against acorpus — the same measurement #522 ran for the change it did make (12 of 3 144 sweep cases). Doing
it inside a round that had otherwise converged would have reopened the review.
Where to look
crates/gamut-exif/src/stream.rs—read_thumbnail, the(Some(_), None)and(None, _)arms.crates/gamut-exif/src/report.rs—DropReason::ThumbnailLengthMissing, named for exactly thisone site.
crates/gamut-exif/src/reader.rs—a_thumbnail_offset_without_a_length_is_rejected_strictly.crates/gamut-exif/tests/report.rs—a_thumbnail_offset_without_a_length_is_namedanda_thumbnail_with_no_jpeg_range_reports_nothing, which pin both directions as they stand.Refs #419, #522.