Skip to main content

tsaur_core/
codec.rs

1//! Compression codecs. v0.1 ships store, zstd, zstd-with-dictionary and xz (LZMA2); the codec id
2//! space is reserved for context mixing, model-predictive, delta and float-split codecs (spec §4.2).
3
4use crate::error::{Error, Result};
5use std::io::{Read, Write};
6use zstd::stream::raw::{CParameter, DParameter};
7
8pub const STORE: u8 = 0;
9pub const ZSTD: u8 = 1;
10pub const ZSTD_DICT: u8 = 2;
11pub const XZ: u8 = 3;
12pub const PPMD: u8 = 4;
13/// zstd with a raw-content dictionary made of chunks of an earlier version (`BlobRecord.d`).
14pub const ZSTD_DELTA: u8 = 5;
15
16pub fn name(codec: u8) -> &'static str {
17    match codec {
18        STORE => "store",
19        ZSTD => "zstd",
20        ZSTD_DICT => "zstd+dict",
21        XZ => "xz",
22        PPMD => "ppmd",
23        ZSTD_DELTA => "zstd+delta",
24        _ => "unknown",
25    }
26}
27
28/// Which codecs the packer may try for a block.
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
30pub enum CodecChoice {
31    /// zstd only (fast)
32    Zstd,
33    /// xz/LZMA2 only (slower, usually smaller on text)
34    Xz,
35    /// PPMd (variant H) only — text specialist, symmetric (decoding as slow as encoding)
36    Ppmd,
37    /// try zstd and xz (and PPMd on text-like blocks) and keep the smallest output (default)
38    Best,
39}
40
41impl CodecChoice {
42    pub fn by_name(s: &str) -> Option<Self> {
43        match s.to_ascii_lowercase().as_str() {
44            "zstd" => Some(Self::Zstd),
45            "xz" | "lzma" | "lzma2" => Some(Self::Xz),
46            "ppmd" => Some(Self::Ppmd),
47            "best" | "auto" => Some(Self::Best),
48            _ => None,
49        }
50    }
51}
52
53/// Encoded output plus the parameters the decoder needs.
54#[derive(Clone, Debug)]
55pub struct Encoded {
56    pub codec: u8,
57    pub bytes: Vec<u8>,
58    pub params: Vec<u32>,
59    /// Pre-filter applied before the codec (0 = none, 1 = x86 BCJ, 2 = ARM64 BCJ).
60    pub filter: u8,
61}
62
63pub const FILTER_NONE: u8 = 0;
64pub const FILTER_X86: u8 = 1;
65
66/// x86 BCJ branch converter (the classic LZMA SDK `x86.c` transform, public domain): rewrites the
67/// relative targets of `E8`/`E9` CALL/JMP instructions into absolute addresses so that repeated
68/// calls to the same function become identical byte sequences. Symmetric: `encoding = false`
69/// restores the original bytes exactly. Returns the number of bytes fully processed.
70pub fn x86_convert(data: &mut [u8], ip: u32, state: &mut u32, encoding: bool) -> usize {
71    #[inline]
72    fn test_ms_byte(b: u8) -> bool {
73        (b.wrapping_add(1) & 0xFE) == 0
74    }
75    if data.len() < 5 {
76        return 0;
77    }
78    let size = data.len() - 4;
79    let ip = ip.wrapping_add(5);
80    let mut pos = 0usize;
81    let mut mask = *state & 7;
82    loop {
83        let mut p = pos;
84        while p < size && (data[p] & 0xFE) != 0xE8 {
85            p += 1;
86        }
87        let d = p - pos;
88        pos = p;
89        if p >= size {
90            *state = if d > 2 { 0 } else { mask >> d };
91            return pos;
92        }
93        if d > 2 {
94            mask = 0;
95        } else {
96            mask >>= d;
97            if mask != 0 && (mask > 4 || mask == 3 || test_ms_byte(data[p + (mask as usize >> 1) + 1])) {
98                mask = (mask >> 1) | 4;
99                pos += 1;
100                continue;
101            }
102        }
103        if test_ms_byte(data[p + 4]) {
104            let mut v = u32::from_le_bytes([data[p + 1], data[p + 2], data[p + 3], data[p + 4]]);
105            let cur = ip.wrapping_add(pos as u32);
106            pos += 5;
107            if encoding {
108                v = v.wrapping_add(cur);
109            } else {
110                v = v.wrapping_sub(cur);
111            }
112            if mask != 0 {
113                let sh = (mask & 6) << 2;
114                if test_ms_byte((v >> sh) as u8) {
115                    v ^= (0x100u32 << sh).wrapping_sub(1);
116                    if encoding {
117                        v = v.wrapping_add(cur);
118                    } else {
119                        v = v.wrapping_sub(cur);
120                    }
121                }
122                mask = 0;
123            }
124            data[p + 1] = v as u8;
125            data[p + 2] = (v >> 8) as u8;
126            data[p + 3] = (v >> 16) as u8;
127            data[p + 4] = 0u8.wrapping_sub(((v >> 24) & 1) as u8);
128        } else {
129            mask = (mask >> 1) | 4;
130            pos += 1;
131        }
132    }
133}
134
135/// Heuristic: x86/x64 machine code has many `E8`/`E9` CALL/JMP opcodes whose rel32 operand has a
136/// most-significant byte of `00` or `FF` (near targets). Random bytes hit that pattern about
137/// 0.006 % of the time; real code sections hit it 0.5–2 % of the time.
138pub fn looks_like_x86(data: &[u8]) -> bool {
139    if data.len() < 4096 || looks_like_text(data) {
140        return false;
141    }
142    let sample = &data[..data.len().min(256 << 10)];
143    let hits = sample.windows(5).filter(|w| (w[0] & 0xFE) == 0xE8 && (w[4] == 0 || w[4] == 0xFF)).count();
144    hits * 1000 / sample.len() >= 1
145}
146
147/// ARM64 branch converter (the `arm64` BCJ filter from XZ Utils, 0BSD): converts the immediate of
148/// `BL` and `ADRP` instructions from PC-relative to absolute form. Symmetric, works on 4-byte words.
149pub fn arm64_convert(data: &mut [u8], now_pos: u32, encoding: bool) -> usize {
150    let mut i = 0usize;
151    while i + 4 <= data.len() {
152        let pc = now_pos.wrapping_add(i as u32);
153        let mut instr = u32::from_le_bytes([data[i], data[i + 1], data[i + 2], data[i + 3]]);
154        if (instr >> 26) == 0x25 {
155            let src = instr;
156            instr = 0x9400_0000;
157            let mut p = pc >> 2;
158            if !encoding {
159                p = 0u32.wrapping_sub(p);
160            }
161            instr |= src.wrapping_add(p) & 0x03FF_FFFF;
162            data[i..i + 4].copy_from_slice(&instr.to_le_bytes());
163        } else if (instr & 0x9F00_0000) == 0x9000_0000 {
164            let src = ((instr >> 29) & 3) | ((instr >> 3) & 0x001F_FFFC);
165            if (src.wrapping_add(0x0002_0000) & 0x001C_0000) == 0 {
166                instr &= 0x9000_001F;
167                let mut p = pc >> 12;
168                if !encoding {
169                    p = 0u32.wrapping_sub(p);
170                }
171                let dest = src.wrapping_add(p);
172                instr |= (dest & 3) << 29;
173                instr |= (dest & 0x0003_FFFC) << 3;
174                instr |= 0u32.wrapping_sub(dest & 0x0002_0000) & 0x00E0_0000;
175                data[i..i + 4].copy_from_slice(&instr.to_le_bytes());
176            }
177        }
178        i += 4;
179    }
180    i
181}
182
183/// Heuristic: ARM64 code is a stream of 4-byte instructions with frequent `BL` calls to near
184/// targets (imm26 sign-extended, magnitude below 2^20 words). Random words qualify 0.05 % of the time;
185/// real code 1–5 %. Blocks start at arbitrary chunk boundaries, so all four word alignments are
186/// tried; returns the byte offset of the first aligned instruction word.
187pub fn arm64_alignment(data: &[u8]) -> Option<u8> {
188    if data.len() < 4096 || looks_like_text(data) {
189        return None;
190    }
191    let sample = &data[..data.len().min(256 << 10)];
192    let words = sample.len() / 4;
193    let mut best = (0usize, 0u8);
194    for off in 0..4usize {
195        let hits = sample[off..]
196            .chunks_exact(4)
197            .filter(|w| {
198                let v = u32::from_le_bytes([w[0], w[1], w[2], w[3]]);
199                (v >> 26) == 0x25 && matches!((v >> 20) & 0x3F, 0 | 0x3F)
200            })
201            .count();
202        if hits > best.0 {
203            best = (hits, off as u8);
204        }
205    }
206    (best.0 * 1000 / words.max(1) >= 5).then_some(best.1)
207}
208
209/// Filter byte layout: low nibble = filter id, high nibble = filter parameter
210/// (ARM64: byte offset of the first aligned instruction word, 0..3).
211pub fn filter_id(f: u8) -> u8 {
212    f & 0x0F
213}
214
215pub fn filter_param(f: u8) -> u8 {
216    f >> 4
217}
218
219pub const FILTER_ARM64: u8 = 2;
220
221fn apply_filter(f: u8, data: &[u8], encoding: bool) -> Result<Vec<u8>> {
222    let mut v = data.to_vec();
223    match filter_id(f) {
224        FILTER_NONE => {}
225        FILTER_X86 => {
226            let mut state = 0u32;
227            x86_convert(&mut v, 0, &mut state, encoding);
228        }
229        FILTER_ARM64 => {
230            let off = (filter_param(f) as usize).min(3).min(v.len());
231            arm64_convert(&mut v[off..], 0, encoding);
232        }
233        other => return Err(Error::Corrupt(format!("unknown filter id {other}"))),
234    }
235    Ok(v)
236}
237
238/// PPMd model order / memory chosen deterministically from the block size: memory grows with the
239/// block (16 MiB..256 MiB), order 16 is a good text default for PPMd H.
240pub fn ppmd_params(len: usize) -> (u32, u32) {
241    let mem = ((len as u64) * 16).clamp(16 << 20, 256 << 20) as u32;
242    (16, mem)
243}
244
245fn ppmd_compress(data: &[u8], order: u32, mem: u32) -> Result<Vec<u8>> {
246    let mut enc = ppmd_rust::Ppmd7Encoder::new(Vec::with_capacity(data.len() / 3 + 64), order, mem).map_err(|e| Error::Encoding(format!("ppmd: {e:?}")))?;
247    enc.write_all(data)?;
248    Ok(enc.finish(true)?)
249}
250
251fn ppmd_decompress(data: &[u8], ulen: usize, order: u32, mem: u32) -> Result<Vec<u8>> {
252    let mut dec = ppmd_rust::Ppmd7Decoder::new(data, order, mem).map_err(|e| Error::Corrupt(format!("ppmd: {e:?}")))?;
253    let mut out = vec![0u8; ulen];
254    dec.read_exact(&mut out)?;
255    Ok(out)
256}
257
258/// Heuristic: does the block look like text (worth a PPMd attempt)? Samples up to 64 KiB.
259pub fn looks_like_text(data: &[u8]) -> bool {
260    let sample = &data[..data.len().min(64 << 10)];
261    if sample.is_empty() {
262        return false;
263    }
264    let textish = sample.iter().filter(|&&b| b == b'\n' || b == b'\r' || b == b'\t' || (0x20..0x7f).contains(&b) || b >= 0x80).count();
265    let zeros = sample.iter().filter(|&&b| b == 0).count();
266    zeros == 0 && textish * 100 / sample.len() >= 97
267}
268
269#[derive(Clone, Debug)]
270pub struct CodecOptions {
271    pub level: i32,
272    /// Upper bound for the zstd window log; the window is otherwise sized to the block.
273    pub long_window_log: u32,
274    pub dict: Option<Vec<u8>>,
275    pub choice: CodecChoice,
276    /// Codec-choice effort for `CodecChoice::Best`, 1..=5 (values outside are clamped):
277    /// 1 = zstd only (identical to `CodecChoice::Zstd`), 2 = zstd or xz decided on a sample of the
278    /// block, 3 = zstd, xz or PPMd (text) decided on a sample, 4 = 3 plus xz on the whole block when
279    /// PPMd won the sample, 5 = every codec on every block (the default; today's full trial, the
280    /// same code path byte for byte). Blocks of at most [`sample_full_trial_max`] bytes always run
281    /// the full trial. Other choices ignore it. See [`sample`] for the sampling rule.
282    pub effort: u8,
283}
284
285impl Default for CodecOptions {
286    fn default() -> Self {
287        Self { level: 19, long_window_log: 27, dict: None, choice: CodecChoice::Zstd, effort: EFFORT_DEFAULT }
288    }
289}
290
291/// Default `effort`: the full trial.
292pub const EFFORT_DEFAULT: u8 = 5;
293
294/// Length of one slice of the codec-choice sample (§2.1 of `docs/design/plans/1.1-codec-sampling.md`).
295pub const SAMPLE_SLICE: usize = 8 << 10;
296
297/// Number of slices the sample of a block of `len` bytes has: one per 128 KiB, at least 8 (a
298/// 1 MiB block gives 8 slices = 64 KiB) and at most 32 (256 KiB at 4 MiB and above). A block prefix
299/// must not be used instead: blocks concatenate chunks of several files and a prefix predicts the
300/// wrong codec on a third of the benchmark blocks (measured, see the plan).
301pub fn sample_slices(len: usize) -> usize {
302    (len / (128 << 10)).clamp(8, 32)
303}
304
305/// Largest block that is never sampled: `2 * slices * SAMPLE_SLICE` (128 KiB for blocks up to
306/// 1 MiB), below which a sampled trial costs about as much as the full one. Every golden archive
307/// holds one blob below this size, so efforts 2..=5 reproduce them by construction.
308pub fn sample_full_trial_max(len: usize) -> usize {
309    2 * sample_slices(len) * SAMPLE_SLICE
310}
311
312/// Byte offsets of the sample slices of a block of `len` bytes, evenly spaced from 0 to
313/// `len - SAMPLE_SLICE`, each rounded down to a multiple of 4 so that the word alignment
314/// `arm64_alignment` finds on the sample is the block's. Empty when the block is not sampled.
315pub fn sample_offsets(len: usize) -> Vec<usize> {
316    if len <= sample_full_trial_max(len) {
317        return Vec::new();
318    }
319    let n = sample_slices(len);
320    let span = (len - SAMPLE_SLICE) as u64;
321    (0..n).map(|i| ((i as u64 * span / (n as u64 - 1)) as usize) & !3).collect()
322}
323
324/// The codec-choice sample of a block: the block itself (borrowed) when it is small enough for
325/// the full trial, otherwise the concatenation of its sample slices. A pure function of the bytes.
326pub fn sample(data: &[u8]) -> std::borrow::Cow<'_, [u8]> {
327    let offsets = sample_offsets(data.len());
328    if offsets.is_empty() {
329        return std::borrow::Cow::Borrowed(data);
330    }
331    let mut s = Vec::with_capacity(offsets.len() * SAMPLE_SLICE);
332    for off in offsets {
333        s.extend_from_slice(&data[off..off + SAMPLE_SLICE]);
334    }
335    std::borrow::Cow::Owned(s)
336}
337
338/// Does `compress_best` decide the codec of a block of `len` bytes on a sample with these options?
339/// (`Best` at efforts 2..=4 and a block above `sample_full_trial_max`.)
340pub fn samples_block(len: usize, o: &CodecOptions) -> bool {
341    o.choice == CodecChoice::Best && (2..=4).contains(&o.effort) && len > sample_full_trial_max(len)
342}
343
344fn zstd_compress(data: &[u8], level: i32, max_window_log: u32) -> Result<Vec<u8>> {
345    let mut c = zstd::bulk::Compressor::new(level)?;
346    // The window never needs to exceed the block: this caps the memory of every parallel
347    // compressor (level 19 with a 128 MiB window costs hundreds of MB per context) without
348    // changing the output for blocks that fit in the window. Long-distance matching across
349    // blocks is unnecessary because deduplication already happens at chunk level.
350    let needed = usize::BITS - data.len().max(1024).leading_zeros(); // ceil(log2(len))
351    let window_log = needed.clamp(10, if max_window_log > 0 { max_window_log } else { 23 });
352    c.set_parameter(CParameter::WindowLog(window_log))?;
353    Ok(c.compress(data)?)
354}
355
356/// xz/LZMA2 preset derived from the zstd-style level: 1..=9 map to xz presets 1..=9, higher levels
357/// use preset 9 extreme. The LZMA2 dictionary is sized to the block (never larger than needed), which
358/// keeps the memory of parallel encoders proportional to the block size instead of the preset's 64 MiB.
359fn xz_compress(data: &[u8], level: i32) -> Result<Vec<u8>> {
360    use liblzma::stream::{Check, Filters, LzmaOptions, Stream};
361    let preset: u32 = if level >= 15 { 9 | (1u32 << 31) } else { (level.clamp(1, 9)) as u32 };
362    let mut opts = LzmaOptions::new_preset(preset).map_err(|e| Error::Encoding(format!("xz preset: {e}")))?;
363    let dict = (data.len().max(4096) as u32).next_power_of_two().min(64 << 20);
364    opts.dict_size(dict);
365    let mut filters = Filters::new();
366    filters.lzma2(&opts);
367    let stream = Stream::new_stream_encoder(&filters, Check::None).map_err(|e| Error::Encoding(format!("xz encoder: {e}")))?;
368    let mut enc = liblzma::write::XzEncoder::new_stream(Vec::with_capacity(data.len() / 2 + 64), stream);
369    enc.write_all(data)?;
370    Ok(enc.finish()?)
371}
372
373/// zstd with `prefix` as a raw-content dictionary (the "patch-from" model): matches may reach into
374/// the prefix, so a new version of a document costs about the size of its edits.
375pub fn zstd_compress_with_prefix(data: &[u8], level: i32, prefix: &[u8]) -> Result<Vec<u8>> {
376    let need = (prefix.len() + data.len()).max(1024);
377    let window_log = (usize::BITS - (need - 1).leading_zeros()).clamp(10, 27);
378    let mut c = zstd::bulk::Compressor::new(level)?;
379    c.set_parameter(CParameter::WindowLog(window_log))?;
380    c.set_dictionary(level, prefix)?;
381    Ok(c.compress(data)?)
382}
383
384pub fn zstd_decompress_with_prefix(data: &[u8], ulen: usize, prefix: &[u8]) -> Result<Vec<u8>> {
385    let mut d = zstd::bulk::Decompressor::with_dictionary(prefix)?;
386    d.set_parameter(DParameter::WindowLogMax(31))?;
387    let out = d.decompress(data, ulen)?;
388    if out.len() != ulen {
389        return Err(Error::Corrupt(format!("decoded {} bytes, expected {ulen}", out.len())));
390    }
391    Ok(out)
392}
393
394/// The filter candidates of a block: none, plus the x86 or the ARM64 branch converter when the
395/// bytes look like machine code (`data` is the whole block, or its sample at efforts 2..=4).
396fn filter_candidates(data: &[u8]) -> Vec<u8> {
397    let mut filters = vec![FILTER_NONE];
398    if looks_like_x86(data) {
399        filters.push(FILTER_X86);
400    } else if let Some(off) = arm64_alignment(data) {
401        filters.push(FILTER_ARM64 | (off << 4));
402    }
403    filters
404}
405
406/// The codecs one filter variant of `compress_trial` runs.
407#[derive(Clone, Copy, Debug)]
408enum CodecSet<'a> {
409    /// The rule of `CodecOptions::choice`: zstd (and zstd+dict when a dictionary is given), xz,
410    /// PPMd on input that looks like text, and for `Best` the incompressible shortcut (zstd cannot
411    /// shrink the input by 3 %: the slower codecs are skipped).
412    ByChoice,
413    /// Exactly these codec ids (decided on a sample); no shortcut.
414    Only(&'a [u8]),
415}
416
417/// Run the given filter variants on a block and keep the smallest output; store if nothing is
418/// smaller than the block. The tie-break is *first tried wins*: variants in the given order, and
419/// within a variant zstd, zstd+dict, xz, PPMd (an output replaces the best only when strictly smaller).
420fn compress_trial(data: &[u8], o: &CodecOptions, variants: &[(u8, CodecSet)]) -> Result<Encoded> {
421    let mut best = Encoded { codec: STORE, bytes: data.to_vec(), params: Vec::new(), filter: FILTER_NONE };
422    for &(filter, set) in variants {
423        let filtered;
424        let input: &[u8] = if filter == FILTER_NONE {
425            data
426        } else {
427            filtered = apply_filter(filter, data, true)?;
428            &filtered
429        };
430        let mut consider = |codec: u8, out: Vec<u8>, params: Vec<u32>| {
431            if out.len() < best.bytes.len() {
432                best = Encoded { codec, bytes: out, params, filter };
433            }
434        };
435        let (try_zstd, try_dict, try_xz, try_ppmd) = match set {
436            CodecSet::ByChoice => {
437                let zstd = matches!(o.choice, CodecChoice::Zstd | CodecChoice::Best);
438                (zstd, zstd, matches!(o.choice, CodecChoice::Xz | CodecChoice::Best), o.choice == CodecChoice::Ppmd || (o.choice == CodecChoice::Best && looks_like_text(input)))
439            }
440            CodecSet::Only(codecs) => (codecs.contains(&ZSTD), codecs.contains(&ZSTD_DICT), codecs.contains(&XZ), codecs.contains(&PPMD)),
441        };
442        let mut incompressible = false;
443        if try_zstd {
444            let z = zstd_compress(input, o.level, o.long_window_log)?;
445            incompressible = z.len() * 100 >= input.len() * 97;
446            consider(ZSTD, z, Vec::new());
447        }
448        if try_dict {
449            if let Some(d) = &o.dict {
450                let mut c = zstd::bulk::Compressor::with_dictionary(o.level, d)?;
451                consider(ZSTD_DICT, c.compress(input)?, Vec::new());
452            }
453        }
454        if incompressible && o.choice == CodecChoice::Best && matches!(set, CodecSet::ByChoice) {
455            continue;
456        }
457        if try_xz && input.len() >= 256 {
458            consider(XZ, xz_compress(input, o.level)?, Vec::new());
459        }
460        if try_ppmd && input.len() >= 1024 {
461            let (order, mem) = ppmd_params(input.len());
462            consider(PPMD, ppmd_compress(input, order, mem)?, vec![order, mem]);
463        }
464    }
465    Ok(best)
466}
467
468/// What a sample decided for a block (`decide_on_sample`).
469#[derive(Clone, Debug, PartialEq, Eq)]
470pub struct Decision {
471    /// Filter byte (id and parameter) to apply to the whole block.
472    pub filter: u8,
473    /// Codec to run on the whole block.
474    pub codec: u8,
475    /// A second codec to run on the whole block with the same filter, keeping the smaller output
476    /// (effort 4: xz whenever PPMd won the sample, the one structural miss of a sample).
477    pub also: Option<u8>,
478    /// zstd could not shrink the unfiltered sample by 3 % (the shortcut of the full trial, taken
479    /// on the sample: xz and PPMd were not tried for that variant).
480    pub incompressible: bool,
481}
482
483/// One candidate of the sample decision: `(filter, codec, size of the sample's encoding)`.
484type Candidate = (u8, u8, usize);
485
486/// The candidate `consider()` would have kept: the first strictly smallest one in push order
487/// (unfiltered before filtered; zstd, zstd+dict, xz, PPMd within a filter).
488fn pick(candidates: &[Candidate]) -> Option<Candidate> {
489    let mut best: Option<Candidate> = None;
490    for &c in candidates {
491        if best.is_none_or(|b| c.2 < b.2) {
492            best = Some(c);
493        }
494    }
495    best
496}
497
498/// Decide filter and codec for a block on its sample (`sample()` of the block; efforts 2..=4).
499/// The same heuristics and codecs as the full trial run on the sample, in the same order, and
500/// the candidate the full trial would have kept on the sample wins. `looks_like_text` is judged on
501/// the (filtered) sample here, not on the first 64 KiB of the block as in the full trial: a strided
502/// sample represents a mixed block better; this is intended.
503pub fn decide_on_sample(sample: &[u8], o: &CodecOptions) -> Result<Decision> {
504    let effort = o.effort.clamp(1, 5);
505    let mut candidates: Vec<Candidate> = Vec::new();
506    let mut incompressible_unfiltered = false;
507    for filter in filter_candidates(sample) {
508        let filtered;
509        let input: &[u8] = if filter == FILTER_NONE {
510            sample
511        } else {
512            filtered = apply_filter(filter, sample, true)?;
513            &filtered
514        };
515        let z = zstd_compress(input, o.level, o.long_window_log)?;
516        let incompressible = z.len() * 100 >= input.len() * 97;
517        if filter == FILTER_NONE {
518            incompressible_unfiltered = incompressible;
519        }
520        candidates.push((filter, ZSTD, z.len()));
521        if let Some(d) = &o.dict {
522            let mut c = zstd::bulk::Compressor::with_dictionary(o.level, d)?;
523            candidates.push((filter, ZSTD_DICT, c.compress(input)?.len()));
524        }
525        if incompressible {
526            continue;
527        }
528        if effort >= 2 && input.len() >= 256 {
529            candidates.push((filter, XZ, xz_compress(input, o.level)?.len()));
530        }
531        if effort >= 3 && input.len() >= 1024 && looks_like_text(input) {
532            let (order, mem) = ppmd_params(input.len());
533            candidates.push((filter, PPMD, ppmd_compress(input, order, mem)?.len()));
534        }
535    }
536    let (filter, codec, _) = pick(&candidates).expect("zstd is always a candidate");
537    let also = (effort == 4 && codec == PPMD).then_some(XZ);
538    Ok(Decision { filter, codec, also, incompressible: incompressible_unfiltered })
539}
540
541/// Try every applicable codec (with and without a branch filter when the block looks like x86 or
542/// ARM64 machine code) and keep the smallest output; store if nothing is smaller. Blocks that zstd cannot shrink
543/// by at least 3 % are treated as incompressible and the slower codecs are skipped.
544///
545/// `o.effort` (for `CodecChoice::Best` only) trades codec-choice time for ratio: at effort 5 (the
546/// default) and for blocks of at most `sample_full_trial_max` bytes this is the full trial above;
547/// at effort 1 only zstd runs (as `CodecChoice::Zstd`); at efforts 2..=4 the filter and the codec
548/// are decided on `sample(data)` (`decide_on_sample`) and only the winner runs on the whole block.
549/// Every path produces only codec ids, parameters and filters the full trial could have produced.
550pub fn compress_best(data: &[u8], o: &CodecOptions) -> Result<Encoded> {
551    if o.choice == CodecChoice::Best && o.effort.clamp(1, 5) == 1 {
552        // zstd only (zstd+dict in granular mode), exactly what `CodecChoice::Zstd` runs, at every block size
553        let variants: Vec<(u8, CodecSet)> = filter_candidates(data).into_iter().map(|f| (f, CodecSet::Only(&[ZSTD, ZSTD_DICT]))).collect();
554        return compress_trial(data, o, &variants);
555    }
556    if !samples_block(data.len(), o) {
557        // the full trial (effort 5, another choice, or a small block): the unchanged code path
558        let variants: Vec<(u8, CodecSet)> = filter_candidates(data).into_iter().map(|f| (f, CodecSet::ByChoice)).collect();
559        return compress_trial(data, o, &variants);
560    }
561    let d = decide_on_sample(&sample(data), o)?;
562    let run: Vec<u8> = std::iter::once(d.codec).chain(d.also).collect();
563    compress_trial(data, o, &[(d.filter, CodecSet::Only(&run))])
564}
565
566pub fn decompress(codec: u8, data: &[u8], ulen: usize, dict: Option<&[u8]>) -> Result<Vec<u8>> {
567    decompress_with(codec, data, ulen, dict, &[], FILTER_NONE)
568}
569
570pub fn decompress_with(codec: u8, data: &[u8], ulen: usize, dict: Option<&[u8]>, params: &[u32], filter: u8) -> Result<Vec<u8>> {
571    let out = decompress_inner(codec, data, ulen, dict, params)?;
572    if filter == FILTER_NONE {
573        return Ok(out);
574    }
575    apply_filter(filter, &out, false)
576}
577
578fn decompress_inner(codec: u8, data: &[u8], ulen: usize, dict: Option<&[u8]>, params: &[u32]) -> Result<Vec<u8>> {
579    let out = match codec {
580        PPMD => {
581            if params.len() != 2 {
582                return Err(Error::Corrupt("ppmd blob without parameters".into()));
583            }
584            let (order, mem) = (params[0], params[1]);
585            if mem > (1 << 30) {
586                return Err(Error::Limit(format!("ppmd memory {mem} exceeds the reader limit")));
587            }
588            ppmd_decompress(data, ulen, order, mem)?
589        }
590        STORE => data.to_vec(),
591        ZSTD => {
592            let mut d = zstd::bulk::Decompressor::new()?;
593            d.set_parameter(DParameter::WindowLogMax(31))?;
594            d.decompress(data, ulen)?
595        }
596        ZSTD_DICT => {
597            let dict = dict.ok_or_else(|| Error::Missing("dictionary required by blob".into()))?;
598            let mut d = zstd::bulk::Decompressor::with_dictionary(dict)?;
599            d.set_parameter(DParameter::WindowLogMax(31))?;
600            d.decompress(data, ulen)?
601        }
602        ZSTD_DELTA => return Err(Error::Corrupt("delta blob needs its dictionary chunks (reader path)".into())),
603        XZ => {
604            let stream = liblzma::stream::Stream::new_stream_decoder(1 << 30, 0).map_err(|e| Error::Encoding(format!("xz decoder: {e}")))?;
605            let mut dec = liblzma::read::XzDecoder::new_stream(data, stream);
606            let mut out = Vec::with_capacity(ulen);
607            // bounded read: never allocate more than the declared length + 1 (detects overlong streams)
608            let mut limited = (&mut dec).take(ulen as u64 + 1);
609            limited.read_to_end(&mut out)?;
610            out
611        }
612        other => return Err(Error::Corrupt(format!("unknown codec id {other}"))),
613    };
614    if out.len() != ulen {
615        return Err(Error::Corrupt(format!("blob length {} != declared {}", out.len(), ulen)));
616    }
617    Ok(out)
618}
619
620/// Train a zstd dictionary on the unique chunks (needs at least 8 samples).
621pub fn train_dict(samples: &[&[u8]], max_size: usize) -> Option<Vec<u8>> {
622    if samples.len() < 8 {
623        return None;
624    }
625    zstd::dict::from_samples(samples, max_size).ok()
626}
627
628#[cfg(test)]
629mod tests {
630    use super::*;
631
632    #[test]
633    fn xz_roundtrip_and_best_choice() {
634        let text: Vec<u8> = "the quick brown fox jumps over the lazy dog; ".repeat(2000).into_bytes();
635        let e = compress_best(&text, &CodecOptions { choice: CodecChoice::Xz, ..Default::default() }).unwrap();
636        assert_eq!(e.codec, XZ);
637        assert_eq!(decompress(XZ, &e.bytes, text.len(), None).unwrap(), text);
638        let b = compress_best(&text, &CodecOptions { choice: CodecChoice::Best, ..Default::default() }).unwrap();
639        assert!(b.bytes.len() <= e.bytes.len());
640        assert!(decompress(XZ, &e.bytes[..e.bytes.len() - 4], text.len(), None).is_err(), "truncated xz must fail");
641    }
642
643    #[test]
644    fn x86_filter_roundtrips_and_helps_on_code() {
645        // pseudo machine code: many E8 calls with small relative offsets
646        let mut code = Vec::new();
647        let mut x = 0x1234_5678u32;
648        for i in 0..20_000u32 {
649            x ^= x << 13;
650            x ^= x >> 17;
651            x ^= x << 5;
652            if i % 7 == 0 {
653                code.push(0xE8);
654                let rel = x % 4096;
655                code.extend_from_slice(&rel.to_le_bytes());
656            } else {
657                code.push((x >> 24) as u8);
658            }
659        }
660        assert!(looks_like_x86(&code));
661        let mut enc = code.clone();
662        let mut st = 0;
663        x86_convert(&mut enc, 0, &mut st, true);
664        assert_ne!(enc, code);
665        let mut dec = enc.clone();
666        let mut st2 = 0;
667        x86_convert(&mut dec, 0, &mut st2, false);
668        assert_eq!(dec, code, "x86 filter must be exactly invertible");
669        let e = compress_best(&code, &CodecOptions { choice: CodecChoice::Zstd, ..Default::default() }).unwrap();
670        assert_eq!(decompress_with(e.codec, &e.bytes, code.len(), None, &e.params, e.filter).unwrap(), code);
671        // random data: never filtered, stored
672        let mut y = 0x2545_F491u32;
673        let rnd: Vec<u8> = (0..50_000)
674            .map(|_| {
675                y ^= y << 13;
676                y ^= y >> 17;
677                y ^= y << 5;
678                (y >> 24) as u8
679            })
680            .collect();
681        assert!(!looks_like_x86(&rnd) && arm64_alignment(&rnd).is_none());
682        let r = compress_best(&rnd, &CodecOptions::default()).unwrap();
683        assert_eq!(r.codec, STORE);
684        assert_eq!(decompress_with(r.codec, &r.bytes, rnd.len(), None, &r.params, r.filter).unwrap(), rnd);
685    }
686
687    #[test]
688    fn branch_filters_roundtrip_on_adversarial_inputs() {
689        // Dense E8/E9 with 00/FF bytes everywhere to exercise every mask path of the x86 converter,
690        // and dense BL/ADRP-looking words for ARM64; many sizes including unaligned tails.
691        let mut x = 0x9E37_79B9u32;
692        let mut next = || {
693            x ^= x << 13;
694            x ^= x >> 17;
695            x ^= x << 5;
696            x
697        };
698        for round in 0..300 {
699            let len = 5 + (next() % 3000) as usize + if round % 3 == 0 { 4096 } else { 0 };
700            let mut buf = vec![0u8; len];
701            for b in buf.iter_mut() {
702                let r = next();
703                *b = match r % 6 {
704                    0 => 0xE8,
705                    1 => 0xE9,
706                    2 => 0x00,
707                    3 => 0xFF,
708                    _ => (r >> 8) as u8,
709                };
710            }
711            for &f in &[FILTER_X86, FILTER_ARM64, FILTER_ARM64 | 0x10, FILTER_ARM64 | 0x20, FILTER_ARM64 | 0x30] {
712                let enc = apply_filter(f, &buf, true).unwrap();
713                let dec = apply_filter(f, &enc, false).unwrap();
714                assert_eq!(dec, buf, "filter {f} round {round} len {len}");
715            }
716            assert!(apply_filter(0x0F, &buf, true).is_err());
717            let mut words = Vec::with_capacity(len);
718            for i in 0..len / 4 {
719                let r = next();
720                let w = match i % 4 {
721                    0 => 0x9400_0000 | (r & 0x0000_FFFF), // BL, small forward displacement
722                    1 => 0x97FF_0000 | (r & 0x0000_FFFF), // BL, small backward displacement
723                    2 => 0x9000_0000 | (r & 0x60FF_FFFF), // ADRP
724                    _ => r,
725                };
726                words.extend_from_slice(&w.to_le_bytes());
727            }
728            let enc = apply_filter(FILTER_ARM64, &words, true).unwrap();
729            assert_ne!(enc, words);
730            assert_eq!(apply_filter(FILTER_ARM64, &enc, false).unwrap(), words);
731            // a misaligned copy of code must be detected at its alignment
732            let mut shifted = vec![0xAAu8; 3];
733            shifted.extend_from_slice(&words);
734            if words.len() >= 4096 {
735                assert_eq!(arm64_alignment(&shifted), Some(3), "round {round}");
736            }
737        }
738    }
739
740    /// Deterministic pseudo-random words.
741    fn xorshift(x: &mut u32) -> u32 {
742        *x ^= *x << 13;
743        *x ^= *x >> 17;
744        *x ^= *x << 5;
745        *x
746    }
747
748    /// Prose-like text of about `len` bytes from a word list.
749    fn prose(len: usize, seed: u32) -> Vec<u8> {
750        let words = [
751            "archive",
752            "agent",
753            "chunk",
754            "merkle",
755            "verify",
756            "content",
757            "reference",
758            "dictionary",
759            "solid",
760            "the",
761            "of",
762            "a",
763            "block",
764            "sample",
765            "codec",
766            "deterministic",
767            "every",
768            "byte",
769            "restored",
770            "and",
771        ];
772        let mut x = seed;
773        let mut out = Vec::with_capacity(len + 16);
774        let mut i = 0usize;
775        while out.len() < len {
776            out.extend_from_slice(words[(xorshift(&mut x) % words.len() as u32) as usize].as_bytes());
777            out.push(if i % 11 == 10 { b'\n' } else { b' ' });
778            i += 1;
779        }
780        out.truncate(len);
781        out
782    }
783
784    /// Random bytes of `len` bytes.
785    fn random(len: usize, seed: u32) -> Vec<u8> {
786        let mut x = seed;
787        (0..len).map(|_| (xorshift(&mut x) >> 24) as u8).collect()
788    }
789
790    fn best(effort: u8) -> CodecOptions {
791        CodecOptions { choice: CodecChoice::Best, effort, ..Default::default() }
792    }
793
794    fn assert_decodes(e: &Encoded, data: &[u8]) {
795        assert_eq!(decompress_with(e.codec, &e.bytes, data.len(), None, &e.params, e.filter).unwrap(), data);
796    }
797
798    fn same(a: &Encoded, b: &Encoded) -> bool {
799        a.codec == b.codec && a.filter == b.filter && a.params == b.params && a.bytes == b.bytes
800    }
801
802    #[test]
803    fn sample_is_deterministic_aligned_and_covers_the_block() {
804        for len in [100usize << 10, 129 << 10, 1 << 20, 4 << 20, 10 << 20] {
805            let n = (len / (128 << 10)).clamp(8, 32);
806            assert_eq!(sample_slices(len), n, "len {len}");
807            let d = random(len, 7);
808            let s = sample(&d);
809            assert_eq!(s, sample(&d), "the sample is a pure function of the block");
810            let offsets = sample_offsets(len);
811            if len <= 2 * n * SAMPLE_SLICE {
812                assert!(offsets.is_empty() && matches!(s, std::borrow::Cow::Borrowed(_)), "len {len}: small blocks are not sampled");
813                assert_eq!(&*s, &d[..]);
814                continue;
815            }
816            assert_eq!(offsets.len(), n);
817            assert_eq!(offsets[0], 0, "the first slice starts at 0");
818            assert!(offsets.iter().all(|o| o % 4 == 0), "{offsets:?}");
819            assert!(offsets.windows(2).all(|w| w[0] + SAMPLE_SLICE <= w[1]), "slices do not overlap: {offsets:?}");
820            let last = *offsets.last().unwrap();
821            assert!(last + SAMPLE_SLICE <= len && last + SAMPLE_SLICE + 3 >= len, "the last slice ends at the block (up to alignment): {last} of {len}");
822            assert_eq!(s.len(), n * SAMPLE_SLICE);
823            for (i, o) in offsets.iter().enumerate() {
824                assert_eq!(&s[i * SAMPLE_SLICE..(i + 1) * SAMPLE_SLICE], &d[*o..*o + SAMPLE_SLICE]);
825            }
826        }
827        assert_eq!(sample_full_trial_max(1 << 20), 128 << 10);
828        assert!(sample(&[]).is_empty() && sample_offsets(0).is_empty() && sample_offsets(SAMPLE_SLICE).is_empty());
829    }
830
831    #[test]
832    fn effort_5_and_small_blocks_take_the_unchanged_path() {
833        assert_eq!(CodecOptions::default().effort, 5);
834        assert_eq!(EFFORT_DEFAULT, 5);
835        // a 100 KiB mixed block: prose, then random bytes
836        let mut block = prose(60 << 10, 11);
837        block.extend_from_slice(&random(40 << 10, 12));
838        for effort in 1..=5 {
839            assert!(!samples_block(block.len(), &best(effort)), "effort {effort}");
840        }
841        let full = compress_best(&block, &best(5)).unwrap();
842        assert_ne!(full.codec, STORE);
843        for effort in 2..=5 {
844            let e = compress_best(&block, &best(effort)).unwrap();
845            assert!(same(&e, &full), "effort {effort} must be the full trial on a block of {} bytes", block.len());
846            assert_decodes(&e, &block);
847        }
848        // effort 1 is `CodecChoice::Zstd` at every size
849        let e1 = compress_best(&block, &best(1)).unwrap();
850        let z = compress_best(&block, &CodecOptions { choice: CodecChoice::Zstd, ..Default::default() }).unwrap();
851        assert!(same(&e1, &z) && e1.codec == ZSTD);
852        // out-of-range values are clamped, never sampled below the threshold
853        assert!(same(&compress_best(&block, &best(0)).unwrap(), &e1));
854        assert!(same(&compress_best(&block, &best(9)).unwrap(), &full));
855        // other choices ignore the effort
856        for choice in [CodecChoice::Zstd, CodecChoice::Xz, CodecChoice::Ppmd] {
857            let big = prose(300 << 10, 13);
858            let a = compress_best(&big, &CodecOptions { choice, effort: 3, ..Default::default() }).unwrap();
859            let b = compress_best(&big, &CodecOptions { choice, effort: 5, ..Default::default() }).unwrap();
860            assert!(same(&a, &b), "{choice:?}");
861            assert!(!samples_block(big.len(), &CodecOptions { choice, effort: 3, ..Default::default() }));
862        }
863    }
864
865    #[test]
866    fn decision_tie_break_matches_consider() {
867        // consider() keeps an output only when strictly smaller: on equal sizes the first tried wins,
868        // unfiltered before filtered and zstd, zstd+dict, xz, PPMd within a filter
869        let order: Vec<Candidate> =
870            [(FILTER_NONE, ZSTD), (FILTER_NONE, ZSTD_DICT), (FILTER_NONE, XZ), (FILTER_NONE, PPMD), (FILTER_X86, ZSTD), (FILTER_X86, ZSTD_DICT), (FILTER_X86, XZ), (FILTER_X86, PPMD)]
871                .iter()
872                .map(|&(f, c)| (f, c, 100))
873                .collect();
874        assert_eq!(pick(&order), Some((FILTER_NONE, ZSTD, 100)));
875        for start in 1..order.len() {
876            assert_eq!(pick(&order[start..]), Some(order[start]), "the first candidate in push order wins a tie");
877        }
878        let mut reversed = order.clone();
879        reversed.reverse();
880        assert_eq!(pick(&reversed), Some((FILTER_X86, PPMD, 100)), "pick follows the order it is given, not the codec id");
881        // a strictly smaller later candidate wins; an equal later one does not
882        let mut c = order.clone();
883        c[6].2 = 99;
884        assert_eq!(pick(&c), Some((FILTER_X86, XZ, 99)));
885        c[7].2 = 99;
886        assert_eq!(pick(&c), Some((FILTER_X86, XZ, 99)));
887        c[3].2 = 99;
888        assert_eq!(pick(&c), Some((FILTER_NONE, PPMD, 99)));
889        assert_eq!(pick(&[]), None);
890    }
891
892    #[test]
893    fn sampled_choice_agrees_with_the_full_trial_on_fixtures() {
894        let len = 1 << 20;
895        // 1. prose from a word list
896        let prose_block = prose(len, 21);
897        // 2. a template repeated with edits (long-range redundancy a sample cannot see whole)
898        let template = prose(40 << 10, 22);
899        let mut versions = Vec::with_capacity(len + template.len());
900        let mut x = 23u32;
901        while versions.len() < len {
902            let mut v = template.clone();
903            for _ in 0..40 {
904                let at = (xorshift(&mut x) as usize) % (v.len() - 8);
905                v[at..at + 8].copy_from_slice(b"EDITED! ");
906            }
907            versions.extend_from_slice(&v);
908        }
909        versions.truncate(len);
910        // 3. random bytes (a compressible tail would let zstd keep the block: consider() keeps any
911        //    output smaller than the block, the 97 % rule only skips the slower codecs)
912        let noisy = random(len, 24);
913        for (name, block) in [("prose", &prose_block), ("versions", &versions), ("random", &noisy)] {
914            assert!(samples_block(block.len(), &best(3)), "{name}");
915            let full = compress_best(block, &best(5)).unwrap();
916            let e3 = compress_best(block, &best(3)).unwrap();
917            let e4 = compress_best(block, &best(4)).unwrap();
918            for e in [&full, &e3, &e4] {
919                assert_decodes(e, block);
920            }
921            let d = decide_on_sample(&sample(block), &best(3)).unwrap();
922            println!(
923                "{name}: full {} {} B, effort 3 {} {} B, effort 4 {} {} B, decision {d:?}",
924                super::name(full.codec),
925                full.bytes.len(),
926                super::name(e3.codec),
927                e3.bytes.len(),
928                super::name(e4.codec),
929                e4.bytes.len()
930            );
931            match name {
932                "prose" => {
933                    assert_eq!(e3.codec, full.codec, "the sample must pick the full trial's codec on prose");
934                    assert!(same(&e3, &full), "the winner run on the whole block gives the full trial's bytes");
935                    assert!(same(&e4, &full));
936                    assert!(!d.incompressible && d.filter == FILTER_NONE);
937                }
938                "versions" => {
939                    // the known PPMd/xz ambiguity of a sample: effort 4 runs xz as well when PPMd
940                    // won the sample and therefore lands on the full trial's size here
941                    assert_eq!(e4.bytes.len(), full.bytes.len(), "effort 4's xz safety net");
942                    assert!(matches!(e3.codec, XZ | PPMD) && matches!(full.codec, XZ | PPMD));
943                }
944                _ => {
945                    assert_eq!(full.codec, STORE, "the full trial stores a block zstd cannot shrink at all");
946                    assert!(same(&e3, &full) && same(&e4, &full));
947                    assert!(d.incompressible && d.codec == ZSTD && d.also.is_none(), "the sample takes the incompressible shortcut: {d:?}");
948                }
949            }
950        }
951    }
952
953    #[test]
954    fn effort_2_never_picks_ppmd_and_effort_1_is_zstd_only() {
955        let block = prose(1 << 20, 31);
956        assert!(looks_like_text(&block));
957        let e2 = compress_best(&block, &best(2)).unwrap();
958        assert!(matches!(e2.codec, ZSTD | XZ), "effort 2 is LZ-only, got {}", name(e2.codec));
959        let d2 = decide_on_sample(&sample(&block), &best(2)).unwrap();
960        assert_ne!(d2.codec, PPMD);
961        assert!(d2.also.is_none());
962        let d3 = decide_on_sample(&sample(&block), &best(3)).unwrap();
963        assert_eq!(d3.codec, PPMD, "PPMd wins prose");
964        assert!(d3.also.is_none(), "only effort 4 adds the xz check");
965        assert_eq!(decide_on_sample(&sample(&block), &best(4)).unwrap().also, Some(XZ));
966        let e1 = compress_best(&block, &best(1)).unwrap();
967        let z = compress_best(&block, &CodecOptions { choice: CodecChoice::Zstd, ..Default::default() }).unwrap();
968        assert_eq!(e1.codec, ZSTD);
969        assert!(same(&e1, &z), "effort 1 is `--codec zstd`");
970        assert!(!samples_block(block.len(), &best(1)) && !samples_block(block.len(), &best(5)));
971        assert!(samples_block(block.len(), &best(2)) && samples_block(block.len(), &best(4)));
972        for e in [&e1, &e2] {
973            assert_decodes(e, &block);
974        }
975        assert!(compress_best(&block, &best(3)).unwrap().bytes.len() <= e1.bytes.len());
976    }
977
978    #[test]
979    fn arm64_alignment_survives_sampling() {
980        // synthetic ARM64 code: three quarters of the words from a small instruction set, one
981        // quarter BL calls to one of eight fixed targets (their PC-relative immediates differ at
982        // every call site; the branch converter makes them identical), shifted by 3 bytes as a
983        // block that starts mid-instruction does
984        let words = 48 << 10; // 192 KiB > the full-trial threshold
985        let mut x = 41u32;
986        let set: Vec<u32> = (0..64).map(|_| (xorshift(&mut x) & !0xFC00_0000) | 0x8800_0000).collect();
987        let targets: Vec<u32> = (0..8).map(|i| 0x1000 + i * 0x340).collect();
988        let mut code = vec![0xAAu8; 3];
989        for i in 0..words {
990            let pc = 4 * i as u32;
991            let w = if i % 4 == 3 { 0x9400_0000 | (targets[(xorshift(&mut x) % 8) as usize].wrapping_sub(pc) >> 2) & 0x03FF_FFFF } else { set[(xorshift(&mut x) % 64) as usize] };
992            code.extend_from_slice(&w.to_le_bytes());
993        }
994        assert_eq!(arm64_alignment(&code), Some(3));
995        let s = sample(&code);
996        assert!(matches!(s, std::borrow::Cow::Owned(_)));
997        assert_eq!(arm64_alignment(&s), Some(3), "4-byte aligned slices keep the block's word alignment");
998        let d = decide_on_sample(&s, &best(3)).unwrap();
999        assert_eq!((filter_id(d.filter), filter_param(d.filter)), (FILTER_ARM64, 3), "{d:?}");
1000        let e3 = compress_best(&code, &best(3)).unwrap();
1001        assert_eq!((filter_id(e3.filter), filter_param(e3.filter)), (FILTER_ARM64, 3));
1002        assert_decodes(&e3, &code);
1003        let full = compress_best(&code, &best(5)).unwrap();
1004        assert!(same(&e3, &full), "the sampled decision agrees with the full trial on this block");
1005        // effort 1 keeps trying the filter on machine code
1006        let e1 = compress_best(&code, &best(1)).unwrap();
1007        assert_eq!((e1.codec, filter_id(e1.filter)), (ZSTD, FILTER_ARM64));
1008        assert_decodes(&e1, &code);
1009    }
1010
1011    #[test]
1012    fn ppmd_roundtrip() {
1013        let mut text = String::new();
1014        for i in 0..4000 {
1015            text.push_str(&format!("Paragraph {i}: content-defined chunking keeps identical data identical; PPMd models text well.\n"));
1016        }
1017        let data = text.into_bytes();
1018        assert!(looks_like_text(&data));
1019        let e = compress_best(&data, &CodecOptions { choice: CodecChoice::Ppmd, ..Default::default() }).unwrap();
1020        assert_eq!(e.codec, PPMD);
1021        assert_eq!(e.params.len(), 2);
1022        assert_eq!(decompress_with(PPMD, &e.bytes, data.len(), None, &e.params, FILTER_NONE).unwrap(), data);
1023        assert!(decompress_with(PPMD, &e.bytes, data.len(), None, &[], FILTER_NONE).is_err());
1024        let z = compress_best(&data, &CodecOptions { choice: CodecChoice::Zstd, ..Default::default() }).unwrap();
1025        println!("ppmd {} vs zstd {}", e.bytes.len(), z.bytes.len());
1026    }
1027}