Skip to content
API Documentation
BastionGuard™
hfs_decmpfs.hpp
Go to the documentation of this file.
1// hfs_decmpfs.hpp -- minimal decmpfs (filesystem-level compression) decoder.
2//
3// macOS 10.6+ (HFS+ Plus, "Snow Leopard") introduced filesystem-level
4// per-file compression for system files. The mechanism:
5//
6// 1. The file's BSDInfo.ownerFlags has UF_COMPRESSED (0x20) set.
7// 2. The catalog file flags has kHFSHasAttributesBit (0x0004) set.
8// 3. The data fork is EMPTY.
9// 4. An extended attribute "com.apple.decmpfs" exists, containing a
10// 16-byte header: magic 'cmpf', compression type (UInt32),
11// uncompressed size (UInt64).
12// 5. Depending on the compression type, the actual compressed bytes
13// are stored in:
14// - The xattr value itself (small files, < ~3.6 KB) for types
15// 3 (zlib), 7 (LZVN), 11 (LZFSE)
16// - The resource fork (larger files) for types 4 (zlib),
17// 8 (LZVN), 12 (LZFSE)
18//
19// In this implementation we cover ONLY type 4 (zlib in resource fork).
20// This is the most common case for files large enough to be
21// interesting (frameworks, dylibs in /System). Files using inline
22// xattr storage (types 3/7/11) require parsing the Attributes B-tree,
23// which is out of scope for session 5.
24//
25// LZVN (type 8) and LZFSE (type 12) are also out of scope: LZVN needs
26// a ~150-line decoder; LZFSE is a substantial codec (~800+ lines).
27// Files using those will be flagged as "decmpfs-unsupported" and
28// skipped with a warning.
29//
30// RESOURCE FORK LAYOUT FOR TYPE 4 (zlib):
31//
32// The resource fork has a Mac classic resource map structure, but
33// for decmpfs the data is in a "cmpf" resource. The simplified layout
34// we observe:
35//
36// offset 0x00: __be32 dataOffset (= 0x100 typically)
37// offset 0x04: __be32 mapOffset
38// offset 0x08: __be32 dataLength
39// offset 0x0C: __be32 mapLength
40// ...
41// offset dataOffset: __be32 chunkedStreamLength (= dataLength - 4)
42// offset dataOffset+4: chunked zlib stream:
43// __le32 numChunks
44// struct ChunkInfo {
45// __le32 offset; // offset within the stream of this chunk
46// __le32 size; // compressed size of the chunk
47// } chunks[numChunks];
48// chunk_data...
49//
50// Each chunk decompresses to up to 64 KB (kDecmpfsBlockSize) of the
51// final file (the last chunk may be smaller).
52//
53// The endianness of the chunked stream is LITTLE-ENDIAN (this is one
54// of the few decmpfs fields not in big-endian -- a quirk).
55
56#pragma once
57
58#include <cstdint>
59#include <string>
60#include <vector>
61
62namespace hfs {
63
64constexpr uint32_t DECMPFS_MAGIC = 0x66706D63; // 'cmpf' (little-endian, 'fpmc'
65 // big-endian = 0x636D7066)
66constexpr size_t DECMPFS_HEADER_SIZE = 16;
67constexpr size_t kDecmpfsBlockSize = 64 * 1024; // 64 KiB per chunk
68
69// Compression types (subset; full list in xnu source).
70constexpr uint32_t kCompressionType_None = 1; // raw, in xattr
71constexpr uint32_t kCompressionType_ZlibInXattr = 3;
72constexpr uint32_t kCompressionType_ZlibInRsrc = 4;
73constexpr uint32_t kCompressionType_LzvnInXattr = 7;
74constexpr uint32_t kCompressionType_LzvnInRsrc = 8;
75constexpr uint32_t kCompressionType_LzfseInXattr = 11;
76constexpr uint32_t kCompressionType_LzfseInRsrc = 12;
77
79 uint32_t magic {0};
80 uint32_t type {0};
81 uint64_t size {0}; // uncompressed file size
82};
83
84// Parse the 16-byte decmpfs disk header. Returns false if magic mismatch.
85bool parse_decmpfs_header(const unsigned char* buf, DecmpfsHeader& out);
86
87// Returns a human-readable name for a compression type (for log messages).
88const char* decmpfs_type_name(uint32_t type);
89
90// Returns true if we can actually decompress the given type with this
91// build (currently only kCompressionType_ZlibInRsrc).
92bool decmpfs_supported(uint32_t type);
93
94// Decompress a complete resource fork buffer (zlib-in-rsrc, type 4)
95// into `out`, sized `expected_uncompressed_size` bytes.
96// Returns false on parse error, zlib error, or size mismatch.
97bool decompress_resource_fork_zlib(const unsigned char* rsrc_buf,
98 size_t rsrc_len,
99 uint64_t expected_uncompressed_size,
100 std::vector<unsigned char>& out,
101 std::string& error);
102
103// Decompress a complete decmpfs xattr buffer (zlib-in-xattr, type 3).
104// The 16-byte decmpfs header is at the start; the rest is either a
105// raw zlib stream or 0xFF-prefixed raw bytes.
106bool decompress_xattr_zlib(const unsigned char* xattr_buf,
107 size_t xattr_len,
108 uint64_t expected_uncompressed_size,
109 std::vector<unsigned char>& out,
110 std::string& error);
111
112} // namespace hfs
bool parse_decmpfs_header(const unsigned char *buf, DecmpfsHeader &out)
bool decompress_resource_fork_zlib(const unsigned char *rsrc_buf, size_t rsrc_len, uint64_t expected_uncompressed_size, std::vector< unsigned char > &out, std::string &error)
constexpr size_t kDecmpfsBlockSize
constexpr uint32_t kCompressionType_ZlibInXattr
bool decompress_xattr_zlib(const unsigned char *xattr_buf, size_t xattr_len, uint64_t expected_uncompressed_size, std::vector< unsigned char > &out, std::string &error)
constexpr uint32_t DECMPFS_MAGIC
const char * decmpfs_type_name(uint32_t type)
constexpr size_t DECMPFS_HEADER_SIZE
constexpr uint32_t kCompressionType_LzfseInXattr
constexpr uint32_t kCompressionType_LzvnInRsrc
bool decmpfs_supported(uint32_t type)
constexpr uint32_t kCompressionType_LzvnInXattr
constexpr uint32_t kCompressionType_LzfseInRsrc
constexpr uint32_t kCompressionType_ZlibInRsrc
constexpr uint32_t kCompressionType_None