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}