From b600e6a5cd5f9ccab89377b3eb0f6bb8dbee0105 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Wed, 28 Oct 2026 22:15:50 -0400 Subject: [PATCH] crw: split decode_entropy under the cognitive-complexity limit main's workspace lints now deny cognitive_complexity above 30, and decode_entropy scored 41. The emit macro becomes a Chains struct, unstuffing its own function, and the three stream-end checks one. Same algorithm: the pinned CC0 digests still match byte for byte. Also correct two record lines: the G5 is the one file that does not match, and the frozen code tables are not the only logs that ship. --- crates/crw/clean-room/02-crw-format.md | 4 +- crates/crw/clean-room/README.md | 4 +- crates/crw/src/lib.rs | 135 ++++++++++++------------- 3 files changed, 71 insertions(+), 72 deletions(-) diff --git a/crates/crw/clean-room/02-crw-format.md b/crates/crw/clean-room/02-crw-format.md index 7b5c855b..9d0304ad 100644 --- a/crates/crw/clean-room/02-crw-format.md +++ b/crates/crw/clean-room/02-crw-format.md @@ -111,5 +111,5 @@ which the Rust implementation may normalise, provided the verification table of ## 5 Verification See `03-verification.md` (written after the freeze): full-frame exact-match decode -against LibRaw 0.22.2 for every sample file, public and private, with zero tuning -after the freeze commit. +against LibRaw 0.22.2, with zero tuning after the freeze commit: every sample file, public +and private, matches except the PowerShot G5, which the decoder refuses (28 of 29). diff --git a/crates/crw/clean-room/README.md b/crates/crw/clean-room/README.md index 1174e713..4c2325ef 100644 --- a/crates/crw/clean-room/README.md +++ b/crates/crw/clean-room/README.md @@ -29,8 +29,8 @@ renumbered `01`–`05` in reading order. - `logs/table_sel{0,1,2}.json`: **the frozen code tables**, the source of truth that `tools/gen_tables.py` renders into `../src/tables.rs`. `table_sel0_d60class.json` is the - revision-1 selector-0 table, kept for the record. These are the only logs that ship with the - crate: without them the frozen tables cannot be regenerated or audited. + revision-1 selector-0 table, kept for the record. These are the frozen code-table logs that ship + with the crate: without them the frozen tables cannot be regenerated or audited. - `logs/private-corpus.tsv`: names, sizes and SHA-256 of the maintainer's own files. No image content, metadata or thumbnails are recorded anywhere here. - `logs/private/` (gitignored): per-file dumps of the private files, which can hold personal diff --git a/crates/crw/src/lib.rs b/crates/crw/src/lib.rs index da12940e..33ad8009 100644 --- a/crates/crw/src/lib.rs +++ b/crates/crw/src/lib.rs @@ -647,13 +647,7 @@ fn build_lut(codes: &[(u16, u8, u8, u8)]) -> Vec { /// initialised to 512 and reset at every row start. A stream that ends early leaves the remaining /// samples at their chain values (the frozen stream-end rule). fn decode_entropy(entropy: &[u8], w: usize, h: usize, tables: &tables::SelTables) -> Result> { - // unstuff: an 0x00 after an 0xff is a stuffing byte - let mut d = Vec::with_capacity(entropy.len()); - let mut i = 0; - while let Some(&b) = entropy.get(i) { - d.push(b); - i += if b == 0xff && entropy.get(i + 1) == Some(&0x00) { 2 } else { 1 }; - } + let d = unstuff(entropy); let nbits = d.len().saturating_mul(8); let peek16 = |pos: usize| -> u16 { let (byte, sh) = (pos >> 3, pos & 7); @@ -664,65 +658,35 @@ fn decode_entropy(entropy: &[u8], w: usize, h: usize, tables: &tables::SelTables let luts = [build_lut(tables.first), build_lut(tables.main)]; let n = w.checked_mul(h).ok_or(Error::LimitExceeded("image too large"))?; - let mut dec = vec![0u16; n]; - let (mut ce, mut co): (i32, i32) = (512, 512); + let mut out = Chains { dec: vec![0u16; n], w, qi: 0, ce: 512, co: 512 }; let mut carry: i32 = 0; let mut pos = 0usize; - let mut qi = 0usize; - // emit one diff: row-start chain reset, parity chain update, 16-bit masked store - macro_rules! emit { - ($d:expr) => {{ - if qi % w == 0 { - ce = 512; - co = 512; - } - let c = if qi % 2 == 0 { &mut ce } else { &mut co }; - *c = c.wrapping_add($d); - if let Some(p) = dec.get_mut(qi) { - *p = (*c & 0xffff) as u16; - } - qi += 1; - }}; - } - 'frame: while qi < n { - let block_end = n.min((qi / 64 + 1) * 64); + while out.qi < n { + let block_end = n.min((out.qi / 64 + 1) * 64); let mut tok = 0u32; - while qi < block_end { + while out.qi < block_end { + // stream exhausted: every remaining sample keeps its chain value if pos >= nbits.saturating_sub(1) { - // stream exhausted: every remaining sample keeps its chain value - while qi < n { - emit!(0); - } - break 'frame; + out.fill(n); + break; } let e = luts[usize::from(tok > 0)].get(peek16(pos) as usize).copied().unwrap_or(0); if e == 0 { return Err(corrupt("undecodable entropy code")); } - let (len, rr, s) = ((e >> 16) as usize, ((e >> 8) & 0xff) as usize, e & 0xff); - if pos.saturating_add(len) > nbits { - while qi < n { - emit!(0); - } - break 'frame; + let (len, mut rr, s) = ((e >> 16) as usize, ((e >> 8) & 0xff) as usize, e & 0xff); + if pos.saturating_add(len).saturating_add(s as usize) > nbits { + out.fill(n); + break; } pos += len; - let mut rr = rr; - if s == 0 && rr == 0 && tok > 0 { + tok += 1; + if s == 0 && rr == 0 && tok > 1 { // end of block - while qi < block_end { - emit!(0); - } - tok += 1; + out.fill(block_end); continue; } let diff: i32 = if s > 0 { - if pos.saturating_add(s as usize) > nbits { - while qi < n { - emit!(0); - } - break 'frame; - } let m = (peek16(pos) >> (16 - s)) as i32; pos += s as usize; // JPEG magnitude rule @@ -730,32 +694,67 @@ fn decode_entropy(entropy: &[u8], w: usize, h: usize, tables: &tables::SelTables } else { 0 }; - let mut first = qi.is_multiple_of(64); + let mut first = out.qi.is_multiple_of(64); if first && rr > 0 { // the block's first sample covered by the skip: its diff is exactly the carry - emit!(carry); + out.emit(carry); rr -= 1; first = false; } - for _ in 0..rr { - if qi >= block_end { - break; - } - emit!(0); - } - if qi >= block_end { - tok += 1; + out.fill(block_end.min(out.qi + rr)); + if out.qi >= block_end { continue; } - let dd = if first { + if first { carry = carry.wrapping_add(diff); - carry + out.emit(carry); } else { - diff - }; - emit!(dd); - tok += 1; + out.emit(diff); + } + } + } + Ok(out.dec) +} + +/// Drop the stuffing byte: an 0x00 after an 0xff. +fn unstuff(entropy: &[u8]) -> Vec { + let mut d = Vec::with_capacity(entropy.len()); + let mut i = 0; + while let Some(&b) = entropy.get(i) { + d.push(b); + i += if b == 0xff && entropy.get(i + 1) == Some(&0x00) { 2 } else { 1 }; + } + d +} + +/// The decoded samples and the two parity DPCM chains that produce them. +struct Chains { + dec: Vec, + w: usize, + qi: usize, + ce: i32, + co: i32, +} + +impl Chains { + /// Emit one diff: row-start chain reset, parity chain update, 16-bit masked store. + fn emit(&mut self, d: i32) { + if self.qi.is_multiple_of(self.w) { + self.ce = 512; + self.co = 512; + } + let c = if self.qi.is_multiple_of(2) { &mut self.ce } else { &mut self.co }; + *c = c.wrapping_add(d); + if let Some(p) = self.dec.get_mut(self.qi) { + *p = (*c & 0xffff) as u16; + } + self.qi += 1; + } + + /// Emit zero diffs up to sample `end`. + fn fill(&mut self, end: usize) { + while self.qi < end { + self.emit(0); } } - Ok(dec) }