archmeld 0.1.5

Secure, memory-safe, type-safe CLI for multi-format archive extraction, inspection and decompression
Documentation
//! LZ4 decompression utilities (inspired by `traceflight/lz4_decompress`).
//!
//! Provides both LZ4 block and frame format decompression using `lz4_flex`,
//! a pure-Rust, memory-safe LZ4 implementation.

use std::io::{self, Read};

use crate::error::{Error, Result};

/// Information about an LZ4 frame.
#[derive(Debug, Clone, serde::Serialize)]
pub struct Lz4FrameInfo {
    /// Whether the frame uses block independence.
    pub block_independent: bool,
    /// Whether block checksums are present.
    pub block_checksum: bool,
    /// Whether a content size field is present.
    pub content_size_present: bool,
    /// Content size (if present in the header).
    pub content_size: Option<u64>,
    /// Whether a content checksum is present.
    pub content_checksum: bool,
    /// Block maximum size code.
    pub block_max_size: u32,
}

/// LZ4 frame magic number.
const LZ4_FRAME_MAGIC: u32 = 0x184D_2204;

/// Parse LZ4 frame header information without decompressing.
///
/// # Errors
///
/// Returns error if the data is not a valid LZ4 frame.
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,
    })
}

/// Decompress LZ4 frame data.
///
/// # Errors
///
/// Returns error if decompression fails.
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)
}

/// Decompress LZ4 block data with a known uncompressed size.
///
/// # Errors
///
/// Returns error if decompression fails.
#[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()))
}

/// Decompress LZ4 block data without knowing the size (uses safe upper bound).
///
/// # Errors
///
/// Returns error if decompression fails.
#[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()))
}

/// Compress data using LZ4 frame format.
///
/// # Errors
///
/// Returns error on I/O failure.
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());
    }
}