use std::io::{self, Read};
use crate::error::{Error, Result};
#[derive(Debug, Clone, serde::Serialize)]
pub struct Lz4FrameInfo {
pub block_independent: bool,
pub block_checksum: bool,
pub content_size_present: bool,
pub content_size: Option<u64>,
pub content_checksum: bool,
pub block_max_size: u32,
}
const LZ4_FRAME_MAGIC: u32 = 0x184D_2204;
pub fn parse_frame_header(data: &[u8]) -> Result<Lz4FrameInfo> {
if data.len() < 7 {
return Err(Error::Lz4("data too short for LZ4 frame header".into()));
}
let magic_bytes: [u8; 4] = data
.get(..4)
.ok_or_else(|| Error::Lz4("data truncated".into()))?
.try_into()
.map_err(|_| Error::Lz4("data truncated".into()))?;
let magic = u32::from_le_bytes(magic_bytes);
if magic != LZ4_FRAME_MAGIC {
return Err(Error::Lz4(format!(
"invalid LZ4 frame magic: 0x{magic:08X}, expected 0x{LZ4_FRAME_MAGIC:08X}"
)));
}
let flg = data
.get(4)
.copied()
.ok_or_else(|| Error::Lz4("data truncated".into()))?;
let bd = data
.get(5)
.copied()
.ok_or_else(|| Error::Lz4("data truncated".into()))?;
let version = (flg >> 6) & 0x03;
if version != 1 {
return Err(Error::Lz4(format!(
"unsupported LZ4 frame version: {version}"
)));
}
let block_independent = (flg & 0x20) != 0;
let block_checksum = (flg & 0x10) != 0;
let content_size_present = (flg & 0x08) != 0;
let content_checksum = (flg & 0x04) != 0;
let block_max_size_code = (bd >> 4) & 0x07;
let block_max_size = match block_max_size_code {
4 => 64 * 1024,
5 => 256 * 1024,
6 => 1024 * 1024,
7 => 4 * 1024 * 1024,
_ => 0,
};
let content_size = if content_size_present {
let cs_bytes: [u8; 8] = data
.get(6..14)
.ok_or_else(|| Error::Lz4("data truncated: missing content size field".into()))?
.try_into()
.map_err(|_| Error::Lz4("data truncated".into()))?;
Some(u64::from_le_bytes(cs_bytes))
} else {
None
};
Ok(Lz4FrameInfo {
block_independent,
block_checksum,
content_size_present,
content_size,
content_checksum,
block_max_size,
})
}
pub fn decompress_frame(data: &[u8]) -> Result<Vec<u8>> {
let mut decoder = lz4_flex::frame::FrameDecoder::new(data);
let mut out = Vec::new();
decoder
.read_to_end(&mut out)
.map_err(|e| Error::Lz4(e.to_string()))?;
Ok(out)
}
#[allow(dead_code)]
pub fn decompress_block(data: &[u8], uncompressed_size: usize) -> Result<Vec<u8>> {
lz4_flex::decompress(data, uncompressed_size).map_err(|e| Error::Lz4(e.to_string()))
}
#[allow(dead_code)]
pub fn decompress_block_safe(data: &[u8]) -> Result<Vec<u8>> {
lz4_flex::decompress_size_prepended(data).map_err(|e| Error::Lz4(e.to_string()))
}
pub fn compress_frame(data: &[u8]) -> Result<Vec<u8>> {
let mut encoder = lz4_flex::frame::FrameEncoder::new(Vec::new());
io::copy(&mut io::Cursor::new(data), &mut encoder)?;
let out = encoder.finish().map_err(|e| Error::Lz4(e.to_string()))?;
Ok(out)
}
#[cfg(test)]
#[allow(clippy::missing_panics_doc)]
mod tests {
use super::*;
#[test]
fn test_roundtrip_frame() {
let original = b"Hello, LZ4 compression roundtrip test data! ".repeat(100);
let compressed = compress_frame(&original).expect("compress failed");
let decompressed = decompress_frame(&compressed).expect("decompress failed");
assert_eq!(original, decompressed.as_slice());
}
#[test]
fn test_parse_frame_header_valid() {
let original = b"test data for header parsing";
let compressed = compress_frame(original).expect("compress failed");
let info = parse_frame_header(&compressed).expect("parse failed");
assert!(info.block_max_size > 0);
}
#[test]
fn test_parse_frame_header_invalid_magic() {
let data = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
assert!(parse_frame_header(&data).is_err());
}
#[test]
fn test_parse_frame_header_too_short() {
let data = [0x04, 0x22, 0x4D];
assert!(parse_frame_header(&data).is_err());
}
#[test]
fn test_block_roundtrip() {
let original = b"block compression test";
let compressed = lz4_flex::compress_prepend_size(original);
let decompressed = decompress_block_safe(&compressed).expect("decompress failed");
assert_eq!(original.as_slice(), decompressed.as_slice());
}
}