2 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 674b2be66d |
feat(pdf): JPEG 2000 decoding — Phase 3 complete, all three codecs
Some checks failed
PDF engine / engine (push) Has been cancelled
PDF engine / makepad-integration (push) Has been cancelled
PDF engine / fuzz (push) Has been cancelled
email.yml / feat(pdf): JPEG 2000 decoding — Phase 3 complete, all three codecs (push) Failing after 0s
repo hygiene / hygiene (push) Has been cancelled
The last codec ADR 0015 deferred. The plan recorded the blocker as a dependency decision, not an algorithm: openjpeg would add a C dependency that breaks the Android cross-compile. This is pure Rust and adds no dependency at all. It shares the MQ arithmetic decoder with JBIG2 — T.800 and T.88 specify the same coder — so the previous tranche paid for most of this one. Context::with_state moved onto the shared type because JPEG 2000 starts three of its nineteen contexts away from state 0 and JBIG2 starts all of them at 0. Implemented: codestream and JP2 container parsing, packet headers with tag trees and the bit-stuffing rule, EBCOT tier-1 (all three passes, four zero-coding context tables, run-length mode), both 5/3 reversible and 9/7 irreversible wavelets, RCT and ICT, arbitrary decomposition levels, and multiple components. Refused by name: multiple tiles, custom precinct partitions, code-block style options, COC/QCC/RGN/POC overrides, subsampled components. Each error says which feature the file needs. This matters more here than anywhere else in the stack, because a JPEG 2000 decoder that quietly skips something does not fail — it returns a slightly soft or banded image that looks entirely fine. That property also dictates how this is tested. Fixtures are produced by OpenJPEG via Pillow and compared **exactly**, sample for sample: the fixtures are lossless 5/3 so no tolerance is needed, and a tolerance is where a subtly wrong decoder hides. Four images — grayscale raw codestream, the same in a JP2 container, a larger one whose tag trees actually branch, and RGB. A generator script is checked in beside them so CI can prove the fixtures still match what produced them. Verified by mutation. The first round was misleading and is worth recording, because it is the same lesson as ADR 0017: DC level shift dropped 3 fail 5/3 lifting rounding changed 2 fail RCT sign flipped PASSED <- survived RCT components swapped PASSED <- survived cleanup run-length disabled PASSED <- survived sign-context XOR dropped PASSED <- survived Four mutations survived because Pillow writes MCT=0 by default, so the RGB fixture coded its three components independently and never reached the colour transform at all. The RCT branch was completely untested while appearing covered — an untested branch that looks tested is worse than one that looks missing. Added rgb8_mct.j2k with mct=1; all four now fail. The header bit-stuffing mutation is caught by the unit test rather than the round-trip. Two real defects found while writing the tests: - A corrupt marker length in a tile-part header walked the read cursor past the codestream and panicked on a slice. Found by the corruption sweep, not by review. The sweep now truncates at every length and flips every byte of a real file, and asserts only that nothing panics. - The 9/7 flat-signal test initially asserted an amplitude I had derived from my own arithmetic. That is a test agreeing with the code by construction. It now asserts flatness — a ripple means the lifting or the edge extension is wrong — and the amplitude is pinned by the OpenJPEG round-trips instead, which use pixels this code did not produce. Also removed two dead fields and an unused parameter that clippy found: Subband::x0/y0 are always zero in the single-tile case this supports, and dead state implying multi-tile support exists is worse than no state. JPX decodes on the image path, like JBIG2, because the codestream carries its own geometry; it stays in REFUSED_CODECS with a reason string saying where it is decoded rather than that it is missing. Engine suite 866 -> 920. Coverage 85.66% -> 86.16%; jpx.rs at 93.72% with a floor at 88. Phase 3 is complete: CCITT, JBIG2 and JPX all land, and the plan is updated to say so and to record how the two gating questions — JBIG2's CVE record and JPX's C dependency — were actually answered. |
|||
| 81e846ae35 |
feat(pdf): JBIG2 generic-region decoding, and the bitonal image path
Some checks failed
email.yml / feat(pdf): JBIG2 generic-region decoding, and the bitonal image path (push) Failing after 0s
repo hygiene / hygiene (push) Has been cancelled
PDF engine / engine (push) Has been cancelled
PDF engine / makepad-integration (push) Has been cancelled
PDF engine / fuzz (push) Has been cancelled
The second codec Phase 3 deferred. ADR 0015 made a threat review the precondition for implementing JBIG2 rather than an effort estimate, so the review's conclusion is encoded in what this does and does not do. What is implemented: the MQ arithmetic decoder (T.88 Annex E), generic region decoding with templates 0-3 and AT pixels, TPGDON typical prediction, and MMR-coded regions. Segment header and region info parsing, and page composition. What is refused, by name: symbol dictionary, text region, halftone region, refinement region, and a non-empty /JBIG2Globals. Those are the segment types that carry the composition machinery, and shipping them means shipping an interpreter over untrusted input — it is what FORCEDENTRY built its computer out of. A file needing them gets a typed error naming the segment type, exactly as the whole codec used to. The MQ coder itself is pure arithmetic with no file-controlled addressing, which is why it is safe to run and the composition parts are not. Every bound is checked against the declared region size before a buffer is indexed: region dimensions against MAX_DIMENSION and a pixel budget before allocation, segment lengths against the remaining stream, and the region's declared position against the page before a single pixel is written. That last one is the format's actual exploit surface and it has its own test saying so. MMR regions delegate to ccitt.rs rather than carrying a second G4 decoder, so the two cannot drift apart. A test decodes the same coded bits through both paths and requires identical pixels — that is what catches an inverted convention, and JBIG2 is natively 1=black where PDF is 0=black, so the inversion is real and easy to get backwards. JBIG2 is decoded on the image path, not in the filter facade, because it needs /Width and /Height from the image dictionary. It therefore stays in REFUSED_CODECS with a reason string that says where it *is* decoded, so a host showing that string does not tell a user the codec is missing when it is not. CCITT moved the other way for the same reason inverted: it derives its dimensions from /DecodeParms, so it decodes in the facade. Wiring both into ImageInfo::decode_to_rgba surfaced a defect in the parallel-array rule that the CCITT tranche had not reached. For /Filter [/FlateDecode /CCITTFaxDecode] the /DecodeParms array has one entry per filter, and the obvious implementation takes arr[0] — handing the Flate parameters to the fax decoder. ccitt_parms_of finds CCITT's own index instead. This is the same bug ADR 0015 records for the old chain code, in a new place. A declared-but-unresolved /JBIG2Globals returns None rather than decoding without it. Decoding anyway yields a blank or partial image that every caller reads as a success — the declared-versus-delivered failure of ADR 0017. Verified by mutation, six injected defects, each confirmed red: compose bounds check removed 1 fails pack() stops inverting 4 fails (both suites) globals silently ignored 1 fails refused segments silently skipped 1 fails declared-globals check dropped 1 fails ccitt_parms_of always takes slot 0 1 fails 29 unit tests and 12 integration tests, asserting pictures rather than buffer lengths. ADR 0016's stub JPEG decoder returned a correctly sized black rectangle and passed everything that checked a length; these say which colour they expect. Engine suite 825 -> 866. Coverage 85.15% -> 85.66%; jbig2.rs at 94.84% with a floor at 90, and image.rs 31.76% -> 44.77% so its floor rises 28 -> 40. JPX remains refused and is the next tranche. |