Skip to content
API Documentation
BastionGuard™
hfs_format.hpp
Go to the documentation of this file.
1// hfs_format.hpp -- HFS+ / HFSX on-disk format definitions.
2//
3// References:
4// - Apple Technote TN1150: "HFS Plus Volume Format"
5// https://developer.apple.com/library/archive/technotes/tn/tn1150.html
6// - Linux kernel sources: fs/hfsplus/hfsplus_raw.h
7//
8// HFS+ is BIG-ENDIAN on disk (Mac/PowerPC heritage). All multi-byte
9// integers in the structures below are stored big-endian. We provide
10// be16/be32/be64 helpers in this header (mirror of xfs_format helpers,
11// kept separate so the two filesystem implementations can evolve
12// independently).
13//
14// VARIANTS:
15// * HFS+ classic: signature "H+" (0x482B), version 4
16// case-insensitive comparison, NFD-normalised UTF-16
17// * HFSX: signature "HX" (0x4858), version 5
18// case-sensitive (or insensitive based on kHFSXKeyCompare),
19// same on-disk layout otherwise
20// * Legacy HFS (no plus) is NOT supported here. It was effectively
21// replaced by HFS+ in 1998 and uses a completely different layout.
22//
23// VOLUME HEADER LOCATION:
24// The volume header lives at OFFSET 1024 from the volume start
25// (sector 2 with 512-byte sectors). A backup copy lives at
26// `volume_size - 1024` from the start (end-1024).
27//
28// THIS HEADER CONTAINS DEFINITIONS ONLY -- no implementation. The
29// in-memory layout matches the on-disk layout where it makes sense
30// (numeric fields), but we don't use packed structs for the whole
31// thing because alignment of nested arrays is fragile across compilers.
32// Instead, parse_volume_header() reads fields one by one with explicit
33// big-endian decoders.
34
35#pragma once
36
37#include <array>
38#include <cstdint>
39#include <cstddef>
40#include <string>
41#include <vector>
42
43namespace hfs {
44
45// -----------------------------------------------------------------------------
46// Big-endian readers (HFS+ is big-endian on disk)
47// -----------------------------------------------------------------------------
48
49inline uint16_t be16(const unsigned char* p) {
50 return static_cast<uint16_t>((p[0] << 8) | p[1]);
51}
52inline uint32_t be32(const unsigned char* p) {
53 return (static_cast<uint32_t>(p[0]) << 24) |
54 (static_cast<uint32_t>(p[1]) << 16) |
55 (static_cast<uint32_t>(p[2]) << 8) |
56 static_cast<uint32_t>(p[3]);
57}
58inline uint64_t be64(const unsigned char* p) {
59 return (static_cast<uint64_t>(p[0]) << 56) |
60 (static_cast<uint64_t>(p[1]) << 48) |
61 (static_cast<uint64_t>(p[2]) << 40) |
62 (static_cast<uint64_t>(p[3]) << 32) |
63 (static_cast<uint64_t>(p[4]) << 24) |
64 (static_cast<uint64_t>(p[5]) << 16) |
65 (static_cast<uint64_t>(p[6]) << 8) |
66 static_cast<uint64_t>(p[7]);
67}
68
69// -----------------------------------------------------------------------------
70// Magic & versioning
71// -----------------------------------------------------------------------------
72
73constexpr uint16_t HFSPLUS_SIGNATURE = 0x482B; // "H+"
74constexpr uint16_t HFSX_SIGNATURE = 0x4858; // "HX"
75constexpr uint16_t HFSPLUS_VERSION = 4;
76constexpr uint16_t HFSX_VERSION = 5;
77
78// Volume header lives at this byte offset within the volume.
79constexpr uint32_t HFSPLUS_VOLUME_HEADER_OFFSET = 1024;
80
81// Volume attributes (selected -- see TN1150 for the full list)
82constexpr uint32_t kHFSVolumeUnmountedBit = (1u << 8);
83constexpr uint32_t kHFSVolumeJournaledBit = (1u << 13);
84constexpr uint32_t kHFSVolumeSoftwareLockBit = (1u << 15);
85
86// Reserved file/folder catalog node IDs (CNIDs)
87constexpr uint32_t kHFSRootParentID = 1;
88constexpr uint32_t kHFSRootFolderID = 2;
89constexpr uint32_t kHFSExtentsFileID = 3;
90constexpr uint32_t kHFSCatalogFileID = 4;
91constexpr uint32_t kHFSBadBlockFileID = 5;
92constexpr uint32_t kHFSAllocationFileID = 6;
93constexpr uint32_t kHFSStartupFileID = 7;
94constexpr uint32_t kHFSAttributesFileID = 8;
95constexpr uint32_t kHFSRepairCatalogFileID = 14;
96constexpr uint32_t kHFSBogusExtentFileID = 15;
97constexpr uint32_t kHFSFirstUserCatalogNodeID = 16;
98
99// -----------------------------------------------------------------------------
100// Forks
101// -----------------------------------------------------------------------------
102//
103// A "fork" is a logical byte-stream of a file. HFS+ has data forks and
104// resource forks; the special files use only the data fork. The on-disk
105// representation contains:
106// __be64 logicalSize
107// __be32 clumpSize
108// __be32 totalBlocks
109// HFSPlusExtentRecord extents -- 8 entries of (startBlock, blockCount)
110//
111// Total size: 80 bytes. If the file has more than 8 extents, the
112// remaining ones live in the Extents Overflow File (special file #3).
113
115 uint32_t start_block {0}; // allocation-block-relative
116 uint32_t block_count {0};
117};
118
119struct ForkData {
120 uint64_t logical_size {0};
121 uint32_t clump_size {0};
122 uint32_t total_blocks {0};
123 std::array<ForkExtent, 8> extents {}; // inline extent record
124};
125
126// Parse 80 bytes into a ForkData.
127bool parse_fork_data(const unsigned char* buf, ForkData& out);
128
129// -----------------------------------------------------------------------------
130// Volume header
131// -----------------------------------------------------------------------------
132
134 uint16_t signature {0}; // "H+" or "HX"
135 uint16_t version {0}; // 4 (HFS+) / 5 (HFSX)
136 uint32_t attributes {0};
138 uint32_t journal_info_block {0};
139 uint32_t create_date {0}; // local time, Mac epoch (1904-01-01)
140 uint32_t modify_date {0};
141 uint32_t backup_date {0};
142 uint32_t checked_date {0};
143 uint32_t file_count {0};
144 uint32_t folder_count {0};
145 uint32_t block_size {0}; // typically 4096 on modern volumes
146 uint32_t total_blocks {0};
147 uint32_t free_blocks {0};
148 uint32_t next_allocation {0};
149 uint32_t rsrc_clump_size {0};
150 uint32_t data_clump_size {0};
151 uint32_t next_catalog_id {0};
152 uint32_t write_count {0};
153 uint64_t encodings_bitmap{0};
154 std::array<uint8_t, 32> finder_info {};
155
161
162 // ---- helpers ----
163 bool is_hfsplus() const { return signature == HFSPLUS_SIGNATURE; }
164 bool is_hfsx() const { return signature == HFSX_SIGNATURE; }
165 bool is_journaled() const {
166 return (attributes & kHFSVolumeJournaledBit) != 0;
167 }
168 uint64_t volume_size_bytes() const {
169 return static_cast<uint64_t>(total_blocks) * block_size;
170 }
171 const char* variant_name() const {
172 return is_hfsx() ? "HFSX" : (is_hfsplus() ? "HFS+" : "Unknown");
173 }
174};
175
176// Parse a 512-byte sector containing the HFS+ / HFSX volume header.
177// Returns false if signature is wrong or sanity checks fail.
178//
179// `buf` must point to at least 512 bytes (typically a sector at byte
180// offset HFSPLUS_VOLUME_HEADER_OFFSET from the volume start).
181bool parse_volume_header(const unsigned char* buf, VolumeHeader& out);
182
183// Convert an allocation-block number to an absolute disk byte offset.
184// disk_offset = volume_offset + (alloc_block * block_size)
185inline uint64_t alloc_block_to_disk_offset(uint64_t volume_offset,
186 uint32_t alloc_block,
187 uint32_t block_size) {
188 return volume_offset + static_cast<uint64_t>(alloc_block) * block_size;
189}
190
191// -----------------------------------------------------------------------------
192// Volume context (parallels xfs::VolumeContext)
193// -----------------------------------------------------------------------------
194
197 uint64_t volume_offset {0}; // disk byte offset where the volume starts
198 bool is_v5_hfsx {false}; // cached from header.signature
199};
200
201} // namespace hfs
bool parse_volume_header(const unsigned char *buf, VolumeHeader &out)
constexpr uint32_t kHFSBadBlockFileID
constexpr uint32_t kHFSRootFolderID
constexpr uint16_t HFSX_SIGNATURE
constexpr uint32_t kHFSRepairCatalogFileID
uint64_t alloc_block_to_disk_offset(uint64_t volume_offset, uint32_t alloc_block, uint32_t block_size)
uint64_t be64(const unsigned char *p)
constexpr uint32_t HFSPLUS_VOLUME_HEADER_OFFSET
bool parse_fork_data(const unsigned char *buf, ForkData &out)
constexpr uint32_t kHFSVolumeJournaledBit
constexpr uint16_t HFSPLUS_SIGNATURE
constexpr uint32_t kHFSBogusExtentFileID
uint32_t be32(const unsigned char *p)
constexpr uint16_t HFSX_VERSION
constexpr uint32_t kHFSVolumeUnmountedBit
constexpr uint32_t kHFSVolumeSoftwareLockBit
constexpr uint32_t kHFSCatalogFileID
constexpr uint32_t kHFSExtentsFileID
constexpr uint32_t kHFSRootParentID
constexpr uint16_t HFSPLUS_VERSION
constexpr uint32_t kHFSAllocationFileID
constexpr uint32_t kHFSStartupFileID
constexpr uint32_t kHFSAttributesFileID
uint16_t be16(const unsigned char *p)
constexpr uint32_t kHFSFirstUserCatalogNodeID
uint64_t logical_size
std::array< ForkExtent, 8 > extents
uint32_t total_blocks
uint32_t clump_size
uint32_t block_count
uint32_t start_block
VolumeHeader header
bool is_hfsx() const
uint32_t next_allocation
ForkData attributes_file
uint64_t encodings_bitmap
uint64_t volume_size_bytes() const
uint32_t data_clump_size
uint32_t next_catalog_id
uint32_t last_mounted_version
uint32_t rsrc_clump_size
uint32_t journal_info_block
ForkData allocation_file
bool is_journaled() const
const char * variant_name() const
bool is_hfsplus() const
std::array< uint8_t, 32 > finder_info