1use std::io::Read;
29
30use forensicnomicon::decmpfs::{self, Algorithm, Storage, CHUNK_SIZE, HEADER_LEN, MAGIC};
31
32#[derive(Debug, Clone, PartialEq, Eq)]
35pub enum DecmpfsError {
36 Truncated,
38 BadMagic(u32),
40 UnknownType(u32),
42 Unsupported(&'static str),
45 MissingResourceFork,
47 OutOfBounds,
49 Codec(&'static str),
51 LengthMismatch {
53 expected: usize,
55 got: usize,
57 },
58}
59
60impl std::fmt::Display for DecmpfsError {
61 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62 match self {
63 Self::Truncated => write!(f, "decmpfs xattr shorter than 16-byte header"),
64 Self::BadMagic(m) => write!(f, "decmpfs bad magic {m:#010x} (expected 'cmpf')"),
65 Self::UnknownType(t) => write!(f, "decmpfs unknown compression_type {t}"),
66 Self::Unsupported(s) => write!(f, "decmpfs unsupported: {s}"),
67 Self::MissingResourceFork => {
68 write!(
69 f,
70 "decmpfs resource-fork type but no resource fork supplied"
71 )
72 }
73 Self::OutOfBounds => write!(f, "decmpfs length/offset field out of bounds"),
74 Self::Codec(s) => write!(f, "decmpfs codec error: {s}"),
75 Self::LengthMismatch { expected, got } => {
76 write!(f, "decmpfs length mismatch: expected {expected}, got {got}")
77 }
78 }
79 }
80}
81
82impl std::error::Error for DecmpfsError {}
83
84type Result<T> = std::result::Result<T, DecmpfsError>;
85
86pub fn decompress(xattr: &[u8], resource_fork: Option<&[u8]>) -> Result<Vec<u8>> {
99 if xattr.len() < HEADER_LEN {
100 return Err(DecmpfsError::Truncated);
101 }
102 let magic = le_u32(xattr, 0)?;
103 if magic != MAGIC {
104 return Err(DecmpfsError::BadMagic(magic));
105 }
106 let compression_type = le_u32(xattr, decmpfs::COMPRESSION_TYPE_OFFSET)?;
107 let uncompressed_size = le_u64(xattr, decmpfs::UNCOMPRESSED_SIZE_OFFSET)? as usize;
108
109 let Some(kind) = decmpfs::classify(compression_type) else {
110 return Err(match compression_type {
111 5 => DecmpfsError::Unsupported("decmpfs type 5 (de-dup generation store)"),
112 other => DecmpfsError::UnknownType(other),
113 });
114 };
115 if kind.algorithm == Algorithm::LzBitmap {
116 return Err(DecmpfsError::Unsupported(
117 "decmpfs LZBitmap (no public spec)",
118 ));
119 }
120
121 let out = match kind.storage {
122 Storage::Inline => {
123 let payload = xattr.get(HEADER_LEN..).ok_or(DecmpfsError::Truncated)?;
124 decode_inline(kind.algorithm, payload, uncompressed_size, compression_type)?
125 }
126 Storage::ResourceFork => {
127 let fork = resource_fork.ok_or(DecmpfsError::MissingResourceFork)?;
128 decode_resource_fork(kind.algorithm, fork, uncompressed_size)?
129 }
130 };
131
132 if out.len() != uncompressed_size {
133 return Err(DecmpfsError::LengthMismatch {
134 expected: uncompressed_size,
135 got: out.len(),
136 });
137 }
138 Ok(out)
139}
140
141fn decode_inline(
152 algorithm: Algorithm,
153 payload: &[u8],
154 uncompressed_size: usize,
155 compression_type: u32,
156) -> Result<Vec<u8>> {
157 match algorithm {
158 Algorithm::Uncompressed => match compression_type {
160 9 => Ok(payload.get(1..).unwrap_or(&[]).to_vec()),
161 _ => Ok(payload.to_vec()),
162 },
163 Algorithm::Zlib => {
164 match payload.first() {
167 Some(0xFF) => Ok(payload[1..].to_vec()),
168 _ => inflate(payload),
169 }
170 }
171 Algorithm::Lzvn => match payload.first() {
174 Some(0x06) => Ok(payload.get(1..).unwrap_or(&[]).to_vec()),
175 _ => lzvn_decode(payload, uncompressed_size),
176 },
177 Algorithm::Lzfse => lzfse_decode(payload),
178 _ => Err(DecmpfsError::Unsupported("decmpfs unsupported algorithm")),
181 }
182}
183
184fn decode_resource_fork(
186 algorithm: Algorithm,
187 fork: &[u8],
188 uncompressed_size: usize,
189) -> Result<Vec<u8>> {
190 match algorithm {
191 Algorithm::Zlib => decode_zlib_resource_fork(fork, uncompressed_size),
192 Algorithm::Lzvn | Algorithm::Lzfse | Algorithm::Uncompressed => {
193 decode_chunked_resource_fork(algorithm, fork, uncompressed_size)
194 }
195 _ => Err(DecmpfsError::Unsupported("decmpfs unsupported algorithm")),
197 }
198}
199
200fn decode_zlib_resource_fork(fork: &[u8], uncompressed_size: usize) -> Result<Vec<u8>> {
202 let header_size = be_u32(fork, 0)? as usize;
204 let table = header_size
211 .checked_add(4)
212 .ok_or(DecmpfsError::OutOfBounds)?;
213 let num_blocks = le_u32(fork, table)? as usize;
214 let mut out = Vec::with_capacity(uncompressed_size);
215 for i in 0..num_blocks {
216 let entry = table
217 .checked_add(4)
218 .and_then(|b| b.checked_add(i.checked_mul(8)?))
219 .ok_or(DecmpfsError::OutOfBounds)?;
220 let offset = le_u32(fork, entry)? as usize;
221 let size = le_u32(fork, entry + 4)? as usize;
222 let start = table.checked_add(offset).ok_or(DecmpfsError::OutOfBounds)?;
223 let end = start.checked_add(size).ok_or(DecmpfsError::OutOfBounds)?;
224 let block = fork.get(start..end).ok_or(DecmpfsError::OutOfBounds)?;
225 out.extend_from_slice(&inflate(block)?);
226 }
227 Ok(out)
228}
229
230fn decode_chunked_resource_fork(
233 algorithm: Algorithm,
234 fork: &[u8],
235 uncompressed_size: usize,
236) -> Result<Vec<u8>> {
237 let header_size = le_u32(fork, 0)? as usize;
238 let n_slots = (header_size / 4)
246 .checked_sub(1)
247 .ok_or(DecmpfsError::OutOfBounds)?;
248 let mut out = Vec::with_capacity(uncompressed_size);
249 let mut src = header_size;
250 for i in 0..n_slots {
251 if out.len() >= uncompressed_size {
252 break;
253 }
254 let end = le_u32(fork, 4 + i * 4)? as usize;
255 if end < src {
256 return Err(DecmpfsError::OutOfBounds);
257 }
258 let chunk = fork.get(src..end).ok_or(DecmpfsError::OutOfBounds)?;
259 let chunk_uncompressed = uncompressed_size
261 .checked_sub(out.len())
262 .ok_or(DecmpfsError::OutOfBounds)?
263 .min(CHUNK_SIZE);
264 let decoded = match algorithm {
265 Algorithm::Lzvn => lzvn_decode(chunk, chunk_uncompressed)?,
266 Algorithm::Lzfse => lzfse_decode(chunk)?,
267 Algorithm::Uncompressed => chunk.to_vec(),
268 _ => return Err(DecmpfsError::Codec("unexpected algorithm for chunked fork")),
271 };
272 out.extend_from_slice(&decoded);
273 src = end;
274 }
275 Ok(out)
276}
277
278fn inflate(data: &[u8]) -> Result<Vec<u8>> {
280 let mut decoder = flate2::read::ZlibDecoder::new(data);
281 let mut out = Vec::new();
282 decoder
283 .read_to_end(&mut out)
284 .map_err(|_| DecmpfsError::Codec("zlib"))?;
285 Ok(out)
286}
287
288fn lzvn_decode(chunk: &[u8], uncompressed_len: usize) -> Result<Vec<u8>> {
297 lzvn::decode(chunk, uncompressed_len).map_err(|_| DecmpfsError::Codec("lzvn"))
298}
299
300fn lzfse_decode(stream: &[u8]) -> Result<Vec<u8>> {
302 let mut out = Vec::new();
303 lzfse_rust::decode_bytes(stream, &mut out).map_err(|_| DecmpfsError::Codec("lzfse/lzvn"))?;
304 Ok(out)
305}
306
307fn le_u32(data: &[u8], offset: usize) -> Result<u32> {
310 let end = offset.checked_add(4).ok_or(DecmpfsError::OutOfBounds)?;
311 let bytes = data.get(offset..end).ok_or(DecmpfsError::OutOfBounds)?;
312 Ok(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
313}
314
315fn be_u32(data: &[u8], offset: usize) -> Result<u32> {
316 let end = offset.checked_add(4).ok_or(DecmpfsError::OutOfBounds)?;
317 let bytes = data.get(offset..end).ok_or(DecmpfsError::OutOfBounds)?;
318 Ok(u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
319}
320
321fn le_u64(data: &[u8], offset: usize) -> Result<u64> {
322 let end = offset.checked_add(8).ok_or(DecmpfsError::OutOfBounds)?;
323 let bytes = data.get(offset..end).ok_or(DecmpfsError::OutOfBounds)?;
324 let mut a = [0u8; 8];
325 a.copy_from_slice(bytes);
326 Ok(u64::from_le_bytes(a))
327}
328
329#[cfg(test)]
330#[allow(clippy::unwrap_used, clippy::expect_used)]
331mod tests {
332 use super::*;
333
334 fn header(compression_type: u32, uncompressed_size: u64) -> Vec<u8> {
336 let mut h = Vec::with_capacity(16);
337 h.extend_from_slice(&MAGIC.to_le_bytes());
338 h.extend_from_slice(&compression_type.to_le_bytes());
339 h.extend_from_slice(&uncompressed_size.to_le_bytes());
340 h
341 }
342
343 fn xattr(compression_type: u32, uncompressed_size: u64, payload: &[u8]) -> Vec<u8> {
344 let mut x = header(compression_type, uncompressed_size);
345 x.extend_from_slice(payload);
346 x
347 }
348
349 #[test]
351 fn decodes_real_macos_lzvn_resource_fork() {
352 let fork = include_bytes!("../tests/data/decmpfs/lzvn.rsrc");
353 let expected = include_bytes!("../tests/data/decmpfs/lzvn.expected");
354 let hdr = header(8, expected.len() as u64);
355 let out = decompress(&hdr, Some(fork)).expect("real LZVN must decode");
356 assert_eq!(out, expected, "decoded bytes must match the original file");
357 }
358
359 #[test]
361 fn decodes_real_macos_zlib_resource_fork() {
362 let fork = include_bytes!("../tests/data/decmpfs/real_zlib_rsrc.rsrc");
363 let expected = include_bytes!("../tests/data/decmpfs/zlib.expected");
364 let hdr = header(4, expected.len() as u64);
365 let out = decompress(&hdr, Some(fork)).expect("real type-4 zlib must decode");
366 assert_eq!(out, expected);
367 }
368
369 #[test]
371 fn decodes_real_macos_inline_zlib() {
372 let payload = include_bytes!("../tests/data/decmpfs/real_zlib_inline.payload");
373 let expected = include_bytes!("../tests/data/decmpfs/real_zlib_inline.expected");
374 let x = xattr(3, expected.len() as u64, payload);
375 let out = decompress(&x, None).expect("real type-3 inline zlib must decode");
376 assert_eq!(out, expected);
377 }
378
379 #[test]
381 fn decodes_inline_zlib_stored_marker() {
382 let payload = include_bytes!("../tests/data/decmpfs/zlib_type3_stored.payload");
383 let expected = include_bytes!("../tests/data/decmpfs/zlib_inline.expected");
384 let x = xattr(3, expected.len() as u64, payload);
385 let out = decompress(&x, None).expect("0xFF-stored type-3 must decode");
386 assert_eq!(out, expected);
387 }
388
389 #[test]
391 fn decodes_inline_uncompressed() {
392 let data = b"the quick brown fox jumps over the lazy dog";
393 let x = xattr(1, data.len() as u64, data);
394 let out = decompress(&x, None).expect("type-1 uncompressed must decode");
395 assert_eq!(out, data);
396 }
397
398 fn chunked_fork(chunks: &[Vec<u8>]) -> Vec<u8> {
401 let header_size = 4 * (chunks.len() + 1);
402 let mut fork = Vec::new();
403 fork.extend_from_slice(&(header_size as u32).to_le_bytes());
404 let mut end = header_size;
405 for c in chunks {
406 end += c.len();
407 fork.extend_from_slice(&(end as u32).to_le_bytes());
408 }
409 for c in chunks {
410 fork.extend_from_slice(c);
411 }
412 fork
413 }
414
415 #[test]
417 fn decodes_real_macos_lzfse_resource_fork() {
418 let fork = include_bytes!("../tests/data/decmpfs/real_lzfse_rsrc.rsrc");
419 let expected = include_bytes!("../tests/data/decmpfs/zlib.expected"); let hdr = header(12, expected.len() as u64);
421 let out = decompress(&hdr, Some(fork)).expect("real type-12 LZFSE must decode");
422 assert_eq!(out, expected);
423 }
424
425 #[test]
427 fn decodes_real_macos_inline_lzfse() {
428 let payload = include_bytes!("../tests/data/decmpfs/real_lzfse_inline.payload");
429 let expected = include_bytes!("../tests/data/decmpfs/real_zlib_inline.expected");
430 let x = xattr(11, expected.len() as u64, payload);
431 let out = decompress(&x, None).expect("real type-11 inline LZFSE must decode");
432 assert_eq!(out, expected);
433 }
434
435 #[test]
437 fn decodes_uncompressed_resource_fork() {
438 let mut data = Vec::new();
439 for i in 0..(CHUNK_SIZE + 5000) {
440 data.push((i % 251) as u8);
441 }
442 let c0 = data[..CHUNK_SIZE].to_vec();
443 let c1 = data[CHUNK_SIZE..].to_vec();
444 let fork = chunked_fork(&[c0, c1]);
445 let hdr = header(10, data.len() as u64);
446 let out = decompress(&hdr, Some(&fork)).expect("type-10 uncompressed fork must decode");
447 assert_eq!(out, data);
448 }
449
450 #[test]
452 fn decodes_inline_uncompressed_type9() {
453 let content = b"type 9 is uncompressed-inline, a variant of type 1";
459 let mut payload = vec![0xCC];
460 payload.extend_from_slice(content);
461 let x = xattr(9, content.len() as u64, &payload);
462 assert_eq!(decompress(&x, None).expect("type-9 must decode"), content);
463 }
464
465 #[test]
468 fn decodes_real_tahoe_type8_lzvn_with_trailing_bytes() {
469 let fork = include_bytes!("../tests/data/decmpfs/tahoe_type8.rsrc");
470 let expected = include_bytes!("../tests/data/decmpfs/tahoe_type8.expected");
471 let hdr = header(8, expected.len() as u64);
472 let out = decompress(&hdr, Some(fork)).expect("Tahoe LZVN must decode");
473 assert_eq!(out.as_slice(), expected.as_slice());
474 }
475
476 #[test]
478 fn decodes_real_tahoe_type9_inline_marker() {
479 let xattr_bytes = include_bytes!("../tests/data/decmpfs/tahoe_type9.decmpfs");
480 let expected = include_bytes!("../tests/data/decmpfs/tahoe_type9.expected");
481 let out = decompress(xattr_bytes, None).expect("Tahoe type-9 must decode");
482 assert_eq!(out.as_slice(), expected.as_slice());
483 }
484
485 #[test]
487 fn length_mismatch_is_loud() {
488 let data = b"the quick brown fox";
489 let x = xattr(1, 999, data); assert!(matches!(
491 decompress(&x, None),
492 Err(DecmpfsError::LengthMismatch { expected: 999, .. })
493 ));
494 }
495
496 #[test]
498 fn rejects_bad_magic() {
499 let mut x = xattr(1, 0, &[]);
500 x[0] ^= 0xFF;
501 assert!(matches!(
502 decompress(&x, None),
503 Err(DecmpfsError::BadMagic(_))
504 ));
505 }
506
507 #[test]
508 fn rejects_truncated_header() {
509 assert_eq!(decompress(&[0u8; 8], None), Err(DecmpfsError::Truncated));
510 }
511
512 #[test]
513 fn rejects_unknown_type() {
514 let x = xattr(99, 0, &[]);
515 assert_eq!(decompress(&x, None), Err(DecmpfsError::UnknownType(99)));
516 }
517
518 #[test]
519 fn rejects_lzbitmap_unsupported() {
520 let x = xattr(14, 0, &[]);
521 assert!(matches!(
522 decompress(&x, None),
523 Err(DecmpfsError::Unsupported(_))
524 ));
525 }
526
527 #[test]
528 fn rejects_dedup_type5_unsupported() {
529 let x = xattr(5, 0, &[]);
530 assert!(matches!(
531 decompress(&x, None),
532 Err(DecmpfsError::Unsupported(_))
533 ));
534 }
535
536 #[test]
537 fn resource_fork_type_without_fork_errors() {
538 let hdr = header(8, 100);
539 assert_eq!(
540 decompress(&hdr, None),
541 Err(DecmpfsError::MissingResourceFork)
542 );
543 }
544}