use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
fn repo_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(|p| p.parent())
.expect("crates/rusty_zstd -> repo root")
.to_path_buf()
}
fn find_oracle() -> Option<PathBuf> {
if let Ok(p) = std::env::var("RUSTY_ZSTD_ORACLE") {
let pb = PathBuf::from(p);
if pb.is_file() {
return Some(pb);
}
}
let extracted = repo_root().join("third_party/zstd/extracted");
walk(&extracted).ok()?.into_iter().find(|p| {
p.file_name()
.and_then(|n| n.to_str())
.is_some_and(|n| n.eq_ignore_ascii_case("zstd.exe") || n.eq_ignore_ascii_case("zstd"))
})
}
fn walk(dir: &Path) -> std::io::Result<Vec<PathBuf>> {
let mut out = Vec::new();
if !dir.is_dir() {
return Ok(out);
}
for ent in fs::read_dir(dir)? {
let p = ent?.path();
if p.is_dir() {
out.extend(walk(&p)?);
} else {
out.push(p);
}
}
Ok(out)
}
fn c_compress(oracle: &Path, src: &[u8], level: i32) -> Vec<u8> {
let nonce = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let tmp = std::env::temp_dir().join(format!("rzstd-m1-{}-{}-{}", level, src.len(), nonce));
let inn = tmp.with_extension("in");
let out = tmp.with_extension("zst");
fs::write(&inn, src).unwrap();
let status = Command::new(oracle)
.args([
"-f",
"-q",
&format!("-{level}"),
"-o",
out.to_str().unwrap(),
inn.to_str().unwrap(),
])
.status()
.expect("spawn zstd");
assert!(status.success(), "zstd -{level} failed");
let zst = fs::read(&out).unwrap();
let _ = fs::remove_file(&inn);
let _ = fs::remove_file(&out);
zst
}
fn xorshift_bytes(seed: u64, n: usize) -> Vec<u8> {
let mut s = seed;
let mut v = vec![0u8; n];
for b in &mut v {
s ^= s << 13;
s ^= s >> 7;
s ^= s << 17;
*b = (s & 0xFF) as u8;
if *b == 0 {
*b = 1;
}
}
v
}
fn assert_roundtrip(oracle: &Path, src: &[u8], level: i32) {
let zst = c_compress(oracle, src, level);
let got = rusty_zstd::decompress(&zst).unwrap_or_else(|e| {
panic!(
"decompress L{level} {} bytes: {e:?} zst={}",
src.len(),
zst.len()
)
});
if got.len() != src.len() {
panic!(
"len mismatch L{level}: got {} want {}",
got.len(),
src.len()
);
}
if got != src {
let pos = got
.iter()
.zip(src.iter())
.position(|(a, b)| a != b)
.unwrap_or(got.len());
panic!(
"mismatch L{level} at byte {pos}/{} got={:02x} want={:02x}",
src.len(),
got.get(pos).copied().unwrap_or(0),
src.get(pos).copied().unwrap_or(0)
);
}
}
#[test]
fn c_small_corpus() {
let Some(oracle) = find_oracle() else {
eprintln!("skip: pinned C zstd not found");
return;
};
let fox = b"The quick brown fox jumps over the lazy dog. 0123456789.\n";
let mut text = Vec::new();
while text.len() < 4096 {
text.extend_from_slice(fox);
}
for level in [1, 3, 19] {
assert_roundtrip(&oracle, b"", level);
assert_roundtrip(&oracle, b"a", level);
assert_roundtrip(&oracle, b"hello", level);
assert_roundtrip(&oracle, &[0u8; 16], level);
assert_roundtrip(&oracle, &vec![0u8; 256], level);
assert_roundtrip(&oracle, &vec![0u8; 4096], level);
assert_roundtrip(&oracle, &text, level);
assert_roundtrip(&oracle, &xorshift_bytes(0xA5A5_5A5A, 1024), level);
assert_roundtrip(&oracle, &xorshift_bytes(0xA5A5_5A5A, 128 * 1024), level);
assert_roundtrip(&oracle, &xorshift_bytes(0xA5A5_5A5A, 256 * 1024), level);
assert_roundtrip(&oracle, &xorshift_bytes(0xA5A5_5A5A, 1024 * 1024), level);
}
}
#[test]
fn c_holdout_incomp_32m() {
let Some(oracle) = find_oracle() else {
eprintln!("skip: pinned C zstd not found");
return;
};
let n = 32 * 1024 * 1024;
let src = xorshift_bytes(0xA5A5_5A5A, n);
for level in [1, 3] {
assert_roundtrip(&oracle, &src, level);
}
}
#[test]
fn c_train_zeros_and_text_32m() {
let Some(oracle) = find_oracle() else {
eprintln!("skip: pinned C zstd not found");
return;
};
let n = 32 * 1024 * 1024;
let zeros = vec![0u8; n];
let fox = b"The quick brown fox jumps over the lazy dog. 0123456789.\n";
let mut text = Vec::with_capacity(n);
while text.len() < n {
let take = (n - text.len()).min(fox.len());
text.extend_from_slice(&fox[..take]);
}
for level in [1, 3] {
assert_roundtrip(&oracle, &zeros, level);
assert_roundtrip(&oracle, &text, level);
}
}
#[test]
fn oversized_hash_log_does_not_panic() {
let src: Vec<u8> = (0..2_000_000u32)
.map(|i| (i.wrapping_mul(2_654_435_761) >> 13) as u8)
.collect();
for hlog in [24u32, 25, 26, 28, 30, 31] {
for lvl in [1i32, 3, 5, 7, 9, 12, 13, 19] {
let mut p = rusty_zstd::compression_params(lvl, Some(src.len() as u64)).unwrap();
p.hash_log = hlog;
let z = rusty_zstd::compress_with_params(&src, p, false)
.unwrap_or_else(|e| panic!("hlog {hlog} L{lvl} failed: {e:?}"));
assert_eq!(
rusty_zstd::decompress(&z).unwrap(),
src,
"hlog {hlog} L{lvl} round-trip"
);
}
}
}
#[test]
fn compression_is_independent_of_call_history() {
let a: Vec<u8> = (0..600_000u32)
.map(|i| (i.wrapping_mul(2_654_435_761) >> 15) as u8)
.collect();
let mut b = Vec::with_capacity(600_000);
while b.len() < 600_000 {
b.extend_from_slice(b"the quick brown fox jumps over the lazy dog 0123456789 ");
}
for lvl in [1i32, 3, 9, 13, 16, 19, 22] {
let want_a = rusty_zstd::compress(&a, lvl).unwrap();
let want_b = rusty_zstd::compress(&b, lvl).unwrap();
for _ in 0..3 {
let _ = rusty_zstd::compress(&b, lvl).unwrap();
assert_eq!(
rusty_zstd::compress(&a, lvl).unwrap(),
want_a,
"L{lvl}: compressing A depended on what preceded it"
);
let _ = rusty_zstd::compress(&a, lvl).unwrap();
assert_eq!(
rusty_zstd::compress(&b, lvl).unwrap(),
want_b,
"L{lvl}: compressing B depended on what preceded it"
);
}
}
}
#[test]
fn bt_specialisation_covers_every_input_size() {
let mut uncovered = Vec::new();
let mut n: u64 = 1024;
while n <= (64 << 20) {
for lvl in [13i32, 14, 15, 16, 17, 18, 19, 20, 21, 22] {
let p = rusty_zstd::compression_params(lvl, Some(n)).unwrap();
let pair = (p.hash_log.min(24), p.chain_log.min(24));
if !rusty_zstd::BT_SPEC_PAIRS.contains(&pair) {
uncovered.push((n >> 10, lvl, pair));
}
}
n += (n / 4).max(1024);
}
assert!(
uncovered.is_empty(),
"bt specialisation misses (KiB, level, (hash_log, chain_log)): {uncovered:?}"
);
let big: Vec<u8> = (0..(3 << 20) as u32)
.map(|i| (i.wrapping_mul(2_654_435_761) >> 13) as u8)
.collect();
for &sz in &[64 << 10, 512 << 10, 1 << 20, 3 << 20] {
let src = &big[..sz.min(big.len())];
for lvl in [13i32, 17, 19, 22] {
rusty_zstd::set_bt_spec_arm(false);
let a = rusty_zstd::compress(src, lvl).unwrap();
rusty_zstd::set_bt_spec_arm(true);
let b = rusty_zstd::compress(src, lvl).unwrap();
assert_eq!(a, b, "specialised body differs from runtime at {sz} L{lvl}");
assert_eq!(rusty_zstd::decompress(&b).unwrap(), src);
}
}
}
#[test]
fn out_of_range_parameters_never_panic() {
let src: Vec<u8> = (0..600_000u32)
.map(|i| (i.wrapping_mul(2_654_435_761) >> 13) as u8)
.collect();
let vals = [
0u32,
1,
2,
3,
5,
8,
16,
24,
31,
32,
64,
1024,
65535,
u32::MAX,
];
type Setter = fn(&mut rusty_zstd::CompressionParameters, u32);
let fields: &[(&str, Setter)] = &[
("window_log", |p, v| p.window_log = v),
("hash_log", |p, v| p.hash_log = v),
("chain_log", |p, v| p.chain_log = v),
("search_log", |p, v| p.search_log = v),
("min_match", |p, v| p.min_match = v),
("target_length", |p, v| p.target_length = v),
];
for (name, set) in fields {
for &v in &vals {
for lvl in [1i32, 3, 7, 13, 19, 22] {
let Ok(mut p) = rusty_zstd::compression_params(lvl, Some(src.len() as u64)) else {
continue;
};
set(&mut p, v);
if let Ok(z) = rusty_zstd::compress_with_params(&src, p, false) {
assert_eq!(
rusty_zstd::decompress(&z).unwrap(),
src,
"round-trip broken with {name} = {v} at L{lvl}"
);
}
}
}
}
}