lazippy 0.0.1

Pure-Rust LZMA (Lempel-Ziv-Markov chain Algorithm), part of the 8z umbrella
Documentation
//! LZMA stream header parsing.
//!
//! The LZMA header is 13 bytes:
//! - 1 byte: properties (lc, lp, pb encoded as `(pb*5 + lp)*9 + lc`)
//! - 4 bytes: dictionary size (little-endian u32)
//! - 8 bytes: uncompressed size (little-endian u64, or `0xFFFF_FFFF_FFFF_FFFF` for unknown)

use crate::error::{LazippyError, LazippyResult};

/// Decoded LZMA stream properties.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Properties {
    /// Literal context bits (0–8).
    pub lc: u8,
    /// Literal position bits (0–4).
    pub lp: u8,
    /// Position bits (0–4).
    pub pb: u8,
    /// Dictionary size in bytes.
    pub dict_size: u32,
    /// Uncompressed size, or `None` if unknown (end-of-stream marker used).
    pub uncompressed_size: Option<u64>,
}

/// Parse a 13-byte LZMA stream header.
///
/// Layout: [props_byte(1)] [dict_size_le(4)] [uncompressed_size_le(8)]
///
/// # Errors
/// Returns [`LazippyError::InvalidHeader`] if the input is too short or the
/// properties byte is invalid.
pub fn parse_header(input: &[u8]) -> LazippyResult<Properties> {
    if input.len() < 13 {
        return Err(LazippyError::invalid_header(format!(
            "header too short: need 13 bytes, got {}",
            input.len()
        )));
    }
    let props_byte = input[0];
    if props_byte >= (4 * 5 + 4) * 9 + 9 {
        return Err(LazippyError::InvalidProperties(props_byte));
    }
    let lc = props_byte % 9;
    let remainder = props_byte / 9;
    let lp = remainder % 5;
    let pb = remainder / 5;

    let dict_size = u32::from_le_bytes([input[1], input[2], input[3], input[4]]);
    let raw_size = u64::from_le_bytes([
        input[5], input[6], input[7], input[8], input[9], input[10], input[11], input[12],
    ]);
    let uncompressed_size = if raw_size == u64::MAX {
        None
    } else {
        Some(raw_size)
    };

    Ok(Properties {
        lc,
        lp,
        pb,
        dict_size,
        uncompressed_size,
    })
}

/// Parse a 5-byte 7z LZMA properties blob (no uncompressed-size field).
///
/// Layout: [props_byte(1)] [dict_size_le(4)]
///
/// # Errors
/// Returns [`LazippyError::InvalidHeader`] if the slice is too short or props
/// byte is invalid.
pub fn parse_7z_props(input: &[u8]) -> LazippyResult<(u8, u32)> {
    if input.len() < 5 {
        return Err(LazippyError::invalid_header(format!(
            "7z LZMA props too short: need 5 bytes, got {}",
            input.len()
        )));
    }
    let props_byte = input[0];
    let dict_size = u32::from_le_bytes([input[1], input[2], input[3], input[4]]);
    Ok((props_byte, dict_size))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_header_parses_13_bytes() {
        // props_byte = (pb=2, lp=0, lc=3) → (2*5+0)*9+3 = 93
        let mut header = [0u8; 13];
        header[0] = 93; // standard LZMA props
        header[1..5].copy_from_slice(&(1u32 << 16).to_le_bytes()); // dict = 64K
        header[5..13].copy_from_slice(&12345u64.to_le_bytes()); // uncompressed_size
        let props = parse_header(&header).unwrap();
        assert_eq!(props.lc, 3);
        assert_eq!(props.lp, 0);
        assert_eq!(props.pb, 2);
        assert_eq!(props.dict_size, 1 << 16);
        assert_eq!(props.uncompressed_size, Some(12345));
    }

    #[test]
    fn parse_header_too_short() {
        let result = parse_header(&[0u8; 5]);
        assert!(matches!(result, Err(LazippyError::InvalidHeader(_))));
    }

    #[test]
    fn parse_header_unknown_size() {
        let mut header = [0u8; 13];
        header[0] = 93;
        header[1..5].copy_from_slice(&(1u32 << 16).to_le_bytes());
        header[5..13].copy_from_slice(&u64::MAX.to_le_bytes());
        let props = parse_header(&header).unwrap();
        assert_eq!(props.uncompressed_size, None);
    }
}