lazippy 0.0.1

Pure-Rust LZMA (Lempel-Ziv-Markov chain Algorithm), part of the 8z umbrella
Documentation
//! LZMA encoder — Phase 1 wrapper around `lzma-rust2`.

pub mod matchfinder;

use std::io::Write;

use lzma_rust2::{LzmaOptions, LzmaWriter};

use crate::decode::header::Properties;
use crate::error::{LazippyError, LazippyResult};

/// Compress bytes to LZMA format with a full 13-byte `.lzma` file header.
///
/// The output header is: [props_byte(1)] [dict_size_le(4)] [uncompressed_size_le(8)]
///
/// # Errors
/// Returns an error if compression fails.
pub fn encode(input: &[u8], props: Properties) -> LazippyResult<Vec<u8>> {
    let mut out = Vec::new();
    let options = props_to_options(&props);
    // Write the .lzma header manually.
    let props_byte = options.get_props();
    out.push(props_byte);
    out.extend_from_slice(&options.dict_size.to_le_bytes());
    let uncomp_size: u64 = props.uncompressed_size.unwrap_or(u64::MAX);
    out.extend_from_slice(&uncomp_size.to_le_bytes());

    // Append the raw LZMA stream (no header from the writer).
    let compressed = encode_raw(input, &options)?;
    out.extend_from_slice(&compressed);
    Ok(out)
}

/// Compress bytes to raw LZMA and return a 5-byte props prefix + compressed data.
///
/// Output format: [props_byte(1)] [dict_size_le(4)] [raw_lzma_stream...]
///
/// This is the format 7z stores in a coder's packed stream. The props bytes are
/// recorded separately in the archive header metadata.
pub fn encode_7z(input: &[u8], dict_size: u32) -> LazippyResult<(Vec<u8>, Vec<u8>)> {
    let options = LzmaOptions {
        dict_size,
        lc: LzmaOptions::LC_DEFAULT,
        lp: LzmaOptions::LP_DEFAULT,
        pb: LzmaOptions::PB_DEFAULT,
        ..LzmaOptions::with_preset(6)
    };
    let props_byte = options.get_props();
    let mut props_blob = Vec::with_capacity(5);
    props_blob.push(props_byte);
    props_blob.extend_from_slice(&dict_size.to_le_bytes());

    let compressed = encode_raw(input, &options)?;
    Ok((props_blob, compressed))
}

/// Encode raw LZMA stream (no header) using the given options.
fn encode_raw(input: &[u8], options: &LzmaOptions) -> LazippyResult<Vec<u8>> {
    let mut buf = Vec::new();
    {
        // no header; use end-marker so decoder knows when stream ends
        let mut writer = LzmaWriter::new_no_header(&mut buf, options, true)
            .map_err(|e| LazippyError::Backend(e.to_string()))?;
        writer
            .write_all(input)
            .map_err(|e| LazippyError::Backend(e.to_string()))?;
        let _ = writer
            .finish()
            .map_err(|e| LazippyError::Backend(e.to_string()))?;
    }
    Ok(buf)
}

fn props_to_options(props: &Properties) -> LzmaOptions {
    LzmaOptions {
        dict_size: props.dict_size.max(4096),
        lc: props.lc as u32,
        lp: props.lp as u32,
        pb: props.pb as u32,
        ..LzmaOptions::with_preset(6)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::decode::header::Properties;

    fn default_props() -> Properties {
        Properties {
            lc: 3,
            lp: 0,
            pb: 2,
            dict_size: 1 << 16,
            uncompressed_size: None,
        }
    }

    #[test]
    fn encode_empty_succeeds() {
        let result = encode(&[], default_props());
        assert!(result.is_ok(), "encode empty failed: {result:?}");
        // Must have at least the 13-byte header.
        assert!(result.unwrap().len() >= 13);
    }

    #[test]
    fn encode_nonempty_succeeds() {
        let input = b"Hello, LZMA world!";
        let result = encode(input, default_props());
        assert!(result.is_ok(), "encode failed: {result:?}");
    }
}