Skip to content
API Documentation
BastionGuard™
hfs_decmpfs.cpp
Go to the documentation of this file.
1// hfs_decmpfs.cpp -- decmpfs zlib decoder (resource-fork variant).
2
3#include "hfs_decmpfs.hpp"
4
5#include <zlib.h>
6
7#include <algorithm>
8#include <cstring>
9#include <sstream>
10
11namespace hfs {
12
13namespace {
14
15// Little-endian readers (decmpfs chunked stream is little-endian)
16inline uint32_t le32(const unsigned char* p) {
17 return static_cast<uint32_t>(p[0])
18 | (static_cast<uint32_t>(p[1]) << 8)
19 | (static_cast<uint32_t>(p[2]) << 16)
20 | (static_cast<uint32_t>(p[3]) << 24);
21}
22
23// Big-endian readers (resource fork header is big-endian, classic Mac)
24inline uint32_t be32(const unsigned char* p) {
25 return (static_cast<uint32_t>(p[0]) << 24)
26 | (static_cast<uint32_t>(p[1]) << 16)
27 | (static_cast<uint32_t>(p[2]) << 8)
28 | static_cast<uint32_t>(p[3]);
29}
30
31// Inflate a single zlib chunk into `out_buf`. Returns true if the
32// uncompressed result is exactly `out_size` bytes (or less if the
33// caller allowed it). The first byte determines whether the chunk is
34// raw or compressed:
35// - 0xFF byte at start -> the rest is raw uncompressed data
36// - otherwise -> standard zlib stream
37bool inflate_chunk(const unsigned char* in_buf, size_t in_size,
38 unsigned char* out_buf, size_t out_size,
39 size_t& out_produced, std::string& error) {
40 if (in_size == 0) {
41 error = "empty chunk";
42 return false;
43 }
44 // Check for the 0xFF "raw" marker
45 if (in_buf[0] == 0xFF) {
46 const size_t raw_len = in_size - 1;
47 if (raw_len > out_size) {
48 error = "raw chunk larger than output buffer";
49 return false;
50 }
51 std::memcpy(out_buf, in_buf + 1, raw_len);
52 out_produced = raw_len;
53 return true;
54 }
55
56 // Standard zlib stream
57 z_stream zs{};
58 zs.next_in = const_cast<Bytef*>(in_buf);
59 zs.avail_in = static_cast<uInt>(in_size);
60 zs.next_out = out_buf;
61 zs.avail_out = static_cast<uInt>(out_size);
62 int ret = inflateInit(&zs);
63 if (ret != Z_OK) {
64 error = "inflateInit failed";
65 return false;
66 }
67 ret = inflate(&zs, Z_FINISH);
68 if (ret != Z_STREAM_END && ret != Z_OK) {
69 error = std::string("inflate failed: ") +
70 (zs.msg ? zs.msg : "unknown error");
71 inflateEnd(&zs);
72 return false;
73 }
74 out_produced = static_cast<size_t>(zs.total_out);
75 inflateEnd(&zs);
76 return true;
77}
78
79} // anonymous namespace
80
81bool parse_decmpfs_header(const unsigned char* buf, DecmpfsHeader& out) {
82 if (buf == nullptr) return false;
83 out = {};
84 out.magic = le32(buf + 0);
85 if (out.magic != DECMPFS_MAGIC) return false;
86 out.type = le32(buf + 4);
87 // The size field is little-endian 64-bit
88 uint64_t size_lo = le32(buf + 8);
89 uint64_t size_hi = le32(buf + 12);
90 out.size = size_lo | (size_hi << 32);
91 return true;
92}
93
94const char* decmpfs_type_name(uint32_t type) {
95 switch (type) {
96 case kCompressionType_None: return "raw";
97 case kCompressionType_ZlibInXattr: return "zlib-xattr";
98 case kCompressionType_ZlibInRsrc: return "zlib-rsrc";
99 case kCompressionType_LzvnInXattr: return "lzvn-xattr";
100 case kCompressionType_LzvnInRsrc: return "lzvn-rsrc";
101 case kCompressionType_LzfseInXattr: return "lzfse-xattr";
102 case kCompressionType_LzfseInRsrc: return "lzfse-rsrc";
103 default: return "unknown";
104 }
105}
106
107bool decmpfs_supported(uint32_t type) {
108 return type == kCompressionType_ZlibInRsrc ||
110}
111
112// =========================================================================
113// Decmpfs type 3 (zlib in xattr) decoder
114// =========================================================================
115//
116// When the file is small enough (typically < ~3.6 KB on HFS+), decmpfs
117// stores both the header AND the compressed payload directly in the
118// "com.apple.decmpfs" extended attribute. The xattr layout is:
119//
120// offset 0: decmpfs header (16 bytes: magic + type + uncompressed_size)
121// offset 16: payload, EITHER
122// - a single zlib deflate stream (most common), OR
123// - 0xFF byte followed by raw uncompressed data
124// (= "no compression" marker, used when the file is
125// incompressible or already smaller raw)
126//
127// Note that this is DIFFERENT from the resource-fork variant (type 4)
128// which uses a chunked stream with little-endian framing. Type 3 is
129// just a plain zlib stream OR a raw byte after the 0xFF marker.
130
131bool decompress_xattr_zlib(const unsigned char* xattr_buf,
132 size_t xattr_len,
133 uint64_t expected_uncompressed_size,
134 std::vector<unsigned char>& out,
135 std::string& error) {
136 out.clear();
137 if (xattr_len < DECMPFS_HEADER_SIZE) {
138 error = "xattr too small for decmpfs header";
139 return false;
140 }
142 if (!parse_decmpfs_header(xattr_buf, h)) {
143 error = "decmpfs header magic mismatch";
144 return false;
145 }
147 error = "compression type is not zlib-xattr";
148 return false;
149 }
150 if (h.size != expected_uncompressed_size) {
151 // The catalog logical_size and the decmpfs header should agree.
152 // We tolerate a mismatch (use the header value).
153 }
154 const unsigned char* payload = xattr_buf + DECMPFS_HEADER_SIZE;
155 const size_t payload_len = xattr_len - DECMPFS_HEADER_SIZE;
156 if (payload_len == 0) {
157 error = "decmpfs xattr has no payload";
158 return false;
159 }
160
161 // The 0xFF marker means "raw uncompressed data follows"
162 if (payload[0] == 0xFF) {
163 const size_t raw_len = payload_len - 1;
164 if (raw_len < h.size) {
165 // Tolerate: maybe the size declared is wrong; use what we
166 // have.
167 out.assign(payload + 1, payload + 1 + raw_len);
168 } else {
169 out.assign(payload + 1, payload + 1 + h.size);
170 }
171 return true;
172 }
173
174 // Standard zlib stream
175 out.resize(static_cast<size_t>(h.size));
176 z_stream zs{};
177 zs.next_in = const_cast<Bytef*>(payload);
178 zs.avail_in = static_cast<uInt>(payload_len);
179 zs.next_out = out.data();
180 zs.avail_out = static_cast<uInt>(out.size());
181 int ret = inflateInit(&zs);
182 if (ret != Z_OK) {
183 error = "inflateInit failed";
184 return false;
185 }
186 ret = inflate(&zs, Z_FINISH);
187 if (ret != Z_STREAM_END && ret != Z_OK) {
188 error = std::string("inflate failed: ") +
189 (zs.msg ? zs.msg : "unknown error");
190 inflateEnd(&zs);
191 return false;
192 }
193 out.resize(static_cast<size_t>(zs.total_out));
194 inflateEnd(&zs);
195 return true;
196}
197
198bool decompress_resource_fork_zlib(const unsigned char* rsrc_buf,
199 size_t rsrc_len,
200 uint64_t expected_uncompressed_size,
201 std::vector<unsigned char>& out,
202 std::string& error) {
203 out.clear();
204 if (rsrc_len < 16) {
205 error = "resource fork too small for header";
206 return false;
207 }
208 // Resource fork header (big-endian):
209 // __be32 dataOffset
210 // __be32 mapOffset
211 // __be32 dataLength
212 // __be32 mapLength
213 const uint32_t dataOffset = be32(rsrc_buf + 0);
214 // const uint32_t mapOffset = be32(rsrc_buf + 4);
215 const uint32_t dataLength = be32(rsrc_buf + 8);
216 // const uint32_t mapLength = be32(rsrc_buf + 12);
217
218 if (static_cast<uint64_t>(dataOffset) + dataLength > rsrc_len) {
219 error = "resource fork data section out of range";
220 return false;
221 }
222
223 const unsigned char* data = rsrc_buf + dataOffset;
224 if (dataLength < 4) {
225 error = "resource fork data section too small";
226 return false;
227 }
228
229 // First 4 bytes of the data section: __be32 stream length
230 // (= dataLength - 4)
231 const uint32_t streamLen = be32(data + 0);
232 if (streamLen + 4u > dataLength) {
233 error = "resource fork stream length inconsistent";
234 return false;
235 }
236 const unsigned char* stream = data + 4;
237
238 // Chunked stream layout (little-endian!):
239 // __le32 numChunks
240 // struct { __le32 offset; __le32 size; } chunks[numChunks]
241 // chunk bytes
242 if (streamLen < 4) {
243 error = "stream too small for chunk count";
244 return false;
245 }
246 const uint32_t numChunks = le32(stream + 0);
247 if (numChunks == 0 || numChunks > 1'000'000) {
248 std::ostringstream m;
249 m << "implausible chunk count: " << numChunks;
250 error = m.str();
251 return false;
252 }
253 // Chunk index table starts at offset 4 within stream
254 const size_t chunk_table_size = static_cast<size_t>(numChunks) * 8;
255 if (4u + chunk_table_size > streamLen) {
256 error = "chunk table overruns stream";
257 return false;
258 }
259 const unsigned char* chunk_table = stream + 4;
260
261 // Pre-allocate output
262 out.reserve(static_cast<size_t>(expected_uncompressed_size));
263
264 // Decompress each chunk
265 for (uint32_t i = 0; i < numChunks; ++i) {
266 const uint32_t c_off = le32(chunk_table + i * 8 + 0);
267 const uint32_t c_size = le32(chunk_table + i * 8 + 4);
268 if (c_off + c_size > streamLen) {
269 std::ostringstream m;
270 m << "chunk " << i << " out of stream bounds";
271 error = m.str();
272 return false;
273 }
274 const unsigned char* c_data = stream + c_off;
275
276 // Each chunk decompresses to up to kDecmpfsBlockSize bytes
277 std::vector<unsigned char> chunk_out(kDecmpfsBlockSize);
278 size_t produced = 0;
279 std::string ce;
280 if (!inflate_chunk(c_data, c_size, chunk_out.data(),
281 chunk_out.size(), produced, ce)) {
282 std::ostringstream m;
283 m << "chunk " << i << ": " << ce;
284 error = m.str();
285 return false;
286 }
287 out.insert(out.end(), chunk_out.begin(), chunk_out.begin() + produced);
288 if (out.size() > expected_uncompressed_size) {
289 // Some files report a smaller logical size than the chunks
290 // produce -- truncate.
291 out.resize(static_cast<size_t>(expected_uncompressed_size));
292 break;
293 }
294 }
295
296 if (out.size() != expected_uncompressed_size) {
297 std::ostringstream m;
298 m << "decompressed size " << out.size()
299 << " differs from expected " << expected_uncompressed_size;
300 // This is a soft warning, not a fatal error: some volumes have
301 // size accounting that doesn't include trailing zeros, etc.
302 // We DO accept the buffer as long as it's non-empty, but log.
303 error = m.str();
304 // Still return true if we have nonzero bytes (caller checks
305 // error and decides). Return false only on truly empty output.
306 if (out.empty()) return false;
307 }
308 return true;
309}
310
311} // namespace hfs
uint32_t be32(const unsigned char *p)
bool inflate_chunk(const unsigned char *in_buf, size_t in_size, unsigned char *out_buf, size_t out_size, size_t &out_produced, std::string &error)
uint32_t le32(const unsigned char *p)
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
uint32_t be32(const unsigned char *p)
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