use std::io::Read;
use forensicnomicon::decmpfs::{self, Algorithm, Storage, CHUNK_SIZE, HEADER_LEN, MAGIC};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DecmpfsError {
Truncated,
BadMagic(u32),
UnknownType(u32),
Unsupported(&'static str),
MissingResourceFork,
OutOfBounds,
Codec(&'static str),
LengthMismatch {
expected: usize,
got: usize,
},
}
impl std::fmt::Display for DecmpfsError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Truncated => write!(f, "decmpfs xattr shorter than 16-byte header"),
Self::BadMagic(m) => write!(f, "decmpfs bad magic {m:#010x} (expected 'cmpf')"),
Self::UnknownType(t) => write!(f, "decmpfs unknown compression_type {t}"),
Self::Unsupported(s) => write!(f, "decmpfs unsupported: {s}"),
Self::MissingResourceFork => {
write!(
f,
"decmpfs resource-fork type but no resource fork supplied"
)
}
Self::OutOfBounds => write!(f, "decmpfs length/offset field out of bounds"),
Self::Codec(s) => write!(f, "decmpfs codec error: {s}"),
Self::LengthMismatch { expected, got } => {
write!(f, "decmpfs length mismatch: expected {expected}, got {got}")
}
}
}
}
impl std::error::Error for DecmpfsError {}
type Result<T> = std::result::Result<T, DecmpfsError>;
pub fn decompress(xattr: &[u8], resource_fork: Option<&[u8]>) -> Result<Vec<u8>> {
if xattr.len() < HEADER_LEN {
return Err(DecmpfsError::Truncated);
}
let magic = le_u32(xattr, 0)?;
if magic != MAGIC {
return Err(DecmpfsError::BadMagic(magic));
}
let compression_type = le_u32(xattr, decmpfs::COMPRESSION_TYPE_OFFSET)?;
let uncompressed_size = le_u64(xattr, decmpfs::UNCOMPRESSED_SIZE_OFFSET)? as usize;
let Some(kind) = decmpfs::classify(compression_type) else {
return Err(match compression_type {
5 => DecmpfsError::Unsupported("decmpfs type 5 (de-dup generation store)"),
other => DecmpfsError::UnknownType(other),
});
};
if kind.algorithm == Algorithm::LzBitmap {
return Err(DecmpfsError::Unsupported(
"decmpfs LZBitmap (no public spec)",
));
}
let out = match kind.storage {
Storage::Inline => {
let payload = xattr.get(HEADER_LEN..).ok_or(DecmpfsError::Truncated)?;
decode_inline(kind.algorithm, payload, uncompressed_size, compression_type)?
}
Storage::ResourceFork => {
let fork = resource_fork.ok_or(DecmpfsError::MissingResourceFork)?;
decode_resource_fork(kind.algorithm, fork, uncompressed_size)?
}
};
if out.len() != uncompressed_size {
return Err(DecmpfsError::LengthMismatch {
expected: uncompressed_size,
got: out.len(),
});
}
Ok(out)
}
fn decode_inline(
algorithm: Algorithm,
payload: &[u8],
uncompressed_size: usize,
compression_type: u32,
) -> Result<Vec<u8>> {
match algorithm {
Algorithm::Uncompressed => match compression_type {
9 => Ok(payload.get(1..).unwrap_or(&[]).to_vec()),
_ => Ok(payload.to_vec()),
},
Algorithm::Zlib => {
match payload.first() {
Some(0xFF) => Ok(payload[1..].to_vec()),
_ => inflate(payload),
}
}
Algorithm::Lzvn => match payload.first() {
Some(0x06) => Ok(payload.get(1..).unwrap_or(&[]).to_vec()),
_ => lzvn_decode(payload, uncompressed_size),
},
Algorithm::Lzfse => lzfse_decode(payload),
_ => Err(DecmpfsError::Unsupported("decmpfs unsupported algorithm")),
}
}
fn decode_resource_fork(
algorithm: Algorithm,
fork: &[u8],
uncompressed_size: usize,
) -> Result<Vec<u8>> {
match algorithm {
Algorithm::Zlib => decode_zlib_resource_fork(fork, uncompressed_size),
Algorithm::Lzvn | Algorithm::Lzfse | Algorithm::Uncompressed => {
decode_chunked_resource_fork(algorithm, fork, uncompressed_size)
}
_ => Err(DecmpfsError::Unsupported("decmpfs unsupported algorithm")),
}
}
fn decode_zlib_resource_fork(fork: &[u8], uncompressed_size: usize) -> Result<Vec<u8>> {
let header_size = be_u32(fork, 0)? as usize;
let table = header_size
.checked_add(4)
.ok_or(DecmpfsError::OutOfBounds)?;
let num_blocks = le_u32(fork, table)? as usize;
let mut out = Vec::with_capacity(uncompressed_size);
for i in 0..num_blocks {
let entry = table
.checked_add(4)
.and_then(|b| b.checked_add(i.checked_mul(8)?))
.ok_or(DecmpfsError::OutOfBounds)?;
let offset = le_u32(fork, entry)? as usize;
let size = le_u32(fork, entry + 4)? as usize;
let start = table.checked_add(offset).ok_or(DecmpfsError::OutOfBounds)?;
let end = start.checked_add(size).ok_or(DecmpfsError::OutOfBounds)?;
let block = fork.get(start..end).ok_or(DecmpfsError::OutOfBounds)?;
out.extend_from_slice(&inflate(block)?);
}
Ok(out)
}
fn decode_chunked_resource_fork(
algorithm: Algorithm,
fork: &[u8],
uncompressed_size: usize,
) -> Result<Vec<u8>> {
let header_size = le_u32(fork, 0)? as usize;
let n_slots = (header_size / 4)
.checked_sub(1)
.ok_or(DecmpfsError::OutOfBounds)?;
let mut out = Vec::with_capacity(uncompressed_size);
let mut src = header_size;
for i in 0..n_slots {
if out.len() >= uncompressed_size {
break;
}
let end = le_u32(fork, 4 + i * 4)? as usize;
if end < src {
return Err(DecmpfsError::OutOfBounds);
}
let chunk = fork.get(src..end).ok_or(DecmpfsError::OutOfBounds)?;
let chunk_uncompressed = uncompressed_size
.checked_sub(out.len())
.ok_or(DecmpfsError::OutOfBounds)?
.min(CHUNK_SIZE);
let decoded = match algorithm {
Algorithm::Lzvn => lzvn_decode(chunk, chunk_uncompressed)?,
Algorithm::Lzfse => lzfse_decode(chunk)?,
Algorithm::Uncompressed => chunk.to_vec(),
_ => return Err(DecmpfsError::Codec("unexpected algorithm for chunked fork")),
};
out.extend_from_slice(&decoded);
src = end;
}
Ok(out)
}
fn inflate(data: &[u8]) -> Result<Vec<u8>> {
let mut decoder = flate2::read::ZlibDecoder::new(data);
let mut out = Vec::new();
decoder
.read_to_end(&mut out)
.map_err(|_| DecmpfsError::Codec("zlib"))?;
Ok(out)
}
fn lzvn_decode(chunk: &[u8], uncompressed_len: usize) -> Result<Vec<u8>> {
lzvn::decode(chunk, uncompressed_len).map_err(|_| DecmpfsError::Codec("lzvn"))
}
fn lzfse_decode(stream: &[u8]) -> Result<Vec<u8>> {
let mut out = Vec::new();
lzfse_rust::decode_bytes(stream, &mut out).map_err(|_| DecmpfsError::Codec("lzfse/lzvn"))?;
Ok(out)
}
fn le_u32(data: &[u8], offset: usize) -> Result<u32> {
let end = offset.checked_add(4).ok_or(DecmpfsError::OutOfBounds)?;
let bytes = data.get(offset..end).ok_or(DecmpfsError::OutOfBounds)?;
Ok(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
}
fn be_u32(data: &[u8], offset: usize) -> Result<u32> {
let end = offset.checked_add(4).ok_or(DecmpfsError::OutOfBounds)?;
let bytes = data.get(offset..end).ok_or(DecmpfsError::OutOfBounds)?;
Ok(u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
}
fn le_u64(data: &[u8], offset: usize) -> Result<u64> {
let end = offset.checked_add(8).ok_or(DecmpfsError::OutOfBounds)?;
let bytes = data.get(offset..end).ok_or(DecmpfsError::OutOfBounds)?;
let mut a = [0u8; 8];
a.copy_from_slice(bytes);
Ok(u64::from_le_bytes(a))
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
fn header(compression_type: u32, uncompressed_size: u64) -> Vec<u8> {
let mut h = Vec::with_capacity(16);
h.extend_from_slice(&MAGIC.to_le_bytes());
h.extend_from_slice(&compression_type.to_le_bytes());
h.extend_from_slice(&uncompressed_size.to_le_bytes());
h
}
fn xattr(compression_type: u32, uncompressed_size: u64, payload: &[u8]) -> Vec<u8> {
let mut x = header(compression_type, uncompressed_size);
x.extend_from_slice(payload);
x
}
#[test]
fn decodes_real_macos_lzvn_resource_fork() {
let fork = include_bytes!("../tests/data/decmpfs/lzvn.rsrc");
let expected = include_bytes!("../tests/data/decmpfs/lzvn.expected");
let hdr = header(8, expected.len() as u64);
let out = decompress(&hdr, Some(fork)).expect("real LZVN must decode");
assert_eq!(out, expected, "decoded bytes must match the original file");
}
#[test]
fn decodes_real_macos_zlib_resource_fork() {
let fork = include_bytes!("../tests/data/decmpfs/real_zlib_rsrc.rsrc");
let expected = include_bytes!("../tests/data/decmpfs/zlib.expected");
let hdr = header(4, expected.len() as u64);
let out = decompress(&hdr, Some(fork)).expect("real type-4 zlib must decode");
assert_eq!(out, expected);
}
#[test]
fn decodes_real_macos_inline_zlib() {
let payload = include_bytes!("../tests/data/decmpfs/real_zlib_inline.payload");
let expected = include_bytes!("../tests/data/decmpfs/real_zlib_inline.expected");
let x = xattr(3, expected.len() as u64, payload);
let out = decompress(&x, None).expect("real type-3 inline zlib must decode");
assert_eq!(out, expected);
}
#[test]
fn decodes_inline_zlib_stored_marker() {
let payload = include_bytes!("../tests/data/decmpfs/zlib_type3_stored.payload");
let expected = include_bytes!("../tests/data/decmpfs/zlib_inline.expected");
let x = xattr(3, expected.len() as u64, payload);
let out = decompress(&x, None).expect("0xFF-stored type-3 must decode");
assert_eq!(out, expected);
}
#[test]
fn decodes_inline_uncompressed() {
let data = b"the quick brown fox jumps over the lazy dog";
let x = xattr(1, data.len() as u64, data);
let out = decompress(&x, None).expect("type-1 uncompressed must decode");
assert_eq!(out, data);
}
fn chunked_fork(chunks: &[Vec<u8>]) -> Vec<u8> {
let header_size = 4 * (chunks.len() + 1);
let mut fork = Vec::new();
fork.extend_from_slice(&(header_size as u32).to_le_bytes());
let mut end = header_size;
for c in chunks {
end += c.len();
fork.extend_from_slice(&(end as u32).to_le_bytes());
}
for c in chunks {
fork.extend_from_slice(c);
}
fork
}
#[test]
fn decodes_real_macos_lzfse_resource_fork() {
let fork = include_bytes!("../tests/data/decmpfs/real_lzfse_rsrc.rsrc");
let expected = include_bytes!("../tests/data/decmpfs/zlib.expected"); let hdr = header(12, expected.len() as u64);
let out = decompress(&hdr, Some(fork)).expect("real type-12 LZFSE must decode");
assert_eq!(out, expected);
}
#[test]
fn decodes_real_macos_inline_lzfse() {
let payload = include_bytes!("../tests/data/decmpfs/real_lzfse_inline.payload");
let expected = include_bytes!("../tests/data/decmpfs/real_zlib_inline.expected");
let x = xattr(11, expected.len() as u64, payload);
let out = decompress(&x, None).expect("real type-11 inline LZFSE must decode");
assert_eq!(out, expected);
}
#[test]
fn decodes_uncompressed_resource_fork() {
let mut data = Vec::new();
for i in 0..(CHUNK_SIZE + 5000) {
data.push((i % 251) as u8);
}
let c0 = data[..CHUNK_SIZE].to_vec();
let c1 = data[CHUNK_SIZE..].to_vec();
let fork = chunked_fork(&[c0, c1]);
let hdr = header(10, data.len() as u64);
let out = decompress(&hdr, Some(&fork)).expect("type-10 uncompressed fork must decode");
assert_eq!(out, data);
}
#[test]
fn decodes_inline_uncompressed_type9() {
let content = b"type 9 is uncompressed-inline, a variant of type 1";
let mut payload = vec![0xCC];
payload.extend_from_slice(content);
let x = xattr(9, content.len() as u64, &payload);
assert_eq!(decompress(&x, None).expect("type-9 must decode"), content);
}
#[test]
fn decodes_real_tahoe_type8_lzvn_with_trailing_bytes() {
let fork = include_bytes!("../tests/data/decmpfs/tahoe_type8.rsrc");
let expected = include_bytes!("../tests/data/decmpfs/tahoe_type8.expected");
let hdr = header(8, expected.len() as u64);
let out = decompress(&hdr, Some(fork)).expect("Tahoe LZVN must decode");
assert_eq!(out.as_slice(), expected.as_slice());
}
#[test]
fn decodes_real_tahoe_type9_inline_marker() {
let xattr_bytes = include_bytes!("../tests/data/decmpfs/tahoe_type9.decmpfs");
let expected = include_bytes!("../tests/data/decmpfs/tahoe_type9.expected");
let out = decompress(xattr_bytes, None).expect("Tahoe type-9 must decode");
assert_eq!(out.as_slice(), expected.as_slice());
}
#[test]
fn length_mismatch_is_loud() {
let data = b"the quick brown fox";
let x = xattr(1, 999, data); assert!(matches!(
decompress(&x, None),
Err(DecmpfsError::LengthMismatch { expected: 999, .. })
));
}
#[test]
fn rejects_bad_magic() {
let mut x = xattr(1, 0, &[]);
x[0] ^= 0xFF;
assert!(matches!(
decompress(&x, None),
Err(DecmpfsError::BadMagic(_))
));
}
#[test]
fn rejects_truncated_header() {
assert_eq!(decompress(&[0u8; 8], None), Err(DecmpfsError::Truncated));
}
#[test]
fn rejects_unknown_type() {
let x = xattr(99, 0, &[]);
assert_eq!(decompress(&x, None), Err(DecmpfsError::UnknownType(99)));
}
#[test]
fn rejects_lzbitmap_unsupported() {
let x = xattr(14, 0, &[]);
assert!(matches!(
decompress(&x, None),
Err(DecmpfsError::Unsupported(_))
));
}
#[test]
fn rejects_dedup_type5_unsupported() {
let x = xattr(5, 0, &[]);
assert!(matches!(
decompress(&x, None),
Err(DecmpfsError::Unsupported(_))
));
}
#[test]
fn resource_fork_type_without_fork_errors() {
let hdr = header(8, 100);
assert_eq!(
decompress(&hdr, None),
Err(DecmpfsError::MissingResourceFork)
);
}
}