mod support;
use zstd_zero::{Decoder, DecoderOptions, MAX_BLOCK_SIZE};
fn decode_zero(compressed: &[u8], history_size: usize) -> Result<Vec<u8>, String> {
decode_with(compressed, history_size, DecoderOptions::default())
}
fn decode_with(
compressed: &[u8],
history_size: usize,
options: DecoderOptions,
) -> Result<Vec<u8>, String> {
let mut buffers = support::Buffers::new(history_size, MAX_BLOCK_SIZE, MAX_BLOCK_SIZE);
let mut decoder = Decoder::with_options(buffers.as_decoder_buffers(), options).unwrap();
let mut out = Vec::new();
let mut collect = |bytes: &[u8]| {
out.extend_from_slice(bytes);
Ok::<_, core::convert::Infallible>(())
};
decoder
.push(compressed, &mut collect)
.map_err(|error| format!("{error:?}"))?;
decoder
.finish_with(collect)
.map_err(|error| format!("{error:?}"))?;
Ok(out)
}
fn payload(size: usize) -> Vec<u8> {
let words = [
"the", "quick", "brown", "fox", "jumps", "over", "lazy", "dog", "and", "then", "runs",
"away", "into", "forest", "where", "nothing", "happens", "at", "all", "today",
];
let mut out = Vec::with_capacity(size + 16);
let mut state = 0x1234_5678_9abc_def0u64;
while out.len() < size {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
out.extend_from_slice(words[(state % words.len() as u64) as usize].as_bytes());
out.push(b' ');
}
out
}
#[test]
fn accepted_corrupt_frames_match_the_reference_decoder() {
let source = payload(12 * 1024);
let clean = zstd::bulk::compress(&source, 9).unwrap();
assert_eq!(
decode_zero(&clean, source.len()).unwrap(),
source,
"clean frame must round-trip"
);
let mut divergences = Vec::new();
let mut accepted = 0usize;
for position in 0..clean.len() {
for mask in [0x01u8, 0x40, 0x80] {
let mut bad = clean.clone();
bad[position] ^= mask;
let ours = match decode_zero(&bad, source.len()) {
Err(_) => continue,
Ok(output) => output,
};
accepted += 1;
match zstd::bulk::decompress(&bad, source.len() * 2) {
Ok(theirs) if theirs == ours => {}
Ok(theirs) => {
let first = ours.iter().zip(&theirs).position(|(a, b)| a != b);
divergences.push(format!(
"byte {position} ^ {mask:#04x}: accepted but differs from reference \
(ours {} bytes, reference {} bytes, first difference at {first:?})",
ours.len(),
theirs.len()
));
}
Err(_) => divergences.push(format!(
"byte {position} ^ {mask:#04x}: accepted a frame the reference rejects \
({} bytes)",
ours.len()
)),
}
}
}
assert!(
divergences.is_empty(),
"{} of {accepted} accepted frames diverge from the reference decoder:\n {}",
divergences.len(),
divergences
.iter()
.take(10)
.cloned()
.collect::<Vec<_>>()
.join("\n ")
);
}
#[test]
fn strict_mode_rejects_what_lenient_mode_tolerates() {
let source = payload(12 * 1024);
let clean = zstd::bulk::compress(&source, 9).unwrap();
let lenient = DecoderOptions {
strict_literal_bitstream: false,
};
assert_eq!(decode_zero(&clean, source.len()).unwrap(), source);
assert_eq!(
decode_with(&clean, source.len(), lenient).unwrap(),
source,
"lenient mode must not change valid decoding"
);
let mut lenient_only = 0usize;
for position in 0..clean.len() {
for mask in [0x01u8, 0x40, 0x80] {
let mut bad = clean.clone();
bad[position] ^= mask;
let strict = decode_zero(&bad, source.len());
let loose = decode_with(&bad, source.len(), lenient);
if strict.is_err() {
if let Ok(output) = loose {
lenient_only += 1;
assert_ne!(
output, source,
"byte {position} ^ {mask:#04x}: lenient mode accepted corruption \
yet reproduced the original bytes"
);
}
}
}
}
assert!(
lenient_only > 0,
"expected lenient mode to accept some frames strict mode rejects; \
the option may no longer have any effect"
);
}
#[test]
fn rejects_saved_truncated_literal_regressions() {
for frame in [
include_bytes!("fixtures/wrong-output.zst").as_slice(),
include_bytes!("fixtures/accepted-corruption.zst").as_slice(),
] {
assert!(decode_zero(frame, 1024).is_err());
}
}