#[cfg(not(feature = "std"))]
use alloc::{format, vec, vec::Vec};
use crate::error::FormatError;
const MAX_LITERAL_RUN: usize = 32;
const MAX_MATCH_LEN: usize = 264;
const MAX_MATCH_DISTANCE: usize = 1 << 13;
const HASH_TABLE_SLOTS: usize = 1 << 13;
const HASH_TABLE_BITS: u32 = HASH_TABLE_SLOTS.trailing_zeros();
pub(crate) const MAX_EXPANSION: usize = MAX_MATCH_LEN / 3;
const H5PY_FILTER_LZF_VERSION: u32 = 4;
const LIBLZF_API_VERSION: u32 = 0x0105;
pub(crate) fn h5py_cd_values(element_size: u32, chunk_dims: &[u64]) -> [u32; 3] {
let chunk_bytes = chunk_dims
.iter()
.try_fold(u64::from(element_size), |acc, &d| acc.checked_mul(d))
.and_then(|b| u32::try_from(b).ok())
.unwrap_or(0);
[H5PY_FILTER_LZF_VERSION, LIBLZF_API_VERSION, chunk_bytes]
}
fn corrupt(reason: &str) -> FormatError {
FormatError::FilterError(format!("lzf: {reason}"))
}
pub(crate) fn decompress(input: &[u8], max_output: Option<usize>) -> Result<Vec<u8>, FormatError> {
let cap = max_output.unwrap_or(usize::MAX);
let mut out = Vec::with_capacity(crate::filters::decode_reservation(
max_output,
input.len(),
MAX_EXPANSION,
));
let mut ip = 0;
while ip < input.len() {
let ctrl = usize::from(input[ip]);
ip += 1;
if ctrl < MAX_LITERAL_RUN {
let len = ctrl + 1;
let literals = input
.get(ip..ip + len)
.ok_or_else(|| corrupt("truncated literal run"))?;
if out.len() + len > cap {
return Err(corrupt("output exceeds expected chunk size"));
}
out.extend_from_slice(literals);
ip += len;
} else {
let mut len = ctrl >> 5;
if len == 7 {
len += usize::from(
*input
.get(ip)
.ok_or_else(|| corrupt("truncated match length"))?,
);
ip += 1;
}
len += 2;
let low = usize::from(
*input
.get(ip)
.ok_or_else(|| corrupt("truncated match offset"))?,
);
ip += 1;
let distance = (((ctrl & 0x1f) << 8) | low) + 1;
if distance > out.len() {
return Err(corrupt("match reaches before start of output"));
}
if out.len() + len > cap {
return Err(corrupt("output exceeds expected chunk size"));
}
let start = out.len() - distance;
if distance >= len {
out.extend_from_within(start..start + len);
} else {
for i in start..start + len {
let byte = out[i];
out.push(byte);
}
}
}
}
Ok(out)
}
pub(crate) fn compress(input: &[u8]) -> Vec<u8> {
fn hash(a: u8, b: u8, c: u8) -> usize {
let v = (usize::from(a) << 16) | (usize::from(b) << 8) | usize::from(c);
(v.wrapping_mul(0x9E37_79B1) >> (32 - HASH_TABLE_BITS)) & (HASH_TABLE_SLOTS - 1)
}
fn flush_literals(out: &mut Vec<u8>, input: &[u8], from: usize, to: usize) {
let mut i = from;
while i < to {
let n = (to - i).min(MAX_LITERAL_RUN);
#[expect(clippy::cast_possible_truncation)]
out.push((n - 1) as u8);
out.extend_from_slice(&input[i..i + n]);
i += n;
}
}
let mut out = Vec::with_capacity(input.len() + input.len() / MAX_LITERAL_RUN + 2);
let mut table = vec![0_usize; HASH_TABLE_SLOTS];
let mut ip = 0;
let mut literal_start = 0;
while ip + 2 < input.len() {
let slot = hash(input[ip], input[ip + 1], input[ip + 2]);
let candidate = table[slot];
table[slot] = ip + 1;
if candidate > 0 {
let match_pos = candidate - 1;
let distance = ip - match_pos;
if (1..=MAX_MATCH_DISTANCE).contains(&distance)
&& input[match_pos..match_pos + 3] == input[ip..ip + 3]
{
let max_len = (input.len() - ip).min(MAX_MATCH_LEN);
let mut len = 3;
while len < max_len && input[match_pos + len] == input[ip + len] {
len += 1;
}
flush_literals(&mut out, input, literal_start, ip);
let off = distance - 1;
let encoded_len = len - 2;
#[expect(clippy::cast_possible_truncation)]
if encoded_len < 7 {
out.push(((encoded_len << 5) | (off >> 8)) as u8);
} else {
out.push(((7 << 5) | (off >> 8)) as u8);
out.push((encoded_len - 7) as u8);
}
#[expect(clippy::cast_possible_truncation)]
out.push((off & 0xff) as u8);
ip += len;
literal_start = ip;
continue;
}
}
ip += 1;
}
flush_literals(&mut out, input, literal_start, input.len());
out
}
#[cfg(all(test, feature = "std"))]
#[path = "lzf_crosscheck.rs"]
mod lzf_crosscheck;
#[cfg(test)]
mod tests {
use super::*;
fn round_trip(data: &[u8]) {
let compressed = compress(data);
let decompressed = decompress(&compressed, Some(data.len())).unwrap();
assert_eq!(decompressed, data);
}
#[test]
fn round_trips() {
round_trip(b"");
round_trip(b"a");
round_trip(b"hello world hello world hello world");
round_trip(&[0_u8; 10_000]);
round_trip(&(0..=255).cycle().take(70_000).collect::<Vec<u8>>());
let mut x = 0x2545_F491_4F6C_DD1D_u64;
let noise: Vec<u8> = (0..50_000)
.map(|_| {
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
(x & 0xff) as u8
})
.collect();
round_trip(&noise);
}
#[test]
fn known_stream_decodes() {
let stream = [4, b'a', b'b', b'c', b'd', b'e', 3 << 5, 4];
assert_eq!(decompress(&stream, None).unwrap(), b"abcdeabcde");
}
#[test]
fn worst_case_expansion_stream_decodes() {
let stream: Vec<u8> = (0..=255u8).flat_map(|b| [0, b]).collect();
let expected: Vec<u8> = (0..=255).collect();
assert_eq!(stream.len(), 2 * expected.len());
assert_eq!(decompress(&stream, Some(expected.len())).unwrap(), expected);
}
#[cfg(feature = "std")]
#[test]
fn compresses_on_a_stack_smaller_than_the_table() {
let stack = HASH_TABLE_SLOTS * size_of::<usize>() * 3 / 4;
let data: Vec<u8> = (0..=255).cycle().take(70_000).collect();
let expected = data.clone();
let out = std::thread::Builder::new()
.stack_size(stack)
.spawn(move || compress(&data))
.expect("spawn small-stack thread")
.join()
.expect("compress must not overflow a stack smaller than the table");
assert_eq!(decompress(&out, Some(expected.len())).unwrap(), expected);
}
#[test]
fn corrupt_streams_error() {
assert!(decompress(&[10, b'x'], None).is_err());
assert!(decompress(&[(3 << 5), 200], None).is_err());
assert!(decompress(&[4, b'a', b'b', b'c', b'd', b'e'], Some(3)).is_err());
}
}