#![cfg(feature = "lha")]
use compcol::lha::{DecoderConfig, Lh1, Lh4, Lh5, Lh6, Lh7};
use compcol::{Algorithm, Decoder, Encoder, Error, Status};
fn run_stream<F>(mut step: F, data: &[u8], in_chunk: usize, out_chunk: usize) -> Vec<u8>
where
F: FnMut(&[u8], &mut [u8]) -> (compcol::Progress, Status),
{
let mut out = Vec::new();
let mut buf = vec![0u8; out_chunk.max(1)];
let mut consumed = 0usize;
while consumed < data.len() {
let end = (consumed + in_chunk.max(1)).min(data.len());
loop {
let (p, status) = step(&data[consumed..end], &mut buf);
out.extend_from_slice(&buf[..p.written]);
consumed += p.consumed;
match status {
Status::OutputFull => continue,
_ => break,
}
}
}
out
}
fn encode_chunked<E: Encoder>(
mut enc: E,
data: &[u8],
in_chunk: usize,
out_chunk: usize,
) -> Vec<u8> {
let mut out = run_stream(
|inp, buf| enc.encode(inp, buf).unwrap(),
data,
in_chunk,
out_chunk,
);
let mut buf = vec![0u8; out_chunk.max(1)];
loop {
let (p, status) = enc.finish(&mut buf).unwrap();
out.extend_from_slice(&buf[..p.written]);
if matches!(status, Status::StreamEnd) {
break;
}
}
out
}
fn decode_chunked<D: Decoder>(
mut dec: D,
data: &[u8],
in_chunk: usize,
out_chunk: usize,
) -> Vec<u8> {
let mut out = run_stream(
|inp, buf| dec.decode(inp, buf).unwrap(),
data,
in_chunk,
out_chunk,
);
let mut buf = vec![0u8; out_chunk.max(1)];
loop {
let (p, status) = dec.finish(&mut buf).unwrap();
out.extend_from_slice(&buf[..p.written]);
if matches!(status, Status::StreamEnd) {
break;
}
if p.written == 0 {
break;
}
}
out
}
fn roundtrip_mode<A: Algorithm<DecoderConfig = DecoderConfig>>(data: &[u8], with_len: bool) {
for &(ic, oc) in &[(1 << 20, 1 << 20), (1, 1), (7, 3), (64, 16)] {
let enc = A::encoder();
let encoded = encode_chunked(enc, data, ic, oc);
let dec = if with_len {
A::decoder_with(DecoderConfig::with_len(data.len()))
} else {
A::decoder()
};
let decoded = decode_chunked(dec, &encoded, ic, oc);
assert_eq!(
decoded,
data,
"round-trip mismatch for {} (with_len={}, in={}, out={}, len={})",
A::NAME,
with_len,
ic,
oc,
data.len()
);
}
}
fn roundtrip_static<A: Algorithm<DecoderConfig = DecoderConfig>>(data: &[u8]) {
roundtrip_mode::<A>(data, false);
roundtrip_mode::<A>(data, true);
}
fn sample_inputs() -> Vec<Vec<u8>> {
let mut v: Vec<Vec<u8>> = vec![
Vec::new(),
b"a".to_vec(),
b"hello world".to_vec(),
b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_vec(),
b"abcabcabcabcabcabcabcabcabcabcabcabcabcabcabc".to_vec(),
];
let mut big = Vec::new();
for i in 0..2000u32 {
big.extend_from_slice(format!("The quick brown fox {} jumps. ", i % 13).as_bytes());
}
v.push(big);
let mut rng = 0x12345678u32;
let mut rnd = Vec::with_capacity(5000);
for _ in 0..5000 {
rng = rng.wrapping_mul(1103515245).wrapping_add(12345);
rnd.push((rng >> 16) as u8);
}
v.push(rnd);
let mut allbytes = Vec::new();
for _ in 0..40 {
for b in 0..=255u8 {
allbytes.push(b);
}
}
v.push(allbytes);
v
}
#[test]
fn roundtrip_lh5() {
for data in sample_inputs() {
roundtrip_static::<Lh5>(&data);
}
}
#[test]
fn roundtrip_lh4() {
for data in sample_inputs() {
roundtrip_static::<Lh4>(&data);
}
}
#[test]
fn roundtrip_lh6() {
for data in sample_inputs() {
roundtrip_static::<Lh6>(&data);
}
}
#[test]
fn roundtrip_lh7() {
for data in sample_inputs() {
roundtrip_static::<Lh7>(&data);
}
}
#[test]
fn roundtrip_lh1() {
for data in sample_inputs() {
roundtrip_mode::<Lh1>(&data, true);
}
}
#[test]
fn large_window_match_lh7() {
let mut data = vec![0u8; 100_000];
for (i, b) in data.iter_mut().enumerate() {
*b = (i % 251) as u8;
}
let head: Vec<u8> = data[..5000].to_vec();
data.extend_from_slice(&head);
roundtrip_static::<Lh7>(&data);
}
#[test]
fn truncated_payload_errors_cleanly() {
let data = b"the quick brown fox jumps over the lazy dog, repeatedly!".repeat(20);
let enc = Lh5::encoder();
let encoded = encode_chunked(enc, &data, 1 << 16, 1 << 16);
let truncated = &encoded[..(encoded.len().min(8))];
let mut dec = Lh5::decoder();
let mut buf = vec![0u8; 4096];
let (_p, _s) = dec.decode(truncated, &mut buf).unwrap();
let res = dec.finish(&mut buf);
match res {
Ok((p, _)) => assert!(p.written <= data.len()),
Err(e) => assert!(matches!(
e,
Error::Corrupt
| Error::UnexpectedEnd
| Error::InvalidHuffmanTree
| Error::InvalidDistance
)),
}
}
#[test]
fn garbage_payload_errors_cleanly() {
let mut stream = Vec::new();
let mut rng = 0xdead_beefu32;
for _ in 0..200 {
rng = rng.wrapping_mul(1103515245).wrapping_add(12345);
stream.push((rng >> 16) as u8);
}
let decoders: [Box<dyn Decoder>; 3] = [
Box::new(Lh5::decoder()),
Box::new(Lh1::decoder()),
Box::new(Lh6::decoder()),
];
for mut dec in decoders {
let mut buf = vec![0u8; 8192];
let _ = dec.decode(&stream, &mut buf);
let mut produced = 0usize;
while let Ok((p, status)) = dec.finish(&mut buf) {
produced += p.written;
assert!(produced <= 4 * 1024 * 1024);
if matches!(status, Status::StreamEnd) || p.written == 0 {
break;
}
}
}
}
#[test]
fn empty_input_to_decoder_is_unexpected_end_or_empty() {
let mut dec = Lh5::decoder();
let mut buf = vec![0u8; 16];
let (p, status) = dec.finish(&mut buf).unwrap();
assert_eq!(p.written, 0);
assert!(matches!(status, Status::StreamEnd));
}
#[test]
fn names_registered() {
#[cfg(feature = "factory")]
{
let names = compcol::factory::names();
for n in ["lh1", "lh4", "lh5", "lh6", "lh7"] {
assert!(names.contains(&n), "{n} not registered");
assert!(compcol::factory::encoder_by_name(n).is_some());
assert!(compcol::factory::decoder_by_name(n).is_some());
}
}
}