use std::io::{Error as IoError, ErrorKind};
use crate::prelude::*;
const HASH_SIZE: usize = 1 << 14;
const MAX_OFFSET: usize = 0x1FFF;
const MAX_REF_LEN: usize = 264;
const MAX_LITERAL: usize = 32;
pub const MIN_COMPRESS_LEN: usize = 20;
#[inline]
fn hash(data: &[u8], pos: usize) -> usize {
let v =
u32::from(data[pos]) | (u32::from(data[pos + 1]) << 8) | (u32::from(data[pos + 2]) << 16);
((v.wrapping_mul(0x1E35_A7BD)) >> 18) as usize & (HASH_SIZE - 1)
}
#[allow(clippy::cast_possible_truncation)]
#[must_use]
pub fn compress(input: &[u8]) -> Option<Vec<u8>> {
if input.len() < 4 {
return None;
}
let mut output = Vec::with_capacity(input.len());
let mut hash_table = vec![usize::MAX; HASH_SIZE];
let mut ip = 0;
let mut anchor = 0;
while ip < input.len() - 2 {
let h = hash(input, ip);
let ref_pos = hash_table[h];
hash_table[h] = ip;
if ref_pos != usize::MAX
&& ip > ref_pos
&& ip - ref_pos <= MAX_OFFSET
&& input[ref_pos] == input[ip]
&& input[ref_pos + 1] == input[ip + 1]
&& input[ref_pos + 2] == input[ip + 2]
{
if ip > anchor {
emit_literals(&mut output, &input[anchor..ip]);
}
let offset = ip - ref_pos;
let mut match_len = 3;
let max_len = (input.len() - ip).min(MAX_REF_LEN);
while match_len < max_len && input[ref_pos + match_len] == input[ip + match_len] {
match_len += 1;
}
emit_backref(&mut output, offset, match_len);
let match_end = ip + match_len;
ip += 1;
while ip < match_end.saturating_sub(2) && ip < input.len() - 2 {
let h = hash(input, ip);
hash_table[h] = ip;
ip += 1;
}
ip = match_end;
anchor = ip;
} else {
ip += 1;
}
}
if anchor < input.len() {
emit_literals(&mut output, &input[anchor..]);
}
if output.len() < input.len() {
Some(output)
} else {
None
}
}
#[allow(clippy::cast_possible_truncation)]
fn emit_literals(output: &mut Vec<u8>, data: &[u8]) {
let mut pos = 0;
while pos < data.len() {
let chunk_len = (data.len() - pos).min(MAX_LITERAL);
output.push((chunk_len - 1) as u8);
output.extend_from_slice(&data[pos..pos + chunk_len]);
pos += chunk_len;
}
}
#[allow(clippy::cast_possible_truncation)]
fn emit_backref(output: &mut Vec<u8>, offset: usize, len: usize) {
debug_assert!(len >= 3 && (1..=MAX_OFFSET).contains(&offset));
let len_code = len - 2;
let offset_minus_1 = offset - 1;
if len_code < 7 {
output.push(((len_code << 5) | (offset_minus_1 >> 8)) as u8);
} else {
output.push((7 << 5) as u8 | (offset_minus_1 >> 8) as u8);
output.push((len_code - 7) as u8);
}
output.push((offset_minus_1 & 0xFF) as u8);
}
#[allow(clippy::cast_possible_truncation)]
pub fn decompress(compressed: &[u8], uncompressed_len: usize) -> Result<Vec<u8>> {
let mut output = Vec::with_capacity(uncompressed_len);
let mut i = 0;
while i < compressed.len() {
let ctrl = compressed[i];
i += 1;
if ctrl < 32 {
let len = (ctrl as usize) + 1;
if i + len > compressed.len() {
return Err(IoError::new(
ErrorKind::InvalidData,
"LZF: unexpected end of compressed data in literal run",
)
.into());
}
output.extend_from_slice(&compressed[i..i + len]);
i += len;
} else {
let mut len = (ctrl >> 5) as usize;
let mut offset = ((ctrl & 0x1F) as usize) << 8;
if len == 7 {
if i >= compressed.len() {
return Err(IoError::new(
ErrorKind::InvalidData,
"LZF: unexpected end of compressed data reading extended length",
)
.into());
}
len += compressed[i] as usize;
i += 1;
}
if i >= compressed.len() {
return Err(IoError::new(
ErrorKind::InvalidData,
"LZF: unexpected end of compressed data reading offset",
)
.into());
}
offset |= compressed[i] as usize;
i += 1;
offset += 1;
len += 2;
if offset > output.len() {
return Err(IoError::new(
ErrorKind::InvalidData,
format!(
"LZF: back-reference offset {} exceeds output length {}",
offset,
output.len()
),
)
.into());
}
let start = output.len() - offset;
for j in 0..len {
let byte = output[start + j];
output.push(byte);
}
}
}
if output.len() != uncompressed_len {
return Err(IoError::new(
ErrorKind::InvalidData,
format!(
"LZF: decompressed size {} doesn't match expected {}",
output.len(),
uncompressed_len
),
)
.into());
}
Ok(output)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_decompress_literal_only() {
let compressed = [0x04, b'h', b'e', b'l', b'l', b'o'];
let result = decompress(&compressed, 5).unwrap();
assert_eq!(result, b"hello");
}
#[test]
fn test_decompress_with_backref() {
let compressed = [0x02, b'a', b'b', b'c', 0x20, 0x02];
let result = decompress(&compressed, 6).unwrap();
assert_eq!(result, b"abcabc");
}
#[test]
fn test_decompress_run_length() {
let compressed = [0x00, b'a', 0x20, 0x00]; let result = decompress(&compressed, 4).unwrap();
assert_eq!(result, b"aaaa");
}
#[test]
fn test_decompress_empty() {
let compressed: [u8; 0] = [];
let result = decompress(&compressed, 0).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_decompress_long_literal() {
let mut compressed = vec![0x1E]; compressed.extend_from_slice(&[b'x'; 31]);
let result = decompress(&compressed, 31).unwrap();
assert_eq!(result, vec![b'x'; 31]);
}
#[test]
fn test_decompress_size_mismatch() {
let compressed = [0x02, b'a', b'b', b'c'];
let result = decompress(&compressed, 5);
assert!(result.is_err());
}
#[test]
fn test_decompress_invalid_backref() {
let compressed = [0x00, b'a', 0x20, 0x05]; let result = decompress(&compressed, 4);
assert!(result.is_err());
}
}