From 25765612395d97f08a816177cfe8fa042a5c37b0 Mon Sep 17 00:00:00 2001 From: JD Lien Date: Thu, 16 Jul 2026 11:59:38 -0600 Subject: [PATCH 1/8] feat(rar3,rar5): in-band RAR3 standard filters + shared Delta transform MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RAR3 (v29) in-band filter declarations (main symbol 257) are now decoded instead of refused. WinRAR's compressor only ever emits a fixed set of standard RarVM programs, so — like libarchive and unrar in practice — the decoder recognizes them by bytecode length + CRC-32 and runs native transforms; any other program still fails with Error::Unsupported, never wrong bytes: - Delta (channel de-interleave; channels in VM register 0) — new, shared with rar5 via the new crate-internal rar_filters module. This also closes the rar5 Delta gap: rar5 FilterKind::Delta now decodes instead of returning Unsupported. - x86 E8 / E8E9 call translation — reuses the rar5 transform (with the nested range checks from the recent corruption fix). The rar5 x86 filters also gain correct solid-archive semantics: unrar and libarchive compute E8 addresses relative to the *containing file*, not the solid stream (libarchive tracks this as solid_offset). A new Decoder::add_file_boundary lets the container register member offsets; without it single-file streams behave as before. Wire format and transform semantics were derived from public format documentation and validated bit-exact against archives produced by RARLAB rar 6.24, differentially compared byte-for-byte with UnRAR.exe 7.23 across a 100+ archive corpus (RAR4 m1-m5 matrix incl. dict sweeps, filter isolation archives, and RAR5 solid/nonsolid groups): 188 entries byte-identical, 0 mismatches. Delta covers more than bitmaps: rar 6.24 emits it for WAV (2ch) and even generic content at -m5 (12ch observed); filters are auto-applied at m2-m5, so this unblocks most real RAR4 archives, not just executables/images. Embedded regression fixtures carry real payloads from that corpus with the archive's own FILE_CRC as ground truth (tests/fixtures/rar3/). Clean-room: structure cross-checked against libarchive (BSD) only; no code from RARLAB's unRAR or The Unarchiver. Fingerprint constants were computed from archives our own tooling generated. --- src/checksum.rs | 11 +- src/lib.rs | 8 +- src/rar3/decoder.rs | 410 +++++++++++++++++- src/rar3/filters.rs | 109 ++++- src/rar3/mod.rs | 12 +- src/rar5/decoder.rs | 55 ++- src/rar5/filters.rs | 113 ++--- src/rar_filters.rs | 166 +++++++ .../rar3/filter_delta_gradient_bmp.bin | Bin 0 -> 395 bytes tests/fixtures/rar3/filter_delta_ramp_wav.bin | Bin 0 -> 320 bytes tests/fixtures/rar3/filter_x86_slice.bin | Bin 0 -> 14249 bytes tests/fixtures/rar3/m5_calls_delta12.bin | Bin 0 -> 1323 bytes tests/rar3.rs | 69 +++ 13 files changed, 847 insertions(+), 106 deletions(-) create mode 100644 src/rar_filters.rs create mode 100644 tests/fixtures/rar3/filter_delta_gradient_bmp.bin create mode 100644 tests/fixtures/rar3/filter_delta_ramp_wav.bin create mode 100644 tests/fixtures/rar3/filter_x86_slice.bin create mode 100644 tests/fixtures/rar3/m5_calls_delta12.bin diff --git a/src/checksum.rs b/src/checksum.rs index f232133..d74321c 100644 --- a/src/checksum.rs +++ b/src/checksum.rs @@ -57,13 +57,13 @@ impl Default for Adler32 { /// IEEE / gzip CRC-32. Polynomial `0xEDB88320` (reflected), initial value /// `0xFFFFFFFF`, final XOR `0xFFFFFFFF`. -#[cfg(any(feature = "gzip", test))] +#[cfg(any(feature = "gzip", feature = "rar3", test))] #[derive(Debug, Clone, Copy)] pub struct Crc32 { state: u32, } -#[cfg(any(feature = "gzip", test))] +#[cfg(any(feature = "gzip", feature = "rar3", test))] impl Crc32 { pub const fn new() -> Self { Self { state: 0xFFFF_FFFF } @@ -102,12 +102,15 @@ impl Crc32 { self.state ^ 0xFFFF_FFFF } + /// Only the gzip codec re-arms a CRC mid-stream; rar3's filter + /// recognition uses one-shot instances. + #[cfg(any(feature = "gzip", test))] pub fn reset(&mut self) { *self = Self::new(); } } -#[cfg(any(feature = "gzip", test))] +#[cfg(any(feature = "gzip", feature = "rar3", test))] impl Default for Crc32 { fn default() -> Self { Self::new() @@ -118,7 +121,7 @@ impl Default for Crc32 { /// standard 256-entry CRC-32 table; `CRC32_TABLE8[n]` for `n >= 1` advances /// the CRC by an extra byte position, so eight bytes can be folded per /// iteration. See Intel's "Slicing-by-8" technique. -#[cfg(any(feature = "gzip", test))] +#[cfg(any(feature = "gzip", feature = "rar3", test))] const CRC32_TABLE8: [[u32; 256]; 8] = { let mut tables = [[0u32; 256]; 8]; diff --git a/src/lib.rs b/src/lib.rs index 8c3aa0c..e918f45 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -47,7 +47,7 @@ pub mod tokio_io; // `lz4`) doesn't pull them in via `cfg(test)`. #[cfg(any(feature = "deflate", feature = "deflate64"))] mod bits; -#[cfg(any(feature = "zlib", feature = "gzip"))] +#[cfg(any(feature = "zlib", feature = "gzip", feature = "rar3"))] mod checksum; #[cfg(any(feature = "deflate", feature = "deflate64"))] mod huffman; @@ -171,6 +171,12 @@ pub mod rar3; #[cfg(feature = "rar5")] pub mod rar5; +// Standard post-decompression filter transforms (x86 E8/E8E9, Delta) shared +// by the rar3 and rar5 decoders — the two container generations declare +// filters differently but run the same byte transforms. +#[cfg(any(feature = "rar3", feature = "rar5"))] +pub(crate) mod rar_filters; + #[cfg(feature = "zip_reduce")] pub mod zip_reduce; #[cfg(feature = "zip_shrink")] diff --git a/src/rar3/decoder.rs b/src/rar3/decoder.rs index b04dd0c..268feae 100644 --- a/src/rar3/decoder.rs +++ b/src/rar3/decoder.rs @@ -21,6 +21,10 @@ //! the full match-length / offset machinery. //! - The keep-table flag — successive blocks may reuse the previous code //! lengths. +//! - **In-band standard filters** (main symbol 257): Delta and x86 +//! E8/E8E9 declarations are recognized by their bytecode fingerprint and +//! run natively over their declared output windows — see +//! `super::filters` for the recognition scheme and provenance. //! - The standalone E8/E9 post-pass filter when enabled via //! [`Decoder::with_e8_filter`]. //! @@ -30,9 +34,10 @@ //! ~1500-line context-mixed arithmetic coder; implementing it faithfully //! is out of scope for this build. Streams containing a PPMd block fail //! with `Error::Unsupported`. -//! - **In-band VM filter declarations** (main symbols 257..=261 that emit -//! bytecode for the RarVM interpreter). These also fail with -//! `Error::Unsupported`. The standalone E8/E9 filter remains available. +//! - **Filter declarations carrying any other VM program** (custom +//! bytecode, or legacy standard programs no current archiver emits — +//! Itanium, RGB, the audio predictor). These fail with +//! `Error::Unsupported` rather than interpreting RarVM bytecode. //! - **Dictionary sizes** other than the default 4 MiB. Streams compressed //! with smaller dictionaries decode correctly with the larger window — //! the larger window doesn't change semantics. @@ -45,7 +50,9 @@ use crate::error::Error; use crate::traits::{RawDecoder, RawProgress}; use super::bits::BitReader; -use super::filters::apply_e8_filter; +use super::filters::{ + PendingFilter, StdProgram, apply_e8_filter, apply_pending, recognize_program, +}; use super::huffman::Huffman; use super::tables::{ DICT_DEFAULT_SIZE, HUFF_TABLE_SIZE, LENGTH_BASE, LENGTH_EXTRA_BITS, LENGTH_SIZE, @@ -261,6 +268,9 @@ fn run_decode( }, window_pos: 0, unpack_size, + programs: Vec::new(), + last_filter_slot: 0, + pending_filters: Vec::new(), }); // The decoder starts by parsing the first block header. @@ -268,6 +278,21 @@ fn run_decode( expand(&mut ctx)?; let mut out = core::mem::take(&mut ctx.out); + + // Run the in-band filters over their declared windows, in declaration + // order (well-formed streams declare filters in ascending window order, + // since each start is relative to the output position at declaration). + // A window the stream never finished producing is dropped, matching + // unrar, which only executes a filter once its full block has been + // decoded. + for f in &ctx.pending_filters { + let end = f.start.saturating_add(f.length as u64); + if end > out.len() as u64 { + continue; + } + apply_pending(f, &mut out[f.start as usize..end as usize])?; + } + if e8_enabled { apply_e8_filter(&mut out, 0, e8_translate_e9); } @@ -301,6 +326,22 @@ struct RunCtx { wmask: usize, window_pos: usize, unpack_size: u64, + /// RarVM program slots declared so far (recognized standard programs + /// only) with the per-slot remembered block length — a declaration may + /// omit the length and reuse the slot's previous one. + programs: Vec, + /// Slot used by the most recent declaration; a declaration without an + /// explicit slot field reuses it. + last_filter_slot: usize, + /// Scheduled filter instances, applied over the finished output. + pending_filters: Vec, +} + +/// A declared filter program plus its per-slot remembered block length. +#[derive(Debug, Clone, Copy)] +struct ProgramSlot { + program: StdProgram, + last_block_length: u32, } impl RunCtx { @@ -537,8 +578,10 @@ fn expand(ctx: &mut RunCtx) -> Result<(), Error> { } } 257 => { - // Filter program declaration — refuse. - return Err(Error::Unsupported); + // Filter declaration: a standard-program instance gets + // scheduled over a window of upcoming output; anything we + // can't run natively fails the stream (inside the parser). + read_filter_declaration(ctx)?; } 258 => { // Repeat last (offset, length). @@ -662,6 +705,185 @@ fn expand(ctx: &mut RunCtx) -> Result<(), Error> { } } +// ─── In-band filter declarations (main symbol 257) ────────────────────── + +/// Upper bound on a filter's block length, derived from the RarVM memory +/// the standard programs operate in (0x40000 bytes, of which 0x3C000 lie +/// below the global-data area). Delta needs separate source and +/// destination halves, so its windows are capped at half that. Real +/// encoders stay far below both caps and split large regions into several +/// filter blocks. +const FILTER_MAX_BLOCK: u32 = 0x3C000; +const FILTER_MAX_BLOCK_DELTA: u32 = 0x1E000; + +/// Read a RarVM variable-length number: a 2-bit tag selects a 4-, 8- +/// (with a sign-extension-style escape for values below 16), 16- or 32-bit +/// payload. +fn read_vm_number(bits: &mut BitReader) -> Result { + Ok(match bits.read_bits(2)? { + 0 => bits.read_bits(4)?, + 1 => { + let v = bits.read_bits(8)?; + if v >= 16 { + v + } else { + 0xFFFF_FF00 | (v << 4) | bits.read_bits(4)? + } + } + 2 => bits.read_bits(16)?, + _ => bits.read_bits(32)?, + }) +} + +/// Parse the declaration that follows main symbol 257 and schedule the +/// filter it describes. +/// +/// Wire layout (validated bit-exact against rar 6.24 archives; see the +/// module docs in `filters.rs` for provenance): an 8-bit flags byte and a +/// 1/2/3-byte length field are read from the main bitstream, then `length` +/// payload bytes (8 bits each, unaligned). The payload forms its own +/// MSB-first bit domain containing, in order: +/// +/// 1. flags bit 7: a program-slot number (RarVM number; 0 resets all +/// declared programs and selects slot 0, n>0 selects slot n-1). Absent → +/// reuse the most recent slot. +/// 2. Window start relative to the current output position (RarVM number; +/// flags bit 6 adds 258). +/// 3. flags bit 5: explicit window length (RarVM number). Absent → the +/// slot's remembered length. +/// 4. flags bit 4: a 7-bit register mask followed by a RarVM number per set +/// bit (registers r0..r6; Delta receives its channel count in r0). +/// 5. For a first-use slot: bytecode as a RarVM number length plus that +/// many bytes, the first being an XOR checksum of the rest. +/// 6. flags bit 3: trailing global data — not needed by any standard +/// program, ignored here. +fn read_filter_declaration(ctx: &mut RunCtx) -> Result<(), Error> { + let flags = ctx.bits.read_bits(8)?; + let mut decl_len = (flags & 0x07) + 1; + if decl_len == 7 { + decl_len = ctx.bits.read_bits(8)? + 7; + } else if decl_len == 8 { + decl_len = ctx.bits.read_bits(16)?; + } + if decl_len == 0 { + return Err(Error::Corrupt); + } + let mut payload = vec![0u8; decl_len as usize]; + for b in payload.iter_mut() { + *b = ctx.bits.read_bits(8)? as u8; + } + // The payload is its own bit domain; running out of payload bits means + // the declaration is malformed, not that the caller should feed more + // input, so map UnexpectedEnd to Corrupt. + let mut db = BitReader::new(); + db.feed_slice(&payload); + parse_declaration_payload(ctx, flags, &mut db).map_err(|e| match e { + Error::UnexpectedEnd => Error::Corrupt, + other => other, + }) +} + +fn parse_declaration_payload( + ctx: &mut RunCtx, + flags: u32, + db: &mut BitReader, +) -> Result<(), Error> { + let slot = if flags & 0x80 != 0 { + let v = read_vm_number(db)?; + if v == 0 { + // Full reset: forget all declared programs (and anything + // scheduled against them that hasn't completed). + ctx.programs.clear(); + 0 + } else { + (v - 1) as usize + } + } else { + ctx.last_filter_slot + }; + // A slot may reference an existing program or append exactly one new + // one; skipping ahead is malformed. + if slot > ctx.programs.len() { + return Err(Error::Corrupt); + } + ctx.last_filter_slot = slot; + + let mut start = read_vm_number(db)? as u64; + if flags & 0x40 != 0 { + start += 258; + } + let start = ctx.out.len() as u64 + start; + + let explicit_length = if flags & 0x20 != 0 { + Some(read_vm_number(db)?) + } else { + None + }; + + // Registers r0..r6. Only r0 matters to the standard transforms (Delta's + // channel count), but all present values must be consumed to stay in + // sync with the fields that follow. + let mut r0 = 0u32; + if flags & 0x10 != 0 { + let mask = db.read_bits(7)?; + for r in 0..7 { + if mask & (1 << r) != 0 { + let v = read_vm_number(db)?; + if r == 0 { + r0 = v; + } + } + } + } + + if slot == ctx.programs.len() { + // First use of this slot: bytecode follows. + let code_len = read_vm_number(db)?; + if code_len == 0 || code_len >= 0x1_0000 { + return Err(Error::Corrupt); + } + let mut code = vec![0u8; code_len as usize]; + for b in code.iter_mut() { + *b = db.read_bits(8)? as u8; + } + // The first bytecode byte is an XOR checksum of the rest. + let checksum = code[1..].iter().fold(0u8, |acc, &b| acc ^ b); + if checksum != code[0] { + return Err(Error::Corrupt); + } + let program = recognize_program(&code).ok_or(Error::Unsupported)?; + ctx.programs.push(ProgramSlot { + program, + last_block_length: 0, + }); + } + // (flags bit 3: global data would follow here; no standard program + // reads it, so it stays unparsed — the payload is self-contained.) + + let length = explicit_length.unwrap_or(ctx.programs[slot].last_block_length); + ctx.programs[slot].last_block_length = length; + if length == 0 { + // A zero-length window is a no-op declaration. + return Ok(()); + } + let program = ctx.programs[slot].program; + let cap = if program == StdProgram::Delta { + FILTER_MAX_BLOCK_DELTA + } else { + FILTER_MAX_BLOCK + }; + if length > cap { + return Err(Error::Corrupt); + } + ctx.pending_filters.push(PendingFilter { + start, + length, + program, + channels: r0, + }); + Ok(()) +} + /// Promote the offset at `idx` (in the rolling buffer) to position 0, /// shifting everything above it down. Used by symbols 259..=262. fn promote_offset(ctx: &mut RunCtx, idx: usize, offs: u32) { @@ -681,6 +903,179 @@ mod tests { extern crate std; use std::vec; + /// MSB-first bit writer used to hand-build declaration payloads. + struct BitWriter { + bytes: std::vec::Vec, + nbits: u32, + } + impl BitWriter { + fn new() -> Self { + Self { + bytes: std::vec::Vec::new(), + nbits: 0, + } + } + fn push(&mut self, value: u32, n: u32) { + for i in (0..n).rev() { + let bit = ((value >> i) & 1) as u8; + if self.nbits.is_multiple_of(8) { + self.bytes.push(0); + } + let last = self.bytes.len() - 1; + self.bytes[last] |= bit << (7 - (self.nbits % 8)); + self.nbits += 1; + } + } + /// Push a value in RarVM variable-number encoding (shortest form). + fn push_vm_number(&mut self, v: u32) { + if v < 16 { + self.push(0, 2); + self.push(v, 4); + } else if v < 256 { + self.push(1, 2); + self.push(v, 8); + } else if v < 0x1_0000 { + self.push(2, 2); + self.push(v, 16); + } else { + self.push(3, 2); + self.push(v, 32); + } + } + } + + fn test_ctx() -> RunCtx { + RunCtx { + bits: BitReader::new(), + lengths: vec![], + main: None, + offset: None, + low_offset: None, + length: None, + old_offsets: [1, 1, 1, 1], + last_offset: 0, + last_length: 0, + last_low_offset: 0, + num_low_offset_repeats: 0, + out: vec![], + window: vec![0u8; 16], + wmask: 15, + window_pos: 0, + unpack_size: 0, + programs: vec![], + last_filter_slot: 0, + pending_filters: vec![], + } + } + + #[test] + fn vm_number_all_tag_widths() { + let mut w = BitWriter::new(); + w.push_vm_number(9); // tag 0, 4-bit + w.push_vm_number(200); // tag 1, 8-bit (>= 16) + w.push_vm_number(0x1234); // tag 2, 16-bit + w.push_vm_number(0x0102_0304); // tag 3, 32-bit + // tag 1 with an 8-bit value below 16: extends to 0xFFFFFF00-form. + w.push(1, 2); + w.push(5, 8); + w.push(0xA, 4); + let mut r = BitReader::new(); + r.feed_slice(&w.bytes); + assert_eq!(read_vm_number(&mut r).unwrap(), 9); + assert_eq!(read_vm_number(&mut r).unwrap(), 200); + assert_eq!(read_vm_number(&mut r).unwrap(), 0x1234); + assert_eq!(read_vm_number(&mut r).unwrap(), 0x0102_0304); + assert_eq!(read_vm_number(&mut r).unwrap(), 0xFFFF_FF5A); + } + + /// Build the payload of a declaration introducing a fresh program. + /// `code` must carry a valid XOR checksum byte already. + fn new_program_payload(block_start: u32, block_len: u32, code: &[u8]) -> std::vec::Vec { + let mut w = BitWriter::new(); + w.push_vm_number(0); // slot field: reset-all + slot 0 + w.push_vm_number(block_start); + w.push_vm_number(block_len); + w.push_vm_number(code.len() as u32); + for &b in code { + w.push(b as u32, 8); + } + w.bytes + } + + /// Wrap arbitrary bytecode with its XOR checksum byte. + fn with_checksum(body: &[u8]) -> std::vec::Vec { + let mut code = std::vec::Vec::with_capacity(body.len() + 1); + code.push(body.iter().fold(0u8, |a, &b| a ^ b)); + code.extend_from_slice(body); + code + } + + #[test] + fn unknown_program_is_unsupported() { + // Valid declaration framing around bytecode we don't recognize + // (flags: slot present + explicit length = 0xA0). + let code = with_checksum(&[0x12, 0x34, 0x56, 0x78]); + let payload = new_program_payload(0, 64, &code); + let mut ctx = test_ctx(); + let mut db = BitReader::new(); + db.feed_slice(&payload); + assert_eq!( + parse_declaration_payload(&mut ctx, 0xA0, &mut db), + Err(Error::Unsupported) + ); + } + + #[test] + fn bad_bytecode_checksum_is_corrupt() { + let mut code = with_checksum(&[0x12, 0x34]); + code[0] ^= 0xFF; // break the checksum + let payload = new_program_payload(0, 64, &code); + let mut ctx = test_ctx(); + let mut db = BitReader::new(); + db.feed_slice(&payload); + assert_eq!( + parse_declaration_payload(&mut ctx, 0xA0, &mut db), + Err(Error::Corrupt) + ); + } + + #[test] + fn slot_skipping_ahead_is_corrupt() { + // Slot field 3 => slot index 2 with no programs declared. + let mut w = BitWriter::new(); + w.push_vm_number(3); + let mut ctx = test_ctx(); + let mut db = BitReader::new(); + db.feed_slice(&w.bytes); + assert_eq!( + parse_declaration_payload(&mut ctx, 0x80, &mut db), + Err(Error::Corrupt) + ); + } + + #[test] + fn truncated_main_stream_mid_declaration_is_unexpected_end() { + // flags byte 0x86 declares (6&7)+1 = 7 → an extra 8-bit length + // field must follow, but the stream ends first. + let mut ctx = test_ctx(); + ctx.bits.feed_slice(&[0x86]); + assert!(matches!( + read_filter_declaration(&mut ctx), + Err(Error::UnexpectedEnd) + )); + } + + #[test] + fn payload_bits_running_out_is_corrupt() { + // flags 0xA0, decl_len 1, payload [0xFF]: the slot field's 2-bit + // tag reads 0b11 → a 32-bit number that the 1-byte payload can't + // hold. Inside the payload's own bit domain that's a malformed + // declaration, so the wrapper maps it to Corrupt. + let mut ctx = test_ctx(); + ctx.bits.feed_slice(&[0xA0, 0xFF]); + assert_eq!(read_filter_declaration(&mut ctx), Err(Error::Corrupt)); + } + #[test] fn unpack_size_zero_is_immediate_done() { let mut dec = Decoder::with_unpack_size(0); @@ -710,6 +1105,9 @@ mod tests { wmask: 15, window_pos: 0, unpack_size: 0, + programs: vec![], + last_filter_slot: 0, + pending_filters: vec![], }; // Promote slot 2 (value 30) — result should be [30, 10, 20, 40]. promote_offset(&mut ctx, 2, 30); diff --git a/src/rar3/filters.rs b/src/rar3/filters.rs index d04f72d..21cf954 100644 --- a/src/rar3/filters.rs +++ b/src/rar3/filters.rs @@ -1,21 +1,102 @@ //! RAR 3.x post-decompression filters. //! -//! RAR3 supports a small set of "VM filters" that the encoder embeds as -//! bytecode programs in symbol 257 of the main code. The decoder is then -//! supposed to instantiate an interpreter for a stack-based RISC-like VM -//! ("RarVM"). Faithfully implementing RarVM is a large effort (the -//! upstream interpreter is several hundred lines plus a full instruction -//! set) and is out of scope for this build. +//! ## In-band standard filters (main symbol 257) //! -//! What we do support is the **stand-alone Intel E8/E9 x86 call translation -//! filter** which can be activated through an external selector -//! ([`Decoder::with_e8_filter`]). This filter is what the vast majority of -//! RAR3 streams over x86 executables actually use, and the operation is -//! the same as the LZX intel-call-translation post-pass. +//! RAR3 embeds filters as bytecode programs for a stack-based RISC-like VM +//! ("RarVM") in symbol 257 of the main code. We do **not** interpret +//! arbitrary programs: WinRAR's compressor only ever emits a fixed set of +//! standard programs, so — like libarchive and unrar in practice — we +//! recognize the standard programs (by bytecode length + CRC-32, +//! [`recognize_program`]) and run native transforms: //! -//! Future versions of this module may grow Itanium, RGB delta and audio -//! delta filters if there's demand; for now any in-band filter declaration -//! is refused with `Error::Unsupported`. +//! - **Delta** (channel de-interleave; emitted for bitmaps, WAV audio and +//! other channel-interleaved content; channel count arrives in VM +//! register 0), +//! - **x86 E8** and **E8/E9** call(-jump) translation. +//! +//! The transforms themselves live in [`crate::rar_filters`], shared with +//! the RAR5 decoder, and were validated byte-for-byte against WinRAR +//! archives (UnRAR 7.23 ground truth). Streams declaring any other program +//! (custom bytecode, or the legacy Itanium/RGB/audio-predictor standard +//! programs, which current archivers no longer emit) fail with +//! `Error::Unsupported` — never wrong bytes. +//! +//! ## Stand-alone E8/E9 pass +//! +//! Separately, the **stand-alone Intel E8/E9 call translation pass** +//! ([`apply_e8_filter`]) can be activated by the caller via +//! `Decoder::with_e8_filter`. It predates in-band filter support and is an +//! LZX-style whole-output transform, *not* the same arithmetic as the +//! in-band x86 filter; it is kept for callers that relied on it. + +use crate::checksum::Crc32; +use crate::error::Error; +use crate::rar_filters::{delta_decode, x86_e8_decode}; + +/// A standard RarVM filter program we recognize and can run natively. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum StdProgram { + /// Channel de-interleave; channel count comes from VM register 0. + Delta, + /// x86 `0xE8` (CALL) relative-address restore. + X86Call, + /// x86 `0xE8`/`0xE9` (CALL/JMP) relative-address restore. + X86CallJmp, +} + +/// Identify a standard filter program from its bytecode. +/// +/// WinRAR's compressor emits each standard filter as a fixed byte string, +/// so `(length, CRC-32)` is a stable fingerprint — the same recognition +/// scheme libarchive uses. The Delta and x86-E8 fingerprints below were +/// computed from programs extracted out of real rar 6.24 archives (this +/// crate's differential corpus); the E8E9 fingerprint (emitted by older +/// WinRAR 3.x builds) matches the value documented in libarchive +/// (BSD-licensed `archive_read_support_format_rar.c`). +/// +/// Returns `None` for anything else — including the legacy Itanium / RGB / +/// audio-predictor standard programs, which no current archiver emits. +pub(super) fn recognize_program(code: &[u8]) -> Option { + let mut crc = Crc32::new(); + crc.update(code); + match (code.len(), crc.finalize()) { + (29, 0x0E06_077D) => Some(StdProgram::Delta), + (53, 0xAD57_6887) => Some(StdProgram::X86Call), + (57, 0x3CD7_E57E) => Some(StdProgram::X86CallJmp), + _ => None, + } +} + +/// A parsed, scheduled instance of a standard filter: it rewrites the +/// window `[start, start + length)` of the unpacked stream. +#[derive(Debug, Clone, Copy)] +pub(super) struct PendingFilter { + /// Absolute byte offset in the unpacked stream. + pub start: u64, + pub length: u32, + pub program: StdProgram, + /// VM register 0 at declaration time — the Delta channel count. + pub channels: u32, +} + +/// Run a scheduled filter over its region (already sliced by the caller). +pub(super) fn apply_pending(filter: &PendingFilter, region: &mut [u8]) -> Result<(), Error> { + match filter.program { + StdProgram::Delta => { + if filter.channels == 0 || filter.channels as usize > region.len() { + // Channel count is supplied by the stream (register 0); + // 0 channels is meaningless and more channels than bytes + // means most planes are empty — real encoders produce + // neither. + return Err(Error::Corrupt); + } + delta_decode(filter.channels as usize, region); + } + StdProgram::X86Call => x86_e8_decode(filter.start, region, false), + StdProgram::X86CallJmp => x86_e8_decode(filter.start, region, true), + } + Ok(()) +} /// Apply the E8/E9 (x86 near-call) translation filter to `data` in place. /// diff --git a/src/rar3/mod.rs b/src/rar3/mod.rs index e0b6244..9cd22a4 100644 --- a/src/rar3/mod.rs +++ b/src/rar3/mod.rs @@ -18,11 +18,13 @@ //! 2. **PPMd-II** — an Order-N context-mixed arithmetic coder. Used by some //! text-heavy archives and `-m5` (best compression) runs. //! -//! This build implements the **LZ77 + Huffman path** in full. PPMd-II blocks -//! are refused with `Error::Unsupported` — see the private `decoder` submodule for details and -//! limitations. The standalone E8/E9 (x86 near-call) post-pass filter can -//! be enabled via [`Decoder::with_e8_filter`]; the in-band RarVM filter -//! mechanism (main symbols 257..=261) is refused. +//! This build implements the **LZ77 + Huffman path** in full, including the +//! in-band standard filters WinRAR declares via main symbol 257 (Delta and +//! x86 E8/E8E9, recognized by bytecode fingerprint and run natively — no +//! RarVM interpreter; unknown programs are refused). PPMd-II blocks are +//! refused with `Error::Unsupported` — see the private `decoder` submodule +//! for details and limitations. The standalone E8/E9 (x86 near-call) +//! post-pass filter can also be enabled via [`Decoder::with_e8_filter`]. //! //! ## Calling convention //! diff --git a/src/rar5/decoder.rs b/src/rar5/decoder.rs index af8a7ea..ce89133 100644 --- a/src/rar5/decoder.rs +++ b/src/rar5/decoder.rs @@ -48,6 +48,11 @@ //! container framing themselves (header blocks, file headers, multi-volume //! continuations, etc.) and hand the inner compressed-data run to this //! decoder. +//! +//! When the run is a **solid group** (several files sharing one LZ +//! stream), the container should also register each member's starting +//! offset via [`Decoder::add_file_boundary`] so the x86 filters can +//! compute file-relative addresses, as unrar does. use alloc::boxed::Box; use alloc::collections::VecDeque; @@ -107,6 +112,12 @@ pub struct Decoder { /// Absolute offset (in the unpacked stream) of the first byte still in /// `out_queue`. Used to identify which pending filters fire when. out_queue_start: u64, + /// Sorted starting offsets (in the unpacked stream) of the files a + /// solid group concatenates, registered via + /// [`Decoder::add_file_boundary`]. Position-dependent filters (x86) + /// compute their addresses relative to the containing file, matching + /// unrar/libarchive. An implicit boundary at 0 always exists. + file_boundaries: Vec, } #[derive(Debug)] @@ -171,6 +182,39 @@ impl Decoder { pending_filters: Vec::new(), ready: VecDeque::new(), out_queue_start: 0, + file_boundaries: Vec::new(), + } + } + + /// Declare that a new file starts at absolute unpacked-stream offset + /// `pos` — needed when this decoder runs over a **solid group** (one + /// continuous LZ stream concatenating several files). + /// + /// The x86 E8/E8E9 filters rewrite call targets relative to the start + /// of the *containing file*, not the solid stream (unrar resets its + /// position base per extracted file; libarchive tracks the same value + /// as `solid_offset`). A container driving a solid group should + /// register each member's starting offset (the cumulative unpacked + /// sizes of the members before it) before decoding; boundaries may be + /// registered ahead of time and in any order. Without registrations + /// every filter is file-relative to offset 0, which is correct for the + /// single-file (non-solid) case. + /// + /// Like the unpack size, boundaries are treated as stream-shape + /// configuration: they survive [`reset`](crate::Decoder::reset). + pub fn add_file_boundary(&mut self, pos: u64) { + if let Err(idx) = self.file_boundaries.binary_search(&pos) { + self.file_boundaries.insert(idx, pos); + } + } + + /// Starting offset of the file containing unpacked-stream position + /// `pos` (0 when no boundary at or before `pos` is registered). + fn file_start_for(&self, pos: u64) -> u64 { + match self.file_boundaries.binary_search(&pos) { + Ok(_) => pos, + Err(0) => 0, + Err(idx) => self.file_boundaries[idx - 1], } } @@ -639,7 +683,16 @@ impl Decoder { // Propagate filter failures instead of silently emitting // the raw, unfiltered bytes. An unsupported or corrupt // filter would otherwise yield wrong output with no error. - super::filters::apply(&f, &mut region)?; + // + // The transform's position base is relative to the + // *containing file*, not the solid stream (see + // `add_file_boundary`); identical to `f.start` in the + // single-file case. + let file_rel = super::filters::Filter { + start: f.start - self.file_start_for(f.start), + ..f + }; + super::filters::apply(&file_rel, &mut region)?; for &b in ®ion { self.ready.push_back(b); } diff --git a/src/rar5/filters.rs b/src/rar5/filters.rs index e001b43..602f7bb 100644 --- a/src/rar5/filters.rs +++ b/src/rar5/filters.rs @@ -7,7 +7,7 @@ //! //! ## Filter types //! -//! - `0` — Delta. RGB pre-processing (channel deinterleaving). +//! - `0` — Delta. Channel de-interleave (bitmap/audio pre-processing). //! - `1` — x86 E8 call-translation. Rewrites the 4-byte relative target of //! every `0xE8` opcode. //! - `2` — x86 E8/E9 call+jump-translation. Same as `1` but also fires on @@ -16,9 +16,10 @@ //! - `4..=7` — Audio, RGB, Itanium, PPM. Not used in any RAR5 stream we have //! seen in the wild; treated as `Unsupported`. //! -//! This crate implements filters `1` and `2` (the most common) and rejects -//! the rest with `Error::Unsupported`. Adding more filters means extending -//! the dispatch in [`apply`]. +//! This crate implements filters `0`, `1` and `2` and rejects ARM and the +//! rest with `Error::Unsupported`. Adding more filters means extending the +//! dispatch in [`apply`]. The byte transforms themselves live in +//! [`crate::rar_filters`], shared with the RAR3 decoder. //! //! ## Activation //! @@ -29,10 +30,11 @@ //! block_length)`. use crate::error::Error; +use crate::rar_filters::{delta_decode, x86_e8_decode}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum FilterKind { - /// 0 — RGB delta. `channels` is the channel count (1..=32). + /// 0 — Delta. `channels` is the channel count (1..=32). Delta { channels: u8 }, /// 1 — x86 `0xE8` (CALL) relative-address rewrite. X86Call, @@ -60,77 +62,30 @@ pub fn apply(filter: &Filter, buf: &mut [u8]) -> Result<(), Error> { if (buf.len() as u64) < filter.length as u64 { return Err(Error::Corrupt); } + let region = &mut buf[..filter.length as usize]; match filter.kind { - FilterKind::X86Call => apply_e8(filter.start, &mut buf[..filter.length as usize], false), - FilterKind::X86CallJmp => apply_e8(filter.start, &mut buf[..filter.length as usize], true), - // Filters we recognise on the wire but do not implement. Rejecting - // these is honest: the decoder is decoder-only and we can either - // surface "unsupported" up to the caller (so they can fall back to - // the official `unrar`) or silently mangle the stream. We pick - // honesty. - FilterKind::Delta { .. } => Err(Error::Unsupported), - FilterKind::Arm => Err(Error::Unsupported), - } -} - -/// RAR5 x86 call/jump filter. Operates on a 16 MiB virtual file-size window; -/// the relative target of each opcode is normalised so that the *absolute* -/// target is encoded instead, which compresses better. -/// -/// `start` is the absolute position of `buf[0]` in the unpacked stream. -/// When `extended` is true the filter fires on `0xE8` *and* `0xE9`; when -/// false it only fires on `0xE8`. The transform is its own inverse. -fn apply_e8(start: u64, buf: &mut [u8], extended: bool) -> Result<(), Error> { - const FILE_SIZE: u32 = 0x0100_0000; - if buf.len() < 5 { - // No room for a [opcode][4-byte rel] sequence. - return Ok(()); - } - let last = buf.len() - 4; - let mut i = 0; - while i < last { - let b = buf[i]; - let matches = b == 0xE8 || (extended && b == 0xE9); - if !matches { - i += 1; - continue; + FilterKind::X86Call => { + x86_e8_decode(filter.start, region, false); + Ok(()) + } + FilterKind::X86CallJmp => { + x86_e8_decode(filter.start, region, true); + Ok(()) } - // 4-byte little-endian relative target sitting at buf[i+1..i+5]. - let rel = u32::from_le_bytes([buf[i + 1], buf[i + 2], buf[i + 3], buf[i + 4]]); - // Libarchive: the offset is computed *after* the opcode byte has - // been consumed, so the relevant absolute position is start + i + 1. - let off = ((start + i as u64 + 1) as u32) & (FILE_SIZE - 1); - // RAR5 transform, decode direction. The two range checks are - // NESTED on the sign of `rel`, exactly as in unrar/libarchive: - // - // if (addr < 0) { if (addr + off >= 0) addr += FILE_SIZE; } - // else { if (addr < FILE_SIZE) addr -= off; } - // - // Flattening the second check into an `else if` is a bug: a - // negative `rel` that stays negative after adding `off` (a byte - // pattern the encoder never rewrote — e.g. a stray 0xE8 inside a - // ModRM/displacement sequence followed by high bytes) also passes - // `(rel - FILE_SIZE) & 0x8000_0000 != 0` and would be wrongly - // rewritten, corrupting real x86 code on decode. - let new = if (rel & 0x8000_0000) != 0 { - if (rel.wrapping_add(off) & 0x8000_0000) == 0 { - rel.wrapping_add(FILE_SIZE) - } else { - rel + FilterKind::Delta { channels } => { + if channels == 0 { + // The wire format encodes channels-1 in 5 bits, so 0 can't + // be parsed off the stream; guard against caller misuse. + return Err(Error::Corrupt); } - } else if (rel.wrapping_sub(FILE_SIZE) & 0x8000_0000) != 0 { - rel.wrapping_sub(off) - } else { - rel - }; - let nb = new.to_le_bytes(); - buf[i + 1] = nb[0]; - buf[i + 2] = nb[1]; - buf[i + 3] = nb[2]; - buf[i + 4] = nb[3]; - i += 5; + delta_decode(channels as usize, region); + Ok(()) + } + // Recognised on the wire but not implemented. Rejecting is honest: + // the caller can fall back to the official unrar instead of us + // silently mangling the stream. + FilterKind::Arm => Err(Error::Unsupported), } - Ok(()) } #[cfg(test)] @@ -235,14 +190,22 @@ mod tests { } #[test] - fn delta_and_arm_return_unsupported() { - let mut buf = alloc::vec![0; 16]; + fn delta_three_channels_reinterleaves() { + // Planar deltas: ch0=[1,1], ch1=[2,2], ch2=[3,3] over a 6-byte + // region. Decode integrates each channel with prev - delta. + let mut buf = alloc::vec![1u8, 1, 2, 2, 3, 3]; let f = Filter { start: 0, length: buf.len() as u32, kind: FilterKind::Delta { channels: 3 }, }; - assert_eq!(apply(&f, &mut buf), Err(Error::Unsupported)); + apply(&f, &mut buf).unwrap(); + assert_eq!(buf, [0xFF, 0xFE, 0xFD, 0xFE, 0xFC, 0xFA]); + } + + #[test] + fn arm_returns_unsupported() { + let mut buf = alloc::vec![0; 16]; let f = Filter { start: 0, length: buf.len() as u32, diff --git a/src/rar_filters.rs b/src/rar_filters.rs new file mode 100644 index 0000000..6693978 --- /dev/null +++ b/src/rar_filters.rs @@ -0,0 +1,166 @@ +//! Byte transforms for RAR's *standard* post-decompression filters, shared +//! by the RAR3 (in-band RarVM programs, main symbol 257) and RAR5 (filter +//! descriptors after main symbol 256) decoders. +//! +//! RAR compressors optionally pre-process content before LZ compression so +//! it compresses better (relative call targets in executables, interleaved +//! channel data in bitmaps/audio). The decoder undoes the transform over a +//! declared `(start, length)` window of the unpacked stream after +//! decompressing it. Both container generations use the same two transforms +//! implemented here: +//! +//! - **x86 E8 / E8E9** call(/jump) translation, [`x86_e8_decode`]. +//! - **Delta** channel de-interleave + integrate, [`delta_decode`]. +//! +//! ## Provenance +//! +//! Semantics derived from public format documentation and validated +//! byte-for-byte against WinRAR-produced archives (differential harness vs +//! `UnRAR.exe` 7.23: RAR4 delta ch=2/3/12 and E8 windows, RAR5 E8/E8E9). +//! Structure cross-checked against libarchive (BSD); no code copied from +//! RARLAB's unRAR or The Unarchiver. + +use alloc::vec::Vec; + +/// x86 call/jump filter, decode direction. Operates on a 16 MiB virtual +/// file-size window; the encoder rewrote each `E8` (and, for the extended +/// variant, `E9`) opcode's 4-byte relative target into absolute form, and +/// this pass restores the original relative value. +/// +/// `start` is the absolute position of `buf[0]` in the unpacked stream. +/// When `also_e9` is true the filter fires on `0xE8` *and* `0xE9`; when +/// false only on `0xE8`. +pub(crate) fn x86_e8_decode(start: u64, buf: &mut [u8], also_e9: bool) { + const FILE_SIZE: u32 = 0x0100_0000; + if buf.len() < 5 { + // No room for a [opcode][4-byte rel] sequence. + return; + } + let last = buf.len() - 4; + let mut i = 0; + while i < last { + let b = buf[i]; + let matches = b == 0xE8 || (also_e9 && b == 0xE9); + if !matches { + i += 1; + continue; + } + // 4-byte little-endian relative target sitting at buf[i+1..i+5]. + let rel = u32::from_le_bytes([buf[i + 1], buf[i + 2], buf[i + 3], buf[i + 4]]); + // The offset is computed *after* the opcode byte has been consumed, + // so the relevant absolute position is start + i + 1. + let off = ((start + i as u64 + 1) as u32) & (FILE_SIZE - 1); + // Decode direction. The two range checks are NESTED on the sign of + // `rel`, exactly as in unrar/libarchive: + // + // if (addr < 0) { if (addr + off >= 0) addr += FILE_SIZE; } + // else { if (addr < FILE_SIZE) addr -= off; } + // + // Flattening the second check into an `else if` is a bug: a + // negative `rel` that stays negative after adding `off` (a byte + // pattern the encoder never rewrote — e.g. a stray 0xE8 inside a + // ModRM/displacement sequence followed by high bytes) also passes + // `(rel - FILE_SIZE) & 0x8000_0000 != 0` and would be wrongly + // rewritten, corrupting real x86 code on decode. + let new = if (rel & 0x8000_0000) != 0 { + if (rel.wrapping_add(off) & 0x8000_0000) == 0 { + rel.wrapping_add(FILE_SIZE) + } else { + rel + } + } else if (rel.wrapping_sub(FILE_SIZE) & 0x8000_0000) != 0 { + rel.wrapping_sub(off) + } else { + rel + }; + let nb = new.to_le_bytes(); + buf[i + 1] = nb[0]; + buf[i + 2] = nb[1]; + buf[i + 3] = nb[2]; + buf[i + 4] = nb[3]; + i += 5; + } +} + +/// Delta filter, decode direction. The encoder de-interleaved the region +/// into `channels` planes of successive byte differences; this pass +/// re-interleaves and integrates them: each output byte is the previous +/// output byte of the same channel *minus* the next source byte. +/// +/// `channels == 0` is a caller error and leaves the buffer untouched +/// (callers validate the channel count when parsing the declaration). +pub(crate) fn delta_decode(channels: usize, data: &mut [u8]) { + debug_assert!(channels >= 1); + if channels == 0 || data.is_empty() { + return; + } + // The source (planar deltas) is consumed sequentially while the + // destination is written strided, so a scratch copy of the source is + // needed. + let src: Vec = data.to_vec(); + let mut sp = 0usize; + for ch in 0..channels { + let mut prev = 0u8; + let mut i = ch; + while i < data.len() { + prev = prev.wrapping_sub(src[sp]); + data[i] = prev; + sp += 1; + i += channels; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + extern crate std; + use std::vec; + + #[test] + fn delta_two_channels_hand_computed() { + // Two channels, planar source: channel 0 deltas [1, 2], channel 1 + // deltas [3, 4]. Integration is prev - delta starting from 0. + // ch0: 0-1=0xFF, 0xFF-2=0xFD ; ch1: 0-3=0xFD, 0xFD-4=0xF9. + let mut data = vec![1u8, 2, 3, 4]; + delta_decode(2, &mut data); + assert_eq!(data, [0xFF, 0xFD, 0xFD, 0xF9]); + } + + #[test] + fn delta_single_channel_is_running_negated_sum() { + let mut data = vec![0u8, 0xFF, 0xFF]; + delta_decode(1, &mut data); + assert_eq!(data, [0, 1, 2]); + } + + #[test] + fn delta_length_not_divisible_by_channels() { + // 5 bytes, 2 channels: channel 0 covers indices 0,2,4 (3 source + // bytes), channel 1 covers 1,3 (2 source bytes) — source is planar + // in that order. + let mut data = vec![1u8, 1, 1, 2, 2]; + delta_decode(2, &mut data); + // ch0 deltas [1,1,1] -> FF,FE,FD at 0,2,4; ch1 deltas [2,2] -> FE,FC. + assert_eq!(data, [0xFF, 0xFE, 0xFE, 0xFC, 0xFD]); + } + + #[test] + fn e8_rewrites_call_target() { + let mut buf = vec![0x00, 0x00, 0xE8, 0x10, 0x00, 0x00, 0x00, 0x90, 0x90]; + x86_e8_decode(0, &mut buf, false); + // off = 2 + 1 = 3; rel = 0x10 in 0..FILE_SIZE => rel - off. + let expected = 0x10u32.wrapping_sub(3).to_le_bytes(); + assert_eq!(&buf[3..7], &expected); + } + + #[test] + fn e8_ignores_e9_unless_extended() { + let mut buf = vec![0xE9, 0x10, 0x00, 0x00, 0x00]; + let orig = buf.clone(); + x86_e8_decode(0, &mut buf, false); + assert_eq!(buf, orig); + x86_e8_decode(0, &mut buf, true); + assert_ne!(buf, orig); + } +} diff --git a/tests/fixtures/rar3/filter_delta_gradient_bmp.bin b/tests/fixtures/rar3/filter_delta_gradient_bmp.bin new file mode 100644 index 0000000000000000000000000000000000000000..3cd8c1052e62a0cba6a48990ddf888fd17f7dd34 GIT binary patch literal 395 zcmZSB-w}W7<^SD@7Z^4$FtqheZ#b)PpiFp)c*}0HmtPguu31#Vy{=%J$1-*K-3=@p zR~-cQ@%&~llao0pvy^R#=M!mb{y&S-Kl9GB{`vUlc^m(@-=%;4{`B9Y_r3Mrhu!zY zs}H@eh<(q!_hC7@(EbeqXBl|@@yz?e>=YvI9U|@&AZk_of#v=IVg3XW=0q1}hpSD0 zzx@BahvD(<7oXR8oPBm`-Myq!dSqg&2A+!y0{a9Sd6ZldHi;zF_&zlDw0>THg}vgA z!xb@&oatxX;_AQOw@I(Nx3A)#)$SYb%|gpQiv^O4f3b)#az6~T{_j4?Q{{ZyXVd5P j7Q!diwSF@`xUcU={SyIKml?iC6~bIs#2qt8xw!-YhiJw} literal 0 HcmV?d00001 diff --git a/tests/fixtures/rar3/filter_delta_ramp_wav.bin b/tests/fixtures/rar3/filter_delta_ramp_wav.bin new file mode 100644 index 0000000000000000000000000000000000000000..478b3e3e9dbf8aa2ada42507bfc9756ff7de6259 GIT binary patch literal 320 zcmZSB-w}W7<^SD@7Z@fmFtqedZ#b)PpiFp)c*}0HmtPguu31#Vy{=%J$1-*K-3=_f zqJi8`PIsT;GG%d8P-9VOc=A$_#bW*~t0D%@_lxVAj3p%c&uu*KXR!6#>09M%v!DLl z_GQWc2O2k8e`ws83dC6Ge+|4LSG}L~O_*@lA+lBAfUm@1HX(_C%ls;w`6ndgFFt9` z4Si*G@9~`j6|Y{+uR53R8it<;#O?mdCqJDy4%PhiTUnQy)N%W(&3p~N1p)#L zr=N7H2spAJqYO2-rj!h|ruy$0ZcW+0SDbAA{P^xBv>FIs3cAZ#-b4n z#RNc+$t{?8<)z?lFSUNBw%6MG^>4Ji4Ph3ngjv9X5h{cMsttHDI5mjcaJ(7!-fIAv z!#mde%#$ENx9iuxkKKEnbu-UA-v+m>>w4a|t{*aglh^Q0yYIbm?|aSpnL5wPXa8x8 zzyg2(O=ZTwN&&&90ssI2G){yCWV;kH=149{dN|? z4S9oDx zo4W6rXgjSYlS<*1e?-en=Sa)En^fO9?uPl)55%l>{$>h!h0 zpQMf6<^h!a8LuZ=jkemLaUHra8WxSGIr+J{H zFr0Djg|Q6}*kG&eZ+}kSJsrBNQmgXJ2SlowWEm>916JWj)T)F@TBzOQ`ueD?$Zd-B zQ}@^V;;4P1u7X%{9#vdx1g+zzE!`4%IG6nAiEM5QP5`;m+;ZIHg@a!a^}@d2+=%Te zu5a^S_;~@kvbCanhM& z3vw=Ye0G~d-8k-uS%1NakPgd}5H1aEo;gc25%rJhYJm1@d3!x>I+g>Vh0?7?2&#vZ zza;m9(4oo1zmpWD$4#fxXa{-=9>3R4;sojK7Z%F zCYgTU{(ItSm+k-Oz9yM|-r6_G^u3n!{2HM`-6j77(%Xt_(r)dny(}G3E^Y-IOuGlDZt|i!zYYvnI(nNhE#k({y-`1aIF5ZmvSRv)KO{S|r z18PBpGqsnQg)|?K5-n87%3!ce86J~RU-L<>KhYpbs2Io+Qz7}@M8?xSfjrhcqM}}z z6gM5GF{ai;N0pKH)~91Tzg@|DMhVu+-D+;Yjaqh{IRSOon9{!NS@BfDF2&u1hM~v! z-PZ-YS@)M#+M72WydRjCrAufEZY|dpTbxU3iq{MpG5!^=>t8RcR2z_Xu6^9}uPe(- zK3eM%Ft(Er^9+ojiQrG?!9)gqs6N?=IFiV{so)*G<^j;YvrtP@XIZH2IBQU0hRi%qN z`RjxLvJ%9~W*0M-O?=+deQwGk`Y9uz;RV!f4R$T4|;gVcv0%yA7>UItJe zlh2U0wPAO-xgUce%pRUYHuoI0RMzAH`^SY?%lOfyLb6bGW16kyR$|!mJZm;v&f^1VX|4$+MRRO=xQvth@c0Lg^1uZ#hf^N{A)~uU@ z%V8p?dD|0l%Ui1c+pZ@2stzf2i1E%7=TyXWkRAR=nUOtp0XGXKo?ekW!j*oXc1?ih zF*pKJ&WLM!+u?d)e(;r%c=<)r*p*Rq;}WDUl6}z@<4WI+LyW5)oPpXgr4?p5q=Aijv zmBplw#(8mZWwSIBJ)-Ex)`gJ-te)u4zc!t#-@h+-iu{TYn+?fQRer@y86wjUF?y2P{{yA?`7lQuYW!%7i-`d*>@HDIU^JPc3jDG zMql!UM>NVvuVIV2#1McP+zC^6dC7Jep=FsyQF}s+p2nl69xUT*R zEGw~-CwWP`2g~99$;Jw5w%xlQ*(tCJIXV8g^$g==t@jc@QyRM^V88yc>Ak^A-5nd1 z(A@h4AHq&*%^lCa7=1tzM`(1}Jue`tOL*tI24~>?c;bvkx6Wn3eZ>mbvfUZcjGG7a zg=ZMyg3Vm#rC;P3nL4t5d}Mr`LO6@#j)&#PCt5DiXk1AGA?&qzZ~~09`c>6*9lsz^ z<2aGA4R^UK0x6sxzAPKmK~X-vz)~KH)zMr%V2_x($K-{K}|1^ z-G8Q^<$k13FjDi=p0kN$yBqxc<#w9-mtMhbbQ4>qU@fx`y|8SRJCzBGuL@mOuad8z zv)>0*h%VC6iHve*uHGP$!@wYS>W%Z6CjVy#!V5GhmG3sXY@DbdDxt~3>WGrx(V+up zr_wnHyj%HRPyT=^=3$~TPk*pG*THz-y1n{I0~1Id2%=*f?iP3+y>zA&E@d8;@0I`dcAEvymyNZdZ}h<;X8 zl53S#phIr|81zxcy_&{s4+PnS4$QWshcd_q+aq6%qx0HgcFvSvvqaOr>Ou=lnWy zH5~~&!5<=Io?xSPFGkHTQuIL=G5hi@?e=qiv1jPut>ckN?i@d~6+K!PZhAKfSrwItuT zl+*MtCzt_bw(zGl7(~bwsR>aPtO-(MbBYuxH#PKsDzbP~g|R15q9X3%rC*6?r5txYJg^#|Vko*Jc8^XI9E zwh1(#w{Km)WZr0Z^0)$OxxUT%J`4*MJSML2@x6NERv#9%1mcCi63}^l1cw*5O-fa( zyJ!+>w(lp^oKo1{K*2C()oc7`SFDe@oszrVT>YY@SF*8k4PaZLW6p>@ta#A;kQ&g%r-5TN=m!b)3p~3P4 zG1oAy8PBBE5K7b?Am8u^8a7@78y$h9T*$0EC7c!4ksQ5BI{B5jVDY-t5vYxL^7f0+ z;55occI>mL;-JNlJ+*n)(kLY#j6(ZeT6{Q=XBE|n6#6zTfU|jUp>6v9hwtPJ9P{pP z!;QCXnw-k8bJMrbH_1gR$~Q3Vir6<>GgJcs;9uZ_kRxN`N97Vhfq6+oCC~AiPYOB9 z-7UU2Z^H7fH9Y2nPv39c;oWMbmpQ^Emt^zA-|_T*`>ra_0|!sXRL}7%aVUEqnMW3k zv(@iwPcu7~gqh|KcFK3X^13+#){(&8bQyiYmK>b;==%Z|qddUAGmXmj4d*?*H^PAW zf!((+^6(4iiRDYLH3gU+R^V|jEMUn{;Zn_yaa(h=$60cz~idGT*L##l4FA z5#Xpg8p+n*6{>MDlcK&wb;c(rN_{!?`z|-m{nr(jgD%`Kyswg?^vCm8Y4A}qU+)JE zEge}aYVCgXcRrF7ezJ9Dx#L%j{%Om9527*H2@tD&0Y?#-)_+MoJ@zZL!uS8A`2uW- zhhYS#R6C)A;K4C<_c;#8kDMCMRPUg|saJ;6>POxmPp1Lpc8%-3#!c>cZUZ36LYH`| z{Ed5@)Kgz{DaC<=M|Y#J(0vlyQQFgoUdhDVC}f>+73AKD8n^*%qg-99S<6>4$OOyp z@>}t?DNT$5&L`i1bUiVcH+jwxrxK-TNswRSiwHfdsylDiw&>cfl{YnR10?D)$IhRB zj%P4DJ_QxGxPE8uFOPOVSYF=QKwEBH25RxVl=eHsIzhSW{RU;jvbO9P;8sJp41~N| z3dmBn8f)(>Kf%4!sn_P|(`YWs{FGXPofXyhwh!MIYGUzJR?L@*^67aFfL`*Doz-&- zZ4$WxN6mL0zw{l&E-<mvACTv%V5kdUy!e`8*`o;fr!@H(m&1lF883K8hqSo?d|~$R zNZ;c+(ZL*k{*bSPPZXZ(!YgZv`b7J5`qca+`;_u2;S=NPO5GHVzCxd?23{5ra?&y z0v^G@!gXe??Oei#Qq17JB@lM?C`5G&7WK+F%H`b+!rkaUd(bY2)v5)S7GfiG3{jgP zmB9c(tXr-CdH^X&M#owbLO#NzFbpqe0GiV&S`Fl_-Vl4X43vie$g^ov(x;_N*N_X+ z>If+W&H(Xc%~C;&Qa)7we;XpaU+w5tsBa z#z>c3LhgqiOQtML62lfc5>jQ4XFx}Z^p(~fkKTZP(<4c4%j|uf0d1>*zt_`z2mQdf zu(woG0=Rkd+k?nZGwwi8Giw1r&fWrnpB4guztbTY&Cz{~wtTkhISK}P0-i_o{2sx5 zkK5fAd2fcq-8Ml}fZFC-D-`v{#uME}hn8{Mck(^HP?^i;Kpb2Cm@DnLdb`FtNI>or*iN$w`taN_QuQ!Y zqL8s9o4hFxc~d?%HKZZo1yA=73j&ynv?BbG)7ny$Q)KYr!Fs1T7y*CFidu!7WYENT zd>M>2q4rbgen=T<%ZRI5>noyqmR4Eq7L@xryB}~Bw%4h;Q>NN>?h_%$D~qOXdDeq| zh~s+oBZS(Ls#Ctul`%b&-%4#74Vx>N0zK^851EgAKLG_6YU>T<^qd;k@0aq7)m|?? zZO)=$PDJT5L@H}_j2QW~p{L0zV>veNpB-cX z+G@^x!{+S;9M&behksS?2OkX{bT2ov8p$}i@2U2v9k!!5MHA1?9FV?j1gt>8m=`1s zknVxK0Qa`DCzheQi7NP}_Y%CXy_Qc-$vsrUf6=FNAh*3FLH%Z_dV_I$6vq1`FeZ?y z0*5bAOb$MX>SO&T?F@pHzn!ZlkH2>Bd_6gZesue{Xh;33a~pxrbeaEC1j0G zxa)v?2Jd}K0iP(NnrfN6Hq7?9rkRGBmYu6i(j8jxU%#uZ!3~pSoC88<;p8_+N6&(b|bN%E@iwkM5ijAwk)mk?o)~mj)O8k`Rwm&d2?v?Hzz63*5!CXOF zJZ!+<$BQr{zN;3tLdh=izZ%_*bf^3Vn_t&Yg_5Sw>Ye@$qB*ICQDHxaa{vc3g6zbY z8-sfa+h9Mbf&ei=w4q?oo%Ol>0u1NMf;3wr@T6mbZB@$6GN%JA2qYJci{;R_zSxWQ ztD#W`_ta(OE8#DUQG&U>QXPtBYk4{EuWPS@QGpJi<<>cntZR^pRL6(T2coK}jE*46 zOpvvk>9`#N)kcd|_FV<#8BeePgnv%#pj1g9F5{vWzur-)01F~isC<$z`K`(?&$GhI z_LK96sDgH`fRB(!IP|Z8JY&-vv4)N;NYpr6J`E|7AjrHr2fG}>+0otv{@|p|dt%cShbPdLD(?#p3jB?KCLE4%mW4eEda^pS z^d;X!ON)jsT;?3SJ{#KW{jktFMJi#7#lt=|o5pDH(T&qDhAuiy5p(OX#aP3;z`TLF zVQ|H!GGh!~e3JIP{RwT1^SSblKR#GH0=FS6&k$%Aa^676+Gm<~Ei-}$_04;mHxVIdb(Tc|IIE8|!svu6`lEfM{}Xeym7`vjcu$zoVa95 z28)%|WtdUB%T3%_7oB?OBu0(pVL}|Td1g=y!gC^@unx$qS~EMuJ2h9z+pHBixCV(j zMCoo>X+ko(pbyj#@T_Ui7)8RJ1C%HGpI~)^YKb=>rzoFuVy90jJ(7C#0zTzj16B7c zF5mm}M-}1J4|o(+<2Ri3Tx9M(vG>-vJ*We~hG7ODL3F0IdAMA%6nWB+w{xFfAl+m@ z>_hMmq0Mn(qS8|(>8l)TEyy4WrP0O-Ed zn=3?j91&9a4g^xwX5v|(&7plkF_pvauCnckPanEY8)nGOm01@Zv0J#6hK=)#h*`Qb zpxvmi$wx*_VdkYM<%(4DR#v^(u8}76{2lrL`^6A*zsL}#)iYG%0HfY>qxOEuA2|nz zie}`fiYrO5L_YEt@OYxfh^$ikE%oE}`bJW01v^S4-a+SU zgj>A3qYn@CU*oe=G2)SV=OS`P!3T}qv%1e!`QqVY zo+(2_`~3aal3MjA3m3ip=^&>l++XHCoQl%kfjv1@%`L~8S7A+5-=QBhb(dz^c+T9V zEz*=9t3P-ae*}OMX7s=PqT9YLJ5sh*RZOk2@UI7OmxnISe0H}p!fAGRau6rym>uR+ z1G~QF@1Aa+6kns$_GNh}&*DwS%*AK(lM+KHQPF`dm%~V6%kSgIm8l%%j8Lx}$g2a<2q!pIbx9@|CdlVCK#kv$Dd+`fp3W z>nXY<49PcY?aL^GoH4B$o9mEhu0kz7&t)5ZPQTw|szCPH=SzcIyem~o5o=0S&R&+> zAzoyR$yd9aE)QeT{X>gHYWU1eB|za1d0_}8RQ5q;n;t+Yp+J1G4ot#$a;y|dYP=4(O2AC_75v@}|K9A27uv&=JEp_)5OF!J|1Mgx zE{KxL?Q>DsK-ZhcYJLd(29YX`t~&=*NJM@p6fQZ2U*PhQ%XguuRT=@yRIGw@83!Xj zv@gq8xmuDRFMqS6zG!9UQ7AspMlZx70`-~h|G6qdmFbtYAAUZIRPUVhk9~0F6)q|9 z56>yW;_l+>X-sNAuo_aaw)~@@2)gwjsbiRq70IjuGl^gM)`x- zZX9+6dej-MDvt;OM}Kt(Nb_K=4tf51G6OBvXiiC!!;z}s6K*Rz;^AOXjaHoiNY6by zhk3pxc0(ZLh>W`gbwFZJeZ_Xe#^gr0WADL9EqKdwP9cEa02 zFxlr{SP1*1OF2xTN03jeT;i_9&Ww+tfDVT12jNIXn{I)H_t!Y9=cGzX%+36!_Udiz zmtqLbxi83B_cTQLRb?l1d*k)U{81808hFJ_A(b(k~ zX4MPi#5_S#=U^7981iBBbI$tYW$6Kr%#k?i#ZVuePqr&K=){1-#BCxw1i_5Gk?ZY0 zqg_{Y8)bv=jE9Lc;Z8J7Sa4C>rauOB6v9 z&ezxav=Y+x<@0-8sa=ZFZ{k|4?aQCaU(0o$FJGOXPh%RY2Q24pA&_)@$_aja$zKd# z7PP&^x$^pce{@~D49Rfpb0xon-4{uz6a?mAX}io7@}&R3t{YU!25L{qn@Y4WFlaX4 zU7)PcIRVgsX{y-QRT!z{k2K)rAFqkGKUfb%nkmHhGYHK?vSfcBR(GNh9A=B8wqU== za@KLse^0@oQ3m|~7^0$Gn4z9XLG!ksOKHQeTE?gBp+ob2`&{{C{x-E6_8XCD=$eZm z&GSc^#c-3v(qeB%FD%^01sC;osg^aP*wp?d2p63~QtMR;QhMQZ)AzxtEmW$bc+Hhye$En{ z1-+{54F5F}s#i&#>IXIRqn#1_@w`WPmp4%+oOqEaN8CfOuBgprHjs>1dYdHi5 zzTRA>94pR>0C9(coDzmk*uX-|JW~6%h~6{(zLhpSw)lraj!n@AcYG)3!Ij~We8KsC z;PkiUJK^G_(Z)`R7nnw@i<~$NTlR$$TjZ}>kc#Jdryk9v} zkdcsm7K!{SRC<3N66vk=8@APY=8PwuZJ&-f>YG*wB*9FVntUb0Z0|eg!u;)MmQRPpDrM)qfx*a0_w zAbMkzX)6#J3pRDw{oH7*^$+Ks_s&A&G6f<*-)}-je^c{NtUI z7hXZSb&ZFNGORcRx(`;8mLq6bYL@=n*i@y18X|>!7Sp6Ewv+6 zt-SWR4p!r^rsJXtATz)Vz78K1KF5G3YFuAoVX`L}C=uL*7_NwB45p)DR#d=EXpOdrp(}zsJ$R2wub}x zlq2WRn{O`a*TiM@U_n(GhJMvc}oYnt{`aKV9LpqO%%&!k9xI`|RvwaT^tL0x+e!+U( zlNk=Nc50hABQT}EA~>!a5Xc^3#I>8r_9M;2 zuvj7$`o{$M5jERnI@UEfv2YJn9(7val6HP$0m4f+R?6IB&+7o|jIGFDQ>y)a7GHI6tp5=Gn2}YPYL}*J+?|z8rvSi`k%IZe9o~v~qUHRo-1E$qvLp_a zSlnvLNQricL&qm67cLm&9Vymoej0|{!7p_g@(;P_f3rYfQpU5QqJ2m7Hx;>o@M5dw zPBELQV7CG{fKwD!sL7&U=4@Mi-08phZ{CMw5W~*;i zwzdVv_et0~<8guV5x{l@0Vn-2sMV+t7voC9k619tAjl^x423+g8P57`ROH%XZ;yf%Ma=xKr*&D4<1}~Hp)$XKi%PZ8wk{-j*~kD# zx;Lpq8w1_$g_X3~C~0%VQiW}k={jvo{NUwv{hEyTPFZ+A5wBf4aya)Ib-n`o1-CkN zQ6L^V+5ByTd~Vix;^1Tzt~t$i3ko@Dzg2Uv`X6n7Rc?LZc@USPOvSd=!99|+7)LDG zol>|CtcWmg@J3UM^GgG2a@sWfF7XJ&{Yc%PX)2Bk`2)3NieJJ-)<)&*4P0dAFZ^E# z8rN$bYN$ms8C8sMGqgM_kFk}wFgaQwKsc@G#U&wDq9)r=x|MnfT#+TC8LqkF#uRZ@ zYdV_=EaWP&pG&U_Lf5p0Ajc)nn9&P19A^Y5-&*I%(bI7Z8&|whqjn8vAE1Quc-b;A zX!;Mw5;ap#(W^i0$XN|j0U^c`><~iV6XAz9lwEomm12syH9gqr(a8Y*j?AX{zAt#- z4H!G~Z}a)BpRZti*Gag)Muw{Ag!)+Hrh~h1#QiZRBLbaVexa@D>B$V1;HcC%Lz!LN zaShiM`&=6>H?#OYmR(-L(Zz&dL>i!Wl`Rh(uG5dRkN16Q+HF=>)2(`am$8%4{ zUd|p|Puy`>l7KQPtR*l(X)GZO0Twkp`MOc(xm|6wDfZ-3qfe<%IGV)!MBo?!xhX{Y z(lq>fRRjEg2|hllK0dIR-MLnEO5hn%Q%T$if)?-&(P~l(9 zByAir2%~MHDAAAb;*Ux{h7_gYK^+HK{m+`4V3+GumibE@be>YTGFbbQY2!U}r8jIR z#>$*)z5D-#4?X;7muJMSG9I&Q=oB`=YqwL~SQSc6R5Abrw@AB-6(l`}H8wm=<{NXw z-n!afKF>NfHA!;kIm6m6b12yo4|`X%l>uDcb&PIgbY#;qDf&bW@#tM19>PVz8W~Bb zIUZ^Rp3kM{p`?^;%g2|uLaiRMUR=J(75oApy#F_Cq+Vj(M%mr*tx)glo$8oeIYrLr zZ}mSDj_}s2?k<+F&?UGzy*Jsqm->mw(~Vw_j zhFbt=0reV0xvy$2%2_QH|IT^qV*m^ zZOYS1e+l2LV~1P4d}Ld;P^2X){Rq=M9PqSd31A=#3FNm??KEvN9c&{%z<}Kv@#1GeNC;S1_`t;% zIg=Ovj@@D=a`Dlz*bZlDuTjWLGS4jcitE$C6E}nLWeuC)t&X9-H;py%H}#*Ws7sEK zy*azX*7!XPQMU%x^}j}i24vgp8_rnxDDkD9+!GuU5V4P!9};AkgP)?-D)fZz^o{3x_nFbwA8}k~xyA zK23zj%vt_2euWsQ?7`&nW~R;p3f4-;VF~cy@%A3`Bs0sd4;djxD9(EE>I~oOdVah+;+Sp)O6~ka zmhA%iD(CVOE^rY;trBst#kkWxeb7Rk^BG(I^iWZ;($`VXG1hJ1jXkRa?Xi#&ei-^P zY+hfdlH3P^YHT{E;yWLzNB$=+@#a1zVG&YJAL#bb&p>EVEBe#9y%#e#LVLhhv~g$D zuu)a-!**elq3k^CdFjCDStcAnyu)@FZOuQqksIx&+E;NRQE!RBo>zVBovb7sx)U;% zUk|TaXvMbD5Ml29TONjU>Lo6Dd8qyBTw!$Jb(d1PC~dX690Bfn$p}bEFZsSC8FQY} zZG8ytbD0PG_UB2erpis$=s#prz-<-PQ?>gsc6}rPTO{lk$&a;hom&TE+jR??CDkkW zu>PR*q}nZkeA>O3?^Q^8+T3h`LXLGNQ34(`fp0A}pQFgE zE!)SVQ_H9;XE|xCu3{IQZIy@^@7hJ!Kb?Bj3`4b>$T6tlj0ig6*#_nq)Y~b>v!}_* zM&T|LAM36>@Kp8<^0@_tKWvb8RpHC@O)Br^3p_~Km}N!;8E_>`JLlq1%$8&dJbz0_7(Th8z)~mDzRw!3r^B0Po!$^ZRh*uAsaZWa z#bfq%hO8^O=~Hwmd zrfiP!#ynFy>&P4uhyZCl09wg^b|`BWsmLfmD*v)P$91FK8<9Hgs8r%;QtD3b@^zer z;|N;xMV~nT)i~foQeV>|_Ubn+G4I=kgl~mzBSLZA=@XhDYoI>-kF-wr`r2Pi&gd=f z+~8*s$W0Mjn{EtpY`@I4j7E-TkG~sJ<8rRpBxpKF9C~qOSooRozmMX6rVpb0bZYU+ zw)39T8i`@x9!0Qfmpajmq?j>Otf}}N3p&^K@PF)+bMgp%_MWw~t^*esVPKwJ3@sNM zj!X4*yKX>&-0--Tn7boyGG(1ds^gq$?=Ww58lR-6)R2h#N{zdQEuD|pz+HSEbIz++ zaKXchG*PW6b;e<)DUv8l)ULK((x-6a6Gqb)xD|ODM^^yr3z|@ZSQpqiT&dxG^2j=z0)KTp+Qp9Jt3~TqZ zJg-kSpY*(-J8bS#Z||(TVwK;(O76H<{{ow;UTUSiHb65w#t_2Ln6HtIN6ap&dzw&Z z^+I9wlGOh(jnI3tFkO)yFrzp5EPP*50~`UiF1-X>&xVbZ(L=AAOToU??T-EVt#r(u zI#h0HxVqm12Hodo-Ly6aqSF3PZNoD9>sQzj;wjh)8I3A==5icfWTS-YH_kIpjYRYJ zSE;go7rZgoEkR|=d)Jm9T!wh6$uoZ4xa#m93$anCWGv_RO@Z~kI{-+%gJ+G7x|{c~81$Wfu% zgpA5z4qA3_Aq|7aE}u~(Ur)h3cSoZ0@kEb`pSWgpY;X9(k zx~a8fRQvBdD{tQ-Nd2u>e9qgjG%NYE5ZO7#avWafZ6Y0G6z#H-Fi{@dqC-uhBw?7< z9~3bqaELhiy_8saT>e8ZE8usbgH}M8V?&y5CZ1I%K6`n~e*>`OhaGxW@jvtR(L54R zaikM4kOa%=f->>wM23;7VYZnlyO4!(DM zlmVIcNalCsp0)-9iv)Nv>?Cs{)bfJXG>$}*`v#MoJvX{}39Jy|m=~SIH~g`Oq%JpD zAR*%jt0n~UBNpbmPvb`wMoYC!n(Lg_2WZWzJ}6J)qb9biXpVi)M`wmmxb=0j_aj+y zy%nLWubv9xZUn+p?A%hR1`SpupyFjgo8@h`NO_EAMrOL-vqmf=%ENM>Di*>(qu zMS-`nF{&Umw%?h5s?6^|;PizBxh4c_NLH*8O`t7WLQF{|t{vR1EVzGvl3;>lm#ICC zxt?=q-wh4;y6Nf57CtAgXhFvA=bm5s=Jyo-HpRwJ*Yub@(Bk2S(@lLhJRiIq^XzEQ z-ya-RiX!`*6m>;wsWB<7$>KB%Y+}oCHJi+{a>g->RlW3ssMpO~RB+sN%^o&Z)^Y2O ztPps<2#j!#1#=bg_e6p&$o$t2BQT6kIJ%j(m|DViZC$pYYyLeCYR}ier}U3^*N0y_ zZeD7R@9=QwzBljc_Bay4{SMwV_inya^6UBcoiD7}FMx)w$q@*?Mm}tpD$QRt-I!o~ zwKNr!SUUKc?4UqJL=&0&6;u^eVg246Eu}Xa?@74DFxRgh_C~K?M(agZb6{-H_z2y< zjvDGw9mU?SV1J$(h_@lfwzamzQS6%4YQh@ARf5b6$Na`#?1UviwLPocK8KJ3fmi^@ zIxH^8#GK&r1+1bi;}U7JJD4a`$WGH)yPKi5xfEGvTX^(rLCC^&JgzKEOD6DMt

T!Mk$a0iJjAvwq#VK++cK(c zximC^qe6ze=@G=8ECH+a`6pLv@~6K_RmM)DYOz&!c|`5f5wE^AE%lSpW~*tQRV`4o3a9BUO); zHn`T?0l@CSNDV3eOi+zV35W6fT`F1gLn@ZEgU3eUS$51 z2m%ZccVHB4ZDkj?J0mpu2;AoWOk#8eZ_OT#qkJ1qoc{WJg!8YT9>>J`K6{uSbkN70 zKm^u)fU~7He!_2cj4F70Op%XB_X13NeCF?}Z&>y8**v!NoS@Qcvui$y_{;I95~j%sV+#olPB*&~lx_ zH%%wH#of9=N$8+?96B%c2Eqy+J9)QOXn{;U!_FhJRUU}qU7UYuT2@_ReY}sHf~A-# za-30!odSzDVJ`7!6OWij$d|N$W$W!o#`FXZtKcmpJo%=FsJiVoPtdiGqF8c&wQ5@} zR$M5$BIn?%E+3S<=W#`^9~f6^tfF5OR%G_qBwTPrb$F=@=ki3E#-|m`=(W|J+wY^% z+lJJxo-^tsL?6~>(Wbh}b*^ac{qY@b5jJXIujuSbb5wRicJ_^zfpN+D(ZJFss1ryESn5{-Af;h=9fUT_vk)^$lE2*<`z2il1 zn9l~H-`2I8=~cPn4#-*eRO1VBEBg{{u?pfh(pA(0pAhK%&G3JhP4*1ggnzW7ZIJ0` zT=4FC!Ddz<=Ap5=(A?Isy#7N-&BFHi4xdgMTlOx!X<;#2064~{+?(uPdlAg=i-x+R zci;|bdURTkZ1?0LN1g%cP$tdKyy|)R>Cb7$Rt0ArLut8(9V}INDf)0%OE6=frrvYd z(@R8W)o!Gz$*uCXA~=Y#|2NDL;Sw&MAzx(~;v%e!8#t5+=fd7i9(O?S52qj>INLgtCDzuTbXxc)yl|=Yyu7>?{H#0XgEt=Zam}u_l zqCbII4*Mkcect=M=Q-a=pN@N-_dD+UzV|)uPEJmK4bSua{aX%z1TX*u#B{s{X$AwZ zoglm=*|$%r1#M*$pj~r?vPCgC{oDXnvj#_*+f>vbcKf!W%?l&icGZW$_HG+1_AU4P z(&y-^wLauh05MznZnTZ2X1X6Xw@a2`Ew-rto_2b*)?2nl!nWs$d%^SNefazf&4&MZ zO+_&ur?%y2eGY}3&h~RYznROGp0MJ^Z|(A3t`v_Ct0Nq5@e#%oh358VY5TrhRafgB zpp($76RM_jxu?vzP_ChM53{|b^0gF-vfUjk4@+J3?7=X@MZ8lPp6k)qRqDL!JyE1^ zt+JWZ*cCENC3dmd{lyiNnw+e@x_c`r`Oi|Z`%GjrA6xdn(sjNg)KanEk5ZfA&#UBS zq9mxesA=wIH&T`pOy&1^q3LTjnT;M(uA^J4-$_Qx+(&h;EhTF^KAhIIZGFw|XXDY# z{Jfcbnh((rVs@TWQJwd1*85T%I{VEPuEc^n%mq>=rmNCo6A1hT0$dfHu=s}hOX{osj}5jb9nnr zHs92Z(&2*3Uy_f_)y7>($L?#nF_CO%dNc|Z9$j4Pr!%qFejA#d+-(12bQhVJ_3vZE zXn|0}D!dTXtrNjt!M0AyC%k-^AqP_%ERIZI=Trnrl_(=hYcMcXD$XOg?b#7ivYC{;p zfR_$}QdmcWyi#dlp$`5fi*V2yDIy_WfR#pwdjTmy4+^y~g&rBwRBnV2(2fLk2N2>M zp;)RUrzIgmAqhqZPVi@N5V&jrJLqA>ArKiUD+N2CIK)+f1S050sLqU`l(;dpJ`;m6 zgi$6PX#}BMei6aR5}eD^;VTOD=O@d#N|@jWNY&&C@PvusB{qh^C1Uk$0!Yb00Usm|K?KahyB#2I3L}{*bjSersBwY``AzHpI0$Xg+QvE*0K*uG6A9#ER75^s#fQWis_}OjtRgv#@?cInn hJkHz5_q%rQJPzM>+}JWa&f8W0+`R9;@0PFb{!p?UlpX*8 literal 0 HcmV?d00001 diff --git a/tests/rar3.rs b/tests/rar3.rs index 6a0c5cc..633ccf7 100644 --- a/tests/rar3.rs +++ b/tests/rar3.rs @@ -287,6 +287,75 @@ fn decoder_with_e8_filter_round_trip_on_synthetic_data() { assert_eq!(out, TESTDIR_TEST_TXT_EXPECTED); } +// ─── In-band standard filters (real-archive fixtures) ──────────────────── +// +// Payloads lifted out of archives created with RARLAB `rar` 6.24 (the last +// encoder able to write RAR4) over synthetic content: the fixture bytes are +// exactly what follows the RAR4 file header. The expected CRC-32 is the +// archive's own FILE_CRC header field (CRC-32 of the uncompressed file, +// written by the encoder); extraction was additionally cross-checked +// byte-identical against WinRAR `UnRAR.exe` 7.23 by the differential +// harness. Each stream opens with a main-symbol-257 declaration carrying a +// standard RarVM program that the decoder must recognize and run natively. + +/// gradient.bmp — Delta filter, 3 channels, window 49152 of 49206 bytes. +static FILTER_DELTA_BMP: &[u8] = include_bytes!("fixtures/rar3/filter_delta_gradient_bmp.bin"); +/// ramp.wav — Delta filter, 2 channels (rar 6.24 uses Delta for WAV, not +/// the legacy audio predictor), window 16384 of 16428 bytes. +static FILTER_DELTA_WAV: &[u8] = include_bytes!("fixtures/rar3/filter_delta_ramp_wav.bin"); +/// calls.bin at -m5 — Delta filter, 12 channels. +static FILTER_DELTA12_CALLS: &[u8] = include_bytes!("fixtures/rar3/m5_calls_delta12.bin"); +/// x86slice.bin — x86 E8 (call-only) filter over the whole 32 KiB. +static FILTER_X86_SLICE: &[u8] = include_bytes!("fixtures/rar3/filter_x86_slice.bin"); + +/// Bitwise CRC-32 (IEEE, reflected 0xEDB88320) — small and table-free; +/// test-only, so speed is irrelevant. +fn crc32(data: &[u8]) -> u32 { + let mut crc = 0xFFFF_FFFFu32; + for &b in data { + crc ^= b as u32; + for _ in 0..8 { + let mask = (crc & 1).wrapping_neg(); + crc = (crc >> 1) ^ (0xEDB8_8320 & mask); + } + } + !crc +} + +fn decode_and_check_crc(block: &[u8], unpack_size: u64, want_crc: u32) -> Vec { + let out = decode_full(block, unpack_size); + assert_eq!(out.len() as u64, unpack_size, "unpacked size mismatch"); + assert_eq!( + crc32(&out), + want_crc, + "decoded bytes differ from the archive's FILE_CRC" + ); + out +} + +#[test] +fn inband_delta_filter_bmp_three_channels() { + let out = decode_and_check_crc(FILTER_DELTA_BMP, 49206, 0x2347_E5ED); + // Spot-check: it really is the bitmap (BMP magic survives filtering). + assert_eq!(&out[..2], b"BM"); +} + +#[test] +fn inband_delta_filter_wav_two_channels() { + let out = decode_and_check_crc(FILTER_DELTA_WAV, 16428, 0x0E8F_2810); + assert_eq!(&out[..4], b"RIFF"); +} + +#[test] +fn inband_delta_filter_twelve_channels() { + decode_and_check_crc(FILTER_DELTA12_CALLS, 6146, 0x6C08_D7DF); +} + +#[test] +fn inband_x86_e8_filter() { + decode_and_check_crc(FILTER_X86_SLICE, 32768, 0x6188_0029); +} + // ─── factory (only if compiled in) ─────────────────────────────────────── #[cfg(feature = "factory")] From 897f684ef5901bce850b6e285d225e4a124b0b76 Mon Sep 17 00:00:00 2001 From: JD Lien Date: Thu, 16 Jul 2026 13:02:39 -0600 Subject: [PATCH 2/8] fix(rar3,rar5): harden the filter stack per review; add e2e solid/delta fixtures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review findings on the in-band filter implementation, all three fixed by moving to incremental filter application (flush_completed_filters): - A slot-0 filter-table reset now applies the completed-window prefix and cancels everything still pending, mirroring unrar's InitFilters — a canceled filter must never rewrite output. Previously pending filters survived the reset and ran at finish. - The pending queue is capped at 8192 concurrent filters (unrar's MAX_UNPACK_FILTERS); when the cap is hit after draining completed windows the stream is rejected as corrupt. - A filter window the stream never finishes producing is now Corrupt instead of silently returning pre-filter bytes as success. (unrar writes the raw bytes and relies on the container CRC to flag the file; surfacing the error directly matches this crate's malformed-input policy.) Flushing is prefix-only — a completed window queued behind an incomplete one waits — so overlapping windows (filter chains) always apply in declaration order. Output is append-only and LZ back-references read the unfiltered window, so early application is equivalent to end-of-stream application. New coverage: - Declaration-level unit tests (reset/cancel semantics, the cap, slot-reuse remembered lengths, prefix ordering, truncated-window integration test) driving the parser with the real 29-byte Delta program. - rar5 end-to-end real-archive fixtures: a Delta-filtered member run and a whole six-member solid group exercising add_file_boundary against the archives' own data-CRCs, plus a regression guard showing exactly the x86 member corrupts when boundaries are not registered. - Both fuzz targets from the rar-fuzz-targets branch (#120) merge-tested against this branch and run under ASan seeded with corpus payloads: decoder_rar3 328k runs / decoder_rar5 341k runs, no findings. Docs: README capability matrix updated for both formats; rar5 module docs now describe Delta support and the solid-group calling convention. Known gap, documented: the 57-byte E8E9 program has no real-archive fixture because no current archiver emits it (rar 6.24 uses the E8-only program); its fingerprint cites libarchive and the transform is unit-tested. --- README.md | 4 +- src/rar3/decoder.rs | 227 +++++++++++++++++++-- src/rar5/mod.rs | 20 +- tests/fixtures/rar5/delta_gradient_bmp.bin | Bin 0 -> 276 bytes tests/fixtures/rar5/solid_group_m3.bin | Bin 0 -> 24323 bytes tests/rar3.rs | 14 ++ tests/rar5.rs | 95 ++++++++- 7 files changed, 327 insertions(+), 33 deletions(-) create mode 100644 tests/fixtures/rar5/delta_gradient_bmp.bin create mode 100644 tests/fixtures/rar5/solid_group_m3.bin diff --git a/README.md b/README.md index 6fee379..19efbe6 100644 --- a/README.md +++ b/README.md @@ -74,8 +74,8 @@ flag, and a `compcol` binary turns the library into a Unix-style filter. | StuffIt 5 Arsenic (method 15) | `arsenic` | `.sit` | `Unsupported` (decode-only) | full (range coder + inverse BWT + MTF/RLE + de-randomization) | **real StuffIt 5 fixtures (in-stream CRC-32 + SHA vs `unar`)** | | RAR 1.x | `rar1` | `.rar` | `Unsupported` (license) | building blocks only (Huffman tables not license-clean) | — | | RAR 2.x | `rar2` | `.rar` | `Unsupported` (license) | full LZ77+Huffman + audio predictor | real rar-2.60 fixtures | -| RAR 3.x | `rar3` | `.rar` | `Unsupported` (license) | full LZ77+Huffman + E8 filter; PPMd & VM filters refused | libarchive RAR3 fixtures | -| RAR 5.x | `rar5` | `.rar` | `Unsupported` (license) | full LZ77+Huffman + x86 filter; Delta/ARM refused | RARLAB-CLI fixtures | +| RAR 3.x | `rar3` | `.rar` | `Unsupported` (license) | full LZ77+Huffman + standard filters (Delta, x86 E8/E8E9); PPMd & non-standard VM programs refused | libarchive RAR3 fixtures + **real rar-6.24 archives (differential vs UnRAR 7.23)** | +| RAR 5.x | `rar5` | `.rar` | `Unsupported` (license) | full LZ77+Huffman + Delta/x86 filters (incl. solid groups); ARM refused | RARLAB-CLI fixtures + **real WinRAR 7.23 archives (differential vs UnRAR 7.23)** | | HTTP/2 HPACK (RFC 7541) | `hpack` | — | full (header codec + `h2-huffman` string codec) | full (static+dynamic tables, integer/string coding) | RFC 7541 Appendix C vectors | | HTTP/3 QPACK (RFC 9204) | `qpack` | — | full (static + dynamic-table encoder driving the encoder stream; eviction-safe) | full (static+dynamic tables via encoder stream, all field representations) | RFC 9204 Appendix B vectors | | Canonical Huffman (standalone) | `huffman` | `.huff` | full (length-limited, self-delimiting) | full | own round-trip | diff --git a/src/rar3/decoder.rs b/src/rar3/decoder.rs index 268feae..1af9781 100644 --- a/src/rar3/decoder.rs +++ b/src/rar3/decoder.rs @@ -43,6 +43,7 @@ //! the larger window doesn't change semantics. use alloc::boxed::Box; +use alloc::collections::VecDeque; use alloc::vec; use alloc::vec::Vec; @@ -270,29 +271,25 @@ fn run_decode( unpack_size, programs: Vec::new(), last_filter_slot: 0, - pending_filters: Vec::new(), + pending_filters: VecDeque::new(), }); // The decoder starts by parsing the first block header. parse_block_header(&mut ctx)?; expand(&mut ctx)?; - let mut out = core::mem::take(&mut ctx.out); - - // Run the in-band filters over their declared windows, in declaration - // order (well-formed streams declare filters in ascending window order, - // since each start is relative to the output position at declaration). - // A window the stream never finished producing is dropped, matching - // unrar, which only executes a filter once its full block has been - // decoded. - for f in &ctx.pending_filters { - let end = f.start.saturating_add(f.length as u64); - if end > out.len() as u64 { - continue; - } - apply_pending(f, &mut out[f.start as usize..end as usize])?; + // Run any in-band filters whose windows the stream completed. A filter + // still pending here declared a window the stream never finished + // producing — a truncated or malformed stream. unrar in that situation + // writes the raw bytes and relies on the container CRC to flag the + // file; this crate's policy is to surface the error instead of + // returning pre-filter bytes as a success. + ctx.flush_completed_filters()?; + if !ctx.pending_filters.is_empty() { + return Err(Error::Corrupt); } + let mut out = core::mem::take(&mut ctx.out); if e8_enabled { apply_e8_filter(&mut out, 0, e8_translate_e9); } @@ -333,8 +330,10 @@ struct RunCtx { /// Slot used by the most recent declaration; a declaration without an /// explicit slot field reuses it. last_filter_slot: usize, - /// Scheduled filter instances, applied over the finished output. - pending_filters: Vec, + /// Scheduled filter instances, in declaration order. Applied (and + /// popped from the front) as soon as their windows are fully decoded — + /// see [`RunCtx::flush_completed_filters`]. + pending_filters: VecDeque, } /// A declared filter program plus its per-slot remembered block length. @@ -408,8 +407,35 @@ impl RunCtx { fn done(&self) -> bool { (self.out.len() as u64) >= self.unpack_size } + + /// Apply and drop every filter at the *front* of the pending queue + /// whose window `[start, start + length)` is fully decoded. + /// + /// Only a prefix is flushed: a filter behind one whose window is still + /// incomplete stays queued even if its own window is complete, so + /// overlapping windows (filter chains) always apply in declaration + /// order — the same order unrar's stack executes them. `out` is + /// append-only and LZ back-references read the (unfiltered) window, + /// not `out`, so applying a filter as soon as its window completes is + /// equivalent to applying it at the end of the stream. + fn flush_completed_filters(&mut self) -> Result<(), Error> { + while let Some(&f) = self.pending_filters.front() { + let end = f.start + f.length as u64; + if end > self.out.len() as u64 { + break; + } + apply_pending(&f, &mut self.out[f.start as usize..end as usize])?; + self.pending_filters.pop_front(); + } + Ok(()) + } } +/// Cap on concurrently scheduled filters, matching unrar's +/// `MAX_UNPACK_FILTERS` (8192). Without a cap, a stream of tiny reused +/// declarations could grow the pending queue without bound. +const MAX_PENDING_FILTERS: usize = 8192; + // ─── Block header parsing ──────────────────────────────────────────────── fn parse_block_header(ctx: &mut RunCtx) -> Result<(), Error> { @@ -791,8 +817,12 @@ fn parse_declaration_payload( let slot = if flags & 0x80 != 0 { let v = read_vm_number(db)?; if v == 0 { - // Full reset: forget all declared programs (and anything - // scheduled against them that hasn't completed). + // Full reset (unrar's InitFilters): apply the filters whose + // windows the stream already completed, then cancel everything + // else — a canceled filter must never run, or it would rewrite + // output the encoder didn't transform. + ctx.flush_completed_filters()?; + ctx.pending_filters.clear(); ctx.programs.clear(); 0 } else { @@ -875,12 +905,21 @@ fn parse_declaration_payload( if length > cap { return Err(Error::Corrupt); } - ctx.pending_filters.push(PendingFilter { + ctx.pending_filters.push_back(PendingFilter { start, length, program, channels: r0, }); + if ctx.pending_filters.len() > MAX_PENDING_FILTERS { + // Match unrar's cap on concurrently scheduled filters: try to + // drain completed windows first; a stream that still exceeds the + // cap is hostile or malformed. + ctx.flush_completed_filters()?; + if ctx.pending_filters.len() > MAX_PENDING_FILTERS { + return Err(Error::Corrupt); + } + } Ok(()) } @@ -964,7 +1003,7 @@ mod tests { unpack_size: 0, programs: vec![], last_filter_slot: 0, - pending_filters: vec![], + pending_filters: VecDeque::new(), } } @@ -1010,6 +1049,150 @@ mod tests { code } + /// The 29-byte standard Delta program as WinRAR emits it, lifted from + /// `tests/fixtures/rar3/filter_delta_gradient_bmp.bin` (rar 6.24 + /// archive of gradient.bmp). CRC-32 0x0E06077D; byte 0 is the XOR + /// checksum of the rest. + const DELTA_PROG: [u8; 29] = [ + 0x2F, 0x01, 0x9A, 0x41, 0x80, 0xEC, 0x27, 0x48, 0x2F, 0x09, 0x76, 0x6D, 0xD3, 0xEA, 0x41, + 0x5B, 0x59, 0x44, 0xE8, 0x17, 0x5C, 0xE1, 0x6C, 0x91, 0x4C, 0x4E, 0x3F, 0x77, 0x00, + ]; + + fn incomplete_filter(start: u64) -> PendingFilter { + PendingFilter { + start, + length: 100, + program: StdProgram::Delta, + channels: 1, + } + } + + #[test] + fn delta_program_is_recognized() { + assert_eq!(recognize_program(&DELTA_PROG), Some(StdProgram::Delta)); + } + + /// A reset declaration (slot field 0) must first run the filters whose + /// windows are already complete, then cancel everything still pending — + /// a canceled filter must never rewrite output. + #[test] + fn reset_applies_completed_and_cancels_pending() { + let mut ctx = test_ctx(); + ctx.out = vec![1, 0, 0, 0, 9, 9, 9, 9]; + ctx.programs.push(ProgramSlot { + program: StdProgram::Delta, + last_block_length: 4, + }); + ctx.pending_filters.push_back(PendingFilter { + start: 0, + length: 4, + program: StdProgram::Delta, + channels: 1, + }); + ctx.pending_filters.push_back(incomplete_filter(4)); + + // Payload: slot reset, block_start 0, explicit length 4, r0 = 1 + // channel, then the (new, post-reset) Delta program bytecode. + let mut w = BitWriter::new(); + w.push_vm_number(0); + w.push_vm_number(0); + w.push_vm_number(4); + w.push(0x01, 7); // register mask: r0 only + w.push_vm_number(1); + w.push_vm_number(DELTA_PROG.len() as u32); + for &b in &DELTA_PROG { + w.push(b as u32, 8); + } + let mut db = BitReader::new(); + db.feed_slice(&w.bytes); + parse_declaration_payload(&mut ctx, 0xB0, &mut db).unwrap(); + + // The completed 1-channel delta over [1,0,0,0] ran: prev-integrate + // gives [0xFF; 4]. The trailing bytes stay raw. + assert_eq!(&ctx.out[..4], &[0xFF; 4]); + assert_eq!(&ctx.out[4..], &[9; 4]); + // The incomplete filter was canceled; only the fresh declaration + // (window at out position 8) is scheduled against the fresh slot. + assert_eq!(ctx.pending_filters.len(), 1); + assert_eq!(ctx.pending_filters[0].start, 8); + assert_eq!(ctx.programs.len(), 1); + } + + /// The pending queue is capped (unrar's MAX_UNPACK_FILTERS): once no + /// completed window can be drained, further declarations are corrupt. + #[test] + fn pending_filter_cap_is_enforced() { + let mut ctx = test_ctx(); + ctx.programs.push(ProgramSlot { + program: StdProgram::Delta, + last_block_length: 5, + }); + for _ in 0..MAX_PENDING_FILTERS { + ctx.pending_filters.push_back(incomplete_filter(1_000_000)); + } + // Reuse-slot declaration: block_start only, remembered length. + let mut w = BitWriter::new(); + w.push_vm_number(0); + let mut db = BitReader::new(); + db.feed_slice(&w.bytes); + assert_eq!( + parse_declaration_payload(&mut ctx, 0x00, &mut db), + Err(Error::Corrupt) + ); + } + + /// A declaration without an explicit length (flags bit 5 clear) reuses + /// the slot's remembered length from the previous declaration. + #[test] + fn slot_reuse_inherits_remembered_length() { + let mut ctx = test_ctx(); + // First declaration: fresh Delta program, explicit length 4. + let mut w = BitWriter::new(); + w.push_vm_number(0); + w.push_vm_number(0); + w.push_vm_number(4); + w.push(0x01, 7); + w.push_vm_number(1); + w.push_vm_number(DELTA_PROG.len() as u32); + for &b in &DELTA_PROG { + w.push(b as u32, 8); + } + let mut db = BitReader::new(); + db.feed_slice(&w.bytes); + parse_declaration_payload(&mut ctx, 0xB0, &mut db).unwrap(); + + // Second declaration: no slot field, no explicit length — inherits + // slot 0's remembered length; window 16 bytes further out. + let mut w = BitWriter::new(); + w.push_vm_number(16); + let mut db = BitReader::new(); + db.feed_slice(&w.bytes); + parse_declaration_payload(&mut ctx, 0x00, &mut db).unwrap(); + + assert_eq!(ctx.pending_filters.len(), 2); + assert_eq!(ctx.pending_filters[1].start, 16); + assert_eq!(ctx.pending_filters[1].length, 4); + } + + /// Only a *prefix* of completed windows may flush: a completed filter + /// queued behind an incomplete one must wait so that overlapping + /// windows always apply in declaration order. + #[test] + fn flush_is_prefix_ordered() { + let mut ctx = test_ctx(); + ctx.out = vec![7; 8]; + ctx.pending_filters.push_back(incomplete_filter(4)); + ctx.pending_filters.push_back(PendingFilter { + start: 0, + length: 4, + program: StdProgram::Delta, + channels: 1, + }); + ctx.flush_completed_filters().unwrap(); + assert_eq!(ctx.pending_filters.len(), 2, "nothing may flush"); + assert_eq!(ctx.out, vec![7; 8], "output must be untouched"); + } + #[test] fn unknown_program_is_unsupported() { // Valid declaration framing around bytecode we don't recognize @@ -1107,7 +1290,7 @@ mod tests { unpack_size: 0, programs: vec![], last_filter_slot: 0, - pending_filters: vec![], + pending_filters: VecDeque::new(), }; // Promote slot 2 (value 30) — result should be [30, 10, 20, 40]. promote_offset(&mut ctx, 2, 30); diff --git a/src/rar5/mod.rs b/src/rar5/mod.rs index d74736e..c14d4b0 100644 --- a/src/rar5/mod.rs +++ b/src/rar5/mod.rs @@ -19,9 +19,16 @@ //! - Single-volume RAR5 LZ77+Huffman compressed-data runs. //! - Cross-block table reuse (`table_present` bit set or clear). //! - The four-deep distance LRU and the "repeat last match" command. -//! - The x86 E8 and x86 E8/E9 post-decompression filters (filter types -//! 1 and 2). Delta (type 0), ARM (type 3), and the rare types 4–7 -//! are recognised on the wire but return [`Error::Unsupported`]. +//! - The Delta (type 0), x86 E8 (type 1) and x86 E8/E9 (type 2) +//! post-decompression filters. ARM (type 3) and the rare types 4–7 are +//! recognised on the wire but return [`Error::Unsupported`]. The +//! transforms live in the crate-internal `rar_filters` module, shared +//! with the RAR3 decoder. +//! - **Solid groups**, when the caller drives them as one continuous +//! stream: the LZ window carries across the group's members naturally, +//! and [`Decoder::add_file_boundary`] lets the container register each +//! member's starting offset so the x86 filters compute file-relative +//! addresses (unrar semantics). See the decoder docs. //! //! # What the decoder does *not* do //! @@ -29,11 +36,8 @@ //! main header, file headers, multi-volume continuations, encryption, //! recovery records, …) is not decoded here. Callers extract the inner //! compressed-data run from the container themselves and feed it to -//! the decoder's `decode()` method. -//! - **No solid-archive cross-file dictionary sharing.** RAR5's solid mode -//! keeps the LZ window alive across consecutive file entries; this -//! decoder treats every stream independently. -//! - **No filter chains for non-`X86Call` filter types.** +//! the decoder's `decode()` method. For solid groups that includes +//! concatenating the members' runs and slicing the decoded output. //! - **No CRC32/Blake2sp verification.** Those checksums live in the file //! header, not the compressed stream. //! diff --git a/tests/fixtures/rar5/delta_gradient_bmp.bin b/tests/fixtures/rar5/delta_gradient_bmp.bin new file mode 100644 index 0000000000000000000000000000000000000000..3c75d2069cf8a556b68dd863925d391d36e29bef GIT binary patch literal 276 zcmZpDW~su$^vmdnb)s8B&r)_3$Ad|G*C$@#>;ATfL4b!r`?iqRwc5GC_kLgbQ)e&V zfBf;sA5g}J*1kYz1E*g`e~)lX5=_o;?Nk&gXJcY|m;1MLg+%kpj^>Htf);zy?j7I_ zpVI$#$9wf;&Krly4CeOWVKj9F7&`#X{K%u^lCVi6smAvY4-0!p7WcIy#FoXcT9L=%bP@NoQ#B{8}(E_8h#I8dH+{dWs>LDnKNg;`+sar gc6Rpcdym;I7EWGs@#4Lc1)n&NzL_%nm3H_809_x4Q~&?~ literal 0 HcmV?d00001 diff --git a/tests/fixtures/rar5/solid_group_m3.bin b/tests/fixtures/rar5/solid_group_m3.bin new file mode 100644 index 0000000000000000000000000000000000000000..d2d4ed214fa2e13591b282b2c8d5f1279ab32944 GIT binary patch literal 24323 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 l>of4Hg`Pb; Result, Error> { - let mut dec = Decoder::with_unpack_size_and_window(unpack, window); + let dec = Decoder::with_unpack_size_and_window(unpack, window); + drive(dec, comp, unpack) +} + +/// Drive an already-configured decoder (e.g. one with file boundaries +/// registered) to completion against a single input slice. +fn drive(mut dec: Decoder, comp: &[u8], unpack: u64) -> Result, Error> { let mut out = vec![0u8; unpack as usize]; let mut total = 0usize; @@ -393,6 +399,93 @@ fn reset_clears_state() { assert_eq!(&out2[..total2], expected.as_slice()); } +// ─── Real-archive fixtures: Delta filter + solid-group boundaries ──────── +// +// Packed runs lifted out of archives produced by WinRAR 7.23 (`rar5` +// format, m3, 128 KiB window) over synthetic content — the fixture bytes +// are exactly the member's compressed run following its file header. The +// expected CRC-32s are the archives' own per-file data-CRC header fields; +// extraction is additionally cross-checked byte-identical against +// `UnRAR.exe` 7.23 by the differential harness. + +/// gradient.bmp, non-solid — carries a Delta filter (3 channels). +static RAR5_DELTA_GRADIENT: &[u8] = include_bytes!("fixtures/rar5/delta_gradient_bmp.bin"); + +/// A whole solid group: six members sharing one continuous LZ stream, in +/// this order. x86slice.bin carries x86 E8 filters, gradient.bmp/ramp.wav/ +/// calls.bin carry Delta filters. +static RAR5_SOLID_GROUP: &[u8] = include_bytes!("fixtures/rar5/solid_group_m3.bin"); + +/// (name, start offset in the group's output, unpacked size, CRC-32) per +/// member, from the archive's file headers. +const SOLID_MEMBERS: [(&str, u64, u64, u32); 6] = [ + ("notes.txt", 0, 20001, 0x0E1A_EC07), + ("calls.bin", 20001, 6146, 0x6C08_D7DF), + ("x86slice.bin", 26147, 32768, 0x6188_0029), + ("gradient.bmp", 58915, 49206, 0x2347_E5ED), + ("ramp.wav", 108121, 16428, 0x0E8F_2810), + ("photo.jpg", 124549, 8198, 0x8420_E285), +]; +const SOLID_TOTAL: u64 = 132_747; +const SOLID_WINDOW: usize = 0x20000; + +/// Bitwise CRC-32 (IEEE, reflected 0xEDB88320) — table-free, test-only. +fn crc32(data: &[u8]) -> u32 { + let mut crc = 0xFFFF_FFFFu32; + for &b in data { + crc ^= b as u32; + for _ in 0..8 { + let mask = (crc & 1).wrapping_neg(); + crc = (crc >> 1) ^ (0xEDB8_8320 & mask); + } + } + !crc +} + +#[test] +fn delta_filter_end_to_end() { + let out = decode_once(RAR5_DELTA_GRADIENT, 49206, 0x20000).unwrap(); + assert_eq!(out.len(), 49206); + assert_eq!(&out[..2], b"BM", "BMP magic must survive de-filtering"); + assert_eq!( + crc32(&out), + 0x2347_E5ED, + "must match the archive's data CRC" + ); +} + +#[test] +fn solid_group_with_file_boundaries_end_to_end() { + let mut dec = Decoder::with_unpack_size_and_window(SOLID_TOTAL, SOLID_WINDOW); + for (_, start, _, _) in SOLID_MEMBERS { + dec.add_file_boundary(start); + } + let out = drive(dec, RAR5_SOLID_GROUP, SOLID_TOTAL).unwrap(); + assert_eq!(out.len() as u64, SOLID_TOTAL); + for (name, start, size, want_crc) in SOLID_MEMBERS { + let member = &out[start as usize..(start + size) as usize]; + assert_eq!(crc32(member), want_crc, "{name} differs from its data CRC"); + } +} + +/// Regression guard for the solid x86 position-base bug: without the +/// registered boundaries the E8 transform runs with solid-stream offsets +/// and corrupts the x86 member, while the position-independent Delta +/// members still decode correctly. +#[test] +fn solid_group_without_boundaries_corrupts_only_x86_member() { + let dec = Decoder::with_unpack_size_and_window(SOLID_TOTAL, SOLID_WINDOW); + let out = drive(dec, RAR5_SOLID_GROUP, SOLID_TOTAL).unwrap(); + for (name, start, size, want_crc) in SOLID_MEMBERS { + let got = crc32(&out[start as usize..(start + size) as usize]); + if name == "x86slice.bin" { + assert_ne!(got, want_crc, "x86 member should differ without boundaries"); + } else { + assert_eq!(got, want_crc, "{name} is position-independent"); + } + } +} + // ─── factory (only if the feature is enabled) ──────────────────────────── #[cfg(feature = "factory")] From bd4d8d068e1701168e4395bf4c37bee001d6504b Mon Sep 17 00:00:00 2001 From: JD Lien Date: Thu, 16 Jul 2026 13:30:45 -0600 Subject: [PATCH 3/8] perf(rar3): vectorize match-copy loops in emit_match MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port the rar2/rar5 segment-copy transformation (#115) to rar3, which predated that pass: non-overlapping matches now bulk-copy disjoint window segments (extend_from_slice + copy_within) and distance-1 runs bulk-fill, both capped against ring wrap; genuinely overlapping matches keep the per-byte loop. Measured against UnRAR.exe 7.23 `t` on 32-61 MB RAR4 archives (Ryzen 9 9950X, best of 5, see the corpus repo's BENCH-RAR4.md): match-heavy text: 790 -> 1480 MB/s (+87%), 4.6x -> 1.9x vs unrar x86 + E8 filters: 631 -> 1145 MB/s (+81%), now at parity literal-heavy: unchanged (~122 MB/s, 1.4x) — path untouched Byte-identical: full differential corpus still 188 pass / 0 mismatch; decoder_rar3 fuzz re-run clean over the new path (129k runs, ASan). --- src/rar3/decoder.rs | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/src/rar3/decoder.rs b/src/rar3/decoder.rs index 1af9781..db19ca5 100644 --- a/src/rar3/decoder.rs +++ b/src/rar3/decoder.rs @@ -369,24 +369,32 @@ impl RunCtx { self.out.reserve(length); if off == 1 { - // Distance-1 run: one repeated byte. Fill directly. + // Distance-1 run: one repeated byte, read once before any + // window write (src is behind window_pos). Bulk-fill both the + // output and the window in wrap-capped segments. let b = self.window[src]; - for _ in 0..length { - self.out.push(b); - self.window[self.window_pos] = b; - self.window_pos = (self.window_pos + 1) & wmask; + self.out.resize(self.out.len() + length, b); + let mut done = 0usize; + while done < length { + let run = (length - done).min(wlen - self.window_pos); + let sp = self.window_pos; + self.window[sp..sp + run].fill(b); + self.window_pos = (self.window_pos + run) & wmask; + done += run; } } else if off >= length { - // Non-overlapping: src and dst regions are disjoint. Copy in - // contiguous window segments (no per-byte recompute of `src`). + // Non-overlapping: src and dst regions are disjoint (or dst + // precedes src in the ring, where a forward copy is still + // exact). `off >= length >= run` and the run is capped against + // ring wrap on both cursors, so the bulk copies match the + // per-byte writes exactly — same transformation as the rar2/ + // rar5 match-copy vectorization (#115). let mut done = 0usize; while done < length { let run = (length - done).min(wlen - src).min(wlen - self.window_pos); - for k in 0..run { - let b = self.window[src + k]; - self.out.push(b); - self.window[self.window_pos + k] = b; - } + let sp = self.window_pos; + self.out.extend_from_slice(&self.window[src..src + run]); + self.window.copy_within(src..src + run, sp); src = (src + run) & wmask; self.window_pos = (self.window_pos + run) & wmask; done += run; From aeadab5e623c36d3c092f7355bcc35bc10a90ec3 Mon Sep 17 00:00:00 2001 From: JD Lien Date: Thu, 16 Jul 2026 14:46:01 -0600 Subject: [PATCH 4/8] feat(rar3,ppmd): PPMd-II variant H decode for RAR3/4 + standalone .ppmd MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement the full PPMII variant H model (Ppmd7) and wire it into both the standalone `.ppmd` framing decoder (7z range-coder flavour) and the RAR3/4 PPMd block path (RAR range-coder flavour, with the RAR literal/match/EOD escape layer). Replaces the earlier order-0 stub (arena.rs/model.rs removed). Model (src/ppmd/ppmd7.rs): information-inheritance context tree (CreateSuccessors/UpdateModel), binary-context fast path, masked-escape suffix walk, SEE, tree-wide Rescale, and the byte-arena suballocator with GlueFreeBlocks coalescing. OOB-safe arena accessors set an error flag rather than panicking. Derived from the public-domain LZMA SDK Ppmd7 and the RAR range-coder description in libarchive; no license-restricted code copied. Range decoder (src/ppmd/range_dec.rs): dual 7z/RAR carry-less range coder with a normalisation-step safety cap. Hardening: reject a declared output length beyond the buffer-then-decode ceiling up front and bail on range-coder overrun mid-loop — a high-probability PPMd symbol can decode repeatedly without consuming input, so `overran()` alone doesn't bound the known-length loop (fuzz-found OOM; regression test added). Fuzzing: add dedicated decoder_rar2/rar3/rar5 targets (input-prefix framing for unpack size / E8 flag / window selector). ASan campaigns over the standalone PPMd and RAR3 paths are clean after the OOM fix. Fixtures: real pyppmd Ppmd7 streams (order 6, 16 MiB) plus a RAR3 PPMd block. The encoder stays permanently Unsupported (RARLAB licence). --- Cargo.toml | 4 +- fuzz/Cargo.toml | 21 + fuzz/fuzz_targets/decoder_rar2.rs | 64 ++ fuzz/fuzz_targets/decoder_rar3.rs | 69 ++ fuzz/fuzz_targets/decoder_rar5.rs | 70 ++ src/ppmd/arena.rs | 165 ---- src/ppmd/decoder.rs | 355 +++----- src/ppmd/mod.rs | 62 +- src/ppmd/model.rs | 192 ----- src/ppmd/ppmd7.rs | 1260 ++++++++++++++++++++++++++++ src/ppmd/range_dec.rs | 260 +++--- src/rar3/bits.rs | 11 + src/rar3/decoder.rs | 173 +++- src/rar3/mod.rs | 11 +- tests/fixtures/ppmd/english.bin | 1 + tests/fixtures/ppmd/english.ppmd | Bin 0 -> 78 bytes tests/fixtures/ppmd/hello.bin | 1 + tests/fixtures/ppmd/hello.ppmd | Bin 0 -> 26 bytes tests/fixtures/ppmd/mixed.bin | Bin 0 -> 20000 bytes tests/fixtures/ppmd/mixed.ppmd | Bin 0 -> 20444 bytes tests/fixtures/ppmd/repeat.bin | 1 + tests/fixtures/ppmd/repeat.ppmd | Bin 0 -> 42 bytes tests/fixtures/ppmd/text.bin | 1 + tests/fixtures/ppmd/text.ppmd | Bin 0 -> 56 bytes tests/fixtures/rar3/ppmd_notes.bin | Bin 0 -> 92 bytes tests/ppmd.rs | 375 +++------ tests/rar3.rs | 26 +- 27 files changed, 2079 insertions(+), 1043 deletions(-) create mode 100644 fuzz/fuzz_targets/decoder_rar2.rs create mode 100644 fuzz/fuzz_targets/decoder_rar3.rs create mode 100644 fuzz/fuzz_targets/decoder_rar5.rs delete mode 100644 src/ppmd/arena.rs delete mode 100644 src/ppmd/model.rs create mode 100644 src/ppmd/ppmd7.rs create mode 100644 tests/fixtures/ppmd/english.bin create mode 100644 tests/fixtures/ppmd/english.ppmd create mode 100644 tests/fixtures/ppmd/hello.bin create mode 100644 tests/fixtures/ppmd/hello.ppmd create mode 100644 tests/fixtures/ppmd/mixed.bin create mode 100644 tests/fixtures/ppmd/mixed.ppmd create mode 100644 tests/fixtures/ppmd/repeat.bin create mode 100644 tests/fixtures/ppmd/repeat.ppmd create mode 100644 tests/fixtures/ppmd/text.bin create mode 100644 tests/fixtures/ppmd/text.ppmd create mode 100644 tests/fixtures/rar3/ppmd_notes.bin diff --git a/Cargo.toml b/Cargo.toml index 6498c98..a73d65a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -196,7 +196,9 @@ arsenic = ["alloc"] # the encoders permanently return `Error::Unsupported`. rar1 = ["alloc"] rar2 = ["alloc"] -rar3 = ["alloc"] +# RAR3/4 v29: LZ77+Huffman, in-band standard filters, and PPMd-II var H +# blocks (the latter via the shared `ppmd` model core). +rar3 = ["alloc", "ppmd"] rar5 = ["alloc"] # ZIP method 1 (Shrink): PKZIP 1.x dynamic LZW with partial-clear marker. # Decoder-only — the encoder permanently returns `Error::Unsupported`. diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 0c1d098..d509353 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -275,3 +275,24 @@ path = "fuzz_targets/roundtrip.rs" test = false doc = false bench = false + +[[bin]] +name = "decoder_rar2" +path = "fuzz_targets/decoder_rar2.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "decoder_rar3" +path = "fuzz_targets/decoder_rar3.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "decoder_rar5" +path = "fuzz_targets/decoder_rar5.rs" +test = false +doc = false +bench = false diff --git a/fuzz/fuzz_targets/decoder_rar2.rs b/fuzz/fuzz_targets/decoder_rar2.rs new file mode 100644 index 0000000..c6af656 --- /dev/null +++ b/fuzz/fuzz_targets/decoder_rar2.rs @@ -0,0 +1,64 @@ +#![no_main] +use compcol::Decoder as _; +use compcol::rar2::Decoder; +use libfuzzer_sys::fuzz_target; + +// Smoke property: the decoder must not panic on arbitrary input. +// libfuzzer feeds us garbage bytes; we drive the decoder over them +// and discard the result. Any panic, abort, or undefined behavior +// trips the harness. +// +// RAR2 streams don't self-delimit — the decompressed length lives in +// the archive container's file header — so we read a 4-byte LE length +// prefix (capped to 1 MiB so a hostile size field can't make the +// harness itself allocate unbounded output). `decode` only buffers; +// the real decompression runs on the first `finish` call. +fn drive(mut dec: Decoder, payload: &[u8]) { + let mut out = vec![0u8; 64 * 1024]; + let mut consumed = 0; + let mut steps = 0; + while consumed < payload.len() { + match dec.decode(&payload[consumed..], &mut out) { + Ok((p, _)) => { + if p.consumed == 0 && p.written == 0 { + break; + } + consumed += p.consumed; + } + Err(_) => return, + } + steps += 1; + if steps > 4096 { + // Defensive: pathological inputs shouldn't make us loop. + return; + } + } + let mut steps = 0; + while let Ok((p, status)) = dec.finish(&mut out) { + if matches!(status, compcol::Status::StreamEnd) { + return; + } + if p.written == 0 { + return; + } + steps += 1; + if steps > 4096 { + return; + } + } +} + +fuzz_target!(|data: &[u8]| { + let (len_bytes, payload) = if data.len() >= 4 { + data.split_at(4) + } else { + // Too short to carry a length prefix: still fuzz the + // no-declared-size path with the whole input as payload. + drive(Decoder::new(), data); + return; + }; + let raw = u32::from_le_bytes([len_bytes[0], len_bytes[1], len_bytes[2], len_bytes[3]]); + let unpack = (raw as u64) % (1024 * 1024 + 1); + + drive(Decoder::with_unpack_size(unpack), payload); +}); diff --git a/fuzz/fuzz_targets/decoder_rar3.rs b/fuzz/fuzz_targets/decoder_rar3.rs new file mode 100644 index 0000000..b5eb4c5 --- /dev/null +++ b/fuzz/fuzz_targets/decoder_rar3.rs @@ -0,0 +1,69 @@ +#![no_main] +use compcol::Decoder as _; +use compcol::rar3::Decoder; +use libfuzzer_sys::fuzz_target; + +// Smoke property: the decoder must not panic on arbitrary input. +// libfuzzer feeds us garbage bytes; we drive the decoder over them +// and discard the result. Any panic, abort, or undefined behavior +// trips the harness. +// +// RAR3 streams don't carry their uncompressed length in-band — it +// lives in the archive container's file header — so we read a 5-byte +// prefix: 4 LE bytes for the unpack size (capped to 1 MiB so a hostile +// size field can't make the harness itself allocate unbounded output) +// and 1 flag byte driving the optional standalone E8/E9 post-pass +// filter (bit 0 enables it, bit 1 also translates E9 jumps). +fn drive(mut dec: Decoder, payload: &[u8]) { + let mut out = vec![0u8; 64 * 1024]; + let mut consumed = 0; + let mut steps = 0; + while consumed < payload.len() { + match dec.decode(&payload[consumed..], &mut out) { + Ok((p, _)) => { + if p.consumed == 0 && p.written == 0 { + break; + } + consumed += p.consumed; + } + Err(_) => return, + } + steps += 1; + if steps > 4096 { + // Defensive: pathological inputs shouldn't make us loop. + return; + } + } + let mut steps = 0; + while let Ok((p, status)) = dec.finish(&mut out) { + if matches!(status, compcol::Status::StreamEnd) { + return; + } + if p.written == 0 { + return; + } + steps += 1; + if steps > 4096 { + return; + } + } +} + +fuzz_target!(|data: &[u8]| { + let (prefix, payload) = if data.len() >= 5 { + data.split_at(5) + } else { + // Too short to carry a prefix: still fuzz the no-declared-size + // path with the whole input as payload. + drive(Decoder::new(), data); + return; + }; + let raw = u32::from_le_bytes([prefix[0], prefix[1], prefix[2], prefix[3]]); + let unpack = (raw as u64) % (1024 * 1024 + 1); + + let mut dec = Decoder::with_unpack_size(unpack); + if prefix[4] & 1 != 0 { + dec = dec.with_e8_filter(prefix[4] & 2 != 0); + } + drive(dec, payload); +}); diff --git a/fuzz/fuzz_targets/decoder_rar5.rs b/fuzz/fuzz_targets/decoder_rar5.rs new file mode 100644 index 0000000..1b5fd65 --- /dev/null +++ b/fuzz/fuzz_targets/decoder_rar5.rs @@ -0,0 +1,70 @@ +#![no_main] +use compcol::Decoder as _; +use compcol::rar5::Decoder; +use libfuzzer_sys::fuzz_target; + +// Smoke property: the decoder must not panic on arbitrary input. +// libfuzzer feeds us garbage bytes; we drive the decoder over them +// and discard the result. Any panic, abort, or undefined behavior +// trips the harness. +// +// RAR5 streams don't carry the unpack size or window size in-band — +// both live in the archive container's file header, so the caller +// supplies them out of band. We read a 5-byte prefix: 4 LE bytes for +// the unpack size (capped to 1 MiB so a hostile size field can't make +// the harness itself allocate unbounded output) and 1 byte selecting +// the window size (128 KiB << 0..=5, i.e. capped at 4 MiB so the +// fuzzer explores window wrap-around without gigabyte allocations). +fn drive(mut dec: Decoder, payload: &[u8]) { + let mut out = vec![0u8; 64 * 1024]; + let mut consumed = 0; + let mut steps = 0; + while consumed < payload.len() { + match dec.decode(&payload[consumed..], &mut out) { + Ok((p, _)) => { + if p.consumed == 0 && p.written == 0 { + break; + } + consumed += p.consumed; + } + Err(_) => return, + } + steps += 1; + if steps > 4096 { + // Defensive: pathological inputs shouldn't make us loop. + return; + } + } + let mut steps = 0; + while let Ok((p, status)) = dec.finish(&mut out) { + if matches!(status, compcol::Status::StreamEnd) { + return; + } + if p.written == 0 { + return; + } + steps += 1; + if steps > 4096 { + return; + } + } +} + +fuzz_target!(|data: &[u8]| { + let (prefix, payload) = if data.len() >= 5 { + data.split_at(5) + } else { + // Too short to carry a prefix: still fuzz the default-window, + // no-declared-size path with the whole input as payload. + drive(Decoder::new(), data); + return; + }; + let raw = u32::from_le_bytes([prefix[0], prefix[1], prefix[2], prefix[3]]); + let unpack = (raw as u64) % (1024 * 1024 + 1); + let window = 0x20000usize << (prefix[4] % 6); // 128 KiB ..= 4 MiB + + drive( + Decoder::with_unpack_size_and_window(unpack, window), + payload, + ); +}); diff --git a/src/ppmd/arena.rs b/src/ppmd/arena.rs deleted file mode 100644 index aa6c30d..0000000 --- a/src/ppmd/arena.rs +++ /dev/null @@ -1,165 +0,0 @@ -//! Byte-level arena backing the PPMd context and state allocator. -//! -//! The reference Ppmd7 implementation places every `CPpmd7_Context` and -//! `CPpmd_State` into a single `Byte *Base` buffer and refers to them by -//! 32-bit offset (`CPpmd_*_Ref` is a `UInt32`). That packing is -//! load-bearing: the free-list pointer and the `Stats` ref are read out -//! of the same 4-byte slot a struct field would occupy, the `Node` -//! coalescer walks the buffer linearly assuming `UNIT_SIZE` granularity, -//! etc. We preserve the layout exactly in a single `Vec` and access -//! it through safe little-endian byte gets/puts. -//! -//! All field accessors are little-endian. The C code relies on the host -//! being little-endian (PPMd has no defined behaviour on big-endian -//! archive readers); we make that explicit so the codec round-trips on -//! any architecture. - -use alloc::vec; -use alloc::vec::Vec; - -// ─── packed layout constants ──────────────────────────────────────────── - -pub(super) const UNIT_SIZE: usize = 12; -pub(super) const STATE_SIZE: usize = 6; - -// CPpmd7_Context (packed, total = UNIT_SIZE = 12 bytes) -pub(super) const CTX_OFF_NUM_STATS: usize = 0; // u16 -pub(super) const CTX_OFF_SUMM_FREQ: usize = 2; // u16 -pub(super) const CTX_OFF_STATS: usize = 4; // u32 — CPpmd_State_Ref -pub(super) const CTX_OFF_SUFFIX: usize = 8; // u32 — CPpmd7_Context_Ref - -// CPpmd_State (packed, total = STATE_SIZE = 6 bytes) -pub(super) const STATE_OFF_SYMBOL: usize = 0; // u8 -pub(super) const STATE_OFF_FREQ: usize = 1; // u8 -pub(super) const STATE_OFF_SUCC_LOW: usize = 2; // u16 -pub(super) const STATE_OFF_SUCC_HIGH: usize = 4; // u16 - -// ─── arena ─────────────────────────────────────────────────────────────── - -/// `Vec` arena with little-endian struct accessors. Out-of-range -/// reads return `None` so the model can refuse a corrupt stream rather -/// than panic. -pub(super) struct Arena { - data: Vec, -} - -impl Arena { - pub fn new(size: usize) -> Self { - Self { - data: vec![0u8; size], - } - } - - pub fn clear(&mut self) { - self.data.fill(0); - } - - // ── byte-level r/w (sole entry to the backing store) ─────────────── - - #[inline] - pub fn read_u8(&self, off: u32) -> Option { - self.data.get(off as usize).copied() - } - - #[inline] - pub fn write_u8(&mut self, off: u32, v: u8) -> Option<()> { - *self.data.get_mut(off as usize)? = v; - Some(()) - } - - #[inline] - pub fn read_u16(&self, off: u32) -> Option { - let s = self.data.get(off as usize..off as usize + 2)?; - Some(u16::from_le_bytes([s[0], s[1]])) - } - - #[inline] - pub fn write_u16(&mut self, off: u32, v: u16) -> Option<()> { - let bytes = v.to_le_bytes(); - let slice = self.data.get_mut(off as usize..off as usize + 2)?; - slice.copy_from_slice(&bytes); - Some(()) - } - - #[inline] - pub fn write_u32(&mut self, off: u32, v: u32) -> Option<()> { - let bytes = v.to_le_bytes(); - let slice = self.data.get_mut(off as usize..off as usize + 4)?; - slice.copy_from_slice(&bytes); - Some(()) - } -} - -// ─── context accessors ───────────────────────────────────────────────── - -#[inline] -pub(super) fn ctx_num_stats(a: &Arena, ctx: u32) -> Option { - a.read_u16(ctx + CTX_OFF_NUM_STATS as u32) -} -#[inline] -pub(super) fn ctx_set_num_stats(a: &mut Arena, ctx: u32, v: u16) -> Option<()> { - a.write_u16(ctx + CTX_OFF_NUM_STATS as u32, v) -} -#[inline] -pub(super) fn ctx_summ_freq(a: &Arena, ctx: u32) -> Option { - a.read_u16(ctx + CTX_OFF_SUMM_FREQ as u32) -} -#[inline] -pub(super) fn ctx_set_summ_freq(a: &mut Arena, ctx: u32, v: u16) -> Option<()> { - a.write_u16(ctx + CTX_OFF_SUMM_FREQ as u32, v) -} -#[inline] -pub(super) fn ctx_set_stats(a: &mut Arena, ctx: u32, v: u32) -> Option<()> { - a.write_u32(ctx + CTX_OFF_STATS as u32, v) -} -#[inline] -pub(super) fn ctx_set_suffix(a: &mut Arena, ctx: u32, v: u32) -> Option<()> { - a.write_u32(ctx + CTX_OFF_SUFFIX as u32, v) -} - -// ─── state accessors ─────────────────────────────────────────────────── - -#[inline] -pub(super) fn state_symbol(a: &Arena, st: u32) -> Option { - a.read_u8(st + STATE_OFF_SYMBOL as u32) -} -#[inline] -pub(super) fn state_set_symbol(a: &mut Arena, st: u32, v: u8) -> Option<()> { - a.write_u8(st + STATE_OFF_SYMBOL as u32, v) -} -#[inline] -pub(super) fn state_freq(a: &Arena, st: u32) -> Option { - a.read_u8(st + STATE_OFF_FREQ as u32) -} -#[inline] -pub(super) fn state_set_freq(a: &mut Arena, st: u32, v: u8) -> Option<()> { - a.write_u8(st + STATE_OFF_FREQ as u32, v) -} -#[inline] -pub(super) fn state_set_successor(a: &mut Arena, st: u32, v: u32) -> Option<()> { - a.write_u16(st + STATE_OFF_SUCC_LOW as u32, (v & 0xFFFF) as u16)?; - a.write_u16(st + STATE_OFF_SUCC_HIGH as u32, ((v >> 16) & 0xFFFF) as u16) -} - -/// Swap two adjacent (or any two) states. -pub(super) fn swap_states(a: &mut Arena, st1: u32, st2: u32) -> Option<()> { - let mut s1 = [0u8; STATE_SIZE]; - let mut s2 = [0u8; STATE_SIZE]; - for i in 0..STATE_SIZE { - s1[i] = a.read_u8(st1 + i as u32)?; - s2[i] = a.read_u8(st2 + i as u32)?; - } - for i in 0..STATE_SIZE { - a.write_u8(st1 + i as u32, s2[i])?; - a.write_u8(st2 + i as u32, s1[i])?; - } - Some(()) -} - -/// Write a full state from a tuple. -#[inline] -pub(super) fn state_store(a: &mut Arena, st: u32, sym: u8, freq: u8, succ: u32) -> Option<()> { - state_set_symbol(a, st, sym)?; - state_set_freq(a, st, freq)?; - state_set_successor(a, st, succ) -} diff --git a/src/ppmd/decoder.rs b/src/ppmd/decoder.rs index 7998082..f078ac8 100644 --- a/src/ppmd/decoder.rs +++ b/src/ppmd/decoder.rs @@ -1,22 +1,14 @@ -//! PPMd streaming decoder. +//! PPMd streaming decoder (standalone `.ppmd` framing). //! -//! The decoder is a small state machine wrapped around the order-0 -//! PPMII model in `model.rs` and the carry-less range decoder in -//! `range_dec.rs`: +//! PPMd streams carry no in-band end marker, so like the RAR3 decoder this +//! one is *buffer-then-drain*: [`raw_decode`] absorbs input, and the actual +//! model decode runs once [`raw_finish`] is called and the whole payload is +//! available. The decoded bytes are then drained to the caller across as +//! many `finish` calls as the output buffer size requires. //! -//! 1. **Header** (11 bytes): order, mem_size_mb, restoration_method, -//! little-endian u64 uncompressed length. See the module docs for the -//! framing layout. -//! 2. **RangeInit** (5 bytes): the first 5 bytes of the payload feed -//! `RangeDec::init`. The first byte must be `0x00`. -//! 3. **Decode**: pull symbols one at a time until either the -//! uncompressed length is reached (when known) or the range decoder -//! detects end-of-stream (`is_finished_ok` after the last symbol). -//! -//! Streaming uses a snapshot/restore pattern: before every symbol decode -//! we save the range coder state and the input position so that if the -//! decode tries to read past the buffered input we can rewind and ask -//! the caller for more bytes — same pattern as the bzip2 decoder. +//! See the module docs (`super`) for the 11-byte framing header. The model +//! is the full PPMII variant H core in [`super::ppmd7`], driven by the 7z +//! range decoder. extern crate alloc; use alloc::vec::Vec; @@ -24,61 +16,33 @@ use alloc::vec::Vec; use crate::error::Error; use crate::traits::{RawDecoder, RawProgress}; -use super::model::Model; -use super::range_dec::{ByteSource, RangeDec}; +use super::ppmd7::Ppmd7; +use super::range_dec::{Mode, RangeDec}; -/// Lengths of the framing components. const HEADER_LEN: usize = 11; -const RANGE_INIT_LEN: usize = 5; -/// Sentinel "unknown length" value (matches the `lzma` alone-format -/// convention). const UNKNOWN_LEN: u64 = u64::MAX; - -#[derive(Clone, Copy, PartialEq, Eq)] -enum Phase { - Header, - RangeInit, - Body, - Done, -} +/// Hard cap on decoded output when the header length is unknown, so a tiny +/// crafted stream can't drive unbounded work. +const MAX_UNKNOWN_OUTPUT: usize = 64 * 1024 * 1024; pub struct Decoder { in_buf: Vec, - in_committed: usize, - decoded: Vec, decoded_idx: usize, - - phase: Phase, + started: bool, + finished_decode: bool, poisoned: bool, - - // Header fields (populated after Phase::Header). - order: u32, - mem_mb: u32, - restoration: u8, - expected_len: u64, - produced_len: u64, - - model: Option, - range_dec: RangeDec, } impl Decoder { pub fn new() -> Self { Self { in_buf: Vec::new(), - in_committed: 0, decoded: Vec::new(), decoded_idx: 0, - phase: Phase::Header, + started: false, + finished_decode: false, poisoned: false, - order: 0, - mem_mb: 0, - restoration: 0, - expected_len: 0, - produced_len: 0, - model: None, - range_dec: RangeDec::new(), } } @@ -87,121 +51,81 @@ impl Decoder { e } - /// Try to advance the state machine using `in_buf[in_committed..]`. - fn step(&mut self) -> Result { - match self.phase { - Phase::Header => self.try_header(), - Phase::RangeInit => self.try_range_init(), - Phase::Body => self.try_body(), - Phase::Done => Ok(false), + /// Decode the whole payload into `self.decoded`. Called once, lazily. + fn run_decode(&mut self) -> Result<(), Error> { + if self.in_buf.len() < HEADER_LEN { + return Err(Error::UnexpectedEnd); } - } - - fn try_header(&mut self) -> Result { - if self.in_buf.len() < self.in_committed + HEADER_LEN { - return Ok(false); - } - let off = self.in_committed; - let h = &self.in_buf[off..off + HEADER_LEN]; + let h = &self.in_buf[..HEADER_LEN]; let order = h[0] as u32; let mem_mb = h[1] as u32; let restoration = h[2]; - if !(2..=16).contains(&order) { - return Err(self.poison(Error::BadHeader)); + if !(2..=64).contains(&order) { + return Err(Error::BadHeader); } if !(1..=255).contains(&mem_mb) { - return Err(self.poison(Error::BadHeader)); + return Err(Error::BadHeader); } if restoration > 2 { - return Err(self.poison(Error::BadHeader)); + return Err(Error::BadHeader); } - let len = u64::from_le_bytes(h[3..11].try_into().unwrap()); + let expected_len = u64::from_le_bytes(h[3..11].try_into().unwrap()); - self.order = order; - self.mem_mb = mem_mb; - self.restoration = restoration; - self.expected_len = len; - self.in_committed += HEADER_LEN; + let mem_bytes = mem_mb.saturating_mul(1024 * 1024); + let mut model = Ppmd7::new(mem_bytes)?; + model.init(order); - let mem_bytes = (mem_mb as usize).saturating_mul(1024 * 1024); - self.model = Some(Model::new(order, mem_bytes).map_err(|e| self.poison(e))?); - self.phase = Phase::RangeInit; - Ok(true) - } + let (mut rc, consumed) = RangeDec::init(Mode::SevenZip, &self.in_buf, HEADER_LEN)?; + let _ = consumed; - fn try_range_init(&mut self) -> Result { - if self.in_buf.len() < self.in_committed + RANGE_INIT_LEN { - return Ok(false); - } - // Tell `range_dec.init` where the next byte lives. - self.range_dec.pos = self.in_committed; - match self.range_dec.init(&self.in_buf) { - Ok(true) => { - self.in_committed = self.range_dec.pos; - self.phase = Phase::Body; - Ok(true) - } - Ok(false) => Ok(false), // shouldn't happen — we checked length - Err(e) => Err(self.poison(e)), - } - } - - fn try_body(&mut self) -> Result { - let model = match self.model.as_mut() { - Some(m) => m, - None => return Err(self.poison(Error::Corrupt)), + let cap = if expected_len == UNKNOWN_LEN { + MAX_UNKNOWN_OUTPUT + } else { + expected_len.min(MAX_UNKNOWN_OUTPUT as u64) as usize }; + let mut out = Vec::with_capacity(cap.min(1 << 20)); - // If we know the uncompressed length and have produced it all, - // verify the range coder's terminal state and finish. - if self.expected_len != UNKNOWN_LEN && self.produced_len >= self.expected_len { - if self.range_dec.is_finished_ok() { - self.phase = Phase::Done; - return Ok(true); - } - // The reference accepts a non-zero `code` if a peek confirms - // it. Our simplified order-0 model can't always finish - // exactly on `code == 0`, so we accept the implicit end-of- - // stream as long as no further symbols are requested. - self.phase = Phase::Done; - return Ok(true); - } - - let mut src = ByteSource::new(&self.in_buf, self.range_dec.pos); - let mut progressed = false; - loop { - // If output buffer pressure will soon force a return, stop - // pulling symbols. We use a fixed budget so a giant input - // doesn't starve the caller. - if self.decoded.len() - self.decoded_idx > 4096 { - break; - } - // Snapshot for rollback. - let rd_pre = self.range_dec.clone(); - let pos_pre = src.pos; - // Decode one symbol. - match model.decode_symbol(&mut self.range_dec, &mut src) { - Ok(sym) => { - self.decoded.push(sym); - self.produced_len += 1; - progressed = true; - if self.expected_len != UNKNOWN_LEN && self.produced_len >= self.expected_len { - break; - } + if expected_len == UNKNOWN_LEN { + while out.len() < MAX_UNKNOWN_OUTPUT { + if rc.overran() { + break; } - Err(Error::UnexpectedEnd) => { - // Need more input — rewind and bail. - self.range_dec = rd_pre; - src.pos = pos_pre; + let sym = model.decode_symbol(&mut rc)?; + if rc.overran() { break; } - Err(e) => return Err(self.poison(e)), + out.push(sym); + } + } else { + // A declared length larger than the buffer-then-decode ceiling + // can't be produced here; reject it up front rather than growing + // `out` toward OOM. (A high-probability PPMd symbol can decode + // many times without consuming input, so `overran()` alone is not + // a sufficient bound.) + if expected_len > MAX_UNKNOWN_OUTPUT as u64 { + return Err(Error::OutputLimitExceeded); + } + for _ in 0..expected_len { + // Truncated input can't supply more symbols; stop before the + // model starts decoding from zero-filled reads. + if rc.overran() { + return Err(Error::UnexpectedEnd); + } + let sym = model.decode_symbol(&mut rc)?; + out.push(sym); + } + if rc.overran() { + return Err(Error::UnexpectedEnd); + } + // 7z streams leave the range coder at `code == 0` after the last + // symbol; a non-zero residue means truncation or corruption. + if !rc.is_finished_ok() { + return Err(Error::Corrupt); } } - // Commit the input position. - self.range_dec.pos = src.pos; - self.in_committed = self.range_dec.pos; - Ok(progressed) + + self.decoded = out; + Ok(()) } fn drain(&mut self, output: &mut [u8], written: &mut usize) { @@ -219,6 +143,10 @@ impl Decoder { self.decoded_idx = 0; } } + + fn all_drained(&self) -> bool { + self.finished_decode && self.decoded_idx == self.decoded.len() + } } impl Default for Decoder { @@ -232,126 +160,45 @@ impl RawDecoder for Decoder { if self.poisoned { return Err(Error::Corrupt); } - let mut consumed = 0usize; + // Absorb input; real decoding is deferred to `finish`. + self.in_buf.extend_from_slice(input); let mut written = 0usize; - - // Drain any already-decoded bytes first. - self.drain(output, &mut written); - - // If output is already full from a previous step's queued bytes, - // return without absorbing more input — the caller hasn't drained - // yet and absorbing would cause the bridge to misreport status. - if written == output.len() && self.decoded_idx < self.decoded.len() { - return Ok(RawProgress { - consumed, - written, - done: false, - }); - } - - loop { - // Quick exit if we've already produced everything and drained. - if matches!(self.phase, Phase::Done) && self.decoded_idx == self.decoded.len() { - return Ok(RawProgress { - consumed, - written, - done: true, - }); - } - - // Absorb caller's input into our buffer. - if consumed < input.len() { - self.in_buf.extend_from_slice(&input[consumed..]); - consumed = input.len(); - } - - let progressed = self.step()?; - - // Drain anything the step produced. + if self.finished_decode { self.drain(output, &mut written); - - // Bound `in_buf` growth by chopping off committed bytes when - // the prefix gets large. - if self.in_committed > 1 << 20 { - let off = self.in_committed; - self.in_buf.drain(..off); - self.in_committed = 0; - self.range_dec.pos = self.range_dec.pos.saturating_sub(off); - } - - if matches!(self.phase, Phase::Done) { - continue; - } - - // Output full + queued bytes → caller must drain. Report by - // *un-consuming* one byte of the absorbed input so the bridge - // sees `consumed < input.len()` and maps to `OutputFull`. The - // un-consumed byte is still buffered internally; we just - // delay acknowledging it until the caller comes back. - if written == output.len() && self.decoded_idx < self.decoded.len() { - // Report `consumed < input.len()` so the bridge maps to - // `OutputFull` rather than `InputEmpty`. The un-consumed - // byte is still buffered in `in_buf`; we just delay - // acknowledging it until the caller drains and returns. - consumed = consumed.saturating_sub(1); - return Ok(RawProgress { - consumed, - written, - done: false, - }); - } - - if !progressed { - // No progress and no more input → ask for more. - if consumed >= input.len() { - return Ok(RawProgress { - consumed, - written, - done: false, - }); - } - } - - if written == output.len() { - return Ok(RawProgress { - consumed, - written, - done: false, - }); - } } + Ok(RawProgress { + consumed: input.len(), + written, + done: self.all_drained(), + }) } fn raw_finish(&mut self, output: &mut [u8]) -> Result { if self.poisoned { return Err(Error::Corrupt); } - let empty: [u8; 0] = []; - let p = self.raw_decode(&empty, output)?; - if matches!(self.phase, Phase::Done) && self.decoded_idx == self.decoded.len() { - Ok(RawProgress { - consumed: 0, - written: p.written, - done: true, - }) - } else { - Err(self.poison(Error::UnexpectedEnd)) + if !self.started { + self.started = true; + match self.run_decode() { + Ok(()) => self.finished_decode = true, + Err(e) => return Err(self.poison(e)), + } } + let mut written = 0usize; + self.drain(output, &mut written); + Ok(RawProgress { + consumed: 0, + written, + done: self.all_drained(), + }) } fn raw_reset(&mut self) { self.in_buf.clear(); - self.in_committed = 0; self.decoded.clear(); self.decoded_idx = 0; - self.phase = Phase::Header; + self.started = false; + self.finished_decode = false; self.poisoned = false; - self.order = 0; - self.mem_mb = 0; - self.restoration = 0; - self.expected_len = 0; - self.produced_len = 0; - self.model = None; - self.range_dec = RangeDec::new(); } } diff --git a/src/ppmd/mod.rs b/src/ppmd/mod.rs index 4352c1e..055f420 100644 --- a/src/ppmd/mod.rs +++ b/src/ppmd/mod.rs @@ -9,31 +9,18 @@ //! //! ### What this build ships //! -//! This module ships the **framing layer** plus a working carry-less -//! 7z range decoder (see `range_dec.rs`) and the order-0/order-(-1) -//! base of the PPMII context tree. The full PPMII variant H model -//! (with the information-inheritance `CreateSuccessors`/`UpdateModel` -//! routines, masked-context escape handling, and SEE adaptation) is -//! large enough that completing it in one pass would have left the -//! codec in a half-finished, untested state — exactly the failure mode -//! the project guidance says to avoid. So: -//! -//! - **Decoder**: parses the 11-byte framing header (order, mem, -//! restoration method, uncompressed length), validates parameters, -//! and decodes the range-coded payload using the order-0 model. -//! This works for the *trivial subset* where every literal in the -//! payload was emitted from the model's order-(-1) escape path -//! (i.e. a stream whose body is essentially random-access raw -//! bytes encoded under the uniform-frequency seed model). Payloads -//! produced by real PPMd encoders, which traverse the full -//! information-inheritance tree, will land on the -//! [`Error::Unsupported`] tag once the decoder detects that the -//! uniform seed model has been escaped beyond — same gap pattern -//! as `lzfse`'s `bvx2` blocks. -//! - **Encoder**: permanently returns [`Error::Unsupported`]. The -//! PPM model maintenance plus carry-less range encoder were out of -//! scope; we follow the `lzfse`/`rar*` precedent and ship the -//! encoder as a stub. +//! - **Decoder**: the **full PPMII variant H model** (`ppmd7`) — the +//! information-inheritance context tree (`CreateSuccessors` / +//! `UpdateModel`), the binary-context fast path, the masked-escape +//! suffix walk, SEE (secondary escape estimation), tree-wide `Rescale`, +//! and the sub-allocator with block coalescing — driven by a carry-less +//! range decoder in both its 7z and RAR flavours (`range_dec`). The +//! standalone framing below uses the 7z flavour; the RAR3/4 decoder +//! feeds the same model core through the RAR flavour. Decodes streams +//! produced by real PPMd encoders (7-Zip, `pyppmd`, WinRAR/`rar`). +//! - **Encoder**: permanently returns [`Error::Unsupported`]. The PPM +//! model maintenance plus carry-less range encoder are out of scope; we +//! follow the `lzfse`/`rar*` precedent and ship the encoder as a stub. //! //! ### Wire framing //! @@ -43,20 +30,20 @@ //! "alone" framing: //! //! ```text -//! byte 0 : order (2..=16, inclusive) -//! byte 1 : mem_size_mb (1..=256, inclusive) +//! byte 0 : order (2..=64, inclusive) +//! byte 1 : mem_size_mb (1..=255, inclusive) //! byte 2 : restoration_method (0=restart, 1=cut-off, 2=freeze) //! bytes 3..=10: little-endian u64 uncompressed length //! (0xFFFF_FFFF_FFFF_FFFF means "unknown — decode to //! stream end") -//! bytes 11.. : the PPMd-coded payload +//! bytes 11.. : the PPMd-coded payload (a raw 7z Ppmd7 stream, i.e. +//! a leading 0x00 byte then the range-coded body) //! ``` //! -//! Only the `restart` restoration method is exercised by the range- -//! coded payload (the 7z PPMd model only ever calls `RestartModel` on -//! memory pressure). The byte is kept in the header so the framing -//! matches archive-wrapper conventions; values other than 0 are -//! accepted and ignored. +//! The order and memory size must match what the encoder used (they are +//! not otherwise recoverable from the stream). The restoration-method +//! byte is retained for archive-wrapper parity; the model restarts on +//! memory pressure regardless. //! //! ### References //! @@ -72,13 +59,18 @@ extern crate alloc; use crate::error::Error; use crate::traits::{Algorithm, RawEncoder, RawProgress}; -mod arena; mod decoder; -mod model; +mod ppmd7; mod range_dec; pub use decoder::Decoder; +// Re-exported for the RAR3/4 PPMd path; unused when `ppmd` is built alone. +#[cfg(feature = "rar3")] +pub(crate) use ppmd7::Ppmd7; +#[cfg(feature = "rar3")] +pub(crate) use range_dec::{Mode as RangeMode, RangeDec}; + /// Zero-sized marker type implementing [`Algorithm`] for PPMd. #[derive(Debug, Clone, Copy, Default)] pub struct Ppmd; diff --git a/src/ppmd/model.rs b/src/ppmd/model.rs deleted file mode 100644 index b1e3939..0000000 --- a/src/ppmd/model.rs +++ /dev/null @@ -1,192 +0,0 @@ -//! PPMII variant H context model — *order-0 subset*. -//! -//! Safe-Rust port of the order-0 portion of Igor Pavlov's public-domain -//! `Ppmd7.{c,h}` (LZMA SDK). The reference packs every `CPpmd7_Context` -//! and `CPpmd_State` into a single `Byte *Base` arena and walks it -//! through 32-bit offsets; we keep that layout in `arena.rs` so the -//! pointer arithmetic translates straight across. -//! -//! ### Implementation scope -//! -//! This file implements **only the order-0 model state** — the 256-state -//! root context that PPMd uses as the bottom of its suffix chain. It -//! decodes any symbol the range coder's threshold lands in, updates the -//! frequency table (with the same `+4` increment + `MAX_FREQ` rescale), -//! and promotes the chosen state when its frequency overtakes its -//! predecessor. This is exactly the behaviour of a fresh model that has -//! never seen `UpdateModel` extend the tree, i.e. the moment immediately -//! after `Ppmd7_Init`. -//! -//! The full PPMII machinery — the per-order context tree built up by -//! `CreateSuccessors`, the binary-context special case for `NumStats==1` -//! contexts, the masked-escape walk through the suffix chain, the SEE -//! (secondary escape estimation) adaptation, and `Rescale` over the -//! entire tree — is **not** implemented in this build. Payloads from -//! real PPMd encoders exercise all of these paths almost immediately, -//! so this decoder is *not* a drop-in replacement for `7z x` or -//! `ppmd-cffi.decompress`. Tests use hand-built order-0 fixtures. - -extern crate alloc; - -use crate::error::Error; - -use super::arena::{ - Arena, STATE_SIZE, UNIT_SIZE, ctx_num_stats, ctx_set_num_stats, ctx_set_stats, ctx_set_suffix, - ctx_set_summ_freq, ctx_summ_freq, state_freq, state_set_freq, state_store, state_symbol, - swap_states, -}; -use super::range_dec::{ByteSource, RangeDec}; - -const MAX_FREQ: u8 = 124; - -/// Order-0 PPMII model. One 256-state context lives at offset -/// [`Model::CTX_OFFSET`] in the arena; states follow it. -pub(super) struct Model { - arena: Arena, - /// Offset of the order-0 context block (always zero in this build). - ctx_off: u32, - /// Offset of the state array (256 entries × `STATE_SIZE`). - stats_off: u32, -} - -impl Model { - /// Pre-computed arena layout. The order-0 context starts at byte 0 - /// (one UNIT = 12 bytes) and the state array starts at byte - /// `UNIT_SIZE`. With 256 states × 6 bytes = 1536 bytes of stats, the - /// minimum usable arena is `UNIT_SIZE + 256 * STATE_SIZE = 1548` - /// bytes. We round up to 2 KiB so the layout has some slack for any - /// future model-update extension. - const MIN_ARENA: usize = 2048; - - pub fn new(order: u32, mem_size_bytes: usize) -> Result { - if !(2..=16).contains(&order) { - return Err(Error::BadHeader); - } - // The header's advertised memory size (`mem_size_bytes`, up to - // 255 MiB) describes the arena the *full* PPMII tree would need. - // This build only implements the order-0 subset, whose arena never - // grows past root (`MIN_ARENA` = 2 KiB) — `arena.rs` has no growth - // path and every offset stays within that window. So we cap the - // eager `vec![0u8; size]` allocation to the actual working size - // rather than honouring the advertised figure, which would let an - // 11-byte header force a 255 MiB allocation (L9). The parsed header - // value is still validated and retained by the decoder; only the - // allocation size is capped here. - let _ = mem_size_bytes; - let size = Self::MIN_ARENA; - let _ = order; // captured in the framing header only; the order-0 - // subset doesn't grow the tree past root. - let mut m = Self { - arena: Arena::new(size), - ctx_off: 0, - stats_off: UNIT_SIZE as u32, - }; - m.restart()?; - Ok(m) - } - - /// Initialise the order-0 context with 256 equally-weighted states. - pub fn restart(&mut self) -> Result<(), Error> { - self.arena.clear(); - // Build context at offset 0. - ctx_set_num_stats(&mut self.arena, self.ctx_off, 256).ok_or(Error::Corrupt)?; - ctx_set_summ_freq(&mut self.arena, self.ctx_off, 256 + 1).ok_or(Error::Corrupt)?; - ctx_set_stats(&mut self.arena, self.ctx_off, self.stats_off).ok_or(Error::Corrupt)?; - ctx_set_suffix(&mut self.arena, self.ctx_off, 0).ok_or(Error::Corrupt)?; - for i in 0..256u32 { - let st = self.stats_off + i * STATE_SIZE as u32; - state_store(&mut self.arena, st, i as u8, 1, 0).ok_or(Error::Corrupt)?; - } - Ok(()) - } - - /// Decode one symbol from the range coder. Mirrors `Ppmd7_DecodeSymbol` - /// for the order-0 case (`NumStats == 256`, no binary fast path, no - /// suffix walk, no SEE). - pub fn decode_symbol( - &mut self, - rd: &mut RangeDec, - src: &mut ByteSource<'_>, - ) -> Result { - // Snapshot for "need more input" rollback. - let rd_snap = rd.clone(); - let pos_snap = src.pos; - - match self.decode_inner(rd, src) { - Ok(sym) => Ok(sym), - Err(Error::UnexpectedEnd) => { - *rd = rd_snap; - src.pos = pos_snap; - Err(Error::UnexpectedEnd) - } - Err(e) => Err(e), - } - } - - fn decode_inner(&mut self, rd: &mut RangeDec, src: &mut ByteSource<'_>) -> Result { - let nstats = ctx_num_stats(&self.arena, self.ctx_off).ok_or(Error::Corrupt)?; - if nstats == 0 { - return Err(Error::Corrupt); - } - let summ_freq = ctx_summ_freq(&self.arena, self.ctx_off).ok_or(Error::Corrupt)?; - let total = summ_freq as u32; - if total == 0 { - return Err(Error::Corrupt); - } - let hi_count = rd.get_threshold(total); - - let mut acc: u32 = 0; - for i in 0..nstats as u32 { - let st = self.stats_off + i * STATE_SIZE as u32; - let f = state_freq(&self.arena, st).ok_or(Error::Corrupt)? as u32; - if acc + f > hi_count { - rd.decode(src, acc, f)?; - let sym = state_symbol(&self.arena, st).ok_or(Error::Corrupt)?; - self.bump_freq(i, f, summ_freq)?; - return Ok(sym); - } - acc += f; - } - Err(Error::Corrupt) - } - - fn bump_freq(&mut self, i: u32, freq: u32, summ_freq: u16) -> Result<(), Error> { - let new_freq = freq + 4; - let new_summ = summ_freq as u32 + 4; - if new_freq > MAX_FREQ as u32 || new_summ > 0xFFFF { - self.rescale()?; - return Ok(()); - } - let st = self.stats_off + i * STATE_SIZE as u32; - state_set_freq(&mut self.arena, st, new_freq as u8).ok_or(Error::Corrupt)?; - ctx_set_summ_freq(&mut self.arena, self.ctx_off, new_summ as u16).ok_or(Error::Corrupt)?; - // Promote: if this state's frequency now exceeds its predecessor's, - // swap them (keeps the array roughly sorted by freq). - if i > 0 { - let prev = self.stats_off + (i - 1) * STATE_SIZE as u32; - let prev_f = state_freq(&self.arena, prev).ok_or(Error::Corrupt)? as u32; - if new_freq > prev_f { - swap_states(&mut self.arena, st, prev).ok_or(Error::Corrupt)?; - } - } - Ok(()) - } - - fn rescale(&mut self) -> Result<(), Error> { - // Halve every frequency, dropping zero-frequency states (the - // reference's behaviour). We never go below one because order-0 - // must keep every symbol decodable. - let nstats = ctx_num_stats(&self.arena, self.ctx_off).ok_or(Error::Corrupt)? as u32; - let mut new_summ: u32 = 0; - for i in 0..nstats { - let st = self.stats_off + i * STATE_SIZE as u32; - let f = state_freq(&self.arena, st).ok_or(Error::Corrupt)? as u32; - let new_f = ((f + 1) >> 1).max(1) as u8; - state_set_freq(&mut self.arena, st, new_f).ok_or(Error::Corrupt)?; - new_summ += new_f as u32; - } - ctx_set_summ_freq(&mut self.arena, self.ctx_off, new_summ.min(0xFFFF) as u16) - .ok_or(Error::Corrupt)?; - Ok(()) - } -} diff --git a/src/ppmd/ppmd7.rs b/src/ppmd/ppmd7.rs new file mode 100644 index 0000000..54ec578 --- /dev/null +++ b/src/ppmd/ppmd7.rs @@ -0,0 +1,1260 @@ +//! PPMII variant H context model (`Ppmd7`) — full decoder. +//! +//! Safe-Rust port of Igor Pavlov's public-domain `Ppmd7.{c,h}` (LZMA SDK, +//! itself based on Dmitry Shkarin's PPMd var.H, 2001 — both public domain). +//! The reference packs every `CPpmd7_Context` (12 bytes) and `CPpmd_State` +//! (6 bytes) into a single `Byte *Base` arena and refers to them by 32-bit +//! offset; we keep that byte-exact layout in one `Vec` and reach it +//! through little-endian accessors. Offsets ("refs") are `u32`; ref `0` is +//! reserved as the null pointer (the arena's live region starts at +//! `align_offset`, always `>= 1`). +//! +//! The range coder is external (see [`super::range_dec::RangeDec`]); the +//! model calls `get_threshold` / `decode` / `decode_bit` on it, so the same +//! model core drives both the 7z framing (standalone `.ppmd`) and RAR's +//! carry-less range decoder. +//! +//! This is a faithful, spec-derived port — not a transliteration of any +//! license-restricted RAR source. `Ppmd7.c` is public domain and may be +//! followed closely; libarchive's BSD RAR reader was consulted only for the +//! RAR range-decoder *description*, not copied. + +// The model walks a flat byte arena by 32-bit offset; explicit index loops +// (`for i in 0..n { ... base[off + i] ... }`) mirror the reference's pointer +// arithmetic and read more clearly than iterator adapters here. +#![allow(clippy::needless_range_loop)] + +extern crate alloc; + +use alloc::vec; +use alloc::vec::Vec; + +use crate::error::Error; + +use super::range_dec::RangeDec; + +const UNIT_SIZE: u32 = 12; +const MAX_FREQ: u8 = 124; +const PPMD_NUM_INDEXES: usize = 38; // N1+N2+N3+N4 with N1=N2=N3=4 +const PPMD_BIN_SCALE: u32 = 1 << 14; +const PPMD_INT_BITS: u32 = 7; +const PPMD_PERIOD_BITS: u32 = 7; +const MAX_ORDER: usize = 64; + +const K_INIT_BIN_ESC: [u16; 8] = [ + 0x3CDD, 0x1F3F, 0x59BF, 0x48F3, 0x64A1, 0x5ABC, 0x6632, 0x6051, +]; +const K_EXP_ESCAPE: [u8; 16] = [25, 14, 9, 7, 5, 5, 4, 4, 4, 3, 3, 3, 2, 2, 2, 2]; + +#[inline] +fn get_mean(summ: u32) -> u32 { + (summ + (1 << (PPMD_PERIOD_BITS - 2))) >> PPMD_PERIOD_BITS +} + +/// One SEE (secondary escape estimation) cell. +#[derive(Clone, Copy, Default)] +struct See { + summ: u16, + shift: u8, + count: u8, +} + +impl See { + /// `Ppmd_See_Update`. + #[inline] + fn update(&mut self) { + if (self.shift as u32) < PPMD_PERIOD_BITS && { + self.count = self.count.wrapping_sub(1); + self.count == 0 + } { + self.summ = self.summ.wrapping_shl(1); + self.count = 3u8.wrapping_shl(self.shift as u32); + self.shift += 1; + } + } +} + +/// Which SEE cell `make_esc_freq` selected — used so the caller can update +/// the same cell after decoding. +#[derive(Clone, Copy)] +enum SeeRef { + Dummy, + Cell(usize, usize), +} + +pub(crate) struct Ppmd7 { + base: Vec, + size: u32, + align_offset: u32, + err: bool, + + // Pointers (byte offsets into `base`). + min_context: u32, + max_context: u32, + found_state: u32, + text: u32, + lo_unit: u32, + hi_unit: u32, + units_start: u32, + glue_count: u32, + + order_fall: u32, + init_esc: u32, + prev_success: u32, + max_order: u32, + hi_bits_flag: u32, + run_length: i32, + init_rl: i32, + + free_list: [u32; PPMD_NUM_INDEXES], + + indx2units: [u8; PPMD_NUM_INDEXES], + units2indx: [u8; 128], + ns2indx: [u8; 256], + ns2bsindx: [u8; 256], + hb2flag: [u8; 256], + + see: [[See; 16]; 25], + dummy_see: See, + bin_summ: [[u16; 64]; 128], +} + +impl Ppmd7 { + /// Build the model for `mem_size` bytes of arena. The order is supplied + /// to [`Ppmd7::init`]. `mem_size` must be at least `UNIT_SIZE`. + pub(crate) fn new(mem_size: u32) -> Result { + if mem_size < UNIT_SIZE { + return Err(Error::BadHeader); + } + let align_offset = 4 - (mem_size & 3); + let total = (align_offset + mem_size + UNIT_SIZE) as usize; + let mut p = Self { + base: vec![0u8; total], + size: mem_size, + align_offset, + err: false, + min_context: 0, + max_context: 0, + found_state: 0, + text: 0, + lo_unit: 0, + hi_unit: 0, + units_start: 0, + glue_count: 0, + order_fall: 0, + init_esc: 0, + prev_success: 0, + max_order: 0, + hi_bits_flag: 0, + run_length: 0, + init_rl: 0, + free_list: [0; PPMD_NUM_INDEXES], + indx2units: [0; PPMD_NUM_INDEXES], + units2indx: [0; 128], + ns2indx: [0; 256], + ns2bsindx: [0; 256], + hb2flag: [0; 256], + see: [[See::default(); 16]; 25], + dummy_see: See::default(), + bin_summ: [[0u16; 64]; 128], + }; + p.construct_tables(); + Ok(p) + } + + /// `Ppmd7_Construct` — the fixed lookup tables. + fn construct_tables(&mut self) { + let mut k = 0usize; + for i in 0..PPMD_NUM_INDEXES { + let mut step = if i >= 12 { 4 } else { (i >> 2) + 1 }; + while step > 0 { + self.units2indx[k] = i as u8; + k += 1; + step -= 1; + } + self.indx2units[i] = k as u8; + } + + self.ns2bsindx[0] = 0; + self.ns2bsindx[1] = 2; + for v in self.ns2bsindx[2..11].iter_mut() { + *v = 4; + } + for v in self.ns2bsindx[11..256].iter_mut() { + *v = 6; + } + + for i in 0..3 { + self.ns2indx[i] = i as u8; + } + let mut m = 3u8; + let mut kk = 1i32; + for i in 3..256 { + self.ns2indx[i] = m; + kk -= 1; + if kk == 0 { + m += 1; + kk = (m as i32) - 2; + } + } + + for v in self.hb2flag[0..0x40].iter_mut() { + *v = 0; + } + for v in self.hb2flag[0x40..256].iter_mut() { + *v = 8; + } + } + + // ─── raw arena accessors (little-endian, OOB-safe) ────────────────── + + #[inline] + fn gu8(&mut self, off: u32) -> u8 { + match self.base.get(off as usize) { + Some(&b) => b, + None => { + self.err = true; + 0 + } + } + } + #[inline] + fn pu8(&mut self, off: u32, v: u8) { + match self.base.get_mut(off as usize) { + Some(b) => *b = v, + None => self.err = true, + } + } + #[inline] + fn gu16(&mut self, off: u32) -> u16 { + let o = off as usize; + match self.base.get(o..o + 2) { + Some(s) => u16::from_le_bytes([s[0], s[1]]), + None => { + self.err = true; + 0 + } + } + } + #[inline] + fn pu16(&mut self, off: u32, v: u16) { + let o = off as usize; + match self.base.get_mut(o..o + 2) { + Some(s) => s.copy_from_slice(&v.to_le_bytes()), + None => self.err = true, + } + } + #[inline] + fn gu32(&mut self, off: u32) -> u32 { + let o = off as usize; + match self.base.get(o..o + 4) { + Some(s) => u32::from_le_bytes([s[0], s[1], s[2], s[3]]), + None => { + self.err = true; + 0 + } + } + } + #[inline] + fn pu32(&mut self, off: u32, v: u32) { + let o = off as usize; + match self.base.get_mut(o..o + 4) { + Some(s) => s.copy_from_slice(&v.to_le_bytes()), + None => self.err = true, + } + } + + // ─── context / state field accessors ──────────────────────────────── + + #[inline] + fn ctx_num_stats(&mut self, c: u32) -> u32 { + self.gu16(c) as u32 + } + #[inline] + fn ctx_set_num_stats(&mut self, c: u32, v: u32) { + self.pu16(c, v as u16); + } + #[inline] + fn ctx_summ_freq(&mut self, c: u32) -> u32 { + self.gu16(c + 2) as u32 + } + #[inline] + fn ctx_set_summ_freq(&mut self, c: u32, v: u32) { + self.pu16(c + 2, v as u16); + } + #[inline] + fn ctx_stats(&mut self, c: u32) -> u32 { + self.gu32(c + 4) + } + #[inline] + fn ctx_set_stats(&mut self, c: u32, v: u32) { + self.pu32(c + 4, v); + } + #[inline] + fn ctx_suffix(&mut self, c: u32) -> u32 { + self.gu32(c + 8) + } + #[inline] + fn ctx_set_suffix(&mut self, c: u32, v: u32) { + self.pu32(c + 8, v); + } + /// `Ppmd7Context_OneState` — the embedded single state overlaps + /// SummFreq+Stats, i.e. offset `c + 2`. + #[inline] + fn one_state(c: u32) -> u32 { + c + 2 + } + + #[inline] + fn st_symbol(&mut self, s: u32) -> u8 { + self.gu8(s) + } + #[inline] + fn st_set_symbol(&mut self, s: u32, v: u8) { + self.pu8(s, v); + } + #[inline] + fn st_freq(&mut self, s: u32) -> u8 { + self.gu8(s + 1) + } + #[inline] + fn st_set_freq(&mut self, s: u32, v: u8) { + self.pu8(s + 1, v); + } + /// `st.Freq += d` (read-modify-write; avoids `setter(getter())`). + #[inline] + fn st_add_freq(&mut self, s: u32, d: u8) { + let f = self.st_freq(s); + self.st_set_freq(s, f.wrapping_add(d)); + } + /// `ctx.SummFreq += d`. + #[inline] + fn ctx_add_summ_freq(&mut self, c: u32, d: u32) { + let f = self.ctx_summ_freq(c); + self.ctx_set_summ_freq(c, f + d); + } + #[inline] + fn st_successor(&mut self, s: u32) -> u32 { + (self.gu16(s + 2) as u32) | ((self.gu16(s + 4) as u32) << 16) + } + #[inline] + fn st_set_successor(&mut self, s: u32, v: u32) { + self.pu16(s + 2, (v & 0xFFFF) as u16); + self.pu16(s + 4, ((v >> 16) & 0xFFFF) as u16); + } + + /// Copy state `src` onto state `dst` (6 bytes). + #[inline] + fn st_copy(&mut self, dst: u32, src: u32) { + for i in 0..6 { + let b = self.gu8(src + i); + self.pu8(dst + i, b); + } + } + /// Swap two 6-byte states. + #[inline] + fn st_swap(&mut self, a: u32, b: u32) { + for i in 0..6 { + let x = self.gu8(a + i); + let y = self.gu8(b + i); + self.pu8(a + i, y); + self.pu8(b + i, x); + } + } + + #[inline] + fn i2u(&self, indx: usize) -> u32 { + self.indx2units[indx] as u32 + } + #[inline] + fn u2i(&self, nu: u32) -> usize { + // nu in 1..=128 + self.units2indx[(nu as usize).wrapping_sub(1).min(127)] as usize + } + #[inline] + fn u2b(nu: u32) -> u32 { + nu * UNIT_SIZE + } + + /// Copy `nu` units (12 bytes each) from `src` to `dst`. + fn copy_units(&mut self, dst: u32, src: u32, nu: u32) { + let n = (nu * UNIT_SIZE) as usize; + let (d, s) = (dst as usize, src as usize); + if d + n <= self.base.len() && s + n <= self.base.len() { + self.base.copy_within(s..s + n, d); + } else { + self.err = true; + } + } + + // ─── allocator ────────────────────────────────────────────────────── + + fn insert_node(&mut self, node: u32, indx: usize) { + let head = self.free_list[indx]; + self.pu32(node, head); + self.free_list[indx] = node; + } + + fn remove_node(&mut self, indx: usize) -> u32 { + let node = self.free_list[indx]; + let next = self.gu32(node); + self.free_list[indx] = next; + node + } + + fn split_block(&mut self, ptr: u32, old_indx: usize, new_indx: usize) { + let nu = self.i2u(old_indx) - self.i2u(new_indx); + let ptr = ptr + Self::u2b(self.i2u(new_indx)); + let mut i = self.u2i(nu); + if self.i2u(i) != nu { + i -= 1; + let k = self.i2u(i); + self.insert_node(ptr + Self::u2b(k), (nu - k - 1) as usize); + } + self.insert_node(ptr, i); + } + + fn glue_free_blocks(&mut self) { + // Node layout: Stamp u16 @0, NU u16 @2, Next u32 @4, Prev u32 @8. + // The reference builds a doubly-linked list of all free blocks, sets + // a head sentinel just past the arena, coalesces adjacent blocks, + // and re-buckets them. We follow it directly. + let head = self.align_offset + self.size; // sentinel node ref + let mut n = head; + self.glue_count = 255; + + for i in 0..PPMD_NUM_INDEXES { + let nu = self.i2u(i) as u16; + let mut next = self.free_list[i]; + self.free_list[i] = 0; + while next != 0 { + let node = next; + // node->Next = n + self.pu32(node + 4, n); + // n = node->Prev = node (NODE(n)->Prev = node; then n = node) + self.pu32(n + 8, node); + n = node; + next = self.gu32(node); // *(Ref*)node — original free-list link + self.pu16(node, 0); // Stamp = 0 + self.pu16(node + 2, nu); // NU + } + } + + // head node + self.pu16(head, 1); // Stamp + self.pu32(head + 4, n); // Next + self.pu32(n + 8, head); // NODE(n)->Prev = head + if self.lo_unit != self.hi_unit { + self.pu16(self.lo_unit, 1); // Stamp guard + } + + // Glue adjacent free blocks (walk head->Next round to head). + let mut n = self.gu32(head + 4); + while n != head { + let node = n; + let mut nu = self.gu16(node + 2) as u32; + loop { + let node2 = node + nu * UNIT_SIZE; + let stamp = self.gu16(node2); + let nu2 = self.gu16(node2 + 2) as u32; + if stamp != 0 || nu + nu2 >= 0x10000 { + break; + } + nu += nu2; + // unlink node2 + let prev = self.gu32(node2 + 8); + let nxt = self.gu32(node2 + 4); + self.pu32(prev + 4, nxt); + self.pu32(nxt + 8, prev); + self.pu16(node + 2, nu as u16); + } + n = self.gu32(node + 4); + if self.err { + return; + } + } + + // Refill free lists. + let mut n = self.gu32(head + 4); + while n != head { + let mut node = n; + let next = self.gu32(node + 4); + let mut nu = self.gu16(node + 2) as u32; + while nu > 128 { + self.insert_node(node, PPMD_NUM_INDEXES - 1); + nu -= 128; + node += 128 * UNIT_SIZE; + } + let mut i = self.u2i(nu); + if self.i2u(i) != nu { + i -= 1; + let k = self.i2u(i); + self.insert_node(node + k * UNIT_SIZE, (nu - k - 1) as usize); + } + self.insert_node(node, i); + n = next; + if self.err { + return; + } + } + } + + fn alloc_units_rare(&mut self, indx: usize) -> u32 { + if self.glue_count == 0 { + self.glue_free_blocks(); + if self.free_list[indx] != 0 { + return self.remove_node(indx); + } + } + let mut i = indx; + loop { + i += 1; + if i == PPMD_NUM_INDEXES { + let num_bytes = Self::u2b(self.i2u(indx)); + self.glue_count = self.glue_count.wrapping_sub(1); + return if self.units_start - self.text > num_bytes { + self.units_start -= num_bytes; + self.units_start + } else { + 0 + }; + } + if self.free_list[i] != 0 { + break; + } + } + let ret = self.remove_node(i); + self.split_block(ret, i, indx); + ret + } + + fn alloc_units(&mut self, indx: usize) -> u32 { + if self.free_list[indx] != 0 { + return self.remove_node(indx); + } + let num_bytes = Self::u2b(self.i2u(indx)); + if num_bytes <= self.hi_unit - self.lo_unit { + let ret = self.lo_unit; + self.lo_unit += num_bytes; + return ret; + } + self.alloc_units_rare(indx) + } + + fn shrink_units(&mut self, old_ptr: u32, old_nu: u32, new_nu: u32) -> u32 { + let i0 = self.u2i(old_nu); + let i1 = self.u2i(new_nu); + if i0 == i1 { + return old_ptr; + } + if self.free_list[i1] != 0 { + let ptr = self.remove_node(i1); + self.copy_units(ptr, old_ptr, new_nu); + self.insert_node(old_ptr, i0); + ptr + } else { + self.split_block(old_ptr, i0, i1); + old_ptr + } + } + + // ─── init / restart ───────────────────────────────────────────────── + + pub(crate) fn init(&mut self, max_order: u32) { + self.max_order = max_order; + self.restart_model(); + self.dummy_see.shift = PPMD_PERIOD_BITS as u8; + self.dummy_see.summ = 0; + self.dummy_see.count = 64; + } + + /// Seed `InitEsc` (RAR's header carries it explicitly). Only used by + /// the RAR3/4 PPMd path. + #[cfg_attr(not(feature = "rar3"), allow(dead_code))] + pub(crate) fn set_init_esc(&mut self, v: u32) { + self.init_esc = v; + } + + fn restart_model(&mut self) { + self.free_list = [0; PPMD_NUM_INDEXES]; + self.text = self.align_offset; + self.hi_unit = self.text + self.size; + let reserved = self.size / 8 / UNIT_SIZE * 7 * UNIT_SIZE; + self.lo_unit = self.hi_unit - reserved; + self.units_start = self.lo_unit; + self.glue_count = 0; + + self.order_fall = self.max_order; + let mo = if self.max_order < 12 { + self.max_order + } else { + 12 + }; + self.init_rl = -(mo as i32) - 1; + self.run_length = self.init_rl; + self.prev_success = 0; + + self.hi_unit -= UNIT_SIZE; + let mc = self.hi_unit; + self.min_context = mc; + self.max_context = mc; + self.ctx_set_suffix(mc, 0); + self.ctx_set_num_stats(mc, 256); + self.ctx_set_summ_freq(mc, 256 + 1); + self.found_state = self.lo_unit; + self.lo_unit += Self::u2b(256 / 2); + let fs = self.found_state; + self.ctx_set_stats(mc, fs); + for i in 0..256u32 { + let s = fs + i * 6; + self.st_set_symbol(s, i as u8); + self.st_set_freq(s, 1); + self.st_set_successor(s, 0); + } + + for i in 0..128 { + for k in 0..8 { + let val = (PPMD_BIN_SCALE - (K_INIT_BIN_ESC[k] as u32) / (i as u32 + 2)) as u16; + let mut m = 0; + while m < 64 { + self.bin_summ[i][k + m] = val; + m += 8; + } + } + } + + for i in 0..25 { + for k in 0..16 { + let shift = (PPMD_PERIOD_BITS - 4) as u8; + self.see[i][k] = See { + summ: ((5 * i as u32 + 10) << shift) as u16, + shift, + count: 4, + }; + } + } + } + + // ─── model update helpers ─────────────────────────────────────────── + + /// `CreateSuccessors`. Returns `Some(ctx)` or `None` (NULL → restart). + fn create_successors(&mut self, skip: bool) -> Option { + let mut c = self.min_context; + let up_branch = self.st_successor(self.found_state); + let mut ps: [u32; MAX_ORDER] = [0; MAX_ORDER]; + let mut num_ps = 0usize; + + if !skip { + ps[num_ps] = self.found_state; + num_ps += 1; + } + + let found_sym = self.st_symbol(self.found_state); + while self.ctx_suffix(c) != 0 { + c = self.ctx_suffix(c); + let s = if self.ctx_num_stats(c) != 1 { + let mut ss = self.ctx_stats(c); + while self.st_symbol(ss) != found_sym { + ss += 6; + } + ss + } else { + Self::one_state(c) + }; + let successor = self.st_successor(s); + if successor != up_branch { + c = successor; + if num_ps == 0 { + return Some(c); + } + break; + } + if num_ps >= MAX_ORDER { + self.err = true; + return None; + } + ps[num_ps] = s; + num_ps += 1; + } + + // upState + let up_symbol = self.gu8(up_branch); + let up_successor = up_branch + 1; + let up_freq: u8 = if self.ctx_num_stats(c) == 1 { + self.st_freq(Self::one_state(c)) + } else { + let mut s = self.ctx_stats(c); + while self.st_symbol(s) != up_symbol { + s += 6; + } + let cf = self.st_freq(s) as u32 - 1; + let s0 = self.ctx_summ_freq(c) - self.ctx_num_stats(c) - cf; + (1 + if 2 * cf <= s0 { + (5 * cf > s0) as u32 + } else { + (2 * cf + 3 * s0 - 1) / (2 * s0) + }) as u8 + }; + + while num_ps != 0 { + // AllocContext + let c1 = if self.hi_unit != self.lo_unit { + self.hi_unit -= UNIT_SIZE; + self.hi_unit + } else if self.free_list[0] != 0 { + self.remove_node(0) + } else { + let r = self.alloc_units_rare(0); + if r == 0 { + return None; + } + r + }; + self.ctx_set_num_stats(c1, 1); + let os = Self::one_state(c1); + self.st_set_symbol(os, up_symbol); + self.st_set_freq(os, up_freq); + self.st_set_successor(os, up_successor); + self.ctx_set_suffix(c1, c); + num_ps -= 1; + self.st_set_successor(ps[num_ps], c1); + c = c1; + } + Some(c) + } + + fn update_model(&mut self) { + let f_successor = self.st_successor(self.found_state); + let found_sym = self.st_symbol(self.found_state); + let found_freq = self.st_freq(self.found_state); + + if found_freq < MAX_FREQ / 4 && self.ctx_suffix(self.min_context) != 0 { + let c = self.ctx_suffix(self.min_context); + if self.ctx_num_stats(c) == 1 { + let s = Self::one_state(c); + if self.st_freq(s) < 32 { + self.st_add_freq(s, 1); + } + } else { + let mut s = self.ctx_stats(c); + if self.st_symbol(s) != found_sym { + loop { + s += 6; + if self.st_symbol(s) == found_sym { + break; + } + } + if self.st_freq(s) >= self.st_freq(s - 6) { + self.st_swap(s, s - 6); + s -= 6; + } + } + if self.st_freq(s) < MAX_FREQ - 9 { + self.st_add_freq(s, 2); + self.ctx_add_summ_freq(c, 2); + } + } + } + + if self.order_fall == 0 { + match self.create_successors(true) { + Some(mc) => { + self.min_context = mc; + self.max_context = mc; + self.st_set_successor(self.found_state, mc); + } + None => self.restart_model(), + } + return; + } + + // *Text++ = found symbol + self.pu8(self.text, found_sym); + self.text += 1; + let successor = self.text; + if self.text >= self.units_start { + self.restart_model(); + return; + } + + let mut f_successor = f_successor; + let mut successor = successor; + if f_successor != 0 { + if f_successor <= self.text { + // f_successor points into the text region → materialise it. + match self.create_successors(false) { + Some(cs) => f_successor = cs, + None => { + self.restart_model(); + return; + } + } + } + self.order_fall -= 1; + if self.order_fall == 0 { + successor = f_successor; + if self.max_context != self.min_context { + self.text -= 1; + } + } + } else { + self.st_set_successor(self.found_state, successor); + f_successor = self.min_context; + } + + let ns = self.ctx_num_stats(self.min_context); + let s0 = self.ctx_summ_freq(self.min_context) - ns - (found_freq as u32 - 1); + + let mut c = self.max_context; + while c != self.min_context { + let ns1 = self.ctx_num_stats(c); + if ns1 != 1 { + if ns1 & 1 == 0 { + // grow the stats block by one unit + let old_nu = ns1 >> 1; + let i = self.u2i(old_nu); + if i != self.u2i(old_nu + 1) { + let ptr = self.alloc_units(i + 1); + if ptr == 0 { + self.restart_model(); + return; + } + let old_ptr = self.ctx_stats(c); + self.copy_units(ptr, old_ptr, old_nu); + self.insert_node(old_ptr, i); + self.ctx_set_stats(c, ptr); + } + } + let add = (2 * ns1 < ns) as u32 + + 2 * ((4 * ns1 <= ns) as u32 & (self.ctx_summ_freq(c) <= 8 * ns1) as u32); + self.ctx_add_summ_freq(c, add); + } else { + let s = self.alloc_units(0); + if s == 0 { + self.restart_model(); + return; + } + self.st_copy(s, Self::one_state(c)); + self.ctx_set_stats(c, s); + let mut fr = self.st_freq(s); + if fr < MAX_FREQ / 4 - 1 { + fr <<= 1; + } else { + fr = MAX_FREQ - 4; + } + self.st_set_freq(s, fr); + self.ctx_set_summ_freq(c, fr as u32 + self.init_esc + (ns > 3) as u32); + } + + let cf = 2 * (found_freq as u32) * (self.ctx_summ_freq(c) + 6); + let sf = s0 + self.ctx_summ_freq(c); + let new_freq; + if cf < 6 * sf { + new_freq = 1 + (cf > sf) as u32 + (cf >= 4 * sf) as u32; + self.ctx_add_summ_freq(c, 3); + } else { + new_freq = + 4 + (cf >= 9 * sf) as u32 + (cf >= 12 * sf) as u32 + (cf >= 15 * sf) as u32; + self.ctx_add_summ_freq(c, new_freq); + } + let stats = self.ctx_stats(c); + let s = stats + ns1 * 6; + self.st_set_successor(s, successor); + self.st_set_symbol(s, found_sym); + self.st_set_freq(s, new_freq as u8); + self.ctx_set_num_stats(c, ns1 + 1); + + c = self.ctx_suffix(c); + if self.err { + return; + } + } + + self.max_context = f_successor; + self.min_context = f_successor; + } + + fn rescale(&mut self) { + let stats = self.ctx_stats(self.min_context); + let mut s = self.found_state; + // Move found state to front (rotate). + { + let mut tmp = [0u8; 6]; + for i in 0..6 { + tmp[i] = self.gu8(s + i as u32); + } + while s != stats { + self.st_copy(s, s - 6); + s -= 6; + } + for i in 0..6 { + self.pu8(s + i as u32, tmp[i]); + } + } + let mut esc_freq = self.ctx_summ_freq(self.min_context) - self.st_freq(s) as u32; + self.st_add_freq(s, 4); + let adder = (self.order_fall != 0) as u32; + { + let nf = ((self.st_freq(s) as u32 + adder) >> 1) as u8; + self.st_set_freq(s, nf); + } + let mut sum_freq = self.st_freq(s) as u32; + + let mut i = self.ctx_num_stats(self.min_context) - 1; + loop { + s += 6; + esc_freq -= self.st_freq(s) as u32; + { + let nf = ((self.st_freq(s) as u32 + adder) >> 1) as u8; + self.st_set_freq(s, nf); + } + sum_freq += self.st_freq(s) as u32; + if self.st_freq(s) > self.st_freq(s - 6) { + let mut s1 = s; + let mut tmp = [0u8; 6]; + for j in 0..6 { + tmp[j] = self.gu8(s1 + j as u32); + } + let tmp_freq = tmp[1]; + loop { + self.st_copy(s1, s1 - 6); + s1 -= 6; + if s1 == stats || tmp_freq <= self.st_freq(s1 - 6) { + break; + } + } + for j in 0..6 { + self.pu8(s1 + j as u32, tmp[j]); + } + } + i -= 1; + if i == 0 { + break; + } + } + + if self.st_freq(s) == 0 { + let mut cnt = 0u32; + loop { + cnt += 1; + s -= 6; + if self.st_freq(s) != 0 { + break; + } + } + esc_freq += cnt; + let num_stats = self.ctx_num_stats(self.min_context); + let new_ns = num_stats - cnt; + self.ctx_set_num_stats(self.min_context, new_ns); + if new_ns == 1 { + let mut tmp = [0u8; 6]; + for j in 0..6 { + tmp[j] = self.gu8(stats + j as u32); + } + let mut tmp_freq = tmp[1]; + loop { + tmp_freq = tmp_freq - (tmp_freq >> 1); + esc_freq >>= 1; + if esc_freq <= 1 { + break; + } + } + tmp[1] = tmp_freq; + self.insert_node(stats, self.u2i((num_stats + 1) >> 1)); + let fs = Self::one_state(self.min_context); + self.found_state = fs; + for j in 0..6 { + self.pu8(fs + j as u32, tmp[j]); + } + return; + } + let n0 = (num_stats + 1) >> 1; + let n1 = (new_ns + 1) >> 1; + if n0 != n1 { + let ns = self.shrink_units(stats, n0, n1); + self.ctx_set_stats(self.min_context, ns); + } + } + let stats = self.ctx_stats(self.min_context); + self.ctx_set_summ_freq(self.min_context, sum_freq + esc_freq - (esc_freq >> 1)); + self.found_state = stats; + } + + /// `Ppmd7_MakeEscFreq`. Returns the SEE ref and writes `esc_freq`. + fn make_esc_freq(&mut self, num_masked: u32) -> (SeeRef, u32) { + let num_stats = self.ctx_num_stats(self.min_context); + if num_stats != 256 { + let non_masked = num_stats - num_masked; + let suffix = self.ctx_suffix(self.min_context); + let suffix_ns = self.ctx_num_stats(suffix) as i64; + let idx_row = self.ns2indx[(non_masked - 1) as usize] as usize; + let diff = suffix_ns - num_stats as i64; + let idx_col = ((non_masked as i64) < diff) as usize + + 2 * (self.ctx_summ_freq(self.min_context) < 11 * num_stats) as usize + + 4 * (num_masked > non_masked) as usize + + self.hi_bits_flag as usize; + let see = &mut self.see[idx_row][idx_col]; + let r = (see.summ >> see.shift) as u32; + see.summ = see.summ.wrapping_sub(r as u16); + let esc = r + (r == 0) as u32; + (SeeRef::Cell(idx_row, idx_col), esc) + } else { + (SeeRef::Dummy, 1) + } + } + + #[inline] + fn see_update(&mut self, sr: SeeRef) { + if let SeeRef::Cell(i, k) = sr { + self.see[i][k].update(); + } + } + #[inline] + fn see_add_summ(&mut self, sr: SeeRef, v: u32) { + if let SeeRef::Cell(i, k) = sr { + let s = &mut self.see[i][k]; + s.summ = s.summ.wrapping_add(v as u16); + } + } + + fn next_context(&mut self) { + let c = self.st_successor(self.found_state); + if self.order_fall == 0 && c > self.text { + self.min_context = c; + self.max_context = c; + } else { + self.update_model(); + } + } + + fn update1(&mut self) { + let s = self.found_state; + self.st_add_freq(s, 4); + let c = self.min_context; + self.ctx_add_summ_freq(c, 4); + if self.st_freq(s) > self.st_freq(s - 6) { + self.st_swap(s, s - 6); + self.found_state = s - 6; + if self.st_freq(s - 6) > MAX_FREQ { + self.rescale(); + } + } + self.next_context(); + } + + fn update1_0(&mut self) { + let s = self.found_state; + self.prev_success = + (2 * self.st_freq(s) as u32 > self.ctx_summ_freq(self.min_context)) as u32; + self.run_length += self.prev_success as i32; + let c = self.min_context; + self.ctx_add_summ_freq(c, 4); + self.st_add_freq(s, 4); + if self.st_freq(s) > MAX_FREQ { + self.rescale(); + } + self.next_context(); + } + + fn update_bin(&mut self) { + let s = self.found_state; + let f = self.st_freq(s); + self.st_set_freq(s, f + (f < 128) as u8); + self.prev_success = 1; + self.run_length += 1; + self.next_context(); + } + + fn update2(&mut self) { + let s = self.found_state; + let c = self.min_context; + self.ctx_add_summ_freq(c, 4); + self.st_add_freq(s, 4); + if self.st_freq(s) > MAX_FREQ { + self.rescale(); + } + self.run_length = self.init_rl; + self.update_model(); + } + + // ─── decode ───────────────────────────────────────────────────────── + + /// Decode one byte symbol. `Err(Corrupt)` on model/stream inconsistency + /// (the reference's `-1`/`-2` returns) or arena OOB. + pub(crate) fn decode_symbol(&mut self, rc: &mut RangeDec) -> Result { + let mut char_mask = [0u8; 256]; + + if self.ctx_num_stats(self.min_context) != 1 { + let mut s = self.ctx_stats(self.min_context); + let count = rc.get_threshold(self.ctx_summ_freq(self.min_context)); + let mut hi_cnt = self.st_freq(s) as u32; + if count < hi_cnt { + rc.decode(0, hi_cnt); + self.found_state = s; + let sym = self.st_symbol(s); + self.update1_0(); + return self.finish(sym); + } + self.prev_success = 0; + let mut i = self.ctx_num_stats(self.min_context) - 1; + loop { + s += 6; + let f = self.st_freq(s) as u32; + hi_cnt += f; + if hi_cnt > count { + rc.decode(hi_cnt - f, f); + self.found_state = s; + let sym = self.st_symbol(s); + self.update1(); + return self.finish(sym); + } + i -= 1; + if i == 0 { + break; + } + } + if count >= self.ctx_summ_freq(self.min_context) { + return Err(Error::Corrupt); + } + self.hi_bits_flag = self.hb2flag[self.st_symbol(self.found_state) as usize] as u32; + rc.decode(hi_cnt, self.ctx_summ_freq(self.min_context) - hi_cnt); + for m in char_mask.iter_mut() { + *m = 0xFF; + } + char_mask[self.st_symbol(s) as usize] = 0; + let mut i = self.ctx_num_stats(self.min_context) - 1; + while i != 0 { + s -= 6; + char_mask[self.st_symbol(s) as usize] = 0; + i -= 1; + } + } else { + let (row, col) = self.bin_summ_index(); + let prob = self.bin_summ[row][col] as u32; + let bit = rc.decode_bit(prob); + if self.err || rc.err() { + return Err(Error::Corrupt); + } + if bit == 0 { + self.bin_summ[row][col] = (prob + (1 << PPMD_INT_BITS) - get_mean(prob)) as u16; + let s = Self::one_state(self.min_context); + self.found_state = s; + let sym = self.st_symbol(s); + self.update_bin(); + return self.finish(sym); + } + let newp = prob - get_mean(prob); + self.bin_summ[row][col] = newp as u16; + self.init_esc = K_EXP_ESCAPE[(newp >> 10) as usize & 0xF] as u32; + for m in char_mask.iter_mut() { + *m = 0xFF; + } + let os = Self::one_state(self.min_context); + char_mask[self.st_symbol(os) as usize] = 0; + self.prev_success = 0; + } + + // Escape loop. + loop { + if self.err || rc.err() { + return Err(Error::Corrupt); + } + let mut num_masked = self.ctx_num_stats(self.min_context); + loop { + self.order_fall += 1; + let suffix = self.ctx_suffix(self.min_context); + if suffix == 0 { + return Err(Error::Corrupt); + } + self.min_context = suffix; + if self.ctx_num_stats(self.min_context) != num_masked { + break; + } + } + let mut hi_cnt = 0u32; + let mut s = self.ctx_stats(self.min_context); + let num = self.ctx_num_stats(self.min_context) - num_masked; + let mut ps: [u32; 256] = [0; 256]; + let mut i = 0usize; + loop { + let sym = self.st_symbol(s) as usize; + let masked = char_mask[sym]; + if masked != 0 { + hi_cnt += self.st_freq(s) as u32; + ps[i] = s; + i += 1; + } + s += 6; + if i as u32 == num { + break; + } + if self.err { + return Err(Error::Corrupt); + } + } + + let (see_ref, mut freq_sum) = self.make_esc_freq(num_masked); + freq_sum += hi_cnt; + let count = rc.get_threshold(freq_sum); + + if count < hi_cnt { + let mut acc = 0u32; + let mut idx = 0usize; + loop { + acc += self.st_freq(ps[idx]) as u32; + if acc > count { + break; + } + idx += 1; + } + let sstate = ps[idx]; + let f = self.st_freq(sstate) as u32; + rc.decode(acc - f, f); + self.see_update(see_ref); + self.found_state = sstate; + let sym = self.st_symbol(sstate); + self.update2(); + return self.finish(sym); + } + if count >= freq_sum { + return Err(Error::Corrupt); + } + rc.decode(hi_cnt, freq_sum - hi_cnt); + self.see_add_summ(see_ref, freq_sum); + let mut j = i; + while j != 0 { + j -= 1; + char_mask[self.st_symbol(ps[j]) as usize] = 0; + } + num_masked = self.ctx_num_stats(self.min_context); + let _ = num_masked; + } + } + + #[inline] + fn finish(&mut self, sym: u8) -> Result { + if self.err { + Err(Error::Corrupt) + } else { + Ok(sym) + } + } + + /// `Ppmd7_GetBinSumm` index computation (also sets `hi_bits_flag`). + fn bin_summ_index(&mut self) -> (usize, usize) { + let os = Self::one_state(self.min_context); + let os_freq = self.st_freq(os); + let suffix = self.ctx_suffix(self.min_context); + let suffix_ns = self.ctx_num_stats(suffix); + let found_sym = self.st_symbol(self.found_state); + self.hi_bits_flag = self.hb2flag[found_sym as usize] as u32; + let os_sym = self.st_symbol(os); + let row = (os_freq - 1) as usize; + let col = self.prev_success as usize + + self.ns2bsindx[(suffix_ns - 1) as usize] as usize + + self.hi_bits_flag as usize + + 2 * self.hb2flag[os_sym as usize] as usize + + ((self.run_length >> 26) & 0x20) as usize; + (row.min(127), col.min(63)) + } +} diff --git a/src/ppmd/range_dec.rs b/src/ppmd/range_dec.rs index 5f0a374..e3b4ad1 100644 --- a/src/ppmd/range_dec.rs +++ b/src/ppmd/range_dec.rs @@ -1,137 +1,197 @@ -//! Carry-less 7z range decoder used by PPMd variant H. +//! Carry-less range decoder for PPMd variant H, in both flavours: //! -//! Differences from the LZMA range decoder: +//! - **7z** (`Ppmd7z_RangeDec`) — used by 7-Zip's `PPMd` method and the +//! standalone `.ppmd` framing in this crate. Init consumes a leading +//! `0x00` byte plus four big-endian bytes; `decode` subtracts from +//! `code`; `Bottom == 0`. +//! - **RAR** (`PpmdRAR_RangeDec`) — used by RAR3/4 PPMd blocks. Init +//! consumes four big-endian bytes (no leading zero); `decode` adds to a +//! tracked `low`; `Bottom == 0x8000`; bit decoding routes through +//! `get_threshold` + `decode` rather than the 7z fast path. //! -//! - **No "first byte must be zero" rule** in the encoder/decoder -//! protocol per se, but the reference `Ppmd7z_RangeDec_Init` requires -//! the first byte read to be `0x00`. (Hand-rolled fixtures and every -//! 7z-/RAR-/ZIP-produced stream do start with `0x00`.) -//! - **`code` is initialised from the next four bytes** big-endian, and -//! `range` starts at `0xFFFF_FFFF`. The decoder calls -//! `Range_Normalize` which conditionally pulls one byte at a time and -//! handles two consecutive shifts. -//! - **`Range_GetThreshold(total)` divides `range /= total`** (mutating -//! `range`!), then returns `code / range`. The decoder uses that -//! quotient as the symbol index into the frequency table. -//! - **`Range_DecodeBit(size0)`** uses an explicit 14-bit shift -//! (`range >> 14`) and never updates probabilities (the model owns -//! that — see `PPMD_UPDATE_PROB_*`). -//! -//! Streaming: callers pull bytes from an internal buffered byte source. -//! When the decoder needs a byte and the buffer is empty, the symbol -//! decode aborts upward via `NeedInput` so the outer loop can refill. +//! Both share `get_threshold` (`range /= total; (code - low) / range`) and +//! the `low`/`bottom`-aware normalisation. Derived from the public-domain +//! `Ppmd7Dec.c` (LZMA SDK) and the RAR variant described in libarchive's +//! BSD RAR reader; no license-restricted code was copied. use crate::error::Error; const K_TOP_VALUE: u32 = 1 << 24; +const PPMD_BIN_SCALE: u32 = 1 << 14; +/// Safety cap on normalisation iterations (a well-formed stream needs at +/// most a few); prevents a crafted/truncated stream from spinning. +const MAX_NORMALIZE_STEPS: u32 = 64; + +#[derive(Clone, Copy, PartialEq, Eq)] +pub(crate) enum Mode { + SevenZip, + /// RAR3/4 PPMd blocks. Only constructed behind the `rar3` feature. + #[cfg_attr(not(feature = "rar3"), allow(dead_code))] + Rar, +} -/// Trait-free byte source so the decoder can be driven either from a -/// pre-buffered slice (during a decode call) or from a "we already read -/// past the end" sentinel during init. -pub(super) struct ByteSource<'a> { - pub buf: &'a [u8], - pub pos: usize, +pub(crate) struct RangeDec<'a> { + range: u32, + code: u32, + low: u32, + bottom: u32, + mode: Mode, + input: &'a [u8], + pos: usize, + err: bool, } -impl<'a> ByteSource<'a> { - pub fn new(buf: &'a [u8], pos: usize) -> Self { - Self { buf, pos } +impl<'a> RangeDec<'a> { + /// Construct and initialise from `input[start..]`. Returns the decoder + /// and the number of input bytes the init consumed. + pub(crate) fn init(mode: Mode, input: &'a [u8], start: usize) -> Result<(Self, usize), Error> { + let mut d = RangeDec { + range: 0xFFFF_FFFF, + code: 0, + low: 0, + bottom: 0, + mode, + input, + pos: start, + err: false, + }; + let consumed = match mode { + Mode::SevenZip => { + // First byte must be zero, then 4 big-endian bytes. + if input.len() < start + 5 { + return Err(Error::UnexpectedEnd); + } + if input[start] != 0 { + return Err(Error::Corrupt); + } + d.pos = start + 1; + for _ in 0..4 { + d.code = (d.code << 8) | d.read_byte() as u32; + } + d.bottom = 0; + 5 + } + Mode::Rar => { + if input.len() < start + 4 { + return Err(Error::UnexpectedEnd); + } + for _ in 0..4 { + d.code = (d.code << 8) | d.read_byte() as u32; + } + d.bottom = 0x8000; + 4 + } + }; + Ok((d, consumed)) } - /// Returns the next byte and advances `pos`. `Err(UnexpectedEnd)` - /// when starved — the outer streaming machinery is expected to - /// translate that into a `NeedInput` rollback by snapshotting state - /// before the symbol decode begins. #[inline] - pub fn read(&mut self) -> Result { - let b = *self.buf.get(self.pos).ok_or(Error::UnexpectedEnd)?; - self.pos += 1; - Ok(b) + pub(crate) fn err(&self) -> bool { + self.err } -} -#[derive(Clone, Debug)] -pub(super) struct RangeDec { - pub range: u32, - pub code: u32, - /// Position into the caller's buffered input where the next byte - /// will be read from. - pub pos: usize, -} + /// The stream read past the end of the available input. + #[inline] + pub(crate) fn overran(&self) -> bool { + self.pos > self.input.len() + } -impl RangeDec { - pub fn new() -> Self { - Self { - range: 0, - code: 0, - pos: 0, - } + #[inline] + fn read_byte(&mut self) -> u8 { + let b = self.input.get(self.pos).copied().unwrap_or(0); + // Advance regardless so `pos`/`overran` reflect demand; a real + // stream never reads past the last symbol's bytes. + self.pos += 1; + b } - /// Initialise from the first 5 bytes of the PPMd payload. First byte - /// must be zero; the remaining four are big-endian and form the - /// initial `code`. - /// - /// Returns `Ok(true)` on successful init, `Ok(false)` if input was - /// short. On `code == 0xFFFF_FFFF` (reference rejects it) returns - /// `Err(Corrupt)`. - pub fn init(&mut self, buf: &[u8]) -> Result { - if buf.len() < self.pos + 5 { - return Ok(false); - } - if buf[self.pos] != 0 { - return Err(Error::Corrupt); + /// `range /= total; return (code - low) / range`. + #[inline] + pub(crate) fn get_threshold(&mut self, total: u32) -> u32 { + if total == 0 { + self.err = true; + return 0; } - let b1 = buf[self.pos + 1] as u32; - let b2 = buf[self.pos + 2] as u32; - let b3 = buf[self.pos + 3] as u32; - let b4 = buf[self.pos + 4] as u32; - self.code = (b1 << 24) | (b2 << 16) | (b3 << 8) | b4; - self.range = 0xFFFF_FFFF; - self.pos += 5; - if self.code == 0xFFFF_FFFF { - return Err(Error::Corrupt); + self.range /= total; + if self.range == 0 { + self.err = true; + return 0; } - Ok(true) + self.code.wrapping_sub(self.low) / self.range } - /// `range /= total; return code / range`. Mutates `self.range`. + /// Advance past a decoded interval `[start, start+size)`. #[inline] - pub fn get_threshold(&mut self, total: u32) -> u32 { - self.range /= total; - self.code / self.range + pub(crate) fn decode(&mut self, start: u32, size: u32) { + match self.mode { + Mode::SevenZip => { + self.code = self.code.wrapping_sub(start.wrapping_mul(self.range)); + } + Mode::Rar => { + self.low = self.low.wrapping_add(start.wrapping_mul(self.range)); + } + } + self.range = self.range.wrapping_mul(size); + self.normalize(); } - /// `range *= size`; advance `code` by `start * range_before`. - /// `range` has already been divided by `total` by `get_threshold`. + /// Decode one binary decision with probability `size0` (out of + /// `PPMD_BIN_SCALE`). Returns the bit. #[inline] - pub fn decode(&mut self, src: &mut ByteSource<'_>, start: u32, size: u32) -> Result<(), Error> { - self.code = self.code.wrapping_sub(start.wrapping_mul(self.range)); - self.range = self.range.wrapping_mul(size); - self.normalize(src) + pub(crate) fn decode_bit(&mut self, size0: u32) -> u32 { + match self.mode { + Mode::SevenZip => { + let new_bound = (self.range >> 14) * size0; + if self.code < new_bound { + self.range = new_bound; + self.normalize(); + 0 + } else { + self.code -= new_bound; + self.range -= new_bound; + self.normalize(); + 1 + } + } + Mode::Rar => { + let value = self.get_threshold(PPMD_BIN_SCALE); + if value < size0 { + self.decode(0, size0); + 0 + } else { + self.decode(size0, PPMD_BIN_SCALE - size0); + 1 + } + } + } } - /// Pull bytes while `range < 1<<24`. The reference loops at most - /// twice (`range` is shifted by 8 per iteration, so two iterations - /// take `range` from anywhere in `1..1<<24` to `>=1<<24`). #[inline] - pub fn normalize(&mut self, src: &mut ByteSource<'_>) -> Result<(), Error> { - if self.range < K_TOP_VALUE { - self.code = (self.code << 8) | src.read()? as u32; + fn normalize(&mut self) { + let mut steps = 0u32; + loop { + if (self.low ^ self.low.wrapping_add(self.range)) >= K_TOP_VALUE { + if self.range >= self.bottom { + break; + } + // range too small: clamp to the bottom window (RAR path). + self.range = self.low.wrapping_neg() & self.bottom.wrapping_sub(1); + } + self.code = (self.code << 8) | self.read_byte() as u32; self.range <<= 8; - if self.range < K_TOP_VALUE { - self.code = (self.code << 8) | src.read()? as u32; - self.range <<= 8; + self.low <<= 8; + steps += 1; + if steps > MAX_NORMALIZE_STEPS { + self.err = true; + break; } } - Ok(()) } - /// Reference's `Ppmd7z_RangeDec_IsFinishedOK`. After draining the - /// final symbol the encoder leaves `code == 0`; anything else means - /// the stream was truncated or the model dropped a symbol. + /// 7z terminal check: after the final symbol the encoder leaves + /// `code == 0`. #[inline] - pub fn is_finished_ok(&self) -> bool { + pub(crate) fn is_finished_ok(&self) -> bool { self.code == 0 } } diff --git a/src/rar3/bits.rs b/src/rar3/bits.rs index 5c9ee94..150ce00 100644 --- a/src/rar3/bits.rs +++ b/src/rar3/bits.rs @@ -102,6 +102,17 @@ impl BitReader { let _ = self.drop_bits(drop); } } + + /// Number of source bytes logically consumed so far. Only meaningful on + /// a byte boundary (call [`byte_align`] first). Used by the PPMd path to + /// hand the raw byte stream after the block header to the RAR range + /// decoder. + pub fn consumed_bytes(&self) -> usize { + // `byte_pos` is the next byte to pull into `acc`; `nbits` bits are + // buffered ahead but unconsumed. On a byte boundary `nbits` is a + // multiple of 8. + self.byte_pos - (self.nbits as usize) / 8 + } } #[cfg(test)] diff --git a/src/rar3/decoder.rs b/src/rar3/decoder.rs index db19ca5..5d4b2d5 100644 --- a/src/rar3/decoder.rs +++ b/src/rar3/decoder.rs @@ -14,33 +14,37 @@ //! //! ## What's supported //! -//! - Non-PPMd blocks (the "LZ77 + Huffman" path used by the vast majority -//! of RAR3 archives). -//! - All five Huffman codes (precode + main + offset + low-offset + length). -//! - The 4-deep rolling-offset buffer, short offsets (codes 263..=270), and -//! the full match-length / offset machinery. -//! - The keep-table flag — successive blocks may reuse the previous code -//! lengths. +//! - The **LZ77 + Huffman path** used by the vast majority of RAR3 +//! archives: all five Huffman codes (precode, main, offset, low-offset, +//! length), the 4-deep rolling-offset buffer, short offsets (codes 263 +//! through 270), the full match-length / offset machinery, and the +//! keep-table flag (successive blocks reusing the previous code lengths). //! - **In-band standard filters** (main symbol 257): Delta and x86 //! E8/E8E9 declarations are recognized by their bytecode fingerprint and //! run natively over their declared output windows — see //! `super::filters` for the recognition scheme and provenance. +//! - **PPMd-II variant H blocks** (bit-0 of the block header): the full +//! PPMII model in [`crate::ppmd`] driven by the RAR range decoder, with +//! the RAR escape layer (literals, LZ matches, end-of-data) on top. A +//! single self-contained block decodes end to end — see +//! [`run_ppmd_block`]. //! - The standalone E8/E9 post-pass filter when enabled via //! [`Decoder::with_e8_filter`]. //! //! ## What's refused //! -//! - **PPMd-II blocks** (the bit-0 flag in the block header). PPMd-II is a -//! ~1500-line context-mixed arithmetic coder; implementing it faithfully -//! is out of scope for this build. Streams containing a PPMd block fail -//! with `Error::Unsupported`. +//! - **PPMd continuations across a new-table boundary** (a PPMd block that +//! reuses a still-live model from a previous block, i.e. no fresh +//! memory/order flag, or a `start-new-table` control code mid-stream). +//! These arise only in solid multi-member streams — out of scope here — +//! and fail with `Error::Unsupported`. //! - **Filter declarations carrying any other VM program** (custom //! bytecode, or legacy standard programs no current archiver emits — //! Itanium, RGB, the audio predictor). These fail with //! `Error::Unsupported` rather than interpreting RarVM bytecode. -//! - **Dictionary sizes** other than the default 4 MiB. Streams compressed -//! with smaller dictionaries decode correctly with the larger window — -//! the larger window doesn't change semantics. +//! - **Dictionary sizes** other than the default 4 MiB for the LZ path. +//! Streams compressed with smaller dictionaries decode correctly with the +//! larger window — the larger window doesn't change semantics. use alloc::boxed::Box; use alloc::collections::VecDeque; @@ -60,6 +64,7 @@ use super::tables::{ LOW_OFFSET_SIZE, MAIN_SIZE, OFFSET_BASE, OFFSET_EXTRA_BITS, OFFSET_SIZE, PRECODE_SIZE, SHORT_BASE, SHORT_EXTRA_BITS, }; +use crate::ppmd::{Ppmd7, RangeDec, RangeMode}; /// Streaming RAR 3.x decoder. See module docs for the calling convention. pub struct Decoder { @@ -272,11 +277,16 @@ fn run_decode( programs: Vec::new(), last_filter_slot: 0, pending_filters: VecDeque::new(), + ppmd: None, }); // The decoder starts by parsing the first block header. parse_block_header(&mut ctx)?; - expand(&mut ctx)?; + if let Some(hdr) = ctx.ppmd.take() { + run_ppmd_block(&mut ctx, &input, hdr)?; + } else { + expand(&mut ctx)?; + } // Run any in-band filters whose windows the stream completed. A filter // still pending here declared a window the stream never finished @@ -334,6 +344,25 @@ struct RunCtx { /// popped from the front) as soon as their windows are fully decoded — /// see [`RunCtx::flush_completed_filters`]. pending_filters: VecDeque, + /// Set when the first block header selected the PPMd path; carries the + /// parameters needed to drive the PPMd model over the raw byte stream. + ppmd: Option, +} + +/// Parameters lifted from a PPMd block header (bit-0 of the block header +/// set). See [`parse_block_header`]. +struct PpmdHeader { + /// Suballocator size in bytes (`(mem + 1) << 20`). + mem_size: u32, + /// Model max order. + max_order: u32, + /// The RAR-layer escape byte (a decoded symbol equal to this introduces + /// a control code rather than a literal). + escape: u8, + /// Explicit `InitEsc` seed if the header carried one. + init_esc: Option, + /// Byte offset in the input where the range-coded payload begins. + payload_start: usize, } /// A declared filter program plus its per-slot remembered block length. @@ -451,13 +480,45 @@ fn parse_block_header(ctx: &mut RunCtx) -> Result<(), Error> { // including the very first one (where alignment is a no-op since we // start on a byte boundary). ctx.bits.byte_align(); - // 1 bit: PPMd-block flag. We reject PPMd unconditionally. + // 1 bit: PPMd-block flag. let is_ppmd = ctx.bits.read_bits(1)?; if is_ppmd != 0 { - // PPMd-II would consume 7 more flag bits and possibly 2 more - // bytes here; we don't bother reading them since we're refusing - // the stream. - return Err(Error::Unsupported); + // PPMd block header: 7 flag bits, then (per flags) a memory byte, + // an escape byte, and an order derived from the flags. + // flag 0x20: read 8-bit mem → suballocator = (mem+1)<<20, and + // order = (flags & 0x1F) + 1 (values > 16 expand as + // 16 + (order-16)*3). + // flag 0x40: read 8-bit escape/InitEsc seed (else escape = 2). + // A header without 0x20 is a continuation reusing the live model — + // not produced for a standalone first block, so we refuse it. + let flags = ctx.bits.read_bits(7)?; + if flags & 0x20 == 0 { + return Err(Error::Unsupported); + } + let mem_mb = ctx.bits.read_bits(8)?; + let mem_size = (mem_mb + 1).saturating_mul(1 << 20); + let mut max_order = (flags & 0x1F) + 1; + if max_order > 16 { + max_order = 16 + (max_order - 16) * 3; + } + if max_order < 2 { + return Err(Error::Corrupt); + } + let (escape, init_esc) = if flags & 0x40 != 0 { + let e = ctx.bits.read_bits(8)? as u8; + (e, Some(e)) + } else { + (2u8, None) + }; + ctx.bits.byte_align(); + ctx.ppmd = Some(PpmdHeader { + mem_size, + max_order, + escape, + init_esc, + payload_start: ctx.bits.consumed_bytes(), + }); + return Ok(()); } // 1 bit: keep-table flag. 0 ⇒ reset the persistent length table. let keep_table = ctx.bits.read_bits(1)? != 0; @@ -739,6 +800,76 @@ fn expand(ctx: &mut RunCtx) -> Result<(), Error> { } } +// ─── PPMd-II variant H block ───────────────────────────────────────────── + +/// Drive the RAR PPMd block: the range-coded payload (starting at +/// `hdr.payload_start` in `input`) feeds the shared [`Ppmd7`] model through +/// the RAR range decoder. Decoded byte symbols are literals unless they +/// equal the escape byte, which introduces a control code (end-of-data, an +/// LZ match, a new table, or a literal-escape). Matches copy through the +/// same sliding window as the LZ path so they interleave seamlessly. +fn run_ppmd_block(ctx: &mut RunCtx, input: &[u8], hdr: PpmdHeader) -> Result<(), Error> { + let mut model = Ppmd7::new(hdr.mem_size)?; + model.init(hdr.max_order); + if let Some(e) = hdr.init_esc { + model.set_init_esc(e as u32); + } + if hdr.payload_start > input.len() { + return Err(Error::UnexpectedEnd); + } + let (mut rc, _) = RangeDec::init(RangeMode::Rar, input, hdr.payload_start)?; + + let sym = |m: &mut Ppmd7, rc: &mut RangeDec| -> Result { + let s = m.decode_symbol(rc)?; + if rc.err() { + return Err(Error::Corrupt); + } + Ok(s) + }; + + while !ctx.done() { + let s = sym(&mut model, &mut rc)?; + if s != hdr.escape { + ctx.emit_literal(s); + continue; + } + let code = sym(&mut model, &mut rc)?; + match code { + 0 => { + // start-new-table: a fresh block header follows. Supporting + // a mid-stream codec switch (back to Huffman, or a new PPMd + // table) is out of scope; no single-file corpus archive + // reaches this before the unpacked size is met. + return Err(Error::Unsupported); + } + 2 => break, // end of PPMd data + 3 => return Err(Error::Unsupported), // VM filter in PPMd stream + 4 => { + // 24-bit distance from three symbols (big-endian), then a + // length symbol. Distance +2, length +32. + let mut dist = 0u32; + for i in (0..3).rev() { + let b = sym(&mut model, &mut rc)? as u32; + dist |= b << (i * 8); + } + let len = sym(&mut model, &mut rc)? as u32; + ctx.emit_match(dist + 2, len + 32)?; + } + 5 => { + // Distance-1 run: length symbol, length +4. + let len = sym(&mut model, &mut rc)? as u32; + ctx.emit_match(1, len + 4)?; + } + _ => { + // Any other control code encodes a literal equal to the + // escape byte (the control symbol is consumed and dropped). + ctx.emit_literal(hdr.escape); + } + } + } + Ok(()) +} + // ─── In-band filter declarations (main symbol 257) ────────────────────── /// Upper bound on a filter's block length, derived from the RarVM memory @@ -1012,6 +1143,7 @@ mod tests { programs: vec![], last_filter_slot: 0, pending_filters: VecDeque::new(), + ppmd: None, } } @@ -1299,6 +1431,7 @@ mod tests { programs: vec![], last_filter_slot: 0, pending_filters: VecDeque::new(), + ppmd: None, }; // Promote slot 2 (value 30) — result should be [30, 10, 20, 40]. promote_offset(&mut ctx, 2, 30); diff --git a/src/rar3/mod.rs b/src/rar3/mod.rs index 9cd22a4..0e9a3d6 100644 --- a/src/rar3/mod.rs +++ b/src/rar3/mod.rs @@ -21,10 +21,13 @@ //! This build implements the **LZ77 + Huffman path** in full, including the //! in-band standard filters WinRAR declares via main symbol 257 (Delta and //! x86 E8/E8E9, recognized by bytecode fingerprint and run natively — no -//! RarVM interpreter; unknown programs are refused). PPMd-II blocks are -//! refused with `Error::Unsupported` — see the private `decoder` submodule -//! for details and limitations. The standalone E8/E9 (x86 near-call) -//! post-pass filter can also be enabled via [`Decoder::with_e8_filter`]. +//! RarVM interpreter; unknown programs are refused), and **PPMd-II variant +//! H** blocks (the full PPMII model in [`crate::ppmd`], driven by the RAR +//! range decoder with the RAR literal/match/end-of-data escape layer). The +//! standalone E8/E9 (x86 near-call) post-pass filter can also be enabled +//! via [`Decoder::with_e8_filter`]. PPMd continuations across a new-table +//! boundary (solid multi-member streams) are refused — see the private +//! `decoder` submodule for the exact boundary. //! //! ## Calling convention //! diff --git a/tests/fixtures/ppmd/english.bin b/tests/fixtures/ppmd/english.bin new file mode 100644 index 0000000..41a4d51 --- /dev/null +++ b/tests/fixtures/ppmd/english.bin @@ -0,0 +1 @@ +the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. the quick brown fox jumps over lazy dog. \ No newline at end of file diff --git a/tests/fixtures/ppmd/english.ppmd b/tests/fixtures/ppmd/english.ppmd new file mode 100644 index 0000000000000000000000000000000000000000..2a754a3ef0b533f3346c39e08360b67481800d4a GIT binary patch literal 78 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 literal 0 HcmV?d00001 diff --git a/tests/fixtures/ppmd/text.bin b/tests/fixtures/ppmd/text.bin new file mode 100644 index 0000000..13d0586 --- /dev/null +++ b/tests/fixtures/ppmd/text.bin @@ -0,0 +1 @@ +PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. PPMd context modelling test. \ No newline at end of file diff --git a/tests/fixtures/ppmd/text.ppmd b/tests/fixtures/ppmd/text.ppmd new file mode 100644 index 0000000000000000000000000000000000000000..8fce48a3fa2f613d045cca31f095df1eb5765636 GIT binary patch literal 56 xcmZP*U=#-eDKtk{J2%A?pvVR?!yesS^5?mQY1d3IMr&4c`C& literal 0 HcmV?d00001 diff --git a/tests/fixtures/rar3/ppmd_notes.bin b/tests/fixtures/rar3/ppmd_notes.bin new file mode 100644 index 0000000000000000000000000000000000000000..e1fd2a1ff59dcdf96548000a453b34855acd7586 GIT binary patch literal 92 zcmV-i0Hgos82?ljJBwm68Hpu*r1g;f3N3?cOFcd7t01jk{6;XRrT+tDtou8)`Y1x6 y<8$6hE*UjR_FSbyGb`|_A|cWn_sHKzcmq*BwbUzeaBKiU!~g&Qzldk{ssGf>zAIAz literal 0 HcmV?d00001 diff --git a/tests/ppmd.rs b/tests/ppmd.rs index e9578b4..68c877d 100644 --- a/tests/ppmd.rs +++ b/tests/ppmd.rs @@ -1,24 +1,32 @@ -//! Integration tests for the PPMd decoder. +//! Integration tests for the PPMd (PPMII variant H) decoder. //! -//! PPMd is decoder-only in this crate (encoder always returns -//! [`Error::Unsupported`]) and the decoder implements *only* the -//! order-0 subset of PPMII variant H (see `src/ppmd/mod.rs` for the -//! documented gap). These tests: -//! -//! - confirm the algorithm metadata and Encoder-side Unsupported contract; -//! - decode hand-built order-0 fixtures (generated by the small encoder -//! helper in this file, which mirrors the decoder's order-0 model); -//! - exercise streaming with 1-byte input chunking; -//! - reject truncated input without panicking; -//! - confirm garbage doesn't panic; -//! - confirm `reset()` puts the decoder back to the Header phase; -//! - exercise the `#[cfg(feature = "factory")]` by-name lookup. +//! PPMd is decoder-only in this crate (the encoder always returns +//! [`Error::Unsupported`]). The round-trip fixtures are **real 7z Ppmd7 +//! streams** produced by `pyppmd.Ppmd7Encoder(order=6, mem=16 MiB)` and +//! wrapped in this crate's 11-byte framing header (order, mem_mb, +//! restoration, u64 length). They live in `tests/fixtures/ppmd/` alongside +//! their expected plaintext (`.bin`); see +//! `compcol-rar-corpus/probe/gen_ppmd_fixtures.py` for how they were +//! generated. Decoding them byte-for-byte exercises the full model: +//! context-tree construction, the binary-context path, masked escapes, SEE, +//! rescale, and the suballocator. #![cfg(feature = "ppmd")] use compcol::ppmd::{Decoder, Encoder, Ppmd}; use compcol::{Algorithm, Decoder as _, Encoder as _, Error, Status}; +// ─── fixtures ───────────────────────────────────────────────────────────── + +macro_rules! fixture { + ($name:literal) => { + ( + include_bytes!(concat!("fixtures/ppmd/", $name, ".ppmd")) as &[u8], + include_bytes!(concat!("fixtures/ppmd/", $name, ".bin")) as &[u8], + ) + }; +} + // ─── helpers ───────────────────────────────────────────────────────────── /// Build a framing header: order, mem_mb, restoration, len_le_u64. @@ -31,8 +39,8 @@ fn make_header(order: u8, mem_mb: u8, restoration: u8, len: u64) -> Vec { h } -/// Drive the decoder to completion, using a small output buffer to -/// exercise the OutputFull/InputEmpty back-pressure paths. +/// Drive the decoder to completion using a small output buffer to exercise +/// the OutputFull / InputEmpty back-pressure paths. fn drive_to_end(dec: &mut Decoder, input: &[u8]) -> Result, Error> { let mut out = Vec::new(); let mut buf = vec![0u8; 4096]; @@ -55,12 +63,8 @@ fn drive_to_end(dec: &mut Decoder, input: &[u8]) -> Result, Error> { break; } spin += 1; - if spin > 100_000 { - panic!( - "decoder spin (consumed={}, out_len={})", - consumed, - out.len() - ); + if spin > 1_000_000 { + panic!("decoder spin (consumed={consumed}, out_len={})", out.len()); } } loop { @@ -76,164 +80,6 @@ fn drive_to_end(dec: &mut Decoder, input: &[u8]) -> Result, Error> { Ok(out) } -// ─── reference order-0 encoder (test-only) ─────────────────────────────── -// -// The public PPMd encoder is permanently Unsupported. To exercise the -// decoder we need byte-perfect fixtures. We embed a tiny test-only -// order-0 PPMII encoder that mirrors the model in `src/ppmd/model.rs` -// (same +4 frequency increment, same MAX_FREQ rescale, same swap-with- -// predecessor promotion) and emits via the 7z carry-less range encoder. -// This keeps the production crate decoder-only while still letting -// `cargo test` round-trip without external tools. - -const MAX_FREQ: u32 = 124; - -struct OrderZeroModel { - // 256 (symbol, freq) entries kept roughly sorted by freq desc. - states: Vec<(u8, u32)>, - summ_freq: u32, -} - -impl OrderZeroModel { - fn new() -> Self { - let states: Vec<(u8, u32)> = (0..=255).map(|s| (s as u8, 1)).collect(); - Self { - states, - summ_freq: 256 + 1, - } - } - - fn find(&self, sym: u8) -> usize { - self.states - .iter() - .position(|&(s, _)| s == sym) - .expect("order-0") - } - - /// Returns (cum, freq, total). - fn encode_lookup(&self, sym: u8) -> (u32, u32, u32) { - let mut acc = 0u32; - for &(s, f) in &self.states { - if s == sym { - return (acc, f, self.summ_freq); - } - acc += f; - } - unreachable!() - } - - fn update(&mut self, sym: u8) { - let i = self.find(sym); - let new_f = self.states[i].1 + 4; - if new_f > MAX_FREQ { - self.rescale(); - return; - } - self.states[i].1 = new_f; - self.summ_freq += 4; - if i > 0 && new_f > self.states[i - 1].1 { - self.states.swap(i, i - 1); - } - } - - fn rescale(&mut self) { - let mut new_summ = 0u32; - for s in &mut self.states { - s.1 = ((s.1 + 1) >> 1).max(1); - new_summ += s.1; - } - self.summ_freq = new_summ; - } -} - -struct RangeEnc { - low: u64, - range: u32, - cache_size: u32, - cache: u8, - out: Vec, -} - -impl RangeEnc { - fn new() -> Self { - Self { - low: 0, - range: 0xFFFF_FFFF, - cache_size: 1, - cache: 0, - out: Vec::new(), - } - } - - /// Encode `(start, size, total)`: `range /= total; low += start*range; range *= size;`. - fn encode(&mut self, start: u32, size: u32, total: u32) { - self.range /= total; - self.low = self.low.wrapping_add(start as u64 * self.range as u64); - self.range = self.range.wrapping_mul(size); - self.normalize(); - } - - fn normalize(&mut self) { - while self.range < (1 << 24) { - self.shift_low(); - self.range <<= 8; - } - } - - fn shift_low(&mut self) { - // Pavlov's PPMd 7z `ShiftLow`. - if (self.low as u32) < 0xFF00_0000 || self.low >> 32 != 0 { - let mut temp = self.cache; - loop { - self.out.push(temp.wrapping_add((self.low >> 32) as u8)); - temp = 0xFF; - self.cache_size -= 1; - if self.cache_size == 0 { - break; - } - } - self.cache = (self.low as u32 >> 24) as u8; - } - self.cache_size += 1; - self.low = (self.low << 8) & 0xFFFF_FFFF; - } - - fn flush(mut self) -> Vec { - for _ in 0..5 { - self.shift_low(); - } - self.out - } -} - -fn encode_payload(input: &[u8]) -> Vec { - // 7z PPMd format: payload starts with a leading 0x00 byte that the - // decoder consumes during `init`. - let mut m = OrderZeroModel::new(); - let mut e = RangeEnc::new(); - for &b in input { - let (start, size, total) = m.encode_lookup(b); - e.encode(start, size, total); - m.update(b); - } - let mut bytes = e.flush(); - // The first byte of the encoded stream is the high-order byte that - // `shift_low`'s first iteration emits *after* `cache` (a leading 0). - // The carry-less PPMd-7z stream always starts with `0x00`, which the - // decoder requires (`Ppmd7z_RangeDec_Init`). We prepend `0x00` if our - // shift_low didn't already emit one. - if bytes.first() != Some(&0) { - bytes.insert(0, 0); - } - bytes -} - -fn make_stream(input: &[u8], order: u8) -> Vec { - let mut s = make_header(order, 1, 0, input.len() as u64); - s.extend_from_slice(&encode_payload(input)); - s -} - // ─── algorithm metadata ────────────────────────────────────────────────── #[test] @@ -279,65 +125,54 @@ fn encoder_reset_is_a_noop() { assert_eq!(enc.encode(b"x", &mut out).unwrap_err(), Error::Unsupported); } -// ─── round-trip via the test-only order-0 encoder ─────────────────────── +// ─── round-trip against real Ppmd7 fixtures ────────────────────────────── -#[test] -fn rt_empty() { - let stream = make_stream(b"", 4); +fn check(stream: &[u8], expected: &[u8]) { let mut dec = Decoder::new(); - let out = drive_to_end(&mut dec, &stream).unwrap(); - assert_eq!(out, b""); + let out = drive_to_end(&mut dec, stream).unwrap(); + assert_eq!(out.len(), expected.len(), "length mismatch"); + assert_eq!(out, expected, "byte mismatch"); } #[test] -fn rt_single_byte() { - let stream = make_stream(b"A", 4); - let mut dec = Decoder::new(); - let out = drive_to_end(&mut dec, &stream).unwrap(); - assert_eq!(out, b"A"); +fn rt_hello() { + let (stream, expected) = fixture!("hello"); + check(stream, expected); } #[test] -fn rt_hello_world() { - let stream = make_stream(b"hello world", 6); - let mut dec = Decoder::new(); - let out = drive_to_end(&mut dec, &stream).unwrap(); - assert_eq!(out, b"hello world"); +fn rt_repeat() { + // Highly repetitive text — heavy match / context reuse. + let (stream, expected) = fixture!("repeat"); + check(stream, expected); } #[test] -fn rt_64k_repeating() { - let mut payload = Vec::with_capacity(64 * 1024); - let pattern = b"the quick brown fox jumps over the lazy dog "; - while payload.len() < 64 * 1024 { - payload.extend_from_slice(pattern); - } - payload.truncate(64 * 1024); - let stream = make_stream(&payload, 8); - let mut dec = Decoder::new(); - let out = drive_to_end(&mut dec, &stream).unwrap(); - assert_eq!(out, payload); +fn rt_text() { + let (stream, expected) = fixture!("text"); + check(stream, expected); } #[test] -fn rt_mixed_corpus() { - let mut payload = Vec::new(); - payload.extend_from_slice(b"ASCII prefix.\n"); - payload.extend((0u8..=255u8).cycle().take(4096)); - payload.extend_from_slice(b"\nASCII suffix."); - let stream = make_stream(&payload, 4); - let mut dec = Decoder::new(); - let out = drive_to_end(&mut dec, &stream).unwrap(); - assert_eq!(out, payload); +fn rt_english() { + // Word-structured input — exercises the model's typical operating point. + let (stream, expected) = fixture!("english"); + check(stream, expected); +} + +#[test] +fn rt_mixed_high_entropy() { + // 20 KiB of deterministic pseudo-random bytes — drives the escape / + // masked-suffix / SEE paths and rescales hard. + let (stream, expected) = fixture!("mixed"); + check(stream, expected); } // ─── streaming: one byte at a time ─────────────────────────────────────── #[test] fn streaming_one_byte_at_a_time() { - let payload = b"the quick brown fox"; - let stream = make_stream(payload, 4); - + let (stream, expected) = fixture!("english"); let mut dec = Decoder::new(); let mut out = Vec::new(); let mut buf = [0u8; 128]; @@ -351,10 +186,11 @@ fn streaming_one_byte_at_a_time() { consumed += p.consumed; out.extend_from_slice(&buf[..p.written]); if matches!(status, Status::StreamEnd) { - return assert_eq!(out, payload); + assert_eq!(out, expected); + return; } spin += 1; - if spin > 100_000 { + if spin > 1_000_000 { panic!("streaming spin"); } } @@ -365,77 +201,81 @@ fn streaming_one_byte_at_a_time() { break; } } - assert_eq!(out, payload); + assert_eq!(out, expected); } // ─── error cases ───────────────────────────────────────────────────────── #[test] -fn truncated_header_returns_input_empty() { +fn truncated_header_returns_unexpected_end_on_finish() { let mut dec = Decoder::new(); let mut buf = [0u8; 16]; - let (p, status) = dec.decode(&[4, 1, 0, 0, 0], &mut buf).unwrap(); + let (p, _status) = dec.decode(&[6, 16, 0, 0, 0], &mut buf).unwrap(); assert_eq!(p.consumed, 5); - assert!(matches!(status, Status::InputEmpty)); - // finish should fail UnexpectedEnd. let r = dec.finish(&mut buf); assert_eq!(r, Err(Error::UnexpectedEnd)); } #[test] fn header_order_too_small_is_bad_header() { + let (stream, _) = fixture!("hello"); + let mut bad = make_header(1, 16, 0, 11); + bad.extend_from_slice(&stream[11..]); let mut dec = Decoder::new(); let mut buf = [0u8; 16]; - let stream = make_header(1, 1, 0, 0); - let r = dec.decode(&stream, &mut buf); - assert_eq!(r, Err(Error::BadHeader)); + let _ = dec.decode(&bad, &mut buf); + assert_eq!(dec.finish(&mut buf), Err(Error::BadHeader)); } #[test] -fn header_order_too_large_is_bad_header() { +fn header_zero_mem_is_bad_header() { + let mut bad = make_header(6, 0, 0, 0); + bad.extend_from_slice(&[0, 0, 0, 0, 0]); let mut dec = Decoder::new(); let mut buf = [0u8; 16]; - let stream = make_header(17, 1, 0, 0); - let r = dec.decode(&stream, &mut buf); - assert_eq!(r, Err(Error::BadHeader)); + let _ = dec.decode(&bad, &mut buf); + assert_eq!(dec.finish(&mut buf), Err(Error::BadHeader)); } #[test] -fn header_zero_mem_is_bad_header() { +fn header_bad_restoration_is_bad_header() { + let mut bad = make_header(6, 16, 9, 0); + bad.extend_from_slice(&[0, 0, 0, 0, 0]); let mut dec = Decoder::new(); let mut buf = [0u8; 16]; - let stream = make_header(4, 0, 0, 0); - let r = dec.decode(&stream, &mut buf); - assert_eq!(r, Err(Error::BadHeader)); + let _ = dec.decode(&bad, &mut buf); + assert_eq!(dec.finish(&mut buf), Err(Error::BadHeader)); } #[test] -fn header_bad_restoration_is_bad_header() { +fn absurd_declared_length_is_rejected_not_oomed() { + // Regression: a fuzz-found stream with a valid header but a wildly + // oversized declared length (~71 quadrillion bytes) used to drive the + // known-length decode loop toward OOM — a high-probability PPMd symbol + // can decode repeatedly without consuming input, so the range coder + // never overruns. The decoder must reject it up front. + let mut bad = make_header(6, 16, 0, 71_213_169_107_795_979); + bad.extend_from_slice(&[0x00, 0xfd, 0x00, 0x00, 0x67, 0xfb, 0x83, 0x7d]); let mut dec = Decoder::new(); let mut buf = [0u8; 16]; - let stream = make_header(4, 1, 9, 0); - let r = dec.decode(&stream, &mut buf); - assert_eq!(r, Err(Error::BadHeader)); + let _ = dec.decode(&bad, &mut buf); + assert_eq!(dec.finish(&mut buf), Err(Error::OutputLimitExceeded)); } #[test] fn payload_first_byte_must_be_zero() { + let mut bad = make_header(6, 16, 0, 8); + bad.extend_from_slice(&[0xFF; 16]); let mut dec = Decoder::new(); let mut buf = [0u8; 16]; - let mut stream = make_header(4, 1, 0, 8); - // Payload starts with 0xFF — the range decoder rejects. - stream.extend_from_slice(&[0xFF; 16]); - let r = dec.decode(&stream, &mut buf); - assert_eq!(r, Err(Error::Corrupt)); + let _ = dec.decode(&bad, &mut buf); + assert_eq!(dec.finish(&mut buf), Err(Error::Corrupt)); } #[test] -fn truncated_payload_returns_unexpected_end_on_finish() { - // Build a known-good stream then chop bytes off the tail. - let payload = b"hello there ppmd world"; - let stream = make_stream(payload, 4); +fn truncated_payload_is_unexpected_end() { + let (stream, _) = fixture!("text"); let truncated = &stream[..stream.len() - 3]; - let mut dec = Decoder::new(); let mut buf = [0u8; 256]; let _ = dec.decode(truncated, &mut buf); @@ -445,12 +285,11 @@ fn truncated_payload_returns_unexpected_end_on_finish() { #[test] fn garbage_after_header_does_not_panic() { - let mut stream = make_header(4, 1, 0, 16); - stream.extend_from_slice(&[0u8; 4]); - stream.extend_from_slice(&[0xAA; 64]); // garbage entropy stream + let mut stream = make_header(6, 16, 0, 64); + stream.extend_from_slice(&[0u8; 1]); + stream.extend_from_slice(&[0xAA; 128]); let mut dec = Decoder::new(); let mut buf = [0u8; 32]; - // Don't care about the result — just that it doesn't panic. let _ = dec.decode(&stream, &mut buf); let _ = dec.finish(&mut buf); } @@ -459,30 +298,30 @@ fn garbage_after_header_does_not_panic() { #[test] fn reset_returns_to_header_phase() { - let stream = make_stream(b"reset test", 4); + let (stream, expected) = fixture!("hello"); let mut dec = Decoder::new(); - let out = drive_to_end(&mut dec, &stream).unwrap(); - assert_eq!(out, b"reset test"); + let out = drive_to_end(&mut dec, stream).unwrap(); + assert_eq!(out, expected); dec.reset(); - let out2 = drive_to_end(&mut dec, &stream).unwrap(); - assert_eq!(out2, b"reset test"); + let out2 = drive_to_end(&mut dec, stream).unwrap(); + assert_eq!(out2, expected); } #[test] fn reset_after_error_recovers() { let mut dec = Decoder::new(); let mut buf = [0u8; 16]; - // Bad header poisons the decoder. - let r = dec.decode(&[99, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0], &mut buf); - assert_eq!(r, Err(Error::BadHeader)); - // Without reset, further calls should error. + let mut bad = make_header(99, 16, 0, 0); + bad.extend_from_slice(&[0, 0, 0, 0, 0]); + let _ = dec.decode(&bad, &mut buf); + assert_eq!(dec.finish(&mut buf), Err(Error::BadHeader)); + // Poisoned until reset. assert!(dec.decode(b"x", &mut buf).is_err()); dec.reset(); - // After reset, the decoder is usable again. - let stream = make_stream(b"", 4); - let out = drive_to_end(&mut dec, &stream).unwrap(); - assert_eq!(out, b""); + let (stream, expected) = fixture!("hello"); + let out = drive_to_end(&mut dec, stream).unwrap(); + assert_eq!(out, expected); } // ─── factory (only if the feature is enabled) ──────────────────────────── diff --git a/tests/rar3.rs b/tests/rar3.rs index 64104ea..10c59b5 100644 --- a/tests/rar3.rs +++ b/tests/rar3.rs @@ -180,12 +180,15 @@ fn decodes_libarchive_test_txt_tight_output_buffer() { assert_eq!(out, TESTDIR_TEST_TXT_EXPECTED); } -// ─── PPMd rejection ────────────────────────────────────────────────────── +// ─── PPMd continuation rejection ───────────────────────────────────────── #[test] -fn ppmd_block_is_unsupported() { - // A block whose first bit (after byte-align) is 1 = PPMd flag. - // 0x80 = 1000_0000 → byte-aligned start, top bit = 1 ⇒ PPMd. +fn ppmd_continuation_without_model_is_unsupported() { + // Top bit set ⇒ PPMd block; the next 7 flag bits are 0, so flag 0x20 + // (fresh model + memory/order) is clear. That marks a continuation + // reusing a live model — which a standalone first block can't have, so + // we refuse it. (A real, self-contained PPMd block decodes — see + // `ppmd_block_decodes`.) let ppmd_marker = [0x80u8, 0x00, 0x00, 0x00]; let mut dec = Decoder::with_unpack_size(32); let (_p, _status) = dec.decode(&ppmd_marker, &mut []).unwrap(); @@ -370,6 +373,21 @@ fn unfinished_filter_window_is_corrupt() { assert_eq!(dec.finish(&mut buf).unwrap_err(), Error::Corrupt); } +// ─── PPMd-II variant H block (real-archive fixture) ────────────────────── +// +// The raw v29 payload of `notes.txt` from a `rar 6.24 -mct+` (forced +// text/PPMd) archive — a self-contained PPMd block (bit-0 of the block +// header set) that decodes to 20001 bytes. Expected CRC-32 is the archive's +// own FILE_CRC; extraction was cross-checked byte-identical against UnRAR +// 7.23. This exercises the RAR range decoder plus the full PPMII model +// (context tree, escapes, SEE, rescale) through the rar3 entry point. +static PPMD_NOTES: &[u8] = include_bytes!("fixtures/rar3/ppmd_notes.bin"); + +#[test] +fn ppmd_block_decodes() { + decode_and_check_crc(PPMD_NOTES, 20001, 0x0E1A_EC07); +} + // ─── factory (only if compiled in) ─────────────────────────────────────── #[cfg(feature = "factory")] From a6b33c434d7a41379bdfc94372a838152e6c4512 Mon Sep 17 00:00:00 2001 From: JD Lien Date: Thu, 16 Jul 2026 15:45:56 -0600 Subject: [PATCH 5/8] fix(ppmd,rar3): harden PPMd decode against truncation & unknown length (codex review) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address a gpt-5.6-sol review of the PPMd-II var H work. Three silent-wrong- output paths are closed; a fourth (arena-exhaustion parity) is documented as a fail-closed limitation. - Standalone PPMd, unknown declared length: PPMd carries no in-band end-of-stream marker, so decoding a u64::MAX-framed stream until the range coder exhausts input appended finalisation-byte garbage (reframing the 1280-byte `repeat` fixture returned 1466 bytes). Refuse unknown length up front (Error::Unsupported) instead of guessing. - RAR3 PPMd block, truncated payload: the decode loop checked rc.err() but not rc.overran(), so a truncated payload kept fabricating symbols from zero-filled reads until it hit the declared unpacked size — returning StreamEnd with a wrong CRC. Fail with UnexpectedEnd once the range coder reads past the input. - RAR3 mixed blocks: a new-table boundary in the LZ path (symbol 256) that selects a PPMd block stored the header but let `expand` keep decoding the range-coded payload through the previous block's stale Huffman tables (garbage). Refuse the mid-stream switch into PPMd (Error::Unsupported), matching run_ppmd_block's own start-new-table stance. - Suballocator exhaustion (documented, not fixed): a high-order model over a large high-entropy payload in a small arena can restart the model at a different symbol than the encoder and desync, surfacing as Error::Corrupt. It fails closed — never wrong bytes, never OOB (verified: no panic under instrumented OOB checks). Full GlueFreeBlocks parity is Phase 3 hardening. Regression tests: unknown-length refusal, absurd-length rejection (existing), and RAR3 truncation-not-fabrication. Full suite 1733 green; ppmd/rar3 ASan fuzz campaigns clean. --- src/ppmd/decoder.rs | 80 +++++++++++++++++++++------------------------ src/ppmd/mod.rs | 16 ++++++--- src/ppmd/ppmd7.rs | 12 +++++++ src/rar3/decoder.rs | 17 ++++++++++ tests/ppmd.rs | 16 +++++++++ tests/rar3.rs | 34 +++++++++++++++++++ 6 files changed, 128 insertions(+), 47 deletions(-) diff --git a/src/ppmd/decoder.rs b/src/ppmd/decoder.rs index f078ac8..539964c 100644 --- a/src/ppmd/decoder.rs +++ b/src/ppmd/decoder.rs @@ -20,10 +20,14 @@ use super::ppmd7::Ppmd7; use super::range_dec::{Mode, RangeDec}; const HEADER_LEN: usize = 11; +/// Sentinel length meaning "unknown". PPMd has no in-band end-of-stream +/// marker, so a stream framed with this length can't be decoded reliably +/// (see [`Decoder::run_decode`]) — it's refused rather than decoded to a +/// guess. const UNKNOWN_LEN: u64 = u64::MAX; -/// Hard cap on decoded output when the header length is unknown, so a tiny -/// crafted stream can't drive unbounded work. -const MAX_UNKNOWN_OUTPUT: usize = 64 * 1024 * 1024; +/// Hard cap on decoded output, so a tiny crafted stream declaring a huge +/// length can't drive unbounded allocation/work. +const MAX_OUTPUT: usize = 64 * 1024 * 1024; pub struct Decoder { in_buf: Vec, @@ -78,50 +82,40 @@ impl Decoder { let (mut rc, consumed) = RangeDec::init(Mode::SevenZip, &self.in_buf, HEADER_LEN)?; let _ = consumed; - let cap = if expected_len == UNKNOWN_LEN { - MAX_UNKNOWN_OUTPUT - } else { - expected_len.min(MAX_UNKNOWN_OUTPUT as u64) as usize - }; - let mut out = Vec::with_capacity(cap.min(1 << 20)); - + // PPMd carries no in-band end-of-stream marker, so a stream whose + // header declares an unknown length has no reliable terminal + // condition: after the true last symbol the range coder's + // finalisation bytes keep decoding into extra (garbage) symbols, and + // exhausting the physical input is not an end signal (a high- + // probability symbol decodes without consuming any input). Refuse + // rather than emit a guess. if expected_len == UNKNOWN_LEN { - while out.len() < MAX_UNKNOWN_OUTPUT { - if rc.overran() { - break; - } - let sym = model.decode_symbol(&mut rc)?; - if rc.overran() { - break; - } - out.push(sym); - } - } else { - // A declared length larger than the buffer-then-decode ceiling - // can't be produced here; reject it up front rather than growing - // `out` toward OOM. (A high-probability PPMd symbol can decode - // many times without consuming input, so `overran()` alone is not - // a sufficient bound.) - if expected_len > MAX_UNKNOWN_OUTPUT as u64 { - return Err(Error::OutputLimitExceeded); - } - for _ in 0..expected_len { - // Truncated input can't supply more symbols; stop before the - // model starts decoding from zero-filled reads. - if rc.overran() { - return Err(Error::UnexpectedEnd); - } - let sym = model.decode_symbol(&mut rc)?; - out.push(sym); - } + return Err(Error::Unsupported); + } + // A declared length larger than the buffer-then-decode ceiling can't + // be produced here; reject it up front rather than growing `out` + // toward OOM. + if expected_len > MAX_OUTPUT as u64 { + return Err(Error::OutputLimitExceeded); + } + let mut out = Vec::with_capacity((expected_len as usize).min(1 << 20)); + + for _ in 0..expected_len { + // Truncated input can't supply more symbols; stop before the + // model starts decoding from zero-filled reads. if rc.overran() { return Err(Error::UnexpectedEnd); } - // 7z streams leave the range coder at `code == 0` after the last - // symbol; a non-zero residue means truncation or corruption. - if !rc.is_finished_ok() { - return Err(Error::Corrupt); - } + let sym = model.decode_symbol(&mut rc)?; + out.push(sym); + } + if rc.overran() { + return Err(Error::UnexpectedEnd); + } + // 7z streams leave the range coder at `code == 0` after the last + // symbol; a non-zero residue means truncation or corruption. + if !rc.is_finished_ok() { + return Err(Error::Corrupt); } self.decoded = out; diff --git a/src/ppmd/mod.rs b/src/ppmd/mod.rs index 055f420..9e7ce7d 100644 --- a/src/ppmd/mod.rs +++ b/src/ppmd/mod.rs @@ -17,7 +17,12 @@ //! range decoder in both its 7z and RAR flavours (`range_dec`). The //! standalone framing below uses the 7z flavour; the RAR3/4 decoder //! feeds the same model core through the RAR flavour. Decodes streams -//! produced by real PPMd encoders (7-Zip, `pyppmd`, WinRAR/`rar`). +//! produced by real PPMd encoders (7-Zip, `pyppmd`, WinRAR/`rar`). One +//! known limitation: a stream that *exhausts the suballocator* (a +//! high-order model over a large, high-entropy payload in a small arena) +//! can desync at the memory-restart point and return [`Error::Corrupt`] +//! rather than decoding — it fails closed (never wrong bytes); see the +//! `glue_free_blocks` note in `ppmd7`. //! - **Encoder**: permanently returns [`Error::Unsupported`]. The PPM //! model maintenance plus carry-less range encoder are out of scope; we //! follow the `lzfse`/`rar*` precedent and ship the encoder as a stub. @@ -33,9 +38,12 @@ //! byte 0 : order (2..=64, inclusive) //! byte 1 : mem_size_mb (1..=255, inclusive) //! byte 2 : restoration_method (0=restart, 1=cut-off, 2=freeze) -//! bytes 3..=10: little-endian u64 uncompressed length -//! (0xFFFF_FFFF_FFFF_FFFF means "unknown — decode to -//! stream end") +//! bytes 3..=10: little-endian u64 uncompressed length. PPMd has no +//! in-band end-of-stream marker, so the length is +//! mandatory: the sentinel 0xFFFF_FFFF_FFFF_FFFF +//! ("unknown") is refused, since decoding to physical +//! input exhaustion would append range-coder finalisation +//! garbage past the true end. //! bytes 11.. : the PPMd-coded payload (a raw 7z Ppmd7 stream, i.e. //! a leading 0x00 byte then the range-coded body) //! ``` diff --git a/src/ppmd/ppmd7.rs b/src/ppmd/ppmd7.rs index 54ec578..d4c79bb 100644 --- a/src/ppmd/ppmd7.rs +++ b/src/ppmd/ppmd7.rs @@ -419,6 +419,18 @@ impl Ppmd7 { // The reference builds a doubly-linked list of all free blocks, sets // a head sentinel just past the arena, coalesces adjacent blocks, // and re-buckets them. We follow it directly. + // + // KNOWN LIMITATION (fails closed): this ports the classic LZMA SDK + // GlueFreeBlocks. When a stream actually exhausts the suballocator — + // e.g. a high-order model over a large, high-entropy payload in a + // small (1 MiB) arena — a coalescing/re-bucketing difference from the + // exact encoder build can make our allocator fail (and thus restart + // the model) at a slightly different symbol than the encoder did, + // desyncing the range coder. That surfaces as `Error::Corrupt`, never + // wrong bytes and never an out-of-bounds access. Streams that don't + // fill the arena (the overwhelming majority) are unaffected. Reaching + // full allocator parity across every arena-exhausting input is + // tracked as Phase 3 hardening. let head = self.align_offset + self.size; // sentinel node ref let mut n = head; self.glue_count = 255; diff --git a/src/rar3/decoder.rs b/src/rar3/decoder.rs index 5d4b2d5..111a5a9 100644 --- a/src/rar3/decoder.rs +++ b/src/rar3/decoder.rs @@ -666,6 +666,16 @@ fn expand(ctx: &mut RunCtx) -> Result<(), Error> { let new_table = ctx.bits.read_bits(1)? != 0; if new_table { parse_block_header(ctx)?; + // A new block header may select PPMd. `expand` decodes + // Huffman-coded symbols; if we continued here we would + // feed the range-coded PPMd payload through the previous + // block's stale Huffman tables and emit garbage. A + // mid-stream switch into PPMd is out of scope (same stance + // as `run_ppmd_block`'s start-new-table handling), so + // refuse rather than misdecode. + if ctx.ppmd.is_some() { + return Err(Error::Unsupported); + } } else { // End of stream marker: any further bytes belong to a // separate stream. @@ -824,6 +834,13 @@ fn run_ppmd_block(ctx: &mut RunCtx, input: &[u8], hdr: PpmdHeader) -> Result<(), if rc.err() { return Err(Error::Corrupt); } + // A truncated payload makes the range coder read past the input; once + // that happens `read_byte` is feeding zeroes and every further symbol + // is fabricated. Fail instead of returning invented output that meets + // the declared unpacked size with a wrong CRC. + if rc.overran() { + return Err(Error::UnexpectedEnd); + } Ok(s) }; diff --git a/tests/ppmd.rs b/tests/ppmd.rs index 68c877d..209c937 100644 --- a/tests/ppmd.rs +++ b/tests/ppmd.rs @@ -247,6 +247,22 @@ fn header_bad_restoration_is_bad_header() { assert_eq!(dec.finish(&mut buf), Err(Error::BadHeader)); } +#[test] +fn unknown_declared_length_is_refused() { + // PPMd has no in-band end-of-stream marker, so a stream framed with the + // "unknown length" sentinel (u64::MAX) has no reliable stopping point: + // decoding until the range coder exhausts input appends its finalisation + // bytes as extra garbage symbols (reframing the 1280-byte `repeat` + // fixture this way used to return 1466 bytes). It must be refused. + let (stream, _) = fixture!("repeat"); + let mut bad = stream.to_vec(); + bad[3..11].copy_from_slice(&u64::MAX.to_le_bytes()); + let mut dec = Decoder::new(); + let mut buf = [0u8; 256]; + let _ = dec.decode(&bad, &mut buf); + assert_eq!(dec.finish(&mut buf), Err(Error::Unsupported)); +} + #[test] fn absurd_declared_length_is_rejected_not_oomed() { // Regression: a fuzz-found stream with a valid header but a wildly diff --git a/tests/rar3.rs b/tests/rar3.rs index 10c59b5..67998bc 100644 --- a/tests/rar3.rs +++ b/tests/rar3.rs @@ -388,6 +388,40 @@ fn ppmd_block_decodes() { decode_and_check_crc(PPMD_NOTES, 20001, 0x0E1A_EC07); } +/// A truncated PPMd payload must fail, not fabricate output. Once the range +/// coder reads past the end of the compressed block, `read_byte` supplies +/// zeroes and every further symbol is invented; without an overrun check the +/// decoder would still reach the declared 20001-byte size (with a wrong CRC) +/// and report success. Dropping the tail bytes must surface an error instead. +#[test] +fn ppmd_truncated_payload_errors_not_fabricates() { + let truncated = &PPMD_NOTES[..PPMD_NOTES.len() - 14]; + let mut dec = Decoder::with_unpack_size(20001); + // Buffer-then-decode: the failure surfaces on the first finish/drain. + let _ = dec.decode(truncated, &mut []); + let mut buf = [0u8; 4096]; + let mut err = None; + let mut produced = 0usize; + loop { + match dec.finish(&mut buf) { + Ok((p, status)) => { + produced += p.written; + if matches!(status, Status::StreamEnd) || p.written == 0 { + break; + } + } + Err(e) => { + err = Some(e); + break; + } + } + } + assert!( + matches!(err, Some(Error::UnexpectedEnd) | Some(Error::Corrupt)), + "truncated PPMd should error (got err={err:?}, produced={produced} bytes)" + ); +} + // ─── factory (only if compiled in) ─────────────────────────────────────── #[cfg(feature = "factory")] From b771232b836cf9f40e9c520818f6cfc165974eaf Mon Sep 17 00:00:00 2001 From: JD Lien Date: Thu, 16 Jul 2026 17:09:12 -0600 Subject: [PATCH 6/8] =?UTF-8?q?feat(rar3):=20solid=20multi-member=20decode?= =?UTF-8?q?=20=E2=80=94=20window,=20tables=20&=20PPMd=20model=20carry-over?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RAR3 solid groups share one compression history across members while each member's payload is its own byte-aligned stream. New API: Decoder::with_solid() + begin_solid_member(n); the LZ window, code tables, offset history, declared filter programs and any live PPMd model persist, and per-member state (bit input, output, pending filter windows) resets. Member-boundary framing was established empirically against the corpus (probe payload heads + differential validation, no unrar source): - The LZ end-of-block marker's second bit announces whether the next member opens with its own table header or continues symbol decode headerless (observed: photo.jpg, ramp.wav); an inline-table marker at the exact boundary parses the tables from the current member's tail. - PPMd members end with an explicit escape+2 marker decoded THROUGH the model (the encoder's model saw it too — required for cross-member model parity); a continuation header (no 0x20 reset flag) reuses the live model and escape char with a freshly initialised range coder. - Mid-member LZ<->PPMd switches (start-new-table in both domains) and PPMd escape-code-3 filter declarations are now decoded; the previous Unsupported arms for these are gone. Fail-closed rules in solid mode: a short member is a hard error (the shared history would silently desync every later member), and range-coder state straddling a member boundary (markerless PPMd member end, or a PPMd block starting exactly at the boundary via an inline announcement) stays Unsupported — rar 6.24 emits neither. Also aligns the in-band filter machinery with UnRAR 7.23 semantics per review: block-length caps at VM_MEMSIZE (0x40000, delta half) and a filter reset (slot 0) now cancels every scheduled filter without applying completed-but-unflushed windows (unrar InitFilters30). Validation: differential harness 206 pass / 0 mismatch (was 188) — all of rar4_m{1,3,5}_solid + rar4_ppmd_solid byte-identical to UnRAR 7.23, plus the 64-bit corpus-large case; whole solid groups embedded as fixtures (m3 6-member group, PPMd pair) asserted against FILE_CRCs; decoder_rar3 fuzz target grew a solid-group mode with real-group seeds, corpus replay + 7-minute ASan campaign clean. Claude-Session: https://claude.ai/code/session_01DYLfPTghh7DayY4M7YyeKY --- README.md | 2 +- fuzz/fuzz_targets/decoder_rar3.rs | 73 ++- src/ppmd/range_dec.rs | 9 + src/rar3/bits.rs | 11 + src/rar3/decoder.rs | 709 ++++++++++++++-------- src/rar3/mod.rs | 25 +- tests/fixtures/rar3/solid_m3_calls.bin | Bin 0 -> 1321 bytes tests/fixtures/rar3/solid_m3_gradient.bin | Bin 0 -> 443 bytes tests/fixtures/rar3/solid_m3_notes.bin | Bin 0 -> 170 bytes tests/fixtures/rar3/solid_m3_photo.bin | Bin 0 -> 8222 bytes tests/fixtures/rar3/solid_m3_ramp.bin | Bin 0 -> 297 bytes tests/fixtures/rar3/solid_m3_x86slice.bin | Bin 0 -> 14246 bytes tests/fixtures/rar3/solid_ppmd_prose.bin | Bin 0 -> 32349 bytes tests/rar3.rs | 143 ++++- 14 files changed, 704 insertions(+), 268 deletions(-) create mode 100644 tests/fixtures/rar3/solid_m3_calls.bin create mode 100644 tests/fixtures/rar3/solid_m3_gradient.bin create mode 100644 tests/fixtures/rar3/solid_m3_notes.bin create mode 100644 tests/fixtures/rar3/solid_m3_photo.bin create mode 100644 tests/fixtures/rar3/solid_m3_ramp.bin create mode 100644 tests/fixtures/rar3/solid_m3_x86slice.bin create mode 100644 tests/fixtures/rar3/solid_ppmd_prose.bin diff --git a/README.md b/README.md index 19efbe6..87d2fb1 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ flag, and a `compcol` binary turns the library into a Unix-style filter. | StuffIt 5 Arsenic (method 15) | `arsenic` | `.sit` | `Unsupported` (decode-only) | full (range coder + inverse BWT + MTF/RLE + de-randomization) | **real StuffIt 5 fixtures (in-stream CRC-32 + SHA vs `unar`)** | | RAR 1.x | `rar1` | `.rar` | `Unsupported` (license) | building blocks only (Huffman tables not license-clean) | — | | RAR 2.x | `rar2` | `.rar` | `Unsupported` (license) | full LZ77+Huffman + audio predictor | real rar-2.60 fixtures | -| RAR 3.x | `rar3` | `.rar` | `Unsupported` (license) | full LZ77+Huffman + standard filters (Delta, x86 E8/E8E9); PPMd & non-standard VM programs refused | libarchive RAR3 fixtures + **real rar-6.24 archives (differential vs UnRAR 7.23)** | +| RAR 3.x | `rar3` | `.rar` | `Unsupported` (license) | full LZ77+Huffman + PPMd-II variant H + standard filters (Delta, x86 E8/E8E9), incl. solid groups; non-standard VM programs refused | libarchive RAR3 fixtures + **real rar-6.24 archives (differential vs UnRAR 7.23)** | | RAR 5.x | `rar5` | `.rar` | `Unsupported` (license) | full LZ77+Huffman + Delta/x86 filters (incl. solid groups); ARM refused | RARLAB-CLI fixtures + **real WinRAR 7.23 archives (differential vs UnRAR 7.23)** | | HTTP/2 HPACK (RFC 7541) | `hpack` | — | full (header codec + `h2-huffman` string codec) | full (static+dynamic tables, integer/string coding) | RFC 7541 Appendix C vectors | | HTTP/3 QPACK (RFC 9204) | `qpack` | — | full (static + dynamic-table encoder driving the encoder stream; eviction-safe) | full (static+dynamic tables via encoder stream, all field representations) | RFC 9204 Appendix B vectors | diff --git a/fuzz/fuzz_targets/decoder_rar3.rs b/fuzz/fuzz_targets/decoder_rar3.rs index b5eb4c5..6c7a5b4 100644 --- a/fuzz/fuzz_targets/decoder_rar3.rs +++ b/fuzz/fuzz_targets/decoder_rar3.rs @@ -12,9 +12,20 @@ use libfuzzer_sys::fuzz_target; // lives in the archive container's file header — so we read a 5-byte // prefix: 4 LE bytes for the unpack size (capped to 1 MiB so a hostile // size field can't make the harness itself allocate unbounded output) -// and 1 flag byte driving the optional standalone E8/E9 post-pass -// filter (bit 0 enables it, bit 1 also translates E9 jumps). -fn drive(mut dec: Decoder, payload: &[u8]) { +// and 1 flag byte: +// bit 0: enable the standalone E8/E9 post-pass filter +// bit 1: ... also translating E9 jumps +// bit 2: solid-group mode — the payload becomes up to 4 members, each +// introduced by a 5-byte header (2-byte LE chunk length + 3-byte +// LE unpack size, capped to 256 KiB — 1 MiB per group); the +// shared LZ window, tables, filter programs and PPMd model +// persist across them. The outer 4-byte unpack prefix is unused +// in this mode. + +/// Feed one member's payload and drain it. Returns false when the decoder +/// errored (a fine outcome — the input is garbage; we only care that it +/// never panics or loops). +fn drive_member(dec: &mut Decoder, payload: &[u8]) -> bool { let mut out = vec![0u8; 64 * 1024]; let mut consumed = 0; let mut steps = 0; @@ -26,24 +37,59 @@ fn drive(mut dec: Decoder, payload: &[u8]) { } consumed += p.consumed; } - Err(_) => return, + Err(_) => return false, } steps += 1; if steps > 4096 { // Defensive: pathological inputs shouldn't make us loop. - return; + return false; } } let mut steps = 0; - while let Ok((p, status)) = dec.finish(&mut out) { - if matches!(status, compcol::Status::StreamEnd) { - return; - } - if p.written == 0 { - return; + loop { + match dec.finish(&mut out) { + Ok((p, status)) => { + if matches!(status, compcol::Status::StreamEnd) { + return true; + } + if p.written == 0 { + return true; + } + } + Err(_) => return false, } steps += 1; if steps > 4096 { + return false; + } + } +} + +fn drive(mut dec: Decoder, payload: &[u8]) { + drive_member(&mut dec, payload); +} + +fn drive_solid(mut payload: &[u8]) { + let mut dec: Option = None; + for _ in 0..4 { + if payload.len() < 5 { + return; + } + let (hdr, rest) = payload.split_at(5); + let want = u16::from_le_bytes([hdr[0], hdr[1]]) as usize; + let unpack = (u32::from_le_bytes([hdr[2], hdr[3], hdr[4], 0]) as u64) % (256 * 1024 + 1); + let (chunk, rest) = rest.split_at(want.min(rest.len())); + payload = rest; + let d = match dec.as_mut() { + None => dec.insert(Decoder::with_unpack_size(unpack).with_solid()), + Some(d) => { + if d.begin_solid_member(unpack).is_err() { + return; + } + d + } + }; + if !drive_member(d, chunk) { return; } } @@ -61,6 +107,11 @@ fuzz_target!(|data: &[u8]| { let raw = u32::from_le_bytes([prefix[0], prefix[1], prefix[2], prefix[3]]); let unpack = (raw as u64) % (1024 * 1024 + 1); + if prefix[4] & 4 != 0 { + drive_solid(payload); + return; + } + let mut dec = Decoder::with_unpack_size(unpack); if prefix[4] & 1 != 0 { dec = dec.with_e8_filter(prefix[4] & 2 != 0); diff --git a/src/ppmd/range_dec.rs b/src/ppmd/range_dec.rs index e3b4ad1..5d51d76 100644 --- a/src/ppmd/range_dec.rs +++ b/src/ppmd/range_dec.rs @@ -96,6 +96,15 @@ impl<'a> RangeDec<'a> { self.pos > self.input.len() } + /// Byte position in the input the decoder has consumed up to (including + /// the init bytes and any normalisation look-ahead). RAR3 resumes its + /// bit-domain block headers at this offset when a PPMd block ends. + #[inline] + #[cfg_attr(not(feature = "rar3"), allow(dead_code))] + pub(crate) fn pos(&self) -> usize { + self.pos.min(self.input.len()) + } + #[inline] fn read_byte(&mut self) -> u8 { let b = self.input.get(self.pos).copied().unwrap_or(0); diff --git a/src/rar3/bits.rs b/src/rar3/bits.rs index 150ce00..2fca1af 100644 --- a/src/rar3/bits.rs +++ b/src/rar3/bits.rs @@ -103,6 +103,17 @@ impl BitReader { } } + /// Reposition the reader to an absolute byte offset in the fed buffer, + /// discarding any buffered look-ahead bits. Used when the PPMd path + /// hands control back to the bit domain: the range decoder consumed raw + /// bytes past the reader's position, and the next block header starts + /// at the byte where the range-coded data ended. + pub fn seek_byte(&mut self, pos: usize) { + self.byte_pos = pos.min(self.buf.len()); + self.acc = 0; + self.nbits = 0; + } + /// Number of source bytes logically consumed so far. Only meaningful on /// a byte boundary (call [`byte_align`] first). Used by the PPMd path to /// hand the raw byte stream after the block header to the RAR range diff --git a/src/rar3/decoder.rs b/src/rar3/decoder.rs index 111a5a9..c5f1826 100644 --- a/src/rar3/decoder.rs +++ b/src/rar3/decoder.rs @@ -19,25 +19,35 @@ //! length), the 4-deep rolling-offset buffer, short offsets (codes 263 //! through 270), the full match-length / offset machinery, and the //! keep-table flag (successive blocks reusing the previous code lengths). -//! - **In-band standard filters** (main symbol 257): Delta and x86 -//! E8/E8E9 declarations are recognized by their bytecode fingerprint and -//! run natively over their declared output windows — see -//! `super::filters` for the recognition scheme and provenance. +//! - **In-band standard filters** (main symbol 257, or escape code 3 in a +//! PPMd block): Delta and x86 E8/E8E9 declarations are recognized by +//! their bytecode fingerprint and run natively over their declared +//! output windows — see `super::filters` for the recognition scheme and +//! provenance. //! - **PPMd-II variant H blocks** (bit-0 of the block header): the full //! PPMII model in [`crate::ppmd`] driven by the RAR range decoder, with -//! the RAR escape layer (literals, LZ matches, end-of-data) on top. A -//! single self-contained block decodes end to end — see -//! [`run_ppmd_block`]. +//! the RAR escape layer (literals, LZ matches, filters, end-of-data) on +//! top — see [`run_ppmd`]. Mid-stream switches between LZ and PPMd (the +//! `start-new-table` paths in both domains) are followed, including PPMd +//! continuation headers that reuse the live model. +//! - **Solid groups** ([`Decoder::with_solid`] + +//! [`Decoder::begin_solid_member`]): the LZ window, code tables, offset +//! history, filter programs and PPMd model persist across members. Each +//! member's payload is decoded as its own byte-aligned stream, with the +//! end-of-member markers consumed through the shared state (a PPMd +//! member's marker updates the model, exactly as the encoder's did). //! - The standalone E8/E9 post-pass filter when enabled via //! [`Decoder::with_e8_filter`]. //! //! ## What's refused //! -//! - **PPMd continuations across a new-table boundary** (a PPMd block that -//! reuses a still-live model from a previous block, i.e. no fresh -//! memory/order flag, or a `start-new-table` control code mid-stream). -//! These arise only in solid multi-member streams — out of scope here — -//! and fail with `Error::Unsupported`. +//! - **Cross-member range-coder state**: a PPMd block whose range coder +//! would have to straddle a solid member boundary (a member ending +//! without an end-of-data marker mid-PPMd, or a new PPMd block starting +//! exactly at the boundary via an inline table announcement). rar 6.24 +//! always ends PPMd members with a marker and starts PPMd blocks with a +//! header in the member that uses them, so these arise only in crafted +//! streams; they fail with `Error::Unsupported`. //! - **Filter declarations carrying any other VM program** (custom //! bytecode, or legacy standard programs no current archiver emits — //! Itanium, RGB, the audio predictor). These fail with @@ -84,6 +94,14 @@ pub struct Decoder { e8_translate_e9: bool, /// Set on any irrecoverable error. poisoned: bool, + /// Solid-group mode: end-of-member markers are consumed (through the + /// PPMd model where applicable) and the decode context persists across + /// [`Decoder::begin_solid_member`] calls. + solid: bool, + /// The persistent decode context (window, code tables, offset history, + /// filter programs, PPMd model). Created on the first `finish`; dropped + /// after each stream unless `solid`. + ctx: Option>, } enum State { @@ -112,20 +130,16 @@ impl Decoder { e8_enabled: false, e8_translate_e9: false, poisoned: false, + solid: false, + ctx: None, } } /// Construct a decoder that will produce at most `n` uncompressed bytes. pub fn with_unpack_size(n: u64) -> Self { - Self { - state: State::Buffering { input: Vec::new() }, - out_buf: Vec::new(), - out_drained: 0, - unpack_size: n, - e8_enabled: false, - e8_translate_e9: false, - poisoned: false, - } + let mut d = Self::new(); + d.unpack_size = n; + d } /// Enable the standalone E8 (and optionally E9) filter as a post-pass. @@ -135,6 +149,40 @@ impl Decoder { self } + /// Enable solid-group mode. In a RAR3 **solid** archive the members of + /// a solid group share one compression history: the LZ window, code + /// tables, offset history, declared filter programs and any live PPMd + /// model all persist from one member to the next, while each member's + /// compressed payload is its own byte-aligned stream. Decode the first + /// member as usual, then call [`Decoder::begin_solid_member`] before + /// feeding each subsequent member. + /// + /// Solid mode also makes truncation a hard error: a member whose stream + /// ends before its declared unpacked size poisons the whole group (the + /// shared history would desync every later member), where the default + /// mode returns the short output and leaves the verdict to the caller. + pub fn with_solid(mut self) -> Self { + self.solid = true; + self + } + + /// Prepare to decode the next member of a solid group: keeps the shared + /// compression history and expects `unpack_size` uncompressed bytes from + /// the next member's compressed payload (fed via `decode`/`finish` as + /// usual). Only valid on a [`Decoder::with_solid`] decoder whose current + /// member has fully drained. + pub fn begin_solid_member(&mut self, unpack_size: u64) -> Result<(), Error> { + if self.poisoned { + return Err(Error::Corrupt); + } + if !self.solid || !matches!(self.state, State::Done) { + return Err(Error::Unsupported); + } + self.unpack_size = unpack_size; + self.state = State::Buffering { input: Vec::new() }; + Ok(()) + } + fn poison(&mut self, e: Error) -> Result { self.poisoned = true; Err(e) @@ -202,15 +250,25 @@ impl RawDecoder for Decoder { // If we still need to decode, do it now. if let State::Buffering { input } = &mut self.state { let input = core::mem::take(input); - // Move into a separate scope so the `match` borrow ends before - // we mutate `self.state`. - match run_decode( - input, - self.unpack_size, - self.e8_enabled, - self.e8_translate_e9, - ) { - Ok(out) => { + let ctx = self + .ctx + .get_or_insert_with(|| Box::new(RunCtx::new(self.unpack_size))); + ctx.unpack_size = self.unpack_size; + let result = if self.unpack_size == 0 { + Ok(Vec::new()) + } else { + run_member(ctx, &input, self.solid) + }; + match result { + Ok(mut out) => { + if self.e8_enabled { + apply_e8_filter(&mut out, 0, self.e8_translate_e9); + } + if !self.solid { + // Match the pre-solid memory profile: a one-shot + // stream has no further use for the 4 MiB window. + self.ctx = None; + } self.out_buf = out; self.out_drained = 0; self.state = State::Draining; @@ -232,60 +290,63 @@ impl RawDecoder for Decoder { self.out_buf.clear(); self.out_drained = 0; self.poisoned = false; - // unpack_size and filter flags are configuration; preserved across - // reset to match the LZX/Quantum conventions. + // A reset starts a fresh stream: any solid history is gone. + self.ctx = None; + // unpack_size, solid and filter flags are configuration; preserved + // across reset to match the LZX/Quantum conventions. } } // ─── Internal decode pipeline ───────────────────────────────────────────── -fn run_decode( - input: Vec, - unpack_size: u64, - e8_enabled: bool, - e8_translate_e9: bool, -) -> Result, Error> { - if unpack_size == 0 { - return Ok(Vec::new()); - } - let mut br = BitReader::new(); - br.feed_slice(&input); - - let mut ctx = Box::new(RunCtx { - bits: br, - // The length table survives across blocks: a successive block can - // signal "keep table" with a single header bit and reuse what was - // most recently decoded. - lengths: vec![0u8; HUFF_TABLE_SIZE], - main: None, - offset: None, - low_offset: None, - length: None, - old_offsets: [1u32, 1, 1, 1], - last_offset: 0, - last_length: 0, - last_low_offset: 0, - num_low_offset_repeats: 0, - out: Vec::new(), - window: vec![0u8; DICT_DEFAULT_SIZE], - wmask: { - debug_assert!(DICT_DEFAULT_SIZE.is_power_of_two()); - DICT_DEFAULT_SIZE - 1 - }, - window_pos: 0, - unpack_size, - programs: Vec::new(), - last_filter_slot: 0, - pending_filters: VecDeque::new(), - ppmd: None, - }); +/// Decode one member's compressed payload against the (possibly carried- +/// over) context. In solid mode the end-of-member marker is consumed so the +/// persistent state is exactly what the next member's stream expects. +fn run_member(ctx: &mut RunCtx, input: &[u8], solid: bool) -> Result, Error> { + // Each member's payload is its own byte-aligned stream (the container + // resets the bit input at every member boundary), so the reader is + // rebuilt even when the rest of the context carries over. + ctx.bits = BitReader::new(); + ctx.bits.feed_slice(input); + ctx.out = Vec::new(); + // Filter *programs* persist across solid members, but scheduled filter + // instances never span a member boundary. + ctx.pending_filters.clear(); + + // A fresh stream — or a previous member that announced new tables — + // starts with a block header; otherwise symbol decoding continues + // directly under the carried-over tables (or PPMd model). + if !ctx.tables_read { + parse_block_header(ctx)?; + } + loop { + let seg = match ctx.block { + BlockKind::Lz => expand(ctx, solid)?, + BlockKind::Ppm => run_ppmd(ctx, input, solid)?, + }; + match seg { + Segment::MemberEnd => break, + Segment::NewTable => parse_block_header(ctx)?, + Segment::NewTableThenEnd => { + parse_block_header(ctx)?; + if matches!(ctx.block, BlockKind::Ppm) { + // A PPMd block starting exactly at the member boundary + // would prime its range coder from this member's tail + // bytes and keep pulling from the next member's payload + // — cross-member coder state we don't support (rar 6.24 + // starts such blocks with a header in the next member + // instead). + return Err(Error::Unsupported); + } + break; + } + } + } - // The decoder starts by parsing the first block header. - parse_block_header(&mut ctx)?; - if let Some(hdr) = ctx.ppmd.take() { - run_ppmd_block(&mut ctx, &input, hdr)?; - } else { - expand(&mut ctx)?; + if solid && (ctx.out.len() as u64) < ctx.unpack_size { + // A short member desyncs the shared history for every member after + // it; fail the group rather than hand back silently-short output. + return Err(Error::UnexpectedEnd); } // Run any in-band filters whose windows the stream completed. A filter @@ -299,11 +360,28 @@ fn run_decode( return Err(Error::Corrupt); } - let mut out = core::mem::take(&mut ctx.out); - if e8_enabled { - apply_e8_filter(&mut out, 0, e8_translate_e9); - } - Ok(out) + Ok(core::mem::take(&mut ctx.out)) +} + +/// How a run of symbol decoding ended. +enum Segment { + /// The member is complete (declared size produced and, in solid mode, + /// the end-of-member marker consumed). + MemberEnd, + /// An in-band "new code tables follow" boundary mid-member: parse a + /// block header and continue decoding this member. + NewTable, + /// "New code tables follow" arrived exactly at the member's declared + /// size: the tables land in this member's tail bytes and the *next* + /// member continues under them without a header of its own. + NewTableThenEnd, +} + +/// Which decoding mode the current block uses. Persists across solid +/// members: a member may continue a block the previous member started. +enum BlockKind { + Lz, + Ppm, } struct RunCtx { @@ -344,25 +422,27 @@ struct RunCtx { /// popped from the front) as soon as their windows are fully decoded — /// see [`RunCtx::flush_completed_filters`]. pending_filters: VecDeque, - /// Set when the first block header selected the PPMd path; carries the - /// parameters needed to drive the PPMd model over the raw byte stream. - ppmd: Option, + /// The live PPMd model, if any block has created one. Persists across + /// blocks and solid members: a later PPMd block header without the + /// reset flag reuses it (with a freshly initialised range coder). + ppmd: Option>, + /// Decoding mode of the current block (LZ+Huffman or PPMd). + block: BlockKind, + /// Whether valid LZ code tables are in effect, i.e. whether the next + /// member of a solid group starts decoding symbols directly instead of + /// parsing a block header first. Cleared by PPMd block headers (a + /// member after a PPMd block always re-reads a header) and by an + /// end-of-member marker announcing new tables. + tables_read: bool, } -/// Parameters lifted from a PPMd block header (bit-0 of the block header -/// set). See [`parse_block_header`]. -struct PpmdHeader { - /// Suballocator size in bytes (`(mem + 1) << 20`). - mem_size: u32, - /// Model max order. - max_order: u32, +/// A live PPMd model plus the RAR escape layer's current escape byte. +struct PpmdBlock { + model: Ppmd7, /// The RAR-layer escape byte (a decoded symbol equal to this introduces - /// a control code rather than a literal). + /// a control code rather than a literal). Persists across blocks and + /// members; updated by headers carrying an explicit escape byte. escape: u8, - /// Explicit `InitEsc` seed if the header carried one. - init_esc: Option, - /// Byte offset in the input where the range-coded payload begins. - payload_start: usize, } /// A declared filter program plus its per-slot remembered block length. @@ -373,6 +453,40 @@ struct ProgramSlot { } impl RunCtx { + fn new(unpack_size: u64) -> Self { + RunCtx { + bits: BitReader::new(), + // The length table survives across blocks (and solid members): + // a successive block can signal "keep table" with a single + // header bit and delta-code against what was most recently + // decoded. + lengths: vec![0u8; HUFF_TABLE_SIZE], + main: None, + offset: None, + low_offset: None, + length: None, + old_offsets: [1u32, 1, 1, 1], + last_offset: 0, + last_length: 0, + last_low_offset: 0, + num_low_offset_repeats: 0, + out: Vec::new(), + window: vec![0u8; DICT_DEFAULT_SIZE], + wmask: { + debug_assert!(DICT_DEFAULT_SIZE.is_power_of_two()); + DICT_DEFAULT_SIZE - 1 + }, + window_pos: 0, + unpack_size, + programs: Vec::new(), + last_filter_slot: 0, + pending_filters: VecDeque::new(), + ppmd: None, + block: BlockKind::Lz, + tables_read: false, + } + } + fn emit_literal(&mut self, b: u8) { self.out.push(b); self.window[self.window_pos] = b; @@ -483,43 +597,58 @@ fn parse_block_header(ctx: &mut RunCtx) -> Result<(), Error> { // 1 bit: PPMd-block flag. let is_ppmd = ctx.bits.read_bits(1)?; if is_ppmd != 0 { - // PPMd block header: 7 flag bits, then (per flags) a memory byte, - // an escape byte, and an order derived from the flags. - // flag 0x20: read 8-bit mem → suballocator = (mem+1)<<20, and - // order = (flags & 0x1F) + 1 (values > 16 expand as - // 16 + (order-16)*3). - // flag 0x40: read 8-bit escape/InitEsc seed (else escape = 2). - // A header without 0x20 is a continuation reusing the live model — - // not produced for a standalone first block, so we refuse it. + // PPMd block header: 7 flag bits, then (per flags) a memory byte + // and an escape byte. + // flag 0x20: model reset — read 8-bit mem → suballocator = + // (mem+1)<<20, and order = (flags & 0x1F) + 1 (values + // > 16 expand as 16 + (order-16)*3). Without 0x20 the + // block *continues* the live model from an earlier + // block or solid member (fresh range coder, same + // statistics); there must be one. + // flag 0x40: read 8-bit escape/InitEsc seed (else escape = 2 on + // reset; unchanged on continuation). let flags = ctx.bits.read_bits(7)?; - if flags & 0x20 == 0 { - return Err(Error::Unsupported); - } - let mem_mb = ctx.bits.read_bits(8)?; - let mem_size = (mem_mb + 1).saturating_mul(1 << 20); - let mut max_order = (flags & 0x1F) + 1; - if max_order > 16 { - max_order = 16 + (max_order - 16) * 3; - } - if max_order < 2 { - return Err(Error::Corrupt); - } - let (escape, init_esc) = if flags & 0x40 != 0 { - let e = ctx.bits.read_bits(8)? as u8; - (e, Some(e)) + if flags & 0x20 != 0 { + let mem_mb = ctx.bits.read_bits(8)?; + let mem_size = (mem_mb + 1).saturating_mul(1 << 20); + let mut max_order = (flags & 0x1F) + 1; + if max_order > 16 { + max_order = 16 + (max_order - 16) * 3; + } + if max_order < 2 { + return Err(Error::Corrupt); + } + let (escape, init_esc) = if flags & 0x40 != 0 { + let e = ctx.bits.read_bits(8)? as u8; + (e, Some(e)) + } else { + (2u8, None) + }; + let mut model = Ppmd7::new(mem_size)?; + model.init(max_order); + if let Some(e) = init_esc { + model.set_init_esc(e as u32); + } + ctx.ppmd = Some(Box::new(PpmdBlock { model, escape })); } else { - (2u8, None) - }; + let ppmd = ctx.ppmd.as_deref_mut().ok_or(Error::Corrupt)?; + if flags & 0x40 != 0 { + // An explicit escape byte updates the escape layer; the + // live model's InitEsc is a model-creation parameter and + // stays as-is. + ppmd.escape = ctx.bits.read_bits(8)? as u8; + } + // The order bits are informational on a continuation — the + // live model keeps the order it was built with. + } ctx.bits.byte_align(); - ctx.ppmd = Some(PpmdHeader { - mem_size, - max_order, - escape, - init_esc, - payload_start: ctx.bits.consumed_bytes(), - }); + ctx.block = BlockKind::Ppm; + // A PPMd block never leaves LZ tables in effect: after a PPMd + // member, the next member always starts with its own header. + ctx.tables_read = false; return Ok(()); } + ctx.block = BlockKind::Lz; // 1 bit: keep-table flag. 0 ⇒ reset the persistent length table. let keep_table = ctx.bits.read_bits(1)? != 0; if !keep_table { @@ -627,15 +756,19 @@ fn parse_block_header(ctx: &mut RunCtx) -> Result<(), Error> { &ctx.lengths[MAIN_SIZE + OFFSET_SIZE + LOW_OFFSET_SIZE ..MAIN_SIZE + OFFSET_SIZE + LOW_OFFSET_SIZE + LENGTH_SIZE], )?)); + ctx.tables_read = true; Ok(()) } // ─── Expansion ─────────────────────────────────────────────────────────── -fn expand(ctx: &mut RunCtx) -> Result<(), Error> { +fn expand(ctx: &mut RunCtx, solid: bool) -> Result { loop { if ctx.done() { - return Ok(()); + if solid { + return read_member_end_lz(ctx); + } + return Ok(Segment::MemberEnd); } // Decode the next main-tree symbol. @@ -643,9 +776,14 @@ fn expand(ctx: &mut RunCtx) -> Result<(), Error> { let sym = match main_tree.decode(&mut ctx.bits) { Ok(s) => s, Err(Error::UnexpectedEnd) => { + if solid { + // A short member desyncs the group; `run_member` turns + // this into a hard error rather than short output. + return Err(Error::UnexpectedEnd); + } // Stream ran out before we've reached unpack_size. The // caller's count of output bytes is authoritative. - return Ok(()); + return Ok(Segment::MemberEnd); } Err(e) => return Err(e), }; @@ -657,30 +795,26 @@ fn expand(ctx: &mut RunCtx) -> Result<(), Error> { match sym { 256 => { - // End-of-block marker; followed by a single bit deciding - // between "this is the end of the stream" and "a new code - // table follows". The PPMd-vs-Huffman test makes one more - // bit (the "new file" flag) optional in libarchive's port, - // but unarr just reads `start_new_table` directly. We - // follow unarr here: one bit = start_new_table. + // End-of-block marker. One bit: set ⇒ new code tables + // follow immediately (the caller parses a block header — + // which may also switch this member to PPMd — and decoding + // continues). Clear ⇒ this member's data ends here; a + // second bit then announces whether the *next* member of a + // solid group starts with its own table header or keeps + // decoding under the current tables. (Framing per the + // libarchive/unarr RAR readers' descriptions, validated + // against the solid corpus.) let new_table = ctx.bits.read_bits(1)? != 0; if new_table { - parse_block_header(ctx)?; - // A new block header may select PPMd. `expand` decodes - // Huffman-coded symbols; if we continued here we would - // feed the range-coded PPMd payload through the previous - // block's stale Huffman tables and emit garbage. A - // mid-stream switch into PPMd is out of scope (same stance - // as `run_ppmd_block`'s start-new-table handling), so - // refuse rather than misdecode. - if ctx.ppmd.is_some() { - return Err(Error::Unsupported); - } - } else { - // End of stream marker: any further bytes belong to a - // separate stream. - return Ok(()); + return Ok(Segment::NewTable); + } + if solid { + let next_has_header = ctx.bits.read_bits(1)? != 0; + ctx.tables_read = !next_has_header; } + // For a one-shot stream any further bytes belong to a + // separate stream; the second marker bit is irrelevant. + return Ok(Segment::MemberEnd); } 257 => { // Filter declaration: a standard-program instance gets @@ -810,93 +944,200 @@ fn expand(ctx: &mut RunCtx) -> Result<(), Error> { } } +/// In solid mode, consume the end-of-member marker once the member's +/// declared size has been produced, so `tables_read` reflects what the +/// next member's stream expects. A well-formed member ends with the 256 +/// marker; a stream that simply stops (no marker) leaves the tables in +/// effect, and anything else is data beyond the declared size, which we +/// leave unread (the next member's payload is a fresh stream regardless). +fn read_member_end_lz(ctx: &mut RunCtx) -> Result { + let main_tree = ctx.main.as_ref().ok_or(Error::InvalidHuffmanTree)?; + let sym = match main_tree.decode(&mut ctx.bits) { + Ok(s) => s, + Err(Error::UnexpectedEnd) => return Ok(Segment::MemberEnd), + Err(e) => return Err(e), + }; + if sym != 256 { + return Ok(Segment::MemberEnd); + } + if ctx.bits.read_bits(1)? != 0 { + // New tables land in this member's tail; the next member continues + // symbol decoding under them directly. + return Ok(Segment::NewTableThenEnd); + } + let next_has_header = ctx.bits.read_bits(1)? != 0; + ctx.tables_read = !next_has_header; + Ok(Segment::MemberEnd) +} + // ─── PPMd-II variant H block ───────────────────────────────────────────── -/// Drive the RAR PPMd block: the range-coded payload (starting at -/// `hdr.payload_start` in `input`) feeds the shared [`Ppmd7`] model through +/// Drive a RAR PPMd block: the range-coded payload (starting at the bit +/// reader's current byte position) feeds the live [`Ppmd7`] model through /// the RAR range decoder. Decoded byte symbols are literals unless they /// equal the escape byte, which introduces a control code (end-of-data, an -/// LZ match, a new table, or a literal-escape). Matches copy through the -/// same sliding window as the LZ path so they interleave seamlessly. -fn run_ppmd_block(ctx: &mut RunCtx, input: &[u8], hdr: PpmdHeader) -> Result<(), Error> { - let mut model = Ppmd7::new(hdr.mem_size)?; - model.init(hdr.max_order); - if let Some(e) = hdr.init_esc { - model.set_init_esc(e as u32); - } - if hdr.payload_start > input.len() { +/// LZ match, a filter declaration, a new table, or a literal-escape). +/// Matches copy through the same sliding window as the LZ path so they +/// interleave seamlessly. On return the bit reader is repositioned to the +/// byte where the range-coded data ended. +fn run_ppmd(ctx: &mut RunCtx, input: &[u8], solid: bool) -> Result { + ctx.bits.byte_align(); + let payload_start = ctx.bits.consumed_bytes(); + if payload_start > input.len() { return Err(Error::UnexpectedEnd); } - let (mut rc, _) = RangeDec::init(RangeMode::Rar, input, hdr.payload_start)?; + // Take the model out of the context so the emit helpers can borrow the + // context mutably alongside it; it is put back on every path. + let mut pb = ctx.ppmd.take().ok_or(Error::Corrupt)?; + let result = run_ppmd_inner(ctx, input, payload_start, &mut pb, solid); + ctx.ppmd = Some(pb); + let (seg, end_pos) = result?; + ctx.bits.seek_byte(end_pos); + Ok(seg) +} - let sym = |m: &mut Ppmd7, rc: &mut RangeDec| -> Result { - let s = m.decode_symbol(rc)?; - if rc.err() { - return Err(Error::Corrupt); - } - // A truncated payload makes the range coder read past the input; once - // that happens `read_byte` is feeding zeroes and every further symbol - // is fabricated. Fail instead of returning invented output that meets - // the declared unpacked size with a wrong CRC. - if rc.overran() { - return Err(Error::UnexpectedEnd); - } - Ok(s) - }; +/// Decode one PPMd symbol, failing closed on range-coder errors and on +/// payload overrun (a truncated payload makes the coder read fabricated +/// zero bytes). +fn ppmd_symbol(m: &mut Ppmd7, rc: &mut RangeDec) -> Result { + let s = m.decode_symbol(rc)?; + if rc.err() { + return Err(Error::Corrupt); + } + if rc.overran() { + return Err(Error::UnexpectedEnd); + } + Ok(s) +} + +fn run_ppmd_inner( + ctx: &mut RunCtx, + input: &[u8], + payload_start: usize, + pb: &mut PpmdBlock, + solid: bool, +) -> Result<(Segment, usize), Error> { + let (mut rc, _) = RangeDec::init(RangeMode::Rar, input, payload_start)?; - while !ctx.done() { - let s = sym(&mut model, &mut rc)?; - if s != hdr.escape { + loop { + if ctx.done() { + if !solid { + return Ok((Segment::MemberEnd, rc.pos())); + } + // The encoder coded this member's end marker through the model; + // decode it the same way or the shared statistics desync from + // the encoder's for every later member. If the payload is + // exhausted right here instead, the member boundary splits a + // still-running range coder across payloads — cross-member + // coder state we don't support (rar 6.24 ends PPMd members + // with an explicit marker). + let s = ppmd_symbol(&mut pb.model, &mut rc).map_err(|e| match e { + Error::UnexpectedEnd => Error::Unsupported, + other => other, + })?; + if s != pb.escape { + return Err(Error::Corrupt); + } + let code = ppmd_symbol(&mut pb.model, &mut rc)?; + return match code { + 2 => Ok((Segment::MemberEnd, rc.pos())), + 0 => Ok((Segment::NewTableThenEnd, rc.pos())), + _ => Err(Error::Corrupt), + }; + } + let s = ppmd_symbol(&mut pb.model, &mut rc)?; + if s != pb.escape { ctx.emit_literal(s); continue; } - let code = sym(&mut model, &mut rc)?; + let code = ppmd_symbol(&mut pb.model, &mut rc)?; match code { 0 => { - // start-new-table: a fresh block header follows. Supporting - // a mid-stream codec switch (back to Huffman, or a new PPMd - // table) is out of scope; no single-file corpus archive - // reaches this before the unpacked size is met. - return Err(Error::Unsupported); + // start-new-table: a fresh block header follows in the bit + // domain at the coder's byte position (it may keep PPMd + // with or without a model reset, or switch back to LZ). + return Ok((Segment::NewTable, rc.pos())); + } + 2 => { + // End of PPMd data before the declared size: short member. + // Solid mode turns this into an error in `run_member`. + return Ok((Segment::MemberEnd, rc.pos())); + } + 3 => { + // A filter declaration carried in the PPMd stream: the same + // wire layout as main symbol 257, with every byte decoded + // through the model. + read_filter_declaration_ppmd(ctx, &mut pb.model, &mut rc)?; } - 2 => break, // end of PPMd data - 3 => return Err(Error::Unsupported), // VM filter in PPMd stream 4 => { // 24-bit distance from three symbols (big-endian), then a // length symbol. Distance +2, length +32. let mut dist = 0u32; for i in (0..3).rev() { - let b = sym(&mut model, &mut rc)? as u32; + let b = ppmd_symbol(&mut pb.model, &mut rc)? as u32; dist |= b << (i * 8); } - let len = sym(&mut model, &mut rc)? as u32; + let len = ppmd_symbol(&mut pb.model, &mut rc)? as u32; ctx.emit_match(dist + 2, len + 32)?; } 5 => { // Distance-1 run: length symbol, length +4. - let len = sym(&mut model, &mut rc)? as u32; + let len = ppmd_symbol(&mut pb.model, &mut rc)? as u32; ctx.emit_match(1, len + 4)?; } _ => { // Any other control code encodes a literal equal to the // escape byte (the control symbol is consumed and dropped). - ctx.emit_literal(hdr.escape); + ctx.emit_literal(pb.escape); } } } - Ok(()) +} + +/// Parse a filter declaration whose bytes arrive as PPMd symbols (escape +/// code 3): an 8-bit flags byte, a 1/2/3-byte length field, then `length` +/// payload bytes forming the same self-contained declaration payload the +/// bit-domain parser (main symbol 257) reads. +fn read_filter_declaration_ppmd( + ctx: &mut RunCtx, + model: &mut Ppmd7, + rc: &mut RangeDec, +) -> Result<(), Error> { + let flags = ppmd_symbol(model, rc)? as u32; + let mut decl_len = (flags & 0x07) + 1; + if decl_len == 7 { + decl_len = ppmd_symbol(model, rc)? as u32 + 7; + } else if decl_len == 8 { + let hi = ppmd_symbol(model, rc)? as u32; + let lo = ppmd_symbol(model, rc)? as u32; + decl_len = (hi << 8) | lo; + } + if decl_len == 0 { + return Err(Error::Corrupt); + } + let mut payload = vec![0u8; decl_len as usize]; + for b in payload.iter_mut() { + *b = ppmd_symbol(model, rc)?; + } + let mut db = BitReader::new(); + db.feed_slice(&payload); + parse_declaration_payload(ctx, flags, &mut db).map_err(|e| match e { + Error::UnexpectedEnd => Error::Corrupt, + other => other, + }) } // ─── In-band filter declarations (main symbol 257) ────────────────────── -/// Upper bound on a filter's block length, derived from the RarVM memory -/// the standard programs operate in (0x40000 bytes, of which 0x3C000 lie -/// below the global-data area). Delta needs separate source and -/// destination halves, so its windows are capped at half that. Real -/// encoders stay far below both caps and split large regions into several -/// filter blocks. -const FILTER_MAX_BLOCK: u32 = 0x3C000; -const FILTER_MAX_BLOCK_DELTA: u32 = 0x1E000; +/// Upper bound on a filter's block length: the RarVM memory the standard +/// programs operate in (0x40000 bytes — modern unrar lets a block use all +/// of it). Delta needs separate source and destination halves, so its +/// windows are capped at half that. Real encoders stay far below both caps +/// and split large regions into several filter blocks. Beyond the cap +/// UnRAR 7.23 skips the transform and emits the raw bytes with success; +/// this crate fails closed instead (same policy as unfinished windows). +const FILTER_MAX_BLOCK: u32 = 0x40000; +const FILTER_MAX_BLOCK_DELTA: u32 = 0x20000; /// Read a RarVM variable-length number: a 2-bit tag selects a 4-, 8- /// (with a sign-extension-style escape for values below 16), 16- or 32-bit @@ -973,11 +1214,14 @@ fn parse_declaration_payload( let slot = if flags & 0x80 != 0 { let v = read_vm_number(db)?; if v == 0 { - // Full reset (unrar's InitFilters): apply the filters whose - // windows the stream already completed, then cancel everything - // else — a canceled filter must never run, or it would rewrite - // output the encoder didn't transform. - ctx.flush_completed_filters()?; + // Full reset (unrar's InitFilters30): cancel every scheduled + // filter — *including* ones whose windows are complete but not + // yet applied. unrar executes filters only when it flushes + // decoded output, which lags decoding by up to a window, so a + // reset discards them and their windows stay raw bytes. (For + // multi-window outputs the reference may have flushed — and + // applied — earlier filters before the reset; real encoders + // only reset at stream start, so that corner stays unmodeled.) ctx.pending_filters.clear(); ctx.programs.clear(); 0 @@ -1161,6 +1405,8 @@ mod tests { last_filter_slot: 0, pending_filters: VecDeque::new(), ppmd: None, + block: BlockKind::Lz, + tables_read: false, } } @@ -1229,17 +1475,20 @@ mod tests { assert_eq!(recognize_program(&DELTA_PROG), Some(StdProgram::Delta)); } - /// A reset declaration (slot field 0) must first run the filters whose - /// windows are already complete, then cancel everything still pending — - /// a canceled filter must never rewrite output. + /// A reset declaration (slot field 0) cancels every scheduled filter — + /// including completed-but-unapplied windows. unrar's InitFilters30 + /// discards its whole filter stack (execution happens at output-flush + /// time, which lags decoding), so those windows stay raw bytes; a + /// canceled filter must never rewrite output. #[test] - fn reset_applies_completed_and_cancels_pending() { + fn reset_cancels_all_pending_without_applying() { let mut ctx = test_ctx(); ctx.out = vec![1, 0, 0, 0, 9, 9, 9, 9]; ctx.programs.push(ProgramSlot { program: StdProgram::Delta, last_block_length: 4, }); + // A completed-but-unapplied window plus an incomplete one. ctx.pending_filters.push_back(PendingFilter { start: 0, length: 4, @@ -1264,12 +1513,10 @@ mod tests { db.feed_slice(&w.bytes); parse_declaration_payload(&mut ctx, 0xB0, &mut db).unwrap(); - // The completed 1-channel delta over [1,0,0,0] ran: prev-integrate - // gives [0xFF; 4]. The trailing bytes stay raw. - assert_eq!(&ctx.out[..4], &[0xFF; 4]); - assert_eq!(&ctx.out[4..], &[9; 4]); - // The incomplete filter was canceled; only the fresh declaration - // (window at out position 8) is scheduled against the fresh slot. + // Nothing ran: both prior filters were canceled outright. + assert_eq!(ctx.out, vec![1, 0, 0, 0, 9, 9, 9, 9]); + // Only the fresh declaration (window at out position 8) is + // scheduled against the fresh slot table. assert_eq!(ctx.pending_filters.len(), 1); assert_eq!(ctx.pending_filters[0].start, 8); assert_eq!(ctx.programs.len(), 1); @@ -1428,28 +1675,8 @@ mod tests { #[test] fn promote_offset_rotates_correctly() { // Construct a context-shaped struct just to test the helper. - let mut ctx = RunCtx { - bits: BitReader::new(), - lengths: vec![], - main: None, - offset: None, - low_offset: None, - length: None, - old_offsets: [10, 20, 30, 40], - last_offset: 0, - last_length: 0, - last_low_offset: 0, - num_low_offset_repeats: 0, - out: vec![], - window: vec![0u8; 16], - wmask: 15, - window_pos: 0, - unpack_size: 0, - programs: vec![], - last_filter_slot: 0, - pending_filters: VecDeque::new(), - ppmd: None, - }; + let mut ctx = test_ctx(); + ctx.old_offsets = [10, 20, 30, 40]; // Promote slot 2 (value 30) — result should be [30, 10, 20, 40]. promote_offset(&mut ctx, 2, 30); assert_eq!(ctx.old_offsets, [30, 10, 20, 40]); diff --git a/src/rar3/mod.rs b/src/rar3/mod.rs index 0e9a3d6..2620f7c 100644 --- a/src/rar3/mod.rs +++ b/src/rar3/mod.rs @@ -19,15 +19,16 @@ //! text-heavy archives and `-m5` (best compression) runs. //! //! This build implements the **LZ77 + Huffman path** in full, including the -//! in-band standard filters WinRAR declares via main symbol 257 (Delta and -//! x86 E8/E8E9, recognized by bytecode fingerprint and run natively — no -//! RarVM interpreter; unknown programs are refused), and **PPMd-II variant -//! H** blocks (the full PPMII model in [`crate::ppmd`], driven by the RAR -//! range decoder with the RAR literal/match/end-of-data escape layer). The -//! standalone E8/E9 (x86 near-call) post-pass filter can also be enabled -//! via [`Decoder::with_e8_filter`]. PPMd continuations across a new-table -//! boundary (solid multi-member streams) are refused — see the private -//! `decoder` submodule for the exact boundary. +//! in-band standard filters WinRAR declares via main symbol 257 or PPMd +//! escape code 3 (Delta and x86 E8/E8E9, recognized by bytecode fingerprint +//! and run natively — no RarVM interpreter; unknown programs are refused), +//! **PPMd-II variant H** blocks (the full PPMII model in [`crate::ppmd`], +//! driven by the RAR range decoder with the RAR literal/match/end-of-data +//! escape layer), mid-stream switches between the two, and **solid +//! multi-member groups** (shared LZ window, code tables, filter programs +//! and PPMd model across members — see [`Decoder::with_solid`] / +//! [`Decoder::begin_solid_member`]). The standalone E8/E9 (x86 near-call) +//! post-pass filter can also be enabled via [`Decoder::with_e8_filter`]. //! //! ## Calling convention //! @@ -53,6 +54,12 @@ //! } //! ``` //! +//! For a **solid group**, construct the decoder with +//! [`Decoder::with_solid`], decode the first member as above, then call +//! [`Decoder::begin_solid_member`] with the next member's unpacked size +//! before feeding its payload — each member's payload is its own +//! byte-aligned stream, but the compression history carries over. +//! //! ## References //! //! - libarchive `archive_read_support_format_rar.c` (BSD): structure and diff --git a/tests/fixtures/rar3/solid_m3_calls.bin b/tests/fixtures/rar3/solid_m3_calls.bin new file mode 100644 index 0000000000000000000000000000000000000000..e0f7859c268fd1810b096b5c0d7ece241b8db091 GIT binary patch literal 1321 zcmV+^1=jiw0aZ~5kIjMO*}0qX!+r+orh%aZElSY@ZDV8dwYIBmDOkv&EViv}rAb?A zBBHS>#t5+=fc_OxqQDi95K6%akh0PORcRw=(X@qaDv9vZT@Cu?Zf0&~TQ#+ZrK|O+ny!w2hWxF+m)g8Iu>&~+06R>XD(KH!;2Zex5;+6QanDajB&rjM;J~Qo7tJC?)h<5U#xb5 zPeQOxs+rE_pEBh_x`os}&i0ea)KV(TbabpeEqB+m1j7v$@l0lWuSZ>1s`IS%Mv=m{ z%4bhtRLL-v+Q(=26jn}ZaXZN^gfguaTOF zlA_|Fr@5HjN?1)Zm)+)vrL5X!GmU1#J{aYTDrqC-9EVtQt0itq~_QTMN z`|#c`YSK3|H^}ZweDctfoanurr^&z{SqseEuIJm)lg5o%(#%j3J9WY0N7Hty=}^^O z)=z0D^xSzUNeR5%|15>QKX1|2Nvz4V*WEg`ug^BQ?t!)j)=qZPFiFc)wFj8x?4mUi z{ONm252EhITkqRkXFi#32h^HtZ*e!E(Nbz}EcSb}5owyb89QRuI4Ka&P1H3Z0B#`Ks=K#D%8RhcxOpbx)4J`I1$twLx^&P zVyKdwl!XX{B^V()!JWWD;jjSjp@$TNKxCw>6z+iI5mp2ci=i5$Ix>b*;KtDSP7KBo zM3{A?5`}U2M+YQIb1zSXtSi@?oq0k$o(Oh=Byc7liOVox{2FH>j$wt4uw&ta0Kpw8 zT^6+}V}KnaSCA*d5+{U|+8YFwi`BFVBP9d`e2_T=6EhC=d64(%f^zT_YvN9G1V0HN zQqKbp_lX2=`vVj3P&@F~u6VVH3&41}J#qBle|H;TJ?*}miy`apVL!5cT*cY=W$zc) z52FPBgWF~Qeh1+ofioDY0H_fk*n#zW0rU^^JrR!j01Q-P8B+N6{Auyw@v9cd-yp^W zXZ6q} fJ8vW2?c2QYJAK=8V94`3ZCC%Oci(r{tNXu{4hECM literal 0 HcmV?d00001 diff --git a/tests/fixtures/rar3/solid_m3_gradient.bin b/tests/fixtures/rar3/solid_m3_gradient.bin new file mode 100644 index 0000000000000000000000000000000000000000..dbd29479860549055fcfc1e01404b6fd6402e513 GIT binary patch literal 443 zcmZSB-w}W7<^SCQ4jUL4+E{olGVuK4X%smeDiIj#^;E?FfRIDJjg8=sg33KJZ2$he z&-wjHblv~gYae~Md{27!;qr>S_q=x>?%rcw-uCXpU38)MGX&0p44n6c*(pTaJ4Dx_e2d^vJ|k4T3xez#b5G zoRaENV(*+)R9tD6z}f5|&uA0jnX`MpJX?MA|NrmzX|A8Yx5#Gqi}z}wWuL?X$;C%_ zBW*%@BLf(HLz*HPjC-3cC7CuzvZOSADl`&I5jj!X(+Lh#yBCPp2k7%( b(3iUq7yDq(AIGZ7rmtUFf3yC-{NDipB-Yud literal 0 HcmV?d00001 diff --git a/tests/fixtures/rar3/solid_m3_notes.bin b/tests/fixtures/rar3/solid_m3_notes.bin new file mode 100644 index 0000000000000000000000000000000000000000..80568b1ca7130e28f6155683971bff13eaf73ace GIT binary patch literal 170 zcmWH|NL4Rnx^(QC-;pQx-rVs!bMKws71pf>&&=I>b?S+^m;ZgsVfggH@5H@xDgJ*y s@W_8&{ree@g#Dv`KWIh%KPV^|xc|TXz7+=bXV<@91+?d>Q~WOm03YaIR{#J2 literal 0 HcmV?d00001 diff --git a/tests/fixtures/rar3/solid_m3_photo.bin b/tests/fixtures/rar3/solid_m3_photo.bin new file mode 100644 index 0000000000000000000000000000000000000000..a545c5343a3e6c7a704c7d06cf47e3c144b7aa5d GIT binary patch literal 8222 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 literal 0 HcmV?d00001 diff --git a/tests/fixtures/rar3/solid_m3_ramp.bin b/tests/fixtures/rar3/solid_m3_ramp.bin new file mode 100644 index 0000000000000000000000000000000000000000..577c5a769692d3730291f0932bde2573dc590f62 GIT binary patch literal 297 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 0 { + dec.begin_solid_member(unp).unwrap(); + } + let (_p, _s) = dec.decode(payload, &mut []).unwrap(); + let mut out = Vec::with_capacity(unp as usize); + drain(&mut dec, &mut out); + assert_eq!(out.len() as u64, unp, "member {i}: unpacked size mismatch"); + assert_eq!( + crc32(&out), + want_crc, + "member {i}: decoded bytes differ from the archive's FILE_CRC" + ); + } +} + +#[test] +fn solid_lz_group_decodes() { + decode_solid_group(SOLID_M3_GROUP); +} + +#[test] +fn solid_ppmd_group_decodes() { + decode_solid_group(&[ + (PPMD_NOTES, 20001, 0x0E1A_EC07), + (SOLID_PPMD_PROSE, 236737, 0x028E_1AC1), + ]); +} + +/// Feeding a solid continuation without its group head must fail closed: +/// prose.txt's block header reuses a live PPMd model that a fresh decoder +/// doesn't have. +#[test] +fn solid_ppmd_continuation_without_head_fails() { + let mut dec = Decoder::with_unpack_size(236737).with_solid(); + let (_p, _s) = dec.decode(SOLID_PPMD_PROSE, &mut []).unwrap(); + let mut buf = [0u8; 4096]; + assert_eq!(dec.finish(&mut buf).unwrap_err(), Error::Corrupt); +} + +/// A truncated member inside a solid group is a hard error — the shared +/// history would silently desync every member after it. +#[test] +fn solid_truncated_member_poisons_group() { + let mut dec = Decoder::with_unpack_size(6146).with_solid(); + let head = SOLID_M3_GROUP[0].0; + let (_p, _s) = dec.decode(&head[..head.len() / 2], &mut []).unwrap(); + let mut buf = [0u8; 4096]; + assert!(matches!( + dec.finish(&mut buf).unwrap_err(), + Error::UnexpectedEnd | Error::Corrupt + )); + // The group is poisoned: the next member can't be started. + assert!(dec.begin_solid_member(32768).is_err()); +} + +/// `begin_solid_member` is only valid on a solid decoder whose current +/// member has fully drained. +#[test] +fn begin_solid_member_misuse_is_rejected() { + // Not a solid decoder. + let mut dec = Decoder::with_unpack_size(4); + assert_eq!(dec.begin_solid_member(4).unwrap_err(), Error::Unsupported); + // Solid, but the current member hasn't been decoded yet. + let mut dec = Decoder::with_unpack_size(6146).with_solid(); + assert_eq!(dec.begin_solid_member(4).unwrap_err(), Error::Unsupported); +} + // ─── factory (only if compiled in) ─────────────────────────────────────── #[cfg(feature = "factory")] From f558c9944b389fd3a05afb7f940e41409593e6ae Mon Sep 17 00:00:00 2001 From: JD Lien Date: Thu, 16 Jul 2026 17:09:33 -0600 Subject: [PATCH 7/8] fix(rar3,rar5,ppmd): harden per high-effort branch review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Findings from the adversarially-verified multi-agent review of this branch, all confirmed against reference semantics: - rar_filters: the shared x86 E8/E8E9 transform gains a wrap_16m mode switch. RAR5 keeps the 16 MiB position-base mask (validated against real archives); RAR3's VM filter uses the unmasked 32-bit position (per libarchive's RAR3 reader) — the shared helper had silently applied RAR5 masking to RAR3, which would corrupt filter windows past 16 MiB of a large executable while reporting success. - ppmd7: decode_symbol now bounds num_stats (raw arena u16) before its `- 1` / `- num_masked` walks — a corrupt context tree could wrap the subtraction into a debug panic, a ps[256] out-of-bounds index, or a ~2^32-iteration stall. Fail closed with Error::Corrupt. Plus hot-path cleanups: char_mask is built only on the escape path, ps is hoisted out of the escape loop, and st_copy/st_swap move states with one range check instead of 12-24 byte-wise accessor calls. - rar5: raw_reset() clears file_boundaries — a decoder reused for an unrelated stream kept the previous solid group's boundaries and skewed every later x86 filter's position base (silent wrong bytes). - rar3 filters: Delta channel count capped at unrar's MAX3_UNPACK_CHANNELS (1024) and no longer rejected merely for exceeding the window length (well-defined, and unrar decodes it); out-of-range counts fail closed where unrar emits raw bytes with success. - ppmd decoder: the 11-byte standalone header is validated as soon as it arrives (fail-fast from decode(), sticky error) instead of after buffering an arbitrarily large payload. Differential harness unchanged at 206 pass / 0 mismatch; rar3 + ppmd fuzz corpora replay clean under ASan. Claude-Session: https://claude.ai/code/session_01DYLfPTghh7DayY4M7YyeKY --- src/ppmd/decoder.rs | 97 ++++++++++++++++++++++++++++----------------- src/ppmd/ppmd7.rs | 60 +++++++++++++++++++--------- src/rar3/filters.rs | 24 +++++++---- src/rar5/decoder.rs | 3 ++ src/rar5/filters.rs | 4 +- src/rar_filters.rs | 47 ++++++++++++++++++---- tests/ppmd.rs | 14 +++++++ 7 files changed, 177 insertions(+), 72 deletions(-) diff --git a/src/ppmd/decoder.rs b/src/ppmd/decoder.rs index 539964c..52f28f1 100644 --- a/src/ppmd/decoder.rs +++ b/src/ppmd/decoder.rs @@ -34,8 +34,49 @@ pub struct Decoder { decoded: Vec, decoded_idx: usize, started: bool, + header_checked: bool, finished_decode: bool, - poisoned: bool, + /// Set on the first irrecoverable error; every later call re-reports + /// the same error (so an early header rejection in `decode` reads the + /// same from a follow-up `finish`). + poisoned: Option, +} + +/// Validate the 11-byte framing header, returning the declared unpacked +/// length. Called from `raw_decode` as soon as 11 bytes have been buffered +/// (so a hostile stream with a bad header is rejected immediately instead +/// of after buffering its whole payload) and again from `run_decode`. +fn validate_header(h: &[u8]) -> Result { + let order = h[0] as u32; + let mem_mb = h[1] as u32; + let restoration = h[2]; + if !(2..=64).contains(&order) { + return Err(Error::BadHeader); + } + if !(1..=255).contains(&mem_mb) { + return Err(Error::BadHeader); + } + if restoration > 2 { + return Err(Error::BadHeader); + } + let expected_len = u64::from_le_bytes(h[3..11].try_into().unwrap()); + // PPMd carries no in-band end-of-stream marker, so a stream whose + // header declares an unknown length has no reliable terminal + // condition: after the true last symbol the range coder's finalisation + // bytes keep decoding into extra (garbage) symbols, and exhausting the + // physical input is not an end signal (a high-probability symbol + // decodes without consuming any input). Refuse rather than emit a + // guess. + if expected_len == UNKNOWN_LEN { + return Err(Error::Unsupported); + } + // A declared length larger than the buffer-then-decode ceiling can't + // be produced here; reject it up front rather than growing the output + // toward OOM. + if expected_len > MAX_OUTPUT as u64 { + return Err(Error::OutputLimitExceeded); + } + Ok(expected_len) } impl Decoder { @@ -45,13 +86,14 @@ impl Decoder { decoded: Vec::new(), decoded_idx: 0, started: false, + header_checked: false, finished_decode: false, - poisoned: false, + poisoned: None, } } fn poison(&mut self, e: Error) -> Error { - self.poisoned = true; + self.poisoned = Some(e); e } @@ -63,17 +105,7 @@ impl Decoder { let h = &self.in_buf[..HEADER_LEN]; let order = h[0] as u32; let mem_mb = h[1] as u32; - let restoration = h[2]; - if !(2..=64).contains(&order) { - return Err(Error::BadHeader); - } - if !(1..=255).contains(&mem_mb) { - return Err(Error::BadHeader); - } - if restoration > 2 { - return Err(Error::BadHeader); - } - let expected_len = u64::from_le_bytes(h[3..11].try_into().unwrap()); + let expected_len = validate_header(h)?; let mem_bytes = mem_mb.saturating_mul(1024 * 1024); let mut model = Ppmd7::new(mem_bytes)?; @@ -82,22 +114,6 @@ impl Decoder { let (mut rc, consumed) = RangeDec::init(Mode::SevenZip, &self.in_buf, HEADER_LEN)?; let _ = consumed; - // PPMd carries no in-band end-of-stream marker, so a stream whose - // header declares an unknown length has no reliable terminal - // condition: after the true last symbol the range coder's - // finalisation bytes keep decoding into extra (garbage) symbols, and - // exhausting the physical input is not an end signal (a high- - // probability symbol decodes without consuming any input). Refuse - // rather than emit a guess. - if expected_len == UNKNOWN_LEN { - return Err(Error::Unsupported); - } - // A declared length larger than the buffer-then-decode ceiling can't - // be produced here; reject it up front rather than growing `out` - // toward OOM. - if expected_len > MAX_OUTPUT as u64 { - return Err(Error::OutputLimitExceeded); - } let mut out = Vec::with_capacity((expected_len as usize).min(1 << 20)); for _ in 0..expected_len { @@ -151,11 +167,19 @@ impl Default for Decoder { impl RawDecoder for Decoder { fn raw_decode(&mut self, input: &[u8], output: &mut [u8]) -> Result { - if self.poisoned { - return Err(Error::Corrupt); + if let Some(e) = self.poisoned { + return Err(e); } - // Absorb input; real decoding is deferred to `finish`. + // Absorb input; real decoding is deferred to `finish`. The header + // is validated as soon as it is complete so a hostile stream fails + // after 11 bytes instead of after buffering its whole payload. self.in_buf.extend_from_slice(input); + if !self.header_checked && self.in_buf.len() >= HEADER_LEN { + self.header_checked = true; + if let Err(e) = validate_header(&self.in_buf[..HEADER_LEN]) { + return Err(self.poison(e)); + } + } let mut written = 0usize; if self.finished_decode { self.drain(output, &mut written); @@ -168,8 +192,8 @@ impl RawDecoder for Decoder { } fn raw_finish(&mut self, output: &mut [u8]) -> Result { - if self.poisoned { - return Err(Error::Corrupt); + if let Some(e) = self.poisoned { + return Err(e); } if !self.started { self.started = true; @@ -192,7 +216,8 @@ impl RawDecoder for Decoder { self.decoded.clear(); self.decoded_idx = 0; self.started = false; + self.header_checked = false; self.finished_decode = false; - self.poisoned = false; + self.poisoned = None; } } diff --git a/src/ppmd/ppmd7.rs b/src/ppmd/ppmd7.rs index d4c79bb..1957959 100644 --- a/src/ppmd/ppmd7.rs +++ b/src/ppmd/ppmd7.rs @@ -343,22 +343,28 @@ impl Ppmd7 { self.pu16(s + 4, ((v >> 16) & 0xFFFF) as u16); } - /// Copy state `src` onto state `dst` (6 bytes). + /// Copy state `src` onto state `dst` (6 bytes) — one range check, one + /// block copy, like [`Ppmd7::copy_units`] (rescale's insertion sort + /// calls this in a loop). #[inline] fn st_copy(&mut self, dst: u32, src: u32) { - for i in 0..6 { - let b = self.gu8(src + i); - self.pu8(dst + i, b); + let (d, s) = (dst as usize, src as usize); + if d + 6 <= self.base.len() && s + 6 <= self.base.len() { + self.base.copy_within(s..s + 6, d); + } else { + self.err = true; } } /// Swap two 6-byte states. #[inline] fn st_swap(&mut self, a: u32, b: u32) { - for i in 0..6 { - let x = self.gu8(a + i); - let y = self.gu8(b + i); - self.pu8(a + i, y); - self.pu8(b + i, x); + let (a, b) = (a as usize, b as usize); + if a + 6 <= self.base.len() && b + 6 <= self.base.len() { + for i in 0..6 { + self.base.swap(a + i, b + i); + } + } else { + self.err = true; } } @@ -1094,9 +1100,20 @@ impl Ppmd7 { /// Decode one byte symbol. `Err(Corrupt)` on model/stream inconsistency /// (the reference's `-1`/`-2` returns) or arena OOB. pub(crate) fn decode_symbol(&mut self, rc: &mut RangeDec) -> Result { - let mut char_mask = [0u8; 256]; + // Initialised on entry to the escape path; the fast paths below + // return without ever reading it. + let mut char_mask: [u8; 256]; + + // `num_stats` is a raw arena u16: a context ref that has been + // corrupted (or dangles into freed/zeroed arena) can present 0 or + // an impossible count (> 256 — the alphabet is bytes). Both would + // wrap the `- 1` walks below; fail closed instead. + let root_stats = self.ctx_num_stats(self.min_context); + if root_stats == 0 || root_stats > 256 { + return Err(Error::Corrupt); + } - if self.ctx_num_stats(self.min_context) != 1 { + if root_stats != 1 { let mut s = self.ctx_stats(self.min_context); let count = rc.get_threshold(self.ctx_summ_freq(self.min_context)); let mut hi_cnt = self.st_freq(s) as u32; @@ -1130,9 +1147,7 @@ impl Ppmd7 { } self.hi_bits_flag = self.hb2flag[self.st_symbol(self.found_state) as usize] as u32; rc.decode(hi_cnt, self.ctx_summ_freq(self.min_context) - hi_cnt); - for m in char_mask.iter_mut() { - *m = 0xFF; - } + char_mask = [0xFF; 256]; char_mask[self.st_symbol(s) as usize] = 0; let mut i = self.ctx_num_stats(self.min_context) - 1; while i != 0 { @@ -1158,15 +1173,14 @@ impl Ppmd7 { let newp = prob - get_mean(prob); self.bin_summ[row][col] = newp as u16; self.init_esc = K_EXP_ESCAPE[(newp >> 10) as usize & 0xF] as u32; - for m in char_mask.iter_mut() { - *m = 0xFF; - } + char_mask = [0xFF; 256]; let os = Self::one_state(self.min_context); char_mask[self.st_symbol(os) as usize] = 0; self.prev_success = 0; } // Escape loop. + let mut ps: [u32; 256] = [0; 256]; loop { if self.err || rc.err() { return Err(Error::Corrupt); @@ -1185,8 +1199,16 @@ impl Ppmd7 { } let mut hi_cnt = 0u32; let mut s = self.ctx_stats(self.min_context); - let num = self.ctx_num_stats(self.min_context) - num_masked; - let mut ps: [u32; 256] = [0; 256]; + // In a consistent model a suffix context is a superset of its + // children, so its `num_stats` exceeds the masked count and + // never tops 256 (byte alphabet). A corrupt tree can violate + // both; the subtraction would wrap and the `ps` walk below + // would run past the array. Fail closed. + let cur_stats = self.ctx_num_stats(self.min_context); + if cur_stats <= num_masked || cur_stats > 256 { + return Err(Error::Corrupt); + } + let num = cur_stats - num_masked; let mut i = 0usize; loop { let sym = self.st_symbol(s) as usize; diff --git a/src/rar3/filters.rs b/src/rar3/filters.rs index 21cf954..0a6eed6 100644 --- a/src/rar3/filters.rs +++ b/src/rar3/filters.rs @@ -79,21 +79,31 @@ pub(super) struct PendingFilter { pub channels: u32, } +/// Delta channel-count ceiling, matching unrar's `MAX3_UNPACK_CHANNELS` +/// (1024). unrar refuses to *run* the transform beyond it and emits the +/// raw bytes with success; this crate fails closed instead (same policy as +/// unfinished filter windows — surfacing an error beats returning bytes +/// that only a container CRC could flag). +const MAX_DELTA_CHANNELS: u32 = 1024; + /// Run a scheduled filter over its region (already sliced by the caller). +/// +/// The x86 transforms use the **unmasked** 32-bit position base (RAR3 VM +/// semantics — see [`x86_e8_decode`]); `filter.start` is file-relative, +/// which for solid archives means member-relative (unrar seeds the VM with +/// its per-member written-size counter). pub(super) fn apply_pending(filter: &PendingFilter, region: &mut [u8]) -> Result<(), Error> { match filter.program { StdProgram::Delta => { - if filter.channels == 0 || filter.channels as usize > region.len() { - // Channel count is supplied by the stream (register 0); - // 0 channels is meaningless and more channels than bytes - // means most planes are empty — real encoders produce - // neither. + if filter.channels == 0 || filter.channels > MAX_DELTA_CHANNELS { return Err(Error::Corrupt); } + // More channels than bytes is well-defined (trailing planes are + // empty) and unrar runs it; no length-based bound here. delta_decode(filter.channels as usize, region); } - StdProgram::X86Call => x86_e8_decode(filter.start, region, false), - StdProgram::X86CallJmp => x86_e8_decode(filter.start, region, true), + StdProgram::X86Call => x86_e8_decode(filter.start, region, false, false), + StdProgram::X86CallJmp => x86_e8_decode(filter.start, region, true, false), } Ok(()) } diff --git a/src/rar5/decoder.rs b/src/rar5/decoder.rs index ce89133..0cb0981 100644 --- a/src/rar5/decoder.rs +++ b/src/rar5/decoder.rs @@ -352,6 +352,9 @@ impl RawDecoder for Decoder { self.pending_filters.clear(); self.ready.clear(); self.out_queue_start = 0; + // A reset starts an unrelated stream: stale solid-group boundaries + // would skew every later x86 filter's position base. + self.file_boundaries.clear(); } } diff --git a/src/rar5/filters.rs b/src/rar5/filters.rs index 602f7bb..1501301 100644 --- a/src/rar5/filters.rs +++ b/src/rar5/filters.rs @@ -65,11 +65,11 @@ pub fn apply(filter: &Filter, buf: &mut [u8]) -> Result<(), Error> { let region = &mut buf[..filter.length as usize]; match filter.kind { FilterKind::X86Call => { - x86_e8_decode(filter.start, region, false); + x86_e8_decode(filter.start, region, false, true); Ok(()) } FilterKind::X86CallJmp => { - x86_e8_decode(filter.start, region, true); + x86_e8_decode(filter.start, region, true, true); Ok(()) } FilterKind::Delta { channels } => { diff --git a/src/rar_filters.rs b/src/rar_filters.rs index 6693978..193ef69 100644 --- a/src/rar_filters.rs +++ b/src/rar_filters.rs @@ -27,10 +27,18 @@ use alloc::vec::Vec; /// variant, `E9`) opcode's 4-byte relative target into absolute form, and /// this pass restores the original relative value. /// -/// `start` is the absolute position of `buf[0]` in the unpacked stream. -/// When `also_e9` is true the filter fires on `0xE8` *and* `0xE9`; when -/// false only on `0xE8`. -pub(crate) fn x86_e8_decode(start: u64, buf: &mut [u8], also_e9: bool) { +/// `start` is the absolute position of `buf[0]` in the unpacked stream +/// (file-relative for both generations). When `also_e9` is true the filter +/// fires on `0xE8` *and* `0xE9`; when false only on `0xE8`. +/// +/// `wrap_16m` selects the position-base arithmetic, where the two container +/// generations differ: RAR5 reduces the position modulo the 16 MiB virtual +/// file size (validated against real WinRAR archives in the differential +/// harness), while RAR3's VM filter uses the unmasked 32-bit position (per +/// libarchive's RAR3 reader; the two agree below 16 MiB, so windows past +/// 16 MiB of a large executable are where masking would corrupt RAR3 +/// output). +pub(crate) fn x86_e8_decode(start: u64, buf: &mut [u8], also_e9: bool, wrap_16m: bool) { const FILE_SIZE: u32 = 0x0100_0000; if buf.len() < 5 { // No room for a [opcode][4-byte rel] sequence. @@ -49,7 +57,10 @@ pub(crate) fn x86_e8_decode(start: u64, buf: &mut [u8], also_e9: bool) { let rel = u32::from_le_bytes([buf[i + 1], buf[i + 2], buf[i + 3], buf[i + 4]]); // The offset is computed *after* the opcode byte has been consumed, // so the relevant absolute position is start + i + 1. - let off = ((start + i as u64 + 1) as u32) & (FILE_SIZE - 1); + let mut off = (start + i as u64 + 1) as u32; + if wrap_16m { + off &= FILE_SIZE - 1; + } // Decode direction. The two range checks are NESTED on the sign of // `rel`, exactly as in unrar/libarchive: // @@ -148,7 +159,7 @@ mod tests { #[test] fn e8_rewrites_call_target() { let mut buf = vec![0x00, 0x00, 0xE8, 0x10, 0x00, 0x00, 0x00, 0x90, 0x90]; - x86_e8_decode(0, &mut buf, false); + x86_e8_decode(0, &mut buf, false, true); // off = 2 + 1 = 3; rel = 0x10 in 0..FILE_SIZE => rel - off. let expected = 0x10u32.wrapping_sub(3).to_le_bytes(); assert_eq!(&buf[3..7], &expected); @@ -158,9 +169,29 @@ mod tests { fn e8_ignores_e9_unless_extended() { let mut buf = vec![0xE9, 0x10, 0x00, 0x00, 0x00]; let orig = buf.clone(); - x86_e8_decode(0, &mut buf, false); + x86_e8_decode(0, &mut buf, false, true); assert_eq!(buf, orig); - x86_e8_decode(0, &mut buf, true); + x86_e8_decode(0, &mut buf, true, true); assert_ne!(buf, orig); } + + /// RAR3 (unmasked) and RAR5 (16 MiB-wrapped) position bases agree below + /// 16 MiB and diverge above — a >16 MiB window must subtract the full + /// offset on the RAR3 path. + #[test] + fn e8_base_masking_diverges_past_16mib() { + const START: u64 = 0x0100_0000; // exactly 16 MiB + let src = vec![0xE8, 0x10, 0x00, 0x00, 0x00]; + + let mut unmasked = src.clone(); + x86_e8_decode(START, &mut unmasked, false, false); + // off = 16 MiB + 1; rel = 0x10 < FILE_SIZE => rel - off (wrapping). + let want = 0x10u32.wrapping_sub(0x0100_0001).to_le_bytes(); + assert_eq!(&unmasked[1..5], &want); + + let mut masked = src.clone(); + x86_e8_decode(START, &mut masked, false, true); + // off wraps to 1 => rel - 1. + assert_eq!(&masked[1..5], &0x0Fu32.to_le_bytes()); + } } diff --git a/tests/ppmd.rs b/tests/ppmd.rs index 209c937..9c08071 100644 --- a/tests/ppmd.rs +++ b/tests/ppmd.rs @@ -247,6 +247,20 @@ fn header_bad_restoration_is_bad_header() { assert_eq!(dec.finish(&mut buf), Err(Error::BadHeader)); } +/// A hostile stream with an obviously invalid header must be rejected as +/// soon as the 11 header bytes have arrived — from `decode` itself — not +/// after the caller has piped (and the decoder buffered) the whole payload. +#[test] +fn bad_header_is_rejected_at_decode_time() { + let mut bad = make_header(0, 16, 0, 100); // order 0: invalid + bad.extend_from_slice(&[0u8; 64]); + let mut dec = Decoder::new(); + let mut buf = [0u8; 16]; + assert_eq!(dec.decode(&bad, &mut buf).unwrap_err(), Error::BadHeader); + // The error is sticky and stays the same across calls. + assert_eq!(dec.finish(&mut buf), Err(Error::BadHeader)); +} + #[test] fn unknown_declared_length_is_refused() { // PPMd has no in-band end-of-stream marker, so a stream framed with the From 4810f35303ab3b3bea70a778829d30b85ae4c5fa Mon Sep 17 00:00:00 2001 From: JD Lien Date: Thu, 16 Jul 2026 18:05:51 -0600 Subject: [PATCH 8/8] perf(rar3): preallocate output buffer + fuse bit consume/Huffman fast path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two low-risk decode speedups, both byte-identical (differential harness 206 pass / 0 mismatch): - Preallocate `out` from the member's `unpack_size` (capped at 64 MiB so a hostile size can't drive a huge up-front allocation; the `u64::MAX` unknown-length sentinel reserves nothing). The output buffer previously grew from empty, reallocating ~log2(size) times mid-decode — ~26 grow-and-copy passes on a 61 MB member. This is the bulk of the win: match-heavy text 1496 -> 1879 MB/s (+26%), x86 1149 -> 1276 (+11%). - Add a check-free `BitReader::consume` for use right after a successful `peek` (the availability re-check in `drop_bits` is redundant there); route `read_bits` and the Huffman LUT fast path through it. After a 9-bit LUT miss the code is provably >9 bits, so the canonical slow-path scan starts at length 10. Small consistent x86 gain; literal/Huffman profile unchanged (it is not bit-decode-bound). Measured note for the record: directly skipping the sliding-window writes (a throwaway experiment) produced ~no speedup, so the out+window double write is not the bottleneck and an out-as-window refactor would not pay off. --- src/rar3/bits.rs | 21 ++++++++++++++++++++- src/rar3/decoder.rs | 21 +++++++++++++++++++-- src/rar3/huffman.rs | 13 +++++++++++-- 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/src/rar3/bits.rs b/src/rar3/bits.rs index 2fca1af..050bdbb 100644 --- a/src/rar3/bits.rs +++ b/src/rar3/bits.rs @@ -84,10 +84,29 @@ impl BitReader { Ok(()) } + /// Consume `n` bits that a prior `peek(m >= n)` on the *same* buffered + /// state already proved are available — no availability check. The caller + /// must guarantee `n <= nbits` (true immediately after a successful + /// `peek`); misuse corrupts the bit position, so keep it to the hot paths + /// that peek-then-consume. `n == 0` is a no-op (the shift below is only + /// valid for `1..=64`). + #[inline] + pub fn consume(&mut self, n: u32) { + debug_assert!(n <= self.nbits); + if n == 0 { + return; + } + self.acc <<= n; + self.nbits -= n; + } + /// Read and consume `n` bits in a single call. + #[inline] pub fn read_bits(&mut self, n: u32) -> Result { + // `peek` guarantees `nbits >= n` on success, so the consume is + // check-free. let v = self.peek(n)?; - self.drop_bits(n)?; + self.consume(n); Ok(v) } diff --git a/src/rar3/decoder.rs b/src/rar3/decoder.rs index c5f1826..3aeac93 100644 --- a/src/rar3/decoder.rs +++ b/src/rar3/decoder.rs @@ -302,13 +302,30 @@ impl RawDecoder for Decoder { /// Decode one member's compressed payload against the (possibly carried- /// over) context. In solid mode the end-of-member marker is consumed so the /// persistent state is exactly what the next member's stream expects. +/// Cap on speculative preallocation of the output buffer, so a hostile +/// `unpack_size` can't drive a huge up-front allocation. Real members past +/// this still decode; `out` just grows the rest of the way. +const OUT_PREALLOC_CAP: u64 = 64 << 20; + +/// Bytes to reserve up front for a member's output, from its declared +/// `unpack_size`. Avoids ~log2(size) grow-and-copy reallocations while a +/// large member decodes. The `u64::MAX` sentinel means "unknown length" +/// (no declared size) — reserve nothing rather than speculate. +fn out_prealloc(unpack_size: u64) -> usize { + if unpack_size == u64::MAX { + 0 + } else { + unpack_size.min(OUT_PREALLOC_CAP) as usize + } +} + fn run_member(ctx: &mut RunCtx, input: &[u8], solid: bool) -> Result, Error> { // Each member's payload is its own byte-aligned stream (the container // resets the bit input at every member boundary), so the reader is // rebuilt even when the rest of the context carries over. ctx.bits = BitReader::new(); ctx.bits.feed_slice(input); - ctx.out = Vec::new(); + ctx.out = Vec::with_capacity(out_prealloc(ctx.unpack_size)); // Filter *programs* persist across solid members, but scheduled filter // instances never span a member boundary. ctx.pending_filters.clear(); @@ -470,7 +487,7 @@ impl RunCtx { last_length: 0, last_low_offset: 0, num_low_offset_repeats: 0, - out: Vec::new(), + out: Vec::with_capacity(out_prealloc(unpack_size)), window: vec![0u8; DICT_DEFAULT_SIZE], wmask: { debug_assert!(DICT_DEFAULT_SIZE.is_power_of_two()); diff --git a/src/rar3/huffman.rs b/src/rar3/huffman.rs index 140d9ec..3ce1452 100644 --- a/src/rar3/huffman.rs +++ b/src/rar3/huffman.rs @@ -155,6 +155,12 @@ impl Huffman { return Err(Error::InvalidHuffmanTree); } let max = self.max_length as u32; + // Slow-path scan lower bound. A LUT miss below (peek succeeded but no + // code ≤ PRIMARY_BITS matched) proves the code is longer than + // PRIMARY_BITS, so the canonical scan can skip lengths 1..=PRIMARY_BITS + // entirely. It stays 1 only when the fast-path peek itself failed + // (stream near end), where a short code may still be valid. + let mut min_len = 1u32; // Fast path: when we can peek PRIMARY_BITS bits, a single LUT // lookup resolves any code of length ≤ PRIMARY_BITS. @@ -162,17 +168,20 @@ impl Huffman { let entry = self.lut[idx as usize]; let len = (entry >> LUT_LEN_SHIFT) as u32; if len > 0 { - reader.drop_bits(len)?; + // `peek(PRIMARY_BITS)` succeeded and `len <= PRIMARY_BITS`, so + // the bits are buffered — consume without re-checking. + reader.consume(len); return Ok(entry & LUT_SYM_MASK); } // Long code (> PRIMARY_BITS) -- fall through to the slow path. + min_len = PRIMARY_BITS + 1; } // Peek `max` bits; if not enough, peek the remaining smaller widths // one at a time. For RAR3 trees this is unlikely to matter -- most // codes fit in the buffer easily. let lookahead = self.peek_padded(reader, max)?; - for length in 1..=max { + for length in min_len..=max { let code = lookahead >> (max - length); let count = self.counts[length as usize] as u32; if count > 0 {