feat(metadata): wire the facade into gamut-jpeg, gamut-jxl and gamut-heic + capability query - #509
Draft
justin13888 wants to merge 15 commits into
Draft
justin13888 wants to merge 15 commits into
justin13888 wants to merge 15 commits into
Conversation
Add `gamut_metadata::capability`: `Format`, `Carrier` and `Direction` enums (`repr(u8)`, append-only discriminants, `ALL` constants) with a `const fn supports(format, carrier, direction)` answering whether the format crate can locate or write a carrier as a raw payload, and `const fn typed_wiring(format)` saying whether that crate also exposes the facade's typed models behind its `metadata` feature. The table is a transcription of each crate's STATUS.md; every arm cites the row that justifies it, and a full-matrix test pins every cell. It is a const table rather than a runtime registry because the format set is the workspace's own and the release topology forbids the facade depending on a format crate. Refs #420, #216
Add an optional, normal dependency on gamut-metadata behind a new `metadata` Cargo feature (off by default), and the gamut-dng pattern on top of the raw APP-segment surface: - `JpegMetadata::blocks` hands the located payloads over as `MetadataBlock`s (EXIF = the TIFF stream without `Exif\0\0`, XMP = the xpacket, ICC = the reassembled profile) and `JpegMetadata::metadata` parses them into a unified `Metadata`; - `JpegEncoder::with_metadata(&Metadata)` embeds through the default `MetadataEmbedder` and `with_encoded_metadata(&EncodedMetadata)` accepts caller-chosen policies, routing each carrier to the existing raw setter. IPTC-IIM (APP13) and C2PA (APP11) blocks are typed `Unsupported`, never dropped; a manifest store is never copied forward. The typed extract -> embed -> extract equality is pinned through the stream. The exiv2 oracle has no JPEG reader, so the container-level differential cell is recorded as untested in STATUS.md. Refs #420
Add `JxlDecoder::metadata` -> `JxlMetadata { exif, xmp, icc }`, reading
a stream's `Exif` / `xml ` container boxes and its codestream ICC
profile without decoding pixels. jxl-rs consumes auxiliary boxes without
exposing them (jxl-rs #674) and its box-header parser is `pub(super)`,
so the crate walks the top-level box sequence itself: the 32-bit,
`size == 0` and 64-bit `largesize` forms, the `Exif` payload's
tiff-header offset applied (ISO/IEC 23008-12 A.2.1, reused by the JXL
container), first box of a kind wins, a `brob`-wrapped `Exif`/`xml `
box is a typed `Unsupported`, and every overrun is `InvalidInput`.
Behind a new optional `metadata` feature (a normal dependency on
gamut-metadata), add the gamut-dng pattern: `JxlMetadata::blocks` /
`metadata`, and `JxlEncoder::with_metadata(&Metadata)` /
`with_encoded_metadata(&EncodedMetadata)` routing EXIF (`Exif\0\0`
stripped) and XMP to the boxes and the ICC profile to `ColorSpec::Icc`.
IPTC-IIM and C2PA blocks are typed `Unsupported`; a manifest store is
never copied forward.
The read-back is pinned against what the encoder writes, the walk's size
forms and hostile-input refusals are unit-tested beside it, and the
facade's typed extract -> embed -> extract equality holds through the
container. The exiv2 oracle has no JPEG XL reader, so that cell is
recorded as untested in STATUS.md.
Refs #420
The `metadata` wiring landed without a nightly `cargo fmt --all` pass, so `fmt-check` has been failing on this branch. Formatting only; no behaviour changes.
The `metadata` wiring landed without a nightly `cargo fmt --all` pass, so `fmt-check` has been failing on this branch. Formatting only; no behaviour changes.
`read_box` returned the offset just past the box it read, so the walk's progress lived in the value a callee returned: a `read_box` that reported a non-advancing offset left `container_metadata_boxes` looping forever. Under `cargo mutants --in-diff` that is seven return-value mutants of `read_box` that no test can kill because they hang instead of failing, and the incremental gate reports them as timeouts. Split the header parse from the walk. `parse_box_header` now only reports what the header claims -- type, header length, box length -- and slices nothing; the walk owns every bound: the box must fit in what remains, it must be at least the 8-byte minimum header, and the body must be a range inside it. The step is then at least 8 bytes per iteration whatever the parser reports, so the loop terminates for any return value a mutant can produce and the mutants become killable by an ordinary assertion. Behaviour is unchanged: every fault keeps the message the tests already pin -- the 64-bit form declaring a `largesize` below its own 16-byte header still ends as `malformed box size` (the body range is empty-to-negative), and a `largesize` no address space can hold saturates and is reported by the overrun check.
The walk's minimum step is the 8-byte header, so a box whose `size` is exactly that header is legal §4.2 framing carrying no payload. Nothing asserted it, and `cargo mutants --in-diff` reported the boundary open: relaxing the rule to `box_len <= 8` -- which rejects the empty box -- survived the suite. Assert both halves of the boundary: an empty `free` box between the metadata boxes is stepped over and the walk keeps going, and an empty `xml ` box yields an empty payload rather than an absent one.
The crate located the Exif and XMP items and the `colr` property already, but handed every payload back opaque, so a caller wanting a typed model had to know the `ExifDataBlock` framing and the `colr` variants itself. Add the two lenses that framing needs, ungated: `HeifItem::exif_tiff_stream` applies the payload's 4-byte big-endian `exif_tiff_header_offset` and yields the TIFF stream `gamut-exif` parses (ISO/IEC 23008-12 §A.2.1), refusing a non-Exif item, a payload shorter than the offset field and an offset past the payload's end; `HeifItem::icc_profile` yields the `rICC`/`prof` bytes whichever order the item's `colr` properties are in, where `colour()` reports only the first. Over them, behind an optional `metadata` feature (off by default, a normal optional dependency so release ordering follows it), `HeifImage::blocks` hands the three located payloads to the facade as `MetadataBlock`s and `HeifImage::metadata` parses them into a unified `Metadata`. Both are fallible: a hostile Exif item can carry a truncated or out-of-range offset, and a facade parse failure is carried as `InvalidInput` with the facade's message, naming the carrier, as the error's detail. HEIF has no IPTC-IIM item type, and a C2PA manifest store lives in a top-level `uuid` box outside the item model, so neither block is produced here; `HeifContainer::c2pa` still locates the store and STATUS.md records that a caller appends it itself.
…umbrella
`gamut-jpeg`, `gamut-jxl` and `gamut-heic` each gained an optional `metadata`
feature carrying the typed accessors (`blocks()`, `metadata()`,
`with_metadata`). The umbrella's own `metadata` feature enabled only the
metadata crates, so `gamut = { features = ["jpeg", "metadata"] }` compiled the
facade and the codec but not the wiring between them: reaching
`JpegMetadata::blocks` meant depending on `gamut-jpeg` directly, which is what
the umbrella exists to avoid.
Add the three weak forwards. Weak (`?/`) is what keeps both directions honest:
`metadata` alone still pulls in no codec, and a format alone still pulls in no
facade -- each forward fires only when that format's feature already brought the
crate into the graph.
Verified at the rustc invocation rather than by inspection: with
`--features jpeg,jxl,heic,metadata` all three crates are compiled with
`--cfg feature="metadata"` and five accessors named only through `gamut::` go
from a compile error to a compile; with `--features jpeg,jxl,heic` the same
three are compiled without it; with the formats alone no facade crate is in the
dependency graph, and with `metadata` alone no codec crate is.
No new feature name is introduced, so `gamut-ffi`'s mirrored table is unchanged,
and no package is added, so the lockfile is unchanged.
The forwards added in the previous commit had nothing holding them: `mise run test` reported the same 3816 tests before and after them, because no test named anything the forwards switch on. A feature edge that nothing notices when it disappears is the defect this suite exists to catch, so it should not be the shape the fix itself ships in. Pin both directions, by different techniques, because only one of them is observable from a compiled build. `the_metadata_feature_reaches_each_format_ crates_accessors` names one accessor per crate -- the fewest it takes to observe the three edges -- and each exists only under that crate's `metadata` feature, so a dropped forward is a compile error; nothing is called and no fixture is built, so a fixture bug or a signature change cannot fail it. `every_format_metadata_forward_is_weak` reads the compiled-in manifest and asserts each entry carries the `?`, which is the whole of what stops `metadata` alone from pulling three codecs into a build that asked for none. It checks the non-weak form first so that dropping the `?` is diagnosed as dropping the `?` rather than as a missing forward. Both were falsified before landing: deleting a forward fails the resolution test at compile time naming that crate, and removing a `?` fails the weakness test with the message about weakness. `crates/gamut/tests/` is mutation-invisible and `AGENTS.md` forbids pinning anything there *by choice*. This is the linkage exception the same rule names: the edge under test is in the umbrella's own feature graph, no lower crate can observe who enabled its features, and the three format crates must not gain dev-dependency edges on one another. The module docs say so at the test.
The weakness guard matched its three forwards over the whole manifest, so it could not see which feature list an entry belonged to. Moving `"gamut-jpeg?/metadata"` out of `metadata = [ … ]` and into `jpeg = [ … ]` left both assertions passing while making `gamut --features jpeg` resolve the format crate with its metadata wiring — and therefore the entire facade — which is the build the weak form exists to prevent. Slice the feature's own entry list out of the manifest and assert presence over that, plus a manifest-wide count of one so the entry cannot also be attached to a format feature. Both attacks now fail, each naming its own cause. The resolution half named one accessor per crate, so a dropped forward and a renamed accessor produced the same compile error. Add a feature witness per crate — the facade's `Metadata`, re-exported under each crate's own `metadata` cfg — which the forward breaks and a rename does not, so the two faults differ by which errors appear.
This was referenced Sep 10, 2026
Open
The table's prose asserted two things the crates disprove. `typed_wiring` was documented as answering for "that crate's `metadata` Cargo feature". `gamut-dng` has no such feature — it depends on `gamut-metadata` unconditionally, since its `DngMetadata` holds the facade's `Exif` by value — so a reader following the instruction reaches a hard cargo error. Three of the four wired crates gate the surface; name them, and name DNG as the one that does not. `supports` was documented as the surface "every format crate ships unconditionally". `gamut-jxl` gates its reader on `decode` and its encoder on `encode`, so a build with `default-features = false, features = ["encode"]` compiles no reader while the table answers `true` for `Read`. A `const fn` can see neither another crate's features nor the target, so say what is true: the table describes the surface a crate defines, not what a build compiled, and name the gated case. The cells themselves are unchanged and were verified correct.
`Format::ALL` and `Carrier::ALL` were fixed-length arrays on `#[non_exhaustive]` enums, so appending a variant would change each constant's *type* and break every caller who had named one — the exact breakage `#[non_exhaustive]` exists to prevent, and it would have shipped baked into new API. A `&'static [Self]` absorbs the append. `Direction` is exhaustive and cannot gain a variant, so it keeps its array; the docs now say why the two differ. The discriminant pin collects instead of mapping over an array, which keeps the length inside what it compares.
…spec `with_metadata` routes a present ICC profile to `with_color(ColorSpec::Icc(..))`, so it overwrites a colour encoding the caller chose through a different builder call, not merely an earlier profile — JPEG XL is the one wired format where the profile *is* the codestream's colour encoding rather than a container payload. The docs said only that absent carriers leave earlier settings untouched, which left the present case for a caller to discover. State the precedence and the ordering it implies. Whether last-write-wins is the right rule here, or the conflict should be refused, is issue #626; this records today's behaviour rather than settling it.
The consumer-integration section described what `with_metadata` embeds but not what it displaces. A carrier absent from the model leaves an earlier setting untouched and a present one overwrites it, which matters most for ICC in JPEG XL, where the profile is the codestream's colour encoding rather than a container box. Say so where a caller reads about the seam, and point at #626 for the open question of whether that rule is the right one.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #503 (
feat/449-xmp-dcterms-provenance); this PR's base is that branch, and only the commits after it belong here.Summary
capabilitymodule —Format/Carrier/Direction(repr(u8), append-only,ALLconstants — slices, not arrays, on the two#[non_exhaustive]enums),const fn supports(format, carrier, direction)andconst fn typed_wiring(format), a const table transcribed from each crate'sSTATUS.mdwith the row cited on every arm and pinned by a full-matrix test. README gains the capability table and the consumer-integration pattern.metadataCargo feature (a normal, feature-gated dependency ongamut-metadata) adding thegamut-dngpattern —blocks()(raw located payloads asMetadataBlocks: JPEG EXIF without theExif\0\0signature, ISOBMFF/JXLExifpayload with itsexif_tiff_header_offsetapplied, ICC reassembled) andmetadata()(Metadata::from_blocks); the two encoders gainwith_metadata(&Metadata)andwith_encoded_metadata(&EncodedMetadata), routing each carrier to the existing raw setter and refusing carriers the container cannot write (IPTC-IIM, C2PA) with a typedUnsupported. C2PA is never copied forward (facade policy).Exif/xmlcontainer boxes back (JxlDecoder::metadata→JxlMetadata): jxl-rs swallows auxiliary boxes, so the crate walks the top-level box sequence itself; a Brotli-compressed (brob) metadata box is a typedUnsupported.HeifItem::exif_tiff_stream(the TIFF stream behind the 4-byte offset, ISO/IEC 23008-12 §A.2.1) andHeifItem::icc_profile(thecolrICC bytes regardless ofnclxorder). HEIF has no IPTC-IIM item type, and a C2PA manifest store lives in a top-leveluuidbox outside the item model, so neither block is produced byblocks();HeifContainer::c2pastill locates the store.metadatafeature now forwardsgamut-jpeg?/metadata,gamut-jxl?/metadataandgamut-heic?/metadata, sogamut = { features = ["jpeg", "metadata"] }reaches the typed accessors from the front door. The forwards are weak (?/):metadataalone still pulls in no codec, and a format alone still pulls in no facade. This bullet has now been revised twice — it first claimed the forwarding existed when it did not, was then corrected to say this branch would not add it, and now records that the branch's manifest was widened so it could. The forwards are pinned bycrates/gamut/tests/feature_forwarding.rs(decision 16). See decisions 15 and 16; the change is what gamut: forward gamut-{jpeg,jxl,heic}'smetadatafeature from the umbrella's ownmetadata#622 describes.Repair round — the five red required checks
Format & Metadataand all fourIncremental (PR diff)shards had been failing since the run of2026-09-06. Both mechanisms are caused by this branch; neither comes from the base.
fmt-check)cargo fmt --allpass. Fifteen hunks acrossgamut-jpeg/src/{lib,metadata}.rs,gamut-jxl/src/{decoder,encoder,lib}.rsandgamut-jxl/tests/metadata_facade.rs.fmt-tooling-checkitself passed; onlyfmt-checkfailedstyle(jpeg)+style(jxl), formatting onlyread_boxreturned the offset just past the box it read, socontainer_metadata_boxes's progress lived in a callee's return value: every one of cargo-mutants' seven return-value replacements ofread_boxmakes the walk loop forever, and each is scoredTIMEOUTafter 60 s. Shards saw 3, 4, 3 and 3 of themrefactor(jxl):parse_box_headernow reports only what the header claims and slices nothing; the walk owns every bound and steps at least the 8-byte minimum header per iteration, so it terminates for any value a mutant can return and the mutants become killable by an assertionMISSEDmutant:size < 8→size <= 8inread_box. Nothing asserted the empty-box boundary, so rejecting a box whosesizeis exactly its 8-byte header survived the suitetest(jxl): an emptyfreebox between the metadata boxes is stepped over, and an emptyxmlbox yields an empty payload rather than an absent oneThe branch's base (
feat/449-xmp-dcterms-provenance,d6fd0a0) has not moved since 2026-09-06and is still an ancestor of this head, so no part of the red is a stale-base effect.
One further discrepancy found while re-observing: the
gamut-heichalf of the deliverable — the twolenses and the
metadatafeature that the Summary and decision 7 both describe — had never beencommitted, so the pull request as pushed did not contain what it described. It is committed now
(decision 13). The umbrella's feature forwarding, which the Summary also claimed, is not done
here and is filed as #622 (decision 14); the Summary is corrected in place.
Correction, appended. The paragraph above says of the umbrella's feature forwarding that it "is not done here and is filed as #622 (decision 14)". That was true when it was written and is no longer: the branch's manifest was subsequently widened to include
crates/gamut/Cargo.tomland the three weak forwards are now in this pull request (decision 15). The sentence it corrects is left standing above. #622 remains open — this run does not close or comment on an issue, including one it filed — and a human can close it against this pull request.Review round 2 — the three Low findings and one design question
The first independent review of this branch found no blocking defect: it cross-checked the
capability table cell by cell against the crates it cites and found no wrong cell, re-verified the
container-walk repair structurally and with a sweep of 21 584 hostile inputs, and re-derived the
feature forwards from cargo's unit graph. What it did find were three
Lowfindings and oneinformational note. All four were reproduced here before anything was changed.
"gamut-jpeg?/metadata"out ofmetadata = [ … ]intojpeg = [ … ]: both tests still passed, whilegamut --features jpegthen resolved the format crate with its metadata wiring and therefore the whole facade — the build the weak form exists to preventmetadataCargo feature"gamut-dnghas no such feature:gamut-metadatais an unconditional dependency there, sogamut-dng/metadatais a hard cargo error. The README's table had DNG right; only the prose and the rustdoc were wronggamut-jxl, whose reader is gated ondecode: underdefault-features = false, features = ["encode"]the table answersrandJxlDecoder::metadatadoes not existconsttable sees neither another crate's features nor the target, so it describes the surface a crate defines — and name the gated case. The table is not weakened"gamut-jpeg?/metadata"and renaming the accessor both produced oneno associated function named blockserrorgamut::jpeg::Metadata, re-exported under each crate's ownmetadatacfg). A dropped forward now breaks the witness and the accessor; a rename breaks only the accessorSeparately, design question 3 was taken:
Format::ALLandCarrier::ALLsat on#[non_exhaustive]enums as fixed-length arrays, so appending a variant would have changed eachconstant's type — a breaking change for any caller who named it, baked into API this pull request
introduces. Both are
&'static [Self]now.Directionis exhaustive and keeps its array.Design questions 1, 2 and 4 are filed, not taken (see
## Issue): each is a behavioural orcross-crate fork that should not be decided at the close of a review loop. The one whose current
behaviour could surprise a caller — a present ICC replacing a
ColorSpecset earlier — is nowdocumented where a caller reads it.
Validation
Every command below was run on this branch at the head this section describes, from a nested
worktree of the repository.
__CARGO_TEST_ROOTis set for the formatting tasks becausefmt-tooling-checkloops over everytooling/*/Cargo.tomlandcargo metadataotherwise walkspast a nested worktree's root; it changes nothing about what is checked.
cargo test -p gamut-heic --all-featurescargo test -p gamut-jxl --all-featurescargo clippy -p gamut-heic --all-targets --all-features -- -D warningscargo clippy -p gamut-jxl --all-targets --all-features -- -D warnings__CARGO_TEST_ROOT=$(git rev-parse --show-toplevel) mise run fmt-checkFormat & Metadatafailure)mise run check-testsconvco check origin/feat/449-xmp-dcterms-provenance..HEADorigin/master)mise run check-release-depsmise run check-ffi-featuresmise run lintmise run testtests/feature_forwarding.rs: the suite now notices if a forward disappearscargo build -p gamut --example <probe> --features "jpeg,jxl,heic,metadata"— before the forwardsblocks/metadata/with_metadatanot found onJpegMetadata,JxlMetadata,HeifImage,JpegEncoder(5 accessors, named only throughgamut::)cargo build -p gamut --features "jpeg,jxl,heic,metadata" -v--cfg feature="metadata"cargo build -p gamut --features "jpeg,jxl,heic" -v--cfg feature="metadata"(0 occurrences)cargo tree -p gamut --no-default-features --features "jpeg,jxl,heic"gamut-metadata/exif/icc/xmp/iptcincargo tree -p gamut --no-default-features --features "metadata"metadataalone does not pullgamut-jpeg/jxl/heicincargo test -p gamut --all-features --test feature_forwardingcargo test -p gamut --no-default-features --features metadata --test feature_forwardingcfg, so it still runs where the resolution pin is compiled out"gamut-jxl?/metadata", re-runblocksnot found onJxlMetadata"gamut-heic?/metadata", re-runblocksnot found onHeifImage"gamut-heic?/metadata"to"gamut-heic/metadata", re-run"gamut-heic/metadata"is not weak; enablingmetadataalone would now pull gamut-heic into builds that asked for no codec"GAMUT_MUTANTS_BASE=origin/feat/449-xmp-dcterms-provenance mise run mutants-diff --shard i/4 --verbose, i = 0..3blocks()return-value replacements, whoseDefault::default()substitute does not typecheck)The probe used for the two compile rows was a throwaway example naming one forwarded accessor per crate; it was deleted after the measurement and is not part of the diff —
tests/feature_forwarding.rsis the committed pin that replaced it. The three falsifier rows were measured by editing the manifest, running, and restoring it; the manifest in the diff is unchanged by them. Every gate above was re-run after the umbrella commit and again after the pin;lintandtestwere each run three times in total and passed every time, and the--in-diffmutant selection is byte-identical to the one the four shards below were run against (the umbrella commit touches only aCargo.toml, which contributes no mutants), so that result stands for this head.Mutation gate base. This branch is stacked, so
GAMUT_MUTANTS_BASEnamesorigin/feat/449-xmp-dcterms-provenance. Against the defaultorigin/masterthe selection wouldfold in every mutant belonging to #503 underneath and would not be evidence about this diff. The
75 mutants selected against the correct base are: 50 in
gamut-jxl/src/decoder.rs, 12 ingamut-heic/src/image.rs, 6 ingamut-metadata/src/capability.rs, 5 ingamut-jpeg/src/metadata.rs, 2 ingamut-jxl/src/encoder.rs.Workspace-wide runs were executed inside a memory-capped scope (
MemoryMax=16G,MemorySwapMax=0,CARGO_BUILD_JOBS=2);mise run fetch-av1-oracleswas run first so theAV1/AVIF oracle builds resolve.
Round 2 — re-run at head
7937317Every command below was executed in this round, on this head, from a nested worktree.
Workspace-wide runs used the same memory-capped scope (
MemoryMax=16G,MemorySwapMax=0,CARGO_BUILD_JOBS=2,ulimit -v 12 GiB).cargo test -p gamut --features "metadata,jpeg,jxl,heic" --test feature_forwardingcargo test -p gamut --features metadata --test feature_forwardingcfgcargo test -p gamut --features "jpeg,jxl,heic" --test feature_forwarding"gamut-jpeg?/metadata"frommetadata = [ … ]intojpeg = [ … ]metadatafeature no longer lists"gamut-jpeg?/metadata"; … and if the entry moved to that format's own feature, that format alone now drags in the whole facade". Before this round the same edit left both tests passingmetadataand also add it tojpeg = [ … ]"gamut-jpeg?/metadata"is listed more than once; a second copy under a format feature makes that format alone drag in the whole facade" (left 2, right 1)"gamut-jpeg?/metadata"cannot find type Metadata in crate gamut::jpegandno associated function named blockscargo test -p gamut-metadata --all-featurescargo clippy -p gamut-metadata --all-targets --all-features -- -D warningscargo tree -p gamut --features "jpeg,jxl,heic"__CARGO_TEST_ROOT=$(git rev-parse --show-toplevel) mise run fmtthen… mise run fmt-checkmise run check-testsmise run check-commits(convco checkagainst the stacked base)mise run lintmise run testmise run check-release-depsmise run check-ffi-featuresGAMUT_MUTANTS_BASE=origin/feat/449-xmp-dcterms-provenance mise run mutants-diffcrates/gamut/tests/is mutation-invisibleEvery falsifier above was reverted;
git statusis clean and the manifest in the diff is unchangedby them.
Mutation gate base and cap. The correct base is the stacked head
origin/feat/449-xmp-dcterms-provenance, and that is what the published count was measured against.A first run against the default
origin/masterwas also executed (87 mutants, 83 caught, 4 unviable,0 missed); it is a diluted superset that folds in #503's mutants and is reported only for
completeness, not as evidence about this diff. Both ran under
ulimit -v— an address-space cap CIdoes not have — so a mutant that hangs by allocating would abort here and score
caughtwhile CIscores it
TIMEOUT. This round adds no loop and no hand-written iterator, and all four CI shardswere already green at the head this round started from.
Risks and rollout
#[non_exhaustive], so the newJxlMetadataand the new methods are minor bumps. gamut-metadata gains a module (minor).gamut-jxl'sJxlDecoder::metadatais available under thedecodefeature regardless ofmetadata(it returns raw bytes); the walk is bounded by the input length and every box overrun is a typed error.metadatafeature of a format crate pullsgamut-metadataand its four leaf crates into that consumer only when enabled.gamut-heicgains an optional, feature-gated normal dependency ongamut-metadata, so release ordering follows it;mise run check-release-depsconfirms no dev-only workspace edge was introduced.parse_box_header+ a walk that owns its bounds). Behaviour is unchanged: every fault keeps the message its tests already pin, and the walk's step is now at least the 8-byte minimum header per iteration, so it cannot fail to terminate on any input.Issue
Refs #420 — the remaining four crates (png, webp, avif, tiff) are filed as #510 because open
pull requests are changing their metadata surfaces.
#622 ("forward gamut-{jpeg,jxl,heic}'s
metadatafeature from the umbrella's own") was filed bythis branch when the forwarding was out of scope, and is now covered by this pull request. It is
deliberately left open and uncommented: this run does not write to any issue, including one it filed.
A human can close it against this pull request.
Three design questions the second review raised are filed rather than decided here, each with the
review's evidence:
with_metadatasilently replaces the caller'sColorSpec#626 —gamut-jxl: a present ICC inwith_metadatasilently replaces the caller'sColorSpec, because in JPEG XL the profile is the codestream's colour encoding. Today'sbehaviour is now documented at
JxlEncoder::with_metadataand in the facade README; the choicebetween last-write-wins, refusing the conflict, and ignoring the carrier is left open.
JxlDecoder::metadataloses located Exif/XMP boxes when the codestream is at fault #627 —gamut-jxl:JxlDecoder::metadatareads the container boxes and then the codestream'sICC, and reports one result, so a codestream fault discards
Exif/xmlboxes this crate hadalready located successfully.
blocksfallible or not, and whose error) #628 —gamut-metadata: the four wired crates disagree on the typed accessor's shape —gamut-heic'sblocks()is fallible and returns its own error while the other three areinfallible. Settling it binds every crate wired later, so it outlives this pull request.
A fourth issue was filed after the three above:
PixelFormat::ALLis a fixed-length array on a#[non_exhaustive]enum, so appending a format is a type change #629 —gamut-core:PixelFormat::ALLis a fixed-length array on a#[non_exhaustive]enumwhose own docs say "new variants append", so appending a twelfth format changes the constant's
type. It is the last
ALLin the workspace with that shape —gamut-icc,gamut-exif,gamut-xmpand (as of this pull request)gamut-metadataall use slices. Not changed here:gamut-coreis outside this branch's manifest and, unlike the constants this branch owns, theconstant is already released at 2.0.1, so converting it is itself a breaking change to the
workspace's root crate — a price somebody has to choose to pay, not a repair to slip into a
documentation round. The issue carries the three options and the in-repo callers, including
crates/gamut-ffi/DESIGN.md, which enumerates that constant to generate the C surface.Decisions taken
Appended during the run (same shape):
Appended during the repair round (same shape):
Appended during the second review round (same shape):
Correction, appended. Decision 21 above is superseded, and its text is left standing
unchanged so this correction can be checked against it. It records as Taken that the finding "is
disclosed here and in the round report instead" of being filed, and as Rejected "filing a fourth
issue past the bound - the bound is the instruction". The bound was subsequently lifted by the
operator as the wrong shape — a numeric cap on filings is not a safety property, and letting one
convert a finding into something nobody knows about is the failure it caused. The finding is now
filed as #629. Nothing else in decision 21 changes: the constant is still not touched, and for
the reasons it gives.
Unresolved review notes
Three design questions are open, not resolved: gamut-jxl: a present ICC in
with_metadatasilently replaces the caller'sColorSpec#626 (a present ICC replaces the caller'sColorSpecin JPEG XL), gamut-jxl:JxlDecoder::metadataloses located Exif/XMP boxes when the codestream is at fault #627 (a codestream fault discards container boxes this crate located),gamut-metadata: settle one shape for the format crates' typed accessors (
blocksfallible or not, and whose error) #628 (the four wired crates disagree on whetherblocks()is fallible and on whose error itreturns). Each is filed with the review's evidence and none changes behaviour in this pull
request. gamut-jxl: a present ICC in
with_metadatasilently replaces the caller'sColorSpec#626's current behaviour is documented so a caller is not surprised by it.gamut_core::PixelFormat::ALLhas the same defect decision 19 fixes here — a fixed-lengtharray constant on a
#[non_exhaustive]enum — and is not fixed:gamut-coreis outside thisbranch's manifest, the constant is already published, and this round's three-issue budget is
spent on the design questions above. Recorded here and reported upward so a human can file it.
upward so a human can file it", because a three-issue budget was "spent". That budget was
lifted as the wrong constraint and the finding is now filed as gamut-core:
PixelFormat::ALLis a fixed-length array on a#[non_exhaustive]enum, so appending a format is a type change #629 (decision 22). Thesentence it corrects is left standing. What remains true and unchanged: the constant is not
fixed by this pull request, for the reasons the bullet gives.
A green local mutation run is not proof CI's mutation gate is green: the local runner caps
address space (
ulimit -v), so a mutant that hangs by allocating aborts and scorescaught,where CI has no such cap and scores it
TIMEOUT. This round adds no loop and no hand-writteniterator, and all four
Incrementalshards were green at the head it started from.The umbrella's
metadatafeature does not reach the format crates'metadatafeatures (gamut: forward gamut-{jpeg,jxl,heic}'smetadatafeature from the umbrella's ownmetadata#622).Until it does,
gamut = { features = ["jpeg", "metadata"] }compiles the facade and the codec butnot the wiring between them; a consumer depends on
gamut-jpeg/gamut-jxl/gamut-heicdirectly to reach the typed accessors. Left undone deliberately —
crates/gamut/Cargo.tomlisoutside this branch's manifest (decision 14).
No container-level exiv2 differential exists for any of the three crates (decision 6):
tooling/exiv2-oracleis block-level and in-memory with no container reader compiled in. Thelocated payloads are pinned byte-exact against each crate's own oracle (libjpeg-turbo, libheif, a
raw box scan of libjxl output) and the leaf crates pin the payloads against exiv2; each affected
STATUS.mdnames the untested cell.No committed regression test pins the umbrella's three feature forwards.Resolved in this round bycrates/gamut/tests/feature_forwarding.rs(decision 16), which pins both directions and was falsified before landing. The note is kept rather than deleted so the trail is legible. What remains true, and is a standing property rather than an open item: that pin is mutation-invisible —.cargo/mutants.tomlexcludescrates/gamut/**, so no mutant anywhere can kill those two tests, and the three forwards are held by that file alone. It is in its only legal home (the linkage exception inAGENTS.md: the edge is in the umbrella's own feature graph and no lower crate can observe it), and the module docs say so at the test.