418 const std::string& output_dir,
419 std::vector<RecoveredFileRecord>& recovered,
420 std::vector<std::string>& logs,
421 std::string& error) {
422 logs.push_back(
trs(
"XFS recovery (session 4: full dir/file extraction) starting at offset") +
423 std::string(
" ") + std::to_string(volume_offset));
426 std::array<unsigned char, 512> sb_sector{};
427 if (!read_exact(
reader_, volume_offset, sb_sector.data(), sb_sector.
size())) {
428 error =
trs(
"XFS: cannot read superblock");
434 error =
trs(
"XFS: not a valid superblock at this offset");
441 if (!validate_geometry(
reader_, volume_offset, sb, ge)) {
442 error =
trs(
"XFS: geometry inconsistent:") + std::string(
" ") + ge;
449 std::ostringstream m;
458 const std::string lbl = sb.
label();
459 if (!lbl.empty()) m <<
" label=\"" << lbl <<
"\"";
460 logs.push_back(m.str());
464 std::ostringstream m;
465 m <<
"XFS V5 features: ";
473 if (!any) m <<
"(none)";
474 logs.push_back(m.str());
485 constexpr uint32_t MAX_AG_TO_PROBE = 4096;
486 const uint32_t agcount = std::min<uint32_t>(sb.
agcount, MAX_AG_TO_PROBE);
488 uint32_t agf_ok = 0, agi_ok = 0;
489 for (uint32_t i = 0; i < agcount; ++i) {
490 load_ag_headers(
reader_, volume_offset, sb, i, vol.
ag[i], logs);
491 if (vol.
ag[i].agf_loaded) ++agf_ok;
492 if (vol.
ag[i].agi_loaded) ++agi_ok;
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());
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");
510 for (uint32_t i = 0; i < agcount; ++i) {
511 if (!vol.
ag[i].agi_loaded)
continue;
512 std::ostringstream m;
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());
533 constexpr int MAX_DEPTH = 64;
534 constexpr size_t MAX_FILES = 200'000;
537 fs::path xfs_out_root =
538 fs::path(output_dir) /
"filesystem_recovered" /
"xfs" /
539 (
"offset_" + std::to_string(volume_offset));
542 fs::create_directories(xfs_out_root, ec);
544 error =
"Cannot create output directory: " + xfs_out_root.string();
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;
557 std::function<bool(uint64_t,
const fs::path&,
int)> walk_dir =
558 [&](uint64_t ino,
const fs::path& out_dir,
int depth) ->
bool
560 if (depth >= MAX_DEPTH) {
561 logs.push_back(
"XFS: max recursion depth reached, stopping branch.");
564 if (!visited_inodes.insert(ino).second) {
567 if (files_exported >= MAX_FILES) {
568 logs.push_back(
"XFS: file export safety cap reached ("
569 + std::to_string(MAX_FILES) +
").");
573 std::vector<unsigned char> ibuf;
574 if (!read_dinode(
reader_, vol, ino, ibuf)) {
576 logs.push_back(
"XFS: failed to read dinode for ino "
577 + std::to_string(ino));
583 logs.push_back(
"XFS: bad dinode magic at ino "
584 + std::to_string(ino));
596 const unsigned char* fork = ibuf.data() + fork_off;
601 std::vector<xfs::DirEntry> dir_entries;
608 logs.push_back(
"XFS: cannot parse shortform dir at ino "
609 + std::to_string(ino));
612 dir_entries = std::move(sf.
entries);
621 std::vector<xfs::Extent> extents;
623 if (!collect_extents(
reader_, vol, core, fork, fork_len,
625 extents, logs, cerr)) {
627 logs.push_back(
"XFS: dir ino " + std::to_string(ino) +
628 " extent list error: " + cerr);
631 if (!collect_dir_entries_block_or_leaf(
reader_, vol, extents,
633 dir_entries, logs)) {
635 logs.push_back(
"XFS: dir ino " + std::to_string(ino) +
636 " could not be parsed (block/leaf format)");
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());
651 for (
const auto& e : dir_entries) {
653 const fs::path child_path = out_dir / safe_name;
655 std::vector<unsigned char> child_buf;
656 if (!read_dinode(
reader_, vol, e.ino, child_buf)) {
658 logs.push_back(
"XFS: cannot read child dinode for '"
659 + e.name +
"' (ino " + std::to_string(e.ino) +
")");
665 logs.push_back(
"XFS: bad child dinode magic for '"
672 const unsigned char* cfork = child_buf.data() + cfork_off;
674 if (child_core.
is_dir()) {
676 fs::create_directories(child_path, ec);
679 logs.push_back(
"XFS: cannot mkdir " + child_path.string());
682 if (!walk_dir(e.ino, child_path, depth + 1))
return false;
684 else if (child_core.
is_reg()) {
688 std::vector<xfs::Extent> extents;
690 if (!collect_extents(
reader_, vol, child_core,
691 cfork, cfork_len, 1'000'000,
692 extents, logs, cerr)) {
694 logs.push_back(
"XFS: cannot read extents for '" + e.name +
699 if (write_extents_to_path(
reader_, vol, std::move(extents),
700 static_cast<uint64_t
>(child_core.
size),
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),
712 logs.push_back(
"XFS: failed to export '" + e.name +
719 std::ofstream out(child_path, std::ios::binary | std::ios::trunc);
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"});
732 logs.push_back(
"XFS: cannot create file " + child_path.string());
737 ++skipped_unsupported;
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));
753 std::vector<xfs::Extent> extents;
755 if (!collect_extents(
reader_, vol, child_core,
756 cfork, cfork_len, 256,
757 extents, logs, cerr)) {
759 logs.push_back(
"XFS: cannot read symlink extents for '"
760 + e.name +
"': " + cerr);
763 std::vector<unsigned char> body;
765 if (!read_extents_data(
reader_, vol, extents,
766 static_cast<uint64_t
>(child_core.
size),
769 logs.push_back(
"XFS: cannot read symlink body for '"
770 + e.name +
"': " + rerr);
773 target.assign(
reinterpret_cast<const char*
>(body.data()),
777 ++skipped_unsupported;
784 fs::create_symlink(target, child_path, ec);
786 std::ofstream out(child_path.string() +
".symlink",
787 std::ios::binary | std::ios::trunc);
788 if (out) out << target;
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"});
800 ++skipped_unsupported;
803 ++skipped_unsupported;
811 error =
"XFS: superblock has no root inode (rootino=0).";
814 walk_dir(sb.
rootino, xfs_out_root, 0);
825 size_t deleted_recovered = 0;
826 size_t deleted_attempted = 0;
827 size_t deleted_stale = 0;
832 fs::path deleted_dir = xfs_out_root /
"deleted";
834 fs::create_directories(deleted_dir, dec);
837 std::vector<xfs::AllocatedInode> free_slots;
838 auto fcb = [&](
const xfs::InobtRec& rec, uint32_t agno) ->
bool {
841 if (free_slots.size() >= 200'000) {
842 logs.push_back(
"XFS FINOBT: candidate cap reached (200k slots)");
847 std::vector<std::string> finobt_logs;
849 for (
auto& l : finobt_logs) logs.push_back(std::move(l));
851 logs.push_back(
"XFS FINOBT: " + std::to_string(free_slots.size())
852 +
" free-inode candidates collected.");
855 for (
const auto& fi : free_slots) {
859 if (visited_inodes.count(fi.ino) != 0)
continue;
862 std::vector<unsigned char> ibuf(vol.
sb.
inodesize, 0);
874 if (core.
mode == 0) { ++deleted_stale;
continue; }
875 if (core.
size <= 0) { ++deleted_stale;
continue; }
881 if (!core.
is_reg()) { ++deleted_stale;
continue; }
886 const unsigned char* cfork = ibuf.data() + cfork_off;
889 std::ostringstream nm;
890 nm <<
"inode_" << fi.ino <<
".bin";
891 const fs::path out_path = deleted_dir / nm.str();
893 std::vector<xfs::Extent> extents;
894 std::vector<std::string> ex_logs;
896 if (!collect_extents(
reader_, vol, core, cfork, cfork_len,
897 1'000'000, extents, ex_logs, ex_err)) {
908 std::vector<unsigned char> file_data;
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);
915 if (!read_extents_data(
reader_, vol, extents,
916 static_cast<uint64_t
>(core.
size),
917 file_data, ex_err)) {
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"});
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());
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.");
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());
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.");
969 return files_exported > 0 || dirs_walked > 0 || deleted_recovered > 0;