Skip to content
API Documentation
BastionGuard™
hfs_btree.hpp
Go to the documentation of this file.
1// hfs_btree.hpp -- HFS+ B-tree node parsing (header level only, session 1).
2//
3// Reference: TN1150 § "B-Trees".
4//
5// Every B-tree node starts with a 14-byte BTNodeDescriptor:
6// __be32 fLink forward sibling node number
7// __be32 bLink backward sibling node number
8// __u8 kind -1=leaf, 0=index, 1=header, 2=map
9// (stored as two's-complement signed byte)
10// __u8 height distance from leaves (leaves = 1, root = depth)
11// __be16 numRecords number of records in this node
12// __be16 reserved always 0
13//
14// The HEADER node (kind=1, always node 0 of the B-tree) is special:
15// after the descriptor, its first record holds a 106-byte BTHeaderRec
16// describing the tree (root node, depth, node size, etc.).
17//
18// In session 1 we only parse these two structures. The full B-tree
19// walker (descend index nodes, iterate leaf records) lands in session
20// 2 alongside the catalog/extents file readers.
21
22#pragma once
23
24#include <cstdint>
25#include <functional>
26#include <string>
27#include <vector>
28
29namespace hfs {
30
31// Node kinds
32constexpr int8_t kBTLeafNode = -1;
33constexpr int8_t kBTIndexNode = 0;
34constexpr int8_t kBTHeaderNode = 1;
35constexpr int8_t kBTMapNode = 2;
36
37// HFSX key-compare types (stored in BTHeaderRec.keyCompareType)
38constexpr uint8_t kHFSCaseFolding = 0xCF; // case-INsensitive (default)
39constexpr uint8_t kHFSBinaryCompare = 0xBC; // case-SENSITIVE (HFSX)
40
41// HFS+ btreeType values (stored in BTHeaderRec.btreeType)
42constexpr uint8_t kHFSBTreeType = 0xFF;
43constexpr uint8_t kUserBTreeType = 0x80;
44constexpr uint8_t kReservedBTreeType = 0x00;
45
46// 14-byte node descriptor at the start of every node.
48 uint32_t fLink {0}; // next-sibling node number, 0 if none
49 uint32_t bLink {0}; // prev-sibling node number, 0 if none
50 int8_t kind {0};
51 uint8_t height {0};
52 uint16_t numRecords {0};
53};
54
55// Parse a 14-byte BTNodeDescriptor. Returns false if reserved fields
56// look broken (we don't enforce reserved == 0 strictly because some
57// damaged nodes still have valid records).
58bool parse_node_descriptor(const unsigned char* buf, NodeDescriptor& out);
59
60// 106-byte BTHeaderRec, found in the first record of node 0 of every
61// HFS+ B-tree (catalog, extents, attributes, allocation has its own
62// special layout).
63struct HeaderRec {
64 uint16_t treeDepth {0}; // height of the tree (1 = single leaf)
65 uint32_t rootNode {0}; // node number of the root
66 uint32_t leafRecords {0}; // total leaf-level records in the tree
67 uint32_t firstLeafNode {0}; // first leaf in left-to-right order
68 uint32_t lastLeafNode {0};
69 uint16_t nodeSize {0}; // node size in bytes (typ. 4096 / 8192)
70 uint16_t maxKeyLength {0};
71 uint32_t totalNodes {0};
72 uint32_t freeNodes {0};
73 uint32_t clumpSize {0};
74 uint8_t btreeType {0};
75 uint8_t keyCompareType {0}; // HFSX only; 0 on HFS+ classic
76 uint32_t attributes {0};
77};
78
79// Parse a 106-byte BTHeaderRec at the given pointer.
80// The caller is responsible for locating the first record of the
81// header node (typically at offset 14 = sizeof(NodeDescriptor) within
82// the node buffer).
83bool parse_header_rec(const unsigned char* buf, HeaderRec& out);
84
85// Returns the byte offset, within a node of size `nodeSize`, of the
86// `i`-th record. The record offsets are stored as 16-bit big-endian
87// values at the END of the node, in REVERSE order: the last 2 bytes
88// are the offset of record 0, the 2 bytes before that are record 1's
89// offset, and so on. (TN1150 § "Record Offsets".)
90//
91// Returns 0 if `i >= numRecords` or if the offset table would land
92// outside the node.
93uint16_t node_record_offset(const unsigned char* node_buf,
94 uint16_t nodeSize,
95 uint16_t numRecords,
96 uint16_t i);
97
98// Convenience wrapper: returns the byte length of record `i`,
99// computed as offset[i+1] - offset[i] (or end-of-records-area - offset
100// for the last record).
101uint16_t node_record_length(const unsigned char* node_buf,
102 uint16_t nodeSize,
103 uint16_t numRecords,
104 uint16_t i);
105
106// =============================================================================
107// Catalog record types (HFS+ kCatalogRecordType*)
108// =============================================================================
109//
110// Each leaf record in the catalog B-tree starts with a 2-byte big-endian
111// recordType discriminator immediately after the key. We expose the four
112// values; their bodies are decoded in session 3.
113
114constexpr int16_t kHFSPlusFolderRecord = 0x0001;
115constexpr int16_t kHFSPlusFileRecord = 0x0002;
116constexpr int16_t kHFSPlusFolderThreadRecord = 0x0003;
117constexpr int16_t kHFSPlusFileThreadRecord = 0x0004;
118
119// =============================================================================
120// Catalog key
121// =============================================================================
122//
123// HFSPlusCatalogKey layout:
124// __be16 keyLength (length of the rest of the key, NOT including
125// this 2-byte length field itself)
126// __be32 parentID (parent folder CNID)
127// __be16 nameLength (number of UTF-16 code units, NOT bytes)
128// UniChar name[nameLength] (UTF-16-BE, NFD)
129//
130// Total key size = 2 + keyLength bytes. The name length in bytes is
131// nameLength * 2.
132
134 uint32_t parentID {0};
135 uint16_t nameLength {0}; // UTF-16 code units
136 const unsigned char* name_utf16be {nullptr}; // points INTO the buffer
137 uint16_t total_key_bytes {0}; // 2 (keyLength) + keyLength
138};
139
140// Parse a catalog key starting at `key_buf`. Returns false if the key
141// length would overflow `max_bytes`.
142bool parse_catalog_key(const unsigned char* key_buf,
143 uint32_t max_bytes,
144 CatalogKey& out);
145
146// Compute the byte offset (within a record buffer) where the record body
147// starts, i.e. immediately after the catalog key. Index nodes use this
148// same layout (key + 4-byte child node pointer).
149//
150// Returns 0 on bad key.
151uint32_t catalog_record_body_offset(const unsigned char* record_buf,
152 uint32_t record_len);
153
154// =============================================================================
155// Generic B-tree walker
156// =============================================================================
157//
158// We walk a B-tree rooted at `header.rootNode` recursively. For every
159// leaf record found, the callback is invoked with the raw record buffer.
160// The callback can return false to stop traversal early.
161//
162// `read_node(node_index, out_buf)` is a user-supplied function that reads
163// `header.nodeSize` bytes for the given node index. This indirection lets
164// us drive the walker against:
165// - a real on-disk catalog file (via fork reader)
166// - an in-memory mock (for tests)
167//
168// The walker DOES NOT itself decode catalog records; callers do that.
169// We only know how to parse the node descriptor + record offsets.
170
172 std::function<bool(uint32_t node_index, std::vector<unsigned char>& out)>;
173
175 std::function<bool(const unsigned char* record_buf, uint16_t record_len)>;
176
177bool walk_btree(const HeaderRec& header,
178 const NodeReader& read_node,
179 const LeafRecordCallback& on_leaf,
180 std::vector<std::string>& logs,
181 std::string& error);
182
183} // namespace hfs
constexpr uint8_t kUserBTreeType
Definition hfs_btree.hpp:43
constexpr uint8_t kHFSBTreeType
Definition hfs_btree.hpp:42
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
constexpr uint8_t kHFSBinaryCompare
Definition hfs_btree.hpp:39
std::function< bool(uint32_t node_index, std::vector< unsigned char > &out)> NodeReader
constexpr int8_t kBTIndexNode
Definition hfs_btree.hpp:33
uint16_t node_record_length(const unsigned char *node_buf, uint16_t nodeSize, uint16_t numRecords, uint16_t i)
Definition hfs_btree.cpp:88
constexpr uint8_t kReservedBTreeType
Definition hfs_btree.hpp:44
uint16_t node_record_offset(const unsigned char *node_buf, uint16_t nodeSize, uint16_t numRecords, uint16_t i)
Definition hfs_btree.cpp:70
constexpr int16_t kHFSPlusFolderRecord
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
constexpr int16_t kHFSPlusFileRecord
constexpr uint8_t kHFSCaseFolding
Definition hfs_btree.hpp:38
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 int16_t kHFSPlusFolderThreadRecord
constexpr int8_t kBTMapNode
Definition hfs_btree.hpp:35
constexpr int8_t kBTLeafNode
Definition hfs_btree.hpp:32
constexpr int16_t kHFSPlusFileThreadRecord
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