Skip to content
API Documentation
BastionGuard™
hfs_catalog.hpp
Go to the documentation of this file.
1// hfs_catalog.hpp -- HFS+ catalog record body parsers.
2//
3// Reference: Apple TN1150, "HFS Plus Volume Format".
4//
5// Each leaf record in the catalog B-tree starts with a 2-byte big-endian
6// recordType discriminator (right after the variable-length catalog
7// key, aligned to a 16-bit boundary). Following the type:
8//
9// * 0x0001 (kHFSPlusFolderRecord) 88-byte body: HFSPlusCatalogFolder
10// * 0x0002 (kHFSPlusFileRecord) 248-byte body: HFSPlusCatalogFile
11// * 0x0003 (kHFSPlusFolderThreadRecord) variable body: thread record
12// * 0x0004 (kHFSPlusFileThreadRecord) variable body: thread record
13//
14// Thread records hold (parentCNID, name) for reverse lookup
15// (CNID -> path). They are NOT directory entries; folder/file records
16// are. We don't need threads for the recovery tree walk: we already
17// know the parent of every record from its catalog KEY (parentID is
18// the parent CNID).
19
20#pragma once
21
22#include "hfs_format.hpp"
23#include "hfs_btree.hpp"
24
25#include <cstdint>
26#include <string>
27#include <vector>
28
29namespace hfs {
30
31// HFSPlusBSDInfo (POSIX permissions, 16 bytes).
32struct BSDInfo {
33 uint32_t ownerID {0};
34 uint32_t groupID {0};
35 uint8_t adminFlags {0};
36 uint8_t ownerFlags {0};
37 uint16_t fileMode {0}; // POSIX file mode (S_IFREG | 0644 etc.)
38 uint32_t special {0}; // device id for char/blk; iNode num for hardlinks
39};
40
41// FolderInfo / FileInfo "Finder info" (16 bytes).
42// For files: fileType + fileCreator at offsets 0x00 and 0x04 (FourCC).
43// For folders: window/scroll info, irrelevant to recovery.
45 uint32_t fileType {0}; // FourCC, e.g. 'slnk' for symlinks
46 uint32_t fileCreator {0}; // FourCC, e.g. 'rhap' for symlinks
47 uint16_t finderFlags {0};
48 // The remaining 6 bytes are layout-specific; we don't need them.
49};
50
51// Symlink magic FourCCs (from /System/Library/Frameworks/...)
52constexpr uint32_t kSymLinkFileType = 0x736C6E6B; // 'slnk'
53constexpr uint32_t kSymLinkCreator = 0x72686170; // 'rhap'
54// Hardlink magic FourCCs (these make a regular file act as a hardlink
55// to an iNode file in HFS+ Private Data)
56constexpr uint32_t kHardLinkFileType = 0x686C6E6B; // 'hlnk'
57constexpr uint32_t kHFSPlusCreator = 0x6866732B; // 'hfs+'
58
59// HFSPlusCatalogFolder body (88 bytes, NOT including the 2-byte
60// recordType discriminator, which the caller has already read).
62 uint16_t flags {0};
63 uint32_t valence {0}; // number of items in this folder
64 uint32_t folderID {0}; // CNID of this folder
65 uint32_t createDate {0};
66 uint32_t contentModDate {0};
67 uint32_t attributeModDate {0};
68 uint32_t accessDate {0};
69 uint32_t backupDate {0};
72 uint32_t textEncoding {0};
73};
74
75// HFSPlusCatalogFile body (248 bytes, NOT including recordType).
77 uint16_t flags {0};
78 uint32_t fileID {0};
79 uint32_t createDate {0};
80 uint32_t contentModDate {0};
81 uint32_t attributeModDate {0};
82 uint32_t accessDate {0};
83 uint32_t backupDate {0};
85 FinderInfo16 userInfo {}; // file: fileType + fileCreator
86 uint32_t textEncoding {0};
89
98};
99
100// HFS+ catalog file flags (subset)
101constexpr uint16_t kHFSFileLockedBit = 0x0001;
102constexpr uint16_t kHFSThreadExistsBit = 0x0002;
103constexpr uint16_t kHFSHasAttributesBit = 0x0004;
104constexpr uint16_t kHFSHasSecurityBit = 0x0008;
105constexpr uint16_t kHFSHasFolderCountBit = 0x0010;
106constexpr uint16_t kHFSHasLinkChainBit = 0x0020;
107constexpr uint16_t kHFSHasChildLinkBit = 0x0040;
108constexpr uint16_t kHFSHasDateAddedBit = 0x0080;
109
110// UF_COMPRESSED in BSDInfo.ownerFlags (== struct stat::st_flags bit)
111// indicates a decmpfs-compressed file.
112constexpr uint8_t UF_COMPRESSED = 0x20;
113
114// =============================================================================
115// Hardlink helpers
116// =============================================================================
117//
118// HFS+ implements POSIX hardlinks via "indirect node files" living
119// under a hidden folder named "\x00\x00\x00\x00HFS+ Private Data" at
120// the volume root. Each indirect node is a regular file named
121// "iNode<NUM>", where <NUM> is a decimal number stored in the link's
122// permissions.special field. The link's data fork is empty; its
123// userInfo has fileType='hlnk' and fileCreator='hfs+'.
124//
125// Resolving a hardlink means:
126// 1. The catalog has a "HFS+ Private Data" folder under root with
127// a special name (4 NUL bytes followed by "HFS+ Private Data")
128// 2. Inside it, files "iNode1", "iNode2", ... contain the target's
129// data fork
130// 3. We build a map iNode_num -> CatalogFile during the walk
131// 4. When exporting a hardlink, we read the target's data fork
132
133// The hidden folder's name as UTF-16 code units (BE on disk). 4 NUL +
134// "HFS+ Private Data" = 4 + 17 = 21 code units.
135constexpr int kHFSPrivateDataNameLength = 21;
136
137// Returns true if `utf16be_name` (length `name_units`) matches the
138// "HFS+ Private Data" folder name (4 leading NUL units).
139bool is_private_data_folder_name(const unsigned char* utf16be_name,
140 uint16_t name_units);
141
142// Returns the iNode number embedded in a name like "iNode42"
143// (UTF-16-BE code units). Returns 0 if the name doesn't match the
144// pattern.
145uint32_t parse_inode_filename(const unsigned char* utf16be_name,
146 uint16_t name_units);
147
148// Parse the body of a folder record. `body` points to the byte AFTER
149// the 2-byte recordType discriminator. `body_len` must be >= 88.
150bool parse_catalog_folder(const unsigned char* body, uint32_t body_len,
151 CatalogFolder& out);
152
153// Parse the body of a file record. `body` points to the byte AFTER
154// the 2-byte recordType discriminator. `body_len` must be >= 248.
155bool parse_catalog_file(const unsigned char* body, uint32_t body_len,
156 CatalogFile& out);
157
158// =============================================================================
159// Record-by-parent index
160// =============================================================================
161//
162// To walk the directory tree from rootCNID, we need to answer "what are
163// the children of folder X?". HFS+ catalog records are KEYED by
164// (parentCNID, name), so when we walk the leaf records we can build a
165// multimap parent -> [child records].
166//
167// We don't store the entire record body in this index (memory waste);
168// instead we store the minimal info needed to recurse:
169// - the child's NFC-converted name
170// - the recordType (folder vs file)
171// - the child's CNID
172// - for files: the dataFork (so we can read its content) and the
173// finderInfo (for symlink/hardlink detection)
174
176 std::string name_nfc;
177 int16_t recordType {0};
178
179 // Common to folders and files
180 uint32_t cnid {0};
181 uint16_t flags {0}; // HFSPlusCatalogFolder/File.flags
182
183 // File-specific: only valid if recordType == kHFSPlusFileRecord
187 uint16_t fileMode {0};
188 uint8_t ownerFlags {0}; // BSD ownerFlags (UF_COMPRESSED bit)
189 uint32_t special {0}; // BSDInfo.special (iNode num for hardlinks)
190};
191
192// Convert a UTF-16-BE name (from a CatalogKey) to NFC UTF-8, then
193// sanitize for the host filesystem (replace illegal chars with '_').
194std::string catalog_name_to_host(const unsigned char* utf16be_name,
195 uint16_t name_units);
196
197} // namespace hfs
constexpr uint32_t kSymLinkCreator
constexpr uint32_t kSymLinkFileType
bool parse_catalog_folder(const unsigned char *body, uint32_t body_len, CatalogFolder &out)
constexpr uint16_t kHFSHasLinkChainBit
constexpr uint32_t kHFSPlusCreator
std::string catalog_name_to_host(const unsigned char *utf16be_name, uint16_t name_units)
constexpr uint32_t kHardLinkFileType
constexpr int kHFSPrivateDataNameLength
constexpr uint16_t kHFSThreadExistsBit
constexpr uint16_t kHFSHasDateAddedBit
bool parse_catalog_file(const unsigned char *body, uint32_t body_len, CatalogFile &out)
constexpr uint16_t kHFSHasAttributesBit
constexpr uint8_t UF_COMPRESSED
constexpr uint16_t kHFSFileLockedBit
constexpr uint16_t kHFSHasChildLinkBit
uint32_t parse_inode_filename(const unsigned char *utf16be_name, uint16_t name_units)
constexpr uint16_t kHFSHasSecurityBit
constexpr uint16_t kHFSHasFolderCountBit
bool is_private_data_folder_name(const unsigned char *utf16be_name, uint16_t name_units)
uint8_t ownerFlags
uint32_t groupID
uint16_t fileMode
uint32_t special
uint32_t ownerID
uint8_t adminFlags
uint32_t attributeModDate
bool is_hardlink() const
FinderInfo16 userInfo
uint32_t contentModDate
uint32_t textEncoding
ForkData resourceFork
bool is_symlink() const
FinderInfo16 userInfo
uint32_t attributeModDate
FinderInfo16 userInfo
ForkData resourceFork
std::string name_nfc