archmeld 1.3.0

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.
//!
// Binary parser: indexing, arithmetic, and numeric casts are
// fundamental to format parsing. Safety is ensured by fuzzing.
#![allow(clippy::indexing_slicing)]
#![allow(clippy::arithmetic_side_effects)]
#![allow(clippy::as_conversions)]
#![allow(clippy::cast_possible_truncation)]
#![allow(clippy::cast_possible_wrap)]
#![allow(clippy::cast_sign_loss)]

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 = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
    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[4];
    let bd = data[5];

    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 && data.len() >= 14 {
        Some(u64::from_le_bytes([
            data[6], data[7], data[8], data[9], data[10], data[11], data[12], data[13],
        ]))
    } 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
/// (reads prepended size).
///
/// Rejects payloads claiming more than 256 MiB
/// uncompressed to prevent memory exhaustion attacks.
///
/// # Errors
///
/// Returns error if decompression fails or the
/// declared size exceeds the safety limit.
#[allow(dead_code)]
pub fn decompress_block_safe(data: &[u8]) -> Result<Vec<u8>> {
    // Safety limit: 256 MiB
    const MAX_UNCOMPRESSED: u32 = 256 * 1024 * 1024;

    // lz4_flex prepends the uncompressed size as a
    // little-endian u32 (4 bytes).
    if data.len() < 4 {
        return Err(Error::Lz4("block too short for size header".into()));
    }
    let declared = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
    if declared > MAX_UNCOMPRESSED {
        return Err(Error::Lz4(format!(
            "declared block size {} exceeds \
             safety limit of {} bytes",
            declared, MAX_UNCOMPRESSED
        )));
    }

    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)]
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());
    }
}