Skip to content
API Documentation
BastionGuard™
hfs_btree.cpp
Go to the documentation of this file.
1// hfs_btree.cpp -- HFS+ B-tree node header parsers.
2//
3// Implementation is intentionally minimal in session 1: we parse the
4// node descriptor (14 bytes) and the BTHeaderRec (106 bytes), plus
5// helpers to compute record offsets within a node. The actual B-tree
6// traversal -- descending index nodes, iterating leaf records -- is
7// session 2 work.
8
9#include "hfs_btree.hpp"
10#include "hfs_format.hpp"
11
12#include <set>
13#include <sstream>
14
15namespace hfs {
16
17bool parse_node_descriptor(const unsigned char* buf, NodeDescriptor& out) {
18 if (buf == nullptr) return false;
19 out = {};
20 out.fLink = be32(buf + 0x00);
21 out.bLink = be32(buf + 0x04);
22 // kind is a SIGNED 8-bit value (-1 for leaf, 0/1/2 otherwise)
23 out.kind = static_cast<int8_t>(buf[0x08]);
24 out.height = buf[0x09];
25 out.numRecords = be16(buf + 0x0A);
26 // reserved at +0x0C..+0x0E ignored
27
28 // Sanity: kind must be -1, 0, 1, or 2
29 if (out.kind != kBTLeafNode &&
30 out.kind != kBTIndexNode &&
31 out.kind != kBTHeaderNode &&
32 out.kind != kBTMapNode) {
33 return false;
34 }
35 return true;
36}
37
38bool parse_header_rec(const unsigned char* buf, HeaderRec& out) {
39 if (buf == nullptr) return false;
40 out = {};
41 out.treeDepth = be16(buf + 0x00);
42 out.rootNode = be32(buf + 0x02);
43 out.leafRecords = be32(buf + 0x06);
44 out.firstLeafNode = be32(buf + 0x0A);
45 out.lastLeafNode = be32(buf + 0x0E);
46 out.nodeSize = be16(buf + 0x12);
47 out.maxKeyLength = be16(buf + 0x14);
48 out.totalNodes = be32(buf + 0x16);
49 out.freeNodes = be32(buf + 0x1A);
50 // 2 bytes reserved at +0x1E
51 out.clumpSize = be32(buf + 0x20);
52 out.btreeType = buf[0x24];
53 out.keyCompareType = buf[0x25];
54 out.attributes = be32(buf + 0x26);
55 // 64 bytes of reserved at +0x2A..+0x6A
56
57 // Sanity: nodeSize must be a power of 2 in [512, 32768]
58 // (uint16_t max is 65535, so the upper bound is implicit; any
59 // power-of-two value in our range is necessarily <= 32768)
60 if (out.nodeSize < 512 ||
61 (out.nodeSize & (out.nodeSize - 1)) != 0) {
62 return false;
63 }
64 // treeDepth must agree with totalNodes (depth==0 means empty tree;
65 // depth==1 means a single leaf; depth>16 is implausible).
66 if (out.treeDepth > 16) return false;
67 return true;
68}
69
70uint16_t node_record_offset(const unsigned char* node_buf,
71 uint16_t nodeSize,
72 uint16_t numRecords,
73 uint16_t i) {
74 if (i >= numRecords) return 0;
75 if (node_buf == nullptr) return 0;
76 if (nodeSize < 14 + 2u * (static_cast<uint32_t>(numRecords) + 1)) {
77 return 0; // not enough room for offset table
78 }
79 // The offsets table sits at the END of the node, with one 16-bit
80 // entry per record (numRecords entries) plus a "free space" offset
81 // (numRecords+1 entries total). The LAST 2 bytes hold the offset
82 // of record 0; the 2 before that hold record 1; and so on.
83 const uint32_t off_idx = static_cast<uint32_t>(nodeSize)
84 - 2u * (static_cast<uint32_t>(i) + 1u);
85 return be16(node_buf + off_idx);
86}
87
88uint16_t node_record_length(const unsigned char* node_buf,
89 uint16_t nodeSize,
90 uint16_t numRecords,
91 uint16_t i) {
92 if (i >= numRecords) return 0;
93 const uint16_t this_off = node_record_offset(node_buf, nodeSize,
94 numRecords, i);
95 if (this_off == 0) return 0;
96 // Next offset (which is at index i+1 in the descending table) gives
97 // the END of record `i`. For i == numRecords-1, the next entry IS
98 // the "free space" offset, which is the end of the records area.
99 const uint32_t next_idx = static_cast<uint32_t>(nodeSize)
100 - 2u * (static_cast<uint32_t>(i) + 2u);
101 if (next_idx + 2 > nodeSize) return 0;
102 const uint16_t next_off = be16(node_buf + next_idx);
103 if (next_off < this_off) return 0; // corrupt
104 return next_off - this_off;
105}
106
107// =============================================================================
108// Catalog key parser (session 2)
109// =============================================================================
110
111bool parse_catalog_key(const unsigned char* key_buf,
112 uint32_t max_bytes,
113 CatalogKey& out) {
114 if (key_buf == nullptr || max_bytes < 8) return false;
115 out = {};
116 const uint16_t keyLength = be16(key_buf + 0);
117 // Total key size = 2 (length field) + keyLength bytes of payload.
118 // Payload = parentID(4) + nameLength(2) + name[nameLength*2].
119 if (keyLength < 6) return false; // must include parentID + nameLen
120 if (2u + keyLength > max_bytes) return false;
121 out.total_key_bytes = 2u + keyLength;
122 out.parentID = be32(key_buf + 2);
123 out.nameLength = be16(key_buf + 6);
124 if (8u + static_cast<uint32_t>(out.nameLength) * 2u > 2u + keyLength) {
125 return false; // name overflows declared length
126 }
127 out.name_utf16be = key_buf + 8;
128 return true;
129}
130
131uint32_t catalog_record_body_offset(const unsigned char* record_buf,
132 uint32_t record_len) {
133 if (record_buf == nullptr || record_len < 8) return 0;
134 const uint16_t keyLength = be16(record_buf + 0);
135 const uint32_t key_total = 2u + keyLength;
136 if (key_total > record_len) return 0;
137 // After the key, records are aligned on a 16-bit boundary in the
138 // catalog file. Round up the key end to the next even byte.
139 return (key_total + 1u) & ~1u;
140}
141
142// =============================================================================
143// Generic B-tree walker (session 2)
144// =============================================================================
145//
146// Standard B-tree descent:
147// 1. Start at header.rootNode
148// 2. Read the node, parse its descriptor
149// 3. If it's an INDEX node:
150// for each record, parse the key, then read the 4-byte child
151// node pointer that immediately follows; recurse into the child
152// 4. If it's a LEAF node:
153// for each record, invoke on_leaf(record_buf, record_len)
154// 5. (Optional) Once we reach the leftmost leaf, we could follow
155// `fLink` siblings to enumerate all leaves without re-descending.
156// We do NOT use that here -- the recursive descent already visits
157// every leaf, and following sibling links risks infinite loops on
158// corrupted disks.
159//
160// Cycle detection: visited-node set, capped at 1M nodes.
161
162namespace {
163
164constexpr size_t MAX_NODES_VISITED = 1'000'000;
165
166bool descend(uint32_t node_index, uint16_t expected_height,
167 const HeaderRec& header,
168 const NodeReader& read_node,
169 const LeafRecordCallback& on_leaf,
170 std::set<uint32_t>& visited,
171 size_t& nodes_visited,
172 std::vector<std::string>& logs,
173 std::string& error,
174 bool& callback_stopped) {
175 if (callback_stopped) return true;
176 if (node_index == 0) {
177 // Node 0 is always the header node; descending into it from an
178 // index pointer means corruption.
179 return true;
180 }
181 if (++nodes_visited > MAX_NODES_VISITED) {
182 error = "HFS+ B-tree: traversal cap reached (possible loop)";
183 return false;
184 }
185 if (!visited.insert(node_index).second) {
186 std::ostringstream m;
187 m << "HFS+ B-tree: cycle detected at node " << node_index << ", skip";
188 logs.push_back(m.str());
189 return true;
190 }
191
192 std::vector<unsigned char> node;
193 if (!read_node(node_index, node)) {
194 std::ostringstream m;
195 m << "HFS+ B-tree: read failed for node " << node_index;
196 logs.push_back(m.str());
197 return true; // not fatal; just lose this branch
198 }
199 if (node.size() < header.nodeSize) {
200 std::ostringstream m;
201 m << "HFS+ B-tree: short read for node " << node_index;
202 logs.push_back(m.str());
203 return true;
204 }
205
207 if (!parse_node_descriptor(node.data(), nd)) {
208 std::ostringstream m;
209 m << "HFS+ B-tree: bad descriptor at node " << node_index;
210 logs.push_back(m.str());
211 return true;
212 }
213 // height check is informational; HFS+ headers historically populate
214 // height in different ways across implementations. Don't enforce.
215 (void)expected_height;
216
217 if (nd.kind != kBTLeafNode && nd.kind != kBTIndexNode) {
218 std::ostringstream m;
219 m << "HFS+ B-tree: unexpected kind=" << static_cast<int>(nd.kind)
220 << " at node " << node_index;
221 logs.push_back(m.str());
222 return true;
223 }
224
225 for (uint16_t i = 0; i < nd.numRecords; ++i) {
226 const uint16_t rec_off = node_record_offset(node.data(),
227 header.nodeSize,
228 nd.numRecords, i);
229 const uint16_t rec_len = node_record_length(node.data(),
230 header.nodeSize,
231 nd.numRecords, i);
232 if (rec_off == 0 || rec_len == 0) continue;
233 if (rec_off + rec_len > header.nodeSize) continue;
234
235 const unsigned char* recp = node.data() + rec_off;
236
237 if (nd.kind == kBTLeafNode) {
238 if (!on_leaf(recp, rec_len)) {
239 callback_stopped = true;
240 return true;
241 }
242 } else {
243 // Index record: [catalog key][__be32 child node pointer]
244 const uint32_t body_off = catalog_record_body_offset(recp, rec_len);
245 if (body_off == 0 || body_off + 4 > rec_len) continue;
246 const uint32_t child = be32(recp + body_off);
247 if (child == 0 || child >= header.totalNodes) {
248 std::ostringstream m;
249 m << "HFS+ B-tree: index node " << node_index
250 << " has implausible child pointer " << child;
251 logs.push_back(m.str());
252 continue;
253 }
254 if (!descend(child,
255 (expected_height > 0) ? expected_height - 1 : 0,
256 header, read_node, on_leaf,
257 visited, nodes_visited, logs, error,
258 callback_stopped)) {
259 return false;
260 }
261 if (callback_stopped) return true;
262 }
263 }
264 return true;
265}
266
267} // anonymous namespace
268
269bool walk_btree(const HeaderRec& header,
270 const NodeReader& read_node,
271 const LeafRecordCallback& on_leaf,
272 std::vector<std::string>& logs,
273 std::string& error) {
274 if (header.rootNode == 0 || header.nodeSize == 0) {
275 error = "HFS+ B-tree: empty/zero rootNode";
276 return false;
277 }
278 if (header.treeDepth > 16) {
279 error = "HFS+ B-tree: implausible tree depth";
280 return false;
281 }
282 std::set<uint32_t> visited;
283 size_t nodes_visited = 0;
284 bool callback_stopped = false;
285 return descend(header.rootNode, header.treeDepth, header, read_node,
286 on_leaf, visited, nodes_visited, logs, error,
287 callback_stopped);
288}
289
290} // namespace hfs
bool descend(uint32_t node_index, uint16_t expected_height, const HeaderRec &header, const NodeReader &read_node, const LeafRecordCallback &on_leaf, std::set< uint32_t > &visited, size_t &nodes_visited, std::vector< std::string > &logs, std::string &error, bool &callback_stopped)
std::function< bool(const unsigned char *record_buf, uint16_t record_len)> LeafRecordCallback
bool parse_node_descriptor(const unsigned char *buf, NodeDescriptor &out)
Definition hfs_btree.cpp:17
std::function< bool(uint32_t node_index, std::vector< unsigned char > &out)> NodeReader
constexpr int8_t kBTIndexNode
Definition hfs_btree.hpp:33
uint32_t be32(const unsigned char *p)
uint16_t node_record_length(const unsigned char *node_buf, uint16_t nodeSize, uint16_t numRecords, uint16_t i)
Definition hfs_btree.cpp:88
uint16_t node_record_offset(const unsigned char *node_buf, uint16_t nodeSize, uint16_t numRecords, uint16_t i)
Definition hfs_btree.cpp:70
bool walk_btree(const HeaderRec &header, const NodeReader &read_node, const LeafRecordCallback &on_leaf, std::vector< std::string > &logs, std::string &error)
bool parse_catalog_key(const unsigned char *key_buf, uint32_t max_bytes, CatalogKey &out)
constexpr int8_t kBTHeaderNode
Definition hfs_btree.hpp:34
uint32_t catalog_record_body_offset(const unsigned char *record_buf, uint32_t record_len)
bool parse_header_rec(const unsigned char *buf, HeaderRec &out)
Definition hfs_btree.cpp:38
constexpr int8_t kBTMapNode
Definition hfs_btree.hpp:35
constexpr int8_t kBTLeafNode
Definition hfs_btree.hpp:32
uint16_t be16(const unsigned char *p)
uint32_t parentID
const unsigned char * name_utf16be
uint16_t total_key_bytes
uint16_t nameLength
uint32_t rootNode
Definition hfs_btree.hpp:65
uint16_t nodeSize
Definition hfs_btree.hpp:69
uint32_t clumpSize
Definition hfs_btree.hpp:73
uint32_t totalNodes
Definition hfs_btree.hpp:71
uint32_t lastLeafNode
Definition hfs_btree.hpp:68
uint32_t leafRecords
Definition hfs_btree.hpp:66
uint8_t btreeType
Definition hfs_btree.hpp:74
uint16_t maxKeyLength
Definition hfs_btree.hpp:70
uint8_t keyCompareType
Definition hfs_btree.hpp:75
uint32_t attributes
Definition hfs_btree.hpp:76
uint32_t firstLeafNode
Definition hfs_btree.hpp:67
uint32_t freeNodes
Definition hfs_btree.hpp:72
uint16_t treeDepth
Definition hfs_btree.hpp:64