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.min(CHUNK_SIZE)),
181 },
182 Algorithm::Lzfse => lzfse_decode(payload),
183 _ => Err(DecmpfsError::Unsupported("decmpfs unsupported algorithm")),
186 }
187}
188
189fn decode_resource_fork(
191 algorithm: Algorithm,
192 fork: &[u8],
193 uncompressed_size: usize,
194) -> Result<Vec<u8>> {
195 match algorithm {
196 Algorithm::Zlib => decode_zlib_resource_fork(fork, uncompressed_size),
197 Algorithm::Lzvn | Algorithm::Lzfse | Algorithm::Uncompressed => {
198 decode_chunked_resource_fork(algorithm, fork, uncompressed_size)
199 }
200 _ => Err(DecmpfsError::Unsupported("decmpfs unsupported algorithm")),
202 }
203}
204
205fn decode_zlib_resource_fork(fork: &[u8], uncompressed_size: usize) -> Result<Vec<u8>> {
207 let header_size = be_u32(fork, 0)? as usize;
209 let table = header_size
216 .checked_add(4)
217 .ok_or(DecmpfsError::OutOfBounds)?;
218 let num_blocks = le_u32(fork, table)? as usize;
219 let mut out = Vec::with_capacity(uncompressed_size.min(fork.len()));
224 for i in 0..num_blocks {
225 let entry = table
226 .checked_add(4)
227 .and_then(|b| b.checked_add(i.checked_mul(8)?))
228 .ok_or(DecmpfsError::OutOfBounds)?;
229 let offset = le_u32(fork, entry)? as usize;
230 let size = le_u32(fork, entry + 4)? as usize;
231 let start = table.checked_add(offset).ok_or(DecmpfsError::OutOfBounds)?;
232 let end = start.checked_add(size).ok_or(DecmpfsError::OutOfBounds)?;
233 let block = fork.get(start..end).ok_or(DecmpfsError::OutOfBounds)?;
234 out.extend_from_slice(&inflate(block)?);
235 }
236 Ok(out)
237}
238
239fn decode_chunked_resource_fork(
242 algorithm: Algorithm,
243 fork: &[u8],
244 uncompressed_size: usize,
245) -> Result<Vec<u8>> {
246 let header_size = le_u32(fork, 0)? as usize;
247 let n_slots = (header_size / 4)
255 .checked_sub(1)
256 .ok_or(DecmpfsError::OutOfBounds)?;
257 let mut out = Vec::with_capacity(uncompressed_size.min(fork.len()));
260 let mut src = header_size;
261 for i in 0..n_slots {
262 if out.len() >= uncompressed_size {
263 break;
264 }
265 let end = le_u32(fork, 4 + i * 4)? as usize;
266 if end < src {
267 return Err(DecmpfsError::OutOfBounds);
268 }
269 let chunk = fork.get(src..end).ok_or(DecmpfsError::OutOfBounds)?;
270 let chunk_uncompressed = uncompressed_size
272 .checked_sub(out.len())
273 .ok_or(DecmpfsError::OutOfBounds)?
274 .min(CHUNK_SIZE);
275 let decoded = match algorithm {
276 Algorithm::Lzvn => lzvn_decode(chunk, chunk_uncompressed)?,
277 Algorithm::Lzfse => lzfse_decode(chunk)?,
278 Algorithm::Uncompressed => chunk.to_vec(),
279 _ => return Err(DecmpfsError::Codec("unexpected algorithm for chunked fork")),
282 };
283 out.extend_from_slice(&decoded);
284 src = end;
285 }
286 Ok(out)
287}
288
289fn inflate(data: &[u8]) -> Result<Vec<u8>> {
291 let mut decoder = flate2::read::ZlibDecoder::new(data);
292 let mut out = Vec::new();
293 decoder
294 .read_to_end(&mut out)
295 .map_err(|_| DecmpfsError::Codec("zlib"))?;
296 Ok(out)
297}
298
299fn lzvn_decode(chunk: &[u8], uncompressed_len: usize) -> Result<Vec<u8>> {
308 lzvn::decode(chunk, uncompressed_len).map_err(|_| DecmpfsError::Codec("lzvn"))
309}
310
311fn lzfse_decode(stream: &[u8]) -> Result<Vec<u8>> {
313 let mut out = Vec::new();
314 lzfse_rust::decode_bytes(stream, &mut out).map_err(|_| DecmpfsError::Codec("lzfse/lzvn"))?;
315 Ok(out)
316}
317
318fn le_u32(data: &[u8], offset: usize) -> Result<u32> {
321 let end = offset.checked_add(4).ok_or(DecmpfsError::OutOfBounds)?;
322 let bytes = data.get(offset..end).ok_or(DecmpfsError::OutOfBounds)?;
323 Ok(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
324}
325
326fn be_u32(data: &[u8], offset: usize) -> Result<u32> {
327 let end = offset.checked_add(4).ok_or(DecmpfsError::OutOfBounds)?;
328 let bytes = data.get(offset..end).ok_or(DecmpfsError::OutOfBounds)?;
329 Ok(u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
330}
331
332fn le_u64(data: &[u8], offset: usize) -> Result<u64> {
333 let end = offset.checked_add(8).ok_or(DecmpfsError::OutOfBounds)?;
334 let bytes = data.get(offset..end).ok_or(DecmpfsError::OutOfBounds)?;
335 let mut a = [0u8; 8];
336 a.copy_from_slice(bytes);
337 Ok(u64::from_le_bytes(a))
338}
339
340#[cfg(test)]
341#[allow(clippy::unwrap_used, clippy::expect_used)]
342mod tests {
343 use super::*;
344
345 fn header(compression_type: u32, uncompressed_size: u64) -> Vec<u8> {
347 let mut h = Vec::with_capacity(16);
348 h.extend_from_slice(&MAGIC.to_le_bytes());
349 h.extend_from_slice(&compression_type.to_le_bytes());
350 h.extend_from_slice(&uncompressed_size.to_le_bytes());
351 h
352 }
353
354 fn xattr(compression_type: u32, uncompressed_size: u64, payload: &[u8]) -> Vec<u8> {
355 let mut x = header(compression_type, uncompressed_size);
356 x.extend_from_slice(payload);
357 x
358 }
359
360 #[test]
362 fn decodes_real_macos_lzvn_resource_fork() {
363 let fork = include_bytes!("../tests/data/decmpfs/lzvn.rsrc");
364 let expected = include_bytes!("../tests/data/decmpfs/lzvn.expected");
365 let hdr = header(8, expected.len() as u64);
366 let out = decompress(&hdr, Some(fork)).expect("real LZVN must decode");
367 assert_eq!(out, expected, "decoded bytes must match the original file");
368 }
369
370 #[test]
372 fn decodes_real_macos_zlib_resource_fork() {
373 let fork = include_bytes!("../tests/data/decmpfs/real_zlib_rsrc.rsrc");
374 let expected = include_bytes!("../tests/data/decmpfs/zlib.expected");
375 let hdr = header(4, expected.len() as u64);
376 let out = decompress(&hdr, Some(fork)).expect("real type-4 zlib must decode");
377 assert_eq!(out, expected);
378 }
379
380 #[test]
382 fn decodes_real_macos_inline_zlib() {
383 let payload = include_bytes!("../tests/data/decmpfs/real_zlib_inline.payload");
384 let expected = include_bytes!("../tests/data/decmpfs/real_zlib_inline.expected");
385 let x = xattr(3, expected.len() as u64, payload);
386 let out = decompress(&x, None).expect("real type-3 inline zlib must decode");
387 assert_eq!(out, expected);
388 }
389
390 #[test]
392 fn decodes_inline_zlib_stored_marker() {
393 let payload = include_bytes!("../tests/data/decmpfs/zlib_type3_stored.payload");
394 let expected = include_bytes!("../tests/data/decmpfs/zlib_inline.expected");
395 let x = xattr(3, expected.len() as u64, payload);
396 let out = decompress(&x, None).expect("0xFF-stored type-3 must decode");
397 assert_eq!(out, expected);
398 }
399
400 #[test]
402 fn decodes_inline_uncompressed() {
403 let data = b"the quick brown fox jumps over the lazy dog";
404 let x = xattr(1, data.len() as u64, data);
405 let out = decompress(&x, None).expect("type-1 uncompressed must decode");
406 assert_eq!(out, data);
407 }
408
409 fn chunked_fork(chunks: &[Vec<u8>]) -> Vec<u8> {
412 let header_size = 4 * (chunks.len() + 1);
413 let mut fork = Vec::new();
414 fork.extend_from_slice(&(header_size as u32).to_le_bytes());
415 let mut end = header_size;
416 for c in chunks {
417 end += c.len();
418 fork.extend_from_slice(&(end as u32).to_le_bytes());
419 }
420 for c in chunks {
421 fork.extend_from_slice(c);
422 }
423 fork
424 }
425
426 #[test]
428 fn decodes_real_macos_lzfse_resource_fork() {
429 let fork = include_bytes!("../tests/data/decmpfs/real_lzfse_rsrc.rsrc");
430 let expected = include_bytes!("../tests/data/decmpfs/zlib.expected"); let hdr = header(12, expected.len() as u64);
432 let out = decompress(&hdr, Some(fork)).expect("real type-12 LZFSE must decode");
433 assert_eq!(out, expected);
434 }
435
436 #[test]
438 fn decodes_real_macos_inline_lzfse() {
439 let payload = include_bytes!("../tests/data/decmpfs/real_lzfse_inline.payload");
440 let expected = include_bytes!("../tests/data/decmpfs/real_zlib_inline.expected");
441 let x = xattr(11, expected.len() as u64, payload);
442 let out = decompress(&x, None).expect("real type-11 inline LZFSE must decode");
443 assert_eq!(out, expected);
444 }
445
446 #[test]
448 fn decodes_uncompressed_resource_fork() {
449 let mut data = Vec::new();
450 for i in 0..(CHUNK_SIZE + 5000) {
451 data.push((i % 251) as u8);
452 }
453 let c0 = data[..CHUNK_SIZE].to_vec();
454 let c1 = data[CHUNK_SIZE..].to_vec();
455 let fork = chunked_fork(&[c0, c1]);
456 let hdr = header(10, data.len() as u64);
457 let out = decompress(&hdr, Some(&fork)).expect("type-10 uncompressed fork must decode");
458 assert_eq!(out, data);
459 }
460
461 #[test]
463 fn decodes_inline_uncompressed_type9() {
464 let content = b"type 9 is uncompressed-inline, a variant of type 1";
470 let mut payload = vec![0xCC];
471 payload.extend_from_slice(content);
472 let x = xattr(9, content.len() as u64, &payload);
473 assert_eq!(decompress(&x, None).expect("type-9 must decode"), content);
474 }
475
476 #[test]
479 fn decodes_real_tahoe_type8_lzvn_with_trailing_bytes() {
480 let fork = include_bytes!("../tests/data/decmpfs/tahoe_type8.rsrc");
481 let expected = include_bytes!("../tests/data/decmpfs/tahoe_type8.expected");
482 let hdr = header(8, expected.len() as u64);
483 let out = decompress(&hdr, Some(fork)).expect("Tahoe LZVN must decode");
484 assert_eq!(out.as_slice(), expected.as_slice());
485 }
486
487 #[test]
489 fn decodes_real_tahoe_type9_inline_marker() {
490 let xattr_bytes = include_bytes!("../tests/data/decmpfs/tahoe_type9.decmpfs");
491 let expected = include_bytes!("../tests/data/decmpfs/tahoe_type9.expected");
492 let out = decompress(xattr_bytes, None).expect("Tahoe type-9 must decode");
493 assert_eq!(out.as_slice(), expected.as_slice());
494 }
495
496 #[test]
498 fn length_mismatch_is_loud() {
499 let data = b"the quick brown fox";
500 let x = xattr(1, 999, data); assert!(matches!(
502 decompress(&x, None),
503 Err(DecmpfsError::LengthMismatch { expected: 999, .. })
504 ));
505 }
506
507 #[test]
509 fn rejects_bad_magic() {
510 let mut x = xattr(1, 0, &[]);
511 x[0] ^= 0xFF;
512 assert!(matches!(
513 decompress(&x, None),
514 Err(DecmpfsError::BadMagic(_))
515 ));
516 }
517
518 #[test]
519 fn rejects_truncated_header() {
520 assert_eq!(decompress(&[0u8; 8], None), Err(DecmpfsError::Truncated));
521 }
522
523 #[test]
524 fn rejects_unknown_type() {
525 let x = xattr(99, 0, &[]);
526 assert_eq!(decompress(&x, None), Err(DecmpfsError::UnknownType(99)));
527 }
528
529 #[test]
530 fn rejects_lzbitmap_unsupported() {
531 let x = xattr(14, 0, &[]);
532 assert!(matches!(
533 decompress(&x, None),
534 Err(DecmpfsError::Unsupported(_))
535 ));
536 }
537
538 #[test]
539 fn rejects_dedup_type5_unsupported() {
540 let x = xattr(5, 0, &[]);
541 assert!(matches!(
542 decompress(&x, None),
543 Err(DecmpfsError::Unsupported(_))
544 ));
545 }
546
547 #[test]
548 fn resource_fork_type_without_fork_errors() {
549 let hdr = header(8, 100);
550 assert_eq!(
551 decompress(&hdr, None),
552 Err(DecmpfsError::MissingResourceFork)
553 );
554 }
555
556 #[test]
559 fn error_display_is_self_describing_per_variant() {
560 assert_eq!(
561 DecmpfsError::Truncated.to_string(),
562 "decmpfs xattr shorter than 16-byte header"
563 );
564 assert_eq!(
565 DecmpfsError::BadMagic(0xdead_beef).to_string(),
566 "decmpfs bad magic 0xdeadbeef (expected 'cmpf')"
567 );
568 assert_eq!(
569 DecmpfsError::UnknownType(99).to_string(),
570 "decmpfs unknown compression_type 99"
571 );
572 assert_eq!(
573 DecmpfsError::Unsupported("LZBitmap (no public spec)").to_string(),
574 "decmpfs unsupported: LZBitmap (no public spec)"
575 );
576 assert_eq!(
577 DecmpfsError::MissingResourceFork.to_string(),
578 "decmpfs resource-fork type but no resource fork supplied"
579 );
580 assert_eq!(
581 DecmpfsError::OutOfBounds.to_string(),
582 "decmpfs length/offset field out of bounds"
583 );
584 assert_eq!(
585 DecmpfsError::Codec("zlib").to_string(),
586 "decmpfs codec error: zlib"
587 );
588 assert_eq!(
589 DecmpfsError::LengthMismatch {
590 expected: 999,
591 got: 19
592 }
593 .to_string(),
594 "decmpfs length mismatch: expected 999, got 19"
595 );
596 }
597}