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