Skip to content
API Documentation
BastionGuard™
HfsRecovery Class Reference

#include <hfs_recovery.hpp>

Public Member Functions

 HfsRecovery (DiskReader &reader)
bool recover (uint64_t volume_offset, const std::string &output_dir, std::vector< RecoveredFileRecord > &recovered, std::vector< std::string > &logs, std::string &error)

Private Attributes

DiskReader & reader_

Detailed Description

Definition at line 28 of file hfs_recovery.hpp.

Constructor & Destructor Documentation

◆ HfsRecovery()

HfsRecovery::HfsRecovery ( DiskReader & reader)
explicit

Definition at line 58 of file hfs_recovery.cpp.

58: reader_(reader) {}
DiskReader & reader_

References reader_.

Member Function Documentation

◆ recover()

bool HfsRecovery::recover ( uint64_t volume_offset,
const std::string & output_dir,
std::vector< RecoveredFileRecord > & recovered,
std::vector< std::string > & logs,
std::string & error )

Definition at line 60 of file hfs_recovery.cpp.

64 {
65 logs.push_back(trs("HFS+ recovery (sessions 4+5: full extents + "
66 "hardlinks + decmpfs-zlib) starting at offset") + std::string(" ")
67 + std::to_string(volume_offset));
68
69 // ---- Step 1: read 512 bytes at +1024 ----
70 std::array<unsigned char, 512> sector{};
71 const uint64_t header_off = volume_offset + hfs::HFSPLUS_VOLUME_HEADER_OFFSET;
72 if (!read_exact(reader_, header_off, sector.data(), sector.size())) {
73 error = trs("HFS+: cannot read volume header sector");
74 return false;
75 }
76
77 hfs::VolumeHeader vh;
78 if (!hfs::parse_volume_header(sector.data(), vh)) {
79 error = trs("HFS+: not a valid volume header at this offset");
80 return false;
81 }
82
83 {
84 std::string ge;
85 if (!validate_geometry(reader_, volume_offset, vh, ge)) {
86 error = trs("HFS+: geometry inconsistent:") + std::string(" ") + ge;
87 return false;
88 }
89 }
90
91 // ---- Step 2: emit volume layout to logs ----
92 {
93 std::ostringstream m;
94 m << "HFS+ volume identified: variant=" << vh.variant_name()
95 << " version=" << vh.version
96 << " block_size=" << vh.block_size
97 << " total_blocks=" << vh.total_blocks
98 << " free_blocks=" << vh.free_blocks
99 << " size=" << (vh.volume_size_bytes() / (1024 * 1024)) << " MiB"
100 << " files=" << vh.file_count
101 << " folders=" << vh.folder_count
102 << " journaled=" << (vh.is_journaled() ? "yes" : "no");
103 logs.push_back(m.str());
104 }
105
106 // ---- Step 3: special files diagnostic ----
107 auto log_fork = [&](const char* name, const hfs::ForkData& f) {
108 std::ostringstream m;
109 m << " " << name << ": logical=" << f.logical_size
110 << " B, total_blocks=" << f.total_blocks;
111 if (f.total_blocks > 0) {
112 m << ", inline_extents=[";
113 bool first = true;
114 for (size_t i = 0; i < 8; ++i) {
115 const auto& e = f.extents[i];
116 if (e.block_count == 0) continue;
117 if (!first) m << ", ";
118 m << "(blk=" << e.start_block
119 << ", count=" << e.block_count << ")";
120 first = false;
121 }
122 m << "]";
123 }
124 logs.push_back(m.str());
125 };
126 logs.push_back("HFS+ special files:");
127 log_fork("allocation", vh.allocation_file);
128 log_fork("extents", vh.extents_file);
129 log_fork("catalog", vh.catalog_file);
130 log_fork("attributes", vh.attributes_file);
131 log_fork("startup", vh.startup_file);
132
133 // ---- Step 4: read the catalog file into memory and walk its B-tree ----
134 //
135 // Session 2 strategy:
136 // 1. Read the catalog file via inline extents (read_fork_inline)
137 // 2. Parse node 0 (header node) -> get nodeSize, rootNode, depth
138 // 3. Walk the B-tree, classifying every leaf record by its
139 // type-discriminator (first 2 bytes of the body): folder / file /
140 // folder-thread / file-thread
141 // 4. Report counts per type in the log
142 //
143 // We do NOT yet decode record bodies (that's session 3). We DO
144 // decode the catalog key (parent CNID + UTF-16BE name length) so we
145 // can sanity-check that names look reasonable on a real volume.
146
147 if (vh.catalog_file.total_blocks == 0 ||
148 vh.catalog_file.extents[0].block_count == 0) {
149 error = trs("HFS+: catalog file has no extents -- volume looks empty "
150 "or damaged");
151 return false;
152 }
153
154 std::vector<unsigned char> catalog_bytes;
155 if (!hfs::read_fork_inline(reader_, volume_offset, vh.block_size,
156 vh.catalog_file,
158 catalog_bytes, logs)) {
159 error = trs("HFS+: failed to read catalog file content");
160 return false;
161 }
162 {
163 std::ostringstream m;
164 m << "HFS+ catalog file: " << catalog_bytes.size() << " bytes "
165 << "loaded from " << vh.catalog_file.total_blocks
166 << " allocation blocks";
167 logs.push_back(m.str());
168 }
169
170 // Parse node 0 (header node) to get nodeSize & B-tree shape.
171 if (catalog_bytes.size() < 14) {
172 error = trs("HFS+: catalog file too small to contain a header node");
173 return false;
174 }
175 hfs::NodeDescriptor n0;
176 if (!hfs::parse_node_descriptor(catalog_bytes.data(), n0) ||
177 n0.kind != hfs::kBTHeaderNode) {
178 error = trs("HFS+: catalog node 0 is not a valid header node");
179 return false;
180 }
181 hfs::HeaderRec catalog_hdr;
182 if (!hfs::parse_header_rec(catalog_bytes.data() + 14, catalog_hdr)) {
183 error = trs("HFS+: catalog B-tree header record parse failed");
184 return false;
185 }
186 {
187 std::ostringstream m;
188 m << "HFS+ catalog B-tree: depth=" << catalog_hdr.treeDepth
189 << " root_node=" << catalog_hdr.rootNode
190 << " leaf_records=" << catalog_hdr.leafRecords
191 << " node_size=" << catalog_hdr.nodeSize
192 << " total_nodes=" << catalog_hdr.totalNodes
193 << " key_compare=0x" << std::hex
194 << static_cast<unsigned>(catalog_hdr.keyCompareType)
195 << std::dec;
196 logs.push_back(m.str());
197 }
198
199 // Validate that the catalog file is large enough to host the
200 // declared total_nodes.
201 const uint64_t expected_size =
202 static_cast<uint64_t>(catalog_hdr.totalNodes) * catalog_hdr.nodeSize;
203 if (expected_size > catalog_bytes.size()) {
204 std::ostringstream m;
205 m << "HFS+: catalog file size " << catalog_bytes.size()
206 << " is smaller than declared total_nodes*node_size = "
207 << expected_size << " -- some nodes lie in the Extents Overflow "
208 << "file (session 4 will handle that). Walking what we have now.";
209 logs.push_back(m.str());
210 }
211
212 // The B-tree walker uses an injectable NodeReader. For session 2 we
213 // serve nodes from the in-memory catalog buffer.
214 auto node_reader = [&](uint32_t node_index,
215 std::vector<unsigned char>& out) -> bool {
216 const uint64_t off =
217 static_cast<uint64_t>(node_index) * catalog_hdr.nodeSize;
218 if (off + catalog_hdr.nodeSize > catalog_bytes.size()) return false;
219 out.assign(catalog_bytes.begin() + off,
220 catalog_bytes.begin() + off + catalog_hdr.nodeSize);
221 return true;
222 };
223
224 // Build a map parentCNID -> [child entries]. We do not store the
225 // entire body in the map (memory waste); we keep only what's needed
226 // to recurse and export: NFC name, recordType, CNID, and (for files)
227 // dataFork + finderInfo + fileMode + flags + ownerFlags + special.
228 std::map<uint32_t, std::vector<hfs::ChildEntry>> children_by_parent;
229
230 // Hardlink resolution: while walking we identify
231 // 1. The "HFS+ Private Data" folder under the root (CNID kept here)
232 // 2. Each "iNode<N>" file under it -> map[N] = {dataFork, ...}
233 uint32_t private_data_cnid = 0;
234 std::map<uint32_t, hfs::ChildEntry> inode_to_target;
235
236 // Per-type counters (kept from session 2)
237 size_t folders = 0, files = 0, folder_threads = 0, file_threads = 0;
238 size_t records_total = 0, key_parse_failures = 0, unknown_record_types = 0;
239 size_t body_parse_failures = 0;
240
241 auto on_leaf = [&](const unsigned char* rec, uint16_t rec_len) -> bool {
242 ++records_total;
243 // Parse the catalog key first
244 hfs::CatalogKey key;
245 if (!hfs::parse_catalog_key(rec, rec_len, key)) {
246 ++key_parse_failures;
247 return true;
248 }
249 // Body starts at 2-byte boundary after the key
250 const uint32_t body_off = hfs::catalog_record_body_offset(rec, rec_len);
251 if (body_off == 0 || body_off + 2 > rec_len) return true;
252 const int16_t recordType = static_cast<int16_t>(hfs::be16(rec + body_off));
253 // Body proper starts AFTER the 2-byte recordType
254 const unsigned char* body = rec + body_off + 2;
255 const uint32_t body_len = rec_len - body_off - 2;
256
257 switch (recordType) {
259 ++folders;
260 hfs::CatalogFolder f;
261 if (!hfs::parse_catalog_folder(body, body_len, f)) {
262 ++body_parse_failures;
263 return true;
264 }
265 // Detect "HFS+ Private Data" folder (parent = rootCNID,
266 // raw name has 4 leading NUL units)
267 if (key.parentID == hfs::kHFSRootFolderID &&
269 key.nameLength)) {
270 private_data_cnid = f.folderID;
271 // Don't add it to children_by_parent: we don't want
272 // to walk it like a normal folder.
273 return true;
274 }
275 hfs::ChildEntry ch;
277 key.nameLength);
278 ch.recordType = recordType;
279 ch.cnid = f.folderID;
280 ch.flags = f.flags;
284 children_by_parent[key.parentID].push_back(std::move(ch));
285 break;
286 }
288 ++files;
289 hfs::CatalogFile f;
290 if (!hfs::parse_catalog_file(body, body_len, f)) {
291 ++body_parse_failures;
292 return true;
293 }
294 // Build the entry in any case
295 hfs::ChildEntry ch;
297 key.nameLength);
298 ch.recordType = recordType;
299 ch.cnid = f.fileID;
300 ch.flags = f.flags;
301 ch.dataFork = f.dataFork;
303 ch.userInfo = f.userInfo;
307
308 // If this file lives inside HFS+ Private Data and its
309 // name matches "iNode<N>", record it as a hardlink target.
310 if (private_data_cnid != 0 &&
311 key.parentID == private_data_cnid) {
312 const uint32_t inum =
314 key.nameLength);
315 if (inum > 0) {
316 inode_to_target[inum] = ch;
317 // Skip adding to children_by_parent: we don't
318 // emit iNode files in the recovery output.
319 return true;
320 }
321 }
322 children_by_parent[key.parentID].push_back(std::move(ch));
323 break;
324 }
325 case hfs::kHFSPlusFolderThreadRecord: ++folder_threads; break;
326 case hfs::kHFSPlusFileThreadRecord: ++file_threads; break;
327 default: ++unknown_record_types; break;
328 }
329 return true;
330 };
331
332 {
333 std::vector<std::string> walk_logs;
334 std::string walk_err;
335 if (!hfs::walk_btree(catalog_hdr, node_reader, on_leaf,
336 walk_logs, walk_err)) {
337 for (auto& l : walk_logs) logs.push_back(std::move(l));
338 logs.push_back("HFS+ catalog walk: " + walk_err);
339 } else {
340 for (auto& l : walk_logs) logs.push_back(std::move(l));
341 }
342 }
343
344 {
345 std::ostringstream m;
346 m << "HFS+ catalog walk completed: "
347 << records_total << " leaf records visited "
348 << "(folders=" << folders
349 << ", files=" << files
350 << ", folder_threads=" << folder_threads
351 << ", file_threads=" << file_threads
352 << ", unknown_types=" << unknown_record_types
353 << ", key_parse_failures=" << key_parse_failures
354 << ", body_parse_failures=" << body_parse_failures << ").";
355 logs.push_back(m.str());
356 if (private_data_cnid != 0) {
357 std::ostringstream m2;
358 m2 << "HFS+ hardlinks: HFS+ Private Data folder found (CNID="
359 << private_data_cnid << "), " << inode_to_target.size()
360 << " iNode targets indexed.";
361 logs.push_back(m2.str());
362 }
363 if (records_total != catalog_hdr.leafRecords) {
364 std::ostringstream m2;
365 m2 << "HFS+ note: visited " << records_total
366 << " records but header reports " << catalog_hdr.leafRecords
367 << " leaf records (difference may be in the Extents "
368 "Overflow file -- session 4)";
369 logs.push_back(m2.str());
370 }
371 }
372
373 // ---- Step 5: walk the directory tree from rootCNID and export ----
374 //
375 // Starting at kHFSPlusRootFolderID (= 2), recursively visit every
376 // child entry. For folders, mkdir + recurse. For files, read the
377 // dataFork via inline extents and write to disk. Symlinks (HFS+
378 // marks them by Finder type/creator 'slnk'/'rhap' AND fileMode
379 // S_IFLNK) write the link target as a real symlink (sidecar fallback
380 // if the host filesystem rejects the link).
381 //
382 // What we DON'T handle in session 3:
383 // * Files whose data fork uses > 8 extents (overflow file --
384 // session 4). These are skipped with a log warning.
385 // * decmpfs-compressed files. These show up with size=0 in
386 // dataFork; the real content is in the Attributes file.
387 // * Hardlinks (iNode files in HFS+ Private Data folder) -- session 5.
388
389 namespace fs = std::filesystem;
390 constexpr int MAX_DEPTH = 64;
391 constexpr size_t MAX_FILES = 200'000;
392
393 fs::path hfs_out_root =
394 fs::path(output_dir) / "filesystem_recovered" / "hfs" /
395 ("offset_" + std::to_string(volume_offset));
396 {
397 std::error_code ec;
398 fs::create_directories(hfs_out_root, ec);
399 if (ec) {
400 error = "Cannot create output directory: " + hfs_out_root.string();
401 return false;
402 }
403 }
404
405 std::set<uint32_t> visited_cnids;
406 size_t files_exported = 0;
407 size_t dirs_walked = 0;
408 size_t skipped_unsupported = 0;
409 size_t errors_logged = 0;
410 size_t hardlinks_resolved = 0;
411 size_t hardlinks_orphaned = 0;
412 size_t decmpfs_decompressed = 0;
413 size_t decmpfs_unsupported = 0;
414 size_t overflow_files = 0;
415
416 // Cache for the Extents Overflow B-tree content. Loaded lazily on
417 // first use (only paid for if at least one file actually has > 8
418 // extents and triggers read_fork_full's overflow path).
419 std::vector<unsigned char> extents_btree_buffer;
420
421 // Cache for the Attributes B-tree content (extended attributes).
422 // Loaded lazily on the first decmpfs file that needs the xattr
423 // path. Empty means "not yet loaded" or "tried and failed".
424 std::vector<unsigned char> attributes_btree_buffer;
425 bool attributes_load_attempted = false;
426
427 // POSIX file mode helpers
428 constexpr uint16_t S_IFLNK_BITS = 0xA000;
429
430 std::function<bool(uint32_t, const fs::path&, int)> walk_tree =
431 [&](uint32_t parent_cnid, const fs::path& out_dir, int depth) -> bool
432 {
433 if (depth >= MAX_DEPTH) {
434 logs.push_back("HFS+: max recursion depth reached, stopping branch.");
435 return true;
436 }
437 if (!visited_cnids.insert(parent_cnid).second) {
438 return true; // already visited (cycle defence)
439 }
440 if (files_exported >= MAX_FILES) {
441 logs.push_back("HFS+: file export safety cap reached ("
442 + std::to_string(MAX_FILES) + ").");
443 return false;
444 }
445
446 auto it = children_by_parent.find(parent_cnid);
447 if (it == children_by_parent.end()) return true;
448 ++dirs_walked;
449
450 for (const auto& ch : it->second) {
451 const fs::path child_path = out_dir / ch.name_nfc;
452
454 std::error_code ec;
455 fs::create_directories(child_path, ec);
456 if (ec) {
457 ++errors_logged;
458 logs.push_back("HFS+: cannot mkdir " + child_path.string());
459 continue;
460 }
461 if (!walk_tree(ch.cnid, child_path, depth + 1)) return false;
462 }
463 else if (ch.recordType == hfs::kHFSPlusFileRecord) {
464 // Symlink? Check Finder type/creator AND POSIX mode.
465 const bool finder_says_symlink =
468 const bool mode_says_symlink =
469 (ch.fileMode & 0xF000) == S_IFLNK_BITS;
470
471 if (finder_says_symlink || mode_says_symlink) {
472 // Read data fork content -- it IS the link target text
473 std::vector<unsigned char> link_data;
474 std::vector<std::string> rd_logs;
475 if (hfs::read_fork_inline(reader_, volume_offset,
476 vh.block_size, ch.dataFork,
478 link_data, rd_logs)) {
479 std::string target(reinterpret_cast<const char*>(link_data.data()),
480 link_data.size());
481 std::error_code ec;
482 fs::create_symlink(target, child_path, ec);
483 if (ec) {
484 // Fallback: sidecar file with the target text
485 std::ofstream out(child_path.string() + ".symlink",
486 std::ios::binary | std::ios::trunc);
487 if (out) out << target;
488 }
489 recovered.push_back({
490 child_path.string(), "HFS+",
491 (out_dir / ch.name_nfc).lexically_relative(hfs_out_root).string(),
492 link_data.size(), "full", "symlink"});
493 ++files_exported;
494 } else {
495 ++errors_logged;
496 for (auto& l : rd_logs) logs.push_back(std::move(l));
497 logs.push_back("HFS+: failed to read symlink target for '"
498 + ch.name_nfc + "'");
499 }
500 continue;
501 }
502
503 // ---- Hardlink resolution (session 5) ----
504 //
505 // A hardlink in HFS+ is a file with userInfo type='hlnk'
506 // and creator='hfs+'. The link's BSDInfo.special holds
507 // the iNode number. We look up the target ChildEntry in
508 // inode_to_target and use ITS dataFork for the export.
509 hfs::ChildEntry effective = ch;
510 bool is_hardlink = (ch.userInfo.fileType == hfs::kHardLinkFileType &&
512 if (is_hardlink) {
513 auto it_t = inode_to_target.find(ch.special);
514 if (it_t != inode_to_target.end()) {
515 // Replace data/resource forks with the target's,
516 // but KEEP the link's name and mode/flags.
517 effective.dataFork = it_t->second.dataFork;
518 effective.resourceFork = it_t->second.resourceFork;
519 effective.userInfo = it_t->second.userInfo;
520 effective.flags = it_t->second.flags;
521 effective.ownerFlags = it_t->second.ownerFlags;
522 effective.cnid = it_t->second.cnid;
523 ++hardlinks_resolved;
524 } else {
525 ++hardlinks_orphaned;
526 ++errors_logged;
527 logs.push_back("HFS+: hardlink '" + ch.name_nfc
528 + "' references missing iNode "
529 + std::to_string(ch.special));
530 continue;
531 }
532 }
533
534 // ---- decmpfs compression (sessions 5 + 6) ----
535 //
536 // A file is decmpfs-compressed when:
537 // - permissions.ownerFlags has UF_COMPRESSED (0x20)
538 // - flags has kHFSHasAttributesBit (0x0004)
539 // - dataFork.logical_size == 0 (real content lives
540 // either in an xattr or in the resource fork)
541 //
542 // The compression TYPE determines where the data lives:
543 // - type 3 (zlib-xattr): inline in com.apple.decmpfs xattr
544 // (small files, < ~3.6 KB)
545 // - type 4 (zlib-rsrc): in resource fork (chunked stream)
546 // (larger files, file system uses
547 // this when the compressed payload
548 // doesn't fit in an inline xattr)
549 // - type 7 (lzvn-xattr): LZVN in xattr (out of scope)
550 // - type 8 (lzvn-rsrc): LZVN in resource fork (out of scope)
551 // - type 11/12: LZFSE (out of scope)
552 //
553 // Strategy:
554 // * If a resource fork exists, try decompress_resource_fork
555 // first (= type 4). If it succeeds, done.
556 // * If resource fork absent OR decompression fails, AND we
557 // can load the Attributes file, look up
558 // com.apple.decmpfs and try decompress_xattr_zlib (= type 3).
559 // * If nothing works and there's a non-empty resource fork,
560 // save raw .compressed.rsrc placeholder.
561 const bool decmpfs_marked =
562 (effective.ownerFlags & hfs::UF_COMPRESSED) != 0 &&
563 (effective.flags & hfs::kHFSHasAttributesBit) != 0 &&
564 effective.dataFork.logical_size == 0;
565
566 if (decmpfs_marked) {
567 std::vector<unsigned char> data;
568 bool decompressed = false;
569 bool tried_rsrc = false;
570 bool tried_xattr = false;
571
572 // Path 1: resource fork (type 4 zlib-rsrc)
573 if (effective.resourceFork.total_blocks > 0) {
574 tried_rsrc = true;
575 std::vector<unsigned char> rsrc;
576 std::vector<std::string> rd_logs;
577 if (hfs::read_fork_full(reader_, volume_offset,
578 vh.block_size,
579 effective.resourceFork,
580 vh.extents_file,
582 effective.cnid,
583 effective.resourceFork.logical_size,
584 extents_btree_buffer,
585 rsrc, rd_logs)) {
586 for (auto& l : rd_logs) logs.push_back(std::move(l));
587 std::string dec_err;
589 rsrc.data(), rsrc.size(),
590 /*expected=*/64u * 1024 * 1024,
591 data, dec_err) && !data.empty()) {
592 decompressed = true;
593 }
594 } else {
595 for (auto& l : rd_logs) logs.push_back(std::move(l));
596 }
597 }
598
599 // Path 2: xattr (type 3 zlib-xattr)
600 if (!decompressed) {
601 // Lazy-load Attributes file
602 if (!attributes_load_attempted &&
604 attributes_load_attempted = true;
605 std::vector<std::string> ld_logs;
606 if (!hfs::read_fork_inline(reader_, volume_offset,
607 vh.block_size,
610 attributes_btree_buffer,
611 ld_logs)) {
612 attributes_btree_buffer.clear();
613 logs.push_back("HFS+: Attributes file could not "
614 "be loaded; xattr-based decmpfs "
615 "files will not be decoded");
616 for (auto& l : ld_logs) logs.push_back(std::move(l));
617 } else {
618 logs.push_back("HFS+: Attributes file loaded "
619 "(" + std::to_string(attributes_btree_buffer.size())
620 + " bytes)");
621 }
622 }
623
624 if (!attributes_btree_buffer.empty()) {
625 tried_xattr = true;
626 std::vector<unsigned char> xattr_buf;
627 std::vector<std::string> attr_logs;
629 attributes_btree_buffer,
630 effective.cnid,
631 "com.apple.decmpfs",
632 xattr_buf, attr_logs)) {
633 std::string dec_err;
635 xattr_buf.data(), xattr_buf.size(),
636 /*expected=*/0, // header has it
637 data, dec_err) && !data.empty()) {
638 decompressed = true;
639 } else if (!dec_err.empty()) {
640 logs.push_back("HFS+: decmpfs xattr '"
641 + ch.name_nfc + "' decode failed: "
642 + dec_err);
643 }
644 }
645 for (auto& l : attr_logs) logs.push_back(std::move(l));
646 }
647 }
648
649 if (decompressed) {
650 std::ofstream out(child_path,
651 std::ios::binary | std::ios::trunc);
652 if (!out) {
653 ++errors_logged;
654 logs.push_back("HFS+: cannot create decompressed "
655 "file " + child_path.string());
656 continue;
657 }
658 out.write(reinterpret_cast<const char*>(data.data()),
659 static_cast<std::streamsize>(data.size()));
660 std::string tag;
661 if (tried_xattr && !tried_rsrc) tag = "decmpfs-zlib-xattr";
662 else if (tried_rsrc) tag = "decmpfs-zlib";
663 else tag = "decmpfs-zlib-xattr";
664 if (is_hardlink) tag += "+hardlink";
665 recovered.push_back({
666 child_path.string(), "HFS+",
667 (out_dir / ch.name_nfc).lexically_relative(hfs_out_root).string(),
668 data.size(), "full", tag});
669 ++files_exported;
670 ++decmpfs_decompressed;
671 continue;
672 }
673
674 // Both paths failed -- this is an LZVN/LZFSE file or
675 // some other unsupported variant. If there's a
676 // resource fork, save it as placeholder.
677 if (effective.resourceFork.total_blocks > 0) {
678 std::vector<unsigned char> rsrc;
679 std::vector<std::string> rd_logs;
680 if (hfs::read_fork_full(reader_, volume_offset,
681 vh.block_size,
682 effective.resourceFork,
683 vh.extents_file,
685 effective.cnid,
686 effective.resourceFork.logical_size,
687 extents_btree_buffer,
688 rsrc, rd_logs)) {
689 const fs::path stub = child_path.string() +
690 ".compressed.rsrc";
691 std::ofstream out(stub,
692 std::ios::binary | std::ios::trunc);
693 if (out) {
694 out.write(reinterpret_cast<const char*>(rsrc.data()),
695 static_cast<std::streamsize>(rsrc.size()));
696 }
697 }
698 }
699 ++decmpfs_unsupported;
700 logs.push_back("HFS+: decmpfs file '" + ch.name_nfc
701 + "' uses unsupported compression "
702 "(LZVN/LZFSE or non-inline xattr). "
703 "Raw resource fork saved if available.");
704 continue;
705 }
706
707 // Regular file: write data fork to disk.
708 if (effective.dataFork.logical_size == 0 &&
709 effective.dataFork.total_blocks == 0) {
710 // Empty file -- still create it
711 std::ofstream out(child_path, std::ios::binary | std::ios::trunc);
712 if (out) {
713 recovered.push_back({
714 child_path.string(), "HFS+",
715 (out_dir / ch.name_nfc).lexically_relative(hfs_out_root).string(),
716 0, "full",
717 is_hardlink ? "hardlink-empty" : "empty-file"});
718 ++files_exported;
719 }
720 continue;
721 }
722
723 std::vector<unsigned char> data;
724 std::vector<std::string> rd_logs;
725 if (!hfs::read_fork_full(reader_, volume_offset, vh.block_size,
726 effective.dataFork, vh.extents_file,
728 effective.cnid,
729 effective.dataFork.logical_size,
730 extents_btree_buffer,
731 data, rd_logs)) {
732 ++errors_logged;
733 for (auto& l : rd_logs) logs.push_back(std::move(l));
734 logs.push_back("HFS+: failed to read data fork for '"
735 + ch.name_nfc + "'");
736 continue;
737 }
738 // Append rd_logs (e.g. "needs overflow file") to main log
739 bool used_overflow = false;
740 for (auto& l : rd_logs) {
741 if (l.find("Extents Overflow") != std::string::npos ||
742 l.find("overflow") != std::string::npos) {
743 used_overflow = true;
744 }
745 logs.push_back(std::move(l));
746 }
747 if (used_overflow) ++overflow_files;
748
749 // Truncate to logical_size in case the fork covers more bytes
750 // than the actual file size.
751 if (data.size() > effective.dataFork.logical_size) {
752 data.resize(static_cast<size_t>(effective.dataFork.logical_size));
753 }
754
755 std::ofstream out(child_path, std::ios::binary | std::ios::trunc);
756 if (!out) {
757 ++errors_logged;
758 logs.push_back("HFS+: cannot create file " + child_path.string());
759 continue;
760 }
761 out.write(reinterpret_cast<const char*>(data.data()),
762 static_cast<std::streamsize>(data.size()));
763
764 std::string source_tag;
765 if (is_hardlink && used_overflow) source_tag = "hardlink+overflow";
766 else if (is_hardlink) source_tag = "hardlink";
767 else if (used_overflow) source_tag = "overflow-extents";
768 else source_tag = "inline-extents";
769
770 recovered.push_back({
771 child_path.string(), "HFS+",
772 (out_dir / ch.name_nfc).lexically_relative(hfs_out_root).string(),
773 data.size(), "full", source_tag});
774 ++files_exported;
775 }
776 else {
777 ++skipped_unsupported;
778 }
779 }
780 return true;
781 };
782
783 walk_tree(hfs::kHFSRootFolderID, hfs_out_root, 0);
784
785 {
786 std::ostringstream m;
787 m << "HFS+ recovery completed. "
788 << "Files exported: " << files_exported
789 << " (hardlinks_resolved=" << hardlinks_resolved
790 << ", hardlinks_orphaned=" << hardlinks_orphaned
791 << ", decmpfs_decompressed=" << decmpfs_decompressed
792 << ", decmpfs_unsupported=" << decmpfs_unsupported
793 << ", overflow_files=" << overflow_files
794 << "), directories walked: " << dirs_walked
795 << ", skipped: " << skipped_unsupported
796 << ", errors: " << errors_logged << ".";
797 logs.push_back(m.str());
798 }
799
800 // Note for the user about residual unsupported items
801 if (decmpfs_unsupported > 0) {
802 logs.push_back("HFS+: " + std::to_string(decmpfs_unsupported)
803 + " files use LZVN/LZFSE compression (out of scope). "
804 "Their compressed resource forks were saved as "
805 "*.compressed.rsrc placeholders for forensic analysis.");
806 }
807 if (hardlinks_orphaned > 0) {
808 logs.push_back("HFS+: " + std::to_string(hardlinks_orphaned)
809 + " hardlinks reference missing iNode targets "
810 "(possibly corrupted Private Data folder).");
811 }
812
813 return files_exported > 0 || dirs_walked > 0;
814}
uint64_t size() const
std::string trs(const char *msgid)
Definition i18n.cpp:22
bool read_exact(std::ifstream &f, char *buf, std::size_t len)
bool validate_geometry(DiskReader &reader, uint64_t volume_offset, const hfs::VolumeHeader &vh, std::string &err)
constexpr uint32_t kSymLinkCreator
constexpr uint8_t kForkTypeResource
constexpr uint32_t kSymLinkFileType
bool decompress_resource_fork_zlib(const unsigned char *rsrc_buf, size_t rsrc_len, uint64_t expected_uncompressed_size, std::vector< unsigned char > &out, std::string &error)
bool parse_catalog_folder(const unsigned char *body, uint32_t body_len, CatalogFolder &out)
constexpr uint32_t kHFSPlusCreator
std::string catalog_name_to_host(const unsigned char *utf16be_name, uint16_t name_units)
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)
constexpr uint32_t kHardLinkFileType
bool parse_volume_header(const unsigned char *buf, VolumeHeader &out)
constexpr uint32_t kHFSRootFolderID
bool decompress_xattr_zlib(const unsigned char *xattr_buf, size_t xattr_len, uint64_t expected_uncompressed_size, std::vector< unsigned char > &out, std::string &error)
bool parse_node_descriptor(const unsigned char *buf, NodeDescriptor &out)
Definition hfs_btree.cpp:17
constexpr uint32_t HFSPLUS_VOLUME_HEADER_OFFSET
bool parse_catalog_file(const unsigned char *body, uint32_t body_len, CatalogFile &out)
constexpr uint16_t kHFSHasAttributesBit
bool find_inline_attribute(const std::vector< unsigned char > &attributes_btree_buffer, uint32_t fileID, const std::string &attr_name, std::vector< unsigned char > &out, std::vector< std::string > &logs)
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)
constexpr uint8_t UF_COMPRESSED
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
uint32_t catalog_record_body_offset(const unsigned char *record_buf, uint32_t record_len)
uint32_t parse_inode_filename(const unsigned char *utf16be_name, uint16_t name_units)
bool parse_header_rec(const unsigned char *buf, HeaderRec &out)
Definition hfs_btree.cpp:38
constexpr int16_t kHFSPlusFolderThreadRecord
constexpr uint8_t kForkTypeData
uint16_t be16(const unsigned char *p)
constexpr int16_t kHFSPlusFileThreadRecord
bool is_private_data_folder_name(const unsigned char *utf16be_name, uint16_t name_units)
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)
uint8_t ownerFlags
uint16_t fileMode
uint32_t special
FinderInfo16 userInfo
ForkData resourceFork
uint32_t parentID
const unsigned char * name_utf16be
uint16_t nameLength
FinderInfo16 userInfo
ForkData resourceFork
std::string name_nfc
uint64_t logical_size
std::array< ForkExtent, 8 > extents
uint32_t total_blocks
uint32_t rootNode
Definition hfs_btree.hpp:65
uint16_t nodeSize
Definition hfs_btree.hpp:69
uint32_t totalNodes
Definition hfs_btree.hpp:71
uint32_t leafRecords
Definition hfs_btree.hpp:66
uint8_t keyCompareType
Definition hfs_btree.hpp:75
uint16_t treeDepth
Definition hfs_btree.hpp:64
ForkData attributes_file
uint64_t volume_size_bytes() const
ForkData allocation_file
bool is_journaled() const
const char * variant_name() const

References hfs::VolumeHeader::allocation_file, hfs::VolumeHeader::attributes_file, hfs::be16(), hfs::VolumeHeader::block_size, hfs::VolumeHeader::catalog_file, hfs::catalog_name_to_host(), hfs::catalog_record_body_offset(), hfs::ChildEntry::cnid, hfs::CatalogFile::dataFork, hfs::ChildEntry::dataFork, hfs::decompress_resource_fork_zlib(), hfs::decompress_xattr_zlib(), hfs::ForkData::extents, hfs::VolumeHeader::extents_file, hfs::VolumeHeader::file_count, hfs::CatalogFile::fileID, hfs::BSDInfo::fileMode, hfs::ChildEntry::fileMode, hfs::find_inline_attribute(), hfs::CatalogFile::flags, hfs::CatalogFolder::flags, hfs::ChildEntry::flags, hfs::VolumeHeader::folder_count, hfs::CatalogFolder::folderID, hfs::VolumeHeader::free_blocks, hfs::HFSPLUS_VOLUME_HEADER_OFFSET, hfs::VolumeHeader::is_journaled(), hfs::is_private_data_folder_name(), hfs::kBTHeaderNode, hfs::HeaderRec::keyCompareType, hfs::kForkTypeData, hfs::kForkTypeResource, hfs::kHardLinkFileType, hfs::kHFSHasAttributesBit, hfs::kHFSPlusCreator, hfs::kHFSPlusFileRecord, hfs::kHFSPlusFileThreadRecord, hfs::kHFSPlusFolderRecord, hfs::kHFSPlusFolderThreadRecord, hfs::kHFSRootFolderID, hfs::NodeDescriptor::kind, hfs::kSymLinkCreator, hfs::kSymLinkFileType, hfs::HeaderRec::leafRecords, hfs::ForkData::logical_size, hfs::ChildEntry::name_nfc, hfs::CatalogKey::name_utf16be, hfs::CatalogKey::nameLength, hfs::HeaderRec::nodeSize, hfs::BSDInfo::ownerFlags, hfs::ChildEntry::ownerFlags, hfs::CatalogKey::parentID, hfs::parse_catalog_file(), hfs::parse_catalog_folder(), hfs::parse_catalog_key(), hfs::parse_header_rec(), hfs::parse_inode_filename(), hfs::parse_node_descriptor(), hfs::parse_volume_header(), hfs::CatalogFile::permissions, hfs::CatalogFolder::permissions, hfs::read_fork_full(), hfs::read_fork_inline(), reader_, hfs::ChildEntry::recordType, hfs::CatalogFile::resourceFork, hfs::ChildEntry::resourceFork, hfs::HeaderRec::rootNode, DiskReader::size(), hfs::BSDInfo::special, hfs::ChildEntry::special, hfs::VolumeHeader::startup_file, hfs::ForkData::total_blocks, hfs::VolumeHeader::total_blocks, hfs::HeaderRec::totalNodes, hfs::HeaderRec::treeDepth, trs(), hfs::UF_COMPRESSED, hfs::CatalogFile::userInfo, hfs::ChildEntry::userInfo, hfs::VolumeHeader::variant_name(), hfs::VolumeHeader::version, hfs::VolumeHeader::volume_size_bytes(), and hfs::walk_btree().

Member Data Documentation

◆ reader_

DiskReader& HfsRecovery::reader_
private

Definition at line 40 of file hfs_recovery.hpp.

Referenced by HfsRecovery(), and recover().


The documentation for this class was generated from the following files: