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
70 std::array<unsigned char, 512> sector{};
73 error =
trs(
"HFS+: cannot read volume header sector");
74 return false;
75 }
76
77 hfs::VolumeHeader vh;
79 error =
trs(
"HFS+: not a valid volume header at this offset");
80 return false;
81 }
82
83 {
84 std::string ge;
86 error =
trs(
"HFS+: geometry inconsistent:") + std::string(
" ") + ge;
87 return false;
88 }
89 }
90
91
92 {
93 std::ostringstream m;
94 m <<
"HFS+ volume identified: variant=" << vh.
variant_name()
103 logs.push_back(m.str());
104 }
105
106
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:");
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
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;
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 "
166 << " allocation blocks";
167 logs.push_back(m.str());
168 }
169
170
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;
178 error =
trs(
"HFS+: catalog node 0 is not a valid header node");
179 return false;
180 }
181 hfs::HeaderRec 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
191 <<
" node_size=" << catalog_hdr.
nodeSize
193 << " key_compare=0x" << std::hex
195 << std::dec;
196 logs.push_back(m.str());
197 }
198
199
200
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());
210 }
211
212
213
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
225
226
227
228 std::map<uint32_t, std::vector<hfs::ChildEntry>> children_by_parent;
229
230
231
232
233 uint32_t private_data_cnid = 0;
234 std::map<uint32_t, hfs::ChildEntry> inode_to_target;
235
236
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
244 hfs::CatalogKey key;
246 ++key_parse_failures;
247 return true;
248 }
249
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
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;
262 ++body_parse_failures;
263 return true;
264 }
265
266
271
272
273 return true;
274 }
275 hfs::ChildEntry ch;
284 children_by_parent[key.
parentID].push_back(std::move(ch));
285 break;
286 }
288 ++files;
289 hfs::CatalogFile f;
291 ++body_parse_failures;
292 return true;
293 }
294
295 hfs::ChildEntry ch;
307
308
309
310 if (private_data_cnid != 0 &&
311 key.
parentID == private_data_cnid) {
312 const uint32_t inum =
315 if (inum > 0) {
316 inode_to_target[inum] = ch;
317
318
319 return true;
320 }
321 }
322 children_by_parent[key.
parentID].push_back(std::move(ch));
323 break;
324 }
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;
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 }
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
374
375
376
377
378
379
380
381
382
383
384
385
386
387
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
417
418
419 std::vector<unsigned char> extents_btree_buffer;
420
421
422
423
424 std::vector<unsigned char> attributes_btree_buffer;
425 bool attributes_load_attempted = false;
426
427
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;
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 }
464
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
473 std::vector<unsigned char> link_data;
474 std::vector<std::string> rd_logs;
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
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 '"
499 }
500 continue;
501 }
502
503
504
505
506
507
508
509 hfs::ChildEntry effective = ch;
512 if (is_hardlink) {
513 auto it_t = inode_to_target.find(ch.
special);
514 if (it_t != inode_to_target.end()) {
515
516
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;
524 } else {
525 ++hardlinks_orphaned;
526 ++errors_logged;
527 logs.push_back(
"HFS+: hardlink '" + ch.
name_nfc
528 + "' references missing iNode "
530 continue;
531 }
532 }
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561 const bool decmpfs_marked =
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
574 tried_rsrc = true;
575 std::vector<unsigned char> rsrc;
576 std::vector<std::string> rd_logs;
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 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
600 if (!decompressed) {
601
602 if (!attributes_load_attempted &&
604 attributes_load_attempted = true;
605 std::vector<std::string> ld_logs;
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,
631 "com.apple.decmpfs",
632 xattr_buf, attr_logs)) {
633 std::string dec_err;
635 xattr_buf.data(), xattr_buf.size(),
636 0,
637 data, dec_err) && !
data.empty()) {
638 decompressed = true;
639 } else if (!dec_err.empty()) {
640 logs.push_back("HFS+: decmpfs xattr '"
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
675
676
678 std::vector<unsigned char> rsrc;
679 std::vector<std::string> rd_logs;
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
710
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;
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 '"
736 continue;
737 }
738
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
750
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
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
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}
std::string trs(const char *msgid)
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)
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
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)
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)
const unsigned char * name_utf16be
std::array< ForkExtent, 8 > extents