diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 176dfe1e..25c7b5e8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -391,6 +391,13 @@ jobs: if: needs.changes.outputs.code == 'true' working-directory: zstd run: cargo nextest run --profile ci -p structured-zstd --features hash,std,dict-builder,caller-workspace + - name: Test (without legacy) + # The default build decodes v0.5-v0.7 frames; without the feature the + # decoders and the command line refuse them as an unknown format. The + # feature changes only those two, so their tests run again here. + if: needs.changes.outputs.code == 'true' + working-directory: zstd + run: cargo nextest run --profile ci -p structured-zstd --no-default-features --features hash,std,dict-builder -E 'test(/^decoding::/) | binary_id(structured-zstd::bin/structured-zstd)' - name: Doc tests if: needs.changes.outputs.code == 'true' run: cargo test --doc -p structured-zstd --features hash,std,dict-builder,caller-workspace @@ -559,6 +566,11 @@ jobs: - name: Clippy (no_std + caller-workspace) working-directory: zstd run: cargo clippy -p structured-zstd --no-default-features --features caller-workspace,hash -- -D warnings + # The v0.5-v0.7 decoders without `hash`, where their content checksum + # is not computed. + - name: Clippy (no_std + legacy) + working-directory: zstd + run: cargo clippy -p structured-zstd --no-default-features --features legacy -- -D warnings # The clippy steps above compile for the host, which has SSE2 and a # float unit. A kernel target has neither (x86_64-unknown-none is # -sse,-sse2,...,+soft-float), so a vector type that @@ -575,6 +587,7 @@ jobs: cargo build -p structured-zstd --lib --profile "$profile" --target x86_64-unknown-none --no-default-features --features hash cargo build -p structured-zstd --lib --profile "$profile" --target x86_64-unknown-none --no-default-features --features kernel-avx2,hash cargo build -p structured-zstd --lib --profile "$profile" --target x86_64-unknown-none --no-default-features --features caller-workspace,hash + cargo build -p structured-zstd --lib --profile "$profile" --target x86_64-unknown-none --no-default-features --features legacy,hash done wasm: @@ -1005,6 +1018,19 @@ jobs: # feature, which the targets above must not get. working-directory: zstd/fuzz run: cargo fuzz run --target "$FUZZ_TARGET_TRIPLE" --features workspace decode_workspace -- -max_total_time=90 -timeout=30 + - name: Fuzz decode_legacy (90s, seeded with the v0.5-v0.7 frames) + # Seeded from `zstd/legacy_frames`: blind mutation rarely gets past + # these formats' entropy headers to their blocks. + working-directory: zstd/fuzz + run: | + mkdir -p corpus/decode_legacy + # Crash inputs committed for this target replay first, as the + # other targets' do. + shopt -s dotglob + if [ -d artifacts/decode_legacy ] && [ -n "$(find artifacts/decode_legacy -mindepth 1 -maxdepth 1 -print -quit)" ]; then + cargo fuzz run --target "$FUZZ_TARGET_TRIPLE" decode_legacy artifacts/decode_legacy/* + fi + cargo fuzz run --target "$FUZZ_TARGET_TRIPLE" decode_legacy corpus/decode_legacy ../legacy_frames -- -max_total_time=90 -timeout=30 - name: Upload new crash artifacts on failure if: failure() uses: actions/upload-artifact@v7 diff --git a/README.md b/README.md index 2282d4f9..51e2fbeb 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ ## Highlights -- **Production-grade decoder** — complete [RFC 8878](https://www.rfc-editor.org/rfc/rfc8878) implementation: dictionary-backed streams, raw / RLE / compressed blocks, the full frame format, optional content checksums, runtime-dispatched SIMD kernels (SSE2 / BMI2 / AVX2 / NEON, opt-in AVX-512). +- **Production-grade decoder** — complete [RFC 8878](https://www.rfc-editor.org/rfc/rfc8878) implementation: dictionary-backed streams, raw / RLE / compressed blocks, the full frame format, optional content checksums, runtime-dispatched SIMD kernels (SSE2 / BMI2 / AVX2 / NEON, opt-in AVX-512). It also reads the v0.5-v0.7 frame formats, as every libzstd 1.5.7 build does by default. - **Full-range encoder** — every C-zstd level (`-131072..=22`) produces valid frames decodable by this crate and by upstream C zstd; named presets, per-knob parameter overrides, long-distance matching, streaming via `std::io::Write`. - **Dictionaries end to end** — compress and decompress with the same dictionary format C zstd consumes; reusable parsed handles; pure-Rust COVER / FastCOVER / legacy training behind the `dict-builder` feature. - **Wire-compatible both ways** — frames interoperate with C zstd in either direction; interop is enforced in CI against the reference implementation. @@ -332,7 +332,8 @@ in pure Rust: | `kernel-simd128` | ✅ | WebAssembly SIMD kernel (needs `-C target-feature=+simd128`) | | `kernel-vbmi2` | ❌ | AVX-512 decode kernel (see note below) | | `kernel-scalar` | ✅ | Marker for the always-compiled scalar fallback | -| `dict-builder` | ❌ | Pure-Rust COVER / FastCOVER / legacy dictionary training | +| `dict-builder` | ✅ | Pure-Rust COVER / FastCOVER / legacy dictionary training | +| `legacy` | ✅ | Decoding of the v0.5, v0.6 and v0.7 frame formats; without it they are refused as an unknown format | | `lsm` | ❌ | [Storage-format extensions](#storage-format-extensions) | | `caller-workspace` | ❌ | [Decoding in caller-owned memory](#decoding-in-caller-owned-memory) | diff --git a/c-api/src/frame.rs b/c-api/src/frame.rs index 8223ea40..d2bd8b74 100644 --- a/c-api/src/frame.rs +++ b/c-api/src/frame.rs @@ -7,7 +7,7 @@ use core::ffi::{c_uint, c_ulonglong}; use codec::decoding::errors::ReadFrameHeaderError; use codec::decoding::{ FrameContentSize, FrameMagic, FrameSizeError, decompression_margin, find_decompressed_size, - find_frame_compressed_size, frame_decompressed_bound, frame_header_size, frame_magic, + find_frame_compressed_size, frame_decompressed_bound, frame_header_size, frame_magic, is_frame, min_decoding_buffer_size, read_frame_header_info, }; use codec::skippable::{ @@ -65,7 +65,9 @@ pub struct ZSTD_FrameHeader { } /// `size_t ZSTD_frameHeaderSize(const void* src, size_t srcSize)` — length of -/// the frame header (including the magic number), or an error code. +/// the frame header (including the magic number), or an error code. A +/// v0.5-v0.7 header, which `ZSTD_isFrame` accepts, is measured by its own +/// format's size fields. /// /// # Safety /// `src` must be valid for `src_size` bytes (or `NULL` with `src_size == 0`). @@ -220,14 +222,15 @@ pub unsafe extern "C" fn ZSTD_findDecompressedSize(src: *const u8, src_size: usi } /// `unsigned ZSTD_isFrame(const void* buffer, size_t size)` — 1 when `buffer` -/// starts with a frame or skippable-frame magic number, else 0. +/// starts with a frame, skippable-frame or v0.5-v0.7 frame magic number, +/// else 0. /// /// # Safety /// `buffer` must be valid for `size` bytes (or `NULL` with `size == 0`). #[unsafe(no_mangle)] pub unsafe extern "C" fn ZSTD_isFrame(buffer: *const u8, size: usize) -> c_uint { let buffer = unsafe { in_slice(buffer, size) }; - c_uint::from(frame_magic(buffer).is_some()) + c_uint::from(is_frame(buffer)) } /// `unsigned ZSTD_isSkippableFrame(const void* buffer, size_t size)` — 1 when diff --git a/c-api/src/streaming.rs b/c-api/src/streaming.rs index ab5b5d4d..81352337 100644 --- a/c-api/src/streaming.rs +++ b/c-api/src/streaming.rs @@ -1084,6 +1084,21 @@ fn enter_stream_frame( let take = (FRAME_HEADER_MAX - held_len).min(available.len()); view[held_len..held_len + take].copy_from_slice(&available[..take]); let view = &view[..held_len + take]; + // A v0.5-v0.7 header is measured by its own size fields; once whole, + // the decoder starts it from the same reader (upstream's + // `ZSTD_decompressStream` hands such a frame to its legacy streams). + if !dctx.magicless + && let Some(version) = codec::decoding::LegacyVersion::of(view) + { + if version + .header_size(view) + .is_some_and(|len| len <= view.len()) + { + start_stream_frame(dctx, inp, src, &held[..held_len])?; + return Ok(FrameEntry::Started); + } + return Ok(hold_header(dctx, inp, view, take)); + } match codec::decoding::read_frame_header_info(view, dctx.magicless) { Ok(_) => { start_stream_frame(dctx, inp, src, &held[..held_len])?; @@ -1105,16 +1120,7 @@ fn enter_stream_frame( | ReadFrameHeaderError::DictionaryIdReadError(_) | ReadFrameHeaderError::FrameContentSizeReadError(_), ) => { - debug_assert!(view.len() < FRAME_HEADER_MAX, "no header is longer"); - inp.pos += take; - let mut bytes = [0; FRAME_HEADER_MAX]; - bytes[..view.len()].copy_from_slice(view); - dctx.stream_entry = StreamEntry::Header { - bytes, - len: view.len(), - }; - dctx.stream_frame_done = false; - return Ok(FrameEntry::WantsInput); + return Ok(hold_header(dctx, inp, view, take)); } // A malformed header (bad magic, invalid descriptor, ...) is a // decode error: the need-more-input hint with nothing consumed @@ -1128,6 +1134,26 @@ fn enter_stream_frame( } } +/// Hold `view`, a frame header the input has not all brought yet: the `take` +/// bytes of it from the input are consumed, and the frame has begun. +fn hold_header( + dctx: &mut ZSTD_DCtx, + inp: &mut ZSTD_inBuffer, + view: &[u8], + take: usize, +) -> FrameEntry { + debug_assert!(view.len() < FRAME_HEADER_MAX, "no header is longer"); + inp.pos += take; + let mut bytes = [0; FRAME_HEADER_MAX]; + bytes[..view.len()].copy_from_slice(view); + dctx.stream_entry = StreamEntry::Header { + bytes, + len: view.len(), + }; + dctx.stream_frame_done = false; + FrameEntry::WantsInput +} + /// Start the decoder on the frame whose header is `held` followed by the /// input, taking from the input what the header still needed. fn start_stream_frame( diff --git a/c-api/src/tests.rs b/c-api/src/tests.rs index 5c08b155..2db18747 100644 --- a/c-api/src/tests.rs +++ b/c-api/src/tests.rs @@ -6,6 +6,7 @@ mod bufferless; mod cctx_params; mod experimental; +mod legacy; mod provided; mod sequences; mod streams; diff --git a/c-api/src/tests/legacy.rs b/c-api/src/tests/legacy.rs new file mode 100644 index 00000000..221f54d3 --- /dev/null +++ b/c-api/src/tests/legacy.rs @@ -0,0 +1,276 @@ +//! v0.5-v0.7 frames through the C ABI, as libzstd 1.5.7 reads them by +//! default: the frame queries, the one-shot decoders and the stream, on the +//! frames the v0.5.1, v0.6.2 and v0.7.5 command lines wrote +//! (`zstd/legacy_frames`). + +use std::path::{Path, PathBuf}; + +use crate::attach::ZSTD_decompress_usingDict; +use crate::cdict::{ZSTD_createDDict, ZSTD_freeDDict}; +use crate::context::{ZSTD_createDCtx, ZSTD_freeDCtx}; +use crate::error::{ZSTD_ErrorCode, ZSTD_getErrorCode, ZSTD_isError}; +use crate::frame::{ZSTD_decompressBound, ZSTD_frameHeaderSize, ZSTD_isFrame}; +use crate::simple::{ZSTD_decompress, ZSTD_findFrameCompressedSize, ZSTD_getFrameContentSize}; +use crate::streaming::{ + ZSTD_createDStream, ZSTD_decompressStream, ZSTD_freeDStream, ZSTD_inBuffer, + ZSTD_initDStream_usingDDict, ZSTD_outBuffer, +}; + +struct Fixture { + name: String, + frame: Vec, + content: Vec, + dict: Option>, +} + +fn codec_dir() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("../zstd") +} + +/// Every fixture, with the file it decodes to and the dictionary it needs. +fn fixtures() -> Vec { + let dir = codec_dir().join("legacy_frames"); + let mut names: Vec = std::fs::read_dir(&dir) + .expect("legacy_frames directory") + .map(|entry| entry.unwrap().file_name().into_string().unwrap()) + .filter(|name| name.ends_with(".zst")) + .collect(); + names.sort(); + assert!(names.len() >= 50, "fixtures missing from {}", dir.display()); + names + .into_iter() + .map(|name| { + let (stem, tag) = ["v05", "v06", "v07"] + .iter() + .find_map(|tag| { + name.split_once(&format!(".{tag}.")) + .map(|(stem, _)| (stem.to_string(), *tag)) + }) + .expect("fixture name carries its version"); + let is_dict = name.contains(".dict"); + // `x` is the file N times over. + let (file, times) = stem + .rsplit_once('x') + .and_then(|(file, n)| Some((file, n.parse::().ok()?))) + .unwrap_or((stem.as_str(), 1)); + let source = if is_dict { + codec_dir().join("dict_tests/files").join(file) + } else { + codec_dir().join("decodecorpus_files").join(file) + }; + Fixture { + frame: std::fs::read(dir.join(&name)).unwrap(), + content: std::fs::read(source).unwrap().repeat(times), + dict: is_dict + .then(|| std::fs::read(dir.join(format!("dictionary.{tag}"))).unwrap()), + name, + } + }) + .collect() +} + +/// The frame queries: a frame, its length through the end block, a bound of +/// whole blocks, and the declared size or unknown. +#[test] +fn frame_queries_read_these_frames() { + for fixture in fixtures() { + let frame = &fixture.frame; + assert_eq!( + unsafe { ZSTD_isFrame(frame.as_ptr(), frame.len()) }, + 1, + "{}", + fixture.name + ); + assert_eq!( + unsafe { ZSTD_findFrameCompressedSize(frame.as_ptr(), frame.len()) }, + frame.len(), + "{}", + fixture.name + ); + // A frame `ZSTD_isFrame` accepts has a header to measure. + let header = unsafe { ZSTD_frameHeaderSize(frame.as_ptr(), frame.len()) }; + assert_eq!(ZSTD_isError(header), 0, "{}", fixture.name); + assert!((5..=18).contains(&header), "{}: {header}", fixture.name); + let bound = unsafe { ZSTD_decompressBound(frame.as_ptr(), frame.len()) }; + assert!(bound >= fixture.content.len() as u64, "{}", fixture.name); + assert_eq!(bound % (128 * 1024), 0, "{}", fixture.name); + // Cut short, the frame's length cannot be told. + let cut = unsafe { ZSTD_findFrameCompressedSize(frame.as_ptr(), frame.len() - 1) }; + assert_eq!( + ZSTD_getErrorCode(cut), + ZSTD_ErrorCode::ZSTD_error_srcSize_wrong, + "{}", + fixture.name + ); + } + let size = |name: &str| { + let fixture = fixtures().into_iter().find(|f| f.name == name).unwrap(); + unsafe { ZSTD_getFrameContentSize(fixture.frame.as_ptr(), fixture.frame.len()) } + }; + // v0.5 declares no size. + assert_eq!(size("abc.txt.v05.l1.zst"), u64::MAX); + // The v0.6.2 command line declares its first 128 KiB chunk. + assert_eq!(size("z000000.v06.l1.zst"), 128 * 1024); +} + +/// `ZSTD_decompress` decodes a frame that needs no dictionary; +/// `ZSTD_decompress_usingDict` one that does, with the dictionary file as +/// that version's command line trained it; one byte short is too small. +#[test] +fn one_shot_decoders_decode_these_frames() { + let dctx = ZSTD_createDCtx(); + for fixture in fixtures() { + let mut out = vec![0u8; fixture.content.len()]; + let written = match &fixture.dict { + None => unsafe { + ZSTD_decompress( + out.as_mut_ptr(), + out.len(), + fixture.frame.as_ptr(), + fixture.frame.len(), + ) + }, + Some(dict) => unsafe { + ZSTD_decompress_usingDict( + dctx, + out.as_mut_ptr(), + out.len(), + fixture.frame.as_ptr(), + fixture.frame.len(), + dict.as_ptr(), + dict.len(), + ) + }, + }; + assert_eq!( + ZSTD_isError(written), + 0, + "{}: {:?}", + fixture.name, + ZSTD_getErrorCode(written) + ); + assert_eq!(written, fixture.content.len(), "{}", fixture.name); + assert!(out == fixture.content, "{}", fixture.name); + if fixture.dict.is_none() && !out.is_empty() { + let short = unsafe { + ZSTD_decompress( + out.as_mut_ptr(), + out.len() - 1, + fixture.frame.as_ptr(), + fixture.frame.len(), + ) + }; + assert_eq!( + ZSTD_getErrorCode(short), + ZSTD_ErrorCode::ZSTD_error_dstSize_tooSmall, + "{}", + fixture.name + ); + } + } + unsafe { ZSTD_freeDCtx(dctx) }; +} + +/// A v0.7 frame naming a dictionary is refused without it, as the wrong +/// dictionary. +#[test] +fn a_v07_frame_without_its_dictionary_is_refused() { + let fixture = fixtures() + .into_iter() + .find(|f| f.name.ends_with(".v07.dict.zst")) + .unwrap(); + let mut out = vec![0u8; fixture.content.len()]; + let rc = unsafe { + ZSTD_decompress( + out.as_mut_ptr(), + out.len(), + fixture.frame.as_ptr(), + fixture.frame.len(), + ) + }; + assert_eq!( + ZSTD_getErrorCode(rc), + ZSTD_ErrorCode::ZSTD_error_dictionary_wrong + ); +} + +/// Decode `input` through a stream referencing `dict`, giving it `step` +/// bytes of input and `room` bytes of output per call. Returns the output +/// and the last call's result. +fn stream(input: &[u8], dict: Option<&[u8]>, step: usize, room: usize) -> (Vec, usize) { + let zds = ZSTD_createDStream(); + let ddict = dict.map(|dict| unsafe { ZSTD_createDDict(dict.as_ptr().cast(), dict.len()) }); + let rc = unsafe { ZSTD_initDStream_usingDDict(zds, ddict.unwrap_or(core::ptr::null_mut())) }; + assert_eq!(ZSTD_isError(rc), 0); + let mut out = Vec::new(); + let mut buf = vec![0u8; room]; + let mut at = 0; + let mut last = 0; + let mut idle = 0; + while at < input.len() || last != 0 { + let end = (at + step).min(input.len()); + let mut inb = ZSTD_inBuffer { + src: input[at..end].as_ptr().cast(), + size: end - at, + pos: 0, + }; + let mut outb = ZSTD_outBuffer { + dst: buf.as_mut_ptr().cast(), + size: buf.len(), + pos: 0, + }; + last = unsafe { ZSTD_decompressStream(zds, &mut outb, &mut inb) }; + if ZSTD_isError(last) != 0 { + break; + } + at += inb.pos; + out.extend_from_slice(&buf[..outb.pos]); + // Input all given and nothing moving: the stream wants no more. + if inb.pos == 0 && outb.pos == 0 { + idle += 1; + if idle > 2 { + break; + } + } else { + idle = 0; + } + } + if let Some(ddict) = ddict { + unsafe { ZSTD_freeDDict(ddict) }; + } + unsafe { ZSTD_freeDStream(zds) }; + (out, last) +} + +/// The stream decodes each frame from input in small pieces into small +/// output, its dictionary referenced as a DDict, and finishes with 0. +#[test] +fn the_stream_decodes_these_frames_in_pieces() { + for fixture in fixtures().into_iter().filter(|f| f.frame.len() < 64 * 1024) { + let (out, last) = stream(&fixture.frame, fixture.dict.as_deref(), 7, 1000); + assert_eq!(last, 0, "{}: {:?}", fixture.name, ZSTD_getErrorCode(last)); + assert!(out == fixture.content, "{}", fixture.name); + } +} + +/// One stream holding RFC 8878 frames and these, in turn, decodes them all. +#[test] +fn the_stream_decodes_these_frames_among_others() { + let fixture = fixtures() + .into_iter() + .find(|f| f.name == "z000001.v06.l19.zst") + .unwrap(); + let rfc = codec::encoding::compress_to_vec( + &b"between"[..], + codec::encoding::CompressionLevel::Fastest, + ); + let mut input = rfc.clone(); + input.extend_from_slice(&fixture.frame); + input.extend_from_slice(&rfc); + let mut expected = b"between".to_vec(); + expected.extend_from_slice(&fixture.content); + expected.extend_from_slice(b"between"); + let (out, last) = stream(&input, None, 5, 333); + assert_eq!(last, 0, "{:?}", ZSTD_getErrorCode(last)); + assert!(out == expected); +} diff --git a/scripts/legacy-fixtures.sh b/scripts/legacy-fixtures.sh new file mode 100755 index 00000000..2dc04e2b --- /dev/null +++ b/scripts/legacy-fixtures.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +# Regenerate zstd/legacy_frames: frames written by the v0.5, v0.6 and v0.7 +# release command lines, which the decoder must read back byte-exact. +# +# Usage: scripts/legacy-fixtures.sh [WORK_DIR] +# +# Each release is fetched from its tag, checked against a pinned digest and +# built from source. The inputs are files already in the repository, so every +# frame decodes to a file the tests can compare against. +set -euo pipefail + +root="$(cd "$(dirname "$0")/.." && pwd)" +# shellcheck source=scripts/upstream-zstd.sh +source "$root/scripts/upstream-zstd.sh" + +work="${1:-$(mktemp -d)}" +mkdir -p "$work" +out="$root/zstd/legacy_frames" +corpus="$root/zstd/decodecorpus_files" +samples="$root/zstd/dict_tests/files" + +# version tag, tarball digest +releases=( + "0.5.1 c23be381911764056087a6d0280544589f2fe44daef4c9223fe08ede6def45dc" + "0.6.2 f80c35b2860d66299c8457b891d05277ce4e88b9bdd1f10a431e74804404cb8f" + "0.7.5 6800defac9a93ddb1d9673d62c78526800df71cf9f353456f8e488ba4de51061" +) + +# Shapes the block decoder takes different paths for: empty, one byte, a +# few bytes, short text, several blocks, data that stays raw, and a mixed +# file with long matches and four-stream literals. +inputs=(z000062 z000013 z000001 abc.txt z000000 z000046 z000021) + +# build : the release's command line; prints its path. +build() { + local version="$1" digest="$2" dir="$work/v$1" + mkdir -p "$dir" + fetch_verified "https://github.com/facebook/zstd/archive/refs/tags/v${version}.tar.gz" \ + "$digest" "$dir/zstd-${version}.tar.gz" + rm -rf "$dir/zstd-${version}" + tar -xzf "$dir/zstd-${version}.tar.gz" -C "$dir" + make -s -C "$dir/zstd-${version}/programs" zstd MOREFLAGS=-Wno-error >&2 + echo "$dir/zstd-${version}/programs/zstd" +} + +rm -rf "$out" +mkdir -p "$out" + +# A frame several windows long, whose matches reach back across the point +# where a buffered decoder starts its next segment: z000000 eight times, each +# copy a match on the one before. Tests rebuild it from the name `x`. +repeated="$work/z000000x8" +: >"$repeated" +for _ in 1 2 3 4 5 6 7 8; do + cat "$corpus/z000000" >>"$repeated" +done + +for release in "${releases[@]}"; do + read -r version digest <<<"$release" + tag="v0${version:2:1}" + zstd_bin="$(build "$version" "$digest")" + + for input in "${inputs[@]}"; do + for level in 1 19; do + "$zstd_bin" -q -f "-$level" "$corpus/$input" -o "$out/$input.$tag.l$level.zst" + done + done + "$zstd_bin" -q -f -1 "$repeated" -o "$out/z000000x8.$tag.l1.zst" + + # A dictionary trained by the release itself, and frames that need it. + # The training set is listed in sorted order, so the dictionary is the same + # on every run; v0.7 would pick a random ID, so it gets a fixed one. + dict_args=() + if [[ "$tag" == v07 ]]; then + dict_args=(--dictID 7) + fi + find "$samples" -type f | LC_ALL=C sort | head -n 200 >"$work/samples.$tag" + # shellcheck disable=SC2046 + "$zstd_bin" -q -f --train $(cat "$work/samples.$tag") --maxdict 16384 \ + "${dict_args[@]}" -o "$out/dictionary.$tag" + for sample in $(LC_ALL=C ls "$samples" | head -n 3); do + "$zstd_bin" -q -f -3 -D "$out/dictionary.$tag" "$samples/$sample" \ + -o "$out/$sample.$tag.dict.zst" + done + + if [[ "$tag" == v07 ]]; then + # The content checksum and the dictionary ID are optional in v0.7. + "$zstd_bin" -q -f -3 --no-check "$corpus/z000000" -o "$out/z000000.$tag.nocheck.zst" + sample="$(LC_ALL=C ls "$samples" | head -n 1)" + "$zstd_bin" -q -f -3 --no-dictID -D "$out/dictionary.$tag" "$samples/$sample" \ + -o "$out/$sample.$tag.dict-noid.zst" + fi +done + +echo "wrote $(find "$out" -type f | wc -l | tr -d ' ') files to $out" diff --git a/zstd/Cargo.toml b/zstd/Cargo.toml index 9157b354..ce737ee7 100644 --- a/zstd/Cargo.toml +++ b/zstd/Cargo.toml @@ -11,7 +11,7 @@ license = "Apache-2.0" homepage = "https://structured-world.github.io/structured-zstd/dev/bench/" repository = "https://github.com/structured-world/structured-zstd" description = "Pure-Rust Zstandard (zstd) compression and decompression: all levels, streaming, dictionaries, no_std and WebAssembly ready — no FFI, no cmake" -exclude = ["fuzz_decodecorpus/*", "decodecorpus_files/*", "dict_tests/files/**"] +exclude = ["fuzz_decodecorpus/*", "decodecorpus_files/*", "dict_tests/files/**", "legacy_frames/*"] # Package metadata points at a crate-local symlink so the packaged crate and repo root README stay in sync. readme = "README.md" keywords = ["zstd", "zstandard", "decompression", "compression", "no-std"] @@ -129,6 +129,7 @@ default = [ # does not want it turns default features off, as `no_std` builds already # do. "dict-builder", + "legacy", ] # Per-CPU-tier kernel selection, covering the decoder and the encoder. The # default build enables every kernel except `kernel-vbmi2`; with `std`, @@ -168,6 +169,11 @@ kernel-sve = ["kernel-neon"] # simd128 equivalent and stay scalar on wasm. kernel-simd128 = [] dict-builder = ["std", "dep:fastrand"] +# Decode frames of the v0.5, v0.6 and v0.7 formats, the ones every libzstd +# 1.5.7 build reads by default (upstream's legacy level 5). Without it such a +# frame is refused as an unknown format, as a libzstd built at level 0 +# refuses it. Pure decoding code, no dependency. +legacy = [] hash = ["dep:twox-hash"] # Long-distance matching in the encoder. Implies `hash`: the LDM match finder # hashes each `min_match_length` window with XXH64. Without it the parameter diff --git a/zstd/fuzz/Cargo.toml b/zstd/fuzz/Cargo.toml index 91e50ae5..e05f9824 100644 --- a/zstd/fuzz/Cargo.toml +++ b/zstd/fuzz/Cargo.toml @@ -31,6 +31,10 @@ members = ["."] name = "decode" path = "fuzz_targets/decode.rs" +[[bin]] +name = "decode_legacy" +path = "fuzz_targets/decode_legacy.rs" + [[bin]] name = "decode_direct" path = "fuzz_targets/decode_direct.rs" diff --git a/zstd/fuzz/fuzz_targets/decode_legacy.rs b/zstd/fuzz/fuzz_targets/decode_legacy.rs new file mode 100644 index 00000000..dd0e0bc4 --- /dev/null +++ b/zstd/fuzz/fuzz_targets/decode_legacy.rs @@ -0,0 +1,61 @@ +#![no_main] +#[macro_use] +extern crate libfuzzer_sys; + +use std::io::Read; + +use structured_zstd::decoding::{ + FrameDecoder, LegacyVersion, StreamingDecoder, frame_decompressed_bound, +}; + +/// Output either path may produce before the input is set aside: enough for +/// many blocks, small enough that a bomb costs nothing. +const OUTPUT_CAP: usize = 4 << 20; + +fuzz_target!(|data: &[u8]| { + // Keep the input on a v0.5-v0.7 frame: when it does not open with one of + // their magic numbers, its first byte picks one and the number replaces + // its first four bytes. The fixtures in `zstd/legacy_frames` seed it as + // they are. + if data.len() < 4 { + return; + } + let mut input = data.to_vec(); + if LegacyVersion::of(&input).is_none() { + let minor = 0x25 + input[0] % 3; + input[..4].copy_from_slice(&[minor, 0xB5, 0x2F, 0xFD]); + } + + // The whole-buffer path, into a slice the frame's bound sizes. + let Ok(bound) = frame_decompressed_bound(&input) else { + // A frame whose blocks do not walk still goes to the stream, which + // must refuse it without panicking. + drain(&input, OUTPUT_CAP); + return; + }; + let room = usize::try_from(bound).map_or(OUTPUT_CAP, |bound| bound.min(OUTPUT_CAP)); + let mut whole = vec![0u8; room]; + let decoded = FrameDecoder::new().decode_all(&input, &mut whole); + + // The streaming path, a block at a time with history dropped behind it. + let streamed = drain(&input, room + 1); + + // Both paths decode the same frames to the same bytes. + if let (Ok(len), Some(streamed)) = (decoded, streamed) { + assert!( + streamed == whole[..len], + "the stream and the whole-buffer decode disagree" + ); + } +}); + +/// Read up to `cap` bytes of `input` through a `StreamingDecoder`; `None` +/// when the stream fails. +fn drain(input: &[u8], cap: usize) -> Option> { + let mut out = Vec::new(); + StreamingDecoder::new(input) + .take(cap as u64) + .read_to_end(&mut out) + .ok()?; + Some(out) +} diff --git a/zstd/legacy_frames/ModemManager.service.v05.dict.zst b/zstd/legacy_frames/ModemManager.service.v05.dict.zst new file mode 100644 index 00000000..9ee9c46d Binary files /dev/null and b/zstd/legacy_frames/ModemManager.service.v05.dict.zst differ diff --git a/zstd/legacy_frames/ModemManager.service.v06.dict.zst b/zstd/legacy_frames/ModemManager.service.v06.dict.zst new file mode 100644 index 00000000..25a3152a Binary files /dev/null and b/zstd/legacy_frames/ModemManager.service.v06.dict.zst differ diff --git a/zstd/legacy_frames/ModemManager.service.v07.dict-noid.zst b/zstd/legacy_frames/ModemManager.service.v07.dict-noid.zst new file mode 100644 index 00000000..aa2f9505 Binary files /dev/null and b/zstd/legacy_frames/ModemManager.service.v07.dict-noid.zst differ diff --git a/zstd/legacy_frames/ModemManager.service.v07.dict.zst b/zstd/legacy_frames/ModemManager.service.v07.dict.zst new file mode 100644 index 00000000..8dbe1dbf Binary files /dev/null and b/zstd/legacy_frames/ModemManager.service.v07.dict.zst differ diff --git a/zstd/legacy_frames/ModemManager.service.zst.v05.dict.zst b/zstd/legacy_frames/ModemManager.service.zst.v05.dict.zst new file mode 100644 index 00000000..a2f5c503 Binary files /dev/null and b/zstd/legacy_frames/ModemManager.service.zst.v05.dict.zst differ diff --git a/zstd/legacy_frames/ModemManager.service.zst.v06.dict.zst b/zstd/legacy_frames/ModemManager.service.zst.v06.dict.zst new file mode 100644 index 00000000..40aa1183 Binary files /dev/null and b/zstd/legacy_frames/ModemManager.service.zst.v06.dict.zst differ diff --git a/zstd/legacy_frames/ModemManager.service.zst.v07.dict.zst b/zstd/legacy_frames/ModemManager.service.zst.v07.dict.zst new file mode 100644 index 00000000..99435f9b Binary files /dev/null and b/zstd/legacy_frames/ModemManager.service.zst.v07.dict.zst differ diff --git a/zstd/legacy_frames/NetworkManager-dispatcher.service.v05.dict.zst b/zstd/legacy_frames/NetworkManager-dispatcher.service.v05.dict.zst new file mode 100644 index 00000000..0f1b0bdb Binary files /dev/null and b/zstd/legacy_frames/NetworkManager-dispatcher.service.v05.dict.zst differ diff --git a/zstd/legacy_frames/NetworkManager-dispatcher.service.v06.dict.zst b/zstd/legacy_frames/NetworkManager-dispatcher.service.v06.dict.zst new file mode 100644 index 00000000..25eb205e Binary files /dev/null and b/zstd/legacy_frames/NetworkManager-dispatcher.service.v06.dict.zst differ diff --git a/zstd/legacy_frames/NetworkManager-dispatcher.service.v07.dict.zst b/zstd/legacy_frames/NetworkManager-dispatcher.service.v07.dict.zst new file mode 100644 index 00000000..316b961c Binary files /dev/null and b/zstd/legacy_frames/NetworkManager-dispatcher.service.v07.dict.zst differ diff --git a/zstd/legacy_frames/abc.txt.v05.l1.zst b/zstd/legacy_frames/abc.txt.v05.l1.zst new file mode 100644 index 00000000..c8159097 Binary files /dev/null and b/zstd/legacy_frames/abc.txt.v05.l1.zst differ diff --git a/zstd/legacy_frames/abc.txt.v05.l19.zst b/zstd/legacy_frames/abc.txt.v05.l19.zst new file mode 100644 index 00000000..c8159097 Binary files /dev/null and b/zstd/legacy_frames/abc.txt.v05.l19.zst differ diff --git a/zstd/legacy_frames/abc.txt.v06.l1.zst b/zstd/legacy_frames/abc.txt.v06.l1.zst new file mode 100644 index 00000000..f0bc759d Binary files /dev/null and b/zstd/legacy_frames/abc.txt.v06.l1.zst differ diff --git a/zstd/legacy_frames/abc.txt.v06.l19.zst b/zstd/legacy_frames/abc.txt.v06.l19.zst new file mode 100644 index 00000000..f0bc759d Binary files /dev/null and b/zstd/legacy_frames/abc.txt.v06.l19.zst differ diff --git a/zstd/legacy_frames/abc.txt.v07.l1.zst b/zstd/legacy_frames/abc.txt.v07.l1.zst new file mode 100644 index 00000000..7e85747a Binary files /dev/null and b/zstd/legacy_frames/abc.txt.v07.l1.zst differ diff --git a/zstd/legacy_frames/abc.txt.v07.l19.zst b/zstd/legacy_frames/abc.txt.v07.l19.zst new file mode 100644 index 00000000..7e85747a Binary files /dev/null and b/zstd/legacy_frames/abc.txt.v07.l19.zst differ diff --git a/zstd/legacy_frames/dictionary.v05 b/zstd/legacy_frames/dictionary.v05 new file mode 100644 index 00000000..3e915d67 Binary files /dev/null and b/zstd/legacy_frames/dictionary.v05 differ diff --git a/zstd/legacy_frames/dictionary.v06 b/zstd/legacy_frames/dictionary.v06 new file mode 100644 index 00000000..d7946396 Binary files /dev/null and b/zstd/legacy_frames/dictionary.v06 differ diff --git a/zstd/legacy_frames/dictionary.v07 b/zstd/legacy_frames/dictionary.v07 new file mode 100644 index 00000000..008bcf64 Binary files /dev/null and b/zstd/legacy_frames/dictionary.v07 differ diff --git a/zstd/legacy_frames/z000000.v05.l1.zst b/zstd/legacy_frames/z000000.v05.l1.zst new file mode 100644 index 00000000..08c32d46 Binary files /dev/null and b/zstd/legacy_frames/z000000.v05.l1.zst differ diff --git a/zstd/legacy_frames/z000000.v05.l19.zst b/zstd/legacy_frames/z000000.v05.l19.zst new file mode 100644 index 00000000..57d2b971 Binary files /dev/null and b/zstd/legacy_frames/z000000.v05.l19.zst differ diff --git a/zstd/legacy_frames/z000000.v06.l1.zst b/zstd/legacy_frames/z000000.v06.l1.zst new file mode 100644 index 00000000..7b5c76c0 Binary files /dev/null and b/zstd/legacy_frames/z000000.v06.l1.zst differ diff --git a/zstd/legacy_frames/z000000.v06.l19.zst b/zstd/legacy_frames/z000000.v06.l19.zst new file mode 100644 index 00000000..1042617d Binary files /dev/null and b/zstd/legacy_frames/z000000.v06.l19.zst differ diff --git a/zstd/legacy_frames/z000000.v07.l1.zst b/zstd/legacy_frames/z000000.v07.l1.zst new file mode 100644 index 00000000..5132f9f1 Binary files /dev/null and b/zstd/legacy_frames/z000000.v07.l1.zst differ diff --git a/zstd/legacy_frames/z000000.v07.l19.zst b/zstd/legacy_frames/z000000.v07.l19.zst new file mode 100644 index 00000000..a98cdf36 Binary files /dev/null and b/zstd/legacy_frames/z000000.v07.l19.zst differ diff --git a/zstd/legacy_frames/z000000.v07.nocheck.zst b/zstd/legacy_frames/z000000.v07.nocheck.zst new file mode 100644 index 00000000..bc5d7d0f Binary files /dev/null and b/zstd/legacy_frames/z000000.v07.nocheck.zst differ diff --git a/zstd/legacy_frames/z000000x8.v05.l1.zst b/zstd/legacy_frames/z000000x8.v05.l1.zst new file mode 100644 index 00000000..0a88157f Binary files /dev/null and b/zstd/legacy_frames/z000000x8.v05.l1.zst differ diff --git a/zstd/legacy_frames/z000000x8.v06.l1.zst b/zstd/legacy_frames/z000000x8.v06.l1.zst new file mode 100644 index 00000000..6921fc05 Binary files /dev/null and b/zstd/legacy_frames/z000000x8.v06.l1.zst differ diff --git a/zstd/legacy_frames/z000000x8.v07.l1.zst b/zstd/legacy_frames/z000000x8.v07.l1.zst new file mode 100644 index 00000000..95ff55f6 Binary files /dev/null and b/zstd/legacy_frames/z000000x8.v07.l1.zst differ diff --git a/zstd/legacy_frames/z000001.v05.l1.zst b/zstd/legacy_frames/z000001.v05.l1.zst new file mode 100644 index 00000000..da41feb8 Binary files /dev/null and b/zstd/legacy_frames/z000001.v05.l1.zst differ diff --git a/zstd/legacy_frames/z000001.v05.l19.zst b/zstd/legacy_frames/z000001.v05.l19.zst new file mode 100644 index 00000000..da41feb8 Binary files /dev/null and b/zstd/legacy_frames/z000001.v05.l19.zst differ diff --git a/zstd/legacy_frames/z000001.v06.l1.zst b/zstd/legacy_frames/z000001.v06.l1.zst new file mode 100644 index 00000000..09b68207 Binary files /dev/null and b/zstd/legacy_frames/z000001.v06.l1.zst differ diff --git a/zstd/legacy_frames/z000001.v06.l19.zst b/zstd/legacy_frames/z000001.v06.l19.zst new file mode 100644 index 00000000..09b68207 Binary files /dev/null and b/zstd/legacy_frames/z000001.v06.l19.zst differ diff --git a/zstd/legacy_frames/z000001.v07.l1.zst b/zstd/legacy_frames/z000001.v07.l1.zst new file mode 100644 index 00000000..6bc9786f Binary files /dev/null and b/zstd/legacy_frames/z000001.v07.l1.zst differ diff --git a/zstd/legacy_frames/z000001.v07.l19.zst b/zstd/legacy_frames/z000001.v07.l19.zst new file mode 100644 index 00000000..6bc9786f Binary files /dev/null and b/zstd/legacy_frames/z000001.v07.l19.zst differ diff --git a/zstd/legacy_frames/z000013.v05.l1.zst b/zstd/legacy_frames/z000013.v05.l1.zst new file mode 100644 index 00000000..85b5035c Binary files /dev/null and b/zstd/legacy_frames/z000013.v05.l1.zst differ diff --git a/zstd/legacy_frames/z000013.v05.l19.zst b/zstd/legacy_frames/z000013.v05.l19.zst new file mode 100644 index 00000000..85b5035c Binary files /dev/null and b/zstd/legacy_frames/z000013.v05.l19.zst differ diff --git a/zstd/legacy_frames/z000013.v06.l1.zst b/zstd/legacy_frames/z000013.v06.l1.zst new file mode 100644 index 00000000..ba916f90 Binary files /dev/null and b/zstd/legacy_frames/z000013.v06.l1.zst differ diff --git a/zstd/legacy_frames/z000013.v06.l19.zst b/zstd/legacy_frames/z000013.v06.l19.zst new file mode 100644 index 00000000..ba916f90 Binary files /dev/null and b/zstd/legacy_frames/z000013.v06.l19.zst differ diff --git a/zstd/legacy_frames/z000013.v07.l1.zst b/zstd/legacy_frames/z000013.v07.l1.zst new file mode 100644 index 00000000..ce9a95d4 Binary files /dev/null and b/zstd/legacy_frames/z000013.v07.l1.zst differ diff --git a/zstd/legacy_frames/z000013.v07.l19.zst b/zstd/legacy_frames/z000013.v07.l19.zst new file mode 100644 index 00000000..ce9a95d4 Binary files /dev/null and b/zstd/legacy_frames/z000013.v07.l19.zst differ diff --git a/zstd/legacy_frames/z000021.v05.l1.zst b/zstd/legacy_frames/z000021.v05.l1.zst new file mode 100644 index 00000000..9938d82a Binary files /dev/null and b/zstd/legacy_frames/z000021.v05.l1.zst differ diff --git a/zstd/legacy_frames/z000021.v05.l19.zst b/zstd/legacy_frames/z000021.v05.l19.zst new file mode 100644 index 00000000..7efffb88 Binary files /dev/null and b/zstd/legacy_frames/z000021.v05.l19.zst differ diff --git a/zstd/legacy_frames/z000021.v06.l1.zst b/zstd/legacy_frames/z000021.v06.l1.zst new file mode 100644 index 00000000..51674a90 Binary files /dev/null and b/zstd/legacy_frames/z000021.v06.l1.zst differ diff --git a/zstd/legacy_frames/z000021.v06.l19.zst b/zstd/legacy_frames/z000021.v06.l19.zst new file mode 100644 index 00000000..cf969885 Binary files /dev/null and b/zstd/legacy_frames/z000021.v06.l19.zst differ diff --git a/zstd/legacy_frames/z000021.v07.l1.zst b/zstd/legacy_frames/z000021.v07.l1.zst new file mode 100644 index 00000000..726dfcda Binary files /dev/null and b/zstd/legacy_frames/z000021.v07.l1.zst differ diff --git a/zstd/legacy_frames/z000021.v07.l19.zst b/zstd/legacy_frames/z000021.v07.l19.zst new file mode 100644 index 00000000..7600981d Binary files /dev/null and b/zstd/legacy_frames/z000021.v07.l19.zst differ diff --git a/zstd/legacy_frames/z000046.v05.l1.zst b/zstd/legacy_frames/z000046.v05.l1.zst new file mode 100644 index 00000000..8ab41480 Binary files /dev/null and b/zstd/legacy_frames/z000046.v05.l1.zst differ diff --git a/zstd/legacy_frames/z000046.v05.l19.zst b/zstd/legacy_frames/z000046.v05.l19.zst new file mode 100644 index 00000000..8ab41480 Binary files /dev/null and b/zstd/legacy_frames/z000046.v05.l19.zst differ diff --git a/zstd/legacy_frames/z000046.v06.l1.zst b/zstd/legacy_frames/z000046.v06.l1.zst new file mode 100644 index 00000000..5e62620c Binary files /dev/null and b/zstd/legacy_frames/z000046.v06.l1.zst differ diff --git a/zstd/legacy_frames/z000046.v06.l19.zst b/zstd/legacy_frames/z000046.v06.l19.zst new file mode 100644 index 00000000..5e62620c Binary files /dev/null and b/zstd/legacy_frames/z000046.v06.l19.zst differ diff --git a/zstd/legacy_frames/z000046.v07.l1.zst b/zstd/legacy_frames/z000046.v07.l1.zst new file mode 100644 index 00000000..dc2b65ca Binary files /dev/null and b/zstd/legacy_frames/z000046.v07.l1.zst differ diff --git a/zstd/legacy_frames/z000046.v07.l19.zst b/zstd/legacy_frames/z000046.v07.l19.zst new file mode 100644 index 00000000..dc2b65ca Binary files /dev/null and b/zstd/legacy_frames/z000046.v07.l19.zst differ diff --git a/zstd/legacy_frames/z000062.v05.l1.zst b/zstd/legacy_frames/z000062.v05.l1.zst new file mode 100644 index 00000000..d685c9b3 Binary files /dev/null and b/zstd/legacy_frames/z000062.v05.l1.zst differ diff --git a/zstd/legacy_frames/z000062.v05.l19.zst b/zstd/legacy_frames/z000062.v05.l19.zst new file mode 100644 index 00000000..a001f3ce Binary files /dev/null and b/zstd/legacy_frames/z000062.v05.l19.zst differ diff --git a/zstd/legacy_frames/z000062.v06.l1.zst b/zstd/legacy_frames/z000062.v06.l1.zst new file mode 100644 index 00000000..931714e1 Binary files /dev/null and b/zstd/legacy_frames/z000062.v06.l1.zst differ diff --git a/zstd/legacy_frames/z000062.v06.l19.zst b/zstd/legacy_frames/z000062.v06.l19.zst new file mode 100644 index 00000000..64b7c7d7 Binary files /dev/null and b/zstd/legacy_frames/z000062.v06.l19.zst differ diff --git a/zstd/legacy_frames/z000062.v07.l1.zst b/zstd/legacy_frames/z000062.v07.l1.zst new file mode 100644 index 00000000..57752b6d --- /dev/null +++ b/zstd/legacy_frames/z000062.v07.l1.zst @@ -0,0 +1 @@ +'µ/ýHê; \ No newline at end of file diff --git a/zstd/legacy_frames/z000062.v07.l19.zst b/zstd/legacy_frames/z000062.v07.l19.zst new file mode 100644 index 00000000..73872dab --- /dev/null +++ b/zstd/legacy_frames/z000062.v07.l19.zst @@ -0,0 +1 @@ +'µ/ýhê; \ No newline at end of file diff --git a/zstd/src/bin/structured-zstd/main.rs b/zstd/src/bin/structured-zstd/main.rs index d3189642..bbf71b0f 100644 --- a/zstd/src/bin/structured-zstd/main.rs +++ b/zstd/src/bin/structured-zstd/main.rs @@ -6650,13 +6650,6 @@ fn encode_frame( encoder.finish().wrap_err("failed to finalize zstd frame") } -/// The magic number every zstd frame opens with (RFC 8878 §3.1.1). -const FRAME_MAGIC: u32 = 0xFD2F_B528; - -/// The 16 magic numbers a skippable frame may open with, less their low -/// nibble (RFC 8878 §3.1.2). -const SKIPPABLE_MAGIC_BASE: u32 = 0x184D_2A50; - /// Streaming decompression core (file, stdout, or sink), optionally /// dict-primed. Returns the number of bytes written. /// @@ -6746,10 +6739,9 @@ fn decode_or_pass( _ => Ok(Decoded::Frames(written)), }; } - let is_frame = read == 4 && { - let magic = u32::from_le_bytes(magic); - magic == FRAME_MAGIC || magic & 0xFFFF_FFF0 == SKIPPABLE_MAGIC_BASE - }; + // The codec's answer, as the reference asks `ZSTD_isFrame`: with the + // `legacy` feature a v0.5-v0.7 frame is one. + let is_frame = read == 4 && structured_zstd::decoding::is_frame(&magic); // Another compressor's stream is named and refused, pass-through or // not, in the order the reference checks it (`FIO_decompressFrames`): // copying it through would hand on compressed bytes as content. Input diff --git a/zstd/src/bin/structured-zstd/tests.rs b/zstd/src/bin/structured-zstd/tests.rs index 3cc44ed5..b6071f77 100644 --- a/zstd/src/bin/structured-zstd/tests.rs +++ b/zstd/src/bin/structured-zstd/tests.rs @@ -1830,6 +1830,47 @@ fn decoding_names_the_foreign_format_it_cannot_read() { assert!(decode_stream_message(b"some data").contains("unsupported format")); } +/// The v0.5-v0.7 frames the reference command decodes by default decode here +/// too, and a v0.7 frame naming a dictionary it was not given fails as the +/// reference's does. +#[cfg(feature = "legacy")] +#[test] +fn decoding_reads_the_v05_to_v07_formats() { + let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("legacy_frames"); + for name in [ + "abc.txt.v05.l19.zst", + "z000000.v06.l1.zst", + "z000001.v07.l19.zst", + ] { + let frame = fs::read(dir.join(name)).unwrap(); + let stem = name.split_once(".v0").unwrap().0; + let source = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("decodecorpus_files") + .join(stem); + let mut out = Vec::new(); + decompress_stream( + &frame[..], + &mut out, + &mut no_dict(), + &DecodeSettings::default(), + ) + .unwrap_or_else(|err| panic!("{name}: {err}")); + assert!(out == fs::read(source).unwrap(), "{name}"); + } + let frame = fs::read(dir.join("ModemManager.service.v07.dict.zst")).unwrap(); + let err = decompress_stream( + &frame[..], + io::sink(), + &mut no_dict(), + &DecodeSettings::default(), + ) + .expect_err("the frame names a dictionary"); + assert_eq!( + named(err, "in.zst").to_string(), + "in.zst : Decoding error (36) : Dictionary mismatch " + ); +} + /// A decoding failure is reported against the input, in the reference /// command's two shapes (`fileio.c` 2463, 2748-2796): `zstd: NAME: message` /// for input that is not a frame, and `NAME : Decoding error (36) : ErrorCode { + match self { + LegacyFrameError::UnsupportedParameter => ErrorCode::FrameParameterUnsupported, + LegacyFrameError::OutputTooSmall => ErrorCode::DstSizeTooSmall, + LegacyFrameError::DictionaryCorrupted => ErrorCode::DictionaryCorrupted, + LegacyFrameError::DictionaryMismatch { .. } => ErrorCode::DictionaryWrong, + LegacyFrameError::ChecksumMismatch { .. } => ErrorCode::ChecksumWrong, + LegacyFrameError::Truncated | LegacyFrameError::Corrupted { .. } => { + ErrorCode::CorruptionDetected + } + } + } +} + +#[cfg(feature = "legacy")] +impl fmt::Display for LegacyFrameError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Truncated => write!(f, "the input ends inside a v0.5-v0.7 frame"), + Self::UnsupportedParameter => { + write!( + f, + "the v0.5-v0.7 frame header asks for an unsupported setting" + ) + } + Self::Corrupted { reason } => write!(f, "corrupt v0.5-v0.7 frame: {reason}"), + Self::OutputTooSmall => write!(f, "the output buffer is too small for the frame"), + Self::DictionaryCorrupted => write!(f, "the dictionary could not be read"), + Self::DictionaryMismatch { expected, provided } => write!( + f, + "the frame needs dictionary 0x{expected:X}, the one supplied is 0x{provided:X}" + ), + Self::ChecksumMismatch { + expected, + calculated, + } => write!( + f, + "content checksum mismatch: frame stored 0x{expected:06X}, decoder calculated 0x{calculated:06X}" + ), + } + } +} + +#[cfg(all(feature = "legacy", feature = "std"))] +impl StdError for LegacyFrameError {} + #[derive(Debug)] #[non_exhaustive] pub enum FrameHeaderError { @@ -844,6 +929,17 @@ pub enum FrameDecoderError { /// to make it resumable. #[cfg(feature = "lsm")] ResumeUnidentifiedDictionary, + /// A frame of the v0.5, v0.6 or v0.7 format failed to decode. + #[cfg(feature = "legacy")] + #[cfg_attr(docsrs, doc(cfg(feature = "legacy")))] + LegacyFrame(LegacyFrameError), +} + +#[cfg(feature = "legacy")] +impl From for FrameDecoderError { + fn from(err: LegacyFrameError) -> Self { + FrameDecoderError::LegacyFrame(err) + } } impl FrameDecoderError { @@ -873,6 +969,8 @@ impl FrameDecoderError { FrameDecoderError::TargetTooSmall => ErrorCode::DstSizeTooSmall, FrameDecoderError::NotYetInitialized => ErrorCode::InitMissing, FrameDecoderError::ChecksumMismatch { .. } => ErrorCode::ChecksumWrong, + #[cfg(feature = "legacy")] + FrameDecoderError::LegacyFrame(err) => err.code(), _ => ErrorCode::CorruptionDetected, } } @@ -894,6 +992,8 @@ impl StdError for FrameDecoderError { FrameDecoderError::FailedToReadChecksum(source) => Some(source), FrameDecoderError::FailedToInitialize(source) => Some(source), FrameDecoderError::FailedToDrainDecodebuffer(source) => Some(source), + #[cfg(feature = "legacy")] + FrameDecoderError::LegacyFrame(source) => Some(source), _ => None, } } @@ -1069,6 +1169,8 @@ impl core::fmt::Display for FrameDecoderError { "a dictionary with no ID cannot be told from another one, so it cannot carry a resume state across frames" ) } + #[cfg(feature = "legacy")] + FrameDecoderError::LegacyFrame(e) => write!(f, "{e}"), } } } diff --git a/zstd/src/decoding/frame.rs b/zstd/src/decoding/frame.rs index aca9aad6..8a4281e9 100644 --- a/zstd/src/decoding/frame.rs +++ b/zstd/src/decoding/frame.rs @@ -107,7 +107,7 @@ pub fn read_frame_header_with_format( Ok((frame_header, bytes_read as u8)) } -fn eof() -> crate::io::Error { +pub(crate) fn eof() -> crate::io::Error { crate::io::Error::from(crate::io::ErrorKind::UnexpectedEof) } diff --git a/zstd/src/decoding/frame_decoder.rs b/zstd/src/decoding/frame_decoder.rs index 4cc002a2..9efc28ff 100644 --- a/zstd/src/decoding/frame_decoder.rs +++ b/zstd/src/decoding/frame_decoder.rs @@ -219,6 +219,11 @@ pub struct FrameDecoder { /// [`Self::reset_with_active_dict`]. Only ever `Some` while `state` is /// `None`: once a state exists the dictionary lives there. armed_dict: Option, + /// The v0.5-v0.7 frame a `Read`-driven reset started, decoded step by + /// step in place of `state`. Boxed so a decoder that never meets one + /// carries a pointer, not the stream. + #[cfg(feature = "legacy")] + legacy: Option>, } /// How the decoder treats a frame's optional XXH64 content checksum @@ -884,6 +889,14 @@ impl DecoderScratchKind { } } +/// `dict` as the v0.5-v0.7 decoders take it. +#[cfg(feature = "legacy")] +fn legacy_dict(dict: Option<&DictionaryHandle>) -> decoding::legacy::LegacyDict<'_> { + dict.map_or(decoding::legacy::LegacyDict::None, |dict| { + decoding::legacy::LegacyDict::Parsed(dict.as_dict()) + }) +} + /// The dictionary of `dicts` a frame naming `dict_id` decodes with: the one /// carrying that ID, else the last, as upstream keeps the most recently /// referenced DDict when its set holds none for the frame @@ -1251,6 +1264,135 @@ impl FrameDecoder { computed_block_checksums: alloc::vec::Vec::new(), kernel: crate::cpu_kernel::detect_cpu_kernel(), armed_dict: None, + #[cfg(feature = "legacy")] + legacy: None, + } + } + + /// The v0.5-v0.7 frame in progress, if the current frame is one. + #[cfg(feature = "legacy")] + #[inline] + fn legacy_frame(&self) -> Option<&decoding::legacy::LegacyStream> { + self.legacy.as_deref().filter(|stream| stream.active()) + } + + /// [`Self::legacy_frame`], mutable. + #[cfg(feature = "legacy")] + #[inline] + fn legacy_frame_mut(&mut self) -> Option<&mut decoding::legacy::LegacyStream> { + self.legacy.as_deref_mut().filter(|stream| stream.active()) + } + + /// Leave any v0.5-v0.7 frame in progress: an RFC 8878 frame has started. + #[inline] + fn end_legacy_frame(&mut self) { + #[cfg(feature = "legacy")] + if let Some(stream) = self.legacy.as_deref_mut() { + stream.end(); + } + } + + /// Read the frame header from `source` into the frame state, creating it + /// on first use. A v0.5-v0.7 frame is started instead when its magic + /// number is what the header begins with, its dictionary chosen from + /// `dicts` (the registry's when empty); `Ok(false)` reports that. + fn read_frame_header_into_state( + &mut self, + source: &mut R, + #[cfg_attr(not(feature = "legacy"), allow(unused_variables))] dicts: &[DictionaryHandle], + ) -> Result { + // A new header ends any v0.5-v0.7 frame, whether or not it reads. + self.end_legacy_frame(); + let magicless = self.magicless; + let max_window = self.max_window_size; + let block_limit = self.max_block_size; + let parsed = match &mut self.state { + Some(s) => s.reset_with_format(&mut *source, magicless, max_window, block_limit), + None => { + FrameDecoderState::new_with_format(&mut *source, magicless, max_window, block_limit) + .map(|state| { + self.state = Some(state); + // A frame that resolves its dictionary by ID drops one armed + // for a forced-dictionary stream that never started. + self.armed_dict = None; + }) + } + }; + match parsed { + Ok(()) => Ok(true), + #[cfg(feature = "legacy")] + Err(FrameDecoderError::ReadFrameHeaderError( + crate::decoding::errors::ReadFrameHeaderError::BadMagicNumber(magic), + )) if crate::decoding::LegacyVersion::of(&magic.to_le_bytes()).is_some() => { + self.start_legacy_frame(magic, source, dicts)?; + Ok(false) + } + Err(err) => Err(err), + } + } + + /// Start the v0.5-v0.7 frame whose magic number `magic` has been read + /// from `source`, reading the rest of its header from it. + #[cfg(feature = "legacy")] + fn start_legacy_frame( + &mut self, + magic: u32, + source: &mut R, + dicts: &[DictionaryHandle], + ) -> Result<(), FrameDecoderError> { + use crate::decoding::errors::ReadFrameHeaderError as ReadError; + use crate::decoding::legacy::{self, HeaderFields, LegacyStream}; + let version = crate::decoding::LegacyVersion::of(&magic.to_le_bytes()) + .expect("the caller checked the magic number"); + // The longest header: magic, descriptor, window, dictionary ID and an + // eight-byte content size. + let mut bytes = [0u8; 18]; + bytes[..4].copy_from_slice(&magic.to_le_bytes()); + // A reader's own error is handed back in the field it failed in, as + // for an RFC 8878 header. + source + .read_exact(&mut bytes[4..5]) + .map_err(ReadError::FrameDescriptorReadError)?; + let fields = HeaderFields::of(version, bytes[4]); + let mut at = 5; + let mut read_field = |len: usize, failed: fn(Error) -> ReadError| { + source + .read_exact(&mut bytes[at..at + len]) + .map_err(failed)?; + at += len; + Ok::<_, ReadError>(()) + }; + read_field(fields.window, ReadError::WindowDescriptorReadError)?; + read_field(fields.dict_id, ReadError::DictionaryIdReadError)?; + read_field(fields.content_size, ReadError::FrameContentSizeReadError)?; + let header = legacy::read_header(&bytes[..at])?; + #[cfg(feature = "lsm")] + self.validate_legacy_expectations(&header, &bytes[..at])?; + if header.window > self.max_window_size { + return Err(FrameDecoderError::WindowSizeTooBig { + requested: header.window, + limit: self.max_window_size, + }); + } + // The stream outlives this call, so it holds the handle: one count, + // once per frame. + let dict = self.legacy_frame_dict(header.dict_id, dicts)?.cloned(); + let options = self.legacy_frame_options(); + self.legacy + .get_or_insert_with(|| alloc::boxed::Box::new(LegacyStream::new())) + .start(header, dict, options)?; + Ok(()) + } + + /// What the decoder's settings ask of a v0.5-v0.7 frame. + #[cfg(feature = "legacy")] + fn legacy_frame_options(&self) -> decoding::legacy::FrameOptions { + decoding::legacy::FrameOptions { + #[cfg(feature = "hash")] + checksum: self.content_checksum, + block_max: self.max_block_size, + #[cfg(all(feature = "lsm", feature = "hash"))] + block_digests: self.per_block_checksums_enabled, } } @@ -1281,9 +1423,16 @@ impl FrameDecoder { /// `ZSTD_sizeof_DCtx`. Shared dictionaries (ref-counted handles) are not /// counted, matching upstream excluding `refDDict` memory. pub fn workspace_size(&self) -> usize { - self.state + let frame = self + .state .as_ref() - .map_or(0, |s| s.decoder_scratch.workspace_bytes()) + .map_or(0, |s| s.decoder_scratch.workspace_bytes()); + #[cfg(feature = "legacy")] + let frame = frame + + self.legacy.as_ref().map_or(0, |stream| { + core::mem::size_of::() + stream.heap_bytes() + }); + frame } /// Select how the frame's optional content checksum is handled @@ -1320,6 +1469,10 @@ impl FrameDecoder { /// Behind `all(feature = "lsm", feature = "hash")`. #[cfg(all(feature = "lsm", feature = "hash"))] pub fn computed_block_checksums(&self) -> &[u32] { + #[cfg(feature = "legacy")] + if let Some(stream) = self.legacy_frame() { + return stream.block_digests(); + } &self.computed_block_checksums } @@ -1400,9 +1553,38 @@ impl FrameDecoder { fn validate_expectations( &self, frame_header: &frame::FrameHeader, + ) -> Result<(), FrameDecoderError> { + self.validate_pinned_fields( + frame_header.dictionary_id(), + frame_header.window_descriptor(), + ) + } + + /// [`Self::validate_expectations`] for a v0.5-v0.7 header, whose fields + /// are held to the same pins: a v0.7 header carries both, v0.5 and v0.6 + /// ones neither. + #[cfg(all(feature = "lsm", feature = "legacy"))] + fn validate_legacy_expectations( + &self, + header: &decoding::legacy::FrameHeader, + header_bytes: &[u8], + ) -> Result<(), FrameDecoderError> { + self.validate_pinned_fields( + header.dictionary_id(), + header.window_descriptor(header_bytes), + ) + } + + /// Hold a header's dictionary ID and window descriptor, `None` where it + /// carries none, to the pinned expectations. + #[cfg(feature = "lsm")] + fn validate_pinned_fields( + &self, + dictionary_id: Option, + window_descriptor: Option, ) -> Result<(), FrameDecoderError> { if let Some(expected) = self.expect_dict_id { - let found = frame_header.dictionary_id(); + let found = dictionary_id; // `Some(0)` is the "no dictionary expected" sentinel — // matches a frame whose header omits the optional // dict_id field (which is reported as `None` by the @@ -1420,7 +1602,7 @@ impl FrameDecoder { } } if let Some(expected) = self.expect_window_descriptor { - let found = frame_header.window_descriptor(); + let found = window_descriptor; if found != Some(expected) { return Err(FrameDecoderError::UnexpectedWindowDescriptor { expected, found }); } @@ -1504,6 +1686,10 @@ impl FrameDecoder { { state.window_refused_at = (window > size).then_some(size); } + #[cfg(feature = "legacy")] + if let Some(stream) = self.legacy_frame_mut() { + stream.set_window_ceiling(size); + } Ok(()) } @@ -1511,6 +1697,15 @@ impl FrameDecoder { /// window after it started (see [`Self::set_max_window_size`]). Every /// entry that decodes a step of the frame asks first. pub(crate) fn check_window_still_admitted(&self) -> Result<(), FrameDecoderError> { + #[cfg(feature = "legacy")] + if let Some(stream) = self.legacy_frame() { + return match stream.window_refusal() { + Some((requested, limit)) => { + Err(FrameDecoderError::WindowSizeTooBig { requested, limit }) + } + None => Ok(()), + }; + } if let Some(state) = self.state.as_ref() && let Some(limit) = state.window_refused_at { @@ -1567,6 +1762,11 @@ impl FrameDecoder { /// content checksum is still to be read, as /// [`Self::read_content_checksum`] takes it. pub fn awaits_checksum(&self) -> bool { + // A v0.7 checksum rides in the end block, read as a block. + #[cfg(feature = "legacy")] + if self.legacy_frame().is_some() { + return false; + } self.state.as_ref().is_some_and(|state| { state.frame_finished && state.frame_header.descriptor.content_checksum_flag() @@ -1596,6 +1796,10 @@ impl FrameDecoder { /// assert!(decoder.input_needed(source) <= source.len()); /// ``` pub fn input_needed(&self, pending: &[u8]) -> usize { + #[cfg(feature = "legacy")] + if let Some(stream) = self.legacy_frame() { + return stream.input_needed(pending); + } let Some(state) = self.state.as_ref() else { return 0; }; @@ -1638,6 +1842,11 @@ impl FrameDecoder { header: &[u8; 3], reserve_window: bool, ) -> Result, FrameDecoderError> { + // A v0.5-v0.7 block is decoded whole, as its step. + #[cfg(feature = "legacy")] + if self.legacy_frame().is_some() { + return Ok(None); + } // Before the window is reserved below. self.check_window_still_admitted()?; let kernel = self.kernel; @@ -2067,36 +2276,20 @@ impl FrameDecoder { /// /// equivalent to init() #[inline] - pub fn reset(&mut self, source: impl Read) -> Result<(), FrameDecoderError> { + pub fn reset(&mut self, mut source: impl Read) -> Result<(), FrameDecoderError> { use FrameDecoderError as err; // Fresh frame → start with an empty per-block checksum vec so // the values for the next frame don't carry over from the // previous one. #[cfg(all(feature = "lsm", feature = "hash"))] self.computed_block_checksums.clear(); - let magicless = self.magicless; - let max_window = self.max_window_size; - let block_limit = self.max_block_size; - let dict_id = match &mut self.state { - Some(s) => { - s.reset_with_format(source, magicless, max_window, block_limit)?; - s.frame_header.dictionary_id() - } - None => { - self.state = Some(FrameDecoderState::new_with_format( - source, - magicless, - max_window, - block_limit, - )?); - // A frame that resolves its dictionary by ID drops one armed - // for a forced-dictionary stream that never started. - self.armed_dict = None; - self.state - .as_ref() - .and_then(|state| state.frame_header.dictionary_id()) - } - }; + if !self.read_frame_header_into_state(&mut source, &[])? { + return Ok(()); + } + let dict_id = self + .state + .as_ref() + .and_then(|state| state.frame_header.dictionary_id()); // Validate any pinned expectations BEFORE block decode work // runs. Catches dict_id substitution / window-descriptor // tampering on inputs already authenticated by an outer @@ -2150,6 +2343,7 @@ impl FrameDecoder { let max_window = crate::common::MAX_DECODER_WINDOW_SIZE; let block_limit = self.max_block_size; let (frame_header, header_size) = frame::read_frame_header_from_slice(input, magicless)?; + self.end_legacy_frame(); let dict_id = match &mut self.state { Some(s) => { s.reset_with_parsed_header(frame_header, header_size, max_window, block_limit)?; @@ -2257,11 +2451,12 @@ impl FrameDecoder { if dicts.is_empty() { return self.reset(source); } - let dict = self.reset_frame_for_dicts(source, dicts)?; - self.state - .as_mut() - .expect("state populated by reset_frame_for_dicts") - .set_active_dict(dict); + if let Some(dict) = self.reset_frame_for_dicts(source, dicts)? { + self.state + .as_mut() + .expect("state populated by reset_frame_for_dicts") + .set_active_dict(dict); + } Ok(()) } @@ -2315,12 +2510,13 @@ impl FrameDecoder { /// Everything [`reset_with_dict_handle`](Self::reset_with_dict_handle) /// does except storing the handle: parse the header, pick the dictionary /// of `dicts` the frame decodes with ([`dictionary_for_frame`]), check the - /// ID, and point the entropy tables at it. `dicts` is not empty. + /// ID, and point the entropy tables at it. `dicts` is not empty. `None` + /// when the frame is a v0.5-v0.7 one, which took its dictionary itself. fn reset_frame_for_dicts<'d>( &mut self, - source: impl Read, + mut source: impl Read, dicts: &'d [DictionaryHandle], - ) -> Result<&'d DictionaryHandle, FrameDecoderError> { + ) -> Result, FrameDecoderError> { use FrameDecoderError as err; // Fresh frame → drop the previous frame's per-block checksum // digests so the next decode starts with an empty vec. @@ -2331,24 +2527,8 @@ impl FrameDecoder { for dict in dicts { Self::validate_dictionary_content(dict.as_dict())?; } - let magicless = self.magicless; - let max_window = self.max_window_size; - let block_limit = self.max_block_size; - // Scope the &mut borrow of `self.state` to the header parse - // alone, so the subsequent `validate_expectations(&self, ...)` - // call below can take a fresh shared borrow of self without - // tripping the borrow checker. - match &mut self.state { - Some(s) => s.reset_with_format(source, magicless, max_window, block_limit)?, - None => { - self.state = Some(FrameDecoderState::new_with_format( - source, - magicless, - max_window, - block_limit, - )?); - self.armed_dict = None; - } + if !self.read_frame_header_into_state(&mut source, dicts)? { + return Ok(None); } // Single source of truth: route through the same // `validate_expectations` used by `reset()`. Routing through @@ -2379,7 +2559,7 @@ impl FrameDecoder { } state.decoder_scratch.init_from_dict(dict); state.using_dict = Some(dict.id()); - Ok(dict) + Ok(Some(dict)) } /// Slice-direct equivalent of [`reset_with_dict_handle`](Self::reset_with_dict_handle) @@ -2415,6 +2595,7 @@ impl FrameDecoder { let max_window = crate::common::MAX_DECODER_WINDOW_SIZE; let block_limit = self.max_block_size; let (frame_header, header_size) = frame::read_frame_header_from_slice(input, magicless)?; + self.end_legacy_frame(); match &mut self.state { Some(s) => { s.reset_with_parsed_header(frame_header, header_size, max_window, block_limit)?; @@ -2499,6 +2680,16 @@ impl FrameDecoder { pub fn force_dict(&mut self, dict_id: u32) -> Result<(), FrameDecoderError> { use FrameDecoderError as err; + #[cfg(feature = "legacy")] + if self.legacy_frame().is_some() { + // The stream outlives this call, so it holds the handle. + let dict = self + .registered_dict(dict_id) + .ok_or(err::DictNotProvided { dict_id })? + .clone(); + let stream = self.legacy_frame_mut().expect("checked above"); + return Ok(stream.force_dict(dict)?); + } let state = self.state.as_mut().ok_or(err::NotYetInitialized)?; let owned_dicts = &self.owned_dicts; #[cfg(target_has_atomic = "ptr")] @@ -2526,6 +2717,10 @@ impl FrameDecoder { /// Returns how many bytes the frame contains after decompression pub fn content_size(&self) -> u64 { + #[cfg(feature = "legacy")] + if let Some(stream) = self.legacy_frame() { + return stream.content_size(); + } match &self.state { None => 0, Some(s) => s.frame_header.frame_content_size(), @@ -2533,7 +2728,13 @@ impl FrameDecoder { } /// Returns the checksum that was read from the data. Only available after all bytes have been read. It is the last 4 bytes of a zstd-frame + /// (of a v0.7 frame, the 22 bits its end block holds, read where the + /// checksum is computed). pub fn get_checksum_from_data(&self) -> Option { + #[cfg(feature = "legacy")] + if let Some(stream) = self.legacy_frame() { + return stream.stored_checksum(); + } let state = self.state.as_ref()?; state.check_sum @@ -2547,6 +2748,10 @@ impl FrameDecoder { /// frame format declares no trailing digest avoids that wasted work). #[cfg(feature = "hash")] pub fn get_calculated_checksum(&self) -> Option { + #[cfg(feature = "legacy")] + if let Some(stream) = self.legacy_frame() { + return stream.calculated_checksum(); + } let state = self.state.as_ref()?; // `ContentChecksum::None` skips the XXH64 pass entirely, so there is // no calculated digest to report. @@ -2586,6 +2791,12 @@ impl FrameDecoder { /// assert_eq!(decoder.collect().unwrap(), b"declared and delivered"); /// ``` pub fn verify_content_size(&self) -> Result<(), FrameDecoderError> { + // A v0.5-v0.7 frame's declared size is not held against its output: + // `LegacyFrame::finish` says why. + #[cfg(feature = "legacy")] + if self.legacy_frame().is_some() { + return Ok(()); + } let Some(state) = self.state.as_ref() else { return Ok(()); }; @@ -2615,6 +2826,11 @@ impl FrameDecoder { if self.content_checksum != ContentChecksum::Verify { return Ok(()); } + // A v0.7 checksum is checked as its end block is read. + #[cfg(feature = "legacy")] + if self.legacy_frame().is_some() { + return Ok(()); + } let Some(state) = self.state.as_ref() else { return Ok(()); }; @@ -2636,6 +2852,10 @@ impl FrameDecoder { /// Bytes the current frame has decoded to so far. pub(crate) fn decoded_size(&self) -> u64 { + #[cfg(feature = "legacy")] + if let Some(stream) = self.legacy_frame() { + return stream.produced(); + } self.state .as_ref() .map_or(0, |state| state.decoder_scratch.total_output()) @@ -2643,6 +2863,10 @@ impl FrameDecoder { /// Counter for how many bytes have been consumed while decoding the frame pub fn bytes_read_from_source(&self) -> u64 { + #[cfg(feature = "legacy")] + if let Some(stream) = self.legacy_frame() { + return stream.consumed(); + } let state = match &self.state { None => return 0, Some(s) => s, @@ -2679,6 +2903,10 @@ impl FrameDecoder { /// assert!(FrameDecoder::new().dictionary().is_none()); /// ``` pub fn dictionary(&self) -> Option<&DictionaryHandle> { + #[cfg(feature = "legacy")] + if let Some(stream) = self.legacy_frame() { + return stream.dictionary(); + } self.state.as_ref().and_then(|s| s.active_dict.as_ref()) } @@ -2686,6 +2914,10 @@ impl FrameDecoder { /// If this returns true you can call the drain* functions to get all content /// (the read() function will drain automatically if this returns true) pub fn is_finished(&self) -> bool { + #[cfg(feature = "legacy")] + if let Some(stream) = self.legacy_frame() { + return stream.is_finished(); + } let state = match &self.state { None => return true, Some(s) => s, @@ -2699,6 +2931,10 @@ impl FrameDecoder { /// Counter for how many blocks have already been decoded pub fn blocks_decoded(&self) -> usize { + #[cfg(feature = "legacy")] + if let Some(stream) = self.legacy_frame() { + return stream.blocks_decoded(); + } let state = match &self.state { None => return 0, Some(s) => s, @@ -2717,6 +2953,26 @@ impl FrameDecoder { ) -> Result { use FrameDecoderError as err; self.check_window_still_admitted()?; + #[cfg(feature = "legacy")] + if let Some(stream) = self.legacy_frame_mut() { + let produced_before = stream.produced(); + let mut blocks = 0usize; + while !stream.is_finished() { + stream.read_step(&mut source)?; + blocks += 1; + let enough = match strat { + BlockDecodingStrategy::All => false, + BlockDecodingStrategy::UptoBlocks(n) => blocks >= n, + BlockDecodingStrategy::UptoBytes(n) => { + stream.produced() - produced_before >= n as u64 + } + }; + if enough { + break; + } + } + return Ok(stream.is_finished()); + } // Apply the content-checksum mode to the streaming drain hash before // any block decodes into the ring. Hash only when a digest is both // wanted (mode != None) AND present in the frame (content_checksum_flag @@ -3252,6 +3508,12 @@ impl FrameDecoder { /// decoding the window stays inside as history, already handed out; once /// it is finished ([`Self::is_finished`]) the buffer is emptied. pub fn collect(&mut self) -> Option> { + #[cfg(feature = "legacy")] + if let Some(stream) = self.legacy_frame_mut() { + let out = stream.undelivered_bytes().to_vec(); + stream.mark_delivered(out.len()); + return Some(out); + } let finished = self.is_finished(); let state = self.state.as_mut()?; if finished { @@ -3263,6 +3525,14 @@ impl FrameDecoder { /// [`Self::collect`] into `w`. pub fn collect_to_writer(&mut self, w: impl Write) -> Result { + #[cfg(feature = "legacy")] + if let Some(stream) = self.legacy_frame_mut() { + let mut w = w; + let len = stream.undelivered(); + w.write_all(stream.undelivered_bytes())?; + stream.mark_delivered(len); + return Ok(len); + } let finished = self.is_finished(); let state = match &mut self.state { None => return Ok(0), @@ -3278,6 +3548,10 @@ impl FrameDecoder { /// How many decoded bytes are waiting to be collected: every byte decoded /// and not collected yet, whether or not the frame is finished. pub fn can_collect(&self) -> usize { + #[cfg(feature = "legacy")] + if let Some(stream) = self.legacy_frame() { + return stream.undelivered(); + } self.state .as_ref() .map_or(0, |state| state.decoder_scratch.buffer_undelivered()) @@ -3336,6 +3610,10 @@ impl FrameDecoder { ) -> Result<(usize, usize), FrameDecoderError> { use FrameDecoderError as err; self.check_window_still_admitted()?; + #[cfg(feature = "legacy")] + if let Some(stream) = self.legacy_frame_mut() { + return Ok(stream.decode(source, target)?); + } let bytes_read_at_start = match &self.state { Some(s) => s.bytes_read_counter, None => 0, @@ -3348,6 +3626,14 @@ impl FrameDecoder { if self.state.is_none() { self.init(&mut mt_source)?; + // The header began a v0.5-v0.7 frame: its stream decodes the + // rest, the header counted in what was read. + #[cfg(feature = "legacy")] + if let Some(stream) = self.legacy_frame_mut() { + let header = source.len() - mt_source.len(); + let (read, written) = stream.decode(mt_source, target)?; + return Ok((header + read, written)); + } } //pseudo block to scope "state" so we can borrow self again after the block @@ -3553,11 +3839,17 @@ impl FrameDecoder { ) -> Result { #[cfg(not(feature = "lsm"))] { - self.decode_all_impl(input, output, |this, src| this.reset_from_slice(src)) + self.decode_all_impl(input, output, |this, src| this.reset_from_slice(src), &[]) } #[cfg(feature = "lsm")] { - self.decode_all_impl(input, output, |this, src| this.reset_from_slice(src), None) + self.decode_all_impl( + input, + output, + |this, src| this.reset_from_slice(src), + None, + &[], + ) } } @@ -3610,6 +3902,7 @@ impl FrameDecoder { output, |this, src| this.reset_from_slice(src), Some(&mut visitor), + &[], ) } @@ -3638,9 +3931,12 @@ impl FrameDecoder { ) -> Result { #[cfg(not(feature = "lsm"))] { - self.decode_all_impl(input, output, |this, src| { - this.reset_from_slice_with_dict_handle(src, dict) - }) + self.decode_all_impl( + input, + output, + |this, src| this.reset_from_slice_with_dict_handle(src, dict), + core::slice::from_ref(dict), + ) } #[cfg(feature = "lsm")] { @@ -3649,6 +3945,7 @@ impl FrameDecoder { output, |this, src| this.reset_from_slice_with_dict_handle(src, dict), None, + core::slice::from_ref(dict), ) } } @@ -3688,9 +3985,12 @@ impl FrameDecoder { } #[cfg(not(feature = "lsm"))] { - self.decode_all_impl(input, output, |this, src| { - this.reset_from_slice_with_dict_handles(src, dicts) - }) + self.decode_all_impl( + input, + output, + |this, src| this.reset_from_slice_with_dict_handles(src, dicts), + dicts, + ) } #[cfg(feature = "lsm")] { @@ -3699,6 +3999,7 @@ impl FrameDecoder { output, |this, src| this.reset_from_slice_with_dict_handles(src, dicts), None, + dicts, ) } } @@ -3710,6 +4011,11 @@ impl FrameDecoder { /// [`Self::decode_current_frame_to_vec`] can decode the rest of the frame /// straight from the remaining source bytes. pub(crate) fn is_at_frame_start(&self) -> bool { + // A v0.5-v0.7 frame decodes through its stream, a block at a time. + #[cfg(feature = "legacy")] + if self.legacy_frame().is_some() { + return false; + } self.state .as_ref() .is_some_and(|s| s.block_counter == 0 && !s.frame_finished) @@ -3769,6 +4075,11 @@ impl FrameDecoder { ) -> Result { let start_len = output.len(); while !input.is_empty() { + #[cfg(feature = "legacy")] + if !self.magicless && crate::decoding::LegacyVersion::of(input).is_some() { + self.decode_legacy_frame_to_vec(input, output, keep_dictionary)?; + continue; + } let init_result = if keep_dictionary { self.reset_with_active_dict(&mut *input) } else { @@ -3903,6 +4214,135 @@ impl FrameDecoder { Ok(produced as usize) } + /// The dictionary registered under `dict_id`, owned or shared. + #[cfg(feature = "legacy")] + fn registered_dict(&self, dict_id: u32) -> Option<&DictionaryHandle> { + let owned = self.owned_dicts.get(&dict_id); + #[cfg(target_has_atomic = "ptr")] + let owned = owned.or_else(|| self.shared_dicts.get(&dict_id)); + owned + } + + /// Decode the v0.5-v0.7 frame starting `*input` into the start of + /// `output`, advancing `*input` past it; returns the bytes written. + /// + /// A v0.7 frame naming a dictionary takes the one of `dicts` with that ID + /// (else the last, which then fails the ID check), or with no `dicts` the + /// one registered under it. A frame naming none takes the last of `dicts`. + #[cfg(feature = "legacy")] + fn decode_legacy_frame( + &mut self, + input: &mut &[u8], + output: &mut [u8], + dicts: &[DictionaryHandle], + ) -> Result { + use crate::decoding::errors::LegacyFrameError; + use crate::decoding::legacy::SliceOutput; + let mut out = SliceOutput { + buf: output, + pos: 0, + }; + let mut stream = self.take_legacy_stream(); + let decoded = self.decode_whole_legacy_frame(&mut stream, input, dicts, &mut out); + self.legacy = Some(stream); + let consumed = decoded.map_err(|err| match err { + FrameDecoderError::LegacyFrame(LegacyFrameError::OutputTooSmall) => { + FrameDecoderError::TargetTooSmall + } + err => err, + })?; + *input = &input[consumed..]; + Ok(out.pos) + } + + /// The decoder's v0.5-v0.7 stream, taken out so a whole-buffer decode can + /// run in its workspace while dictionaries are borrowed from the decoder; + /// the caller puts it back. + #[cfg(feature = "legacy")] + fn take_legacy_stream(&mut self) -> alloc::boxed::Box { + self.legacy + .take() + .unwrap_or_else(|| alloc::boxed::Box::new(decoding::legacy::LegacyStream::new())) + } + + /// Decode the v0.5-v0.7 frame starting `input` into `out`, with the + /// dictionary [`Self::legacy_frame_dict`] picks from `dicts`, in + /// `stream`'s workspace so its tables are reused from frame to frame. + /// Returns the bytes the frame took. + #[cfg(feature = "legacy")] + fn decode_whole_legacy_frame( + &self, + stream: &mut decoding::legacy::LegacyStream, + input: &[u8], + dicts: &[DictionaryHandle], + out: &mut O, + ) -> Result { + let header = decoding::legacy::read_header(input)?; + #[cfg(feature = "lsm")] + self.validate_legacy_expectations(&header, input)?; + let dict = self.legacy_frame_dict(header.dict_id, dicts)?; + Ok(stream.decode_whole(input, legacy_dict(dict), self.legacy_frame_options(), out)?) + } + + /// Decode the v0.5-v0.7 frame starting `*input`, appending to `output` + /// and advancing `*input` past it. With `keep_dictionary` it takes the + /// dictionary the decoder holds, else the registry's. + #[cfg(feature = "legacy")] + fn decode_legacy_frame_to_vec( + &mut self, + input: &mut &[u8], + output: &mut Vec, + keep_dictionary: bool, + ) -> Result<(), FrameDecoderError> { + use crate::decoding::legacy::VecOutput; + let mut stream = self.take_legacy_stream(); + let held = if keep_dictionary { + self.state + .as_ref() + .and_then(|state| state.active_dict.as_ref()) + .or(self.armed_dict.as_ref()) + } else { + None + }; + let dicts = held.map_or(&[][..], core::slice::from_ref); + let start = output.len(); + let mut out = VecOutput { buf: output, start }; + let decoded = self.decode_whole_legacy_frame(&mut stream, input, dicts, &mut out); + self.legacy = Some(stream); + match decoded { + Ok(consumed) => { + *input = &input[consumed..]; + Ok(()) + } + Err(err) => { + // No byte of a frame that failed is handed out. + output.truncate(start); + Err(err) + } + } + } + + /// The dictionary a v0.5-v0.7 frame naming `dict_id` (0 for none) + /// decodes with: the one of `dicts` with that ID, else the last, which + /// then fails the frame's ID check; with no `dicts`, the one registered + /// under the ID. + #[cfg(feature = "legacy")] + fn legacy_frame_dict<'s>( + &'s self, + dict_id: u32, + dicts: &'s [DictionaryHandle], + ) -> Result, FrameDecoderError> { + let named = (dict_id != 0).then_some(dict_id); + Ok(match (named, dicts.is_empty()) { + (_, false) => Some(dictionary_for_frame(dicts, named)), + (Some(dict_id), true) => Some( + self.registered_dict(dict_id) + .ok_or(FrameDecoderError::DictNotProvided { dict_id })?, + ), + (None, true) => None, + }) + } + /// Default-feature decode_all_impl: no visitor parameter so the /// no-lsm build's call surface and codegen are byte-identical to /// the pre-#172 implementation. Compiles only when `lsm` is OFF. @@ -3912,9 +4352,18 @@ impl FrameDecoder { mut input: &[u8], mut output: &mut [u8], mut init_frame: impl FnMut(&mut Self, &mut &[u8]) -> Result<(), FrameDecoderError>, + // The dictionaries a v0.5-v0.7 frame chooses from. + #[cfg_attr(not(feature = "legacy"), allow(unused_variables))] dicts: &[DictionaryHandle], ) -> Result { let mut total_bytes_written = 0; while !input.is_empty() { + #[cfg(feature = "legacy")] + if !self.magicless && crate::decoding::LegacyVersion::of(input).is_some() { + let written = self.decode_legacy_frame(&mut input, output, dicts)?; + output = &mut output[written..]; + total_bytes_written += written; + continue; + } match init_frame(self, &mut input) { Ok(_) => {} Err(FrameDecoderError::ReadFrameHeaderError( @@ -4059,9 +4508,18 @@ impl FrameDecoder { mut output: &mut [u8], mut init_frame: impl FnMut(&mut Self, &mut &[u8]) -> Result<(), FrameDecoderError>, mut skippable_visitor: Option<&mut dyn FnMut(u8, &[u8])>, + // The dictionaries a v0.5-v0.7 frame chooses from. + #[cfg_attr(not(feature = "legacy"), allow(unused_variables))] dicts: &[DictionaryHandle], ) -> Result { let mut total_bytes_written = 0; while !input.is_empty() { + #[cfg(feature = "legacy")] + if !self.magicless && crate::decoding::LegacyVersion::of(input).is_some() { + let written = self.decode_legacy_frame(&mut input, output, dicts)?; + output = &mut output[written..]; + total_bytes_written += written; + continue; + } match init_frame(self, &mut input) { Ok(_) => {} Err(FrameDecoderError::ReadFrameHeaderError( @@ -4672,6 +5130,10 @@ impl FrameDecoder { /// this will retain window_size bytes, else it will drain it completely impl Read for FrameDecoder { fn read(&mut self, target: &mut [u8]) -> Result { + #[cfg(feature = "legacy")] + if let Some(stream) = self.legacy_frame_mut() { + return Ok(stream.hand_out(target)); + } let state = match &mut self.state { None => return Ok(0), Some(s) => s, diff --git a/zstd/src/decoding/frame_inspection_tests.rs b/zstd/src/decoding/frame_inspection_tests.rs index e99ef0bd..594f1e56 100644 --- a/zstd/src/decoding/frame_inspection_tests.rs +++ b/zstd/src/decoding/frame_inspection_tests.rs @@ -12,6 +12,41 @@ fn frame(content: &[u8]) -> Vec { compress_slice_to_vec(content, CompressionLevel::Default) } +/// Built without `legacy`, a v0.5-v0.7 frame is no frame: every query and +/// the decoder refuse its magic number as an unknown prefix, the error a +/// libzstd built at legacy level 0 returns. +#[cfg(not(feature = "legacy"))] +#[test] +fn without_the_legacy_feature_older_frames_are_unknown() { + use crate::decoding::FrameDecoder; + use crate::decoding::errors::{ErrorCode, FrameDecoderError}; + let frame = include_bytes!("../../legacy_frames/abc.txt.v07.l1.zst"); + assert!(!super::is_frame(frame)); + assert!(matches!( + read_frame_content_size(frame), + Err(ReadFrameHeaderError::BadMagicNumber(0xFD2F_B527)) + )); + assert!(matches!( + find_frame_compressed_size(frame), + Err(FrameSizeError::Header( + ReadFrameHeaderError::BadMagicNumber(0xFD2F_B527) + )) + )); + let err = FrameDecoder::new() + .decode_all(frame, &mut [0u8; 64]) + .unwrap_err(); + assert_eq!(err.code(), ErrorCode::PrefixUnknown); + assert!( + matches!( + err, + FrameDecoderError::ReadFrameHeaderError(ReadFrameHeaderError::BadMagicNumber( + 0xFD2F_B527 + )) + ), + "{err}" + ); +} + /// A single-segment frame declaring `size` in an 8-byte Frame_Content_Size /// field, holding one empty last raw block (RFC 8878 3.1.1). fn frame_declaring(size: u64) -> Vec { diff --git a/zstd/src/decoding/legacy/entropy.rs b/zstd/src/decoding/legacy/entropy.rs new file mode 100644 index 00000000..87722471 --- /dev/null +++ b/zstd/src/decoding/legacy/entropy.rs @@ -0,0 +1,705 @@ +//! Entropy coding shared by the v0.5, v0.6 and v0.7 formats: the backward +//! bitstream, FSE tables and their compact description, and Huffman literals. +//! +//! The three formats agree on all of it (upstream `lib/legacy/zstd_v0{5,6,7}.c` +//! carry the same code), and it differs from RFC 8878 in details that matter +//! to a decoder: a Huffman description may be a run of weight 1, the largest +//! FSE table has 4096 states, and a table may be a fixed-width read. + +use super::corrupt; +use crate::bit_io::BitReaderReversed; +use crate::decoding::errors::LegacyFrameError as Error; +use alloc::vec::Vec; + +/// A backward bitstream, read from its last byte toward its first. +pub(super) type Bits<'s> = BitReaderReversed<'s>; + +/// Largest FSE table the formats build: 4096 states (`FSEv07_MAX_TABLELOG`). +const FSE_MAX_TABLE_LOG: u32 = 12; +/// Smallest table log a description encodes (`FSEv07_MIN_TABLELOG`). +const FSE_MIN_TABLE_LOG: i32 = 5; +/// Largest table log a description may name (`FSEv07_TABLELOG_ABSOLUTE_MAX`). +const FSE_DESCRIPTION_MAX_LOG: i32 = 15; +/// Largest Huffman code length a table may use (`HUFv07_TABLELOG_MAX`). +const HUF_MAX_TABLE_LOG: u32 = 12; +/// Weights at or past this value are refused (`HUFv07_TABLELOG_ABSOLUTEMAX`). +const HUF_WEIGHT_LIMIT: u8 = 16; + +/// Open a backward bitstream over `src`, past the padding and the end mark +/// in its last byte. +pub(super) fn open_bits(src: &[u8]) -> Result, Error> { + let &last = src.last().ok_or(corrupt("empty bitstream"))?; + if last == 0 { + return Err(corrupt("bitstream without an end mark")); + } + let mut bits = Bits::new(src); + bits.get_bits(last.leading_zeros() as u8 + 1); + Ok(bits) +} + +/// Whether `bits` has been read past its first byte. +#[inline] +pub(super) fn overflowed(bits: &Bits<'_>) -> bool { + bits.bits_remaining() < 0 +} + +/// A normalised symbol distribution, as an FSE table description encodes it: +/// `-1` marks a symbol below one state. +pub(super) struct Distribution { + counts: [i16; 256], + /// Highest symbol the description lists. + max_symbol: usize, + table_log: u32, +} + +/// Read an FSE table description (`FSEv07_readNCount`), listing at most +/// `max_symbol + 1` symbols. Returns the distribution and the bytes read. +pub(super) fn read_distribution( + src: &[u8], + max_symbol: usize, +) -> Result<(Distribution, usize), Error> { + let end = src.len(); + if end < 4 { + return Err(Error::Truncated); + } + let read32 = |at: usize| u32::from_le_bytes([src[at], src[at + 1], src[at + 2], src[at + 3]]); + let mut counts = [0i16; 256]; + let mut ip = 0usize; + let mut stream = read32(0); + let mut nb_bits = (stream & 0xF) as i32 + FSE_MIN_TABLE_LOG; + if nb_bits > FSE_DESCRIPTION_MAX_LOG { + return Err(corrupt("FSE table log too large")); + } + let table_log = nb_bits as u32; + stream >>= 4; + let mut bit_count: i32 = 4; + let mut remaining: i32 = (1 << nb_bits) + 1; + let mut threshold: i32 = 1 << nb_bits; + nb_bits += 1; + let mut symbol = 0usize; + let mut previous_zero = false; + + while remaining > 1 && symbol <= max_symbol { + if previous_zero { + let mut run_end = symbol; + while stream & 0xFFFF == 0xFFFF { + run_end += 24; + if ip + 5 < end { + ip += 2; + stream = read32(ip).wrapping_shr(bit_count as u32); + } else { + stream >>= 16; + bit_count += 16; + } + } + while stream & 3 == 3 { + run_end += 3; + stream >>= 2; + bit_count += 2; + } + run_end += (stream & 3) as usize; + bit_count += 2; + if run_end > max_symbol { + return Err(corrupt("FSE description lists too many symbols")); + } + // `counts` was zeroed; the run only moves past its symbols. + symbol = run_end; + if ip + 7 <= end || ip + (bit_count >> 3) as usize + 4 <= end { + ip += (bit_count >> 3) as usize; + bit_count &= 7; + stream = read32(ip).wrapping_shr(bit_count as u32); + } else { + stream >>= 2; + } + } + let max = (2 * threshold - 1) - remaining; + let mut count; + if ((stream & (threshold as u32 - 1)) as i32) < max { + count = (stream & (threshold as u32 - 1)) as i32; + bit_count += nb_bits - 1; + } else { + count = (stream & (2 * threshold as u32 - 1)) as i32; + if count >= threshold { + count -= max; + } + bit_count += nb_bits; + } + count -= 1; + remaining -= count.abs(); + // `count` is at most `remaining` before the subtraction, so at least + // one state is always left. + debug_assert!(remaining >= 1); + counts[symbol] = count as i16; + symbol += 1; + previous_zero = count == 0; + while remaining < threshold { + nb_bits -= 1; + threshold >>= 1; + } + if ip + 7 <= end || ip + (bit_count >> 3) as usize + 4 <= end { + ip += (bit_count >> 3) as usize; + bit_count &= 7; + } else { + bit_count -= 8 * (end - 4 - ip) as i32; + ip = end - 4; + } + stream = read32(ip).wrapping_shr((bit_count & 31) as u32); + } + if remaining != 1 { + return Err(corrupt("FSE description does not fill its table")); + } + let consumed = ip + ((bit_count + 7) >> 3) as usize; + if consumed > end { + return Err(Error::Truncated); + } + Ok(( + Distribution { + counts, + max_symbol: symbol - 1, + table_log, + }, + consumed, + )) +} + +impl Distribution { + /// A predefined distribution, as the formats' default tables list them. + pub(super) fn predefined(counts: &[i16], table_log: u32) -> Distribution { + let mut all = [0i16; 256]; + all[..counts.len()].copy_from_slice(counts); + Distribution { + counts: all, + max_symbol: counts.len() - 1, + table_log, + } + } + + /// The distribution an RFC 8878 table description parsed to, listing at + /// most `max_symbol + 1` symbols. + pub(super) fn from_probabilities( + probabilities: &[i32], + table_log: u32, + max_symbol: usize, + ) -> Result { + if probabilities.is_empty() || probabilities.len() > max_symbol + 1 { + return Err(corrupt("FSE description lists too many symbols")); + } + let mut counts = [0i16; 256]; + for (count, &probability) in counts.iter_mut().zip(probabilities) { + *count = i16::try_from(probability).map_err(|_| corrupt("FSE count out of range"))?; + } + Ok(Distribution { + counts, + max_symbol: probabilities.len() - 1, + table_log, + }) + } + + pub(super) fn table_log(&self) -> u32 { + self.table_log + } +} + +#[derive(Clone, Copy, Default)] +struct FseCell { + new_state: u16, + symbol: u8, + nb_bits: u8, +} + +/// An FSE decoding table. +#[derive(Clone)] +pub(super) struct FseTable { + log: u32, + cells: Vec, + /// Whether every state reads at least one bit: no symbol holds half the + /// table or more (`fastMode`). + fast: bool, +} + +/// How an FSE-coded byte string marks its end. +#[derive(Clone, Copy, PartialEq, Eq)] +pub(super) enum StreamEnd { + /// The decoder reads until a read passes the stream's first bit, then + /// takes one more symbol from the other state (v0.6, v0.7). + PastStart, + /// The stream ends exactly at its first bit with both states at 0 + /// (v0.5, `FSEv05_decompress_usingDTable_generic`). + Exact, +} + +impl FseTable { + /// A table holding nothing yet, for [`Self::build`] and the setters to + /// fill. + pub(super) const fn empty() -> FseTable { + FseTable { + log: 0, + cells: Vec::new(), + fast: false, + } + } + + /// The table a distribution spreads (`FSEv07_buildDTable`). + #[cfg(test)] + pub(super) fn new(dist: &Distribution) -> Result { + let mut table = FseTable::empty(); + table.build(dist)?; + Ok(table) + } + + /// Spread `dist` into this table, in the cells it already has, as a block + /// sending a new description rebuilds its field's table. + pub(super) fn build(&mut self, dist: &Distribution) -> Result<(), Error> { + let log = dist.table_log; + if log > FSE_MAX_TABLE_LOG { + return Err(corrupt("FSE table log too large")); + } + let size = 1usize << log; + let counts = &dist.counts[..=dist.max_symbol]; + let cells = &mut self.cells; + cells.clear(); + cells.resize(size, FseCell::default()); + let mut next = [0u16; 256]; + + // Symbols below one state take the top cells, one each. + let mut high = size; + for (symbol, &count) in counts.iter().enumerate() { + if count == -1 { + high = high + .checked_sub(1) + .ok_or(corrupt("FSE distribution overfills its table"))?; + cells[high].symbol = symbol as u8; + next[symbol] = 1; + } else { + next[symbol] = count.max(0) as u16; + } + } + + let mask = size - 1; + let step = (size >> 1) + (size >> 3) + 3; + let mut position = 0usize; + for (symbol, &count) in counts.iter().enumerate() { + for _ in 0..count.max(0) { + if high == 0 { + return Err(corrupt("FSE distribution overfills its table")); + } + cells[position].symbol = symbol as u8; + position = (position + step) & mask; + while position >= high { + position = (position + step) & mask; + } + } + } + // A distribution that sums to the table size reaches every cell once. + if position != 0 { + return Err(corrupt("FSE distribution does not fill its table")); + } + + for cell in cells.iter_mut() { + let state = next[usize::from(cell.symbol)]; + next[usize::from(cell.symbol)] += 1; + let nb_bits = log - (31 - u32::from(state).leading_zeros()); + cell.nb_bits = nb_bits as u8; + cell.new_state = ((u32::from(state) << nb_bits) - size as u32) as u16; + } + let large = 1i16 << (log - 1); + self.fast = counts.iter().all(|&count| count < large); + self.log = log; + Ok(()) + } + + /// Read every symbol as `nb_bits` raw bits (`FSEv07_buildDTable_raw`). + pub(super) fn set_raw(&mut self, nb_bits: u32) { + self.cells.clear(); + self.cells + .extend((0..1usize << nb_bits).map(|symbol| FseCell { + new_state: 0, + symbol: symbol as u8, + nb_bits: nb_bits as u8, + })); + self.log = nb_bits; + self.fast = true; + } + + /// Produce `symbol` without reading (`FSEv07_buildDTable_rle`). + pub(super) fn set_rle(&mut self, symbol: u8) { + self.cells.clear(); + self.cells.push(FseCell { + new_state: 0, + symbol, + nb_bits: 0, + }); + self.log = 0; + self.fast = false; + } + + /// Heap bytes the table holds. + pub(super) fn heap_bytes(&self) -> usize { + self.cells.capacity() * core::mem::size_of::() + } +} + +/// A table a later block may use again, with whether one is set: unsetting +/// it keeps its storage for the next table built into it. +pub(super) struct Slot { + pub(super) table: T, + pub(super) set: bool, +} + +impl Slot { + pub(super) const fn empty() -> Self { + Slot { + table: FseTable::empty(), + set: false, + } + } +} + +impl Slot { + pub(super) const fn empty() -> Self { + Slot { + table: HufTable::empty(), + set: false, + } + } +} + +impl Slot { + /// The table, once one is set. + pub(super) fn get(&self) -> Option<&T> { + self.set.then_some(&self.table) + } +} + +/// A position in an [`FseTable`]. +pub(super) struct FseState { + state: usize, +} + +impl FseState { + pub(super) fn new(table: &FseTable, bits: &mut Bits<'_>) -> FseState { + FseState { + state: bits.get_bits(table.log as u8) as usize, + } + } + + /// The symbol at the current state. + #[inline] + pub(super) fn symbol(&self, table: &FseTable) -> u8 { + table.cells[self.state].symbol + } + + /// Move to the next state, reading the bits the current one calls for. + #[inline] + pub(super) fn update(&mut self, table: &FseTable, bits: &mut Bits<'_>) { + let cell = table.cells[self.state]; + // A cell's `new_state` plus any value of its `nb_bits` stays inside + // the table by construction. + self.state = usize::from(cell.new_state) + bits.get_bits(cell.nb_bits) as usize; + } +} + +/// Decode an FSE-compressed byte string of at most `limit` symbols +/// (`FSEv07_decompress`): a description, then two interleaved states. +fn fse_decompress( + src: &[u8], + out: &mut [u8; 256], + limit: usize, + end: StreamEnd, + table: &mut FseTable, +) -> Result { + if src.len() < 2 { + return Err(Error::Truncated); + } + let (dist, description_len) = read_distribution(src, 255)?; + if description_len >= src.len() { + return Err(Error::Truncated); + } + table.build(&dist)?; + let table = &*table; + let mut bits = open_bits(&src[description_len..])?; + let mut first = FseState::new(table, &mut bits); + let mut second = FseState::new(table, &mut bits); + if end == StreamEnd::Exact { + return decode_to_exact_end(table, &mut bits, [first, second], out, limit); + } + let mut n = 0usize; + loop { + if n + 2 > limit { + return Err(corrupt("FSE stream decodes past its output")); + } + out[n] = first.symbol(table); + n += 1; + first.update(table, &mut bits); + if overflowed(&bits) { + out[n] = second.symbol(table); + return Ok(n + 1); + } + if n + 2 > limit { + return Err(corrupt("FSE stream decodes past its output")); + } + out[n] = second.symbol(table); + n += 1; + second.update(table, &mut bits); + if overflowed(&bits) { + out[n] = first.symbol(table); + return Ok(n + 1); + } + } +} + +/// The v0.5 ending: alternate the two states until the stream is read to its +/// first bit and the state about to decode is at 0 (in a table where every +/// state reads a bit, as soon as the stream is read); the string is whole +/// only if the stream ended exactly with both states at 0. +fn decode_to_exact_end( + table: &FseTable, + bits: &mut Bits<'_>, + mut states: [FseState; 2], + out: &mut [u8; 256], + limit: usize, +) -> Result { + let mut n = 0usize; + let mut turn = 0; + loop { + let state = &mut states[turn]; + let exhausted = bits.bits_remaining() == 0; + if overflowed(bits) || n == limit || (exhausted && (table.fast || state.state == 0)) { + break; + } + out[n] = state.symbol(table); + n += 1; + state.update(table, bits); + turn ^= 1; + } + if bits.bits_remaining() == 0 && states[0].state == 0 && states[1].state == 0 { + Ok(n) + } else { + Err(corrupt("FSE stream does not end with its symbols")) + } +} + +/// A Huffman decoding table: one entry per value of its `log` leading bits. +#[derive(Clone)] +pub(super) struct HufTable { + log: u32, + /// `(symbol, code length)` per entry. + cells: Vec<(u8, u8)>, + /// The table an FSE-coded weight description is read with, kept for the + /// next description. + weight_table: FseTable, +} + +/// Run lengths a description starting with byte `242 + i` stands for: that +/// many symbols, all of weight 1 (`HUFv07_readStats`). +const WEIGHT_RUNS: [usize; 14] = [1, 2, 3, 4, 7, 8, 15, 16, 31, 32, 63, 64, 127, 128]; + +impl HufTable { + /// A table holding nothing yet, for [`Self::read_into`] to fill. + pub(super) const fn empty() -> HufTable { + HufTable { + log: 0, + cells: Vec::new(), + weight_table: FseTable::empty(), + } + } + + /// [`Self::read_into`] a new table. Returns the table and the bytes read. + #[cfg(test)] + pub(super) fn read(src: &[u8], end: StreamEnd) -> Result<(HufTable, usize), Error> { + let mut table = HufTable::empty(); + let consumed = table.read_into(src, end)?; + Ok((table, consumed)) + } + + /// Heap bytes the table holds, its weight table's included. + pub(super) fn heap_bytes(&self) -> usize { + self.cells.capacity() * core::mem::size_of::<(u8, u8)>() + self.weight_table.heap_bytes() + } + + /// Read a Huffman table description (`HUFv07_readStats` and + /// `HUFv07_readDTableX2`), its weights FSE-coded with the ending `end`, + /// into this table's own storage, as each block with a new description + /// replaces the table. Returns the bytes read. + pub(super) fn read_into(&mut self, src: &[u8], end: StreamEnd) -> Result { + let &header = src.first().ok_or(Error::Truncated)?; + let mut weights = [0u8; 256]; + let (explicit, consumed) = match header { + 242..=255 => { + weights.fill(1); + (WEIGHT_RUNS[usize::from(header - 242)], 1) + } + 128..=241 => { + let explicit = usize::from(header) - 127; + let packed = explicit.div_ceil(2); + let bytes = src.get(1..1 + packed).ok_or(Error::Truncated)?; + for (n, &byte) in bytes.iter().enumerate() { + weights[2 * n] = byte >> 4; + weights[2 * n + 1] = byte & 0xF; + } + (explicit, packed + 1) + } + _ => { + let len = usize::from(header); + let body = src.get(1..1 + len).ok_or(Error::Truncated)?; + let explicit = + fse_decompress(body, &mut weights, 255, end, &mut self.weight_table)?; + (explicit, len + 1) + } + }; + self.fill(&mut weights, explicit)?; + Ok(consumed) + } + + /// Build the table whose weights are an RFC 8878 description's code + /// lengths, the last one included, under a longest code of `max_bits`. + pub(super) fn fill_from_code_lengths( + &mut self, + lengths: &[u8], + max_bits: u8, + ) -> Result<(), Error> { + let explicit = lengths + .len() + .checked_sub(1) + .filter(|&explicit| explicit < 256) + .ok_or(corrupt("Huffman description without symbols"))?; + let mut weights = [0u8; 256]; + for (weight, &len) in weights.iter_mut().zip(&lengths[..explicit]) { + if len > max_bits { + return Err(corrupt("Huffman code longer than its table")); + } + *weight = if len == 0 { 0 } else { max_bits + 1 - len }; + } + // The last weight is inferred again, from the others. + self.fill(&mut weights, explicit) + } + + /// Build the table from the first `explicit` weights, inferring the + /// weight of the symbol after them. + fn fill(&mut self, weights: &mut [u8; 256], explicit: usize) -> Result<(), Error> { + let mut ranks = [0u32; HUF_WEIGHT_LIMIT as usize + 1]; + let mut total = 0u32; + for &weight in &weights[..explicit] { + if weight >= HUF_WEIGHT_LIMIT { + return Err(corrupt("Huffman weight too large")); + } + ranks[usize::from(weight)] += 1; + total += (1 << weight) >> 1; + } + if total == 0 { + return Err(corrupt("Huffman description without symbols")); + } + // The last symbol's weight is implied: it brings the total to a + // power of two. + let log = 32 - total.leading_zeros(); + if log > u32::from(HUF_WEIGHT_LIMIT) { + return Err(corrupt("Huffman table too large")); + } + let rest = (1u32 << log) - total; + if !rest.is_power_of_two() { + return Err(corrupt("Huffman weights do not sum to a power of two")); + } + let last = rest.trailing_zeros() + 1; + weights[explicit] = last as u8; + ranks[last as usize] += 1; + // A prefix code has an even number, at least two, of longest codes. + if ranks[1] < 2 || ranks[1] % 2 != 0 { + return Err(corrupt("Huffman weights do not form a prefix code")); + } + if log > HUF_MAX_TABLE_LOG { + return Err(corrupt("Huffman table too large")); + } + + let mut start = [0u32; HUF_WEIGHT_LIMIT as usize + 1]; + let mut next = 0u32; + for weight in 1..=log as usize { + start[weight] = next; + next += ranks[weight] << (weight - 1); + } + let cells = &mut self.cells; + cells.clear(); + cells.resize(1 << log, (0, 0)); + for (symbol, &weight) in weights[..=explicit].iter().enumerate() { + if weight == 0 { + continue; + } + let len = 1usize << (weight - 1); + let first = start[usize::from(weight)] as usize; + cells[first..first + len].fill((symbol as u8, (log + 1 - u32::from(weight)) as u8)); + start[usize::from(weight)] += len as u32; + } + self.log = log; + Ok(()) + } + + #[inline] + fn decode(&self, bits: &mut Bits<'_>) -> u8 { + let log = self.log as u8; + bits.ensure_bits(log); + let (symbol, len) = self.cells[bits.peek_bits(log) as usize]; + bits.consume(len); + symbol + } + + /// Fill `out` from one stream, which must end exactly with it + /// (`HUFv07_decompress1X2_usingDTable`). + pub(super) fn decode_one_stream(&self, src: &[u8], out: &mut [u8]) -> Result<(), Error> { + let mut bits = open_bits(src)?; + for byte in out.iter_mut() { + *byte = self.decode(&mut bits); + } + if bits.bits_remaining() != 0 { + return Err(corrupt("Huffman stream does not end with its literals")); + } + Ok(()) + } + + /// Fill `out` from four streams behind a six-byte jump table, each + /// filling a quarter (`HUFv07_decompress4X2_usingDTable`). + pub(super) fn decode_four_streams(&self, src: &[u8], out: &mut [u8]) -> Result<(), Error> { + if src.len() < 10 { + return Err(corrupt("four-stream Huffman literals too short")); + } + let len1 = usize::from(u16::from_le_bytes([src[0], src[1]])); + let len2 = usize::from(u16::from_le_bytes([src[2], src[3]])); + let len3 = usize::from(u16::from_le_bytes([src[4], src[5]])); + let streams_end = 6 + len1 + len2 + len3; + if streams_end > src.len() { + return Err(corrupt("Huffman jump table points past its streams")); + } + let streams = [ + &src[6..6 + len1], + &src[6 + len1..6 + len1 + len2], + &src[6 + len1 + len2..streams_end], + &src[streams_end..], + ]; + let segment = out.len().div_ceil(4); + // Below six literals the first three quarters reach past the output; + // upstream decodes them into the literal buffer's slack and keeps what + // falls inside. A scratch of that size gives the same output. + let mut small = [0u8; 8]; + let work: &mut [u8] = if 3 * segment > out.len() { + &mut small[..3 * segment] + } else { + &mut *out + }; + let total = work.len(); + for (index, stream) in streams.iter().enumerate() { + let start = (index * segment).min(total); + let end = if index == 3 { + total + } else { + ((index + 1) * segment).min(total) + }; + self.decode_one_stream(stream, &mut work[start..end])?; + } + if 3 * segment > out.len() { + let len = out.len(); + out.copy_from_slice(&small[..len]); + } + Ok(()) + } +} + +#[cfg(test)] +mod tests; diff --git a/zstd/src/decoding/legacy/entropy/tests.rs b/zstd/src/decoding/legacy/entropy/tests.rs new file mode 100644 index 00000000..6fa62a07 --- /dev/null +++ b/zstd/src/decoding/legacy/entropy/tests.rs @@ -0,0 +1,221 @@ +use super::*; +use crate::decoding::legacy::v07::{LL_DEFAULT, LL_DEFAULT_LOG}; +use alloc::vec; + +/// A bitstream opens past the padding up to its end mark; an empty one, or +/// one whose last byte has no mark, is refused. +#[test] +fn bitstreams_open_at_their_end_mark() { + assert!(open_bits(&[]).is_err()); + assert!(open_bits(&[0x5A, 0x00]).is_err()); + // The mark is the top set bit of the last byte: seven bits remain. + let bits = open_bits(&[0x01, 0x01]).unwrap(); + assert_eq!(bits.bits_remaining(), 8); + let mut bits = open_bits(&[0b1010_0101]).unwrap(); + assert_eq!(bits.bits_remaining(), 7); + assert_eq!(bits.get_bits(7), 0b010_0101); + assert!(!overflowed(&bits)); + bits.get_bits(1); + assert!(overflowed(&bits)); +} + +/// The predefined literal length distribution spreads into a full table in +/// which every symbol it lists appears. +#[test] +fn predefined_distributions_fill_their_table() { + let dist = Distribution::predefined(&LL_DEFAULT, LL_DEFAULT_LOG); + let table = FseTable::new(&dist).unwrap(); + assert_eq!(table.cells.len(), 1 << LL_DEFAULT_LOG); + for symbol in 0..LL_DEFAULT.len() as u8 { + assert!( + table.cells.iter().any(|cell| cell.symbol == symbol), + "{symbol}" + ); + } + // Every state's successor stays inside the table. + for cell in &table.cells { + assert!(usize::from(cell.new_state) + (1 << cell.nb_bits) <= table.cells.len()); + } +} + +/// A table built over another's storage, a larger one or one set to raw +/// bits, is the table a fresh build gives: nothing of the earlier table stays. +#[test] +fn a_table_rebuilt_in_place_is_a_fresh_one() { + let dist = Distribution::predefined(&LL_DEFAULT, LL_DEFAULT_LOG); + let fresh = FseTable::new(&dist).unwrap(); + let mut reused = FseTable::empty(); + reused.set_raw(9); + reused.build(&dist).unwrap(); + assert_eq!(reused.log, fresh.log); + assert_eq!(reused.fast, fresh.fast); + assert_eq!(reused.cells.len(), fresh.cells.len()); + for (a, b) in reused.cells.iter().zip(&fresh.cells) { + assert_eq!( + (a.symbol, a.nb_bits, a.new_state), + (b.symbol, b.nb_bits, b.new_state) + ); + } + // A Huffman table read over a wider one is the narrower table alone. + let (fresh, _) = HufTable::read(&[127 + 2, 0x11], StreamEnd::PastStart).unwrap(); + let (mut reused, _) = HufTable::read(&[255], StreamEnd::PastStart).unwrap(); + reused + .read_into(&[127 + 2, 0x11], StreamEnd::PastStart) + .unwrap(); + assert_eq!((reused.log, &reused.cells), (fresh.log, &fresh.cells)); +} + +/// A distribution that does not sum to its table is refused, as one with +/// more below-one symbols than cells. +#[test] +fn a_distribution_that_does_not_fill_its_table_is_refused() { + let mut short = LL_DEFAULT; + short[0] -= 1; + assert!(FseTable::new(&Distribution::predefined(&short, LL_DEFAULT_LOG)).is_err()); + let all_low = [-1i16; 40]; + assert!(FseTable::new(&Distribution::predefined(&all_low, 5)).is_err()); +} + +/// A description too short to hold its first word, or naming a table past +/// the largest log, is refused. +#[test] +fn bad_descriptions_are_refused() { + assert_eq!( + read_distribution(&[0x00, 0x00], 35).err(), + Some(Error::Truncated) + ); + // Log 5 + 15 = 20. + assert!(read_distribution(&[0x0F, 0, 0, 0], 35).is_err()); +} + +/// A raw table reads every symbol as its width, from the stream's end +/// backward; a one-symbol table reads nothing. +#[test] +fn raw_and_rle_tables_read_as_described() { + let mut raw = FseTable::empty(); + raw.set_raw(5); + // Under the end mark: 11010 then 11, padded with zeros past the start. + let mut bits = open_bits(&[0b1110_1011]).unwrap(); + let mut state = FseState::new(&raw, &mut bits); + assert_eq!(state.symbol(&raw), 0b11010); + state.update(&raw, &mut bits); + assert_eq!(state.symbol(&raw), 0b11000); + assert!(overflowed(&bits)); + + // Set over the raw table's storage, as a block's next mode is. + let mut rle = raw; + rle.set_rle(42); + let mut bits = open_bits(&[0x80]).unwrap(); + let mut state = FseState::new(&rle, &mut bits); + for _ in 0..3 { + assert_eq!(state.symbol(&rle), 42); + state.update(&rle, &mut bits); + } + assert_eq!(bits.bits_remaining(), 7); +} + +/// A description byte of 242 and up is a run of weight-1 symbols: 242 is +/// one, which with the implied last symbol makes a one-bit code. +#[test] +fn a_weight_run_makes_a_flat_code() { + let (table, consumed) = HufTable::read(&[242], StreamEnd::PastStart).unwrap(); + assert_eq!(consumed, 1); + assert_eq!(table.log, 1); + // One stream of bits 1, 0, 1 then the end mark: symbols 1, 0, 1. + let mut out = [0u8; 3]; + table.decode_one_stream(&[0b0000_1101], &mut out).unwrap(); + assert_eq!(out, [1, 0, 1]); + // A stream with a bit left over is refused. + let mut out = [0u8; 2]; + assert!(table.decode_one_stream(&[0b0000_1101], &mut out).is_err()); +} + +/// Weights stored as nibbles (a header of `127 + n` for `n` weights): symbols +/// 0 and 1 of weight 1 and the implied symbol 2 of weight 2 make codes of 2, +/// 2 and 1 bits. +#[test] +fn nibble_weights_build_their_code() { + let end = StreamEnd::PastStart; + let (table, consumed) = HufTable::read(&[127 + 2, 0x11], end).unwrap(); + assert_eq!(consumed, 2); + assert_eq!(table.log, 2); + assert_eq!(table.cells, [(0, 2), (1, 2), (2, 1), (2, 1)]); + // A nibble section cut short is truncated. + assert_eq!( + HufTable::read(&[127 + 4, 0x11], end).err(), + Some(Error::Truncated) + ); + // Weights 3 and 1 sum to 5, which no last weight brings to a power of + // two. + assert!(HufTable::read(&[127 + 2, 0x31], end).is_err()); + // A weight of 16 is refused. + assert!(HufTable::read(&[127 + 2, 0xF1], end).is_err()); +} + +/// Four streams fill the quarters of the output in order, and below six +/// literals the quarters past the output decode into nothing kept. +#[test] +fn four_streams_fill_their_quarters() { + let (table, _) = HufTable::read(&[242], StreamEnd::PastStart).unwrap(); + // Each stream: one byte, symbols 0 and 1 under the end mark. + let stream = 0b0000_0101u8; + let mut src = vec![1, 0, 1, 0, 1, 0]; + src.extend_from_slice(&[stream, stream, stream, stream]); + let mut out = [9u8; 8]; + table.decode_four_streams(&src, &mut out).unwrap(); + assert_eq!(out, [0, 1, 0, 1, 0, 1, 0, 1]); + + // Five literals: quarters of two, the last quarter empty. Its stream + // must still end exactly, so it holds only the end mark. + let mut src = vec![1, 0, 1, 0, 1, 0]; + src.extend_from_slice(&[stream, stream, stream, 0x01]); + let mut out = [9u8; 5]; + table.decode_four_streams(&src, &mut out).unwrap(); + assert_eq!(out, [0, 1, 0, 1, 0]); + + // A jump table pointing past the section is refused. + let mut src = vec![200, 0, 1, 0, 1, 0]; + src.extend_from_slice(&[stream, stream, stream, stream]); + assert!(table.decode_four_streams(&src, &mut out).is_err()); +} + +/// Arbitrary bytes read as FSE or Huffman descriptions, and as Huffman +/// streams under whatever table they described, are refused or decode, but +/// never panic or read past their input: the descriptions in a damaged +/// frame are attacker-chosen. +#[test] +fn arbitrary_descriptions_never_panic() { + // xorshift64: deterministic, so a failure reproduces. + let mut state = 0x9E37_79B9_7F4A_7C15u64; + let mut next = move || { + state ^= state << 13; + state ^= state >> 7; + state ^= state << 17; + state + }; + let mut table = FseTable::empty(); + let mut huf = HufTable::empty(); + for _ in 0..20_000 { + let len = (next() % 24) as usize + 1; + let src: vec::Vec = (0..len).map(|_| next() as u8).collect(); + for max_symbol in [28, 35, 52, 255] { + if let Ok((dist, consumed)) = read_distribution(&src, max_symbol) { + assert!(consumed <= src.len()); + // A description may still ask for a table too large to build. + let _built = table.build(&dist).is_ok(); + } + } + for end in [StreamEnd::PastStart, StreamEnd::Exact] { + let Ok(consumed) = huf.read_into(&src, end) else { + continue; + }; + assert!(consumed <= src.len()); + let stream: vec::Vec = (0..(next() % 16) as usize + 1) + .map(|_| next() as u8) + .collect(); + let mut out = vec![0u8; (next() % 32) as usize + 1]; + let _one = huf.decode_one_stream(&stream, &mut out).is_ok(); + let _four = huf.decode_four_streams(&stream, &mut out).is_ok(); + } + } +} diff --git a/zstd/src/decoding/legacy/mod.rs b/zstd/src/decoding/legacy/mod.rs new file mode 100644 index 00000000..16d9c64c --- /dev/null +++ b/zstd/src/decoding/legacy/mod.rs @@ -0,0 +1,1423 @@ +//! Decoders for the frame formats zstd wrote before 1.0: v0.5, v0.6 and v0.7. +//! +//! libzstd 1.5.7 reads these by default (upstream's legacy level 5 in every +//! build system), so a consumer that holds an archive from 2016 gets its +//! content back from `ZSTD_decompress`; this module gives the same answer. +//! The formats differ from RFC 8878 and from each other in their headers, +//! literal and sequence coding, and dictionaries: each has its own module, +//! and what they share lives here and in [`entropy`]. +//! +//! Upstream's decoders are `lib/legacy/zstd_v0{5,6,7}.c`. + +mod entropy; +mod v05; +mod v06; +mod v07; + +#[cfg(feature = "hash")] +use crate::decoding::ContentChecksum; +use crate::decoding::Dictionary; +use crate::decoding::errors::LegacyFrameError as Error; +use alloc::vec::Vec; + +/// One of the pre-1.0 frame formats this crate decodes. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum LegacyVersion { + /// The v0.5 format, magic number `0xFD2FB525`. + V05, + /// The v0.6 format, magic number `0xFD2FB526`. + V06, + /// The v0.7 format, magic number `0xFD2FB527`. + V07, +} + +impl LegacyVersion { + /// The format whose magic number starts `src`, if it is one of these. + pub fn of(src: &[u8]) -> Option { + let magic = u32::from_le_bytes(src.get(..4)?.try_into().ok()?); + match magic { + 0xFD2F_B525 => Some(LegacyVersion::V05), + 0xFD2F_B526 => Some(LegacyVersion::V06), + 0xFD2F_B527 => Some(LegacyVersion::V07), + _ => None, + } + } + + /// The minor version number: 5, 6 or 7. + pub fn minor(self) -> u8 { + match self { + LegacyVersion::V05 => 5, + LegacyVersion::V06 => 6, + LegacyVersion::V07 => 7, + } + } + + /// The length of a frame header of this format starting `src`, magic + /// number included, as its size fields give it; `None` until `src` holds + /// the descriptor byte after the magic number. For a caller reading a + /// header in pieces, as [`frame_header_input_needed`] serves an RFC 8878 + /// one. + /// + /// [`frame_header_input_needed`]: crate::decoding::frame_header_input_needed + /// + /// ``` + /// use structured_zstd::decoding::LegacyVersion; + /// // v0.7, a one-byte dictionary ID and a window byte. + /// let header = [0x27, 0xB5, 0x2F, 0xFD, 0x01]; + /// let version = LegacyVersion::of(&header).unwrap(); + /// assert_eq!(version.header_size(&header[..4]), None); + /// assert_eq!(version.header_size(&header), Some(7)); + /// ``` + pub fn header_size(self, src: &[u8]) -> Option { + header_len(self, src).ok() + } +} + +/// Most bytes one block decodes to, the formats' `BLOCKSIZE`. +const BLOCK_SIZE_MAX: usize = 128 * 1024; + +fn corrupt(reason: &'static str) -> Error { + Error::Corrupted { reason } +} + +/// A dictionary as the legacy decoders take it. +#[derive(Clone, Copy)] +pub(crate) enum LegacyDict<'a> { + None, + /// A dictionary the RFC 8878 parser has read. Its layout is the v0.7 one, + /// so a v0.7 frame takes its tables, repeat offsets and ID; v0.5 and v0.6 + /// read any dictionary of the v0.7 layout as plain content, and are given + /// its content. + Parsed(&'a Dictionary), +} + +impl<'a> LegacyDict<'a> { + /// The bytes a v0.5 or v0.6 frame reads as its dictionary. + /// + /// Those formats take a dictionary of the v0.7 layout whole, header and + /// tables too, as content. A parsed one keeps only its content, which is + /// the end of those bytes: a match reaching past it into the header + /// fails as reaching before the dictionary, rather than decoding to + /// other bytes. + pub(crate) fn content_bytes(&self) -> &'a [u8] { + match *self { + LegacyDict::None => &[], + LegacyDict::Parsed(dict) => &dict.dict_content, + } + } +} + +/// The frame header fields the decoders and the size queries need. +#[derive(Debug, Clone, Copy)] +pub(crate) struct FrameHeader { + pub(crate) version: LegacyVersion, + /// Header length including the magic number. + pub(crate) size: usize, + /// The window the frame needs, in bytes. + pub(crate) window: u64, + /// The declared content size; the formats write 0 for unknown. + pub(crate) content_size: Option, + /// The dictionary the frame names, 0 for none. + pub(crate) dict_id: u32, + /// Whether a v0.7 frame carries a content checksum. + pub(crate) checksum: bool, +} + +/// Read the frame header at the start of `src`. +pub(crate) fn read_header(src: &[u8]) -> Result { + match LegacyVersion::of(src).ok_or(corrupt("not a v0.5-v0.7 frame"))? { + LegacyVersion::V05 => v05::read_header(src), + LegacyVersion::V06 => v06::read_header(src), + LegacyVersion::V07 => v07::read_header(src), + } +} + +/// The content size the frame at the start of `src` declares, `None` when it +/// declares none or its header does not read: libzstd reports both as an +/// unknown size (`ZSTD_getDecompressedSize_legacy`). +pub(crate) fn declared_content_size(src: &[u8]) -> Option { + read_header(src).ok()?.content_size +} + +/// A block as its three-byte header describes it. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Block { + Compressed(usize), + Raw(usize), + /// A byte repeated this many times. + Rle(usize), + End, +} + +impl Block { + fn read(src: &[u8]) -> Result { + let header = src.get(..3).ok_or(Error::Truncated)?; + let size = + usize::from(header[2]) | usize::from(header[1]) << 8 | usize::from(header[0] & 7) << 16; + Ok(match header[0] >> 6 { + 0 => Block::Compressed(size), + 1 => Block::Raw(size), + 2 => Block::Rle(size), + _ => Block::End, + }) + } + + /// Bytes the block's body takes after its header. + fn body_len(self) -> usize { + match self { + Block::Compressed(size) | Block::Raw(size) => size, + Block::Rle(_) => 1, + Block::End => 0, + } + } + + /// Refuse a block its header alone rules out: an RLE block in v0.5 or + /// v0.6, which reserve the type (`ZSTDv0{5,6}_decompressFrame` fail on + /// `bt_rle`), and a body past `block_max`. + fn check(self, version: LegacyVersion, block_max: usize) -> Result<(), Error> { + if let Block::Rle(_) = self + && version != LegacyVersion::V07 + { + return Err(corrupt("reserved block type")); + } + if self.body_len() > block_max { + return Err(corrupt("block larger than the block maximum")); + } + Ok(()) + } +} + +/// Whether `block` ends the frame. v0.5 and v0.6 end it at any block whose +/// body is empty, as their decoders and size walks do +/// (`if (cBlockSize == 0) break;`); v0.7 only at the end block. +fn ends_frame(version: LegacyVersion, block: Block) -> bool { + match version { + LegacyVersion::V07 => block == Block::End, + LegacyVersion::V05 | LegacyVersion::V06 => block.body_len() == 0, + } +} + +/// The length of the frame header at the start of `src` from its size fields +/// alone, its other fields unchecked, as upstream's size walks read it +/// (`ZSTDv0{6,7}_frameHeaderSize`; v0.5 headers are always 5 bytes). +pub(crate) fn header_len(version: LegacyVersion, src: &[u8]) -> Result { + let descriptor = *src.get(4).ok_or(Error::Truncated)?; + let fields = HeaderFields::of(version, descriptor); + Ok(5 + fields.window + fields.dict_id + fields.content_size) +} + +impl FrameHeader { + /// The window byte of the header starting `src`, which a v0.7 header + /// that is not single-segment carries right after its descriptor, laid + /// out as RFC 8878's `Window_Descriptor`. v0.5 and v0.6 headers keep + /// their window in the descriptor and carry none. + #[cfg(feature = "lsm")] + pub(crate) fn window_descriptor(&self, src: &[u8]) -> Option { + let fields = HeaderFields::of(self.version, src[4]); + (fields.window == 1).then(|| src[5]) + } + + /// The dictionary ID the header names, `None` for none. + #[cfg(feature = "lsm")] + pub(crate) fn dictionary_id(&self) -> Option { + (self.dict_id != 0).then_some(self.dict_id) + } +} + +/// The lengths of the fields that follow a frame header's descriptor byte, +/// in the order they follow it. +#[derive(Debug, Clone, Copy)] +pub(crate) struct HeaderFields { + pub(crate) window: usize, + pub(crate) dict_id: usize, + pub(crate) content_size: usize, +} + +impl HeaderFields { + pub(crate) fn of(version: LegacyVersion, descriptor: u8) -> HeaderFields { + match version { + LegacyVersion::V05 => HeaderFields { + window: 0, + dict_id: 0, + content_size: 0, + }, + LegacyVersion::V06 => HeaderFields { + window: 0, + dict_id: 0, + content_size: [0, 1, 2, 8][usize::from(descriptor >> 6)], + }, + LegacyVersion::V07 => { + let single_segment = descriptor & 0x20 != 0; + let fcs = [0, 2, 4, 8][usize::from(descriptor >> 6)]; + HeaderFields { + window: usize::from(!single_segment), + dict_id: [0, 1, 2, 4][usize::from(descriptor & 3)], + // A single-segment frame always carries its size, one + // byte at least. + content_size: fcs + usize::from(single_segment && fcs == 0), + } + } + } + } +} + +/// Where the `version` frame starting `src` ends, and the most it can decode +/// to: one block's maximum per block (`ZSTD_findFrameSizeInfoLegacy`). `None` +/// when `src` ends inside the frame, the only way the walk fails. +pub(crate) fn frame_size(version: LegacyVersion, src: &[u8]) -> Option<(usize, u64)> { + let mut at = header_len(version, src).ok()?; + let mut blocks = 0u64; + loop { + let block = Block::read(src.get(at..)?).ok()?; + at += 3; + let body = block.body_len(); + if body > src.len().checked_sub(at)? { + return None; + } + if ends_frame(version, block) { + break; + } + at += body; + blocks += 1; + } + Some((at, blocks * BLOCK_SIZE_MAX as u64)) +} + +/// Where decoded bytes go: an output that knows how much of the frame it has +/// produced and how much of that it still holds for matches to reach. +pub(crate) trait Output { + /// Bytes of the current frame produced so far. + fn produced(&self) -> usize; + /// Bytes of the current frame still held, the most a match can reach + /// back within the frame. + fn held(&self) -> usize; + /// Refuse `len` more bytes the output cannot take. + fn reserve(&mut self, len: usize) -> Result<(), Error>; + fn extend(&mut self, bytes: &[u8]); + fn repeat(&mut self, byte: u8, len: usize); + /// Append `len` bytes copied from `offset` back, `1 <= offset <= held`; + /// an overlapping copy repeats the bytes it has just written. + fn copy_back(&mut self, offset: usize, len: usize); + /// The bytes produced since `from` (a value of `produced()`), for the + /// content checksum. + #[cfg(feature = "hash")] + fn since(&self, from: usize) -> &[u8]; + /// Append a match of `len` bytes from `offset` back. An output holding + /// the whole frame takes it from the frame, the dictionary in front of + /// it, or both ([`copy_match`]); the room for `len` bytes is reserved. + fn copy_match(&mut self, offset: usize, len: usize, dict: &[u8]) -> Result<(), Error> + where + Self: Sized, + { + copy_match(self, offset, len, dict) + } +} + +/// Output into a growing vector, the current frame starting at `start`. +pub(crate) struct VecOutput<'a> { + pub(crate) buf: &'a mut Vec, + pub(crate) start: usize, +} + +impl Output for VecOutput<'_> { + fn produced(&self) -> usize { + self.held() + } + + fn held(&self) -> usize { + self.buf.len() - self.start + } + + fn reserve(&mut self, len: usize) -> Result<(), Error> { + // The block budget in front of this output bounds `len`. + self.buf.reserve(len); + Ok(()) + } + + fn extend(&mut self, bytes: &[u8]) { + self.buf.extend_from_slice(bytes); + } + + fn repeat(&mut self, byte: u8, len: usize) { + self.buf.resize(self.buf.len() + len, byte); + } + + fn copy_back(&mut self, offset: usize, len: usize) { + let from = self.buf.len() - offset; + if offset >= len { + self.buf.extend_from_within(from..from + len); + } else { + for i in 0..len { + let byte = self.buf[from + i]; + self.buf.push(byte); + } + } + } + + #[cfg(feature = "hash")] + fn since(&self, from: usize) -> &[u8] { + &self.buf[self.start + from..] + } +} + +/// Output into a caller's slice, the current frame starting at its start. +pub(crate) struct SliceOutput<'a> { + pub(crate) buf: &'a mut [u8], + pub(crate) pos: usize, +} + +impl Output for SliceOutput<'_> { + fn produced(&self) -> usize { + self.pos + } + + fn held(&self) -> usize { + self.pos + } + + fn reserve(&mut self, len: usize) -> Result<(), Error> { + if len > self.buf.len() - self.pos { + return Err(Error::OutputTooSmall); + } + Ok(()) + } + + fn extend(&mut self, bytes: &[u8]) { + self.buf[self.pos..self.pos + bytes.len()].copy_from_slice(bytes); + self.pos += bytes.len(); + } + + fn repeat(&mut self, byte: u8, len: usize) { + self.buf[self.pos..self.pos + len].fill(byte); + self.pos += len; + } + + fn copy_back(&mut self, offset: usize, len: usize) { + let from = self.pos - offset; + if offset >= len { + self.buf.copy_within(from..from + len, self.pos); + } else { + for i in 0..len { + self.buf[self.pos + i] = self.buf[from + i]; + } + } + self.pos += len; + } + + #[cfg(feature = "hash")] + fn since(&self, from: usize) -> &[u8] { + &self.buf[from..self.pos] + } +} + +/// Copy a match of `len` bytes from `offset` back: within the frame, from +/// the dictionary's content in front of it, or across both +/// (`ZSTDv07_execSequence`). The output holds the whole frame so far and has +/// room for `len` more bytes. +fn copy_match(out: &mut O, offset: usize, len: usize, dict: &[u8]) -> Result<(), Error> { + // An output that drops older bytes reads matches through its own + // `Output::copy_match`. + debug_assert_eq!(out.held(), out.produced()); + if offset == 0 { + return Err(corrupt("match with offset 0")); + } + let held = out.held(); + if offset <= held { + out.copy_back(offset, len); + return Ok(()); + } + let back = offset - held; + if back > dict.len() { + return Err(corrupt("match reaches before the dictionary")); + } + let from = dict.len() - back; + let first = len.min(back); + out.extend(&dict[from..from + first]); + let rest = len - first; + if rest > 0 { + // The match runs on from the frame's first byte. + let distance = out.held(); + out.copy_back(distance, rest); + } + Ok(()) +} + +/// Append one sequence: `lit_len` literals from `literals[*lit_pos..]`, then a +/// match of `match_len` bytes from `offset` back. +fn execute( + out: &mut O, + literals: &[u8], + lit_pos: &mut usize, + lit_len: usize, + match_len: usize, + offset: usize, + dict: &[u8], +) -> Result<(), Error> { + out.reserve( + lit_len + .checked_add(match_len) + .ok_or(Error::OutputTooSmall)?, + )?; + let lits = literals + .get(*lit_pos..) + .and_then(|rest| rest.get(..lit_len)) + .ok_or(corrupt("sequence takes more literals than the block has"))?; + out.extend(lits); + *lit_pos += lit_len; + out.copy_match(offset, match_len, dict) +} + +/// The literal section's header, common to the three formats. +enum Literals { + /// Huffman-coded: `size` literals from `compressed` bytes after a + /// `header`-byte header, in one stream or four. + Huffman { + header: usize, + size: usize, + compressed: usize, + single_stream: bool, + }, + /// Huffman-coded with the previous table: one stream. + Repeat { + size: usize, + compressed: usize, + }, + Raw { + header: usize, + size: usize, + }, + Rle { + header: usize, + size: usize, + }, +} + +impl Literals { + /// Read the literal header at the start of a compressed block + /// (`ZSTDv07_decodeLiteralsBlock`). + fn read(src: &[u8]) -> Result { + // A compressed block holds at least a literal header byte, one + // literal or run byte, and the sequence count. + if src.len() < 3 { + return Err(corrupt("compressed block too short")); + } + let b0 = usize::from(src[0]); + let b1 = usize::from(src[1]); + let b2 = usize::from(src[2]); + let size_format = (b0 >> 4) & 3; + Ok(match b0 >> 6 { + 0 => { + if src.len() < 5 { + return Err(corrupt("compressed block too short")); + } + let b3 = usize::from(src[3]); + let b4 = usize::from(src[4]); + let (header, size, compressed, single_stream) = match size_format { + 0 | 1 => ( + 3, + ((b0 & 15) << 6) + (b1 >> 2), + ((b1 & 3) << 8) + b2, + b0 & 16 != 0, + ), + 2 => ( + 4, + ((b0 & 15) << 10) + (b1 << 2) + (b2 >> 6), + ((b2 & 63) << 8) + b3, + false, + ), + _ => ( + 5, + ((b0 & 15) << 14) + (b1 << 6) + (b2 >> 2), + ((b2 & 3) << 16) + (b3 << 8) + b4, + false, + ), + }; + if size > BLOCK_SIZE_MAX { + return Err(corrupt("literal section too large")); + } + if compressed + header > src.len() { + return Err(corrupt("literals run past the block")); + } + Literals::Huffman { + header, + size, + compressed, + single_stream, + } + } + 1 => { + // Only the small single-stream form exists. + if size_format != 1 { + return Err(corrupt("repeated-table literals in an unknown form")); + } + let size = ((b0 & 15) << 6) + (b1 >> 2); + let compressed = ((b1 & 3) << 8) + b2; + if compressed + 3 > src.len() { + return Err(corrupt("literals run past the block")); + } + Literals::Repeat { size, compressed } + } + kind => { + let (header, size) = match size_format { + 0 | 1 => (1, b0 & 31), + 2 => (2, ((b0 & 15) << 8) + b1), + _ => (3, ((b0 & 15) << 16) + (b1 << 8) + b2), + }; + if kind == 2 { + if size + header > src.len() { + return Err(corrupt("literals run past the block")); + } + Literals::Raw { header, size } + } else { + if header == 3 && src.len() < 4 { + return Err(corrupt("compressed block too short")); + } + if size > BLOCK_SIZE_MAX { + return Err(corrupt("literal section too large")); + } + Literals::Rle { header, size } + } + } + }) + } +} + +/// Where a block's literals are once decoded. +enum LiteralSource { + /// In the block itself, at this range. + Block(core::ops::Range), + /// In the decoder's literal buffer. + Buffer, +} + +/// The per-format state of a frame being decoded: its entropy tables and +/// literal buffer, kept from frame to frame so their storage is reused. +enum VersionDecoder { + V05(v05::Decoder), + V06(v06::Decoder), + V07(v07::Decoder), +} + +impl VersionDecoder { + fn version(&self) -> LegacyVersion { + match self { + VersionDecoder::V05(_) => LegacyVersion::V05, + VersionDecoder::V06(_) => LegacyVersion::V06, + VersionDecoder::V07(_) => LegacyVersion::V07, + } + } + + fn heap_bytes(&self) -> usize { + match self { + VersionDecoder::V05(decoder) => decoder.heap_bytes(), + VersionDecoder::V06(decoder) => decoder.heap_bytes(), + VersionDecoder::V07(decoder) => decoder.heap_bytes(), + } + } +} + +/// What a frame decodes in, kept from frame to frame so its storage is +/// reused: the per-format decoder, and the per-block digests when asked for. +#[derive(Default)] +struct Workspace { + decoder: Option, + /// The current frame's per-block XXH64 digests, low 32 bits. + #[cfg(all(feature = "lsm", feature = "hash"))] + block_digests: Vec, +} + +impl Workspace { + fn heap_bytes(&self) -> usize { + let bytes = self.decoder.as_ref().map_or(0, VersionDecoder::heap_bytes); + #[cfg(all(feature = "lsm", feature = "hash"))] + let bytes = bytes + self.block_digests.capacity() * core::mem::size_of::(); + bytes + } +} + +/// What the caller set on the decoder that a frame of these formats obeys. +#[derive(Debug, Clone, Copy)] +pub(crate) struct FrameOptions { + /// Without `hash` no checksum is computed, whatever the mode. + #[cfg(feature = "hash")] + pub(crate) checksum: ContentChecksum, + /// Most bytes a block may hold, compressed or decoded. + pub(crate) block_max: usize, + /// Whether each data block's output is digested, for the per-block + /// checksums. + #[cfg(all(feature = "lsm", feature = "hash"))] + pub(crate) block_digests: bool, +} + +impl Default for FrameOptions { + fn default() -> FrameOptions { + FrameOptions { + #[cfg(feature = "hash")] + checksum: ContentChecksum::EmitOnly, + block_max: BLOCK_SIZE_MAX, + #[cfg(all(feature = "lsm", feature = "hash"))] + block_digests: false, + } + } +} + +/// An output that refuses to grow past `end` produced bytes: a block decodes +/// to at most [`BLOCK_SIZE_MAX`], however many sequences it holds. +struct Budget<'o, O: Output> { + out: &'o mut O, + end: usize, +} + +impl Output for Budget<'_, O> { + fn produced(&self) -> usize { + self.out.produced() + } + fn held(&self) -> usize { + self.out.held() + } + fn reserve(&mut self, len: usize) -> Result<(), Error> { + if len > self.end - self.out.produced() { + return Err(corrupt("block decodes past the largest block size")); + } + self.out.reserve(len) + } + fn extend(&mut self, bytes: &[u8]) { + self.out.extend(bytes); + } + fn repeat(&mut self, byte: u8, len: usize) { + self.out.repeat(byte, len); + } + fn copy_back(&mut self, offset: usize, len: usize) { + self.out.copy_back(offset, len); + } + #[cfg(feature = "hash")] + fn since(&self, from: usize) -> &[u8] { + self.out.since(from) + } + fn copy_match(&mut self, offset: usize, len: usize, dict: &[u8]) -> Result<(), Error> { + self.out.copy_match(offset, len, dict) + } +} + +/// A frame being decoded block by block: the whole-buffer and the streaming +/// decoders both drive one. The dictionary it was started with is passed to +/// each block again, as [`LegacyDict::content_bytes`], so the frame holds no +/// borrow and a stream can keep it beside the dictionary's handle; the +/// per-format decoder is passed too, so it outlives the frame. +struct LegacyFrame { + version: LegacyVersion, + /// The dictionary the header names, 0 for none. + dict_id: u32, + /// Where the dictionary's content lies within its bytes. + content: core::ops::Range, + /// Most bytes a block may hold, the caller's limit within the formats'. + block_max: usize, + #[cfg(feature = "hash")] + checksum_mode: ContentChecksum, + /// The declared content size, 0 when the frame declares none. + content_size: u64, + /// Data blocks decoded so far. + blocks: usize, + /// Whether the end block holds a content checksum. + has_checksum: bool, + /// The checksum the end block stores, once read. + stored_checksum: Option, + /// The checksum the content gives, once the end block is read and the + /// mode computes one. + #[cfg(feature = "hash")] + calculated_checksum: Option, + #[cfg(feature = "hash")] + hash: Option, + #[cfg(all(feature = "lsm", feature = "hash"))] + block_digests: bool, +} + +impl LegacyFrame { + /// Start decoding the frame `header` describes, readying `workspace` for + /// it: the decoder the previous frame left, when of the same format. + fn new( + header: FrameHeader, + dict: LegacyDict<'_>, + options: FrameOptions, + workspace: &mut Workspace, + ) -> Result { + #[cfg(all(feature = "lsm", feature = "hash"))] + workspace.block_digests.clear(); + let decoder = &mut workspace.decoder; + if decoder.as_ref().map(VersionDecoder::version) != Some(header.version) { + *decoder = Some(match header.version { + LegacyVersion::V05 => VersionDecoder::V05(v05::Decoder::empty()), + LegacyVersion::V06 => VersionDecoder::V06(v06::Decoder::empty()), + LegacyVersion::V07 => VersionDecoder::V07(v07::Decoder::empty()), + }); + } + let content = prime(header.dict_id, dict, workspace)?; + // Without `hash` no digest can be computed, so `Verify` checks + // nothing, as for an RFC 8878 frame. + #[cfg(feature = "hash")] + let hash = (header.checksum && options.checksum != ContentChecksum::None) + .then(|| twox_hash::XxHash64::with_seed(0)); + Ok(LegacyFrame { + version: header.version, + dict_id: header.dict_id, + content, + block_max: options.block_max.min(BLOCK_SIZE_MAX), + #[cfg(feature = "hash")] + checksum_mode: options.checksum, + content_size: header.content_size.unwrap_or(0), + blocks: 0, + has_checksum: header.checksum, + stored_checksum: None, + #[cfg(feature = "hash")] + calculated_checksum: None, + #[cfg(feature = "hash")] + hash, + #[cfg(all(feature = "lsm", feature = "hash"))] + block_digests: options.block_digests, + }) + } + + /// Prime the frame's decoder with `dict` for the blocks still to come, as + /// an RFC 8878 frame's decoder takes a forced dictionary: its tables and + /// repeat offsets become the dictionary's, and matches reach into its + /// content. What the frame has decoded so far stands. + fn set_dict(&mut self, dict: LegacyDict<'_>, workspace: &mut Workspace) -> Result<(), Error> { + self.content = prime(self.dict_id, dict, workspace)?; + Ok(()) + } + + /// Decode one block, `body` being the bytes after its header, with the + /// bytes of the dictionary the frame was started with. The block has + /// passed [`Block::check`]. + fn block( + &mut self, + workspace: &mut Workspace, + block: Block, + body: &[u8], + out: &mut O, + dict_bytes: &[u8], + ) -> Result<(), Error> { + let decoder = workspace.decoder.as_mut().expect("readied with the frame"); + let before = out.produced(); + let mut out = Budget { + end: before + self.block_max, + out, + }; + let dict = &dict_bytes[self.content.clone()]; + match block { + Block::Compressed(_) => match decoder { + VersionDecoder::V05(decoder) => decoder.block(body, &mut out, dict)?, + VersionDecoder::V06(decoder) => decoder.block(body, &mut out, dict)?, + VersionDecoder::V07(decoder) => decoder.block(body, &mut out, dict)?, + }, + Block::Raw(len) => { + out.reserve(len)?; + out.extend(body); + } + Block::Rle(len) => { + out.reserve(len)?; + out.repeat(body[0], len); + } + Block::End => {} + } + #[cfg(feature = "hash")] + if let Some(hash) = &mut self.hash { + use core::hash::Hasher; + hash.write(out.since(before)); + } + #[cfg(all(feature = "lsm", feature = "hash"))] + if self.block_digests { + let digest = twox_hash::XxHash64::oneshot(0, out.since(before)); + // The low 32 bits, as for an RFC 8878 frame's blocks. + workspace.block_digests.push(digest as u32); + } + self.blocks += 1; + Ok(()) + } + + /// Close the frame at its end block, whose header is `end`: the content + /// checksum a v0.7 frame keeps there, held against the content as the + /// caller's [`ContentChecksum`] mode says. + /// + /// The declared content size is not checked against the output. It is + /// no integrity field in these formats: the v0.6.2 command line writes + /// the size of the first chunk it read, so a v0.6 archive past 128 KiB + /// declares too little. Upstream's streaming decoder (`zstd -d`) reads + /// such an archive whole; its one-shot `ZSTD_decompress` calls it + /// corrupt, which would refuse intact data. + fn finish(&mut self, end: [u8; 3]) -> Result<(), Error> { + // Only a v0.7 header announces a checksum; with none the end block's + // size bits are 0 and nothing reads them. + if !self.has_checksum { + return Ok(()); + } + let expected = u32::from(end[2]) | u32::from(end[1]) << 8 | u32::from(end[0] & 0x3F) << 16; + self.stored_checksum = Some(expected); + #[cfg(feature = "hash")] + if let Some(hash) = &self.hash { + use core::hash::Hasher; + let calculated = ((hash.finish() >> 11) & 0x3F_FFFF) as u32; + self.calculated_checksum = Some(calculated); + if self.checksum_mode == ContentChecksum::Verify && expected != calculated { + return Err(Error::ChecksumMismatch { + expected, + calculated, + }); + } + } + Ok(()) + } +} + +/// Load `dict` into `workspace`'s decoder, readied for the frame's format, +/// and say where the dictionary's content lies in its bytes. A v0.7 header +/// naming a dictionary (`dict_id`, 0 for none) takes only that one, as +/// `ZSTDv07_decompressFrameHeader` refuses another. +fn prime( + dict_id: u32, + dict: LegacyDict<'_>, + workspace: &mut Workspace, +) -> Result, Error> { + let bytes = dict.content_bytes(); + Ok( + match workspace.decoder.as_mut().expect("readied for the frame") { + VersionDecoder::V07(decoder) => { + let content = decoder.reset(dict)?; + if dict_id != 0 && dict_id != decoder.dict_id { + return Err(Error::DictionaryMismatch { + expected: dict_id, + provided: decoder.dict_id, + }); + } + content + } + VersionDecoder::V06(decoder) => decoder.reset(bytes)?, + VersionDecoder::V05(decoder) => decoder.reset(bytes)?, + }, + ) +} + +/// Decode the frame starting `src` into `out`, in `workspace`. Returns the +/// bytes the frame took and the frame, finished. +fn decode_frame( + src: &[u8], + dict: LegacyDict<'_>, + options: FrameOptions, + workspace: &mut Workspace, + out: &mut O, +) -> Result<(usize, LegacyFrame), Error> { + let header = read_header(src)?; + let mut frame = LegacyFrame::new(header, dict, options, workspace)?; + let dict_bytes = dict.content_bytes(); + let mut at = header.size; + loop { + let block_header = src.get(at..at + 3).ok_or(Error::Truncated)?; + let block = Block::read(block_header)?; + block.check(header.version, frame.block_max)?; + let end = [block_header[0], block_header[1], block_header[2]]; + at += 3; + let body = src.get(at..at + block.body_len()).ok_or(Error::Truncated)?; + if ends_frame(header.version, block) { + frame.finish(end)?; + return Ok((at, frame)); + } + frame.block(workspace, block, body, out, dict_bytes)?; + at += body.len(); + } +} + +/// A v0.5-v0.7 frame decoded from input that arrives in pieces, a block at a +/// time, its output handed out as each block completes. It holds the window +/// the formats reach back into plus one block, as upstream's buffered +/// decoders (`ZBUFFv0{5,6,7}`) size theirs, and drops older output once it +/// has been handed out. +pub(crate) struct LegacyStream { + /// The frame in progress; `None` when no frame is. + frame: Option, + /// What frames decode in, kept across frames, whole-buffer ones too. + workspace: Workspace, + /// The dictionary the frame was started with. + dict: Option, + /// The window the frame needs. + window: u64, + /// The window ceiling, once the caller lowered it below `window` after + /// the frame started; the next step refuses the frame. + window_refused_at: Option, + /// The output segments: the current one at `..end`, and past it what is + /// left of the one before. + buf: Vec, + /// End of the current segment. + end: usize, + /// Bytes of the frame in segments before the current one. + earlier: usize, + /// End of the segment before the current one, once there is one. + previous_end: Option, + /// How much of the current segment has been handed out. + delivered: usize, + /// Most the buffer holds before a block starts a new segment: the window + /// and one block, so the segment before keeps the window in reach. + capacity: usize, + /// Whether the frame's end block has been read. + finished: bool, + /// Bytes of the frame read so far, its header included. + consumed: u64, + /// A block read from a `Read` source, held while it decodes. + block: Vec, +} + +impl LegacyStream { + pub(crate) const fn new() -> LegacyStream { + LegacyStream { + frame: None, + workspace: Workspace { + decoder: None, + #[cfg(all(feature = "lsm", feature = "hash"))] + block_digests: Vec::new(), + }, + dict: None, + window: 0, + window_refused_at: None, + buf: Vec::new(), + end: 0, + earlier: 0, + previous_end: None, + delivered: 0, + capacity: 0, + finished: false, + consumed: 0, + block: Vec::new(), + } + } + + /// Start the frame `header` describes, with `dict`. A frame already in + /// progress is dropped. + pub(crate) fn start( + &mut self, + header: FrameHeader, + dict: Option, + options: FrameOptions, + ) -> Result<(), Error> { + self.frame = None; + let legacy_dict = dict + .as_ref() + .map_or(LegacyDict::None, |dict| LegacyDict::Parsed(dict.as_dict())); + self.frame = Some(LegacyFrame::new( + header, + legacy_dict, + options, + &mut self.workspace, + )?); + self.dict = dict; + self.window = header.window; + self.window_refused_at = None; + self.buf.clear(); + self.end = 0; + self.earlier = 0; + self.previous_end = None; + self.delivered = 0; + // Every header reader holds the window to its format's largest, at + // most 128 MiB, which every target addresses. + debug_assert!(header.window <= 1 << 27); + self.capacity = header.window as usize + BLOCK_SIZE_MAX; + self.finished = false; + self.consumed = header.size as u64; + Ok(()) + } + + /// Leave the frame in progress, if any, keeping the buffer's capacity + /// and the decoder's. + pub(crate) fn end(&mut self) { + self.frame = None; + self.dict = None; + self.window_refused_at = None; + self.buf.clear(); + self.end = 0; + self.delivered = 0; + self.finished = false; + } + + /// Decode the whole frame starting `src` into `out`, in the stream's + /// workspace, leaving any frame the stream had in progress. The frame + /// stays the stream's, finished with all its output handed out, so the + /// decoder's accessors describe it. Returns the bytes the frame took. + pub(crate) fn decode_whole( + &mut self, + src: &[u8], + dict: LegacyDict<'_>, + options: FrameOptions, + out: &mut O, + ) -> Result { + self.end(); + let before = out.produced(); + let (consumed, frame) = decode_frame(src, dict, options, &mut self.workspace, out)?; + self.frame = Some(frame); + self.earlier = out.produced() - before; + self.previous_end = None; + self.finished = true; + self.consumed = consumed as u64; + Ok(consumed) + } + + /// The content size the frame declares, 0 when it declares none. + pub(crate) fn content_size(&self) -> u64 { + self.frame.as_ref().map_or(0, |frame| frame.content_size) + } + + /// Data blocks the frame has decoded so far. + pub(crate) fn blocks_decoded(&self) -> usize { + self.frame.as_ref().map_or(0, |frame| frame.blocks) + } + + /// The checksum the frame's end block stores, once read. + pub(crate) fn stored_checksum(&self) -> Option { + self.frame.as_ref()?.stored_checksum + } + + /// The checksum the frame's content gives, once its end block is read + /// and the checksum mode computes one. + #[cfg(feature = "hash")] + pub(crate) fn calculated_checksum(&self) -> Option { + self.frame.as_ref()?.calculated_checksum + } + + /// Decode the rest of the frame with `dict`, as an RFC 8878 frame takes a + /// forced dictionary. + pub(crate) fn force_dict( + &mut self, + dict: crate::decoding::DictionaryHandle, + ) -> Result<(), Error> { + let frame = self.frame.as_mut().ok_or(corrupt("no frame in progress"))?; + frame.set_dict(LegacyDict::Parsed(dict.as_dict()), &mut self.workspace)?; + self.dict = Some(dict); + Ok(()) + } + + /// The dictionary the frame decodes with, when the stream holds one. + pub(crate) fn dictionary(&self) -> Option<&crate::decoding::DictionaryHandle> { + self.dict.as_ref() + } + + /// The frame's per-block digests so far, when they were asked for. + #[cfg(all(feature = "lsm", feature = "hash"))] + pub(crate) fn block_digests(&self) -> &[u32] { + &self.workspace.block_digests + } + + /// Take a new window ceiling: a frame whose window it no longer admits + /// is refused at its next step, one it admits again goes on. + pub(crate) fn set_window_ceiling(&mut self, limit: u64) { + if self.frame.is_some() && !self.finished { + self.window_refused_at = (self.window > limit).then_some(limit); + } + } + + /// The frame's window and the ceiling it was refused at, when lowered + /// below it after the frame started. + pub(crate) fn window_refusal(&self) -> Option<(u64, u64)> { + Some((self.window, self.window_refused_at?)) + } + + /// Whether a frame is in progress, or finished with output still to hand + /// out. + pub(crate) fn active(&self) -> bool { + self.frame.is_some() + } + + /// Whether the frame's end block has been read. + pub(crate) fn is_finished(&self) -> bool { + self.finished + } + + /// Decoded bytes not handed out yet. + pub(crate) fn undelivered(&self) -> usize { + self.end - self.delivered + } + + /// Bytes the frame has decoded to so far. + pub(crate) fn produced(&self) -> u64 { + (self.earlier + self.end) as u64 + } + + /// Bytes of the frame read so far, its header included. + pub(crate) fn consumed(&self) -> u64 { + self.consumed + } + + /// Heap bytes the stream holds: its buffers and the decoder's tables and + /// literal buffer. + pub(crate) fn heap_bytes(&self) -> usize { + self.buf.capacity() + self.block.capacity() + self.workspace.heap_bytes() + } + + /// How many bytes from the start of `pending` the next step needs: a + /// block header, then the whole block; 0 once the frame is finished. A + /// block its header rules out needs only the header, whose step then + /// refuses it. + pub(crate) fn input_needed(&self, pending: &[u8]) -> usize { + if self.finished { + return 0; + } + let Ok(block) = Block::read(pending) else { + return 3; + }; + match &self.frame { + Some(frame) if block.check(frame.version, frame.block_max).is_err() => 3, + _ => 3 + block.body_len(), + } + } + + /// Decode the whole steps `src` holds into `target`, a block at a time, + /// decoding no further while `target` cannot take what is decoded. + /// Returns the bytes read and written. + pub(crate) fn decode( + &mut self, + src: &[u8], + target: &mut [u8], + ) -> Result<(usize, usize), Error> { + let mut read = 0; + let mut written = 0; + loop { + written += self.hand_out(&mut target[written..]); + if self.undelivered() > 0 || self.finished { + break; + } + let rest = &src[read..]; + let need = self.input_needed(rest); + if need > rest.len() { + break; + } + self.step(&rest[..need])?; + read += need; + } + Ok((read, written)) + } + + /// Hand out what `target` takes of the undelivered output. + pub(crate) fn hand_out(&mut self, target: &mut [u8]) -> usize { + let n = self.undelivered().min(target.len()); + target[..n].copy_from_slice(&self.buf[self.delivered..self.delivered + n]); + self.delivered += n; + n + } + + /// The undelivered output, which [`Self::mark_delivered`] then hands out. + pub(crate) fn undelivered_bytes(&self) -> &[u8] { + &self.buf[self.delivered..self.end] + } + + /// Count the first `n` undelivered bytes as handed out. + pub(crate) fn mark_delivered(&mut self, n: usize) { + debug_assert!(n <= self.undelivered()); + self.delivered += n; + } + + /// Read the next block from `source` and decode it. + pub(crate) fn read_step( + &mut self, + source: &mut R, + ) -> Result<(), crate::decoding::errors::FrameDecoderError> { + use crate::blocks::block::BlockType; + use crate::decoding::errors::{ + BlockHeaderReadError, DecodeBlockContentError, FrameDecoderError, + }; + // Lent out while the block decodes, which needs `self`. + let mut block = core::mem::take(&mut self.block); + block.clear(); + block.resize(3, 0); + let result = source + .read_exact(&mut block) + .map_err(|source| { + FrameDecoderError::FailedToReadBlockHeader(BlockHeaderReadError::ReadError(source)) + }) + .and_then(|()| { + let need = self.input_needed(&block); + block.resize(need, 0); + // The header names the block whose body is read. + let step = match Block::read(&block) { + Ok(Block::Raw(_)) => BlockType::Raw, + Ok(Block::Rle(_)) => BlockType::RLE, + _ => BlockType::Compressed, + }; + source.read_exact(&mut block[3..]).map_err(|source| { + FrameDecoderError::FailedToReadBlockBody(DecodeBlockContentError::ReadError { + step, + source, + }) + }) + }) + .and_then(|()| Ok(self.step(&block)?)); + self.block = block; + result + } + + /// Decode one block, `step` holding its header and body. + fn step(&mut self, step: &[u8]) -> Result<(), Error> { + let LegacyStream { + frame, + workspace, + dict, + window: _, + window_refused_at: _, + buf, + end, + earlier, + previous_end, + delivered, + capacity, + finished, + consumed, + block: _, + } = self; + let frame = frame.as_mut().ok_or(corrupt("no frame in progress"))?; + let block = Block::read(step)?; + block.check(frame.version, frame.block_max)?; + *consumed += step.len() as u64; + if ends_frame(frame.version, block) { + frame.finish([step[0], step[1], step[2]])?; + *finished = true; + return Ok(()); + } + // A block that would not fit starts a new segment at the buffer's + // start, once the current one is all handed out; the one before stays + // where it is for matches, as upstream's buffered decoders wrap + // (`ZBUFFv07_decompressContinue`). Output held back by a caller that + // has not collected it grows the buffer instead. + if *end + BLOCK_SIZE_MAX > *capacity && *delivered == *end && *end > 0 { + *earlier += *end; + *previous_end = Some(*end); + *end = 0; + *delivered = 0; + } + let dict_bytes = dict + .as_ref() + .map_or(LegacyDict::None, |dict| LegacyDict::Parsed(dict.as_dict())) + .content_bytes(); + let mut out = SegmentOutput { + buf, + end: *end, + earlier: *earlier, + previous_end: *previous_end, + capacity: *capacity, + }; + let decoded = frame.block(workspace, block, &step[3..], &mut out, dict_bytes); + *end = out.end; + decoded + } +} + +/// Output into a stream's segments: the current segment from the buffer's +/// start to `end`, and the segment before it, which ends at `previous_end`, +/// past the current one. +struct SegmentOutput<'b> { + buf: &'b mut Vec, + end: usize, + earlier: usize, + previous_end: Option, + /// What the buffer grows to at most while segments wrap. + capacity: usize, +} + +impl SegmentOutput<'_> { + /// Make room for `len` more bytes at `end`; the buffer grows only where + /// the segment runs past what it holds, doubling up to `capacity` and + /// past it only for output a caller holds back. + fn room(&mut self, len: usize) -> core::ops::Range { + let to = self.end + len; + if to > self.buf.capacity() { + // A `Vec` holds at most `isize::MAX` bytes, so doubling its + // length stays within `usize`. + let doubled = 2 * self.buf.len(); + let target = if to <= self.capacity { + doubled.clamp(to, self.capacity) + } else { + doubled.max(to) + }; + self.buf.reserve_exact(target - self.buf.len()); + } + if to > self.buf.len() { + self.buf.resize(to, 0); + } + self.end..to + } +} + +impl Output for SegmentOutput<'_> { + fn produced(&self) -> usize { + self.earlier + self.end + } + + fn held(&self) -> usize { + self.end + } + + fn reserve(&mut self, _len: usize) -> Result<(), Error> { + // The block's own budget bounds what it adds. + Ok(()) + } + + fn extend(&mut self, bytes: &[u8]) { + let at = self.room(bytes.len()); + self.buf[at].copy_from_slice(bytes); + self.end += bytes.len(); + } + + fn repeat(&mut self, byte: u8, len: usize) { + let at = self.room(len); + self.buf[at].fill(byte); + self.end += len; + } + + fn copy_back(&mut self, offset: usize, len: usize) { + let from = self.end - offset; + let at = self.room(len); + if offset >= len { + self.buf.copy_within(from..from + len, at.start); + } else { + for i in 0..len { + self.buf[at.start + i] = self.buf[from + i]; + } + } + self.end += len; + } + + #[cfg(feature = "hash")] + fn since(&self, from: usize) -> &[u8] { + &self.buf[self.end - (self.produced() - from)..self.end] + } + + fn copy_match(&mut self, offset: usize, len: usize, dict: &[u8]) -> Result<(), Error> { + let Some(previous_end) = self.previous_end else { + // One segment so far: it holds the whole frame. + return copy_match(self, offset, len, dict); + }; + if offset == 0 { + return Err(corrupt("match with offset 0")); + } + if offset <= self.end { + self.copy_back(offset, len); + return Ok(()); + } + // Into the segment before, which the current one has not yet + // overwritten from `end` on. + let back = offset - self.end; + let from = previous_end + .checked_sub(back) + .filter(|&from| from >= self.end) + .ok_or(corrupt("match reaches before the window"))?; + let first = len.min(back); + let at = self.room(first); + self.buf.copy_within(from..from + first, at.start); + self.end += first; + let rest = len - first; + if rest > 0 { + // The match runs on from the current segment's first byte. + let distance = self.end; + self.copy_back(distance, rest); + } + Ok(()) + } +} + +#[cfg(test)] +mod tests; diff --git a/zstd/src/decoding/legacy/tests.rs b/zstd/src/decoding/legacy/tests.rs new file mode 100644 index 00000000..9f421936 --- /dev/null +++ b/zstd/src/decoding/legacy/tests.rs @@ -0,0 +1,1759 @@ +//! Frames written by the v0.5, v0.6 and v0.7 release command lines +//! (`scripts/legacy-fixtures.sh`) decode to the files they were made from. + +use super::*; +use alloc::vec; +use alloc::vec::Vec; + +#[cfg(feature = "std")] +mod fixtures { + use super::*; + use alloc::format; + use alloc::string::{String, ToString}; + use std::path::{Path, PathBuf}; + + pub(super) struct Fixture { + pub(super) name: String, + pub(super) frame: Vec, + pub(super) content: Vec, + pub(super) dict: Option>, + /// `dict` as the dictionary parser reads it. + pub(super) parsed: Option, + pub(super) version: LegacyVersion, + } + + impl Fixture { + /// The fixture's dictionary as the decoders take it. + pub(super) fn legacy_dict(&self) -> LegacyDict<'_> { + self.parsed + .as_ref() + .map_or(LegacyDict::None, LegacyDict::Parsed) + } + } + + fn root() -> &'static Path { + Path::new(env!("CARGO_MANIFEST_DIR")) + } + + /// Every fixture, with the file it decodes to and the dictionary it needs. + pub(super) fn all() -> Vec { + let dir = root().join("legacy_frames"); + let mut names: Vec = std::fs::read_dir(&dir) + .expect("legacy_frames directory") + .map(|entry| entry.unwrap().file_name().into_string().unwrap()) + .filter(|name| name.ends_with(".zst")) + .collect(); + names.sort(); + assert!(names.len() >= 50, "fixtures missing from {}", dir.display()); + names + .into_iter() + .map(|name| { + let (stem, rest) = ["v05", "v06", "v07"] + .iter() + .find_map(|tag| { + name.split_once(&format!(".{tag}.")) + .map(|split| (split, *tag)) + }) + .map(|((stem, _), tag)| (stem.to_string(), tag)) + .expect("fixture name carries its version"); + let version = match rest { + "v05" => LegacyVersion::V05, + "v06" => LegacyVersion::V06, + _ => LegacyVersion::V07, + }; + let is_dict = name.contains(".dict"); + // `x` is the file N times over. + let (file, times) = stem + .rsplit_once('x') + .and_then(|(file, n)| Some((file, n.parse::().ok()?))) + .unwrap_or((stem.as_str(), 1)); + let source: PathBuf = if is_dict { + root().join("dict_tests/files").join(file) + } else { + root().join("decodecorpus_files").join(file) + }; + let dict = + is_dict.then(|| std::fs::read(dir.join(format!("dictionary.{rest}"))).unwrap()); + Fixture { + frame: std::fs::read(dir.join(&name)).unwrap(), + content: std::fs::read(&source).unwrap().repeat(times), + parsed: dict.as_deref().map(|raw| { + crate::decoding::Dictionary::from_serialized_or_raw_content(raw).unwrap() + }), + dict, + version, + name, + } + }) + .collect() + } +} + +#[cfg(feature = "std")] +use fixtures::all as fixtures; + +fn decode_to_vec(frame: &[u8], dict: LegacyDict<'_>) -> Result<(Vec, usize), Error> { + decode_to_vec_in( + frame, + dict, + FrameOptions::default(), + &mut Workspace::default(), + ) +} + +/// [`decode_to_vec`] refusing a frame whose content checksum does not match. +fn decode_verified(frame: &[u8], dict: LegacyDict<'_>) -> Result<(Vec, usize), Error> { + let options = FrameOptions { + #[cfg(feature = "hash")] + checksum: crate::decoding::ContentChecksum::Verify, + ..FrameOptions::default() + }; + decode_to_vec_in(frame, dict, options, &mut Workspace::default()) +} + +/// [`decode_to_vec`] with `options`, in `workspace`, the storage an earlier +/// frame left. +fn decode_to_vec_in( + frame: &[u8], + dict: LegacyDict<'_>, + options: FrameOptions, + workspace: &mut Workspace, +) -> Result<(Vec, usize), Error> { + let mut buf = Vec::new(); + let mut out = VecOutput { + buf: &mut buf, + start: 0, + }; + let (consumed, _) = decode_frame(frame, dict, options, workspace, &mut out)?; + Ok((buf, consumed)) +} + +/// Each fixture names its own format and decodes byte-exact, taking exactly +/// its own bytes: in a fresh decoder, and in one every fixture before it +/// left its tables in, whatever its format and dictionary. +#[cfg(feature = "std")] +#[test] +fn fixtures_decode_to_their_content() { + let mut reused = Workspace::default(); + for fixture in fixtures() { + assert_eq!( + LegacyVersion::of(&fixture.frame), + Some(fixture.version), + "{}", + fixture.name + ); + for workspace in [&mut Workspace::default(), &mut reused] { + let (decoded, consumed) = decode_to_vec_in( + &fixture.frame, + fixture.legacy_dict(), + FrameOptions::default(), + workspace, + ) + .unwrap_or_else(|err| panic!("{}: {err}", fixture.name)); + assert_eq!(consumed, fixture.frame.len(), "{}", fixture.name); + assert!( + decoded == fixture.content, + "{} decodes to other bytes", + fixture.name + ); + } + } +} + +/// Decoding into a caller's slice gives the same bytes, and a slice one byte +/// short is refused as too small rather than written past. +#[cfg(feature = "std")] +#[test] +fn fixtures_decode_into_a_slice() { + for fixture in fixtures() { + let dict = fixture.legacy_dict(); + let mut buf = vec![0u8; fixture.content.len()]; + let mut out = SliceOutput { + buf: &mut buf, + pos: 0, + }; + decode_frame( + &fixture.frame, + dict, + FrameOptions::default(), + &mut Workspace::default(), + &mut out, + ) + .unwrap_or_else(|err| panic!("{}: {err}", fixture.name)); + assert_eq!(out.pos, fixture.content.len(), "{}", fixture.name); + assert!(buf == fixture.content, "{}", fixture.name); + + if let Some(short) = fixture.content.len().checked_sub(1) { + let mut buf = vec![0u8; short]; + let mut out = SliceOutput { + buf: &mut buf, + pos: 0, + }; + assert!( + matches!( + decode_frame( + &fixture.frame, + dict, + FrameOptions::default(), + &mut Workspace::default(), + &mut out + ), + Err(Error::OutputTooSmall) + ), + "{}", + fixture.name + ); + } + } +} + +/// The size walk ends where the decoder does, and bounds the content by +/// whole blocks. +#[cfg(feature = "std")] +#[test] +fn frame_size_matches_the_decoded_frame() { + for fixture in fixtures() { + let (size, bound) = frame_size(fixture.version, &fixture.frame).unwrap(); + assert_eq!(size, fixture.frame.len(), "{}", fixture.name); + assert!(bound >= fixture.content.len() as u64, "{}", fixture.name); + assert_eq!(bound % BLOCK_SIZE_MAX as u64, 0, "{}", fixture.name); + } +} + +/// A v0.7 frame naming a dictionary by ID is refused without it, and with a +/// dictionary of another ID, saying which it needs. (v0.5 and v0.6 frames +/// carry no ID; one whose blocks never reach into the dictionary decodes +/// without it.) +#[cfg(feature = "std")] +#[test] +fn a_v07_frame_naming_a_dictionary_needs_it() { + for fixture in fixtures() + .into_iter() + .filter(|f| f.name.ends_with(".v07.dict.zst")) + { + assert_eq!( + decode_to_vec(&fixture.frame, LegacyDict::None).unwrap_err(), + Error::DictionaryMismatch { + expected: 7, + provided: 0 + }, + "{}", + fixture.name + ); + let mut other = fixture.dict.clone().unwrap(); + other[4] ^= 1; + let other = crate::decoding::Dictionary::decode_dict(&other).unwrap(); + assert_eq!( + decode_to_vec(&fixture.frame, LegacyDict::Parsed(&other)).unwrap_err(), + Error::DictionaryMismatch { + expected: 7, + provided: 6 + }, + "{}", + fixture.name + ); + } +} + +/// A v0.7 dictionary read by the RFC 8878 parser primes the decoder as its +/// raw bytes do, and a raw-content one gives its content and its ID. +#[cfg(feature = "std")] +#[test] +fn a_parsed_dictionary_decodes_like_its_bytes() { + for fixture in fixtures().into_iter().filter(|f| f.dict.is_some()) { + let raw = fixture.dict.as_deref().unwrap(); + let parsed = crate::decoding::Dictionary::from_serialized_or_raw_content(raw).unwrap(); + let (decoded, _) = decode_to_vec(&fixture.frame, LegacyDict::Parsed(&parsed)) + .unwrap_or_else(|err| panic!("{}: {err}", fixture.name)); + assert!(decoded == fixture.content, "{}", fixture.name); + } + // A raw-content dictionary answers to the ID it was given. + let fixture = fixtures() + .into_iter() + .find(|f| f.name.ends_with(".v07.dict.zst")) + .unwrap(); + let raw = crate::decoding::Dictionary::from_raw_content(6, vec![0; 64]).unwrap(); + assert_eq!( + decode_to_vec(&fixture.frame, LegacyDict::Parsed(&raw)).unwrap_err(), + Error::DictionaryMismatch { + expected: 7, + provided: 6 + } + ); +} + +/// `FrameDecoder::decode_all` decodes each fixture, its dictionary given as a +/// handle or registered under the ID a v0.7 frame names, and a frame between +/// RFC 8878 frames; a slice one byte short is too small. +#[cfg(feature = "std")] +#[test] +fn decode_all_decodes_the_fixtures() { + use crate::decoding::{DictionaryHandle, FrameDecoder, errors::FrameDecoderError}; + for fixture in fixtures() { + let mut out = vec![0u8; fixture.content.len()]; + let mut decoder = FrameDecoder::new(); + let written = match &fixture.dict { + None => decoder.decode_all(&fixture.frame, &mut out), + Some(raw) => { + let dict = DictionaryHandle::from_serialized_or_raw_content(raw).unwrap(); + decoder.decode_all_with_dict_handle(&fixture.frame, &mut out, &dict) + } + } + .unwrap_or_else(|err| panic!("{}: {err}", fixture.name)); + assert_eq!(written, fixture.content.len(), "{}", fixture.name); + assert!(out == fixture.content, "{}", fixture.name); + if let Some(short) = fixture.content.len().checked_sub(1) + && fixture.dict.is_none() + { + assert!(matches!( + FrameDecoder::new().decode_all(&fixture.frame, &mut out[..short]), + Err(FrameDecoderError::TargetTooSmall) + )); + } + } + + let fixture = fixtures() + .into_iter() + .find(|f| f.name.ends_with(".v07.dict.zst")) + .unwrap(); + let mut out = vec![0u8; fixture.content.len()]; + assert!(matches!( + FrameDecoder::new().decode_all(&fixture.frame, &mut out), + Err(FrameDecoderError::DictNotProvided { dict_id: 7 }) + )); + let mut decoder = FrameDecoder::new(); + decoder + .add_dict_from_bytes(fixture.dict.as_deref().unwrap()) + .unwrap(); + decoder.decode_all(&fixture.frame, &mut out).unwrap(); + assert!(out == fixture.content); + + let fixture = fixtures() + .into_iter() + .find(|f| f.name == "abc.txt.v06.l1.zst") + .unwrap(); + let rfc = crate::encoding::compress_to_vec( + &b"between"[..], + crate::encoding::CompressionLevel::Fastest, + ); + let mut input = rfc.clone(); + input.extend_from_slice(&fixture.frame); + input.extend_from_slice(&rfc); + let mut expected = b"between".to_vec(); + expected.extend_from_slice(&fixture.content); + expected.extend_from_slice(b"between"); + let mut out = vec![0u8; expected.len()]; + assert_eq!( + FrameDecoder::new().decode_all(&input, &mut out).unwrap(), + expected.len() + ); + assert!(out == expected); +} + +/// A source that hands out one byte per read. +#[cfg(feature = "std")] +struct Trickle<'a>(&'a [u8]); + +#[cfg(feature = "std")] +impl std::io::Read for Trickle<'_> { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + let Some((&first, rest)) = self.0.split_first() else { + return Ok(0); + }; + if buf.is_empty() { + return Ok(0); + } + buf[0] = first; + self.0 = rest; + Ok(1) + } +} + +/// `StreamingDecoder` decodes each fixture whole, and from a source that +/// gives one byte at a time into a small buffer, which drives the stream a +/// step at a time and keeps only a window of history; a stream mixing these +/// frames with RFC 8878 ones decodes in order. +#[cfg(feature = "std")] +#[test] +fn streaming_decoder_decodes_the_fixtures() { + use crate::decoding::{DictionaryHandle, StreamingDecoder}; + use std::io::Read; + for fixture in fixtures() { + let dict = fixture + .dict + .as_deref() + .map(|raw| DictionaryHandle::from_serialized_or_raw_content(raw).unwrap()); + let open = |source| match &dict { + None => StreamingDecoder::new(source), + Some(dict) => StreamingDecoder::new_with_dictionary_handle(source, dict).unwrap(), + }; + let mut whole = Vec::new(); + open(&fixture.frame[..]) + .read_to_end(&mut whole) + .unwrap_or_else(|err| panic!("{}: {err}", fixture.name)); + assert!(whole == fixture.content, "{}", fixture.name); + + if fixture.frame.len() < 64 * 1024 { + let mut decoder = match &dict { + None => StreamingDecoder::new(Trickle(&fixture.frame)), + Some(dict) => { + StreamingDecoder::new_with_dictionary_handle(Trickle(&fixture.frame), dict) + .unwrap() + } + }; + let mut pieces = Vec::new(); + let mut buf = [0u8; 1000]; + loop { + let n = decoder + .read(&mut buf) + .unwrap_or_else(|err| panic!("{}: {err}", fixture.name)); + if n == 0 { + break; + } + pieces.extend_from_slice(&buf[..n]); + } + assert!(pieces == fixture.content, "{} in pieces", fixture.name); + } + } + + let fixture = fixtures() + .into_iter() + .find(|f| f.name == "z000001.v07.l19.zst") + .unwrap(); + let rfc = crate::encoding::compress_to_vec( + &b"between"[..], + crate::encoding::CompressionLevel::Fastest, + ); + let mut input = rfc.clone(); + input.extend_from_slice(&fixture.frame); + input.extend_from_slice(&rfc); + input.extend_from_slice(&fixture.frame); + let mut expected = b"between".to_vec(); + expected.extend_from_slice(&fixture.content); + expected.extend_from_slice(b"between"); + expected.extend_from_slice(&fixture.content); + let mut out = Vec::new(); + StreamingDecoder::new(&input[..]) + .read_to_end(&mut out) + .unwrap(); + assert!(out == expected); + let mut out = Vec::new(); + let mut decoder = StreamingDecoder::new(&input[..]); + let mut buf = [0u8; 4096]; + loop { + let n = decoder.read(&mut buf).unwrap(); + if n == 0 { + break; + } + out.extend_from_slice(&buf[..n]); + } + assert!(out == expected); +} + +/// The block-level interface drives these frames too: `reset` starts one, +/// `decode_blocks` decodes it a block at a time and `collect` hands it out; +/// `decode_from_to` decodes it from input given in small pieces. +#[cfg(feature = "std")] +#[test] +fn frame_decoder_steps_through_the_fixtures() { + use crate::decoding::{BlockDecodingStrategy, FrameDecoder}; + for fixture in fixtures().into_iter().filter(|f| f.dict.is_none()) { + let mut source = &fixture.frame[..]; + let mut decoder = FrameDecoder::new(); + decoder.reset(&mut source).unwrap(); + let mut out = Vec::new(); + while !decoder + .decode_blocks(&mut source, BlockDecodingStrategy::UptoBlocks(1)) + .unwrap_or_else(|err| panic!("{}: {err}", fixture.name)) + { + out.extend(decoder.collect().unwrap()); + } + out.extend(decoder.collect().unwrap()); + assert!(source.is_empty(), "{}", fixture.name); + assert!(out == fixture.content, "{}", fixture.name); + assert_eq!( + decoder.bytes_read_from_source(), + fixture.frame.len() as u64, + "{}", + fixture.name + ); + + // `decode_from_to` with what the decoder asks for, and a target + // smaller than a block. + let mut decoder = FrameDecoder::new(); + let mut source = &fixture.frame[..]; + decoder.reset(&mut source).unwrap(); + let mut out = Vec::new(); + let mut target = [0u8; 777]; + while !(decoder.is_finished() && decoder.can_collect() == 0) { + let need = decoder.input_needed(source).min(source.len()); + let (read, written) = decoder + .decode_from_to(&source[..need], &mut target) + .unwrap_or_else(|err| panic!("{}: {err}", fixture.name)); + source = &source[read..]; + out.extend_from_slice(&target[..written]); + assert!(read > 0 || written > 0, "{} stalled", fixture.name); + } + assert!(out == fixture.content, "{}", fixture.name); + } +} + +/// The frame queries answer for these frames as libzstd does: a frame, its +/// length through the end block, its declared size or unknown, and a bound +/// of 128 KiB per block; a cut frame is truncated. +#[cfg(feature = "std")] +#[test] +fn frame_queries_answer_for_the_fixtures() { + use crate::decoding::{ + FrameContentSize, FrameSizeError, find_decompressed_size, find_frame_compressed_size, + frame_decompressed_bound, is_frame, read_frame_content_size, + }; + for fixture in fixtures() { + let frame = &fixture.frame; + assert!(is_frame(frame), "{}", fixture.name); + assert_eq!( + find_frame_compressed_size(frame).unwrap(), + frame.len(), + "{}", + fixture.name + ); + let bound = frame_decompressed_bound(frame).unwrap(); + assert!(bound >= fixture.content.len() as u64, "{}", fixture.name); + let declared = declared_content_size(frame); + let expected = declared.map_or(FrameContentSize::Unknown, FrameContentSize::Known); + assert_eq!( + read_frame_content_size(frame).unwrap(), + expected, + "{}", + fixture.name + ); + assert_eq!( + find_decompressed_size(frame).unwrap(), + expected, + "{}", + fixture.name + ); + assert!(matches!( + find_frame_compressed_size(&frame[..frame.len() - 1]), + Err(FrameSizeError::Truncated) + )); + } + // v0.5 declares no size. + let v05 = fixtures() + .into_iter() + .find(|f| f.name == "abc.txt.v05.l1.zst") + .unwrap(); + assert_eq!( + read_frame_content_size(&v05.frame).unwrap(), + FrameContentSize::Unknown + ); +} + +/// Every truncation of every fixture is refused or decodes to a prefix, and +/// none panics. +#[cfg(feature = "std")] +#[test] +fn truncated_frames_are_refused() { + for fixture in fixtures().into_iter().filter(|f| f.frame.len() < 4096) { + let dict = fixture.legacy_dict(); + for len in 0..fixture.frame.len() { + assert!( + decode_to_vec(&fixture.frame[..len], dict).is_err(), + "{} cut to {len} bytes decoded", + fixture.name + ); + } + } +} + +/// Flipping any one bit of a frame is refused or decodes to some output, but +/// never panics; a v0.7 frame with a checksum, verified, decodes to other +/// content only when that content's checksum is the one the frame carries, +/// which with 22 bits of it does happen. +#[cfg(feature = "std")] +#[test] +fn damaged_frames_never_panic() { + for fixture in fixtures().into_iter().filter(|f| f.frame.len() < 4096) { + let dict = fixture.legacy_dict(); + let mut frame = fixture.frame.clone(); + for byte in 0..frame.len() { + for bit in 0..8 { + frame[byte] ^= 1 << bit; + let decoded = decode_verified(&frame, dict); + #[cfg(feature = "hash")] + if let Ok((content, end)) = &decoded + && *content != fixture.content + && read_header(&frame).is_ok_and(|header| header.checksum) + { + let carried = u64::from(frame[end - 1]) + | u64::from(frame[end - 2]) << 8 + | u64::from(frame[end - 3] & 0x3F) << 16; + let digest = twox_hash::XxHash64::oneshot(0, content) >> 11 & 0x3F_FFFF; + assert_eq!( + digest, carried, + "{}: bit {bit} of byte {byte} changed the content unchecked", + fixture.name + ); + } + drop(decoded); + frame[byte] ^= 1 << bit; + } + } + } +} + +/// A v0.7 frame whose content checksum does not match is refused with the +/// checksum error when verified, and decodes when not. +#[cfg(all(feature = "std", feature = "hash"))] +#[test] +fn a_wrong_v07_checksum_is_refused() { + let fixture = fixture("abc.txt.v07.l1.zst"); + let mut frame = fixture.frame.clone(); + // The end block's header holds the checksum in its low 22 bits. + let last = frame.len() - 1; + frame[last] ^= 1; + assert!(matches!( + decode_verified(&frame, LegacyDict::None), + Err(Error::ChecksumMismatch { .. }) + )); + assert!(decode_to_vec(&frame, LegacyDict::None).unwrap().0 == fixture.content); +} + +/// The headers of the three formats: version, size, window and content size. +#[test] +fn headers_read_their_fields() { + // v0.5: magic, then a window descriptor of log 11 + 1. + let v05 = read_header(&[0x25, 0xB5, 0x2F, 0xFD, 0x01]).unwrap(); + assert_eq!( + (v05.version, v05.size, v05.window, v05.content_size), + (LegacyVersion::V05, 5, 1 << 12, None) + ); + // v0.5 reserves the descriptor's high nibble. + assert_eq!( + read_header(&[0x25, 0xB5, 0x2F, 0xFD, 0x10]).unwrap_err(), + Error::UnsupportedParameter + ); + // v0.6: a one-byte content size of 200, window log 12 + 3. + let v06 = read_header(&[0x26, 0xB5, 0x2F, 0xFD, 0x43, 200]).unwrap(); + assert_eq!( + (v06.version, v06.size, v06.window, v06.content_size), + (LegacyVersion::V06, 6, 1 << 15, Some(200)) + ); + // A two-byte content size counts from 256. + let v06 = read_header(&[0x26, 0xB5, 0x2F, 0xFD, 0x80, 1, 0]).unwrap(); + assert_eq!(v06.content_size, Some(257)); + // v0.6 reserves bit 5. + assert_eq!( + read_header(&[0x26, 0xB5, 0x2F, 0xFD, 0x20]).unwrap_err(), + Error::UnsupportedParameter + ); + // v0.7: checksum, a one-byte dictionary ID, single segment with a + // one-byte content size, which is also the window. + let v07 = read_header(&[0x27, 0xB5, 0x2F, 0xFD, 0x25, 9, 100]).unwrap(); + assert_eq!( + ( + v07.size, + v07.window, + v07.content_size, + v07.dict_id, + v07.checksum + ), + (7, 100, Some(100), 9, true) + ); + // v0.7 reserves bit 3, and a header cut short is truncated. + assert_eq!( + read_header(&[0x27, 0xB5, 0x2F, 0xFD, 0x08, 0]).unwrap_err(), + Error::UnsupportedParameter + ); + assert_eq!( + read_header(&[0x27, 0xB5, 0x2F, 0xFD, 0x25, 9]).unwrap_err(), + Error::Truncated + ); + // A window past the format's largest is refused. + assert_eq!( + read_header(&[0x27, 0xB5, 0x2F, 0xFD, 0x00, 0xF8]).unwrap_err(), + Error::UnsupportedParameter + ); +} + +/// An RFC 8878 frame and a frame of another magic number are not legacy. +#[test] +fn other_magic_numbers_are_not_legacy() { + assert_eq!(LegacyVersion::of(&[0x28, 0xB5, 0x2F, 0xFD]), None); + assert_eq!(LegacyVersion::of(&[0x24, 0xB5, 0x2F, 0xFD]), None); + assert_eq!(LegacyVersion::of(&[0x25, 0xB5, 0x2F]), None); + assert_eq!(declared_content_size(&[0x26, 0xB5, 0x2F, 0xFD, 0x43]), None); +} + +/// v0.5 and v0.6 end a frame at any block whose body is empty, v0.7 only at +/// its end block: a raw block of no bytes is an empty block there. +#[test] +fn an_empty_block_ends_only_older_frames() { + let empty_raw = [0x40, 0, 0]; + let end = [0xC0, 0, 0]; + let mut v06 = vec![0x26, 0xB5, 0x2F, 0xFD, 0x00]; + v06.extend_from_slice(&empty_raw); + assert_eq!(frame_size(LegacyVersion::V06, &v06), Some((8, 0))); + let mut v07 = vec![0x27, 0xB5, 0x2F, 0xFD, 0x00, 0x00]; + v07.extend_from_slice(&empty_raw); + assert_eq!(frame_size(LegacyVersion::V07, &v07), None); + v07.extend_from_slice(&end); + assert_eq!( + frame_size(LegacyVersion::V07, &v07), + Some((12, BLOCK_SIZE_MAX as u64)) + ); + assert_eq!( + decode_to_vec(&v07, LegacyDict::None).unwrap(), + (Vec::new(), 12) + ); +} + +/// The size walk measures a header from its size fields alone, as upstream's +/// does: a reserved bit the decoder refuses does not stop the walk. +#[test] +fn frame_size_reads_only_the_size_fields() { + let end = [0xC0, 0, 0]; + // v0.6 with the reserved bit and a one-byte content size. + let mut v06 = vec![0x26, 0xB5, 0x2F, 0xFD, 0x60, 9]; + v06.extend_from_slice(&end); + assert_eq!(frame_size(LegacyVersion::V06, &v06), Some((9, 0))); + // v0.7 with the reserved bit, a two-byte dictionary ID and a window byte. + let mut v07 = vec![0x27, 0xB5, 0x2F, 0xFD, 0x0A, 0, 1, 2]; + v07.extend_from_slice(&end); + assert_eq!(frame_size(LegacyVersion::V07, &v07), Some((11, 0))); + // v0.7 single segment without a size field still carries one byte of it. + let mut v07 = vec![0x27, 0xB5, 0x2F, 0xFD, 0x20, 7]; + v07.extend_from_slice(&end); + assert_eq!(frame_size(LegacyVersion::V07, &v07), Some((9, 0))); + // v0.5 headers are five bytes whatever the descriptor says. + let mut v05 = vec![0x25, 0xB5, 0x2F, 0xFD, 0xF0]; + v05.extend_from_slice(&end); + assert_eq!(frame_size(LegacyVersion::V05, &v05), Some((8, 0))); + assert_eq!(frame_size(LegacyVersion::V05, &v05[..4]), None); +} + +/// A stream keeps its window and a block of history, not the whole frame: +/// a frame of a 1 KiB window and 1 MiB of blocks decodes in order while the +/// buffer stays within a few blocks. +#[test] +fn a_stream_holds_its_window_not_the_frame() { + // v0.7, window byte 0: a 1 KiB window. + let mut frame = vec![0x27, 0xB5, 0x2F, 0xFD, 0x00, 0x00]; + let mut expected = Vec::new(); + for n in 0..8u8 { + let size = 128 * 1024 - 1; + // A raw block of `size` bytes of `n`. + frame.extend_from_slice(&[0x40 | (size >> 16) as u8, (size >> 8) as u8, size as u8]); + frame.extend(core::iter::repeat_n(n, size)); + expected.extend(core::iter::repeat_n(n, size)); + } + frame.extend_from_slice(&[0xC0, 0, 0]); + let header = read_header(&frame).unwrap(); + let mut stream = LegacyStream::new(); + stream.start(header, None, FrameOptions::default()).unwrap(); + let mut src = &frame[header.size..]; + let mut out = Vec::new(); + let mut target = vec![0u8; 50_000]; + let mut largest = 0; + while !(stream.is_finished() && stream.undelivered() == 0) { + let (read, written) = stream.decode(src, &mut target).unwrap(); + src = &src[read..]; + out.extend_from_slice(&target[..written]); + largest = largest.max(stream.heap_bytes()); + assert!(read > 0 || written > 0); + } + assert!(src.is_empty()); + assert!(out == expected); + assert_eq!(stream.produced(), expected.len() as u64); + assert_eq!(stream.consumed(), frame.len() as u64); + assert!(largest <= 4 * BLOCK_SIZE_MAX, "held {largest} bytes"); +} + +/// A frame several windows long decodes through the stream with its matches +/// reaching into the segment before the current one, the buffer never past +/// the window and a block. +#[cfg(feature = "std")] +#[test] +fn a_stream_reads_matches_from_the_segment_before() { + for fixture in fixtures() + .into_iter() + .filter(|f| f.name.starts_with("z000000x8.")) + { + let header = read_header(&fixture.frame).unwrap(); + let capacity = header.window as usize + BLOCK_SIZE_MAX; + assert!( + fixture.content.len() > 2 * capacity, + "{}: the frame must outrun the buffer", + fixture.name + ); + let mut stream = LegacyStream::new(); + stream.start(header, None, FrameOptions::default()).unwrap(); + let mut src = &fixture.frame[header.size..]; + let mut out = Vec::new(); + let mut target = vec![0u8; 4096]; + while !(stream.is_finished() && stream.undelivered() == 0) { + let (read, written) = stream + .decode(src, &mut target) + .unwrap_or_else(|err| panic!("{}: {err}", fixture.name)); + assert!(read > 0 || written > 0, "{} stalled", fixture.name); + src = &src[read..]; + out.extend_from_slice(&target[..written]); + } + assert!(out == fixture.content, "{}", fixture.name); + assert!( + stream.buf.capacity() <= capacity, + "{}: held {} bytes for a window of {}", + fixture.name, + stream.buf.capacity(), + header.window + ); + } +} + +/// The fixture of this name. +#[cfg(feature = "std")] +fn fixture(name: &str) -> fixtures::Fixture { + fixtures() + .into_iter() + .find(|f| f.name == name) + .unwrap_or_else(|| panic!("no fixture {name}")) +} + +/// A fresh decoder handed one of these frames by `decode_from_to` starts it +/// itself, as it starts an RFC 8878 frame, and decodes it to its content. +#[cfg(feature = "std")] +#[test] +fn decode_from_to_starts_these_frames_on_a_fresh_decoder() { + use crate::decoding::FrameDecoder; + for name in [ + "abc.txt.v05.l1.zst", + "z000001.v06.l19.zst", + "z000001.v07.l19.zst", + ] { + let fixture = fixture(name); + let mut decoder = FrameDecoder::new(); + let mut src = &fixture.frame[..]; + let mut out = Vec::new(); + let mut target = [0u8; 500]; + loop { + let (read, written) = decoder + .decode_from_to(src, &mut target) + .unwrap_or_else(|err| panic!("{name}: {err}")); + src = &src[read..]; + out.extend_from_slice(&target[..written]); + if decoder.is_finished() && decoder.can_collect() == 0 { + break; + } + assert!(read > 0 || written > 0, "{name} stalled"); + } + assert!(src.is_empty(), "{name}"); + assert!(out == fixture.content, "{name}"); + } +} + +/// The checksum mode applies to a v0.7 frame's 22-bit checksum as to an +/// RFC 8878 frame's: `Verify` refuses a frame whose content does not match +/// it, `EmitOnly` decodes it and exposes both values, `None` computes +/// nothing. +#[cfg(all(feature = "std", feature = "hash"))] +#[test] +fn the_checksum_mode_applies_to_v07_frames() { + use crate::decoding::errors::FrameDecoderError; + use crate::decoding::{BlockDecodingStrategy, ContentChecksum, FrameDecoder}; + let fixture = fixture("abc.txt.v07.l1.zst"); + let mut frame = fixture.frame.clone(); + let last = frame.len() - 1; + frame[last] ^= 1; + let end = &frame[frame.len() - 3..]; + let stored = u32::from(end[2]) | u32::from(end[1]) << 8 | u32::from(end[0] & 0x3F) << 16; + let run = |mode| { + let mut decoder = FrameDecoder::new(); + decoder.set_content_checksum(mode); + let mut src = &frame[..]; + decoder.reset(&mut src)?; + decoder.decode_blocks(&mut src, BlockDecodingStrategy::All)?; + let content = decoder.collect().unwrap(); + Ok::<_, FrameDecoderError>(( + content, + decoder.get_checksum_from_data(), + decoder.get_calculated_checksum(), + )) + }; + assert!(matches!( + run(ContentChecksum::Verify), + Err(FrameDecoderError::LegacyFrame( + Error::ChecksumMismatch { .. } + )) + )); + let (content, from_data, calculated) = run(ContentChecksum::EmitOnly).unwrap(); + assert!(content == fixture.content); + assert_eq!(from_data, Some(stored)); + assert!(calculated.is_some_and(|calculated| calculated != stored)); + let (content, _, calculated) = run(ContentChecksum::None).unwrap(); + assert!(content == fixture.content); + assert_eq!(calculated, None); + + // The same on the whole-buffer path. + let mut out = vec![0u8; fixture.content.len()]; + let mut decoder = FrameDecoder::new(); + decoder.set_content_checksum(ContentChecksum::Verify); + assert!(decoder.decode_all(&frame, &mut out).is_err()); + for mode in [ContentChecksum::EmitOnly, ContentChecksum::None] { + decoder.set_content_checksum(mode); + assert_eq!( + decoder.decode_all(&frame, &mut out).unwrap(), + fixture.content.len() + ); + } +} + +/// A block limit set on the decoder holds these frames' blocks to it, on +/// both paths. +#[cfg(feature = "std")] +#[test] +fn the_block_limit_applies_to_these_frames() { + use crate::decoding::errors::FrameDecoderError; + use crate::decoding::{BlockDecodingStrategy, FrameDecoder}; + for name in [ + "z000000.v05.l1.zst", + "z000000.v06.l1.zst", + "z000000.v07.l1.zst", + ] { + let fixture = fixture(name); + let mut decoder = FrameDecoder::new(); + decoder.set_max_block_size(Some(1024)); + let mut out = vec![0u8; fixture.content.len()]; + assert!( + matches!( + decoder.decode_all(&fixture.frame, &mut out), + Err(FrameDecoderError::LegacyFrame(_)) + ), + "{name}" + ); + let mut src = &fixture.frame[..]; + decoder.reset(&mut src).unwrap(); + assert!( + matches!( + decoder.decode_blocks(&mut src, BlockDecodingStrategy::All), + Err(FrameDecoderError::LegacyFrame(_)) + ), + "{name}" + ); + decoder.set_max_block_size(None); + assert_eq!( + decoder.decode_all(&fixture.frame, &mut out).unwrap(), + out.len(), + "{name}" + ); + } +} + +/// A window ceiling lowered under a started frame's window refuses its next +/// step, as it refuses an RFC 8878 frame's. +#[cfg(feature = "std")] +#[test] +fn a_lowered_window_ceiling_binds_a_started_frame() { + use crate::decoding::errors::FrameDecoderError; + use crate::decoding::{BlockDecodingStrategy, FrameDecoder}; + let fixture = fixture("z000000.v07.l1.zst"); + let window = read_header(&fixture.frame).unwrap().window; + let mut decoder = FrameDecoder::new(); + let mut src = &fixture.frame[..]; + decoder.reset(&mut src).unwrap(); + decoder.set_max_window_size(window / 2).unwrap(); + assert!(matches!( + decoder.decode_blocks(&mut src, BlockDecodingStrategy::All), + Err(FrameDecoderError::WindowSizeTooBig { requested, limit }) + if requested == window && limit == window / 2 + )); + let mut target = [0u8; 64]; + assert!(matches!( + decoder.decode_from_to(src, &mut target), + Err(FrameDecoderError::WindowSizeTooBig { .. }) + )); + // Raised again, the frame decodes on. + decoder.set_max_window_size(window).unwrap(); + decoder + .decode_blocks(&mut src, BlockDecodingStrategy::All) + .unwrap(); + assert!(decoder.collect().unwrap() == fixture.content); + + // A ceiling below the window refuses the frame from its header. + decoder.set_max_window_size(window / 2).unwrap(); + assert!(matches!( + decoder.reset(&fixture.frame[..]), + Err(FrameDecoderError::WindowSizeTooBig { requested, limit }) + if requested == window && limit == window / 2 + )); +} + +/// The decoder's accessors describe a started frame of these formats: its +/// declared content size, and the blocks decoded so far, not what the frame +/// before left. +#[cfg(feature = "std")] +#[test] +fn the_accessors_describe_these_frames() { + use crate::decoding::{BlockDecodingStrategy, FrameDecoder}; + let rfc = crate::encoding::compress_slice_to_vec( + &[7u8; 500], + crate::encoding::CompressionLevel::Fastest, + ); + let mut decoder = FrameDecoder::new(); + let mut src = &rfc[..]; + decoder.reset(&mut src).unwrap(); + decoder + .decode_blocks(&mut src, BlockDecodingStrategy::All) + .unwrap(); + assert_eq!(decoder.content_size(), 500); + + // v0.6.2 declares its first chunk's size. + let v06 = fixture("z000000.v06.l1.zst"); + let mut src = &v06.frame[..]; + decoder.reset(&mut src).unwrap(); + assert_eq!(decoder.content_size(), 128 * 1024); + assert_eq!(decoder.blocks_decoded(), 0); + decoder + .decode_blocks(&mut src, BlockDecodingStrategy::All) + .unwrap(); + let (_, bound) = frame_size(LegacyVersion::V06, &v06.frame).unwrap(); + assert_eq!( + decoder.blocks_decoded() as u64, + bound / BLOCK_SIZE_MAX as u64 + ); + // The stream's buffers, past a block of output, count toward the + // decoder's footprint. + assert!(decoder.workspace_size() > BLOCK_SIZE_MAX); + + // v0.5 declares none. + let v05 = fixture("abc.txt.v05.l1.zst"); + let mut src = &v05.frame[..]; + decoder.reset(&mut src).unwrap(); + assert_eq!(decoder.content_size(), 0); +} + +/// Each failure carries the code libzstd returns for it, and keeps that code, +/// its message and itself as the source when the frame decoder wraps it. +#[cfg(feature = "std")] +#[test] +fn errors_report_libzstd_codes() { + use crate::decoding::errors::{ErrorCode, FrameDecoderError}; + use std::error::Error as _; + use std::string::ToString; + let cases = [ + ( + Error::Truncated, + ErrorCode::CorruptionDetected, + "ends inside", + ), + ( + Error::UnsupportedParameter, + ErrorCode::FrameParameterUnsupported, + "unsupported setting", + ), + ( + Error::Corrupted { + reason: "bad table", + }, + ErrorCode::CorruptionDetected, + "bad table", + ), + ( + Error::OutputTooSmall, + ErrorCode::DstSizeTooSmall, + "too small", + ), + ( + Error::DictionaryCorrupted, + ErrorCode::DictionaryCorrupted, + "dictionary could not be read", + ), + ( + Error::DictionaryMismatch { + expected: 0xAB, + provided: 0, + }, + ErrorCode::DictionaryWrong, + "dictionary 0xAB", + ), + ( + Error::ChecksumMismatch { + expected: 1, + calculated: 2, + }, + ErrorCode::ChecksumWrong, + "0x000001", + ), + ]; + for (err, code, text) in cases { + assert_eq!(err.code(), code, "{err:?}"); + let message = err.to_string(); + assert!(message.contains(text), "{message}"); + let wrapped = FrameDecoderError::from(err); + assert_eq!(wrapped.code(), code); + assert_eq!(wrapped.to_string(), message); + assert!( + wrapped + .source() + .is_some_and(|source| source.to_string() == message) + ); + } +} + +/// After `decode_all` the accessors describe the frame it decoded last, as +/// they do an RFC 8878 one, not the frame decoded before it. +#[cfg(feature = "std")] +#[test] +fn the_accessors_describe_a_frame_decoded_whole() { + use crate::decoding::FrameDecoder; + let rfc = crate::encoding::compress_slice_to_vec( + &[7u8; 500], + crate::encoding::CompressionLevel::Fastest, + ); + let v06 = fixture("z000000.v06.l1.zst"); + let mut decoder = FrameDecoder::new(); + let mut out = vec![0u8; v06.content.len()]; + decoder.decode_all(&rfc, &mut out).unwrap(); + assert_eq!(decoder.content_size(), 500); + + assert_eq!( + decoder.decode_all(&v06.frame, &mut out).unwrap(), + v06.content.len() + ); + let (_, bound) = frame_size(LegacyVersion::V06, &v06.frame).unwrap(); + assert_eq!(decoder.content_size(), 128 * 1024); + assert_eq!( + decoder.blocks_decoded() as u64, + bound / BLOCK_SIZE_MAX as u64 + ); + assert_eq!(decoder.bytes_read_from_source(), v06.frame.len() as u64); + assert!(decoder.is_finished()); + assert_eq!(decoder.can_collect(), 0); + + #[cfg(feature = "hash")] + { + let v07 = fixture("abc.txt.v07.l1.zst"); + let end = &v07.frame[v07.frame.len() - 3..]; + let stored = u32::from(end[2]) | u32::from(end[1]) << 8 | u32::from(end[0] & 0x3F) << 16; + decoder.decode_all(&v07.frame, &mut out).unwrap(); + assert_eq!(decoder.get_checksum_from_data(), Some(stored)); + assert_eq!(decoder.get_calculated_checksum(), Some(stored)); + } +} + +/// Per-block checksums cover these frames' blocks on every path, as they +/// cover an RFC 8878 frame's. +#[cfg(all(feature = "std", feature = "lsm", feature = "hash"))] +#[test] +fn per_block_checksums_cover_these_frames() { + use crate::decoding::{BlockDecodingStrategy, FrameDecoder}; + // One data block: its digest is the content's. + let one = fixture("abc.txt.v07.l1.zst"); + let digest = twox_hash::XxHash64::oneshot(0, &one.content) as u32; + let many = fixture("z000000.v07.l1.zst"); + let (_, bound) = frame_size(LegacyVersion::V07, &many.frame).unwrap(); + let blocks = (bound / BLOCK_SIZE_MAX as u64) as usize; + + let mut decoder = FrameDecoder::new(); + decoder.enable_per_block_checksums(); + let mut out = vec![0u8; many.content.len()]; + decoder.decode_all(&one.frame, &mut out).unwrap(); + assert_eq!(decoder.computed_block_checksums(), [digest]); + decoder.decode_all(&many.frame, &mut out).unwrap(); + assert_eq!(decoder.computed_block_checksums().len(), blocks); + + let mut src = &one.frame[..]; + decoder.reset(&mut src).unwrap(); + decoder + .decode_blocks(&mut src, BlockDecodingStrategy::All) + .unwrap(); + assert_eq!(decoder.computed_block_checksums(), [digest]); +} + +/// A pinned dictionary ID or window descriptor holds these frames to it, on +/// both paths: v0.7 carries both fields, v0.5 and v0.6 neither. +#[cfg(all(feature = "std", feature = "lsm"))] +#[test] +fn pinned_header_fields_bind_these_frames() { + use crate::decoding::FrameDecoder; + use crate::decoding::errors::FrameDecoderError; + let v07 = fixture("z000000x8.v07.l1.zst"); + // Not single-segment: the window byte follows the descriptor. + assert_eq!(v07.frame[4] & 0x20, 0); + let window = v07.frame[5]; + let v05 = fixture("abc.txt.v05.l1.zst"); + let mut out = vec![0u8; v07.content.len()]; + + let mut decoder = FrameDecoder::new(); + decoder.expect_dict_id(Some(5)); + assert!(matches!( + decoder.decode_all(&v07.frame, &mut out), + Err(FrameDecoderError::UnexpectedDictId { + expected: Some(5), + found: None + }) + )); + assert!(matches!( + decoder.reset(&v07.frame[..]), + Err(FrameDecoderError::UnexpectedDictId { .. }) + )); + decoder.expect_dict_id(Some(0)); + decoder.expect_window_descriptor(Some(window ^ 1)); + assert!(matches!( + decoder.decode_all(&v07.frame, &mut out), + Err(FrameDecoderError::UnexpectedWindowDescriptor { found: Some(w), .. }) if w == window + )); + assert!(matches!( + decoder.reset(&v05.frame[..]), + Err(FrameDecoderError::UnexpectedWindowDescriptor { found: None, .. }) + )); + decoder.expect_window_descriptor(Some(window)); + assert_eq!( + decoder.decode_all(&v07.frame, &mut out).unwrap(), + v07.content.len() + ); +} + +/// A v0.5-v0.7 magic number with nothing after it is a truncated frame to +/// the aggregate size query, as upstream's `ZSTD_findDecompressedSize` +/// refuses a tail shorter than a frame's first five bytes. +#[cfg(feature = "std")] +#[test] +fn a_bare_magic_number_is_a_truncated_frame() { + use crate::decoding::find_decompressed_size; + let magic = [0x27, 0xB5, 0x2F, 0xFD]; + assert!(find_decompressed_size(&magic).is_err()); + let mut stream = fixture("z000000.v07.l1.zst").frame; + stream.extend_from_slice(&magic); + assert!(find_decompressed_size(&stream).is_err()); +} + +/// A reader failing after the magic number hands its own error back, in the +/// header field it failed in, rather than calling the frame truncated. +#[cfg(feature = "std")] +#[test] +fn reader_errors_inside_the_header_are_kept() { + use crate::decoding::FrameDecoder; + use crate::decoding::errors::{FrameDecoderError, ReadFrameHeaderError}; + struct Stalls<'a>(&'a [u8]); + impl std::io::Read for Stalls<'_> { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + if self.0.is_empty() { + return Err(std::io::ErrorKind::WouldBlock.into()); + } + let n = buf.len().min(self.0.len()); + buf[..n].copy_from_slice(&self.0[..n]); + self.0 = &self.0[n..]; + Ok(n) + } + } + let mut decoder = FrameDecoder::new(); + let err = decoder + .reset(Stalls(&[0x27, 0xB5, 0x2F, 0xFD])) + .unwrap_err(); + assert!( + matches!( + &err, + FrameDecoderError::ReadFrameHeaderError(ReadFrameHeaderError::FrameDescriptorReadError(e)) + if e.kind() == std::io::ErrorKind::WouldBlock + ), + "{err:?}" + ); + // v0.7 with a window byte still to come. + let err = decoder + .reset(Stalls(&[0x27, 0xB5, 0x2F, 0xFD, 0x00])) + .unwrap_err(); + assert!( + matches!( + &err, + FrameDecoderError::ReadFrameHeaderError(ReadFrameHeaderError::WindowDescriptorReadError(e)) + if e.kind() == std::io::ErrorKind::WouldBlock + ), + "{err:?}" + ); + // v0.7 with its window byte read and a one-byte dictionary ID to come. + let err = decoder + .reset(Stalls(&[0x27, 0xB5, 0x2F, 0xFD, 0x01, 0x00])) + .unwrap_err(); + assert!( + matches!( + &err, + FrameDecoderError::ReadFrameHeaderError(ReadFrameHeaderError::DictionaryIdReadError(e)) + if e.kind() == std::io::ErrorKind::WouldBlock + ), + "{err:?}" + ); + // v0.6 with a two-byte content size still to come. + let err = decoder + .reset(Stalls(&[0x26, 0xB5, 0x2F, 0xFD, 0x80, 1])) + .unwrap_err(); + assert!( + matches!( + &err, + FrameDecoderError::ReadFrameHeaderError(ReadFrameHeaderError::FrameContentSizeReadError(e)) + if e.kind() == std::io::ErrorKind::WouldBlock + ), + "{err:?}" + ); +} + +/// The stream counts in its footprint the entropy tables and literal buffer +/// its frame holds, not only its output buffer. +#[cfg(feature = "std")] +#[test] +fn the_stream_footprint_counts_its_tables() { + for name in [ + "z000000.v05.l1.zst", + "z000000.v06.l1.zst", + "z000000.v07.l1.zst", + ] { + let fixture = fixture(name); + let header = read_header(&fixture.frame).unwrap(); + let mut stream = LegacyStream::new(); + stream.start(header, None, FrameOptions::default()).unwrap(); + let mut target = vec![0u8; fixture.content.len()]; + stream + .decode(&fixture.frame[header.size..], &mut target) + .unwrap(); + assert!( + stream.heap_bytes() > stream.buf.capacity() + stream.block.capacity(), + "{name}: {} bytes held, {} of them the output buffer", + stream.heap_bytes(), + stream.buf.capacity() + ); + } +} + +/// A block whose header declares more than the largest block is refused from +/// the header alone: the stream asks for no more than it. +#[test] +fn an_oversized_block_is_refused_from_its_header() { + // v0.7 with a 1 KiB window. + let frame = [0x27, 0xB5, 0x2F, 0xFD, 0x00, 0x00]; + let header = read_header(&frame).unwrap(); + let mut stream = LegacyStream::new(); + stream.start(header, None, FrameOptions::default()).unwrap(); + // Less than a block header asks for the header. + assert_eq!(stream.input_needed(&[0x40]), 3); + // A raw block of 0x3FFFF bytes, compressed and raw alike. + for kind in [0x00u8, 0x40] { + let block = [kind | 0x03, 0xFF, 0xFF]; + assert_eq!(stream.input_needed(&block), 3); + assert!(stream.decode(&block, &mut [0u8; 16]).is_err()); + } +} + +/// A v0.7 block whose sequence tables are each one symbol decodes, and one +/// naming a symbol past its field is refused. +#[test] +fn one_symbol_sequence_tables_decode_in_v07() { + // Raw literals "a"; one sequence; every table one symbol: literal length + // code 1, offset code 2 (two extra bits, here 0, so offset 1), match + // length code 0 (3 bytes). The bitstream holds those two bits under its + // end mark. + let block = [0x81, b'a', 0x01, 0x54, 1, 2, 0, 0x04]; + let mut frame = vec![0x27, 0xB5, 0x2F, 0xFD, 0x00, 0x00, 0x00, 0x00]; + frame.push(block.len() as u8); + frame.extend_from_slice(&block); + frame.extend_from_slice(&[0xC0, 0, 0]); + assert_eq!(decode_to_vec(&frame, LegacyDict::None).unwrap().0, b"aaaa"); + // A match length symbol past the field's largest. + let at = frame.len() - 4; + frame[at - 1] = 200; + assert!(matches!( + decode_to_vec(&frame, LegacyDict::None), + Err(Error::Corrupted { .. }) + )); +} + +/// A sequence count takes one byte below 0x80, two up to 0x7EFF, and three +/// after a 0xFF where the format allows them. +#[test] +fn sequence_counts_take_their_forms() { + assert_eq!(v07::sequence_count(&[0x7F], true).unwrap(), (0x7F, 1)); + assert_eq!( + v07::sequence_count(&[0x81, 0x02], true).unwrap(), + (0x102, 2) + ); + assert_eq!( + v07::sequence_count(&[0xFF, 0x01, 0x00], true).unwrap(), + (0x7F01, 3) + ); + assert_eq!( + v07::sequence_count(&[0xFF, 0x01], false).unwrap(), + (0x7F01, 2) + ); + assert!(v07::sequence_count(&[], true).is_err()); + assert!(v07::sequence_count(&[0xFF, 0x01], true).is_err()); + assert!(v07::sequence_count(&[0x80], true).is_err()); +} + +/// The version queries: each format's minor version, and a header's length +/// from its size fields once the descriptor is in hand. +#[test] +fn versions_answer_their_queries() { + assert_eq!(LegacyVersion::V05.minor(), 5); + assert_eq!(LegacyVersion::V06.minor(), 6); + assert_eq!(LegacyVersion::V07.minor(), 7); + let v05 = [0x25, 0xB5, 0x2F, 0xFD, 0x01]; + assert_eq!(LegacyVersion::V05.header_size(&v05[..4]), None); + assert_eq!(LegacyVersion::V05.header_size(&v05), Some(5)); + // v0.6 with a two-byte content size. + assert_eq!( + LegacyVersion::V06.header_size(&[0x26, 0xB5, 0x2F, 0xFD, 0x80]), + Some(7) + ); + // v0.7, single-segment with an eight-byte content size and a four-byte + // dictionary ID. + assert_eq!( + LegacyVersion::V07.header_size(&[0x27, 0xB5, 0x2F, 0xFD, 0xE3]), + Some(17) + ); +} + +/// A literal section header the block cannot hold, or one past the largest +/// block, is refused before anything decodes. +#[test] +fn literal_headers_refuse_what_does_not_fit() { + for header in [ + // Huffman literals with less than their five-byte minimum. + &[0x00, 0, 0, 0][..], + // Huffman literals declaring more than a block. + &[0x3F, 0xFF, 0xFC, 0, 0, 0], + // Huffman literals running past the block. + &[0x00, 0x04, 0xFF, 0, 0], + // Repeated-table literals in a form other than the small one. + &[0x40, 0, 0], + // Repeated-table literals running past the block. + &[0x50, 0x03, 0xFF], + // Raw literals running past the block. + &[0x9F, 0, 0], + // A run with a three-byte header and nothing after it. + &[0xF0, 0, 0], + // A run longer than a block. + &[0xFF, 0xFF, 0xFF, 0], + // Less than any literal section. + &[0x80, 0], + ] { + assert!( + matches!(Literals::read(header), Err(Error::Corrupted { .. })), + "{header:02X?}" + ); + } +} + +/// A block's budget passes everything but its size check to the output it +/// guards. +#[cfg(feature = "hash")] +#[test] +fn a_budget_passes_through_to_its_output() { + let mut buf = Vec::new(); + let mut inner = VecOutput { + buf: &mut buf, + start: 0, + }; + let mut out = Budget { + out: &mut inner, + end: 8, + }; + out.reserve(2).unwrap(); + out.extend(b"ab"); + out.repeat(b'c', 2); + out.copy_back(4, 2); + assert_eq!((out.produced(), out.held()), (6, 6)); + assert_eq!(out.since(2), b"ccab"); + assert!(out.reserve(3).is_err()); + out.copy_match(1, 2, &[]).unwrap(); + assert_eq!(buf, b"abccabbb"); +} + +/// The size walk refuses a frame cut inside a block's body. +#[cfg(feature = "std")] +#[test] +fn frame_size_refuses_a_cut_block() { + let fixture = fixture("z000000.v07.l1.zst"); + let frame = &fixture.frame[..fixture.frame.len() - 10]; + assert_eq!(frame_size(LegacyVersion::V07, frame), None); +} + +/// An RLE block decodes into a caller's slice and through a stream as it +/// does into a vector. +#[cfg(feature = "std")] +#[test] +fn rle_blocks_decode_on_every_path() { + use crate::decoding::FrameDecoder; + let frame = [ + 0x27, 0xB5, 0x2F, 0xFD, 0x00, 0x00, 0x80, 0, 5, b'z', 0xC0, 0, 0, + ]; + let mut out = [0u8; 5]; + assert_eq!(FrameDecoder::new().decode_all(&frame, &mut out).unwrap(), 5); + assert_eq!(&out, b"zzzzz"); + let mut decoder = FrameDecoder::new(); + let mut target = [0u8; 8]; + let (read, written) = decoder.decode_from_to(&frame, &mut target).unwrap(); + assert_eq!((read, &target[..written]), (frame.len(), &b"zzzzz"[..])); +} + +/// A `Read` source failing inside a block hands back its own error, in the +/// block header or the body it failed in. +#[cfg(feature = "std")] +#[test] +fn reader_errors_inside_a_block_are_kept() { + use crate::decoding::errors::FrameDecoderError; + use crate::decoding::{BlockDecodingStrategy, FrameDecoder}; + let fixture = fixture("z000000.v07.l1.zst"); + let header = read_header(&fixture.frame).unwrap(); + for (cut, header_error) in [(header.size + 1, true), (header.size + 4, false)] { + let mut decoder = FrameDecoder::new(); + let mut src = &fixture.frame[..cut]; + decoder.reset(&mut src).unwrap(); + let err = decoder + .decode_blocks(&mut src, BlockDecodingStrategy::All) + .unwrap_err(); + if header_error { + assert!( + matches!(err, FrameDecoderError::FailedToReadBlockHeader(_)), + "{err:?}" + ); + } else { + assert!( + matches!(err, FrameDecoderError::FailedToReadBlockBody(_)), + "{err:?}" + ); + } + } +} + +/// `read_to_end` over an RFC 8878 frame and then a v0.7 one decodes both +/// with the dictionary the reader was forced to, and hands out nothing of a +/// v0.7 frame that fails. +#[cfg(feature = "std")] +#[test] +fn read_to_end_keeps_a_forced_dictionary_for_these_frames() { + use crate::decoding::{DictionaryHandle, StreamingDecoder}; + use std::io::Read; + let v07 = fixture("ModemManager.service.v07.dict.zst"); + let dict = + DictionaryHandle::from_serialized_or_raw_content(v07.dict.as_deref().unwrap()).unwrap(); + let leading = b"a leading frame"; + let mut input = + crate::encoding::compress_slice_to_vec(leading, crate::encoding::CompressionLevel::Fastest); + let rfc_len = input.len(); + input.extend_from_slice(&v07.frame); + let mut out = Vec::new(); + StreamingDecoder::new_with_dictionary_handle(&input[..], &dict) + .unwrap() + .read_to_end(&mut out) + .unwrap(); + let mut expected = leading.to_vec(); + expected.extend_from_slice(&v07.content); + assert!(out == expected); + + // The v0.7 frame cut short: its bytes are not handed out. + let cut = &input[..input.len() - 1]; + assert!(cut.len() > rfc_len); + let mut out = Vec::new(); + assert!( + StreamingDecoder::new_with_dictionary_handle(cut, &dict) + .unwrap() + .read_to_end(&mut out) + .is_err() + ); + assert_eq!(out, leading); +} + +/// `force_dict` gives a started v0.7 frame that names no dictionary the +/// registered one, as it does an RFC 8878 frame, and `dictionary()` reports +/// the handle the frame decodes with. +#[cfg(all(feature = "std", target_has_atomic = "ptr"))] +#[test] +fn force_dict_and_dictionary_cover_these_frames() { + use crate::decoding::{BlockDecodingStrategy, DictionaryHandle, FrameDecoder}; + let noid = fixture("ModemManager.service.v07.dict-noid.zst"); + let dict = + DictionaryHandle::from_serialized_or_raw_content(noid.dict.as_deref().unwrap()).unwrap(); + let id = dict.id(); + let mut decoder = FrameDecoder::new(); + decoder.add_dict_handle(dict).unwrap(); + + let mut src = &noid.frame[..]; + decoder.reset(&mut src).unwrap(); + assert!(decoder.dictionary().is_none()); + decoder.force_dict(id).unwrap(); + assert_eq!(decoder.dictionary().map(DictionaryHandle::id), Some(id)); + decoder + .decode_blocks(&mut src, BlockDecodingStrategy::All) + .unwrap(); + assert!(decoder.collect().unwrap() == noid.content); + + // A frame naming the dictionary takes it from the registry. + let named = fixture("ModemManager.service.v07.dict.zst"); + decoder.reset(&named.frame[..]).unwrap(); + assert_eq!(decoder.dictionary().map(DictionaryHandle::id), Some(id)); +} + +/// The header size query measures these frames by their own size fields. +#[test] +fn frame_header_size_measures_these_frames() { + use crate::decoding::frame_header_size; + assert_eq!( + frame_header_size(&[0x25, 0xB5, 0x2F, 0xFD, 0x01]).unwrap(), + 5 + ); + assert_eq!( + frame_header_size(&[0x26, 0xB5, 0x2F, 0xFD, 0x80]).unwrap(), + 7 + ); + assert_eq!( + frame_header_size(&[0x27, 0xB5, 0x2F, 0xFD, 0xE3]).unwrap(), + 17 + ); + assert!(frame_header_size(&[0x27, 0xB5, 0x2F, 0xFD]).is_err()); +} + +/// A reader failing inside a raw or RLE block's body names that block's +/// type, not a compressed block's. +#[cfg(feature = "std")] +#[test] +fn reader_errors_name_the_block_type() { + use crate::blocks::block::BlockType; + use crate::decoding::errors::{DecodeBlockContentError, FrameDecoderError}; + use crate::decoding::{BlockDecodingStrategy, FrameDecoder}; + // v0.7, a raw block of five bytes cut after two. + let frame = [0x27, 0xB5, 0x2F, 0xFD, 0x00, 0x00, 0x40, 0, 5, b'a', b'b']; + let mut decoder = FrameDecoder::new(); + let mut src = &frame[..]; + decoder.reset(&mut src).unwrap(); + let err = decoder + .decode_blocks(&mut src, BlockDecodingStrategy::All) + .unwrap_err(); + assert!( + matches!( + err, + FrameDecoderError::FailedToReadBlockBody(DecodeBlockContentError::ReadError { + step: BlockType::Raw, + .. + }) + ), + "{err:?}" + ); +} + +/// Output a caller does not collect stays in the buffer however far past the +/// window it grows, and all of it is handed out once collected. +#[cfg(feature = "std")] +#[test] +fn output_held_back_grows_the_buffer() { + use crate::decoding::{BlockDecodingStrategy, FrameDecoder}; + let fixture = fixture("z000000x8.v07.l1.zst"); + let mut decoder = FrameDecoder::new(); + let mut src = &fixture.frame[..]; + decoder.reset(&mut src).unwrap(); + decoder + .decode_blocks(&mut src, BlockDecodingStrategy::All) + .unwrap(); + assert!(decoder.collect().unwrap() == fixture.content); +} + +/// Damaged frames are refused or decode to some output, but never panic, +/// wherever the damage lands: random bytes overwritten, through every format +/// and both decoding paths. +#[cfg(feature = "std")] +#[test] +fn overwritten_bytes_never_panic() { + use crate::decoding::FrameDecoder; + let mut state = 0x2545_F491_4F6C_DD1Du64; + let mut next = move || { + state ^= state << 13; + state ^= state >> 7; + state ^= state << 17; + state + }; + for fixture in fixtures().into_iter().filter(|f| f.frame.len() < 16384) { + let dict = fixture.legacy_dict(); + let header = read_header(&fixture.frame).unwrap().size; + for _ in 0..64 { + let mut frame = fixture.frame.clone(); + for _ in 0..(next() % 4) + 1 { + let at = header + (next() as usize) % (frame.len() - header); + frame[at] = next() as u8; + } + let _whole = decode_verified(&frame, dict).is_ok(); + let mut decoder = FrameDecoder::new(); + let mut target = vec![0u8; 4096]; + let mut src = &frame[..]; + while let Ok((read, written)) = decoder.decode_from_to(src, &mut target) { + if read == 0 && written == 0 { + break; + } + src = &src[read..]; + } + } + } +} + +/// An RLE block repeats its byte in v0.7; v0.5 and v0.6 reserve the type, +/// and their decoders refuse it (`ZSTDv0{5,6}_decompressFrame`). +#[test] +fn rle_blocks_repeat_their_byte_in_v07_only() { + for magic in [0x25u8, 0x26, 0x27] { + let mut frame = vec![magic, 0xB5, 0x2F, 0xFD, 0x00]; + if magic == 0x27 { + frame.push(0x00); + } + frame.extend_from_slice(&[0x80, 0, 5, b'z', 0xC0, 0, 0]); + let decoded = decode_to_vec(&frame, LegacyDict::None); + if magic == 0x27 { + let (decoded, consumed) = decoded.unwrap(); + assert_eq!(decoded, b"zzzzz"); + assert_eq!(consumed, frame.len()); + } else { + assert!( + matches!(decoded, Err(Error::Corrupted { .. })), + "{magic:#x}: {decoded:?}" + ); + } + } +} + +/// A block may not decode past the largest block size, whatever its header +/// claims. +#[test] +fn a_block_past_the_largest_size_is_refused() { + let mut frame = vec![0x27, 0xB5, 0x2F, 0xFD, 0x00, 0x00]; + // An RLE block of 128 KiB + 1. + let size = BLOCK_SIZE_MAX + 1; + frame.extend_from_slice(&[ + 0x80 | (size >> 16) as u8, + (size >> 8) as u8, + size as u8, + b'z', + ]); + frame.extend_from_slice(&[0xC0, 0, 0]); + assert!(matches!( + decode_to_vec(&frame, LegacyDict::None), + Err(Error::Corrupted { .. }) + )); +} + +/// The declared content size does not bound the output: the v0.6.2 command +/// line declares its first chunk's size, and the blocks are what decode. +#[cfg(feature = "std")] +#[test] +fn the_declared_content_size_is_not_enforced() { + // v0.6 with a one-byte content size of 4 and a raw block of 5. + let frame = [ + 0x26, 0xB5, 0x2F, 0xFD, 0x40, 4, 0x40, 0, 5, b'z', b'z', b'z', b'z', b'z', 0xC0, 0, 0, + ]; + assert_eq!(declared_content_size(&frame), Some(4)); + assert_eq!( + decode_to_vec(&frame, LegacyDict::None).unwrap(), + (b"zzzzz".to_vec(), frame.len()) + ); + // A v0.6 archive from that command line, past 128 KiB, declares 128 KiB. + let fixture = fixtures() + .into_iter() + .find(|f| f.name == "z000000.v06.l1.zst") + .unwrap(); + assert_eq!(declared_content_size(&fixture.frame), Some(128 * 1024)); + assert_eq!(fixture.content.len(), 224_787); +} diff --git a/zstd/src/decoding/legacy/v05.rs b/zstd/src/decoding/legacy/v05.rs new file mode 100644 index 00000000..389ebce2 --- /dev/null +++ b/zstd/src/decoding/legacy/v05.rs @@ -0,0 +1,462 @@ +//! The v0.5 format (upstream `lib/legacy/zstd_v05.c`): lengths coded as +//! symbols with long values in a separate "dumps" area, a single repeat +//! offset, and a minimum match of 4. + +use super::entropy::{FseState, FseTable, HufTable, Slot, StreamEnd, open_bits, overflowed}; +use super::v06::huffman; +use super::v07::{SeqTables, sequence_count}; +use super::{ + BLOCK_SIZE_MAX, FrameHeader, LegacyVersion, LiteralSource, Literals, Output, corrupt, execute, +}; +use crate::decoding::errors::LegacyFrameError as Error; +use alloc::vec::Vec; + +/// Magic number of a v0.5 dictionary. +const DICT_MAGIC: u32 = 0xEC30_A435; + +const LL_BITS: u32 = 6; +const ML_BITS: u32 = 7; +const OFF_BITS: u32 = 5; +const MAX_LL: usize = (1 << LL_BITS) - 1; +const MAX_ML: usize = (1 << ML_BITS) - 1; +const MAX_OFF: usize = (1 << OFF_BITS) - 1; +const LL_LOG: u32 = 10; +const ML_LOG: u32 = 10; +const OFF_LOG: u32 = 9; +const MIN_MATCH: usize = 4; + +/// Offset code `n` reads `n - 1` bits on top of this; code 0 is the repeat +/// offset, and codes 27 to 31 are not used. +const OFFSET_PREFIX: [usize; MAX_OFF + 1] = [ + 1, 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072, + 262144, 524288, 1048576, 2097152, 4194304, 8388608, 16777216, 33554432, 1, 1, 1, 1, 1, +]; + +/// Read a v0.5 frame header (`ZSTDv05_getFrameParams`): five bytes, no +/// content size. +pub(super) fn read_header(src: &[u8]) -> Result { + let &descriptor = src.get(4).ok_or(Error::Truncated)?; + let window_log = u32::from(descriptor & 0xF) + 11; + if descriptor >> 4 != 0 || (usize::BITS == 32 && window_log > 25) { + return Err(Error::UnsupportedParameter); + } + Ok(FrameHeader { + version: LegacyVersion::V05, + size: 5, + window: 1 << window_log, + content_size: None, + dict_id: 0, + checksum: false, + }) +} + +/// The decoding state a v0.5 frame carries from block to block. +pub(super) struct Decoder { + /// The dictionary's Huffman table, which repeat-table literals use. + dict_huf: Slot, + /// The table a block's own Huffman description is read into. + huf: HufTable, + tables: SeqTables, + /// Whether a block may keep the previous tables: once a dictionary has + /// been loaded (`flagStaticTables`). + repeat_tables: bool, + literals: Vec, +} + +impl Decoder { + pub(super) const fn empty() -> Decoder { + Decoder { + dict_huf: Slot::::empty(), + huf: HufTable::empty(), + tables: SeqTables::empty(), + repeat_tables: false, + literals: Vec::new(), + } + } + + /// Ready the decoder for a frame primed with the dictionary `bytes`, in + /// the storage the previous frame left, and say where its content lies in + /// them. + pub(super) fn reset(&mut self, bytes: &[u8]) -> Result, Error> { + self.dict_huf.set = false; + self.tables.unset(); + self.repeat_tables = false; + self.literals.clear(); + if bytes.len() < 4 + || u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) != DICT_MAGIC + { + return Ok(0..bytes.len()); + } + // `ZSTDv05_loadEntropy`. + let src = &bytes[4..]; + let bad = |_| Error::DictionaryCorrupted; + let mut at = self + .dict_huf + .table + .read_into(src, StreamEnd::Exact) + .map_err(bad)?; + for (slot, max, max_log) in [ + (&mut self.tables.of, MAX_OFF, OFF_LOG), + (&mut self.tables.ml, MAX_ML, ML_LOG), + (&mut self.tables.ll, MAX_LL, LL_LOG), + ] { + let (dist, len) = super::entropy::read_distribution( + src.get(at..).ok_or(Error::DictionaryCorrupted)?, + max, + ) + .map_err(bad)?; + if dist.table_log() > max_log { + return Err(Error::DictionaryCorrupted); + } + slot.table.build(&dist).map_err(bad)?; + slot.set = true; + at += len; + } + self.dict_huf.set = true; + self.repeat_tables = true; + Ok(4 + at..bytes.len()) + } + + /// Heap bytes the decoder holds. + pub(super) fn heap_bytes(&self) -> usize { + self.dict_huf.table.heap_bytes() + + self.huf.heap_bytes() + + self.tables.heap_bytes() + + self.literals.capacity() + } + + /// Decode one compressed block into `out` (`ZSTDv05_decompressBlock_internal`). + pub(super) fn block( + &mut self, + block: &[u8], + out: &mut O, + dict: &[u8], + ) -> Result<(), Error> { + if block.len() >= BLOCK_SIZE_MAX { + return Err(corrupt("compressed block too large")); + } + let (source, consumed) = self.literals(block)?; + // The buffer is lent out for the sequence pass, which also needs + // `self` for its tables, and handed back after it. + let buffer = core::mem::take(&mut self.literals); + let literals = match &source { + LiteralSource::Block(range) => &block[range.clone()], + LiteralSource::Buffer => &buffer[..], + }; + let result = self.sequences(&block[consumed..], literals, out, dict); + self.literals = buffer; + result + } + + fn literals(&mut self, block: &[u8]) -> Result<(LiteralSource, usize), Error> { + Ok(match Literals::read(block)? { + Literals::Huffman { + header, + size, + compressed, + single_stream, + } => { + let src = &block[header..header + compressed]; + self.literals.clear(); + if single_stream { + self.literals.resize(size, 0); + huffman( + &mut self.huf, + src, + &mut self.literals, + false, + StreamEnd::Exact, + )?; + } else { + // `HUFv05_decompress`: an uncompressed section is not + // Huffman literals here, a one-byte section is a run. + if size == 0 || compressed >= size { + return Err(corrupt("Huffman literals no smaller than their content")); + } + if compressed == 1 { + self.literals.resize(size, src[0]); + } else { + self.literals.resize(size, 0); + huffman( + &mut self.huf, + src, + &mut self.literals, + true, + StreamEnd::Exact, + )?; + } + } + (LiteralSource::Buffer, header + compressed) + } + Literals::Repeat { size, compressed } => { + if !self.repeat_tables { + return Err(Error::DictionaryCorrupted); + } + let table = self.dict_huf.get().ok_or(Error::DictionaryCorrupted)?; + self.literals.clear(); + self.literals.resize(size, 0); + table + .decode_one_stream(&block[3..3 + compressed], &mut self.literals) + .map_err(|_| corrupt("bad Huffman literals"))?; + (LiteralSource::Buffer, 3 + compressed) + } + Literals::Raw { header, size } => { + (LiteralSource::Block(header..header + size), header + size) + } + Literals::Rle { header, size } => { + self.literals.clear(); + self.literals.resize(size, block[header]); + (LiteralSource::Buffer, header + 1) + } + }) + } + + fn sequences( + &mut self, + src: &[u8], + literals: &[u8], + out: &mut O, + dict: &[u8], + ) -> Result<(), Error> { + let (count, mut at) = sequence_count(src, false)?; + let mut lit_pos = 0; + if count > 0 { + // `ZSTDv05_decodeSeqHeaders`: the field modes, then the dumps. + let &modes = src.get(at).ok_or(Error::Truncated)?; + let dumps_len; + if modes & 2 != 0 { + let bytes = src.get(at + 1..at + 3).ok_or(Error::Truncated)?; + dumps_len = usize::from(bytes[1]) + (usize::from(bytes[0]) << 8); + at += 3; + } else { + let &low = src.get(at + 1).ok_or(Error::Truncated)?; + dumps_len = usize::from(low) + (usize::from(modes & 1) << 8); + at += 2; + } + let dumps_start = at; + at += dumps_len; + if at + 3 > src.len() { + return Err(Error::Truncated); + } + let dumps_end = at; + let repeat = self.repeat_tables; + at += field_table( + &mut self.tables.ll, + modes >> 6, + src, + at, + Field::LITERAL_LENGTH, + repeat, + )?; + at += field_table( + &mut self.tables.of, + (modes >> 4) & 3, + src, + at, + Field::OFFSET, + repeat, + )?; + at += field_table( + &mut self.tables.ml, + (modes >> 2) & 3, + src, + at, + Field::MATCH_LENGTH, + repeat, + )?; + let (ll, of, ml) = self.tables.all()?; + + let mut bits = open_bits(&src[at..]).map_err(|_| corrupt("bad sequence bitstream"))?; + let mut ll_state = FseState::new(ll, &mut bits); + let mut of_state = FseState::new(of, &mut bits); + let mut ml_state = FseState::new(ml, &mut bits); + let mut dumps = Dumps { + src, + at: dumps_start, + end: dumps_end, + }; + // The previous sequence's offset and the one before a literal- + // free sequence, both starting at 1 in every block. + let mut last_offset = 1usize; + let mut prev_offset = 1usize; + for _ in 0..count { + if overflowed(&bits) { + return Err(corrupt("sequence bitstream ends early")); + } + // `ZSTDv05_decodeSequence`. + let mut lit_len = usize::from(ll_state.symbol(ll)); + let repeat_offset = if lit_len != 0 { + last_offset + } else { + prev_offset + }; + if lit_len == MAX_LL { + lit_len = dumps.extend(lit_len, true); + } + let code = usize::from(of_state.symbol(of)); + let extra = if code == 0 { 0 } else { code as u8 - 1 }; + let mut offset = OFFSET_PREFIX[code] + bits.get_bits(extra) as usize; + if code == 0 { + offset = repeat_offset; + } + if code != 0 || lit_len == 0 { + prev_offset = last_offset; + } + of_state.update(of, &mut bits); + ll_state.update(ll, &mut bits); + let mut match_len = usize::from(ml_state.symbol(ml)); + ml_state.update(ml, &mut bits); + if match_len == MAX_ML { + match_len = dumps.extend(match_len, false); + } + match_len += MIN_MATCH; + last_offset = offset; + execute( + out, + literals, + &mut lit_pos, + lit_len, + match_len, + offset, + dict, + )?; + } + } + let rest = &literals[lit_pos..]; + out.reserve(rest.len())?; + out.extend(rest); + Ok(()) + } +} + +/// The largest symbol, largest table log and raw width of one sequence field. +struct Field { + max: usize, + max_log: u32, + raw_bits: u32, + /// Whether a one-symbol table's symbol is masked to the field and needs + /// a byte after it (offsets and match lengths), or taken as is (literal + /// lengths). + checked_rle: bool, +} + +impl Field { + const LITERAL_LENGTH: Field = Field { + max: MAX_LL, + max_log: LL_LOG, + raw_bits: LL_BITS, + checked_rle: false, + }; + const OFFSET: Field = Field { + max: MAX_OFF, + max_log: OFF_LOG, + raw_bits: OFF_BITS, + checked_rle: true, + }; + const MATCH_LENGTH: Field = Field { + max: MAX_ML, + max_log: ML_LOG, + raw_bits: ML_BITS, + checked_rle: true, + }; +} + +/// Set one field's table by its mode, the description starting at +/// `src[at..]` (`ZSTDv05_decodeSeqHeaders`). Returns the bytes it took. +fn field_table( + slot: &mut Slot, + mode: u8, + src: &[u8], + at: usize, + field: Field, + repeat: bool, +) -> Result { + if mode == 2 { + if !repeat || !slot.set { + return Err(corrupt("sequence table repeated before one was sent")); + } + return Ok(0); + } + // Built in the previous table's storage; left unset should the + // description be bad, so no later block repeats a half-built table. + slot.set = false; + let new = &mut slot.table; + let len = match mode { + // Every symbol read as raw bits. + 0 => { + new.set_raw(field.raw_bits); + 0 + } + 1 => { + if field.checked_rle && at + 2 > src.len() { + return Err(Error::Truncated); + } + let &symbol = src.get(at).ok_or(Error::Truncated)?; + // Only offsets are masked; a literal or match length symbol past + // its field is taken as it stands. + let symbol = if field.max == MAX_OFF { + symbol & MAX_OFF as u8 + } else { + symbol + }; + new.set_rle(symbol); + 1 + } + _ => { + let (dist, len) = super::entropy::read_distribution(&src[at..], field.max) + .map_err(|_| corrupt("bad sequence table"))?; + if dist.table_log() > field.max_log { + return Err(corrupt("sequence table too large")); + } + new.build(&dist)?; + len + } + }; + slot.set = true; + Ok(len) +} + +/// The dumps area, read forward for lengths past a field's largest symbol. +struct Dumps<'s> { + src: &'s [u8], + at: usize, + end: usize, +} + +impl Dumps<'_> { + /// The length a symbol of `base` (the field's largest) stands for: `base` + /// plus one dumps byte, or, when that byte is 255, a value of 16 or 24 + /// bits halved. Reads as upstream does, including its clamp back to the + /// last dumps byte; the area sits inside the sequence section, ahead of + /// at least three more bytes, so every read stays in `src`. + fn extend(&mut self, base: usize, literal: bool) -> usize { + // A literal length always takes its byte; a match length only while + // the area has one. + let add = if literal || self.at < self.end { + let byte = usize::from(self.src[self.at]); + self.at += 1; + byte + } else { + 0 + }; + let mut value = base; + if add < 255 { + value += add; + } else if self.at + 2 <= self.end { + value = usize::from(u16::from_le_bytes([ + self.src[self.at], + self.src[self.at + 1], + ])); + self.at += 2; + if value & 1 != 0 && self.at < self.end { + value += usize::from(self.src[self.at]) << 16; + self.at += 1; + } + value >>= 1; + } + if self.at >= self.end { + self.at = self.end - 1; + } + value + } +} diff --git a/zstd/src/decoding/legacy/v06.rs b/zstd/src/decoding/legacy/v06.rs new file mode 100644 index 00000000..0f66ddd0 --- /dev/null +++ b/zstd/src/decoding/legacy/v06.rs @@ -0,0 +1,351 @@ +//! The v0.6 format (upstream `lib/legacy/zstd_v06.c`): v0.7's coding without +//! a dictionary ID or checksum, repeat offsets that restart every block, and +//! offsets coded two higher. + +use super::entropy::{FseState, HufTable, Slot, StreamEnd, open_bits, overflowed}; +use super::v07::{ + LL_BASE, LL_BITS, LL_DEFAULT, LL_DEFAULT_LOG, ML_BASE, ML_BITS, ML_DEFAULT, ML_DEFAULT_LOG, + OF_DEFAULT, OF_DEFAULT_LOG, SeqTables, seq_table, sequence_count, +}; +use super::{ + BLOCK_SIZE_MAX, FrameHeader, LegacyVersion, LiteralSource, Literals, Output, corrupt, execute, +}; +use crate::decoding::errors::LegacyFrameError as Error; +use alloc::vec::Vec; + +/// Magic number of a v0.6 dictionary. +const DICT_MAGIC: u32 = 0xEC30_A436; + +const MAX_LL: usize = 35; +const MAX_ML: usize = 52; +const MAX_OFF: usize = 28; +const LL_LOG: u32 = 9; +const ML_LOG: u32 = 9; +const OFF_LOG: u32 = 8; + +/// Offset bases; codes 27 and 28 are not used and read as 1. +const OF_BASE: [u32; MAX_OFF + 1] = [ + 0, 1, 3, 7, 0xF, 0x1F, 0x3F, 0x7F, 0xFF, 0x1FF, 0x3FF, 0x7FF, 0xFFF, 0x1FFF, 0x3FFF, 0x7FFF, + 0xFFFF, 0x1FFFF, 0x3FFFF, 0x7FFFF, 0xFFFFF, 0x1FFFFF, 0x3FFFFF, 0x7FFFFF, 0xFFFFFF, 0x1FFFFFF, + 0x3FFFFFF, 1, 1, +]; + +/// Read a v0.6 frame header (`ZSTDv06_getFrameParams`). +pub(super) fn read_header(src: &[u8]) -> Result { + let &descriptor = src.get(4).ok_or(Error::Truncated)?; + let fcs_len = [0usize, 1, 2, 8][usize::from(descriptor >> 6)]; + let header = src.get(..5 + fcs_len).ok_or(Error::Truncated)?; + let window_log = u32::from(descriptor & 0xF) + 12; + if descriptor & 0x20 != 0 || (usize::BITS == 32 && window_log > 25) { + return Err(Error::UnsupportedParameter); + } + let fcs = &header[5..]; + let content_size = match descriptor >> 6 { + 0 => 0, + 1 => u64::from(fcs[0]), + 2 => u64::from(u16::from_le_bytes([fcs[0], fcs[1]])) + 256, + _ => u64::from_le_bytes(fcs.try_into().expect("eight bytes")), + }; + Ok(FrameHeader { + version: LegacyVersion::V06, + size: header.len(), + window: 1 << window_log, + content_size: (content_size != 0).then_some(content_size), + dict_id: 0, + checksum: false, + }) +} + +/// The decoding state a v0.6 frame carries from block to block. +pub(super) struct Decoder { + /// The dictionary's Huffman table, which repeat-table literals use. + dict_huf: Slot, + /// The table a block's own Huffman description is read into. + huf: HufTable, + tables: SeqTables, + /// Whether the next block may use the dictionary's tables: from loading a + /// dictionary until the first sequence section is read + /// (`flagRepeatTable`). + repeat_tables: bool, + literals: Vec, +} + +impl Decoder { + pub(super) const fn empty() -> Decoder { + Decoder { + dict_huf: Slot::::empty(), + huf: HufTable::empty(), + tables: SeqTables::empty(), + repeat_tables: false, + literals: Vec::new(), + } + } + + /// Ready the decoder for a frame primed with the dictionary `bytes`, in + /// the storage the previous frame left, and say where its content lies in + /// them. + pub(super) fn reset(&mut self, bytes: &[u8]) -> Result, Error> { + self.dict_huf.set = false; + self.tables.unset(); + self.repeat_tables = false; + self.literals.clear(); + if bytes.len() < 4 + || u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) != DICT_MAGIC + { + return Ok(0..bytes.len()); + } + // `ZSTDv06_loadEntropy`. + let src = &bytes[4..]; + let bad = |_| Error::DictionaryCorrupted; + let mut at = self + .dict_huf + .table + .read_into(src, StreamEnd::PastStart) + .map_err(bad)?; + for (slot, max, max_log) in [ + (&mut self.tables.of, MAX_OFF, OFF_LOG), + (&mut self.tables.ml, MAX_ML, ML_LOG), + (&mut self.tables.ll, MAX_LL, LL_LOG), + ] { + let (dist, len) = super::entropy::read_distribution( + src.get(at..).ok_or(Error::DictionaryCorrupted)?, + max, + ) + .map_err(bad)?; + if dist.table_log() > max_log { + return Err(Error::DictionaryCorrupted); + } + slot.table.build(&dist).map_err(bad)?; + slot.set = true; + at += len; + } + self.dict_huf.set = true; + self.repeat_tables = true; + Ok(4 + at..bytes.len()) + } + + /// Heap bytes the decoder holds. + pub(super) fn heap_bytes(&self) -> usize { + self.dict_huf.table.heap_bytes() + + self.huf.heap_bytes() + + self.tables.heap_bytes() + + self.literals.capacity() + } + + /// Decode one compressed block into `out` (`ZSTDv06_decompressBlock_internal`). + pub(super) fn block( + &mut self, + block: &[u8], + out: &mut O, + dict: &[u8], + ) -> Result<(), Error> { + if block.len() >= BLOCK_SIZE_MAX { + return Err(corrupt("compressed block too large")); + } + let (source, consumed) = self.literals(block)?; + // The buffer is lent out for the sequence pass, which also needs + // `self` for its tables, and handed back after it. + let buffer = core::mem::take(&mut self.literals); + let literals = match &source { + LiteralSource::Block(range) => &block[range.clone()], + LiteralSource::Buffer => &buffer[..], + }; + let result = self.sequences(&block[consumed..], literals, out, dict); + self.literals = buffer; + result + } + + fn literals(&mut self, block: &[u8]) -> Result<(LiteralSource, usize), Error> { + Ok(match Literals::read(block)? { + Literals::Huffman { + header, + size, + compressed, + single_stream, + } => { + let src = &block[header..header + compressed]; + self.literals.clear(); + if single_stream { + self.literals.resize(size, 0); + huffman( + &mut self.huf, + src, + &mut self.literals, + false, + StreamEnd::PastStart, + )?; + } else { + // `HUFv06_decompress`: a section as long as its content + // is the content, a one-byte section is a run of it. + if size == 0 || compressed > size { + return Err(corrupt("Huffman literals larger than their content")); + } + if compressed == size { + self.literals.extend_from_slice(src); + } else if compressed == 1 { + self.literals.resize(size, src[0]); + } else { + self.literals.resize(size, 0); + huffman( + &mut self.huf, + src, + &mut self.literals, + true, + StreamEnd::PastStart, + )?; + } + } + (LiteralSource::Buffer, header + compressed) + } + Literals::Repeat { size, compressed } => { + if !self.repeat_tables { + return Err(Error::DictionaryCorrupted); + } + let table = self.dict_huf.get().ok_or(Error::DictionaryCorrupted)?; + self.literals.clear(); + self.literals.resize(size, 0); + table + .decode_one_stream(&block[3..3 + compressed], &mut self.literals) + .map_err(|_| corrupt("bad Huffman literals"))?; + (LiteralSource::Buffer, 3 + compressed) + } + Literals::Raw { header, size } => { + (LiteralSource::Block(header..header + size), header + size) + } + Literals::Rle { header, size } => { + self.literals.clear(); + self.literals.resize(size, block[header]); + (LiteralSource::Buffer, header + 1) + } + }) + } + + fn sequences( + &mut self, + src: &[u8], + literals: &[u8], + out: &mut O, + dict: &[u8], + ) -> Result<(), Error> { + let (count, mut at) = sequence_count(src, true)?; + let mut lit_pos = 0; + if count > 0 { + if at + 4 > src.len() { + return Err(Error::Truncated); + } + let modes = src[at]; + at += 1; + let repeat = self.repeat_tables; + at += seq_table( + &mut self.tables.ll, + modes >> 6, + &src[at..], + (MAX_LL, LL_LOG, &LL_DEFAULT, LL_DEFAULT_LOG), + repeat, + )?; + at += seq_table( + &mut self.tables.of, + (modes >> 4) & 3, + &src[at..], + (MAX_OFF, OFF_LOG, &OF_DEFAULT, OF_DEFAULT_LOG), + repeat, + )?; + at += seq_table( + &mut self.tables.ml, + (modes >> 2) & 3, + &src[at..], + (MAX_ML, ML_LOG, &ML_DEFAULT, ML_DEFAULT_LOG), + repeat, + )?; + } + // Read once, the dictionary's tables are no longer on offer, even to + // a block that sent none. + self.repeat_tables = false; + if count > 0 { + let (ll, of, ml) = self.tables.all()?; + let mut reps = [1usize; 3]; + let mut bits = open_bits(&src[at..]).map_err(|_| corrupt("bad sequence bitstream"))?; + let mut ll_state = FseState::new(ll, &mut bits); + let mut of_state = FseState::new(of, &mut bits); + let mut ml_state = FseState::new(ml, &mut bits); + for _ in 0..count { + if overflowed(&bits) { + return Err(corrupt("sequence bitstream ends early")); + } + let ll_code = usize::from(ll_state.symbol(ll)); + let ml_code = usize::from(ml_state.symbol(ml)); + let of_code = of_state.symbol(of); + let mut offset = if of_code == 0 { + 0 + } else { + OF_BASE[usize::from(of_code)] as usize + bits.get_bits(of_code) as usize + }; + if offset < 3 { + if ll_code == 0 && offset <= 1 { + offset = 1 - offset; + } + if offset != 0 { + let picked = reps[offset]; + if offset != 1 { + reps[2] = reps[1]; + } + reps[1] = reps[0]; + reps[0] = picked; + offset = picked; + } else { + offset = reps[0]; + } + } else { + offset -= 2; + reps[2] = reps[1]; + reps[1] = reps[0]; + reps[0] = offset; + } + let match_len = + ML_BASE[ml_code] as usize + bits.get_bits(ML_BITS[ml_code]) as usize; + let lit_len = LL_BASE[ll_code] as usize + bits.get_bits(LL_BITS[ll_code]) as usize; + ll_state.update(ll, &mut bits); + ml_state.update(ml, &mut bits); + of_state.update(of, &mut bits); + execute( + out, + literals, + &mut lit_pos, + lit_len, + match_len, + offset, + dict, + )?; + } + } + let rest = &literals[lit_pos..]; + out.reserve(rest.len())?; + out.extend(rest); + Ok(()) + } +} + +/// Decode Huffman literals with the table their section starts with, read +/// into `table`, into `out` (`HUFv06_decompress1X2` / `HUFv06_decompress4X2`); +/// `end` is how the format ends the table's FSE-coded weights. +pub(super) fn huffman( + table: &mut HufTable, + src: &[u8], + out: &mut [u8], + four_streams: bool, + end: StreamEnd, +) -> Result<(), Error> { + let table_len = table + .read_into(src, end) + .map_err(|_| corrupt("bad Huffman table"))?; + if table_len >= src.len() { + return Err(corrupt("Huffman table fills the literal section")); + } + let streams = &src[table_len..]; + if four_streams { + table.decode_four_streams(streams, out) + } else { + table.decode_one_stream(streams, out) + } + .map_err(|_| corrupt("bad Huffman literals")) +} diff --git a/zstd/src/decoding/legacy/v07.rs b/zstd/src/decoding/legacy/v07.rs new file mode 100644 index 00000000..2c6d2268 --- /dev/null +++ b/zstd/src/decoding/legacy/v07.rs @@ -0,0 +1,525 @@ +//! The v0.7 format (upstream `lib/legacy/zstd_v07.c`): the last before 1.0, +//! with a content checksum, a dictionary ID, and repeat offsets that carry +//! from block to block. + +use super::entropy::{ + Distribution, FseState, FseTable, HufTable, Slot, StreamEnd, open_bits, overflowed, +}; +use super::{ + BLOCK_SIZE_MAX, FrameHeader, LegacyDict, LegacyVersion, LiteralSource, Literals, Output, + corrupt, execute, +}; +use crate::decoding::errors::LegacyFrameError as Error; +use alloc::vec::Vec; + +/// Largest window a frame may ask for (`ZSTDv07_WINDOWLOG_MAX`). +const WINDOW_LOG_MAX: u32 = if usize::BITS == 32 { 25 } else { 27 }; + +const MAX_LL: usize = 35; +const MAX_ML: usize = 52; +const MAX_OFF: usize = 28; +const LL_LOG: u32 = 9; +const ML_LOG: u32 = 9; +const OFF_LOG: u32 = 8; + +pub(super) const LL_BITS: [u8; MAX_LL + 1] = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 3, 3, 4, 6, 7, 8, 9, 10, 11, + 12, 13, 14, 15, 16, +]; +pub(super) const LL_BASE: [u32; MAX_LL + 1] = [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20, 22, 24, 28, 32, 40, 48, 64, + 0x80, 0x100, 0x200, 0x400, 0x800, 0x1000, 0x2000, 0x4000, 0x8000, 0x10000, +]; +pub(super) const ML_BITS: [u8; MAX_ML + 1] = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 2, 2, 3, 3, 4, 4, 5, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, +]; +/// Match length bases, the minimum match of 3 included. +pub(super) const ML_BASE: [u32; MAX_ML + 1] = [ + 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, + 28, 29, 30, 31, 32, 33, 34, 35, 37, 39, 41, 43, 47, 51, 59, 67, 83, 99, 0x83, 0x103, 0x203, + 0x403, 0x803, 0x1003, 0x2003, 0x4003, 0x8003, 0x10003, +]; +const OF_BASE: [u32; MAX_OFF + 1] = [ + 0, 1, 1, 5, 0xD, 0x1D, 0x3D, 0x7D, 0xFD, 0x1FD, 0x3FD, 0x7FD, 0xFFD, 0x1FFD, 0x3FFD, 0x7FFD, + 0xFFFD, 0x1FFFD, 0x3FFFD, 0x7FFFD, 0xFFFFD, 0x1FFFFD, 0x3FFFFD, 0x7FFFFD, 0xFFFFFD, 0x1FFFFFD, + 0x3FFFFFD, 0x7FFFFFD, 0xFFFFFFD, +]; + +/// The predefined distributions, shared with v0.6. +pub(super) const LL_DEFAULT: [i16; MAX_LL + 1] = [ + 4, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 2, 1, 1, 1, 1, 1, + -1, -1, -1, -1, +]; +pub(super) const ML_DEFAULT: [i16; MAX_ML + 1] = [ + 1, 4, 3, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -1, -1, -1, -1, -1, -1, -1, +]; +pub(super) const OF_DEFAULT: [i16; MAX_OFF + 1] = [ + 1, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -1, -1, -1, -1, -1, +]; +pub(super) const LL_DEFAULT_LOG: u32 = 6; +pub(super) const ML_DEFAULT_LOG: u32 = 6; +pub(super) const OF_DEFAULT_LOG: u32 = 5; + +/// Read a v0.7 frame header (`ZSTDv07_getFrameParams`). +pub(super) fn read_header(src: &[u8]) -> Result { + let &descriptor = src.get(4).ok_or(Error::Truncated)?; + let dict_id_code = descriptor & 3; + let checksum = descriptor & 4 != 0; + let single_segment = descriptor & 0x20 != 0; + let fcs_code = descriptor >> 6; + let fcs_len = [0usize, 2, 4, 8][usize::from(fcs_code)]; + let size = 5 + + usize::from(!single_segment) + + [0usize, 1, 2, 4][usize::from(dict_id_code)] + + fcs_len + + usize::from(single_segment && fcs_len == 0); + let header = src.get(..size).ok_or(Error::Truncated)?; + if descriptor & 0x08 != 0 { + return Err(Error::UnsupportedParameter); + } + let mut pos = 5; + let mut window: u32 = 0; + if !single_segment { + let byte = header[pos]; + pos += 1; + let log = u32::from(byte >> 3) + 10; + if log > WINDOW_LOG_MAX { + return Err(Error::UnsupportedParameter); + } + window = 1 << log; + window += (window >> 3) * u32::from(byte & 7); + } + let le = |at: usize, len: usize| { + header[at..at + len] + .iter() + .rev() + .fold(0u64, |acc, &byte| acc << 8 | u64::from(byte)) + }; + let dict_id = match dict_id_code { + 0 => 0, + 1 => le(pos, 1) as u32, + 2 => le(pos, 2) as u32, + _ => le(pos, 4) as u32, + }; + pos += [0usize, 1, 2, 4][usize::from(dict_id_code)]; + let content_size = match fcs_code { + 0 if single_segment => le(pos, 1), + 0 => 0, + 1 => le(pos, 2) + 256, + 2 => le(pos, 4), + _ => le(pos, 8), + }; + if window == 0 { + // A single-segment frame's window is its content, cut to 32 bits as + // upstream's `windowSize` field holds it. + window = content_size as u32; + } + if window > 1 << WINDOW_LOG_MAX { + return Err(Error::UnsupportedParameter); + } + Ok(FrameHeader { + version: LegacyVersion::V07, + size, + window: u64::from(window), + content_size: (content_size != 0).then_some(content_size), + dict_id, + checksum, + }) +} + +/// The literal length, offset and match length tables last in use, which a +/// block may keep instead of sending new ones. +pub(super) struct SeqTables { + pub(super) ll: Slot, + pub(super) of: Slot, + pub(super) ml: Slot, +} + +impl SeqTables { + pub(super) const fn empty() -> SeqTables { + SeqTables { + ll: Slot::::empty(), + of: Slot::::empty(), + ml: Slot::::empty(), + } + } + + /// Set none of the tables, keeping their storage. + pub(super) fn unset(&mut self) { + self.ll.set = false; + self.of.set = false; + self.ml.set = false; + } + + pub(super) fn heap_bytes(&self) -> usize { + self.ll.table.heap_bytes() + self.of.table.heap_bytes() + self.ml.table.heap_bytes() + } +} + +/// The decoding state a v0.7 frame carries from block to block. +pub(super) struct Decoder { + /// The last Huffman table, which repeat-table literals reuse. + huf: Slot, + tables: SeqTables, + /// Whether a block may keep the previous tables: once a dictionary or a + /// block with sequences has set them (`fseEntropy`). + repeat_tables: bool, + reps: [usize; 3], + literals: Vec, + /// ID of the dictionary in use, 0 for none. + pub(super) dict_id: u32, +} + +impl Decoder { + pub(super) const fn empty() -> Decoder { + Decoder { + huf: Slot::::empty(), + tables: SeqTables::empty(), + repeat_tables: false, + reps: [1, 4, 8], + literals: Vec::new(), + dict_id: 0, + } + } + + /// Ready the decoder for a frame primed with `dict`, in the storage the + /// previous frame left, and say where the dictionary's content lies + /// within [`LegacyDict::content_bytes`]. + pub(super) fn reset(&mut self, dict: LegacyDict<'_>) -> Result, Error> { + self.huf.set = false; + self.tables.unset(); + self.repeat_tables = false; + self.reps = [1, 4, 8]; + self.literals.clear(); + self.dict_id = 0; + match dict { + LegacyDict::None => Ok(0..0), + LegacyDict::Parsed(dict) => { + // A raw-content dictionary has no tables to load. + if dict.huf.table.max_num_bits != 0 { + self.load_parsed(dict)?; + } + self.dict_id = dict.id; + Ok(0..dict.dict_content.len()) + } + } + } + + /// Heap bytes the decoder holds. + pub(super) fn heap_bytes(&self) -> usize { + self.huf.table.heap_bytes() + self.tables.heap_bytes() + self.literals.capacity() + } + + /// Load the tables and offsets a dictionary of the v0.7 layout primes, + /// from what the RFC 8878 parser read of it: the two layouts are the same + /// bytes, so the parsed distributions and code lengths are the ones + /// `ZSTDv07_loadEntropy` would read, held to the v0.7 symbol limits. + /// + /// The repeat offsets are not held to the dictionary's size here, as the + /// parser kept no count of its table bytes; one reaching past the content + /// fails at the match that uses it. + fn load_parsed(&mut self, dict: &crate::decoding::Dictionary) -> Result<(), Error> { + let bad = |_| Error::DictionaryCorrupted; + let huf_table = &dict.huf.table; + self.huf + .table + .fill_from_code_lengths(huf_table.code_lengths(), huf_table.max_num_bits) + .map_err(bad)?; + self.huf.set = true; + let load = |slot: &mut Slot, + fse: &crate::decoding::scratch::AlignedFSETable, + max: usize, + max_log: u32| + -> Result<(), Error> { + let log = u32::from(fse.accuracy_log); + if log > max_log { + return Err(Error::DictionaryCorrupted); + } + let dist = Distribution::from_probabilities(&fse.symbol_probabilities, log, max) + .map_err(bad)?; + slot.table.build(&dist).map_err(bad)?; + slot.set = true; + Ok(()) + }; + load(&mut self.tables.of, &dict.fse.offsets, MAX_OFF, OFF_LOG)?; + load(&mut self.tables.ml, &dict.fse.match_lengths, MAX_ML, ML_LOG)?; + load( + &mut self.tables.ll, + &dict.fse.literal_lengths, + MAX_LL, + LL_LOG, + )?; + for (rep, &offset) in self.reps.iter_mut().zip(&dict.offset_hist) { + if offset == 0 { + return Err(Error::DictionaryCorrupted); + } + *rep = offset as usize; + } + self.repeat_tables = true; + Ok(()) + } + + /// Decode one compressed block into `out` (`ZSTDv07_decompressBlock_internal`). + pub(super) fn block( + &mut self, + block: &[u8], + out: &mut O, + dict: &[u8], + ) -> Result<(), Error> { + if block.len() >= BLOCK_SIZE_MAX { + return Err(corrupt("compressed block too large")); + } + let (source, consumed) = self.literals(block)?; + // The buffer is lent out for the sequence pass, which also needs + // `self` for its tables, and handed back after it. + let buffer = core::mem::take(&mut self.literals); + let literals = match &source { + LiteralSource::Block(range) => &block[range.clone()], + LiteralSource::Buffer => &buffer[..], + }; + let result = self.sequences(&block[consumed..], literals, out, dict); + self.literals = buffer; + result + } + + fn literals(&mut self, block: &[u8]) -> Result<(LiteralSource, usize), Error> { + Ok(match Literals::read(block)? { + Literals::Huffman { + header, + size, + compressed, + single_stream, + } => { + let src = &block[header..header + compressed]; + if !single_stream && (compressed >= size || compressed <= 1) { + return Err(corrupt("Huffman literals no smaller than their content")); + } + // Built in the previous table's storage; left unset should the + // description be bad, so no later block repeats a half-built + // table. + self.huf.set = false; + let table = &mut self.huf.table; + let table_len = table + .read_into(src, StreamEnd::PastStart) + .map_err(|_| corrupt("bad Huffman table"))?; + if table_len >= src.len() { + return Err(corrupt("Huffman table fills the literal section")); + } + self.literals.clear(); + self.literals.resize(size, 0); + let streams = &src[table_len..]; + if single_stream { + table.decode_one_stream(streams, &mut self.literals) + } else { + table.decode_four_streams(streams, &mut self.literals) + } + .map_err(|_| corrupt("bad Huffman literals"))?; + self.huf.set = true; + (LiteralSource::Buffer, header + compressed) + } + Literals::Repeat { size, compressed } => { + let table = self.huf.get().ok_or(Error::DictionaryCorrupted)?; + self.literals.clear(); + self.literals.resize(size, 0); + table + .decode_one_stream(&block[3..3 + compressed], &mut self.literals) + .map_err(|_| corrupt("bad Huffman literals"))?; + (LiteralSource::Buffer, 3 + compressed) + } + Literals::Raw { header, size } => { + (LiteralSource::Block(header..header + size), header + size) + } + Literals::Rle { header, size } => { + self.literals.clear(); + self.literals.resize(size, block[header]); + (LiteralSource::Buffer, header + 1) + } + }) + } + + fn sequences( + &mut self, + src: &[u8], + literals: &[u8], + out: &mut O, + dict: &[u8], + ) -> Result<(), Error> { + let (count, mut at) = sequence_count(src, true)?; + let mut lit_pos = 0; + if count > 0 { + if at + 4 > src.len() { + return Err(Error::Truncated); + } + let modes = src[at]; + at += 1; + let repeat = self.repeat_tables; + at += seq_table( + &mut self.tables.ll, + modes >> 6, + &src[at..], + (MAX_LL, LL_LOG, &LL_DEFAULT, LL_DEFAULT_LOG), + repeat, + )?; + at += seq_table( + &mut self.tables.of, + (modes >> 4) & 3, + &src[at..], + (MAX_OFF, OFF_LOG, &OF_DEFAULT, OF_DEFAULT_LOG), + repeat, + )?; + at += seq_table( + &mut self.tables.ml, + (modes >> 2) & 3, + &src[at..], + (MAX_ML, ML_LOG, &ML_DEFAULT, ML_DEFAULT_LOG), + repeat, + )?; + self.repeat_tables = true; + let (ll, of, ml) = self.tables.all()?; + + let mut reps = self.reps; + let mut bits = open_bits(&src[at..]).map_err(|_| corrupt("bad sequence bitstream"))?; + let mut ll_state = FseState::new(ll, &mut bits); + let mut of_state = FseState::new(of, &mut bits); + let mut ml_state = FseState::new(ml, &mut bits); + for _ in 0..count { + if overflowed(&bits) { + return Err(corrupt("sequence bitstream ends early")); + } + let ll_code = usize::from(ll_state.symbol(ll)); + let ml_code = usize::from(ml_state.symbol(ml)); + let of_code = of_state.symbol(of); + let mut offset = if of_code == 0 { + 0 + } else { + OF_BASE[usize::from(of_code)] as usize + bits.get_bits(of_code) as usize + }; + if of_code <= 1 { + if ll_code == 0 && offset <= 1 { + offset = 1 - offset; + } + if offset != 0 { + let picked = reps[offset]; + if offset != 1 { + reps[2] = reps[1]; + } + reps[1] = reps[0]; + reps[0] = picked; + offset = picked; + } else { + offset = reps[0]; + } + } else { + reps[2] = reps[1]; + reps[1] = reps[0]; + reps[0] = offset; + } + let match_len = + ML_BASE[ml_code] as usize + bits.get_bits(ML_BITS[ml_code]) as usize; + let lit_len = LL_BASE[ll_code] as usize + bits.get_bits(LL_BITS[ll_code]) as usize; + ll_state.update(ll, &mut bits); + ml_state.update(ml, &mut bits); + of_state.update(of, &mut bits); + execute( + out, + literals, + &mut lit_pos, + lit_len, + match_len, + offset, + dict, + )?; + } + self.reps = reps; + } + let rest = &literals[lit_pos..]; + out.reserve(rest.len())?; + out.extend(rest); + Ok(()) + } +} + +/// The sequence count at the start of a sequence section and the bytes it +/// takes (`ZSTDv07_decodeSeqHeaders`); `long_form` allows the three-byte +/// count v0.6 and v0.7 have. +pub(super) fn sequence_count(src: &[u8], long_form: bool) -> Result<(usize, usize), Error> { + let &first = src.first().ok_or(Error::Truncated)?; + let first = usize::from(first); + Ok(if first <= 0x7F { + (first, 1) + } else if long_form && first == 0xFF { + let bytes = src.get(1..3).ok_or(Error::Truncated)?; + ( + usize::from(u16::from_le_bytes([bytes[0], bytes[1]])) + 0x7F00, + 3, + ) + } else { + let &second = src.get(1).ok_or(Error::Truncated)?; + (((first - 0x80) << 8) + usize::from(second), 2) + }) +} + +impl SeqTables { + /// The three tables, all set once a block has used them. + pub(super) fn all(&self) -> Result<(&FseTable, &FseTable, &FseTable), Error> { + match (self.ll.get(), self.of.get(), self.ml.get()) { + (Some(ll), Some(of), Some(ml)) => Ok((ll, of, ml)), + _ => Err(corrupt("sequence table repeated before one was sent")), + } + } +} + +/// Set the table one sequence field is coded with, by its mode +/// (`ZSTDv07_buildSeqTable`): `field` gives the largest symbol, the largest +/// table log, and the predefined distribution; the repeat mode keeps `slot` +/// as it is, where `repeat` allows it. Returns the bytes the mode's +/// description took. +pub(super) fn seq_table( + slot: &mut Slot, + mode: u8, + src: &[u8], + field: (usize, u32, &[i16], u32), + repeat: bool, +) -> Result { + let (max, max_log, default, default_log) = field; + // The previous table, in place. + if mode == 2 { + if !repeat || !slot.set { + return Err(corrupt("sequence table repeated before one was sent")); + } + return Ok(0); + } + // Built in the previous table's storage; left unset should the + // description be bad, so no later block repeats a half-built table. + slot.set = false; + let new = &mut slot.table; + let len = match mode { + // Predefined. + 0 => { + new.build(&Distribution::predefined(default, default_log))?; + 0 + } + // One symbol throughout. + 1 => { + let &symbol = src.first().ok_or(Error::Truncated)?; + if usize::from(symbol) > max { + return Err(corrupt("sequence symbol out of range")); + } + new.set_rle(symbol); + 1 + } + _ => { + let (dist, len) = super::entropy::read_distribution(src, max) + .map_err(|_| corrupt("bad sequence table"))?; + if dist.table_log() > max_log { + return Err(corrupt("sequence table too large")); + } + new.build(&dist)?; + len + } + }; + slot.set = true; + Ok(len) +} diff --git a/zstd/src/decoding/mod.rs b/zstd/src/decoding/mod.rs index e19100be..63e5691a 100644 --- a/zstd/src/decoding/mod.rs +++ b/zstd/src/decoding/mod.rs @@ -41,6 +41,9 @@ pub mod workspace; pub use dictionary::{Dictionary, DictionaryHandle, MAGIC_NUM as DICTIONARY_MAGIC}; pub use frame_decoder::{BlockDecodingStrategy, ContentChecksum, FrameDecoder}; +#[cfg(feature = "legacy")] +#[cfg_attr(docsrs, doc(cfg(feature = "legacy")))] +pub use legacy::LegacyVersion; /// Largest window a frame may declare before a decoder that holds the window /// itself refuses it, unless [`FrameDecoder::set_max_window_size`] raised the @@ -78,6 +81,11 @@ pub enum FrameContentSize { /// This backs the C `ZSTD_getFrameContentSize` entry point, where the two /// variants map to a concrete size and `ZSTD_CONTENTSIZE_UNKNOWN`. /// +/// With the `legacy` feature a v0.5-v0.7 frame reports the size its header +/// declares, and [`FrameContentSize::Unknown`] when it declares none or its +/// header does not read, as libzstd does: the field is advisory in those +/// formats. +/// /// # Errors /// Returns [`ReadFrameHeaderError`](errors::ReadFrameHeaderError) when `src` /// is too short to hold a header, carries a bad magic number, or begins with @@ -92,6 +100,11 @@ pub enum FrameContentSize { pub fn read_frame_content_size( src: &[u8], ) -> Result { + #[cfg(feature = "legacy")] + if LegacyVersion::of(src).is_some() { + return Ok(legacy::declared_content_size(src) + .map_or(FrameContentSize::Unknown, FrameContentSize::Known)); + } let (header, _consumed) = frame::read_frame_header_with_format(src, false)?; Ok(if header.fcs_declared() { FrameContentSize::Known(header.frame_content_size()) @@ -122,9 +135,10 @@ pub enum FrameSizeError { /// header, every block, and the trailing content checksum when present — /// computed by walking the block headers without decoding any block body. /// -/// For a skippable frame, returns its full `8 + Frame_Size` length. This backs -/// the C `ZSTD_findFrameCompressedSize` entry point; the returned value is the -/// offset at which a following concatenated frame would begin. +/// For a skippable frame, returns its full `8 + Frame_Size` length, and with +/// the `legacy` feature a v0.5-v0.7 frame's length through its end block. This +/// backs the C `ZSTD_findFrameCompressedSize` entry point; the returned value +/// is the offset at which a following concatenated frame would begin. /// /// # Errors /// [`FrameSizeError`] when the header is unreadable, the buffer is truncated @@ -137,6 +151,12 @@ pub enum FrameSizeError { /// assert_eq!(find_frame_compressed_size(&frame).unwrap(), frame.len()); /// ``` pub fn find_frame_compressed_size(src: &[u8]) -> Result { + #[cfg(feature = "legacy")] + if let Some(version) = LegacyVersion::of(src) { + return legacy::frame_size(version, src) + .map(|(size, _)| size) + .ok_or(FrameSizeError::Truncated); + } let (header, header_len) = match frame::read_frame_header_with_format(src, false) { Ok(parsed) => parsed, // Skippable frame: magic (4) + Frame_Size field (4) + payload. @@ -191,6 +211,14 @@ pub fn find_decompressed_size(src: &[u8]) -> Result { total = total @@ -292,11 +320,20 @@ fn walk_blocks( /// Returns the exact size when the header declares `Frame_Content_Size`; /// otherwise a valid (loose) bound of `block_count * block_size_max`, where /// `block_size_max = min(window_size, 128 KiB)` — every block decompresses to -/// at most that many bytes. Skippable frames contribute `0`. +/// at most that many bytes. Skippable frames contribute `0`. With the `legacy` +/// feature a v0.5-v0.7 frame is bounded by 128 KiB per block whatever its +/// header declares, as libzstd bounds it, since those formats do not hold a +/// frame to its declared size. /// /// # Errors /// [`FrameSizeError`] on an unreadable header, truncation, or a reserved block. pub fn frame_decompressed_bound(src: &[u8]) -> Result { + #[cfg(feature = "legacy")] + if let Some(version) = LegacyVersion::of(src) { + return legacy::frame_size(version, src) + .map(|(_, bound)| bound) + .ok_or(FrameSizeError::Truncated); + } let (header, header_len) = match frame::read_frame_header_with_format(src, false) { Ok(parsed) => parsed, // Skippable frame contributes 0, but its full payload must be present: @@ -365,6 +402,25 @@ pub fn frame_magic(src: &[u8]) -> Option { } } +/// Whether `src` starts with a frame this build decodes or skips: a Zstandard +/// frame, a skippable frame, or with the `legacy` feature a v0.5-v0.7 frame. +/// Backs the C `ZSTD_isFrame`, which answers the same for upstream's default +/// build. +/// +/// ```rust +/// use structured_zstd::decoding::is_frame; +/// assert!(is_frame(&[0x28, 0xB5, 0x2F, 0xFD])); +/// assert!(is_frame(&[0x50, 0x2A, 0x4D, 0x18])); +/// assert!(!is_frame(b"zip")); +/// ``` +pub fn is_frame(src: &[u8]) -> bool { + #[cfg(feature = "legacy")] + if LegacyVersion::of(src).is_some() { + return true; + } + frame_magic(src).is_some() +} + /// Bytes the compressed input must trail the end of the output by for every /// frame in `src` to decode in place: the compressed data sits at the end of /// the output buffer and the decoder writes from its start. Backs the C @@ -490,7 +546,9 @@ pub struct FrameHeaderInfo { /// prefix learns how much of the header is still to read. Upstream's /// `ZSTD_frameHeaderSize` measures it the same way /// (`zstd_decompress.c:416-429`), without looking at the descriptor's other -/// bits, so a header this library refuses to decode is still measured. +/// bits, so a header this library refuses to decode is still measured. With +/// the `legacy` feature a v0.5-v0.7 header is measured by its own format's +/// size fields, as [`LegacyVersion::header_size`] does. /// /// # Errors /// [`ReadFrameHeaderError`](errors::ReadFrameHeaderError) when `src` is shorter @@ -502,6 +560,14 @@ pub struct FrameHeaderInfo { /// assert_eq!(frame_header_size(&[0x28, 0xB5, 0x2F, 0xFD, 0x00]).unwrap(), 6); /// ``` pub fn frame_header_size(src: &[u8]) -> Result { + // A v0.5-v0.7 header is measured by its own format's size fields. Upstream + // reads every header with RFC 8878's layout, which only v0.7 shares. + #[cfg(feature = "legacy")] + if let Some(version) = LegacyVersion::of(src) { + return version + .header_size(src) + .ok_or_else(|| errors::ReadFrameHeaderError::FrameDescriptorReadError(frame::eof())); + } frame::measure_header_from_slice(src) } @@ -616,6 +682,8 @@ pub(crate) mod exec_sequence_inline; // rationale. pub(crate) mod flat_buf; pub(crate) mod frame; +#[cfg(feature = "legacy")] +pub(crate) mod legacy; pub(crate) mod literals_section_decoder; pub(crate) mod prefetch; mod ringbuffer; diff --git a/zstd/src/decoding/simd_copy/tests.rs b/zstd/src/decoding/simd_copy/tests.rs index 9691c206..b4dfcbb7 100644 --- a/zstd/src/decoding/simd_copy/tests.rs +++ b/zstd/src/decoding/simd_copy/tests.rs @@ -36,37 +36,38 @@ fn small_exact_copy_writes_exactly_the_run() { /// a kernel only the tier dispatch reaches would otherwise be checked only on /// the hosts that select it. fn runnable_medium_kernels() -> vec::Vec<(&'static str, CopyKernel)> { - let mut kernels: vec::Vec<(&'static str, CopyKernel)> = - vec![("copy_exact_u64", copy_exact_u64)]; - #[cfg(all( - feature = "std", - any(target_arch = "x86", target_arch = "x86_64"), - feature = "kernel-sse" - ))] - if std::arch::is_x86_feature_detected!("sse2") { - kernels.push(("copy_exact_sse2", copy_exact_sse2)); - } - #[cfg(all( - feature = "std", - any(target_arch = "x86", target_arch = "x86_64"), - feature = "kernel-avx2" - ))] - if std::arch::is_x86_feature_detected!("avx2") { - kernels.push(("copy_exact_avx2", copy_exact_avx2)); - } - #[cfg(all( - target_arch = "aarch64", - target_feature = "neon", - feature = "kernel-neon" - ))] - kernels.push(("copy_exact_neon", copy_exact_neon)); - #[cfg(all( - target_arch = "wasm32", - target_feature = "simd128", - feature = "kernel-simd128" - ))] - kernels.push(("copy_exact_simd128", copy_exact_simd128)); - kernels + // Which entries exist depends on the build's kernel features, so the list + // is one array with each entry under its own `cfg`. + let kernels: &[Option<(&'static str, CopyKernel)>] = &[ + Some(("copy_exact_u64", copy_exact_u64)), + #[cfg(all( + feature = "std", + any(target_arch = "x86", target_arch = "x86_64"), + feature = "kernel-sse" + ))] + std::arch::is_x86_feature_detected!("sse2") + .then_some::<(&'static str, CopyKernel)>(("copy_exact_sse2", copy_exact_sse2)), + #[cfg(all( + feature = "std", + any(target_arch = "x86", target_arch = "x86_64"), + feature = "kernel-avx2" + ))] + std::arch::is_x86_feature_detected!("avx2") + .then_some::<(&'static str, CopyKernel)>(("copy_exact_avx2", copy_exact_avx2)), + #[cfg(all( + target_arch = "aarch64", + target_feature = "neon", + feature = "kernel-neon" + ))] + Some(("copy_exact_neon", copy_exact_neon)), + #[cfg(all( + target_arch = "wasm32", + target_feature = "simd128", + feature = "kernel-simd128" + ))] + Some(("copy_exact_simd128", copy_exact_simd128)), + ]; + kernels.iter().copied().flatten().collect() } #[test] diff --git a/zstd/src/decoding/streaming_decoder.rs b/zstd/src/decoding/streaming_decoder.rs index 61b5587f..52c40370 100644 --- a/zstd/src/decoding/streaming_decoder.rs +++ b/zstd/src/decoding/streaming_decoder.rs @@ -316,6 +316,11 @@ fn header_len(bytes: &[u8], magicless: bool) -> usize { return 8; } if magic != crate::common::MAGIC_NUM { + // A v0.5-v0.7 header: its descriptor, then the fields it names. + #[cfg(feature = "legacy")] + if let Some(version) = crate::decoding::LegacyVersion::of(bytes) { + return crate::decoding::legacy::header_len(version, bytes).unwrap_or(5); + } return 4; } 4 diff --git a/zstd/src/decoding/workspace.rs b/zstd/src/decoding/workspace.rs index 2d7acac0..a8aa7d75 100644 --- a/zstd/src/decoding/workspace.rs +++ b/zstd/src/decoding/workspace.rs @@ -16,6 +16,8 @@ //! //! Frames that name a dictionary are refused with //! [`Unsupported::Dictionary`]; this decoder keeps no dictionary registry. +//! With the `legacy` feature, v0.5-v0.7 frames are refused with +//! `Unsupported::LegacyFrame`, since their decoders allocate. //! //! The decoding itself is the same code the other decoders run, so output is //! identical to theirs. @@ -356,6 +358,14 @@ pub enum Unsupported { /// The dictionary id the frame header names. id: u32, }, + /// A v0.5, v0.6 or v0.7 frame, which the owned decoders read and this + /// one does not: their decoders allocate. Upstream's static context + /// refuses them likewise. + #[cfg(feature = "legacy")] + LegacyFrame { + /// The format of the frame. + version: crate::decoding::LegacyVersion, + }, } /// How the input breaks the zstd format. @@ -437,6 +447,12 @@ impl core::fmt::Display for WorkspaceDecodeError { "frame needs dictionary {id}, which this decoder does not take" ) } + #[cfg(feature = "legacy")] + Self::Unsupported(Unsupported::LegacyFrame { version }) => write!( + f, + "v0.{} frame, which this decoder does not read", + version.minor() + ), Self::Truncated => write!(f, "input ended in the middle of a frame"), Self::Failed => write!(f, "an earlier step failed; reset the decoder"), } @@ -1000,6 +1016,13 @@ impl<'w> WorkspaceDecoder<'w> { } else if magic == MAGIC_NUM { self.expect_header(Phase::Descriptor, 5); } else { + #[cfg(feature = "legacy")] + if let Some(version) = crate::decoding::LegacyVersion::of(&magic.to_le_bytes()) + { + return Err(WorkspaceDecodeError::Unsupported( + Unsupported::LegacyFrame { version }, + )); + } return Err(Malformed(MalformedInput::FrameHeader( ReadFrameHeaderError::BadMagicNumber(magic), ))); diff --git a/zstd/src/decoding/workspace/tests.rs b/zstd/src/decoding/workspace/tests.rs index ac61df9d..8086871f 100644 --- a/zstd/src/decoding/workspace/tests.rs +++ b/zstd/src/decoding/workspace/tests.rs @@ -506,6 +506,36 @@ fn dictionary_frame_is_unsupported() { )); } +/// A v0.5-v0.7 frame, which the owned decoders read, is refused as +/// unsupported rather than as malformed: decoding it would allocate. +#[cfg(feature = "legacy")] +#[test] +fn v05_to_v07_frames_are_unsupported() { + // v0.7 with a one-byte dictionary ID of 0 and no blocks before its end. + let frame = [ + 0x27, 0xB5, 0x2F, 0xFD, 0x51, 0x00, 0x00, 0xFA, 0x00, 0xFF, 0x00, 0x00, + ]; + let mut out = [0u8; 16]; + assert_eq!( + crate::decoding::FrameDecoder::new() + .decode_all(&frame, &mut out) + .unwrap(), + 0 + ); + assert!(matches!( + decode_whole(&frame, DecodeLimits::new(1 << 20)), + Err(WorkspaceDecodeError::Unsupported( + Unsupported::LegacyFrame { + version: crate::decoding::LegacyVersion::V07 + } + )) + )); + let message = alloc::string::ToString::to_string( + &decode_whole(&frame, DecodeLimits::new(1 << 20)).unwrap_err(), + ); + assert!(message.contains("v0.7"), "{message}"); +} + /// Bytes after the last frame that are not a frame are malformed; the error /// holds until a reset, after which the decoder works again. #[test] diff --git a/zstd/src/huff0/huff0_decoder.rs b/zstd/src/huff0/huff0_decoder.rs index a0da9a67..b0af3c22 100644 --- a/zstd/src/huff0/huff0_decoder.rs +++ b/zstd/src/huff0/huff0_decoder.rs @@ -291,6 +291,13 @@ impl HuffmanTable { self.fse_table.reset(); } + /// Code length of every symbol the parsed description lists, 0 for a + /// symbol it leaves out, the last symbol's inferred length included. + #[cfg(feature = "legacy")] + pub(crate) fn code_lengths(&self) -> &[u8] { + &self.bits + } + /// Build the equivalent encoder-side Huffman table from parsed weights. pub(crate) fn to_encoder_table(&self) -> Option { if self.bits.is_empty() || self.max_num_bits == 0 { diff --git a/zstd/src/lib.rs b/zstd/src/lib.rs index 547567d5..155ca7e4 100644 --- a/zstd/src/lib.rs +++ b/zstd/src/lib.rs @@ -13,6 +13,11 @@ //! * `decoding::workspace` (feature `caller-workspace`): streaming decoder //! that runs entirely in caller-owned memory, never allocating, with //! caller-set limits on window, output and work. +//! * v0.5, v0.6 and v0.7 frames (feature `legacy`, on by default): the +//! formats every libzstd 1.5.7 build reads by default decode through the +//! same entry points, the frame queries and the streaming decoder +//! included; `decoding::LegacyVersion` names them. Without the feature +//! they are refused as an unknown format. //! //! No FFI, no cmake, no system zstd. `no_std` builds are supported by //! disabling the default `std` feature.