car-parser 0.1.0

Rust parser for Apple .car asset catalog archives
Documentation
use std::borrow::Cow;
#[cfg(not(target_arch = "wasm32"))]
use std::process::Command;
#[cfg(not(target_arch = "wasm32"))]
use std::sync::atomic::{AtomicU64, Ordering};

use thiserror::Error;

#[cfg(not(target_arch = "wasm32"))]
static RAW_TEMP_FILE_SEQ: AtomicU64 = AtomicU64::new(0);

#[derive(Debug, Error)]
pub enum AppleCompressionError {
    #[error("Apple compression stream decompression failed: {0}")]
    DecodeFailed(String),
    #[error("Native fallback unavailable: {0}")]
    NativeFallbackUnavailable(&'static str),
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
}

pub fn is_apple_compression_stream(data: &[u8]) -> bool {
    matches!(
        data.get(..4),
        Some(b"bvx1") | Some(b"bvx2") | Some(b"bvxn") | Some(b"bvx-") | Some(b"bvx$")
    )
}

pub fn maybe_decode_lzfse_with_fallback<'a>(
    data: &'a [u8],
) -> Result<Cow<'a, [u8]>, AppleCompressionError> {
    if !is_apple_compression_stream(data) {
        return Ok(Cow::Borrowed(data));
    }

    decode_lzfse_with_fallback(data).map(Cow::Owned)
}

pub fn decode_lzfse_with_fallback(data: &[u8]) -> Result<Vec<u8>, AppleCompressionError> {
    let cap = data.len().saturating_mul(16).max(4096);
    let mut out = Vec::with_capacity(cap);
    let mut decoder = lzfse_rust::LzfseRingDecoder::default();
    if let Ok(n) = decoder.decode_bytes(data, &mut out)
        && (n > 0 || data.is_empty())
    {
        out.truncate(n as usize);
        return Ok(out);
    }

    decode_lzfse_with_tool(data)
}

#[cfg(target_arch = "wasm32")]
fn decode_lzfse_with_tool(_data: &[u8]) -> Result<Vec<u8>, AppleCompressionError> {
    Err(AppleCompressionError::NativeFallbackUnavailable(
        "Apple compression stream decompression requires /usr/bin/compression_tool",
    ))
}

#[cfg(not(target_arch = "wasm32"))]
fn decode_lzfse_with_tool(data: &[u8]) -> Result<Vec<u8>, AppleCompressionError> {
    let pid = std::process::id();
    let seq = RAW_TEMP_FILE_SEQ.fetch_add(1, Ordering::Relaxed);
    let input_path = std::env::temp_dir().join(format!("carparser-raw-{pid}-{seq}.lzfse"));
    let output_path = input_path.with_extension("out");

    std::fs::write(&input_path, data)?;
    let result = Command::new("/usr/bin/compression_tool")
        .args([
            "-decode",
            "-a",
            "lzfse",
            "-i",
            input_path.to_str().unwrap_or_default(),
            "-o",
            output_path.to_str().unwrap_or_default(),
        ])
        .output();

    let decoded = match result {
        Ok(out) if out.status.success() => std::fs::read(&output_path)?,
        _ => {
            remove_temp_files(&input_path, &output_path);
            return Err(AppleCompressionError::DecodeFailed(
                "Apple compression stream decompression failed".to_string(),
            ));
        }
    };

    remove_temp_files(&input_path, &output_path);
    Ok(decoded)
}

#[cfg(not(target_arch = "wasm32"))]
fn remove_temp_files(input_path: &std::path::Path, output_path: &std::path::Path) {
    let _ = std::fs::remove_file(input_path);
    let _ = std::fs::remove_file(output_path);
}

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

    #[test]
    fn recognizes_all_apple_compression_block_headers() {
        for magic in [b"bvx1", b"bvx2", b"bvxn", b"bvx-", b"bvx$"] {
            assert!(is_apple_compression_stream(magic), "{magic:?}");
        }
        assert!(!is_apple_compression_stream(b"bvx"));
        assert!(!is_apple_compression_stream(b"plain data"));
    }

    #[test]
    fn maybe_decode_decompresses_lzfse_v1_stream() {
        // An LZFSE v1 stream encoding 65,536 zero bytes, also verified with
        // Apple's compression_tool. The encoder currently emits v2 headers.
        let compressed = include_bytes!("../tests/data/lzfse-v1.bin");
        assert_eq!(&compressed[..4], b"bvx1");
        let decoded = maybe_decode_lzfse_with_fallback(compressed).unwrap();
        assert_eq!(decoded.len(), 65_536);
        assert!(decoded.iter().all(|byte| *byte == 0));
    }
}