61 const std::string& output_dir,
62 std::vector<RecoveredFileRecord>& recovered,
63 std::vector<std::string>& logs,
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));
70 std::array<unsigned char, 512> sector{};
72 if (!read_exact(
reader_, header_off, sector.data(), sector.
size())) {
73 error =
trs(
"HFS+: cannot read volume header sector");
79 error =
trs(
"HFS+: not a valid volume header at this offset");
85 if (!validate_geometry(
reader_, volume_offset, vh, ge)) {
86 error =
trs(
"HFS+: geometry inconsistent:") + std::string(
" ") + ge;
94 m <<
"HFS+ volume identified: variant=" << vh.
variant_name()
103 logs.push_back(m.str());
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=[";
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 <<
")";
124 logs.push_back(m.str());
126 logs.push_back(
"HFS+ special files:");
149 error =
trs(
"HFS+: catalog file has no extents -- volume looks empty "
154 std::vector<unsigned char> catalog_bytes;
158 catalog_bytes, logs)) {
159 error =
trs(
"HFS+: failed to read catalog file content");
163 std::ostringstream m;
164 m <<
"HFS+ catalog file: " << catalog_bytes.size() <<
" bytes "
166 <<
" allocation blocks";
167 logs.push_back(m.str());
171 if (catalog_bytes.size() < 14) {
172 error =
trs(
"HFS+: catalog file too small to contain a header node");
178 error =
trs(
"HFS+: catalog node 0 is not a valid header node");
183 error =
trs(
"HFS+: catalog B-tree header record parse failed");
187 std::ostringstream m;
188 m <<
"HFS+ catalog B-tree: depth=" << catalog_hdr.
treeDepth
189 <<
" root_node=" << catalog_hdr.
rootNode
191 <<
" node_size=" << catalog_hdr.
nodeSize
193 <<
" key_compare=0x" << std::hex
196 logs.push_back(m.str());
201 const uint64_t expected_size =
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());
214 auto node_reader = [&](uint32_t node_index,
215 std::vector<unsigned char>& out) ->
bool {
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);
228 std::map<uint32_t, std::vector<hfs::ChildEntry>> children_by_parent;
233 uint32_t private_data_cnid = 0;
234 std::map<uint32_t, hfs::ChildEntry> inode_to_target;
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;
241 auto on_leaf = [&](
const unsigned char* rec, uint16_t rec_len) ->
bool {
246 ++key_parse_failures;
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));
254 const unsigned char* body = rec + body_off + 2;
255 const uint32_t body_len = rec_len - body_off - 2;
257 switch (recordType) {
262 ++body_parse_failures;
284 children_by_parent[key.
parentID].push_back(std::move(ch));
291 ++body_parse_failures;
310 if (private_data_cnid != 0 &&
311 key.
parentID == private_data_cnid) {
312 const uint32_t inum =
316 inode_to_target[inum] = ch;
322 children_by_parent[key.
parentID].push_back(std::move(ch));
327 default: ++unknown_record_types;
break;
333 std::vector<std::string> walk_logs;
334 std::string walk_err;
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);
340 for (
auto& l : walk_logs) logs.push_back(std::move(l));
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());
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());
389 namespace fs = std::filesystem;
390 constexpr int MAX_DEPTH = 64;
391 constexpr size_t MAX_FILES = 200'000;
393 fs::path hfs_out_root =
394 fs::path(output_dir) /
"filesystem_recovered" /
"hfs" /
395 (
"offset_" + std::to_string(volume_offset));
398 fs::create_directories(hfs_out_root, ec);
400 error =
"Cannot create output directory: " + hfs_out_root.string();
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;
419 std::vector<unsigned char> extents_btree_buffer;
424 std::vector<unsigned char> attributes_btree_buffer;
425 bool attributes_load_attempted =
false;
428 constexpr uint16_t S_IFLNK_BITS = 0xA000;
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
433 if (depth >= MAX_DEPTH) {
434 logs.push_back(
"HFS+: max recursion depth reached, stopping branch.");
437 if (!visited_cnids.insert(parent_cnid).second) {
440 if (files_exported >= MAX_FILES) {
441 logs.push_back(
"HFS+: file export safety cap reached ("
442 + std::to_string(MAX_FILES) +
").");
446 auto it = children_by_parent.find(parent_cnid);
447 if (it == children_by_parent.end())
return true;
450 for (
const auto& ch : it->second) {
451 const fs::path child_path = out_dir / ch.name_nfc;
455 fs::create_directories(child_path, ec);
458 logs.push_back(
"HFS+: cannot mkdir " + child_path.string());
461 if (!walk_tree(ch.cnid, child_path, depth + 1))
return false;
465 const bool finder_says_symlink =
468 const bool mode_says_symlink =
469 (ch.fileMode & 0xF000) == S_IFLNK_BITS;
471 if (finder_says_symlink || mode_says_symlink) {
473 std::vector<unsigned char> link_data;
474 std::vector<std::string> rd_logs;
477 ch.dataFork.logical_size,
478 link_data, rd_logs)) {
479 std::string target(
reinterpret_cast<const char*
>(link_data.data()),
482 fs::create_symlink(target, child_path, ec);
485 std::ofstream out(child_path.string() +
".symlink",
486 std::ios::binary | std::ios::trunc);
487 if (out) out << target;
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"});
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 +
"'");
513 auto it_t = inode_to_target.find(ch.special);
514 if (it_t != inode_to_target.end()) {
517 effective.
dataFork = it_t->second.dataFork;
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;
525 ++hardlinks_orphaned;
527 logs.push_back(
"HFS+: hardlink '" + ch.name_nfc
528 +
"' references missing iNode "
529 + std::to_string(ch.special));
561 const bool decmpfs_marked =
566 if (decmpfs_marked) {
567 std::vector<unsigned char> data;
568 bool decompressed =
false;
569 bool tried_rsrc =
false;
570 bool tried_xattr =
false;
575 std::vector<unsigned char> rsrc;
576 std::vector<std::string> rd_logs;
584 extents_btree_buffer,
586 for (
auto& l : rd_logs) logs.push_back(std::move(l));
589 rsrc.data(), rsrc.size(),
591 data, dec_err) && !data.empty()) {
595 for (
auto& l : rd_logs) logs.push_back(std::move(l));
602 if (!attributes_load_attempted &&
604 attributes_load_attempted =
true;
605 std::vector<std::string> ld_logs;
610 attributes_btree_buffer,
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));
618 logs.push_back(
"HFS+: Attributes file loaded "
619 "(" + std::to_string(attributes_btree_buffer.size())
624 if (!attributes_btree_buffer.empty()) {
626 std::vector<unsigned char> xattr_buf;
627 std::vector<std::string> attr_logs;
629 attributes_btree_buffer,
632 xattr_buf, attr_logs)) {
635 xattr_buf.data(), xattr_buf.size(),
637 data, dec_err) && !data.empty()) {
639 }
else if (!dec_err.empty()) {
640 logs.push_back(
"HFS+: decmpfs xattr '"
641 + ch.name_nfc +
"' decode failed: "
645 for (
auto& l : attr_logs) logs.push_back(std::move(l));
650 std::ofstream out(child_path,
651 std::ios::binary | std::ios::trunc);
654 logs.push_back(
"HFS+: cannot create decompressed "
655 "file " + child_path.string());
658 out.write(
reinterpret_cast<const char*
>(data.data()),
659 static_cast<std::streamsize
>(data.size()));
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});
670 ++decmpfs_decompressed;
678 std::vector<unsigned char> rsrc;
679 std::vector<std::string> rd_logs;
687 extents_btree_buffer,
689 const fs::path stub = child_path.string() +
691 std::ofstream out(stub,
692 std::ios::binary | std::ios::trunc);
694 out.write(
reinterpret_cast<const char*
>(rsrc.data()),
695 static_cast<std::streamsize
>(rsrc.size()));
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.");
711 std::ofstream out(child_path, std::ios::binary | std::ios::trunc);
713 recovered.push_back({
714 child_path.string(),
"HFS+",
715 (out_dir / ch.name_nfc).lexically_relative(hfs_out_root).string(),
717 is_hardlink ?
"hardlink-empty" :
"empty-file"});
723 std::vector<unsigned char> data;
724 std::vector<std::string> rd_logs;
730 extents_btree_buffer,
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 +
"'");
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;
745 logs.push_back(std::move(l));
747 if (used_overflow) ++overflow_files;
755 std::ofstream out(child_path, std::ios::binary | std::ios::trunc);
758 logs.push_back(
"HFS+: cannot create file " + child_path.string());
761 out.write(
reinterpret_cast<const char*
>(data.data()),
762 static_cast<std::streamsize
>(data.size()));
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";
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});
777 ++skipped_unsupported;
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());
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.");
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).");
813 return files_exported > 0 || dirs_walked > 0;