use crate::block::{parse_block_header, BlockType};
use crate::compressed::{decode_compressed_block, BlockState};
#[cfg(feature = "alloc")]
use crate::dict::Dictionary;
use crate::error::Error;
use crate::frame::{parse_kind, FrameHeader, FrameKind, DEFAULT_WINDOW_MAX, MAGIC};
use crate::reader::Reader;
use crate::xxh64::content_checksum;
#[cfg(feature = "alloc")]
use alloc::vec::Vec;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DecompressOptions {
pub window_max: u64,
pub force_ignore_checksum: bool,
}
impl Default for DecompressOptions {
fn default() -> Self {
Self {
window_max: DEFAULT_WINDOW_MAX,
force_ignore_checksum: false,
}
}
}
const CK_FUSE_MIN: usize = usize::MAX;
static CK_FUSE_ARM: core::sync::atomic::AtomicUsize = core::sync::atomic::AtomicUsize::new(0);
pub fn set_ck_fuse_arm(v: usize) {
CK_FUSE_ARM.store(v, core::sync::atomic::Ordering::Relaxed);
}
#[inline]
fn ck_fuse_min() -> usize {
let v = CK_FUSE_ARM.load(core::sync::atomic::Ordering::Relaxed);
if v == 0 {
CK_FUSE_MIN
} else {
v
}
}
static CK_STREAM_ARM: core::sync::atomic::AtomicU8 = core::sync::atomic::AtomicU8::new(0);
pub fn set_ck_stream_arm(on: bool) {
CK_STREAM_ARM.store(u8::from(on) + 1, core::sync::atomic::Ordering::Relaxed);
}
#[inline]
fn ck_stream_enabled() -> bool {
matches!(CK_STREAM_ARM.load(core::sync::atomic::Ordering::Relaxed), 2)
}
#[cfg(feature = "alloc")]
pub fn decompress(src: &[u8]) -> Result<Vec<u8>, Error> {
decompress_with(src, DecompressOptions::default())
}
#[cfg(feature = "alloc")]
pub fn decompress_using_dict(src: &[u8], dict: &Dictionary) -> Result<Vec<u8>, Error> {
decompress_using_dict_with(src, dict, DecompressOptions::default())
}
#[cfg(feature = "alloc")]
pub fn decompress_using_dict_with(
src: &[u8],
dict: &Dictionary,
opts: DecompressOptions,
) -> Result<Vec<u8>, Error> {
decompress_with_history(src, opts, Some(dict), &[])
}
#[cfg(feature = "alloc")]
pub fn decompress_using_prefix(src: &[u8], prefix: &[u8]) -> Result<Vec<u8>, Error> {
decompress_using_prefix_with(src, prefix, DecompressOptions::default())
}
#[cfg(feature = "alloc")]
pub fn decompress_using_prefix_with(
src: &[u8],
prefix: &[u8],
opts: DecompressOptions,
) -> Result<Vec<u8>, Error> {
decompress_with_history(src, opts, None, prefix)
}
#[cfg(feature = "alloc")]
pub fn decompress_with(src: &[u8], opts: DecompressOptions) -> Result<Vec<u8>, Error> {
decompress_with_history(src, opts, None, &[])
}
#[cfg(feature = "alloc")]
pub fn decompress_into(dst: &mut Vec<u8>, src: &[u8]) -> Result<usize, Error> {
decompress_into_with(dst, src, DecompressOptions::default())
}
#[cfg(feature = "alloc")]
pub fn decompress_into_with(
dst: &mut Vec<u8>,
src: &[u8],
opts: DecompressOptions,
) -> Result<usize, Error> {
let start = dst.len();
decompress_into_history(src, opts, None, &[], dst)?;
Ok(dst.len() - start)
}
#[cfg(feature = "alloc")]
fn decompress_with_history(
src: &[u8],
opts: DecompressOptions,
dict: Option<&Dictionary>,
prefix: &[u8],
) -> Result<Vec<u8>, Error> {
let mut out = Vec::new();
decompress_into_history(src, opts, dict, prefix, &mut out)?;
Ok(out)
}
#[cfg(feature = "alloc")]
fn decompress_into_history(
src: &[u8],
opts: DecompressOptions,
dict: Option<&Dictionary>,
prefix: &[u8],
out: &mut Vec<u8>,
) -> Result<(), Error> {
if src.is_empty() {
return Err(Error::UnexpectedEof);
}
let hist = dict.map(Dictionary::content).unwrap_or(prefix);
let mut r = Reader::new(src);
let mut saw_zstd = false;
while !r.is_empty() {
match r.peek_u32_le() {
Ok(m) if m == MAGIC || crate::frame::is_skippable_magic(m) => {}
Ok(_) if saw_zstd => return Err(Error::TrailingBytes),
Ok(_) => return Err(Error::BadMagic),
Err(e) => {
if saw_zstd && r.remaining() > 0 {
return Err(Error::TrailingBytes);
}
return Err(e);
}
}
match parse_kind(&mut r)? {
FrameKind::Skippable { user_data_size, .. } => {
let n = user_data_size as usize;
let _ = r.take(n)?;
}
FrameKind::Zstd(header) => {
decode_zstd_frame(&mut r, header, opts, dict, hist, out)?;
saw_zstd = true;
}
}
}
if !saw_zstd {
return Err(Error::UnexpectedEof);
}
Ok(())
}
pub fn content_size(src: &[u8]) -> Result<Option<u64>, Error> {
let mut r = Reader::new(src);
loop {
match parse_kind(&mut r)? {
FrameKind::Skippable { user_data_size, .. } => {
let _ = r.take(user_data_size as usize)?;
}
FrameKind::Zstd(h) => return Ok(h.content_size),
}
}
}
pub fn find_frame_compressed_size(src: &[u8]) -> Result<usize, Error> {
let mut r = Reader::new(src);
match parse_kind(&mut r)? {
FrameKind::Skippable { user_data_size, .. } => {
let _ = r.take(user_data_size as usize)?;
Ok(r.pos())
}
FrameKind::Zstd(header) => {
skip_blocks(&mut r, header)?;
Ok(r.pos())
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ListedFrame {
pub kind: FrameKind,
pub compressed_size: usize,
}
#[cfg(feature = "alloc")]
pub fn inspect_frames(src: &[u8]) -> Result<Vec<ListedFrame>, Error> {
let mut out = Vec::new();
let mut off = 0usize;
if src.is_empty() {
return Err(Error::UnexpectedEof);
}
while off < src.len() {
let n = find_frame_compressed_size(&src[off..])?;
if n == 0 {
return Err(Error::Corruption);
}
let kind = crate::get_frame_header(&src[off..off + n])?;
out.push(ListedFrame {
kind,
compressed_size: n,
});
off += n;
}
Ok(out)
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct BlockCensus {
pub raw: u32,
pub rle: u32,
pub compressed: u32,
pub raw_bytes: u64,
pub rle_regen: u64,
pub compressed_payload: u64,
}
#[cfg(feature = "alloc")]
pub fn frame_block_census(src: &[u8]) -> Result<BlockCensus, Error> {
let mut r = Reader::new(src);
match parse_kind(&mut r)? {
FrameKind::Skippable { .. } => Err(Error::BadMagic),
FrameKind::Zstd(_) => {
let mut c = BlockCensus::default();
loop {
let bh = parse_block_header(&mut r)?;
match bh.ty {
BlockType::Raw => {
c.raw += 1;
c.raw_bytes += u64::from(bh.size);
}
BlockType::Rle => {
c.rle += 1;
c.rle_regen += u64::from(bh.size);
}
BlockType::Compressed => {
c.compressed += 1;
c.compressed_payload += u64::from(bh.size);
}
}
let _ = r.take(bh.payload_len() as usize)?;
if bh.last {
break;
}
}
Ok(c)
}
}
}
#[cfg(feature = "alloc")]
fn decode_zstd_frame(
r: &mut Reader<'_>,
header: FrameHeader,
opts: DecompressOptions,
dict: Option<&Dictionary>,
hist: &[u8],
out: &mut Vec<u8>,
) -> Result<(), Error> {
let _dec = crate::prof::scope(crate::prof::Stage::DecodeTotal);
if header.window_size > opts.window_max {
return Err(Error::WindowTooLarge);
}
if let Some(id) = header.dict_id {
match dict {
None => return Err(Error::DictionaryNeeded { id }),
Some(d) if d.id() != id => {
return Err(Error::DictionaryMismatch {
frame: id,
loaded: d.id(),
});
}
Some(_) => {}
}
}
if let Some(n) = header.content_size {
let extra = usize::try_from(n).map_err(|_| Error::ContentSizeTooLarge)?;
out.try_reserve(extra)
.map_err(|_| Error::ContentSizeTooLarge)?;
}
let block_max = header.block_size_max();
let start_len = out.len();
let mut block_state = BlockState::from_dict(dict);
let fuse_by_size = match header.content_size {
Some(n) => n >= ck_fuse_min() as u64,
None => false,
};
let mut running = if header.checksum
&& !opts.force_ignore_checksum
&& (ck_stream_enabled() || fuse_by_size)
{
Some(crate::xxh64::Xxh64::new())
} else {
None
};
let mut hashed_to = start_len;
{
let _b = crate::prof::scope(crate::prof::Stage::DecodeBlocks);
loop {
let bh = parse_block_header(r)?;
match bh.ty {
BlockType::Raw => {
if bh.size > block_max {
return Err(Error::BlockTooLarge);
}
let payload = r.take(bh.size as usize)?;
out.extend_from_slice(payload);
}
BlockType::Rle => {
if bh.size > block_max {
return Err(Error::BlockTooLarge);
}
let b = r.u8()?;
let n = bh.size as usize;
out.resize(out.len() + n, b);
}
BlockType::Compressed => {
if bh.size > block_max {
return Err(Error::BlockTooLarge);
}
let payload = r.take(bh.size as usize)?;
decode_compressed_block(
payload,
out,
header.window_size,
block_max,
&mut block_state,
hist,
start_len,
0,
)?;
}
}
if let Some(h) = running.as_mut() {
h.update(&out[hashed_to..]);
hashed_to = out.len();
}
if bh.last {
break;
}
}
}
if header.checksum {
let _c = crate::prof::scope(crate::prof::Stage::DecodeChecksum);
let got = r.u32_le()?;
if !opts.force_ignore_checksum {
let computed = match running {
Some(h) => {
debug_assert_eq!(hashed_to, out.len());
h.digest() as u32
}
None => content_checksum(&out[start_len..]),
};
if computed != got {
return Err(Error::ChecksumMismatch);
}
}
}
if let Some(n) = header.content_size {
let produced = (out.len() - start_len) as u64;
if produced != n {
return Err(Error::ContentSizeMismatch);
}
}
Ok(())
}
fn skip_blocks(r: &mut Reader<'_>, header: FrameHeader) -> Result<(), Error> {
loop {
let bh = parse_block_header(r)?;
if bh.ty == BlockType::Compressed {
}
if matches!(bh.ty, BlockType::Raw | BlockType::Compressed)
&& bh.size > header.block_size_max()
{
return Err(Error::BlockTooLarge);
}
let _ = r.take(bh.payload_len() as usize)?;
if bh.last {
break;
}
}
if header.checksum {
let _ = r.u32_le()?;
}
Ok(())
}
#[cfg(test)]
mod tests {
#[test]
fn into_matches_decompress() {
for src in [
alloc::vec![0u8; 300_000],
alloc::vec![7u8; 1],
b"hello hello hello world world".to_vec(),
(0..70_000u32).map(|i| (i % 251) as u8).collect(),
alloc::vec![],
] {
for lvl in [1, 3, 9, 19] {
let f = crate::compress(&src, lvl).unwrap();
let want = decompress(&f).unwrap();
let mut got = Vec::new();
let n = decompress_into(&mut got, &f).unwrap();
assert_eq!(n, want.len(), "returned count, level {lvl}");
assert_eq!(got, want, "level {lvl}, {} bytes", src.len());
assert_eq!(want, src);
}
}
}
#[test]
fn into_appends_without_disturbing_prefix() {
let src: Vec<u8> = (0..40_000u32).map(|i| (i % 97) as u8).collect();
let f = crate::compress(&src, 5).unwrap();
let mut buf = b"PREFIX".to_vec();
let n = decompress_into(&mut buf, &f).unwrap();
assert_eq!(n, src.len());
assert_eq!(&buf[..6], b"PREFIX");
assert_eq!(&buf[6..], &src[..]);
}
#[test]
fn into_appends_correctly_at_every_prefix_length() {
let contents: [Vec<u8>; 3] = [
(0..90_000u32).map(|i| (i % 97) as u8).collect(),
alloc::vec![0u8; 200_000],
b"hello hello hello world world world".to_vec(),
];
const PREFILLS: &[usize] = &[
0, 1, 7, 8, 9, 63, 64, 65, 4095, 4096, 65_535, 65_536, 131_072, 131_073,
];
let mut cases = 0usize;
let mut appended = 0usize;
for (ci, src) in contents.iter().enumerate() {
for lvl in [1, 5, 19] {
let f = crate::compress(src, lvl).unwrap();
let want = decompress(&f).unwrap();
assert_eq!(&want, src, "oracle c{ci} L{lvl}");
for &p in PREFILLS {
let prefix: Vec<u8> = (0..p).map(|i| (i % 97) as u8).collect();
let mut buf = prefix.clone();
let n = decompress_into(&mut buf, &f).unwrap();
assert_eq!(n, want.len(), "count c{ci} L{lvl} prefill {p}");
assert_eq!(buf.len(), p + want.len(), "len c{ci} L{lvl} prefill {p}");
assert_eq!(&buf[..p], &prefix[..], "prefix c{ci} L{lvl} prefill {p}");
assert_eq!(&buf[p..], &want[..], "appended c{ci} L{lvl} prefill {p}");
cases += 1;
appended += n;
}
}
}
assert_eq!(cases, 3 * 3 * PREFILLS.len(), "cases actually exercised");
assert_eq!(appended, 3 * PREFILLS.len() * (90_000 + 200_000 + 35));
}
#[test]
fn into_reuse_across_frames_is_stable() {
let a = crate::compress(&alloc::vec![0u8; 200_000], 1).unwrap();
let b = crate::compress(b"second frame contents", 3).unwrap();
let mut buf = Vec::new();
for _ in 0..3 {
buf.clear();
assert_eq!(decompress_into(&mut buf, &a).unwrap(), 200_000);
assert!(buf.iter().all(|&x| x == 0));
buf.clear();
assert_eq!(decompress_into(&mut buf, &b).unwrap(), 21);
assert_eq!(&buf[..], b"second frame contents");
}
}
#[test]
fn force_ignore_checksum_skips_verification_not_parsing() {
let src: Vec<u8> = (0..50_000u32).map(|i| (i % 131) as u8).collect();
let f = crate::compress(&src, 3).unwrap();
let skip = DecompressOptions {
force_ignore_checksum: true,
..Default::default()
};
assert_eq!(decompress_with(&f, skip).unwrap(), src);
assert_eq!(
decompress_with(&f, DecompressOptions::default()).unwrap(),
src
);
let mut bad = f.clone();
let n = bad.len();
bad[n - 1] ^= 0xFF;
assert!(matches!(
decompress_with(&bad, DecompressOptions::default()),
Err(Error::ChecksumMismatch)
));
assert_eq!(decompress_with(&bad, skip).unwrap(), src);
let mut two = f.clone();
two.extend_from_slice(&f);
let got = decompress_with(&two, skip).unwrap();
assert_eq!(got.len(), src.len() * 2);
assert_eq!(&got[..src.len()], &src[..]);
assert_eq!(&got[src.len()..], &src[..]);
}
#[test]
fn into_propagates_errors() {
let mut buf = Vec::new();
assert!(decompress_into(&mut buf, b"").is_err());
assert!(decompress_into(&mut buf, b"not a zstd frame").is_err());
let f = crate::compress(b"payload here", 3).unwrap();
assert!(decompress_into(&mut buf, &f[..f.len() / 2]).is_err());
}
use super::*;
use crate::frame::{get_frame_header, FrameKind};
const EMPTY: &[u8] = &[
0x28, 0xB5, 0x2F, 0xFD, 0x24, 0x00, 0x01, 0x00, 0x00, 0x99, 0xE9, 0xD8, 0x51,
];
const EMPTY_NC: &[u8] = &[0x28, 0xB5, 0x2F, 0xFD, 0x20, 0x00, 0x01, 0x00, 0x00];
const A: &[u8] = &[
0x28, 0xB5, 0x2F, 0xFD, 0x24, 0x01, 0x09, 0x00, 0x00, 0x61, 0x5B, 0x6E, 0x8C, 0xA9,
];
const A_NC: &[u8] = &[0x28, 0xB5, 0x2F, 0xFD, 0x20, 0x01, 0x09, 0x00, 0x00, 0x61];
const A_NC_NCS: &[u8] = &[0x28, 0xB5, 0x2F, 0xFD, 0x00, 0x00, 0x09, 0x00, 0x00, 0x61];
const HI_NC: &[u8] = &[
0x28, 0xB5, 0x2F, 0xFD, 0x20, 0x02, 0x11, 0x00, 0x00, 0x68, 0x69,
];
const HELLO: &[u8] = &[
0x28, 0xB5, 0x2F, 0xFD, 0x24, 0x05, 0x29, 0x00, 0x00, 0x68, 0x65, 0x6C, 0x6C, 0x6F, 0xA3,
0x6D, 0x9F, 0x88,
];
const ZEROS16: &[u8] = &[
0x28, 0xB5, 0x2F, 0xFD, 0x24, 0x10, 0x45, 0x00, 0x00, 0x10, 0x00, 0x00, 0x01, 0x00, 0x32,
0xC0, 0x02, 0x32, 0x7C, 0x24, 0x16,
];
#[test]
fn c_empty() {
assert_eq!(decompress(EMPTY).unwrap(), b"");
assert_eq!(decompress(EMPTY_NC).unwrap(), b"");
assert_eq!(content_size(EMPTY).unwrap(), Some(0));
assert_eq!(find_frame_compressed_size(EMPTY).unwrap(), EMPTY.len());
}
#[test]
fn c_raw_small() {
assert_eq!(decompress(A).unwrap(), b"a");
assert_eq!(decompress(A_NC).unwrap(), b"a");
assert_eq!(decompress(A_NC_NCS).unwrap(), b"a");
assert_eq!(decompress(HI_NC).unwrap(), b"hi");
assert_eq!(decompress(HELLO).unwrap(), b"hello");
assert_eq!(content_size(A_NC_NCS).unwrap(), None);
}
#[test]
fn c_zeros16_compressed() {
assert_eq!(decompress(ZEROS16).unwrap(), [0u8; 16]);
match get_frame_header(ZEROS16).unwrap() {
FrameKind::Zstd(h) => {
assert_eq!(h.content_size, Some(16));
assert!(h.checksum);
}
other => panic!("{other:?}"),
}
assert_eq!(find_frame_compressed_size(ZEROS16).unwrap(), ZEROS16.len());
}
#[test]
fn rle_handcrafted() {
let src = [0x28, 0xB5, 0x2F, 0xFD, 0x20, 0x05, 0x2B, 0x00, 0x00, b'A'];
assert_eq!(decompress(&src).unwrap(), b"AAAAA");
}
#[test]
fn skippable_then_zstd() {
let mut src = vec![
0x50, 0x2A, 0x4D, 0x18, 0x04, 0x00, 0x00, 0x00, 0xDE, 0xAD, 0xBE, 0xEF,
];
src.extend_from_slice(A_NC);
assert_eq!(decompress(&src).unwrap(), b"a");
assert_eq!(content_size(&src).unwrap(), Some(1));
assert_eq!(find_frame_compressed_size(&src).unwrap(), 12);
}
#[test]
fn multi_frame() {
let mut src = EMPTY_NC.to_vec();
src.extend_from_slice(HELLO);
assert_eq!(decompress(&src).unwrap(), b"hello");
let mut ab = A_NC.to_vec();
ab.extend_from_slice(HI_NC);
assert_eq!(decompress(&ab).unwrap(), b"ahi");
}
#[test]
fn checksum_mismatch() {
let mut bad = A.to_vec();
let n = bad.len();
bad[n - 1] ^= 0xFF;
assert_eq!(decompress(&bad).unwrap_err(), Error::ChecksumMismatch);
}
#[test]
fn truncated() {
assert_eq!(decompress(&A[..4]).unwrap_err(), Error::UnexpectedEof);
assert_eq!(decompress(&[]).unwrap_err(), Error::UnexpectedEof);
}
#[test]
fn dict_id_needed() {
let src = [0x28, 0xB5, 0x2F, 0xFD, 0x21, 0x07, 0x00, 0x01, 0x00, 0x00];
assert_eq!(
decompress(&src).unwrap_err(),
Error::DictionaryNeeded { id: 7 }
);
}
#[test]
fn reserved_bit() {
let src = [0x28, 0xB5, 0x2F, 0xFD, 0x08];
assert_eq!(get_frame_header(&src).unwrap_err(), Error::ReservedBitSet);
}
#[test]
fn window_too_large() {
let desc = 18u8 << 3;
let src = [0x28, 0xB5, 0x2F, 0xFD, 0x00, desc, 0x01, 0x00, 0x00];
assert_eq!(decompress(&src).unwrap_err(), Error::WindowTooLarge);
}
#[test]
fn trailing_garbage() {
let mut src = A_NC.to_vec();
src.push(0xFF);
assert_eq!(decompress(&src).unwrap_err(), Error::TrailingBytes);
}
}