Skip to content
API Documentation
BastionGuard™
hfs_fork_io.cpp
Go to the documentation of this file.
1// hfs_fork_io.cpp -- implementation of fork reading via inline extents.
2
3#include "hfs_fork_io.hpp"
4#include "hfs_btree.hpp"
5
6#include <algorithm>
7#include <cstring>
8#include <sstream>
9
10namespace hfs {
11
13 uint64_t volume_offset,
14 uint32_t block_size,
15 const ForkData& fork,
16 uint64_t max_bytes,
17 std::vector<unsigned char>& out,
18 std::vector<std::string>& logs) {
19 if (block_size == 0) return false;
20
21 // Total bytes we want to read: min of fork.logical_size, max_bytes,
22 // and inline-coverage (sum of all extent lengths in bytes).
23 uint64_t inline_bytes = 0;
24 for (const auto& e : fork.extents) {
25 inline_bytes += static_cast<uint64_t>(e.block_count) * block_size;
26 }
27
28 uint64_t want = std::min<uint64_t>(fork.logical_size, max_bytes);
29 if (want > inline_bytes) {
30 std::ostringstream m;
31 m << "HFS+ fork: requested " << want
32 << " bytes but only " << inline_bytes
33 << " bytes covered by inline extents (file > 8 extents would "
34 "need Extents Overflow file -- session 4)";
35 logs.push_back(m.str());
36 want = inline_bytes; // soft truncate
37 }
38
39 out.clear();
40 out.reserve(static_cast<size_t>(want));
41
42 uint64_t produced = 0;
43 for (const auto& e : fork.extents) {
44 if (produced >= want) break;
45 if (e.block_count == 0) continue;
46
47 const uint64_t ext_disk_off =
48 volume_offset
49 + static_cast<uint64_t>(e.start_block) * block_size;
50 const uint64_t ext_bytes =
51 static_cast<uint64_t>(e.block_count) * block_size;
52 const uint64_t copy = std::min<uint64_t>(ext_bytes, want - produced);
53
54 const size_t before = out.size();
55 out.resize(before + static_cast<size_t>(copy));
56 if (!reader.read_at(ext_disk_off, out.data() + before, copy)) {
57 std::ostringstream m;
58 m << "HFS+ fork: read failed at disk offset " << ext_disk_off
59 << " for " << copy << " bytes";
60 logs.push_back(m.str());
61 out.resize(before);
62 return false;
63 }
64 produced += copy;
65 }
66 return true;
67}
68
70 uint64_t volume_offset,
71 uint32_t block_size,
72 const ForkData& fork,
73 uint64_t fork_logical_offset,
74 unsigned char* block_buf) {
75 if (block_size == 0 || block_buf == nullptr) return false;
76
77 // The fork is a concatenation of its extents. We walk extents
78 // accumulating their byte coverage; when we reach the extent that
79 // contains fork_logical_offset, we compute the absolute disk offset
80 // and issue a single read.
81 uint64_t covered = 0;
82 for (const auto& e : fork.extents) {
83 if (e.block_count == 0) continue;
84 const uint64_t ext_bytes =
85 static_cast<uint64_t>(e.block_count) * block_size;
86 if (fork_logical_offset < covered + ext_bytes) {
87 // Hit. The byte we want is at:
88 const uint64_t byte_within_ext = fork_logical_offset - covered;
89 // Round to a block boundary (fork_logical_offset should be
90 // block-aligned, but be defensive).
91 const uint64_t blk_within_ext = byte_within_ext / block_size;
92 const uint64_t disk_off =
93 volume_offset
94 + (static_cast<uint64_t>(e.start_block) + blk_within_ext)
95 * block_size;
96 return reader.read_at(disk_off, block_buf, block_size);
97 }
98 covered += ext_bytes;
99 }
100 // Offset beyond inline coverage -- needs Extents Overflow file.
101 return false;
102}
103
104// =============================================================================
105// read_fork_full (session 4: combines inline + overflow extents)
106// =============================================================================
107
108namespace {
109
110// Read all blocks covered by a single ForkExtent and append to `out`.
111// Returns false on read failure; the caller logs/decides.
112bool read_one_extent(DiskReader& reader, uint64_t volume_offset,
113 uint32_t block_size, const ForkExtent& e,
114 uint64_t want_bytes,
115 std::vector<unsigned char>& out) {
116 if (e.block_count == 0 || want_bytes == 0) return true;
117 const uint64_t off = volume_offset
118 + static_cast<uint64_t>(e.start_block) * block_size;
119 const uint64_t ext_bytes =
120 static_cast<uint64_t>(e.block_count) * block_size;
121 const uint64_t copy = std::min<uint64_t>(ext_bytes, want_bytes);
122 const size_t before = out.size();
123 out.resize(before + static_cast<size_t>(copy));
124 if (!reader.read_at(off, out.data() + before, copy)) {
125 out.resize(before);
126 return false;
127 }
128 return true;
129}
130
131// Helper: peek into the Extents Overflow B-tree to find all leaf records
132// matching (forkType, fileID), accumulating their extents into `out`.
133// Returns the total count of extents found (0 if none).
134//
135// This is a SCAN of the entire B-tree -- not optimal -- but it's
136// straightforward and works for any volume. For really huge volumes a
137// keyed descent would be better; we stay simple here.
138size_t collect_overflow_extents(const std::vector<unsigned char>& btree_buf,
139 const HeaderRec& hdr,
140 uint8_t forkType,
141 uint32_t fileID,
142 std::vector<ForkExtent>& out,
143 std::vector<std::string>& logs) {
144 auto node_reader = [&](uint32_t node_index,
145 std::vector<unsigned char>& nodebuf) -> bool {
146 const uint64_t off = static_cast<uint64_t>(node_index) * hdr.nodeSize;
147 if (off + hdr.nodeSize > btree_buf.size()) return false;
148 nodebuf.assign(btree_buf.begin() + off,
149 btree_buf.begin() + off + hdr.nodeSize);
150 return true;
151 };
152
153 size_t found = 0;
154 auto on_leaf = [&](const unsigned char* rec, uint16_t rec_len) -> bool {
155 // Extents Overflow KEY layout:
156 // __be16 keyLength
157 // __u8 forkType
158 // __u8 pad
159 // __be32 fileID
160 // __be32 startBlock
161 if (rec_len < 12) return true;
162 const uint16_t keyLength = be16(rec + 0);
163 if (keyLength < 10 || 2u + keyLength > rec_len) return true;
164 const uint8_t rec_forkType = rec[2];
165 // pad at +3
166 const uint32_t rec_fileID = be32(rec + 4);
167 // const uint32_t rec_startBlock = be32(rec + 8); // not used here
168 if (rec_forkType != forkType || rec_fileID != fileID) {
169 return true; // not for us, skip
170 }
171 // Body: 64 bytes of HFSPlusExtentRecord = 8 extents.
172 // Body starts after the 2+keyLength key, aligned to 2 bytes.
173 const uint32_t key_total = 2u + keyLength;
174 const uint32_t body_off = (key_total + 1u) & ~1u;
175 if (body_off + 64 > rec_len) return true;
176 const unsigned char* body = rec + body_off;
177 for (int i = 0; i < 8; ++i) {
178 ForkExtent e;
179 e.start_block = be32(body + i * 8 + 0);
180 e.block_count = be32(body + i * 8 + 4);
181 if (e.block_count == 0) break; // first zero terminates the run
182 out.push_back(e);
183 ++found;
184 }
185 return true;
186 };
187
188 std::vector<std::string> walk_logs;
189 std::string walk_err;
190 walk_btree(hdr, node_reader, on_leaf, walk_logs, walk_err);
191 for (auto& l : walk_logs) logs.push_back(std::move(l));
192 if (!walk_err.empty()) {
193 logs.push_back("HFS+ extents-overflow walk: " + walk_err);
194 }
195 return found;
196}
197
198} // anonymous namespace
199
201 uint64_t volume_offset,
202 uint32_t block_size,
203 const ForkData& fork,
204 const ForkData& extents_file,
205 uint8_t forkType,
206 uint32_t fileID,
207 uint64_t max_bytes,
208 std::vector<unsigned char>& extents_btree_buffer,
209 std::vector<unsigned char>& out,
210 std::vector<std::string>& logs) {
211 if (block_size == 0) return false;
212
213 // Compute inline coverage.
214 uint64_t inline_blocks = 0;
215 int inline_extent_count = 0;
216 for (const auto& e : fork.extents) {
217 if (e.block_count == 0) break; // run terminator
218 inline_blocks += e.block_count;
219 ++inline_extent_count;
220 }
221 const uint64_t inline_bytes = inline_blocks * block_size;
222
223 const uint64_t want = std::min<uint64_t>(fork.logical_size, max_bytes);
224
225 // Fast path: fork fits in inline extents.
226 if (fork.total_blocks <= inline_blocks || inline_extent_count < 8) {
227 return read_fork_inline(reader, volume_offset, block_size,
228 fork, want, out, logs);
229 }
230
231 // Slow path: file has > 8 extents (overflow needed).
232 // Step 1: load the Extents Overflow B-tree if not already cached.
233 if (extents_btree_buffer.empty()) {
234 std::vector<std::string> ld_logs;
235 if (!read_fork_inline(reader, volume_offset, block_size,
236 extents_file, extents_file.logical_size,
237 extents_btree_buffer, ld_logs)) {
238 logs.push_back("HFS+: cannot load Extents Overflow file content "
239 "(file with >8 extents will be soft-truncated)");
240 for (auto& l : ld_logs) logs.push_back(std::move(l));
241 extents_btree_buffer.clear(); // mark as "tried but failed"
242 return read_fork_inline(reader, volume_offset, block_size,
243 fork, want, out, logs);
244 }
245 }
246 if (extents_btree_buffer.size() < 14) {
247 logs.push_back("HFS+: Extents Overflow file too small for B-tree");
248 return read_fork_inline(reader, volume_offset, block_size,
249 fork, want, out, logs);
250 }
251
252 // Step 2: parse overflow B-tree header (node 0).
254 if (!parse_node_descriptor(extents_btree_buffer.data(), n0) ||
255 n0.kind != kBTHeaderNode) {
256 logs.push_back("HFS+: Extents Overflow node 0 not a header node");
257 return read_fork_inline(reader, volume_offset, block_size,
258 fork, want, out, logs);
259 }
260 HeaderRec ohdr;
261 if (!parse_header_rec(extents_btree_buffer.data() + 14, ohdr)) {
262 logs.push_back("HFS+: Extents Overflow B-tree header parse failed");
263 return read_fork_inline(reader, volume_offset, block_size,
264 fork, want, out, logs);
265 }
266
267 // Step 3: collect extra extents from overflow B-tree.
268 std::vector<ForkExtent> overflow_extents;
269 collect_overflow_extents(extents_btree_buffer, ohdr,
270 forkType, fileID, overflow_extents, logs);
271
272 // Step 4: build a unified extent list (8 inline followed by overflow).
273 std::vector<ForkExtent> all_extents;
274 all_extents.reserve(8 + overflow_extents.size());
275 for (const auto& e : fork.extents) {
276 if (e.block_count == 0) break;
277 all_extents.push_back(e);
278 }
279 for (const auto& e : overflow_extents) all_extents.push_back(e);
280
281 // Step 5: read all blocks in order.
282 out.clear();
283 out.reserve(static_cast<size_t>(want));
284 uint64_t produced = 0;
285 for (const auto& e : all_extents) {
286 if (produced >= want) break;
287 const uint64_t ext_bytes =
288 static_cast<uint64_t>(e.block_count) * block_size;
289 const uint64_t copy = std::min<uint64_t>(ext_bytes, want - produced);
290 if (!read_one_extent(reader, volume_offset, block_size, e, copy, out)) {
291 std::ostringstream m;
292 m << "HFS+: read failure on overflow extent (start_block="
293 << e.start_block << ", count=" << e.block_count << ")";
294 logs.push_back(m.str());
295 return false;
296 }
297 produced += copy;
298 }
299
300 // If we have less than `want`, log it but don't fail (best-effort).
301 if (produced < want) {
302 std::ostringstream m;
303 m << "HFS+: requested " << want << " bytes from fork (fileID="
304 << fileID << "), got " << produced
305 << " (file may be truncated or overflow records missing)";
306 logs.push_back(m.str());
307 }
308
309 (void)inline_bytes;
310 return true;
311}
312
313} // namespace hfs
bool read_at(uint64_t offset, void *buffer, size_t len)
size_t collect_overflow_extents(const std::vector< unsigned char > &btree_buf, const HeaderRec &hdr, uint8_t forkType, uint32_t fileID, std::vector< ForkExtent > &out, std::vector< std::string > &logs)
bool read_one_extent(DiskReader &reader, uint64_t volume_offset, uint32_t block_size, const ForkExtent &e, uint64_t want_bytes, std::vector< unsigned char > &out)
bool read_fork_full(DiskReader &reader, uint64_t volume_offset, uint32_t block_size, const ForkData &fork, const ForkData &extents_file, uint8_t forkType, uint32_t fileID, uint64_t max_bytes, std::vector< unsigned char > &extents_btree_buffer, std::vector< unsigned char > &out, std::vector< std::string > &logs)
bool read_fork_block_inline(DiskReader &reader, uint64_t volume_offset, uint32_t block_size, const ForkData &fork, uint64_t fork_logical_offset, unsigned char *block_buf)
bool parse_node_descriptor(const unsigned char *buf, NodeDescriptor &out)
Definition hfs_btree.cpp:17
uint32_t be32(const unsigned char *p)
bool walk_btree(const HeaderRec &header, const NodeReader &read_node, const LeafRecordCallback &on_leaf, std::vector< std::string > &logs, std::string &error)
constexpr int8_t kBTHeaderNode
Definition hfs_btree.hpp:34
bool parse_header_rec(const unsigned char *buf, HeaderRec &out)
Definition hfs_btree.cpp:38
uint16_t be16(const unsigned char *p)
bool read_fork_inline(DiskReader &reader, uint64_t volume_offset, uint32_t block_size, const ForkData &fork, uint64_t max_bytes, std::vector< unsigned char > &out, std::vector< std::string > &logs)
uint64_t logical_size
std::array< ForkExtent, 8 > extents
uint32_t total_blocks
uint32_t block_count
uint32_t start_block
uint16_t nodeSize
Definition hfs_btree.hpp:69