Skip to content

gamut-exif: should the thumbnail JPEG pair rule be conditioned on Compression? #574

Description

@justin13888

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):

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

  1. 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.
  2. 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.

Why it is filed rather than decided in #522

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.rsread_thumbnail, the (Some(_), None) and (None, _) arms.
  • crates/gamut-exif/src/report.rsDropReason::ThumbnailLengthMissing, named for exactly this
    one site.
  • crates/gamut-exif/src/reader.rsa_thumbnail_offset_without_a_length_is_rejected_strictly.
  • crates/gamut-exif/tests/report.rsa_thumbnail_offset_without_a_length_is_named and
    a_thumbnail_with_no_jpeg_range_reports_nothing, which pin both directions as they stand.

Refs #419, #522.

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions