Skip to content

dicos.rs 2.0.0 — standards-conformant codecs, hardened parsing, honest docs - #17

Merged
jpfielding merged 31 commits into
mainfrom
v2
Jul 20, 2026
Merged

dicos.rs 2.0.0 — standards-conformant codecs, hardened parsing, honest docs#17
jpfielding merged 31 commits into
mainfrom
v2

Conversation

@jpfielding

Copy link
Copy Markdown
Owner

dicos.rs 2.0.0 — standards-conformant codecs, hardened parsing, honest docs

A four-way critical review found the just-published 1.0.0 crates overstated conformance — most severely, pure_jpeg2k emitted raw DWT coefficients, not JPEG 2000 at all. This release makes all five crates genuinely conformant within an explicit, documented scope, hardens every decode path against untrusted input, and corrects the docs to match what the code actually does.

Planned and adversarially reviewed with Codex (gpt-5.6-sol, high effort) twice — once on the plan, once on the finished branch.

Codecs — real conformance, verified against reference implementations

  • pure_jpeg2k now emits genuine ITU-T T.800 lossless codestreams (5/3 DWT + EBCOT tier-1/tier-2 + MQ coder). OpenJPEG 2.5.4 decodes our output byte-exactly, both directions. Wiring the entropy coder exposed two shared encoder/decoder bugs self-round-trips had masked (inverted MQ interval-split convention; reversed DWT pass order) — both fixed. 1.0.0 / Go raw-DWT files remain decodable via LegacyPolicy.
  • pure_jpegls — conformant ITU-T T.87 (single-bit stuffing, limited Golomb, run mode, LSE presets, near-lossless). Profile::LegacyGo preserves the frozen 1.0.0 / Go-compatible bytes.
  • pure_jpegli — conformant ITU-T T.81 Process 14 (predictors 1–7, point transform, row-aligned restart intervals). Verified both directions against libjpeg-turbo 3.1.4.
  • Legal-but-unsupported features are rejected loudly via validated support matrices, not silently mis-encoded.

Untrusted-input hardening (second review pass)

Fixed a class of panics and allocation bombs on attacker-controlled bytes across every decoder: DHT oversubscription, SOS selector bounds, zero dimensions, QCD shift overflow, the DICOM Basic Offset Table allocation bomb, SOF pixel-count caps, strict truncation handling, and multi-precinct rejection. Multi-fragment DICOM frame assembly and native 8-bit pixel handling corrected. Mutation-test suites, per-codec cargo-fuzz targets, and a two-direction interop CI job added.

dicos core API 2.0 (breaking)

Typed #[non_exhaustive] errors, truncation semantics, ParseWarning, private GrayImage fields with accessors, Option-returning attribute accessors, Dataset::iter() -> &Element. See MIGRATION.md for the full 1.x → 2.0 guide.

roxel viewer

Background volume loading off the render thread, state consolidation, module splits, rayon-parallel slice compositing, GPU error dialogs.

Verification

  • 536 tests pass; cargo clippy --workspace --all-features --all-targets clean; cargo package --workspace --locked succeeds.
  • Third-party interop fixtures (OpenJPEG, libjpeg-turbo) checked in; CharLS harness present.

🤖 Generated with Claude Code

jpfielding and others added 30 commits July 19, 2026 20:11
jpeg2k raw-DWT and jpegls Go-compatible bitstreams are pinned by
checked-in fixtures + byte-identity/decode regression tests, before any
conformance work changes the coders (plan Workstream 0).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaces .expect() panics on surface/adapter/device acquisition with an
rfd error dialog and a low-power fallback-adapter retry; Camera::MIN/
MAX_DISTANCE consts shared by camera.rs and renderer.rs (plan WS4 B1).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Step 1-3 of the T.800 conformance work: Jpeg2kOptions::validate()
(cb exps 2..=10 sum<=12, multi-tile rejected), Annex-D context init and
C.3.4 BYTEIN marker handling in the MQ coder, floor-semantics 5/3
lifting with conformant multi-level entry points (degenerate regions,
hoisted scratch). Legacy raw-DWT pipeline preserved byte-identically
behind private truncating-division copies; crate-wide dead_code allow
replaced with targeted TODO(t800) allows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
J1-J3b of the conformance work: SOF3/SOS contract enforcement
(precision 2..=16, Nf=1, Ss 1..=7, Se=0, Ah=0, Al<P), arithmetic-shift
predictors 5-7, EncodeOptions with predictor/point-transform/precision
selection, reduced-domain point transform (P' = P - Pt), and the
Table H.2 SSSS=16 no-appended-bits case both directions. Also fixes a
pre-existing Huffman EOF short-circuit that broke long codes at end of
stream. Restart emission reserved for J4 (rejected until then).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
WS3 A1+A2: CodecError/DicosError gain #[non_exhaustive] plus typed
variants (Backend, BadPreamble, NestingTooDeep, UnexpectedTag,
LengthExceedsLimit/Buffer, UndefinedLengthInFixedSq, Truncated);
reader distinguishes clean EOF from partial headers via a byte
counter, verifies skip counts, and parses all element sequences
through one parse_elements_until; pixel-data integrity downgrades
become ParseWarning values surfaced by parse_with_warnings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Precinct/band/code-block partition math via the general Eq. B-15 form
(proven equivalent to the ceil-difference simplification), Mallat
placement verified against the conformant DWT packing; canonical
tag-tree coder with incremental cross-packet state, proptest-verified
bit-exact round-trips. Both unwired until tier-2 (plan WS1 steps 4+6).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Completes J4: DRI emission and validation (Ri must be a multiple of
samples-per-row), RSTn cycling with order checking, predictor reset at
restart rows (first-line semantics), BitReader stops at any marker
instead of silently skipping RSTn, BitWriter pad_and_align refactor.
Round-trip matrix + error-path tests; 82 unit tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
L0-L2: 1.0.0/Go-compatible scan loops and bit I/O frozen verbatim in
legacy.rs (public API reroutes through it, fixtures byte-identical);
bitstream.rs rewritten for T.87 single-bit stuffing + A.5.3 limited
Golomb with structured marker signaling; context.rs rewritten per spec
(A[Q]=max(2,(RANGE+32)/64) init, A.12/A.13 updates, near-aware A.3.3
quantizer with both MAXVAL branches, run contexts with Nn stats).
New paths unwired until L3/L4.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
WS4 B3: egui drawing moves to ui/{sidebar,slice_panel,controls,
band_slider} with a thin dispatcher; UiState decomposes into
VolumeLibrary/TransferState/RenderSettings/UiActions in state.rs
(RenderSettings is pub, ready to ride RenderParams in B4). app.rs
drops from 1227 to 584 lines; behavior unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
WS4 B4: the eight per-frame-resynced VolumeRenderer fields are gone --
render() reads RenderParams::settings (RenderSettings) directly;
same-dimension layer switches reuse the existing GPU texture via
queue.write_texture with no allocation or bind-group rebuild.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Full rewrite: 4-row stripe scan, orientation-aware zero-coding tables,
5 sign contexts with XOR, 3 magnitude-refinement contexts, run-length
cleanup mode, cleanup-only MSB plane with num_passes = 3*nb-2, live
significance in sig-prop and pi/VISITED semantics per D.4. One shared
Tier1 state machine drives encode and decode through a single MQ
code() primitive, so the two directions cannot drift. Self-golden 8x8
vector pinned until the OpenJPEG cross-check lands in step 10.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
WS4 B5: VolumeLoader runs parse, decode, threat sidecar merge,
pack_for_gpu, and center_of_mass on a worker thread; the render thread
only performs wgpu uploads from the precomputed buffer. Spinner +
disabled Open buttons while in flight, load failures surfaced in the
sidebar. Layer switching keeps the synchronous pack fallback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
WS4 B6: render_composite parallelizes by row via composite_pixel with
a pixel-identity test against a sequential reference; the multiplicative
opacity chain (density curve x band alpha x global x gradient boost) is
documented on TransferFunction and cross-referenced in raycast.wgsl.
ColorBand.alpha retained as material-selective opacity.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Steps 7+8: B.10 packet header codec (FF-stuffed bit I/O, inclusion +
zero-bitplane tag trees, Table B.4 pass counts -- corrected from the
plan's prose, which was non-conformant -- Lblock length signaling) and
the DWT-to-EBCOT-to-LRCP-packet tile pipeline with Mb from a QCD
closure. Legacy raw-DWT tile coder frozen verbatim in legacy.rs; the
public API still routes through it until step 9.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
L3-L5: regular-mode scan per A.4-A.6 (column-0 Rc border model, A.5.2
special-case mapping, near-lossless reconstruction over RANGE), run
mode per A.7/A.19-A.23 (RUNindex persists across lines, EOL lone-bit
rule, limited Golomb interruption coding), LSE ID=1 presets honored,
DRI/multi-component rejected loudly, precision/NEAR validated. Public
default is now Profile::T87; Profile::LegacyGo keeps the frozen Go
bitstream, fixtures byte-identical. Also fixes a trailing-0xFF stuff
byte omission found during bring-up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Step 9: encode() emits real T.800 (DC level shift, COD from options,
E-10 QCD exponents with G=2, verified Psot); decode() walks markers
structurally with a full support-matrix (specific Unsupported per
legal-but-unsupported feature) and routes through the tier-2 pipeline.
LegacyPolicy{Auto,StandardOnly,LegacyOnly} behind default-on
legacy-decode feature; Auto discriminates on the all-zero-QCD anomaly
plus payload fingerprint. Legacy fixtures decode under Auto and are
refused under StandardOnly. Also documents that the 1.0.0 writer
emitted a wrong SOD code (FFD3); both accepted at the legacy offset.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
WS3 A3+A4: GrayImage fields private behind accessors; Dataset::iter
yields &Element (+ tags()); rows/columns/bits_allocated/
pixel_representation return Option (number_of_frames keeps its
DICOM-defined default of 1); root re-exports for the common surface.
Registry adapters collapse into codec_adapter! with typed Backend
errors and honest DimensionMismatch; decode_frame no longer sniffs
(explicit decode_frame_sniffed instead); sniff_codec drops the RLE
guess and JP2 magic and walks JPEG markers structurally; jpeg2k
decodes pinned to LegacyPolicy::StandardOnly for untrusted data.
roxel distinguishes MissingAttribute from zero-valued Rows/Columns.
MIGRATION.md started.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
WS4 B2 (pure code motion): Volume + GPU packing stay in mod.rs with
re-exports preserving every crate::volume::* path; DICOS extraction
and value parsing move to dataset.rs; ThreatBox parsing/coloring and
sidecar loading move to threats.rs. Tests split along the same seams;
no consumer edits needed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
WS3 A5: reader tests move to reader/{tests,test_util}.rs with reusable
wire-format builders; the luggage generator becomes an example and
imports UIDs from the new dicos::uid module instead of local dupes;
write/parse fixed-point proptests (4 properties x 100 cases); cargo-fuzz
parse_dataset target under fuzz/ (workspace-excluded); criterion
removed from workspace deps; codec_adapter! gated to codec features.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds Truncated/InvalidParameter variants matching jpegls, checked
width*height with a 2^28-pixel cap before allocation in both decoders,
structured truncation errors in RLE segment handling, and removes the
unreachable MAX_SEGMENTS encode check.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
jpegli: PASSES two-direction interop with libjpeg-turbo 3.1.4 (our
streams decode under djpeg; cjpeg -lossless psv/Pt/restart fixtures
decode exactly). jpegls: env-gated charls harness (fixtures populated
where charls tooling exists). jpeg2k: OpenJPEG fixtures + two-direction
harness checked in but IGNORED — decoding real opj_compress output
exposed a tier-1/MQ conformance defect that self-round-trips masked;
root-cause fix to follow before the ignores come off.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two shared encoder/decoder deviations from T.800 that self-round-trips
masked and OpenJPEG interop exposed: the MQ coder placed the MPS
sub-interval at the interval base (producing bitwise-complement
codewords vs Annex C), and the forward 5/3 DWT ran rows-then-columns
(observable through floor rounding). Both fixed to the normative
behavior; our code-block bytes are now byte-identical to OpenJPEG's
for the cross-check vector, and the two-direction opj interop tests
are un-ignored and green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mutation tests (truncation at every prefix, bit flips, chopped
trailing markers, reordered RSTn) across all four codecs; cargo-fuzz
decode targets per codec crate (jpegls covers both profiles); CI gains
an interop job (OpenJPEG both directions; jpegli fixture-decode only
on ubuntu's turbo 2.x) and a weekly fuzz-smoke job. The jpegli
mutation suite found a real bug -- unchecked SSSS > 16 from a corrupted
DHT panicked in debug and silently mis-read in release -- fixed with a
range check in the scan loop and the test un-ignored.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
WS3 A6: workspace and inter-crate versions to 2.0.0; READMEs and lib
docs rewritten to the verified conformance claims (OpenJPEG and
libjpeg-turbo interop noted); MIGRATION.md completed; PUBLISHING.md
updated; testdata/ (37MB) and roxel-lrg.gif (12MB) untracked forward-
only with CI generating synthetic fixtures via the example; README GIF
points at the v2.0.0 release asset (upload on release).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The workspace /target/ ignore did not match crates/*/fuzz/target, so
~213 MB of fuzz build outputs (856 files) were committed. Untrack them
and extend .gitignore to cover every nested fuzz target/corpus/artifacts
dir.
Round-2 hardening from the final Codex review: crafted QCD exponents
+ guard bits could drive num_bitplanes >= 32 and shift past 31 bits
(debug panic / release corruption) -- rejected at QCD parse plus a
defensive guard before the tier-1 shift loop; multi-precinct images
(dimension > 32768) were silently packed as one packet per resolution
-- now rejected loudly as Unsupported in both encode and decode, making
the README's 'rejected loudly' guarantee true; CodecError and
Jpeg2kOptions gain #[non_exhaustive] for 2.x semver headroom.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Round-2 hardening from the final Codex review: the Basic Offset Table
length drove an unbounded reserve() before any bytes were read (a tiny
file could request ~1B entries) -- entries are now pushed in the read
loop with a bounded speculative reservation; encapsulated fragments
are assembled into frames using the BOT offsets per PS3.5 A.4 instead
of exposing every fragment as its own frame; native 8-bit pixel data
respects BitsAllocated (one sample per byte) instead of always pairing
into u16, with a ParseWarning when BitsAllocated is absent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Round-2 hardening from the final Codex review: SOF55 rejects zero and
oversized dimensions (2^28-pixel cap) before the pixel-buffer alloc
(was an ~8 GiB bomb); LSE ID=1 presets are validated per T.87
C.2.4.1.1 (MAXVAL within precision, NEAR < T1 <= T2 <= T3 <= MAXVAL,
RESET >= 3) instead of feeding context arithmetic unchecked. The
frozen-fixture regenerator is demoted from an #[ignore] test to a
reference-only function so the suite has zero ignored tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Round-2 hardening from the final Codex review: oversubscribed DHT
tables (Kraft check), SOS DC selector >= 4, and zero SOF3 dimensions
(also a DRI divide-by-zero) all now return errors instead of panicking;
SSSS category 16 is rejected below full precision per T.81 H.1.2.2;
truncated entropy now returns Truncated instead of zero-padding to a
full-size wrong Ok. Mutation suite strengthened to assert Err on
mid-entropy truncation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… enums

Follow-through on the final Codex review: Value and PixelData gain
#[non_exhaustive] (dicosctl grows a wildcard arm); README corrects the
SV1 conflation (Process 14 lossless, default SV1, predictors 1-7
selectable), the CharLS claim (harness present, self-skips until
fixtures supplied), and refreshes test counts to 536; MIGRATION
describes the actual LegacyPolicy::Auto QCD-fingerprint flow and the
new non_exhaustive data enums. The jpeg2k multi-precinct rejection and
non_exhaustive error enum make the previously-inaccurate 'rejected
loudly' and 'every codec error enum' claims true.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The A6 forward-only cleanup untracked all of testdata/, but bag_ct.dcs
is a curated sample the reader and luggage integration tests assert on
and the example generator cannot reproduce -- so those tests silently
skipped on a fresh checkout. Re-track the curated samples (bag_ct.dcs
plus the two threat sample dirs); only the CI-generated
testdata/synthetic/ stays gitignored. README/MIGRATION/CI reconciled.

Note: the two threat sample directories (~32 MB) are not referenced by
any automated test -- they are roxel demo data -- and can be dropped
later if repo weight matters.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jpfielding
jpfielding merged commit c13210d into main Jul 20, 2026
2 of 4 checks passed
@jpfielding
jpfielding deleted the v2 branch July 20, 2026 13:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant