feat(cli): report a HEIC's C2PA manifest store in gamut inspect, without validating it - #531
Open
justin13888 wants to merge 18 commits into
Open
justin13888 wants to merge 18 commits into
justin13888 wants to merge 18 commits into
Conversation
A reporting tool needs presence, a byte range, a size and a box_purpose, and must not print the store itself: it is opaque to this crate, routinely tens or hundreds of kilobytes, and rendering it invites the reading that gamut has understood — and so checked — the manifest. C2paSummary carries exactly those facts and renders them, so a host that only formats output holds no logic of its own and cannot drop the disclaimer on the way to the terminal. C2PA 2.4 §15.12 puts validation on a validator, so the line that reports a store says gamut checks no signature, no hash binding and no trust list, and names c2pa-rs as what does — inline, never as a footnote a reader can skip. Refs #448
gamut inspect gains a HEIC arm that answers one question - does this file carry a C2PA manifest store, and where? - and answers only it. Every store is listed with its box_purpose, its size and its half-open byte range, because a file mid-update legitimately carries two and collapsing them to a count hides the one fact that tells them apart. The arm gates nothing. It classifies no bytes and looks for no unknowns, so it has nothing to hold against the file: presence and absence are both ordinary outcomes and both exit 0, and only a container that cannot be parsed exits non-zero. Every reportable fact and every word of the report come from gamut-heic, since gamut-cli is outside the coverage gate and logic placed here would ship untested; this file prefixes its own indent and nothing else. The sniff matches the ftyp *major* brand alone. An AVIF lists mif1 among its compatible brands, and reading one through the HEIF container reader would report a store this command has no slice for. Refs #448
A top-level box whose extended type is the C2PA one and that still yields nothing — a non-zero FullBox version or flags, an auxiliary `merkle` or unknown `box_purpose`, a truncated body, or no valid JUMBF `LBox` where the purpose puts the store — was dropped silently, so `c2pa_summary` reported it byte-identically to a file carrying no C2PA box at all. That is the mirror of the verdict `C2PA_NOT_VALIDATED` exists to prevent: it stops a reader inferring *verified* from a located store, then lets one infer *no provenance* from bytes the crate merely could not read through. `C2paSummary::unread` now lists those boxes with a `C2paUnreadReason`, and `report_lines` gives each its own line naming the reason. A `uuid` box whose extended type is not `C2PA_UUID` — even one byte off — deliberately stays absence: §A.5.1.1 makes the extended type the whole test, and an ordinary file carries vendor `uuid` boxes that are no evidence of provenance. Both public views are now built from one scan, so "a store" and "a box that yielded no store" cannot disagree about which boxes were looked at. Refs #448
`gamut inspect` sniffed the HEIC arm from a hand-rolled list of `ftyp` major brands whose doc claimed that matching the major brand alone excludes AVIF. The premise is false: `mif1` is the generic MIAF structural brand and an AVIF may carry it as its *major* brand, so an AVIF was reported as `HEIF/HEIC` at exit 0 — and had it carried a `ContentProvenanceBox`, that box would have been reported as a HEIC's. The repository already ships the correct predicate. The `ftyp` test is now a cheap route into the arm, and `inspect_heic` confirms the container with `gamut-heic`'s own `HeifImage::is_hevc_still` (`references/heif` §7, which for `mif1` additionally requires the primary item to carry an `hvcC`) after the parse it performs anyway, reporting an unsupported container brand instead of a HEIC banner when the confirmation fails. That also fixes the diagnostic for a real AVIF, which previously fell through to TIFF and was rejected as `TIFF: bad byte-order mark` by a sniffer this command had already taught to read `ftyp`. `--format heic` skips the sniff and the confirmation: a forced format is the caller's own assertion about the file, and overriding detection is what the flag is for. Refs #448
Nothing in the workspace executed `gamut inspect`: adding `.skip(1)` to the report lines deleted the disclaimer and the pointer to `c2pa-rs` from the output while every gate stayed green. The premise offered for that — that `gamut-cli` is excluded from the coverage gate, so logic placed in it would ship untested — is false in-repo twice over: `tests/icc.rs` is a working binary-driving integration test, and `inspect.rs` already carries an inline `#[cfg(test)] mod tests`. Coverage exclusion is not test exclusion, and this file retracts the premise by contradicting it. Four tests on the `icc.rs` model pin what the command itself decides: that the non-validation disclaimer and the store's own line reach stdout unabridged, that a present-but-unreadable C2PA box is not printed with the absence wording, that an AVIF whose major brand is `mif1` is declined by name rather than reported as a HEIC, and that `--format heic` overrides that confirmation. `brand_label`'s escaping is pinned inline beside the code, being private. Refs #448
The reporting shape has a third outcome beside a located store and no C2PA box at all, and the ledger row that claimed the shape did not mention it. Refs #448
The normative contract copied the false claim that matching an `ftyp` major brand alone excludes AVIF, and told callers to gate on stdout without saying that a C2PA box gamut could not read through prints there as well. It now records the route/confirm split and the brands a list in the command would have had to carry, the unreadable-box row and the near-miss extended type that deliberately stays absence, that `--format heic` skips both the sniff and the confirmation, and that the exit code says whether the inspection succeeded rather than what it found. Refs #448
A scan that reported only the manifest stores it read, and the C2PA boxes it could not read through, left two facts about the bytes unstated. A `uuid` box whose extended type is one byte off the C2PA one is not C2PA framing — §A.5.1.1 makes the sixteen bytes the box's whole identity and the specification has no notion of an approximate one — but it was reported by saying nothing at all, so a file carrying such a box rendered byte-for-byte identically to a file carrying no `uuid` box whatsoever. The first is what a signed file corrupted in transit looks like, and it was the file the report said the least about. `other_uuid_boxes` counts them and the report states the count, disclaiming provenance in the same line: no range, no reason, no claim. `C2paBoxPosition` records whether the walk had already passed an `mdat` when it reached the box. §A.5.3 places a manifest-store box before the first one, and an appended box is the adversarial shape — but the clause the report adds is positional and not a verdict, since §A.5.3 equally requires a mid-update file's `update` box to be its last box. The `moov` half of that boundary is unreachable: the container refuses a top-level movie box before any of this runs, and a test pins that. Rendering splits into `summary_lines` (the headline and the count, at most two lines whatever the file holds) and lazy `detail_lines` with `detail_line_count` beside them, so a host can truncate a list whose length the input chooses without rewording anything; `report_lines` still renders the whole report. Reading a box now takes one fallible split per field that can genuinely be absent. The old code took four, three of which could not fail once the one before it had succeeded, and justified the fourth with a rationale about a short body that the container rejects outright.
The near-miss count, the positional fact, and the split rendering a host caps a terminal report with.
The provenance report was the only list in `gamut inspect` with no cap. C2PA 2.4 §A.5.3 permits any number of these boxes, so their count is chosen by the input and needs no malformity to grow: a legal 2.7 MB file carrying fifty thousand of them printed 50 002 lines at exit 0 and put the headline — non-validation disclaimer and all — at line 2 of fifty thousand, where the unread lines below it each repeat their own hedge and the unprinted ones carry none. It now truncates at the same twenty entries, with the same "… and N more" tail, as every other list here; `gamut-heic` keeps returning all of them, and the headline and the `uuid`-box count are never truncated. The forced path printed `<path>: HEIF/HEIC`, which is this command's own claim about the file — over a file for which `--format` skipped both the sniff and the confirmation, so nothing about the container was tested. It now says the format was asserted rather than detected.
Plus the positional clause, the asserted-format label, and why a machine consumer gets a filed issue rather than a third exit code.
…order A host caps the detail lines, so a class of box that falls wholly past the cut left no trace at all: a file with twenty legal stores and one C2PA box no store could be read from rendered zero unread lines under a headline reading "20 manifest stores located". That defeats the contract the report is for, which is that an unreadable box is unknown and never absence. Repaired where it is caused rather than at the cap. The headline now names every non-empty class with its count, so no cap can reach it; and the detail lines are merged back into true file order rather than grouped by kind, which is what `c2pa_summary`'s own documentation already promised and what makes the cut category-blind instead of systematically favouring stores. The `uuid`-box count line loses its detail indent, being part of the head rather than the first list entry, and names what it counts without an "other" the file may give no antecedent for. Its documentation now states what it cannot do: a corrupted C2PA extended type and an ordinary vendor box are the same observation to it.
The guard asserted a tautology. Given a hand-built value - `C2paSummary::default()`, or a struct literal - inference is free to pick `'static` for a lifetime parameter the type does not carry today, so the bound was satisfied whatever the type became: executed, a summary type grown a borrowing field still compiled under it, and for the literal forms it was only a missing-field detector. Taking the bound on values produced from a buffer local to the test forces the lifetime instead. Executed under the same drift, this form fails to compile with E0597 while the old form still passes.
Two cases the command's own tests could not see before, both on the built binary, since this crate is outside the mutation gate and outside the coverage gate alike: twenty legal stores ahead of one unreadable box, where the headline must still name the class whose line the cap dropped; and an unreadable box ahead of the stores, where the surviving lines must be the file's first boxes rather than its first stores. The near-miss assertion follows the count line's new wording, and the command's comment records why one budget over one file-ordered list is enough.
The cap paragraph promised only that the headline and the count line survive, which left the caller-guidance sentence above it false: gating on the presence of an `unread C2PA box` line was exactly what the cap could defeat. It now states the three things separately - the cap can hide any individual box's line of either kind and does not say which kind, it cannot hide that a kind exists, and it cannot silence one kind systematically - and the caller guidance points at the headline, which is not capped. Records that there is deliberately one budget rather than one per kind, what the near-miss count cannot separate, and which binary-driving test holds each claim, this command being outside both the mutation and the coverage gate.
… merge The file-order merge was a hand-written `Iterator::next`, which is a function free to decide both what the stream contains and how long it is. Replacing its body with `Some(...)` - the mutation the gate generates for any `-> Option<T>` - therefore yields an unbounded stream, and every consumer of `report_lines` fills memory until something stops it. What stops it differs by machine, which is why this was invisible here and red in CI: under this checkout's `ulimit -v` the mutant aborts on the failed allocation and counts as caught, while an unconstrained runner sits in the loop until the 60s test timeout. The merge is now a bounded list of borrowed entries, ordered by the byte each box starts at, mapped to lines on demand. The length is fixed by the summary - one entry per store plus one per unread box, which is exactly `detail_line_count` - so no defect in the ordering can lengthen it, and the tie a hand-assembled summary can present resolves to the store by the sort being stable rather than by a comparison written to prefer it. Lines are still built only as they are taken, which is what a host capping the list at twenty pays for; what is built eagerly is one borrowed entry per box, which the summary already holds.
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 #485 (
feat/224-png-encoder-efficiency) and opened against that branch, because #485changed
crates/gamut-cli/src/commands/inspect.rssubstantially — the exit-code contract, the PNGarm and
docs/inspect-exit-codes.mdare all its work, and this section fits inside that contractrather than fighting it. Review the nine commits this branch adds on top of that base only.
No human approved this plan. This is an unattended automation run; the decision record below is
what a human reads afterwards.
Summary
gamut inspectgains a HEIC arm that answers one question — does this file carry a C2PA manifeststore, and where? — and answers only it:
Real output from the binary built at
e54622c5, verbatim (long lines are one terminal line each):An earlier revision of this body showed a sample no run produced. It is replaced above.
gamut-heic, not in the CLI — because that is where the facts and the wordsabout them belong, so every host renders the same wording and none can reword the disclaimer
away.
HeifContainer::c2pa_summary()returns aC2paSummaryofC2paStoreSummaryentries(half-open range, size,
box_purpose) andreport_lines()renders them;inspect.rsprefixesits own indent and holds nothing else. The premise an earlier revision of this body gave for
that placement — that
gamut-cliis outside the coverage gate, so logic there ships untested —is false and is retracted. Coverage exclusion is not test exclusion:
crates/gamut-cli/tests/drives the built binary, and
inspect.rscarries an inline#[cfg(test)] mod tests. Seedecision 15.
ContentProvenanceBoxwhoseFullBoxversion is non-zero, whosebox_purposeis the auxiliarymerkle, that is truncated, or whose JUMBFLBoxis hostile now gets its own line naming thereason (
C2paSummary::unread). Printing "no manifest store found" for it would let a reader inferno provenance from bytes gamut merely could not read — the mirror of the defect this PR exists
to prevent. It stays exit
0: the exit code says whether the inspection succeeded, not what itfound (decisions 11 and 12).
C2paStoreSummarydoes not hold the bytes, by construction —they are opaque to gamut, routinely hundreds of kilobytes once a manifest embeds a thumbnail, and
a byte range is what a caller hands to
c2pa-rs.C2PA_NOT_VALIDATEDlives beside the helper,so the wording is under test rather than living only in a CLI format string. C2PA 2.4 §15.12 puts
validation on a validator; "C2PA: present" printed beside EXIF and ICC otherwise reads as
verified to anyone who has seen a Content Credentials badge.
originaland anupdatebox (§A.5.3), and which is active is a validator's judgement.unknowns, so it has nothing to hold against the file: a located store, an unreadable box and no
box at all all exit
0. Only a container that cannot be parsed, or one that is not this arm'scontainer, exits non-zero.
docs/inspect-exit-codes.mdrecords that, and states plainly thatabsence of a store line is not absence of provenance.
ftyptest is a route into the arm; the arm thenconfirms with
gamut-heic's ownHeifImage::is_hevc_still(references/heif§7) after the parseit performs anyway, and reports an unsupported container brand otherwise. An earlier revision
matched the major brand alone against a hand-rolled list on the claim that this excludes AVIF;
that claim is false —
mif1is the generic MIAF structural brand and an AVIF may carry it asits major brand. A real AVIF is now declined by name rather than blamed on TIFF for having no
byte-order mark.
--format heicskips both the sniff and the confirmation, and is documented asdoing so.
Validation
Every command below completed in the run that produced this body, in the worktree at
e54622c5.GAMUT_MUTANTS_BASEis set to this PR's base branch so--diffselects this PR's changes ratherthan also the stacked base's.
cargo test -p gamut-heic -p gamut-cli --all-featuresgamut-heic/tests/c2pa.rs, 4 in the newgamut-cli/tests/inspect_c2pa.rscargo clippy -p gamut-cli -p gamut-heic --all-targets --all-features -- -D warnings__CARGO_TEST_ROOT=$(git rev-parse --show-toplevel) mise run fmt__CARGO_TEST_ROOT=$(git rev-parse --show-toplevel) mise run fmt-checkmise run check-testsmise run check-commitsconvco check origin/feat/224-png-encoder-efficiency..HEADmise run lintmise run testGAMUT_MUTANTS_BASE=origin/feat/224-png-encoder-efficiency mise run mutants-diffmise run lint,mise run testandmise run mutants-diffran inside aMemoryMax=16G,MemorySwapMax=0systemd scope withCARGO_BUILD_JOBS=2andulimit -v.mise run check-release-deps/check-ffi-features/check-ffi-headerwere not run: noCargo.tomlchanged and no C-surface type changed —gamut-ffiandgamut-wasmmention C2PAnowhere (
grep -rni c2pa crates/gamut-ffi crates/gamut-wasmis empty).What the mutation gate does not cover.
crates/gamut-cliis in.cargo/mutants.toml'sexclude_globs, so none of the 40 mutants above came frominspect.rsorerror.rs, and the newtests/inspect_c2pa.rskilled none of them. That test's value is demonstrated differently: revertingthe
is_hevc_stillconfirmation, or adding.skip(1)to the report loop, fails it. The gate isstated here rather than implied.
End-to-end behaviour, against fifteen hand-built fixtures plus a
gamut-encoded AVIF, run throughthe built binary. Before/after on the two defects this revision repairs:
gamut-encoded AVIFerror: invalid input: TIFF: bad byte-order mark [origin: gamut-ifd], exit 1error: …: unsupported container brand 'avif' — …, exit 1mif1…: HEIF/HEIC+ a full C2PA section, exit 0error: …: unsupported container brand 'mif1' — …, exit 1FullBoxversion 1C2PA: no manifest store found …C2PA: no manifest store could be read, but 1 C2PA box is present — that is NOT absence of provenance …+unread C2PA box at [16, 90): its FullBox version or flags are not zero …box_purpose = "merkle"C2PA: no manifest store found …… 1 C2PA box is present …+unread C2PA box at [16, 80): its box_purpose is not one C2PA 2.4 §A.5.3 gives a manifest storeLBox= 0 / 4 /0xFFFFFFFFC2PA: no manifest store found …… 1 C2PA box is present …+unread C2PA box at [16, 90): no valid JUMBF store length sits where its box_purpose puts the storebox_purposeC2PA: no manifest store found …… 1 C2PA box is present …+unread C2PA box at […): the box ends before the framing C2PA 2.4 §A.5.1.2 requiresuuidbox one byte off the C2PA extended typeC2PA: no manifest store found …C2PA: no manifest store found …gamut inspect --format heic <a real AVIF>…: HEIF/HEIC+ C2PA section, exit 0__CARGO_TEST_ROOTis a known artefact of running in a nested.claude/worktrees/worktree: cargootherwise walks past the worktree root to the primary checkout's
Cargo.tomlwhen loading thetooling/*manifests andfmt-checkexits 101 on an untouched tree. No manifest was changed forit.
Round 3 (head
35450f51)Every command below completed in this round's run, in the same worktree, after the four commits
this round adds.
GAMUT_MUTANTS_BASEis again the stacked base, so--diffselects only thisPR's changes.
cargo test -p gamut-heic -p gamut-cli --all-featuresgamut-heic's lib tests, 43 intests/c2pa.rs, 7 ingamut-cli/tests/inspect_c2pa.rscargo clippy -p gamut-heic -p gamut-cli --all-targets --all-features__CARGO_TEST_ROOT=$(git rev-parse --show-toplevel) mise run fmtthenmise run fmt-checkmise run check-testsconvco check origin/master..HEADmise run lintmise run testGAMUT_MUTANTS_BASE=origin/feat/224-png-encoder-efficiency mise run mutants-diffmise run lint,mise run testandmise run mutants-diffran inside aMemoryMax=16G,MemorySwapMax=0systemd scope withCARGO_BUILD_JOBS=2andulimit -v. An earlier invocation ofmutants-diffwithoutGAMUT_MUTANTS_BASEselected 450 mutants — the whole stacked diff againstmaster, i.e. #485's PNG work as well — and was stopped and re-run against the correct base; itsresult is not claimed here.
mise run check-release-deps/check-ffi-features/check-ffi-headerwere not run for thesame reason as before: no
Cargo.tomlchanged and no C-surface type changed.Which gates can see
crates/gamut-cli, stated again plainly.mise run test,mise run lint,mise run fmt-checkandmise run check-testsall see it: its tests run, its clippy runs, itsformatting is checked. The mutation gate cannot —
crates/gamut-cli/**is in.cargo/mutants.toml'sexclude_globs, so none of the 75 mutants above came frominspect.rs,and no test in
tests/inspect_c2pa.rskilled any of them. The coverage gate cannot either — thesame paths are excluded by
--ignore-filename-regex. So the cap, the… and N moretail and theasserted-format label are held only by
tests/inspect_c2pa.rsdriving the built binary, and bymise run testrunning it.Rendered output, before and after. Fixtures were written by the test helpers in
crates/gamut-cli/tests/inspect_c2pa.rs; "before" is the binary built from this branch's previoushead
e54622c5with only the three source files reverted.The near miss — a top-level
uuidbox one byte off the C2PA extended type:Before,
diffbetween that report and the report for a file carrying nouuidbox at all wasempty below the path line — the two were byte-identical. After, they differ by exactly the one
line above.
The uncapped list — a legal 2.95 MB file carrying fifty thousand top-level C2PA boxes:
The headline, disclaimer included, is line 2 of 23 rather than line 2 of 50 002.
The asserted format —
gamut inspect --format heicover a genuine AVIF:A detected container keeps the plain
<path>: HEIF/HEIClabel.Round 5 (head
a451684e)Round 4 was a review round and added no commits. This round adds five: four repairing what that
review established, and a fifth repairing what the mutation gate then found in the first of them.
Every command below completed at this head, in the same worktree.
GAMUT_MUTANTS_BASEis againthe stacked base —
origin/feat/224-png-encoder-efficiency(cce9939c), this PR's own base — so--diffselects this PR's changes and not #485's underneath.cargo test -p gamut-heic -p gamut-cli --all-featuresgamut-heic's lib tests, 44 intests/c2pa.rs, 9 ingamut-cli/tests/inspect_c2pa.rscargo clippy -p gamut-heic -p gamut-cli --all-targets --all-features -- -D warnings__CARGO_TEST_ROOT=$(git rev-parse --show-toplevel) mise run fmtthenmise run fmt-checkmise run check-testsconvco check origin/feat/224-png-encoder-efficiency..HEADmise run lintmise run testGAMUT_MUTANTS_BASE=origin/feat/224-png-encoder-efficiency mise run mutants-diffmise run lintandmise run testran inside aMemoryMax=16G,MemorySwapMax=0systemd scopewith
CARGO_BUILD_JOBS=2andulimit -v; the mutation task builds its own capped scope.check-release-deps/check-ffi-features/check-ffi-headerwere again not run: noCargo.tomlchanged and no C-surface type changed.
The gate this round failed, and why the same gate was green here. The first repair below merged
the two lists in file order through a hand-written
Iterator::next. The mutation gate replaces sucha body with
Some(…), which makes the stream unbounded, so every consumer ofreport_linesfillsmemory until something stops it — and what stops it is a property of the machine, not of the code.
CI's runner sat in the loop until the 60 s test timeout and reported exit 3 on two shards
(
Incremental (PR diff) (0)and(1), one mutant each:replace <impl Iterator for DetailLines<'_>>::next -> Option<String> with Some(String::new())and withSome("xyzzy".into())).Under this checkout's
ulimit -v 8 GiBthe identical mutant aborted on the failed allocation withinthe timeout and counted as caught, which is why the local run of the same gate had been green
(80 mutants, 70 caught, 10 unviable). A local mutation run therefore cannot be trusted about a
hang here; only the count above, taken after the merge was made bounded, is claimed. The repair
removes the shape rather than the mutant: see decision 31.
Where the required checks stand on this head.
Format & Metadata,Clippy & Doctestsand allfour
Incremental (PR diff)mutation shards pass; shards 0 and 1, which failed with exit 3 onthe previous head, are green.
Coverage (test gate)failed on its first attempt at1h23mwith no logs uploaded at all (the job's log blob 404s and its "Generate coverage report"step never completed), which is a lost runner rather than a gate verdict — a threshold failure
completes the step and uploads its log. It was re-run. Coverage on this stacked branch is
long-running by nature: the two most recent successful coverage jobs on it took 4 h and 5 h,
so the failed attempt was shorter than either. The re-run was still in progress when this round
ended.
The blind spot this round's first defect lived in, stated plainly.
crates/gamut-cli/**isexcluded from the mutation gate (
.cargo/mutants.toml'sexclude_globs) and from the coveragegate (the
--ignore-filename-regex). The defect — a cap that could hide a whole class of box — satin the interaction between that command's cap and
gamut-heic's grouped rendering, so a greenworkspace gate was never evidence about it. What holds each repair is named in
docs/inspect-exit-codes.mdand reproduced here:gamut-cli/tests/inspect_c2pa.rs::the_headline_names_a_class_of_box_the_cap_hides_entirely(built binary)gamut-cli/tests/inspect_c2pa.rs::the_capped_list_is_the_files_first_boxes_and_not_its_first_stores(built binary)gamut-heic/src/c2pa.rs::tests::the_headline_states_both_categories_when_the_file_carries_bothand the three single-class tests beside it (inside the mutation gate)gamut-heic/src/c2pa.rs::tests::the_detail_lines_interleave_the_two_kinds_in_file_order, plus…::a_store_and_an_unread_box_starting_together_render_the_store_firstfor the tiegamut-heic/src/c2pa.rs::tests::the_capped_and_uncapped_renderings_are_the_same_lines, which assertsdetail_line_count() == detail_lines().count()gamut-heic/src/c2pa.rs::tests::uuid_boxes_of_another_extended_type_are_reported_as_a_count_of_bytes_present, andgamut-cli/tests/inspect_c2pa.rs::a_uuid_box_of_another_extended_type_reaches_stdout_as_a_countgamut-heic/tests/c2pa.rs::a_summary_holds_no_borrow_of_the_file_it_describesThe hidden-class shape, rendered before and after. Twenty legal stores followed by one C2PA box
whose
FullBoxversion is 1, spliced afterftyp. Twenty store-shaped boxes need no malformity —§A.5.3 permits any number — so this is a file anyone can build. Both runs are the built binary; the
"before" is this branch's head before this round,
35450f51, with nothing else changed. Both wereexecuted at the heads named, and the store lines in the middle are elided here for length only.
Nothing on that stdout says the file carries a C2PA box gamut could not read through. A caller
following the normative document's own instruction — treat the
unread C2PA boxline as unknown,never as absence — read a clean bill of health.
The unreadable box's own line falls past the cap either way — that is what a cap does — but the
class is now stated in the line no cap can reach. With that box first in the file instead, the
cut is category-blind: its line is the report's first entry and one store falls past the cut.
Executed at the same head:
The count line, after decision 28's two wording fixes:
It now sits at the head's indent rather than the detail list's, and "other" no longer needs an
antecedent this file does not supply.
What reproduced, and what did not. All four findings this round was given reproduced. The hidden
class was executed exactly as described and is shown above. The drift guard was falsified in both
directions: a type carrying a borrowing field compiled clean under the old form, and — with that
drift applied to the real
C2paSummary— the old form still compiled while the repaired form failedwith
error[E0597]naming the local buffer. The near-miss report and an unrelated vendor UUID'sreport were byte-identical below the path line. Finding 4's reading of decision 25 is correct, and is
verified against the code:
classify_uuid_boxstill carries its short-body arm, correctly, and thecorrection at the head of the decision record says what was actually removed.
Residual risk. The rendering repaired here is held by tests in two places, and only one of them
is inside the mutation gate. The wording, the four headline shapes, the file ordering and the length
bound are pinned in
gamut-heicand are covered by the count above; the command's cap and its tailare pinned only by
gamut-cli/tests/inspect_c2pa.rsdriving the built binary, which no mutation orcoverage figure can speak for. That is the same blind spot this round's defect came out of, and it
is a property of where the gates are drawn rather than of this change.
Risks and rollout
ftyp) into the HEIC arm, where it used tofall through to TIFF unless its major brand was on a five-entry list. Nothing gamut inspects
starts with
ftypat offset 4 other than ISOBMFF, so no TIFF/DNG/PNG verdict moves; what changesis the message an ISOBMFF file that is not a HEVC still image gets, from
TIFF: bad byte-order marktounsupported container brand '…'. Both are exit 1.gamut-heic(C2paSummary,C2paStoreSummary,C2paUnreadBox,C2paUnreadReason,C2PA_NOT_VALIDATED,C2paBoxPurpose::as_str,HeifContainer::c2pa_summary) — all additive; the structs are#[non_exhaustive], soC2paSummarygaining itsunreadfield is a minor change, andC2paUnreadReasonis a fieldless#[repr(u8)]enum with permanent discriminants.C2paSummary::is_present()keeps its meaning (a store was located) and so is now narrower than"the file carries C2PA framing". Its docs say so and point at
unread; a caller that treated itas "has provenance" was already wrong, and now has a way to be right.
gamut inspectprints no byte accounting for a HEIC: the arm is a provenance report, not adeconstruct.
gamut-heicdoes account every byte (HeifContainer::segments), so a HEICdeconstruct arm remains possible later; it was not in this issue's scope and is not implied by
it.
Format::Heicvariant; thegamut-heichelper isindependently useful and can stay.
Issue
Closes #448.
Filed for the remainder, which is other issues' work and not declined scope:
#530 — gamut-cli: extend gamut inspect's C2PA section to each container as its
locator slice lands (PNG #511, DNG #508, AVIF #506, RIFF/WebP #515, TIFF #520).
Filed by this lane's round-2 repair: #551 — gamut-cli: issue #530 rests on a
false premise — coverage exclusion is not test exclusion. The premise is retracted in-repo by this
PR; #530's own body still carries it, and editing an existing issue is outside what this run may do
(decision 17).
Filed by this lane's round-3 repair: #596 — gamut-cli: gamut inspect has no
machine-readable output, so a consumer must parse prose (decision 22) — and
#597 — gamut-heic: C2paSummary::is_present is a two-valued answer to a
three-valued question (decision 23). Neither is declined scope of #448; both are follow-on work
this round deliberately did not take.
Decisions taken
Correction to decision 25, appended in round 5. Its "both removed" overstates the code. The
short-body arm of
classify_uuid_boxwas kept, and had to be: the function must be total for abody its own signature does not constrain, and the arm is still there with a comment that now
describes it accurately. What was actually removed is the false rationale — the old comment
claimed a
uuidbody too short to hold the extended type "is a foreign box rather than a truncatedone", which nothing can reach, since
gamut_isobmff::BoxReader::next_boxrejects such a box and thefile never parses — together with four unreachable fallible operations in
read_content_provenance_box. The code that entry describes is right; the entry's summary of it isnot. The frozen entry below is left exactly as it was written.
Appended by this lane, in the record's shape:
Appended by this lane in round 2, after review. The record above is left exactly as it was written,
including the decisions round 2 reverses.
Appended by this lane in round 3, after review. The record above is left exactly as it was written,
including the decisions round 3 revises.
Appended by this lane in round 5, after review. The record above is left exactly as it was written,
including the decisions round 5 reverses and the entry the correction at the head of this section
names.
Unresolved review notes
None.