Skip to main content

forensicnomicon_core/
decmpfs.rs

1//! HFS+/APFS transparent-compression (`decmpfs`) on-disk format constants.
2//!
3//! Apple's AppleFSCompression mechanism ("decmpfs") stores a file's data in
4//! compressed form, transparently decompressed by the kernel on read. A
5//! compressed file carries a `com.apple.decmpfs` extended attribute whose first
6//! 16 bytes are a header (magic, compression type, uncompressed size). The
7//! `compression_type` selects both the **algorithm** (Zlib / LZVN / LZFSE /
8//! uncompressed / LZBitmap) and the **storage location** of the payload:
9//!
10//! - **odd** types store the payload **inline** in the `com.apple.decmpfs`
11//!   xattr, immediately after the 16-byte header (used for small files);
12//! - **even** types store the payload in the file's **resource fork**, split
13//!   into independently-compressed [`CHUNK_SIZE`]-byte chunks indexed by a block
14//!   table (used for larger files).
15//!
16//! This module is facts only — the magic, the type→(algorithm, storage) map,
17//! the header field offsets, and the chunk size. The decoding algorithm (header
18//! parse, block-table walk, codec dispatch) lives in the consuming reader
19//! (`hfsplus-forensic`), per forensicnomicon's knowledge-only charter.
20//!
21//! # Authoritative sources
22//!
23//! Apple has never published a decmpfs specification; the layout below is the
24//! settled reverse-engineered consensus of the forensic community:
25//!
26//! - Apple XNU kernel, `bsd/kern/decmpfs.c` + `bsd/sys/decmpfs.h` — the
27//!   `decmpfs_disk_header` struct (`compression_magic` / `compression_type` /
28//!   `uncompressed_size`) and the kernel-handled types (1/3/4/7/8/11/12):
29//!   <https://github.com/apple-oss-distributions/xnu/blob/main/bsd/kern/decmpfs.c>
30//! - The Sleuth Kit, `tsk/fs/hfs.c` — the canonical forensic implementation;
31//!   its `decmpfs` switch documents types 3/4/7/8/9/10/11/12 and the 64 KiB
32//!   chunking: <https://github.com/sleuthkit/sleuthkit>
33//! - ydkhatri `mac_apt`, `plugins/helpers/structs.py` + `hfs_alt.py` — the
34//!   `HFSPlusDecmpfs`, `HFSPlusCmpfRsrcHead` (Zlib resource fork) and
35//!   `HFSPlusCmpfLZVNRsrcHead` (LZVN/LZFSE resource fork) block-table layouts:
36//!   <https://github.com/ydkhatri/mac_apt>
37//! - libyal `libfshfs`, *Apple Hierarchical File System plus (HFS+)* — the
38//!   resource-fork compressed-data block table:
39//!   <https://github.com/libyal/libfshfs>
40//!
41//! # Compression types
42//!
43//! | Type | Algorithm    | Storage        | Notes                                   |
44//! |------|--------------|----------------|-----------------------------------------|
45//! | 1    | uncompressed | inline xattr   | payload verbatim after the header       |
46//! | 3    | Zlib         | inline xattr   | leading `0xFF` ⇒ remainder stored raw   |
47//! | 4    | Zlib         | resource fork  | classic resource-manager block table    |
48//! | 5    | (dedup)      | —              | de-dup generation store; no payload here |
49//! | 7    | LZVN         | inline xattr   |                                         |
50//! | 8    | LZVN         | resource fork  | macOS default for most files            |
51//! | 9    | uncompressed | inline xattr   | variant of type 1                       |
52//! | 10   | uncompressed | resource fork  | chunked, uncompressed                   |
53//! | 11   | LZFSE        | inline xattr   |                                         |
54//! | 12   | LZFSE        | resource fork  |                                         |
55//! | 13   | LZBitmap     | inline xattr   | no public spec                          |
56//! | 14   | LZBitmap     | resource fork  | no public spec                          |
57
58/// `com.apple.decmpfs` header magic: ASCII `'cmpf'` read as a little-endian
59/// `u32` (on-disk bytes `66 70 6d 63`).
60pub const MAGIC: u32 = 0x636d_7066;
61
62/// Length of the fixed `decmpfs` header that prefixes the xattr.
63pub const HEADER_LEN: usize = 16;
64
65/// Byte offset of `compression_type` (`u32` LE) within the header.
66pub const COMPRESSION_TYPE_OFFSET: usize = 4;
67
68/// Byte offset of `uncompressed_size` (`u64` LE) within the header.
69pub const UNCOMPRESSED_SIZE_OFFSET: usize = 8;
70
71/// Uncompressed size of each resource-fork chunk (the last chunk may be
72/// shorter). Every even (resource-fork) compression type chunks at this size.
73pub const CHUNK_SIZE: usize = 65536;
74
75/// Where a compression type keeps its payload.
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub enum Storage {
78    /// Inline in the `com.apple.decmpfs` xattr, after the 16-byte header.
79    Inline,
80    /// In the file's resource fork, chunked and indexed by a block table.
81    ResourceFork,
82}
83
84/// The compression algorithm a decmpfs payload uses.
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86#[non_exhaustive]
87pub enum Algorithm {
88    /// Stored verbatim (no compression).
89    Uncompressed,
90    /// Zlib / DEFLATE (types 3, 4).
91    Zlib,
92    /// Apple LZVN / libFastCompression (types 7, 8).
93    Lzvn,
94    /// Apple LZFSE (types 11, 12).
95    Lzfse,
96    /// Apple LZBitmap (types 13, 14) — no public specification.
97    LzBitmap,
98}
99
100/// A decoded `compression_type`: its algorithm and where the payload lives.
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub struct Compression {
103    /// The compression algorithm.
104    pub algorithm: Algorithm,
105    /// Where the payload is stored.
106    pub storage: Storage,
107}
108
109/// Classify a raw `compression_type` value.
110///
111/// Returns `None` for type 5 (the de-dup generation store, which carries no
112/// payload in this xattr) and for any type not in the documented set — the
113/// caller must fail loud rather than guess.
114#[must_use]
115pub fn classify(compression_type: u32) -> Option<Compression> {
116    use Algorithm::{LzBitmap, Lzfse, Lzvn, Uncompressed, Zlib};
117    use Storage::{Inline, ResourceFork};
118    let (algorithm, storage) = match compression_type {
119        1 | 9 => (Uncompressed, Inline),
120        10 => (Uncompressed, ResourceFork),
121        3 => (Zlib, Inline),
122        4 => (Zlib, ResourceFork),
123        7 => (Lzvn, Inline),
124        8 => (Lzvn, ResourceFork),
125        11 => (Lzfse, Inline),
126        12 => (Lzfse, ResourceFork),
127        13 => (LzBitmap, Inline),
128        14 => (LzBitmap, ResourceFork),
129        // Type 5 is the de-dup generation store (no payload here); every other
130        // value is undocumented — the caller must fail loud, not guess.
131        _ => return None,
132    };
133    Some(Compression { algorithm, storage })
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn magic_is_cmpf_little_endian() {
142        assert_eq!(MAGIC, 0x636d_7066);
143        assert_eq!(MAGIC.to_le_bytes(), *b"fpmc");
144    }
145
146    #[test]
147    fn header_layout_constants() {
148        assert_eq!(HEADER_LEN, 16);
149        assert_eq!(COMPRESSION_TYPE_OFFSET, 4);
150        assert_eq!(UNCOMPRESSED_SIZE_OFFSET, 8);
151        assert_eq!(CHUNK_SIZE, 65536);
152    }
153
154    #[test]
155    fn zlib_types_3_inline_4_resource_fork() {
156        assert_eq!(
157            classify(3),
158            Some(Compression {
159                algorithm: Algorithm::Zlib,
160                storage: Storage::Inline
161            })
162        );
163        assert_eq!(
164            classify(4),
165            Some(Compression {
166                algorithm: Algorithm::Zlib,
167                storage: Storage::ResourceFork
168            })
169        );
170    }
171
172    #[test]
173    fn lzvn_types_7_inline_8_resource_fork() {
174        assert_eq!(
175            classify(7),
176            Some(Compression {
177                algorithm: Algorithm::Lzvn,
178                storage: Storage::Inline
179            })
180        );
181        assert_eq!(
182            classify(8),
183            Some(Compression {
184                algorithm: Algorithm::Lzvn,
185                storage: Storage::ResourceFork
186            })
187        );
188    }
189
190    #[test]
191    fn lzfse_types_11_inline_12_resource_fork() {
192        assert_eq!(
193            classify(11),
194            Some(Compression {
195                algorithm: Algorithm::Lzfse,
196                storage: Storage::Inline
197            })
198        );
199        assert_eq!(
200            classify(12),
201            Some(Compression {
202                algorithm: Algorithm::Lzfse,
203                storage: Storage::ResourceFork
204            })
205        );
206    }
207
208    #[test]
209    fn uncompressed_types_1_9_inline_10_resource_fork() {
210        for inline in [1, 9] {
211            assert_eq!(
212                classify(inline),
213                Some(Compression {
214                    algorithm: Algorithm::Uncompressed,
215                    storage: Storage::Inline
216                })
217            );
218        }
219        assert_eq!(
220            classify(10),
221            Some(Compression {
222                algorithm: Algorithm::Uncompressed,
223                storage: Storage::ResourceFork
224            })
225        );
226    }
227
228    #[test]
229    fn lzbitmap_types_13_inline_14_resource_fork() {
230        assert_eq!(
231            classify(13),
232            Some(Compression {
233                algorithm: Algorithm::LzBitmap,
234                storage: Storage::Inline
235            })
236        );
237        assert_eq!(
238            classify(14),
239            Some(Compression {
240                algorithm: Algorithm::LzBitmap,
241                storage: Storage::ResourceFork
242            })
243        );
244    }
245
246    #[test]
247    fn dedup_type_5_and_unknown_types_are_none() {
248        assert_eq!(classify(5), None); // de-dup generation store
249        assert_eq!(classify(0), None);
250        assert_eq!(classify(2), None);
251        assert_eq!(classify(99), None);
252    }
253
254    #[test]
255    fn parity_rule_odd_inline_even_resource_fork() {
256        // Every documented compressing type follows odd⇒inline, even⇒resource-fork.
257        for t in [3, 4, 7, 8, 11, 12, 13, 14] {
258            let c = classify(t).expect("documented type");
259            let expected = if t % 2 == 1 {
260                Storage::Inline
261            } else {
262                Storage::ResourceFork
263            };
264            assert_eq!(c.storage, expected, "type {t}");
265        }
266    }
267}