From 3acc132ff03fa04b36f2b635369716fcbcd499de Mon Sep 17 00:00:00 2001 From: aiongg Date: Sat, 24 Oct 2026 21:00:51 +0000 Subject: [PATCH] jpeg: mutation test, fuzz target and CI workflow tests/mutation.rs runs a seeded mutation pass over the test images and small SOF9, SOF10 and 12-bit seeds, checking that decode never panics and stays within small limits. fuzz/ is a cargo-fuzz crate (own workspace) with a decode target and those seeds. jpeg.yml runs fmt, clippy, tests and a wasm32 check on Linux. The README gains a testing section. --- .github/workflows/jpeg.yml | 37 ++++++ crates/jpeg/README.md | 32 +++++ crates/jpeg/fuzz/.gitignore | 4 + crates/jpeg/fuzz/Cargo.toml | 23 ++++ crates/jpeg/fuzz/corpus/decode/sof1-12bit.jpg | Bin 0 -> 138 bytes .../fuzz/corpus/decode/sof10-progressive.jpg | Bin 0 -> 394 bytes crates/jpeg/fuzz/corpus/decode/sof9-12bit.jpg | Bin 0 -> 206 bytes .../jpeg/fuzz/corpus/decode/sof9-restart.jpg | Bin 0 -> 430 bytes .../fuzz/corpus/decode/testimgari-huffman.jpg | Bin 0 -> 5756 bytes crates/jpeg/fuzz/corpus/decode/testimgari.jpg | Bin 0 -> 5126 bytes crates/jpeg/fuzz/corpus/decode/testorig.jpg | Bin 0 -> 5770 bytes crates/jpeg/fuzz/corpus/decode/testorig12.jpg | Bin 0 -> 12394 bytes crates/jpeg/fuzz/fuzz_targets/decode.rs | 23 ++++ crates/jpeg/tests/mutation.rs | 125 ++++++++++++++++++ 14 files changed, 244 insertions(+) create mode 100644 .github/workflows/jpeg.yml create mode 100644 crates/jpeg/fuzz/.gitignore create mode 100644 crates/jpeg/fuzz/Cargo.toml create mode 100644 crates/jpeg/fuzz/corpus/decode/sof1-12bit.jpg create mode 100644 crates/jpeg/fuzz/corpus/decode/sof10-progressive.jpg create mode 100644 crates/jpeg/fuzz/corpus/decode/sof9-12bit.jpg create mode 100644 crates/jpeg/fuzz/corpus/decode/sof9-restart.jpg create mode 100644 crates/jpeg/fuzz/corpus/decode/testimgari-huffman.jpg create mode 100644 crates/jpeg/fuzz/corpus/decode/testimgari.jpg create mode 100644 crates/jpeg/fuzz/corpus/decode/testorig.jpg create mode 100644 crates/jpeg/fuzz/corpus/decode/testorig12.jpg create mode 100644 crates/jpeg/fuzz/fuzz_targets/decode.rs create mode 100644 crates/jpeg/tests/mutation.rs diff --git a/.github/workflows/jpeg.yml b/.github/workflows/jpeg.yml new file mode 100644 index 00000000..d10c2598 --- /dev/null +++ b/.github/workflows/jpeg.yml @@ -0,0 +1,37 @@ +name: Shared JPEG + +on: + push: + branches: [main] + paths: ["crates/jpeg/**", "Cargo.toml", "Cargo.lock", ".github/workflows/jpeg.yml"] + pull_request: + paths: ["crates/jpeg/**", "Cargo.toml", "Cargo.lock", ".github/workflows/jpeg.yml"] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: shared-jpeg-${{ github.ref }} + cancel-in-progress: true + +jobs: + check: + timeout-minutes: 10 + name: JPEG lint, tests, WASM + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: dtolnay/rust-toolchain@b725eda525362e7cf34af9a7243cac1e3f01a7ec # stable + with: + components: rustfmt, clippy + targets: wasm32-unknown-unknown + - uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3 + with: + key: craft-jpeg + - run: cargo fmt -p craft-jpeg --check + - run: cargo clippy -p craft-jpeg --all-targets --locked -- -D warnings + - run: cargo test -p craft-jpeg --locked + - run: cargo check -p craft-jpeg --locked --target wasm32-unknown-unknown diff --git a/crates/jpeg/README.md b/crates/jpeg/README.md index 661be5aa..44eaee7c 100644 --- a/crates/jpeg/README.md +++ b/crates/jpeg/README.md @@ -95,6 +95,38 @@ SOF11 (lossless) falls in that range and returns `Unsupported`. XMP, Photoshop resources, resolution and the APP14 `Adobe` segment are the app's to read. Color management and encoding also stay in the apps. +## Testing + +`cargo test -p craft-jpeg` runs: + +- unit tests in `src/tests.rs`: arithmetic-coded streams from a test-only encoder must decode to + the coefficients that went in, and Huffman streams are compared against `zune-jpeg`. +- `tests/conformance.rs`, on libjpeg-turbo's test images in `tests/data` (provenance and how + the derived files were made: `tests/data/SOURCE.md`): + - `testimgari.jpg` (SOF9) decodes to exactly the samples of its jpegtran Huffman transcode. + - `testorig.jpg` matches `zune-jpeg`, and `testorig12.jpg` matches libjpeg-turbo's 12-bit + decode, within the float-against-integer IDCT difference (mean under 0.75, max 4). +- `tests/mutation.rs`: a seeded mutation run (truncations, bit flips, byte and frame-header + overwrites, deleted and repeated spans) over the test images and the fuzz seeds. Decoding must + not panic and must stay within small `Limits`. + +`.github/workflows/jpeg.yml` runs fmt, clippy, the tests and a `wasm32-unknown-unknown` check on +Linux. + +### Fuzzing + +`fuzz/` is a cargo-fuzz crate outside the workspace, with one target, `decode`. Its seeds in +`fuzz/corpus/decode` are the test images plus small SOF9 (with restarts), 12-bit SOF9, SOF10 +and 12-bit SOF1 streams. With nightly and `cargo install cargo-fuzz`: + +```sh +cd crates/jpeg/fuzz +cargo +nightly fuzz run decode +``` + +Running it adds the inputs it finds to `fuzz/corpus/decode`; commit only seeds that cover +something new. + ## Credits The decoder was written by Michael Higgins (Higgs32584) for PhotoCraft. diff --git a/crates/jpeg/fuzz/.gitignore b/crates/jpeg/fuzz/.gitignore new file mode 100644 index 00000000..d02230c0 --- /dev/null +++ b/crates/jpeg/fuzz/.gitignore @@ -0,0 +1,4 @@ +target +artifacts +coverage +Cargo.lock diff --git a/crates/jpeg/fuzz/Cargo.toml b/crates/jpeg/fuzz/Cargo.toml new file mode 100644 index 00000000..c4881e66 --- /dev/null +++ b/crates/jpeg/fuzz/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "craft-jpeg-fuzz" +version = "0.0.0" +publish = false +edition = "2024" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.4" +craft-jpeg = { path = ".." } + +# Its own workspace: cargo-fuzz needs nightly and sanitizer flags, so this crate stays out of the +# craft-libs workspace and its lockfile. +[workspace] + +[[bin]] +name = "decode" +path = "fuzz_targets/decode.rs" +test = false +doc = false +bench = false diff --git a/crates/jpeg/fuzz/corpus/decode/sof1-12bit.jpg b/crates/jpeg/fuzz/corpus/decode/sof1-12bit.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7c0e6860918b8dfacc83e92890446d7b324e755f GIT binary patch literal 138 zcmex==J%f(-wUFsLywGN1xp0dx_D|F=Nu85ryt${GLP F1OPx2452jZ5UintrRQqGqBzNJJ!7N3(-kQmRyCr76 z0vhMpkX^R;pY)rg*xK8!`*lrcUbL$Cc`GHYPy5V^-kf_Me^;`dd^zdsWMSTz9q)F{ zNbC5*(s2Ri07h2D%n3kC?pFX!3WA9;FbFgN1sQ;1KpNsrMpl6)pjawU;9@LHkj-8| u@#Nx%HUSI_>_ACh%f0>vlTS+M%K#PSzP=pK5p}0)GIQ?skbeySZvp_H>5dEl literal 0 HcmV?d00001 diff --git a/crates/jpeg/fuzz/corpus/decode/sof9-12bit.jpg b/crates/jpeg/fuzz/corpus/decode/sof9-12bit.jpg new file mode 100644 index 0000000000000000000000000000000000000000..dd0531825b1f532328bf04ec4676e82d840254d4 GIT binary patch literal 206 zcmex=o+4GBDUP{Ac*v64oim_13x4m8(4A{x+L^thbSD|%7u9&Sw$)b-!zk<`ELmPkcTPsao@c$+N DZS+FX literal 0 HcmV?d00001 diff --git a/crates/jpeg/fuzz/corpus/decode/sof9-restart.jpg b/crates/jpeg/fuzz/corpus/decode/sof9-restart.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c165ab4678b17a758fbbb2c49749ba4895083983 GIT binary patch literal 430 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 S25o&-SUN00f`hCtCjSG6MZ^UF literal 0 HcmV?d00001 diff --git a/crates/jpeg/fuzz/corpus/decode/testimgari.jpg b/crates/jpeg/fuzz/corpus/decode/testimgari.jpg new file mode 100644 index 0000000000000000000000000000000000000000..896648741b9b512b2f1f8e2ef0471a88ee0e6ca5 GIT binary patch literal 5126 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 literal 0 HcmV?d00001 diff --git a/crates/jpeg/fuzz/corpus/decode/testorig12.jpg b/crates/jpeg/fuzz/corpus/decode/testorig12.jpg new file mode 100644 index 0000000000000000000000000000000000000000..861aff98e2a78131bf546b82fd38b00a4f741dd2 GIT binary patch literal 12394 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 Iua1WP4;iQWivR!s literal 0 HcmV?d00001 diff --git a/crates/jpeg/fuzz/fuzz_targets/decode.rs b/crates/jpeg/fuzz/fuzz_targets/decode.rs new file mode 100644 index 00000000..b03dbfe9 --- /dev/null +++ b/crates/jpeg/fuzz/fuzz_targets/decode.rs @@ -0,0 +1,23 @@ +#![no_main] + +use craft_jpeg::{Limits, Samples, decode, frame}; +use libfuzzer_sys::fuzz_target; + +const LIMITS: Limits = Limits { max_pixels: 1 << 20, max_alloc: 64 << 20 }; + +fuzz_target!(|data: &[u8]| { + let _ = frame(data); + // The input length picks the Adobe transform, so every color path is reached without + // changing the stream. + let adobe = [None, Some(0), Some(1), Some(2)][data.len() % 4]; + if let Ok(img) = decode(data, &LIMITS, adobe) { + let pixels = u64::from(img.width) * u64::from(img.height); + let (len, width) = match &img.samples { + Samples::U8(v) => (v.len() as u64, 1), + Samples::U16(v) => (v.len() as u64, 2), + }; + assert!(pixels <= LIMITS.max_pixels); + assert!(len * width <= LIMITS.max_alloc); + assert_eq!(len, pixels * u64::from(img.components)); + } +}); diff --git a/crates/jpeg/tests/mutation.rs b/crates/jpeg/tests/mutation.rs new file mode 100644 index 00000000..a23a706b --- /dev/null +++ b/crates/jpeg/tests/mutation.rs @@ -0,0 +1,125 @@ +//! Deterministic mutation run over the test images and fuzz seeds: decoding corrupted input never panics and +//! stays within the caller's limits. + +use craft_jpeg::{Limits, Samples, decode, frame}; + +/// Inputs and mutation counts. The 227×149 test images take tens of milliseconds each to decode +/// in a debug build; the small fuzz seeds (SOF9 with restarts, 12-bit SOF9, SOF10, 12-bit SOF1) +/// take microseconds and get most of the cases. +const INPUTS: [(&str, usize); 8] = [ + ("tests/data/testorig.jpg", 12), + ("tests/data/testimgari.jpg", 12), + ("tests/data/testimgari-huffman.jpg", 12), + ("tests/data/testorig12.jpg", 12), + ("fuzz/corpus/decode/sof9-restart.jpg", 75), + ("fuzz/corpus/decode/sof9-12bit.jpg", 75), + ("fuzz/corpus/decode/sof10-progressive.jpg", 75), + ("fuzz/corpus/decode/sof1-12bit.jpg", 75), +]; + +/// Room for the 227×149 fixtures, too little for a mutated frame header that grows them much. +const LIMITS: Limits = Limits { max_pixels: 1 << 16, max_alloc: 1 << 20 }; + +struct Rng(u64); + +impl Rng { + fn next(&mut self) -> u64 { + // xorshift64* + self.0 ^= self.0 >> 12; + self.0 ^= self.0 << 25; + self.0 ^= self.0 >> 27; + self.0.wrapping_mul(0x2545_F491_4F6C_DD1D) + } + + fn below(&mut self, n: usize) -> usize { + (self.next() % n.max(1) as u64) as usize + } + + fn byte(&mut self) -> u8 { + self.next() as u8 + } +} + +/// Offset of the first SOFn segment's body, if any. +fn sof_body(b: &[u8]) -> Option { + (2..b.len().saturating_sub(1)) + .find(|&i| b[i] == 0xFF && matches!(b[i + 1], 0xC0..=0xCF) && !matches!(b[i + 1], 0xC4 | 0xC8 | 0xCC)) + .map(|i| i + 4) +} + +fn mutate(rng: &mut Rng, original: &[u8]) -> Vec { + let mut b = original.to_vec(); + match rng.below(6) { + 0 => b.truncate(rng.below(original.len())), + 1 => { + for _ in 0..1 + rng.below(8) { + let at = rng.below(b.len()); + b[at] ^= 1 << rng.below(8); + } + } + 2 => { + for _ in 0..1 + rng.below(4) { + let at = rng.below(b.len()); + b[at] = rng.byte(); + } + } + 3 => { + // Frame header fields: precision, height, width, component count and the + // per-component sampling and table selectors. + if let Some(body) = sof_body(&b) { + for _ in 0..1 + rng.below(3) { + let at = body + rng.below(15); + if let Some(v) = b.get_mut(at) { + *v = rng.byte(); + } + } + } + } + 4 => { + let at = rng.below(b.len()); + let len = rng.below(64).min(b.len() - at); + b.drain(at..at + len); + } + _ => { + // Repeat a span, e.g. a marker segment or part of the entropy-coded data. + let at = rng.below(b.len()); + let len = rng.below(256).min(b.len() - at); + let span = b[at..at + len].to_vec(); + let to = rng.below(b.len()); + b.splice(to..to, span); + } + } + b +} + +#[test] +fn mutated_streams_never_panic_and_stay_within_limits() { + let mut rng = Rng(0x9E37_79B9_7F4A_7C15); + let (mut ok, mut errors) = (0, 0); + for (name, cases) in INPUTS { + let path = format!("{}/{name}", env!("CARGO_MANIFEST_DIR")); + let original = std::fs::read(&path).unwrap_or_else(|e| panic!("{path}: {e}")); + assert!(decode(&original, &LIMITS, None).is_ok(), "{name} must decode within the test limits"); + for case in 0..cases { + let bytes = mutate(&mut rng, &original); + let _ = frame(&bytes); + match decode(&bytes, &LIMITS, None) { + Ok(img) => { + let pixels = u64::from(img.width) * u64::from(img.height); + let (len, width) = match &img.samples { + Samples::U8(v) => (v.len() as u64, 1), + Samples::U16(v) => (v.len() as u64, 2), + }; + assert!(pixels <= LIMITS.max_pixels, "{name} case {case}: {pixels} pixels"); + assert!(len * width <= LIMITS.max_alloc, "{name} case {case}: {} bytes", len * width); + assert_eq!(len, pixels * u64::from(img.components), "{name} case {case}"); + ok += 1; + } + Err(_) => errors += 1, + } + } + } + eprintln!("{ok} decoded, {errors} rejected"); + // A mutation run that only produces errors would no longer reach the entropy decoders. + assert!(ok > 50 && errors > 50, "{ok} decoded, {errors} rejected"); +}