421 {
422 logs.push_back(
trs(
"XFS recovery (session 4: full dir/file extraction) starting at offset") +
423 std::string(" ") + std::to_string(volume_offset));
424
425
426 std::array<unsigned char, 512> sb_sector{};
428 error =
trs(
"XFS: cannot read superblock");
429 return false;
430 }
431
432 xfs::Superblock sb;
434 error =
trs(
"XFS: not a valid superblock at this offset");
435 return false;
436 }
437
438
439 {
440 std::string ge;
442 error =
trs(
"XFS: geometry inconsistent:") + std::string(
" ") + ge;
443 return false;
444 }
445 }
446
447
448 {
449 std::ostringstream m;
458 const std::string lbl = sb.
label();
459 if (!lbl.empty()) m << " label=\"" << lbl << "\"";
460 logs.push_back(m.str());
461 }
462
464 std::ostringstream m;
465 m << "XFS V5 features: ";
466 bool any = false;
473 if (!any) m << "(none)";
474 logs.push_back(m.str());
475 }
476
477
478 xfs::VolumeContext vol;
481 vol.
ag.assign(sb.
agcount, xfs::AGContext{});
482
483
484
485 constexpr uint32_t MAX_AG_TO_PROBE = 4096;
486 const uint32_t agcount = std::min<uint32_t>(sb.
agcount, MAX_AG_TO_PROBE);
487
488 uint32_t agf_ok = 0, agi_ok = 0;
489 for (uint32_t i = 0; i < agcount; ++i) {
491 if (vol.
ag[i].agf_loaded) ++agf_ok;
492 if (vol.
ag[i].agi_loaded) ++agi_ok;
493 }
494
495 {
496 std::ostringstream m;
497 m << "XFS AG headers loaded: AGF " << agf_ok << "/" << agcount
498 << ", AGI " << agi_ok << "/" << agcount;
499 logs.push_back(m.str());
500 }
501
502 if (agf_ok == 0 && agi_ok == 0) {
503 error =
trs(
"XFS: no AG headers could be parsed; volume looks corrupt or "
504 "this is a partition without an XFS volume at offset 0");
505 return false;
506 }
507
508
509
510 for (uint32_t i = 0; i < agcount; ++i) {
511 if (!vol.
ag[i].agi_loaded)
continue;
512 std::ostringstream m;
513 m << " AG " << i
514 <<
": ialloc=" << vol.
ag[i].agi.count
515 <<
" ifree=" << vol.
ag[i].agi.freecount
516 <<
" ibtree_root=blk" << vol.
ag[i].agi.root
517 <<
" ibtree_depth=" << vol.
ag[i].agi.level;
518 logs.push_back(m.str());
519 }
520
521
522
523
524
525
526
527
528
529
530
531
532
533 constexpr int MAX_DEPTH = 64;
534 constexpr size_t MAX_FILES = 200'000;
535
536
537 fs::path xfs_out_root =
538 fs::path(output_dir) / "filesystem_recovered" / "xfs" /
539 ("offset_" + std::to_string(volume_offset));
540 {
541 std::error_code ec;
542 fs::create_directories(xfs_out_root, ec);
543 if (ec) {
544 error = "Cannot create output directory: " + xfs_out_root.string();
545 return false;
546 }
547 }
548
549 std::set<uint64_t> visited_inodes;
550 size_t files_exported = 0;
551 size_t dirs_walked = 0;
552 size_t skipped_unsupported = 0;
553 size_t errors_logged = 0;
554
555
556
557 std::function<bool(uint64_t, const fs::path&, int)> walk_dir =
558 [&](uint64_t ino, const fs::path& out_dir, int depth) -> bool
559 {
560 if (depth >= MAX_DEPTH) {
561 logs.push_back("XFS: max recursion depth reached, stopping branch.");
562 return true;
563 }
564 if (!visited_inodes.insert(ino).second) {
565 return true;
566 }
567 if (files_exported >= MAX_FILES) {
568 logs.push_back("XFS: file export safety cap reached ("
569 + std::to_string(MAX_FILES) + ").");
570 return false;
571 }
572
573 std::vector<unsigned char> ibuf;
575 ++errors_logged;
576 logs.push_back("XFS: failed to read dinode for ino "
577 + std::to_string(ino));
578 return true;
579 }
580 xfs::DinodeCore core;
582 ++errors_logged;
583 logs.push_back("XFS: bad dinode magic at ino "
584 + std::to_string(ino));
585 return true;
586 }
588
589
590 return true;
591 }
592 ++dirs_walked;
593
596 const unsigned char* fork = ibuf.data() + fork_off;
597
598
599
600
601 std::vector<xfs::DirEntry> dir_entries;
602
604 xfs::ShortformDir sf;
607 ++errors_logged;
608 logs.push_back("XFS: cannot parse shortform dir at ino "
609 + std::to_string(ino));
610 return true;
611 }
612 dir_entries = std::move(sf.
entries);
613 }
616
617
618
619
620
621 std::vector<xfs::Extent> extents;
622 std::string cerr;
624 100'000,
625 extents, logs, cerr)) {
626 ++errors_logged;
627 logs.push_back("XFS: dir ino " + std::to_string(ino) +
628 " extent list error: " + cerr);
629 return true;
630 }
633 dir_entries, logs)) {
634 ++errors_logged;
635 logs.push_back("XFS: dir ino " + std::to_string(ino) +
636 " could not be parsed (block/leaf format)");
637 return true;
638 }
639 }
640 else {
641 ++skipped_unsupported;
642 std::ostringstream m;
643 m << "XFS: directory ino " << ino
644 << " uses unsupported di_format="
645 <<
static_cast<int>(core.
format);
646 logs.push_back(m.str());
647 return true;
648 }
649
650
651 for (const auto& e : dir_entries) {
653 const fs::path child_path = out_dir / safe_name;
654
655 std::vector<unsigned char> child_buf;
657 ++errors_logged;
658 logs.push_back("XFS: cannot read child dinode for '"
659 + e.name + "' (ino " + std::to_string(e.ino) + ")");
660 continue;
661 }
662 xfs::DinodeCore child_core;
664 ++errors_logged;
665 logs.push_back("XFS: bad child dinode magic for '"
666 + e.name + "'");
667 continue;
668 }
669
672 const unsigned char* cfork = child_buf.data() + cfork_off;
673
674 if (child_core.
is_dir()) {
675 std::error_code ec;
676 fs::create_directories(child_path, ec);
677 if (ec) {
678 ++errors_logged;
679 logs.push_back("XFS: cannot mkdir " + child_path.string());
680 continue;
681 }
682 if (!walk_dir(e.ino, child_path, depth + 1)) return false;
683 }
684 else if (child_core.
is_reg()) {
685
688 std::vector<xfs::Extent> extents;
689 std::string cerr;
691 cfork, cfork_len, 1'000'000,
692 extents, logs, cerr)) {
693 ++errors_logged;
694 logs.push_back("XFS: cannot read extents for '" + e.name +
695 "': " + cerr);
696 continue;
697 }
698 std::string ferr;
700 static_cast<uint64_t
>(child_core.
size),
701 child_path, ferr)) {
703 ? "btree-extents" : "extents";
704 recovered.push_back({
705 child_path.string(), "XFS",
706 (out_dir / safe_name).lexically_relative(xfs_out_root).string(),
707 static_cast<uint64_t
>(child_core.
size),
708 "full", note});
709 ++files_exported;
710 } else {
711 ++errors_logged;
712 logs.push_back("XFS: failed to export '" + e.name +
713 "': " + ferr);
714 }
715 }
717
718
719 std::ofstream out(child_path, std::ios::binary | std::ios::trunc);
720 if (out) {
721 const uint64_t bytes = std::min<uint64_t>(
722 static_cast<uint64_t
>(child_core.
size), cfork_len);
723 out.write(reinterpret_cast<const char*>(cfork),
724 static_cast<std::streamsize>(bytes));
725 recovered.push_back({
726 child_path.string(), "XFS",
727 (out_dir / safe_name).lexically_relative(xfs_out_root).string(),
728 bytes, "full", "shortform-inline"});
729 ++files_exported;
730 } else {
731 ++errors_logged;
732 logs.push_back("XFS: cannot create file " + child_path.string());
733 }
734 }
735 else {
736
737 ++skipped_unsupported;
738 }
739 }
741 std::string target;
743
744 const uint64_t link_bytes = std::min<uint64_t>(
745 static_cast<uint64_t
>(child_core.
size), cfork_len);
746 target.assign(reinterpret_cast<const char*>(cfork),
747 static_cast<size_t>(link_bytes));
748 }
751
752
753 std::vector<xfs::Extent> extents;
754 std::string cerr;
756 cfork, cfork_len, 256,
757 extents, logs, cerr)) {
758 ++errors_logged;
759 logs.push_back("XFS: cannot read symlink extents for '"
760 + e.name + "': " + cerr);
761 continue;
762 }
763 std::vector<unsigned char> body;
764 std::string rerr;
766 static_cast<uint64_t
>(child_core.
size),
767 body, rerr)) {
768 ++errors_logged;
769 logs.push_back("XFS: cannot read symlink body for '"
770 + e.name + "': " + rerr);
771 continue;
772 }
773 target.assign(reinterpret_cast<const char*>(body.data()),
774 body.size());
775 }
776 else {
777 ++skipped_unsupported;
778 continue;
779 }
780
781
782
783 std::error_code ec;
784 fs::create_symlink(target, child_path, ec);
785 if (ec) {
786 std::ofstream out(child_path.string() + ".symlink",
787 std::ios::binary | std::ios::trunc);
788 if (out) out << target;
789 }
790 recovered.push_back({
791 child_path.string(), "XFS",
792 (out_dir / safe_name).lexically_relative(xfs_out_root).string(),
793 target.size(), "full",
795 ? "symlink-shortform" : "symlink-extents"});
796 ++files_exported;
797 }
799
800 ++skipped_unsupported;
801 }
802 else {
803 ++skipped_unsupported;
804 }
805 }
806 return true;
807 };
808
809
811 error = "XFS: superblock has no root inode (rootino=0).";
812 return false;
813 }
814 walk_dir(sb.
rootino, xfs_out_root, 0);
815
816
817
818
819
820
821
822
823
824
825 size_t deleted_recovered = 0;
826 size_t deleted_attempted = 0;
827 size_t deleted_stale = 0;
828
831
832 fs::path deleted_dir = xfs_out_root / "deleted";
833 std::error_code dec;
834 fs::create_directories(deleted_dir, dec);
835
836
837 std::vector<xfs::AllocatedInode> free_slots;
838 auto fcb = [&](const xfs::InobtRec& rec, uint32_t agno) -> bool {
840
841 if (free_slots.size() >= 200'000) {
842 logs.push_back("XFS FINOBT: candidate cap reached (200k slots)");
843 return false;
844 }
845 return true;
846 };
847 std::vector<std::string> finobt_logs;
849 for (auto& l : finobt_logs) logs.push_back(std::move(l));
850
851 logs.push_back("XFS FINOBT: " + std::to_string(free_slots.size())
852 + " free-inode candidates collected.");
853
854
855 for (const auto& fi : free_slots) {
856
857
858
859 if (visited_inodes.count(fi.ino) != 0) continue;
860 ++deleted_attempted;
861
862 std::vector<unsigned char> ibuf(vol.
sb.
inodesize, 0);
864 continue;
865 }
866 xfs::DinodeCore core;
868 continue;
869 }
870
871
872
873
874 if (core.
mode == 0) { ++deleted_stale;
continue; }
875 if (core.
size <= 0) { ++deleted_stale;
continue; }
876
877
878
879
880
881 if (!core.
is_reg()) { ++deleted_stale;
continue; }
882
886 const unsigned char* cfork = ibuf.data() + cfork_off;
887
888
889 std::ostringstream nm;
890 nm << "inode_" << fi.ino << ".bin";
891 const fs::path out_path = deleted_dir / nm.str();
892
893 std::vector<xfs::Extent> extents;
894 std::vector<std::string> ex_logs;
895 std::string ex_err;
897 1'000'000, extents, ex_logs, ex_err)) {
898 ++deleted_stale;
899 continue;
900 }
901
902
904 ++deleted_stale;
905 continue;
906 }
907
908 std::vector<unsigned char> file_data;
910
911 const uint64_t bytes = std::min<uint64_t>(
912 static_cast<uint64_t
>(core.
size), cfork_len);
913 file_data.assign(cfork, cfork + bytes);
914 } else {
916 static_cast<uint64_t
>(core.
size),
917 file_data, ex_err)) {
918 ++deleted_stale;
919 continue;
920 }
921 }
922
923 std::ofstream out(out_path, std::ios::binary | std::ios::trunc);
924 if (!out) { ++deleted_stale; continue; }
925 out.write(reinterpret_cast<const char*>(file_data.data()),
926 static_cast<std::streamsize>(file_data.size()));
927 recovered.push_back({
928 out_path.string(), "XFS",
929 ("deleted/" + nm.str()),
930 file_data.size(), "deleted", "finobt"});
931 ++deleted_recovered;
932 }
933
934 std::ostringstream m;
935 m << "XFS FINOBT: deleted-file recovery -- attempted "
936 << deleted_attempted << " slots, recovered "
937 << deleted_recovered << " files, "
938 << deleted_stale << " stale/unsupported.";
939 logs.push_back(m.str());
940
941 if (deleted_recovered == 0 && deleted_attempted > 0) {
942 logs.push_back("XFS FINOBT: no deleted files recovered. "
943 "This is normal on a long-running filesystem "
944 "where free inode slots have been overwritten "
945 "by new allocations. Recovery only works on "
946 "deletions that occurred since the last reuse.");
947 }
948 }
949
950 {
951 std::ostringstream m;
952 m << "XFS recovery completed. "
953 << "Files exported: " << files_exported
954 << ", directories walked: " << dirs_walked
955 << ", deleted files recovered: " << deleted_recovered
956 << ", skipped (non-shortform/unsupported): " << skipped_unsupported
957 << ", per-inode errors: " << errors_logged << ".";
958 logs.push_back(m.str());
959 }
960
961
962 if (skipped_unsupported > 0) {
963 logs.push_back("XFS: some entries were skipped (special device files, "
964 "non-standard inode formats, or unrecognised data blocks). "
965 "Regular shortform/block/leaf directories and "
966 "extents/btree files were processed.");
967 }
968
969 return files_exported > 0 || dirs_walked > 0 || deleted_recovered > 0;
970}
bool read_at(uint64_t offset, void *buffer, size_t len)
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)
void load_ag_headers(DiskReader &reader, uint64_t volume_offset, const xfs::Superblock &sb, uint32_t agno, xfs::AGContext &ag, std::vector< std::string > &logs)
bool read_dinode(DiskReader &reader, const xfs::VolumeContext &vol, uint64_t ino, std::vector< unsigned char > &buf)
bool read_extents_data(DiskReader &reader, const xfs::VolumeContext &vol, const std::vector< xfs::Extent > &extents, uint64_t file_size, std::vector< unsigned char > &out, std::string &err)
bool collect_extents(DiskReader &reader, const xfs::VolumeContext &vol, const xfs::DinodeCore &core, const unsigned char *fork_data, uint32_t fork_len, uint32_t max_extents, std::vector< xfs::Extent > &out, std::vector< std::string > &logs, std::string &err)
bool write_extents_to_path(DiskReader &reader, const xfs::VolumeContext &vol, std::vector< xfs::Extent > extents, uint64_t file_size, const fs::path &out_path, std::string &err)
bool collect_dir_entries_block_or_leaf(DiskReader &reader, const xfs::VolumeContext &vol, const std::vector< xfs::Extent > &extents, bool has_ftype, std::vector< xfs::DirEntry > &out, std::vector< std::string > &logs)
std::string sanitize_component(const std::string &in)
constexpr uint32_t XFS_SB_FEAT_INCOMPAT_BIGTIME
constexpr uint8_t XFS_DINODE_FMT_EXTENTS
bool walk_all_free_inode_records(DiskReader &reader, const VolumeContext &vol, const InobtRecordCallback &on_record, std::vector< std::string > &logs)
constexpr uint8_t XFS_DINODE_FMT_LOCAL
bool parse_dinode_core(const unsigned char *buf, DinodeCore &out)
bool parse_superblock(const unsigned char *buf, Superblock &out)
uint32_t dinode_data_fork_offset(const DinodeCore &core)
constexpr uint8_t XFS_DINODE_FMT_BTREE
constexpr uint32_t XFS_SB_FEAT_INCOMPAT_SPINODES
constexpr uint32_t XFS_SB_FEAT_RO_COMPAT_FINOBT
uint32_t dinode_data_fork_length(const DinodeCore &core, uint32_t inode_size)
constexpr uint32_t XFS_SB_FEAT_INCOMPAT_FTYPE
constexpr uint32_t XFS_SB_FEAT_RO_COMPAT_REFLINK
bool parse_shortform_dir(const unsigned char *data, uint32_t data_len, bool has_ftype, ShortformDir &out)
void expand_chunk_free(const InobtRec &rec, uint32_t agno, const VolumeContext &vol, std::vector< AllocatedInode > &out)
constexpr uint32_t XFS_SB_FEAT_INCOMPAT_NREXT64
uint64_t volume_size_bytes() const
uint32_t features_incompat
std::string label() const
unsigned format_version() const
uint32_t features_ro_compat
std::vector< AGContext > ag