160 const std::string& output_dir,
161 std::vector<RecoveredFileRecord>& recovered,
162 std::vector<std::string>& logs,
163 std::string& error) {
167 fs::path out_root = fs::path(output_dir) /
"filesystem_recovered" /
"ext4" / (
"offset_" + std::to_string(volume_offset));
168 fs::create_directories(out_root);
170 logs.push_back(
trs(
"ext4 recovery started at offset") + std::string(
" ") + std::to_string(volume_offset));
172 logs.push_back(
trs(
"ext4 block size") + std::string(
": ") + std::to_string(
block_size_) +
", inode size: " + std::to_string(
inode_size_));
174 std::set<uint32_t> dir_guard;
180 logs.push_back(
trs(
"ext4 recovery completed."));
274 std::string local_error;
276 std::vector<ExportRange> ranges;
277 if (!
inode_ranges(journal_inode, ranges, local_error))
return;
278 std::vector<unsigned char> block(
static_cast<size_t>(
block_size_));
280 std::vector<std::pair<uint32_t, uint32_t>> pending_tags;
281 std::map<uint32_t, std::vector<unsigned char>> pending_data;
282 std::set<uint32_t> pending_revokes;
283 uint32_t pending_sequence = 0;
284 bool transaction_open =
false;
286 auto apply_transaction = [&]() {
287 if (!transaction_open)
return;
288 for (uint32_t blk : pending_revokes) {
292 for (
const auto& [target_block, payload] : pending_data) {
293 if (pending_revokes.find(target_block) != pending_revokes.end())
continue;
295 if (slot.second.empty() || pending_sequence >= slot.first) {
296 slot.first = pending_sequence;
297 slot.second = payload;
303 pending_tags.clear();
304 pending_data.clear();
305 pending_revokes.clear();
306 transaction_open =
false;
307 pending_sequence = 0;
310 for (
const auto& r : ranges) {
311 const uint64_t offset = r.offset;
312 const uint64_t length = r.length;
313 uint64_t cursor = offset;
314 uint64_t remaining = std::min<uint64_t>(length, remaining_total);
318 const uint32_t magic =
be32(block.data());
319 if (magic == 0xC03B3998u) {
320 const uint32_t type =
be32(block.data() + 4);
321 const uint32_t sequence =
be32(block.data() + 8);
323 if (transaction_open && sequence != pending_sequence && (!pending_data.empty() || !pending_revokes.empty())) {
326 if (!transaction_open) {
327 pending_data.clear();
328 pending_revokes.clear();
330 pending_tags.clear();
331 pending_sequence = sequence;
332 transaction_open =
true;
334 while (pos + 8 <= block.size()) {
335 const uint32_t target_block =
be32(block.data() + pos + 0);
336 const uint32_t flags =
be32(block.data() + pos + 4);
337 if (target_block == 0)
break;
338 pending_tags.push_back({target_block, flags});
340 if ((flags & 0x8u) != 0)
break;
342 }
else if (type == 2u) {
343 if (transaction_open && sequence == pending_sequence) apply_transaction();
344 }
else if (type == 5u) {
345 if (!transaction_open || sequence != pending_sequence) {
346 pending_data.clear();
347 pending_revokes.clear();
348 pending_tags.clear();
349 pending_sequence = sequence;
350 transaction_open =
true;
352 for (
size_t pos = 12; pos + 4 <= block.size(); pos += 4) {
353 const uint32_t revoked_block =
be32(block.data() + pos);
354 if (revoked_block == 0)
break;
355 pending_revokes.insert(revoked_block);
358 }
else if (transaction_open && !pending_tags.empty()) {
359 const auto [target_block, _flags] = pending_tags.front();
360 pending_tags.erase(pending_tags.begin());
361 pending_data[target_block] = block;
385 std::vector<unsigned char> gd;
387 uint64_t inode_bitmap_block =
le32(gd.data() + 4);
389 inode_bitmap_block |=
static_cast<uint64_t
>(
le32(gd.data() + 0x24)) << 32;
391 if (inode_bitmap_block == 0)
return false;
392 std::vector<unsigned char> bitmap(
static_cast<size_t>(
block_size_));
393 if (!
read_fs_block(inode_bitmap_block, bitmap.data(), bitmap.size()))
return false;
394 const uint32_t byte_index = index / 8;
395 const uint32_t bit_index = index % 8;
396 if (byte_index >= bitmap.size())
return false;
397 return (bitmap[byte_index] &
static_cast<unsigned char>(1u << bit_index)) != 0;
402 error =
trs(
"Invalid ext4 inode number");
407 std::vector<unsigned char> gd;
409 error =
trs(
"Failed to read ext4 group descriptor");
412 uint64_t inode_table_block =
le32(gd.data() + 8);
414 inode_table_block |=
static_cast<uint64_t
>(
le32(gd.data() + 40)) << 32;
416 if (inode_table_block == 0) {
417 error =
trs(
"Invalid ext4 inode table pointer");
420 const uint64_t inode_rel_offset = inode_table_block *
block_size_ +
static_cast<uint64_t
>(index) *
inode_size_;
423 error =
trs(
"Failed to read ext4 inode");
426 inode.
mode =
le16(raw.data() + 0x0);
427 inode.
uid =
le16(raw.data() + 0x2);
433 inode.
gid =
le16(raw.data() + 0x18);
437 std::copy(raw.begin() + 0x28, raw.begin() + 0x28 + 60, inode.
block.begin());
454 inode.
extra_bytes.assign(raw.begin() + 128, raw.end());
501 std::vector<unsigned char>& out)
const {
504 const size_t xattr_start_in_extra = inode.
extra_isize;
505 if (xattr_start_in_extra + 4 > inode.
extra_bytes.size())
return false;
506 const unsigned char* xattr_area = inode.
extra_bytes.data() + xattr_start_in_extra;
507 const size_t xattr_area_len = inode.
extra_bytes.size() - xattr_start_in_extra;
509 if (xattr_area[0] != 0xEA || xattr_area[1] != 0x02 ||
510 xattr_area[2] != 0x00 || xattr_area[3] != 0x00)
return false;
512 size_t entry_off = 4;
513 while (entry_off + 16 <= xattr_area_len) {
514 const unsigned char* e = xattr_area + entry_off;
515 const uint8_t e_name_len = e[0];
516 const uint8_t e_name_index = e[1];
517 const uint16_t e_value_offs =
le16(e + 2);
518 const uint32_t e_value_inum =
le32(e + 4);
519 const uint32_t e_value_size =
le32(e + 8);
521 if (e_name_len == 0 && e_name_index == 0 &&
522 e_value_offs == 0 && e_value_inum == 0 &&
523 e_value_size == 0)
break;
525 const size_t name_padded = (e_name_len + 3u) & ~3u;
526 if (entry_off + 16 + name_padded > xattr_area_len)
break;
527 const char* name =
reinterpret_cast<const char*
>(e + 16);
529 const bool is_system_data =
530 e_name_index == 7 && e_name_len == 4 &&
531 std::string(name, 4) ==
"data";
532 if (is_system_data && e_value_inum == 0) {
535 if (e_value_offs + e_value_size <= xattr_area_len) {
536 out.assign(xattr_area + e_value_offs,
537 xattr_area + e_value_offs + e_value_size);
542 entry_off += 16 + name_padded;
548 std::vector<ExtentRef>& extents,
549 std::string& error) {
550 std::vector<unsigned char> block(
static_cast<size_t>(
block_size_));
551 if (!
read_fs_block(node_phys_block, block.data(), block.size())) {
552 error =
trs(
"Failed to read ext4 extent node");
555 const uint16_t magic =
le16(block.data() + 0);
556 const uint16_t entries =
le16(block.data() + 2);
557 const uint16_t node_depth =
le16(block.data() + 6);
558 if (magic != 0xF30A) {
559 error =
trs(
"Invalid ext4 extent header");
562 const unsigned char* p = block.data() + 12;
563 if (node_depth == 0) {
564 for (uint16_t i = 0; i < entries; ++i, p += 12) {
565 const uint32_t ee_block =
le32(p + 0);
566 const uint16_t ee_len =
le16(p + 4);
567 const uint16_t ee_start_hi =
le16(p + 6);
568 const uint32_t ee_start_lo =
le32(p + 8);
569 const bool uninit = (ee_len & 0x8000u) != 0;
570 const uint32_t count = uninit ? (ee_len & 0x7FFFu) : ee_len;
571 if (count == 0)
continue;
573 ref.
physical_block = (
static_cast<uint64_t
>(ee_start_hi) << 32) | ee_start_lo;
577 extents.push_back(ref);
581 for (uint16_t i = 0; i < entries; ++i, p += 12) {
582 const uint32_t ei_leaf_lo =
le32(p + 4);
583 const uint16_t ei_leaf_hi =
le16(p + 8);
584 const uint64_t child = (
static_cast<uint64_t
>(ei_leaf_hi) << 32) | ei_leaf_lo;
646 if (needed_blocks == 0)
return true;
648 std::array<uint32_t, 15> ptrs{};
649 for (
size_t i = 0; i < ptrs.size(); ++i) ptrs[i] =
le32(inode.
block.data() + i * 4);
651 for (
size_t i = 0; i < 12 && blocks.size() < needed_blocks; ++i) {
652 if (ptrs[i] != 0) blocks.push_back(ptrs[i]);
655 std::vector<uint32_t> level1;
656 std::vector<uint32_t> level2;
657 std::vector<uint32_t> level3;
659 auto append_ptrs = [&](
const std::vector<uint32_t>& src) {
660 for (uint32_t block : src) {
661 if (blocks.size() >= needed_blocks)
break;
662 if (block != 0) blocks.push_back(block);
666 if (blocks.size() < needed_blocks && ptrs[12] != 0) {
670 if (blocks.size() < needed_blocks && ptrs[13] != 0) {
672 for (uint32_t indirect : level1) {
673 if (indirect == 0 || blocks.size() >= needed_blocks)
continue;
678 if (blocks.size() < needed_blocks && ptrs[14] != 0) {
680 for (uint32_t doubly : level1) {
681 if (doubly == 0 || blocks.size() >= needed_blocks)
continue;
683 for (uint32_t indirect : level2) {
684 if (indirect == 0 || blocks.size() >= needed_blocks)
continue;
691 if (blocks.size() > needed_blocks) blocks.resize(
static_cast<size_t>(needed_blocks));
753 const std::string& name,
755 const fs::path& out_dir,
756 std::vector<RecoveredFileRecord>& recovered,
757 std::vector<std::string>& logs,
758 std::string& error) {
761 if (size_bytes == 0) {
762 fs::create_directories(out_dir);
764 std::ofstream(out_path, std::ios::binary);
765 recovered.push_back({out_path.string(),
"ext4", name, 0,
"empty",
"zero-length file"});
773 constexpr uint32_t EXT4_INLINE_DATA_FL = 0x10000000u;
774 if (inode.
flags & EXT4_INLINE_DATA_FL) {
775 fs::create_directories(out_dir);
777 std::string unique = out_path.string();
779 while (fs::exists(unique)) {
781 std::to_string(suffix++))).
string();
785 constexpr uint64_t inline_iblock_bytes = 60;
787 std::ofstream out(out_path, std::ios::binary | std::ios::trunc);
789 error =
trs(
"Cannot create ext4 file");
794 const uint64_t first_chunk = std::min<uint64_t>(size_bytes, inline_iblock_bytes);
795 out.write(
reinterpret_cast<const char*
>(inode.
block.data()),
796 static_cast<std::streamsize
>(first_chunk));
797 uint64_t written = first_chunk;
800 if (size_bytes > inline_iblock_bytes) {
801 std::vector<unsigned char> xattr_data;
803 const uint64_t need = size_bytes - inline_iblock_bytes;
804 const uint64_t can = std::min<uint64_t>(need, xattr_data.size());
805 out.write(
reinterpret_cast<const char*
>(xattr_data.data()),
806 static_cast<std::streamsize
>(can));
808 if (written == size_bytes) {
809 recovered.push_back({out_path.string(),
"ext4", name,
811 "inline-data+xattr-overflow"});
812 logs.push_back(
trs(
"Recovered ext4 inline-data file with "
814 std::string(
" ") + out_path.string());
816 logs.push_back(
trs(
"ext4 inline_data file truncated "
817 "(xattr 'system.data' too short):") +
818 std::string(
" ") + name);
819 recovered.push_back({out_path.string(),
"ext4", name,
821 "inline-data+xattr-partial"});
824 logs.push_back(
trs(
"ext4 inline_data file truncated to 60 bytes "
825 "(xattr 'system.data' not found):") +
826 std::string(
" ") + name);
827 recovered.push_back({out_path.string(),
"ext4", name,
828 written,
"partial",
"inline-data-60B"});
831 recovered.push_back({out_path.string(),
"ext4", name,
832 written,
"full",
"inline-data"});
833 logs.push_back(
trs(
"Recovered ext4 inline-data file:") +
834 std::string(
" ") + out_path.string());
839 std::vector<ExportRange> ranges;
841 logs.push_back(
trs(
"Skipped ext4 file without supported data mapping") + std::string(
": inode ") + std::to_string(inode_no));
846 fs::create_directories(out_dir);
848 std::string unique = out_path.string();
850 while (fs::exists(unique)) {
857 }, ranges, out_path, size_bytes, error))
return false;
858 recovered.push_back({out_path.string(),
"ext4", name, size_bytes,
"full",
"filesystem export"});
859 logs.push_back(
trs(
"Recovered ext4 file:") + std::string(
" ") + out_path.string());
888 uint32_t target_block,
890 std::vector<JournalDirentCandidate>& out) {
892 while (pos + 8 <= block.size()) {
893 const uint32_t inode =
le32(block.data() + pos + 0);
894 const uint16_t rec_len =
le16(block.data() + pos + 4);
895 const uint8_t name_len = block[pos + 6];
896 const uint8_t file_type = block[pos + 7];
897 if (rec_len < 8 || rec_len > block.size() - pos) {
901 if (inode != 0 && name_len > 0 && 8u + name_len <= rec_len &&
looks_like_printable_name(block.data() + pos + 8, name_len)) {
902 std::string name(
reinterpret_cast<const char*
>(block.data() + pos + 8), name_len);
903 if (name !=
"." && name !=
"..") {
904 out.push_back({inode, rec_len, file_type, name, target_block, sequence});
912 std::vector<RecoveredFileRecord>& recovered,
913 std::vector<std::string>& logs) {
917 std::string local_error;
919 logs.push_back(
trs(
"ext4 journal inode could not be read"));
923 std::vector<ExportRange> ranges;
924 if (!
inode_ranges(journal_inode, ranges, local_error)) {
925 logs.push_back(
trs(
"ext4 journal mapping unsupported or unreadable"));
929 const fs::path journal_root = out_root /
"journal";
930 fs::create_directories(journal_root);
932 const fs::path journal_copy = journal_root /
"journal_copy.bin";
935 }, ranges, journal_copy, journal_size, local_error)) {
936 logs.push_back(
trs(
"ext4 journal export failed") + std::string(
": ") + local_error);
939 recovered.push_back({journal_copy.string(),
"ext4",
"journal/journal_copy.bin", journal_size,
"metadata",
"journal copy"});
941 std::ofstream info(journal_root /
"journal_info.txt", std::ios::binary);
944 std::map<uint32_t, size_t> type_counts;
945 std::vector<unsigned char> block(
static_cast<size_t>(
block_size_));
946 uint64_t remaining_total = journal_size;
947 size_t metadata_blocks_exported = 0;
948 size_t payload_blocks_exported = 0;
949 const fs::path replay_dir = journal_root /
"replayed_metadata";
950 fs::create_directories(replay_dir);
952 std::vector<std::pair<uint32_t, uint32_t>> pending_tags;
953 std::vector<JournalDirentCandidate> journal_dirents;
954 uint32_t pending_sequence = 0;
955 uint64_t block_index = 0;
956 for (
const auto& r : ranges) {
957 const uint64_t offset = r.offset;
958 const uint64_t length = r.length;
959 uint64_t cursor = offset;
960 uint64_t remaining = std::min<uint64_t>(length, remaining_total);
963 const uint32_t magic =
be32(block.data());
964 if (magic == 0xC03B3998u) {
965 const uint32_t type =
be32(block.data() + 4);
966 const uint32_t sequence =
be32(block.data() + 8);
968 info <<
"block=" << block_index <<
" type=" << type <<
" sequence=" << sequence <<
"\n";
969 pending_tags.clear();
970 pending_sequence = sequence;
973 while (pos + 8 <= block.size()) {
974 const uint32_t target_block =
be32(block.data() + pos + 0);
975 const uint32_t flags =
be32(block.data() + pos + 4);
976 if (target_block == 0)
break;
977 pending_tags.push_back({target_block, flags});
978 info <<
"descriptor[" << block_index <<
"].tag_target=" << target_block
979 <<
" flags=" << flags <<
"\n";
981 if ((flags & 0x8u) != 0)
break;
983 }
else if (type == 5u) {
984 const fs::path block_file = replay_dir / (
"journal_block_" + std::to_string(block_index) +
"_revoke.bin");
985 std::ofstream bout(block_file, std::ios::binary);
987 bout.write(
reinterpret_cast<const char*
>(block.data()),
static_cast<std::streamsize
>(block.size()));
988 ++metadata_blocks_exported;
990 for (
size_t pos = 12; pos + 4 <= block.size(); pos += 4) {
991 const uint32_t revoked_block =
be32(block.data() + pos);
992 if (revoked_block == 0)
break;
993 info <<
"revoke[" << block_index <<
"].target_block=" << revoked_block <<
" sequence=" << sequence <<
"\n";
996 }
else if (!pending_tags.empty()) {
997 const auto [target_block, flags] = pending_tags.front();
998 pending_tags.erase(pending_tags.begin());
999 const fs::path block_file = replay_dir /
1000 (
"journal_payload_" + std::to_string(block_index) +
1001 "_target_" + std::to_string(target_block) +
1002 "_seq_" + std::to_string(pending_sequence) +
".bin");
1003 std::ofstream bout(block_file, std::ios::binary);
1005 bout.write(
reinterpret_cast<const char*
>(block.data()),
static_cast<std::streamsize
>(block.size()));
1006 ++payload_blocks_exported;
1008 info <<
"payload[" << block_index <<
"].target_block=" << target_block
1009 <<
" flags=" << flags <<
" sequence=" << pending_sequence <<
"\n";
1022 info <<
"journal_size=" << journal_size <<
"\n";
1023 for (
const auto& [type, count] : type_counts) {
1024 info <<
"type_" << type <<
"=" << count <<
"\n";
1026 info <<
"metadata_blocks_exported=" << metadata_blocks_exported <<
"\n";
1027 info <<
"payload_blocks_exported=" << payload_blocks_exported <<
"\n";
1029 if (!journal_dirents.empty()) {
1030 std::ofstream dirent_out(journal_root /
"recovered_dirents.txt", std::ios::binary);
1031 std::set<uint32_t> exported_candidate_inodes;
1032 const fs::path candidate_dir = journal_root /
"candidate_files";
1033 fs::create_directories(candidate_dir);
1034 for (
const auto& d : journal_dirents) {
1036 <<
" name=" << d.name <<
" target_block=" << d.target_block
1037 <<
" sequence=" << d.sequence <<
"\n";
1038 if (d.file_type == 1 && exported_candidate_inodes.insert(d.inode).second) {
1041 std::string cand_error;
1047 info <<
"dirent_candidates=" << journal_dirents.size() <<
"\n";
1049 info <<
"dirent_candidates=0\n";
1051 logs.push_back(
trs(
"ext4 journal exported") + std::string(
": ") + journal_copy.string());
1055 const fs::path& out_dir,
1056 std::vector<RecoveredFileRecord>& recovered,
1057 std::vector<std::string>& logs,
1059 std::set<uint32_t>& dir_guard,
1061 if (depth > 64)
return true;
1062 if (!dir_guard.insert(inode_no).second)
return true;
1065 if (!
read_inode(inode_no, inode, error))
return false;
1068 std::vector<ExportRange> ranges;
1070 logs.push_back(
trs(
"Skipped ext4 directory without supported data mapping") + std::string(
": inode ") + std::to_string(inode_no));
1075 std::vector<unsigned char> block(
static_cast<size_t>(
block_size_));
1076 for (
const auto& r : ranges) {
1077 const uint64_t offset = r.offset;
1078 const uint64_t length = r.length;
1079 uint64_t cursor = offset;
1080 uint64_t remaining = length;
1084 while (pos + 8 <= block.size()) {
1085 const uint32_t entry_inode =
le32(block.data() + pos + 0);
1086 const uint16_t rec_len =
le16(block.data() + pos + 4);
1087 const uint8_t name_len = block[pos + 6];
1088 const uint8_t file_type = block[pos + 7];
1089 if (rec_len < 8 || pos + rec_len > block.size())
break;
1090 if (entry_inode == 0) { pos += rec_len;
continue; }
1091 std::string name(
reinterpret_cast<const char*
>(block.data() + pos + 8),
1092 reinterpret_cast<const char*
>(block.data() + pos + 8 + std::min<uint16_t>(name_len, rec_len - 8)));
1094 if (name.empty() || name ==
"." || name ==
"..") {
1098 if (file_type == 2) {
1099 fs::path subdir = out_dir / name;
1100 fs::create_directories(subdir);
1101 if (!
recover_directory(entry_inode, subdir, recovered, logs, error, dir_guard, depth + 1))
return false;
1102 }
else if (file_type == 1 || file_type == 7 || file_type == 0) {
1105 if (!
export_file(entry_inode, name, child, out_dir, recovered, logs, error))
return false;