#![cfg(feature = "decompression")]
mod decoder_support;
use decoder_support::{
Rng, compress, corpora, decode_fixed, decode_streaming, mixed, params, reference_decompress,
skippable, text,
};
use mzstd::{DecodeError, DecodeLimits, DecoderConfig, Decompressor, FrameMode};
use zstd_ffi::oracle::{self, Directive, Params};
fn stream_unknown_size(params: &Params, input: &[u8]) -> Vec<u8> {
let (head, tail) = input.split_at(input.len() / 2);
let frame = oracle::compress_stream(
params,
&[(head, Directive::Continue), (tail, Directive::End)],
)
.expect("the reference streams");
assert_eq!(frame[4] & 0xe0, 0, "no content size, not single-segment");
frame
}
fn assert_one_shot(decoder: &mut Decompressor, frame: &[u8], expected: &[u8], label: &str) {
assert_eq!(
decoder.decompress(frame).expect(label),
expected,
"{label}: decompress"
);
let mut appended = b"prefix".to_vec();
let range = decoder.decompress_into(frame, &mut appended).expect(label);
assert_eq!(&appended[range], expected, "{label}: decompress_into");
let mut slice = vec![0xa5u8; expected.len() + 3];
let written = decoder.decompress_to_slice(frame, &mut slice).expect(label);
assert_eq!(&slice[..written], expected, "{label}: decompress_to_slice");
assert_eq!(
&slice[written..],
&[0xa5; 3],
"{label}: slice suffix untouched"
);
}
#[test]
fn reference_frames_decode_across_levels_sizes_and_flags() {
let mut decoder = Decompressor::default();
let levels = [-5, -1, 1, 3, 6, 9, 13, 19];
let mut flags = 0usize;
for size in [0usize, 1, 2, 17, 300, 4_000, 140_000] {
for (name, input) in corpora(size, size as u64 + 1) {
for level in levels {
if level >= 13 && size > 4_000 && name != "text" {
continue; }
flags += 1;
let checksum = flags.is_multiple_of(2);
let content_size = !flags.is_multiple_of(3);
let frame = compress(&input, ¶ms(level, checksum, content_size));
let label =
format!("{name} {size} level {level} checksum {checksum} size {content_size}");
assert_one_shot(&mut decoder, &frame, &input, &label);
assert_eq!(
reference_decompress(&frame, input.len()).expect("reference"),
input,
"{label}: reference agrees"
);
}
}
}
}
#[test]
fn streaming_matches_one_shot_for_any_chunking() {
let mut decoder = Decompressor::default();
let inputs = [
("text", text(300_000, 7), 3),
("mixed", mixed(260_000, 8), 9),
("text-19", text(90_000, 9), 19),
];
for (name, input, level) in inputs {
let frame = compress(&input, ¶ms(level, true, true));
for (in_chunk, out_chunk) in [
(1, 1 << 16),
(7, 3),
(13, 1),
(4096, 64),
(1 << 20, 1 << 20),
] {
let decoded = decode_fixed(&mut decoder, &frame, in_chunk, out_chunk)
.unwrap_or_else(|error| panic!("{name} {in_chunk}/{out_chunk}: {error}"));
assert_eq!(decoded, input, "{name} {in_chunk}/{out_chunk}");
}
let mut rng = Rng::new(level as u64);
let decoded = decode_streaming(&mut decoder, &frame, || rng.below(5_000), 777)
.expect("random chunks");
assert_eq!(decoded, input, "{name} random chunks");
let mut session = Decompressor::default().into_session();
let mut output = vec![0u8; input.len()];
let progress = session
.process(&frame, &mut output, mzstd::DecodeOperation::Finish)
.expect("owned session");
assert_eq!(progress.status, mzstd::DecoderStatus::Finished);
assert_eq!(&output[..progress.produced], input, "{name} owned");
}
}
#[test]
fn reference_streaming_frames_with_flushes_decode() {
let mut decoder = Decompressor::default();
let input = mixed(400_000, 21);
for level in [1, 4, 12] {
let mut steps = Vec::new();
let mut rng = Rng::new(level as u64 + 100);
let mut position = 0;
while position < input.len() {
let end = (position + 1 + rng.below(60_000)).min(input.len());
let directive = if rng.below(3) == 0 {
Directive::Flush
} else {
Directive::Continue
};
steps.push((&input[position..end], directive));
position = end;
}
let params = Params::level(level).with(zstd_ffi::ZSTD_c_checksumFlag, 1);
let frame = oracle::compress_stream(¶ms, &steps).expect("reference streams");
assert_one_shot(
&mut decoder,
&frame,
&input,
&format!("flushed level {level}"),
);
let decoded = decode_fixed(&mut decoder, &frame, 1000, 5000).expect("streams");
assert_eq!(decoded, input);
assert_eq!(
oracle::decompress_stream(&frame, 1000, None).expect("reference"),
input
);
}
}
#[test]
fn small_windows_compact_history_without_losing_matches() {
let input = text(1_200_000, 31);
for window_log in [10, 12, 17] {
let params = Params::level(7)
.with(zstd_ffi::ZSTD_c_windowLog, window_log)
.with(zstd_ffi::ZSTD_c_checksumFlag, 1);
let frame = stream_unknown_size(¶ms, &input);
let limits = DecodeLimits::default()
.with_max_window_log(window_log as u8)
.expect("valid log");
let mut decoder = Decompressor::new(limits.into());
let decoded = decode_fixed(&mut decoder, &frame, 3000, 1 << 15).expect("small window");
assert_eq!(decoded, input, "window log {window_log}");
assert_eq!(decoder.decompress(&frame).expect("one-shot"), input);
}
}
#[test]
fn concatenated_and_skippable_frames_match_the_reference() {
let first = text(50_000, 1);
let second = mixed(70_000, 2);
let mut input = skippable(0, b"metadata");
input.extend_from_slice(&compress(&first, ¶ms(3, true, true)));
input.extend_from_slice(&skippable(15, &[]));
input.extend_from_slice(&compress(&[], ¶ms(3, false, true)));
input.extend_from_slice(&compress(&second, ¶ms(5, false, false)));
input.extend_from_slice(&skippable(7, &[0xee; 300]));
let expected = [first.as_slice(), &second].concat();
let mut decoder = Decompressor::default();
assert_one_shot(&mut decoder, &input, &expected, "concatenated");
assert_eq!(
decode_fixed(&mut decoder, &input, 5, 11).expect("streams"),
expected
);
assert_eq!(
reference_decompress(&input, expected.len()).expect("reference"),
expected
);
let mut single = Decompressor::new(FrameMode::Single.into());
match single.decompress(&input) {
Err(DecodeError::TrailingData { offset }) => {
assert_eq!(
offset as usize,
16 + compress(&first, ¶ms(3, true, true)).len()
);
}
other => panic!("expected trailing data, got {other:?}"),
}
let mut session = single.start();
let mut output = vec![0u8; first.len() + 10];
let progress = session
.process(&input, &mut output, mzstd::DecodeOperation::Process)
.expect("single frame");
assert_eq!(progress.status, mzstd::DecoderStatus::Finished);
assert_eq!(&output[..progress.produced], first);
assert_eq!(session.frames_decoded(), 1);
}
#[test]
fn window_limits_match_the_reference_streaming_limit() {
let input = text(100_000, 5);
let params = Params::level(3).with(zstd_ffi::ZSTD_c_windowLog, 17);
let frame = stream_unknown_size(¶ms, &input);
for log in [10u8, 16] {
let limits = DecodeLimits::default()
.with_max_window_log(log)
.expect("valid");
let mut decoder = Decompressor::new(limits.into());
assert!(matches!(
decoder.decompress(&frame),
Err(DecodeError::WindowLimitExceeded {
window_size: 0x2_0000,
..
})
));
assert!(oracle::decompress_stream(&frame, 1 << 20, Some(i32::from(log))).is_err());
}
let limits = DecodeLimits::default()
.with_max_window_log(17)
.expect("valid");
assert_eq!(
Decompressor::new(limits.into())
.decompress(&frame)
.expect("fits"),
input
);
let mut wide = frame.clone();
let descriptor = wide[4];
assert_eq!(descriptor & 0x20, 0, "the frame has a window descriptor");
wide[5] = 18 << 3;
assert!(matches!(
Decompressor::default().decompress(&wide),
Err(DecodeError::WindowLimitExceeded {
window_size: 0x1000_0000,
limit: 0x0800_0000
})
));
assert!(oracle::decompress_stream(&wide, 1 << 20, None).is_err());
let limits = DecodeLimits::default()
.with_max_window_log(28)
.expect("valid");
let config = DecoderConfig::default().with_limits(limits);
assert_eq!(
Decompressor::new(config)
.decompress(&wide)
.expect("allowed"),
input
);
}
#[test]
#[cfg_attr(debug_assertions, ignore = "slow without optimizations")]
fn reference_frames_decode_across_every_level() {
let mut decoder = Decompressor::default();
for (name, input) in corpora(3_000_000, 99) {
for level in -7..=22 {
let checksum = level % 2 == 0;
let frame = compress(&input, ¶ms(level, checksum, true));
let label = format!("{name} level {level}");
assert_eq!(decoder.decompress(&frame).expect(&label), input, "{label}");
let decoded = decode_fixed(&mut decoder, &frame, 65_536, 100_000).expect(&label);
assert_eq!(decoded, input, "{label} streaming");
}
}
let input = mixed(20_000_000, 7);
for (level, long) in [(3, true), (19, true), (22, false)] {
let params = Params::level(level)
.with(
zstd_ffi::ZSTD_c_enableLongDistanceMatching,
if long { 1 } else { 0 },
)
.with(zstd_ffi::ZSTD_c_windowLog, 27)
.with(zstd_ffi::ZSTD_c_checksumFlag, 1);
let frame = stream_unknown_size(¶ms, &input);
let decoded = decode_fixed(&mut decoder, &frame, 1 << 17, 1 << 17).expect("long window");
assert_eq!(decoded, input, "level {level} long {long}");
}
}