Skip to content
API Documentation
BastionGuard™
ext4_recovery.cpp
Go to the documentation of this file.
1#include "ext4_recovery.hpp"
2#include "i18n.hpp"
3#include "text_utils.hpp"
4
5#include <algorithm>
6#include <array>
7#include <cctype>
8#include <cstdint>
9#include <filesystem>
10#include <fstream>
11#include <map>
12#include <set>
13#include <string>
14#include <vector>
15
16namespace fs = std::filesystem;
17
18namespace {
19uint16_t le16(const unsigned char* p) {
20 return static_cast<uint16_t>(p[0]) | (static_cast<uint16_t>(p[1]) << 8);
21}
22
23uint32_t le32(const unsigned char* p) {
24 return static_cast<uint32_t>(p[0]) |
25 (static_cast<uint32_t>(p[1]) << 8) |
26 (static_cast<uint32_t>(p[2]) << 16) |
27 (static_cast<uint32_t>(p[3]) << 24);
28}
29
30uint64_t le64(const unsigned char* p) {
31 return static_cast<uint64_t>(p[0]) |
32 (static_cast<uint64_t>(p[1]) << 8) |
33 (static_cast<uint64_t>(p[2]) << 16) |
34 (static_cast<uint64_t>(p[3]) << 24) |
35 (static_cast<uint64_t>(p[4]) << 32) |
36 (static_cast<uint64_t>(p[5]) << 40) |
37 (static_cast<uint64_t>(p[6]) << 48) |
38 (static_cast<uint64_t>(p[7]) << 56);
39}
40uint32_t be32(const unsigned char* p) {
41 return (static_cast<uint32_t>(p[0]) << 24) |
42 (static_cast<uint32_t>(p[1]) << 16) |
43 (static_cast<uint32_t>(p[2]) << 8) |
44 static_cast<uint32_t>(p[3]);
45}
46
47
48// Bug #15, #18 fix: replaced the broken ASCII-only sanitize with a
49// shared UTF-8-aware implementation that also handles all Windows-illegal
50// characters consistently. Italian/European filenames are now preserved.
51inline std::string sanitize_path_component(const std::string& in) {
53}
54
55// Range descriptor for write_ranges_to_file. Each entry covers
56// `length` bytes of OUTPUT FILE CONTENT. If `zero_fill` is true,
57// the bytes come from a synthesized zero buffer (= sparse hole).
58// If false, bytes are read from `offset` on the disk.
60 uint64_t offset {0};
61 uint64_t length {0};
62 bool zero_fill {false};
63};
64
65template <typename ReadFn>
66bool write_ranges_to_file(ReadFn&& read_fn,
67 const std::vector<ExportRange>& ranges,
68 const fs::path& out_path,
69 uint64_t expected_size,
70 std::string& error) {
71 std::ofstream out(out_path, std::ios::binary);
72 if (!out) {
73 error = trs("Unable to create output file") + std::string(": ") + out_path.string();
74 return false;
75 }
76 std::vector<unsigned char> buffer(1024 * 1024);
77 uint64_t written = 0;
78 for (const auto& r : ranges) {
79 uint64_t remaining = r.length;
80 uint64_t cursor = r.offset;
81 while (remaining > 0 && written < expected_size) {
82 const uint64_t allowed = std::min<uint64_t>(remaining, expected_size - written);
83 const size_t chunk = static_cast<size_t>(std::min<uint64_t>(buffer.size(), allowed));
84 if (r.zero_fill) {
85 std::fill(buffer.begin(), buffer.begin() + chunk, 0u);
86 } else if (!read_fn(cursor, buffer.data(), chunk)) {
87 error = trs("Read error while exporting recovered file");
88 return false;
89 }
90 out.write(reinterpret_cast<const char*>(buffer.data()), static_cast<std::streamsize>(chunk));
91 if (!out) {
92 error = trs("Write error while exporting recovered file");
93 return false;
94 }
95 cursor += chunk;
96 remaining -= chunk;
97 written += chunk;
98 }
99 if (written >= expected_size) break;
100 }
101 return true;
102}
103
104struct Ext4Inode {
105 uint16_t mode {0};
106 uint16_t uid {0};
107 uint32_t size_lo {0};
108 uint32_t atime {0};
109 uint32_t ctime {0};
110 uint32_t mtime {0};
111 uint32_t dtime {0};
112 uint16_t gid {0};
113 uint16_t links_count {0};
114 uint32_t blocks_lo {0};
115 uint32_t flags {0};
116 std::array<unsigned char, 60> block{};
117 uint32_t generation {0};
118 uint32_t file_acl_lo {0};
119 uint32_t size_high {0};
120 // Bug #19 fix: allocation flag from inode bitmap. Caller decides what to do.
121 bool allocated {true};
122 // For ext4_inline_data overflow: bytes of the inode AFTER the
123 // GOOD_OLD_INODE_SIZE (= 128) area. This is where in-inode
124 // extended attributes live, including the 'system.data' xattr that
125 // holds inline_data overflow beyond the first 60 bytes. Empty if
126 // inode_size_ <= 128 (no in-inode xattrs possible).
127 std::vector<unsigned char> extra_bytes;
128 // i_extra_isize at offset 0x80: how much of the post-128 area is
129 // extended inode core fields (vs xattr space).
130 uint16_t extra_isize {0};
131};
132
133struct ExtentRef {
134 uint64_t physical_block {0};
135 uint64_t logical_block {0};
136 uint32_t block_count {0};
137 // ext4 marks extents whose data has been ALLOCATED but never written
138 // (i.e. sparse holes within the file's logical address space) by
139 // setting bit 15 of ee_len. When reading these blocks, the kernel
140 // returns zeros instead of whatever raw bytes happen to be at the
141 // physical location. We must do the same: if uninitialized, write
142 // a zero range rather than reading the disk content.
143 bool uninitialized {false};
144};
145
147 uint32_t inode {0};
148 uint16_t rec_len {0};
149 uint8_t file_type {0};
150 std::string name;
151 uint32_t target_block {0};
152 uint32_t sequence {0};
153};
154
156public:
157 explicit Ext4RecoveryImpl(DiskReader& reader) : reader_(reader) {}
158
159 bool recover(uint64_t volume_offset,
160 const std::string& output_dir,
161 std::vector<RecoveredFileRecord>& recovered,
162 std::vector<std::string>& logs,
163 std::string& error) {
164 volume_offset_ = volume_offset;
165 if (!load_superblock(error)) return false;
166
167 fs::path out_root = fs::path(output_dir) / "filesystem_recovered" / "ext4" / ("offset_" + std::to_string(volume_offset));
168 fs::create_directories(out_root);
169
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_));
173
174 std::set<uint32_t> dir_guard;
175 if (!recover_directory(2, out_root, recovered, logs, error, dir_guard, 0)) {
176 return false;
177 }
178
179 export_journal(out_root, recovered, logs);
180 logs.push_back(trs("ext4 recovery completed."));
181 return true;
182 }
183
184private:
186 uint64_t volume_offset_ {0};
187 uint64_t block_size_ {0};
188 uint32_t blocks_per_group_ {0};
189 uint32_t inodes_per_group_ {0};
190 uint16_t inode_size_ {0};
191 uint64_t desc_table_offset_ {0};
192 uint16_t desc_size_ {32};
193 uint32_t first_data_block_ {0};
194 uint64_t total_inodes_ {0};
195 uint32_t journal_inode_ {0};
196 std::map<uint64_t, std::pair<uint32_t, std::vector<unsigned char>>> journal_replay_blocks_;
197 std::set<uint64_t> journal_revoked_blocks_;
199 size_t revoke_records_ {0};
200
201 bool load_superblock(std::string& error) {
202 std::array<unsigned char, 1024> super{};
203 if (!reader_.read_at(volume_offset_ + 1024, super.data(), super.size())) {
204 error = trs("Failed to read ext4 superblock");
205 return false;
206 }
207 if (le16(super.data() + 0x38) != 0xEF53) {
208 error = trs("Invalid ext4 superblock magic");
209 return false;
210 }
211
212 const uint32_t log_block_size = le32(super.data() + 0x18);
213 block_size_ = 1024ull << log_block_size;
214 first_data_block_ = le32(super.data() + 0x14);
215 blocks_per_group_ = le32(super.data() + 0x20);
216 inodes_per_group_ = le32(super.data() + 0x28);
217 inode_size_ = le16(super.data() + 0x58);
218 desc_size_ = le16(super.data() + 0xFE);
219 total_inodes_ = le32(super.data() + 0x00);
220 journal_inode_ = le32(super.data() + 0xE0);
221
222 if (inode_size_ < 128) inode_size_ = 128;
223 if (desc_size_ < 32) desc_size_ = 32;
224 if (blocks_per_group_ == 0 || inodes_per_group_ == 0 || block_size_ < 1024) {
225 error = trs("Unsupported ext4 geometry");
226 return false;
227 }
228
229 const uint64_t desc_block = (block_size_ == 1024) ? 2 : 1;
231 return true;
232 }
233
234 uint64_t block_to_offset(uint64_t block) const {
235 return volume_offset_ + block * block_size_;
236 }
237
238 bool read_fs_block(uint64_t block_no, unsigned char* buffer, size_t len) {
239 if (len > block_size_) return false;
240 if (journal_revoked_blocks_.find(block_no) == journal_revoked_blocks_.end()) {
241 auto it = journal_replay_blocks_.find(block_no);
242 if (it != journal_replay_blocks_.end() && it->second.second.size() >= len) {
243 std::copy_n(it->second.second.data(), len, buffer);
244 return true;
245 }
246 }
247 return reader_.read_at(block_to_offset(block_no), buffer, len);
248 }
249
250 bool read_volume_bytes_replayed(uint64_t volume_rel_offset, unsigned char* buffer, size_t len) {
251 if (len == 0) return true;
252 uint64_t pos = volume_rel_offset;
253 size_t done = 0;
254 while (done < len) {
255 const uint64_t block_no = pos / block_size_;
256 const size_t within = static_cast<size_t>(pos % block_size_);
257 const size_t take = std::min<size_t>(len - done, static_cast<size_t>(block_size_ - within));
258 std::vector<unsigned char> tmp(static_cast<size_t>(block_size_));
259 if (!read_fs_block(block_no, tmp.data(), tmp.size())) return false;
260 std::copy_n(tmp.data() + within, take, buffer + done);
261 pos += take;
262 done += take;
263 }
264 return true;
265 }
266
267 void build_journal_replay_map(std::vector<std::string>& logs) {
271 revoke_records_ = 0;
272 if (journal_inode_ == 0) return;
273 Ext4Inode journal_inode{};
274 std::string local_error;
275 if (!read_inode(journal_inode_, journal_inode, local_error)) return;
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_));
279 uint64_t remaining_total = inode_size_bytes(journal_inode);
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;
285
286 auto apply_transaction = [&]() {
287 if (!transaction_open) return;
288 for (uint32_t blk : pending_revokes) {
289 journal_revoked_blocks_.insert(blk);
290 journal_replay_blocks_.erase(blk);
291 }
292 for (const auto& [target_block, payload] : pending_data) {
293 if (pending_revokes.find(target_block) != pending_revokes.end()) continue;
294 auto& slot = journal_replay_blocks_[target_block];
295 if (slot.second.empty() || pending_sequence >= slot.first) {
296 slot.first = pending_sequence;
297 slot.second = payload;
298 }
299 journal_revoked_blocks_.erase(target_block);
300 }
301 if (!pending_data.empty() || !pending_revokes.empty()) ++committed_transactions_;
302 revoke_records_ += pending_revokes.size();
303 pending_tags.clear();
304 pending_data.clear();
305 pending_revokes.clear();
306 transaction_open = false;
307 pending_sequence = 0;
308 };
309
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);
315 while (remaining >= block_size_) {
316 const uint64_t fs_block = (cursor - volume_offset_) / block_size_;
317 if (!reader_.read_at(block_to_offset(fs_block), block.data(), block.size())) break;
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);
322 if (type == 1u) {
323 if (transaction_open && sequence != pending_sequence && (!pending_data.empty() || !pending_revokes.empty())) {
324 apply_transaction();
325 }
326 if (!transaction_open) {
327 pending_data.clear();
328 pending_revokes.clear();
329 }
330 pending_tags.clear();
331 pending_sequence = sequence;
332 transaction_open = true;
333 size_t pos = 12;
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});
339 pos += 8;
340 if ((flags & 0x8u) != 0) break;
341 }
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;
351 }
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);
356 }
357 }
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;
362 }
363 cursor += block_size_;
364 remaining -= block_size_;
365 remaining_total = remaining_total >= block_size_ ? remaining_total - block_size_ : 0;
366 if (remaining_total < block_size_) break;
367 }
368 if (remaining_total < block_size_) break;
369 }
370 if (!journal_replay_blocks_.empty()) logs.push_back(trs("ext4 journal replay overlay blocks") + std::string(": ") + std::to_string(journal_replay_blocks_.size()));
371 if (committed_transactions_ > 0) logs.push_back(trs("ext4 committed journal transactions") + std::string(": ") + std::to_string(committed_transactions_));
372 if (revoke_records_ > 0) logs.push_back(trs("ext4 journal revoke records") + std::string(": ") + std::to_string(revoke_records_));
373 }
374
375 bool read_group_desc(uint32_t group, std::vector<unsigned char>& out) {
376 out.resize(desc_size_);
377 const uint64_t rel = desc_table_offset_ - volume_offset_ + static_cast<uint64_t>(group) * desc_size_;
378 return read_volume_bytes_replayed(rel, out.data(), out.size());
379 }
380
381 bool inode_marked_allocated(uint32_t inode_no) {
382 if (inode_no == 0 || inode_no > total_inodes_) return false;
383 const uint32_t group = (inode_no - 1) / inodes_per_group_;
384 const uint32_t index = (inode_no - 1) % inodes_per_group_;
385 std::vector<unsigned char> gd;
386 if (!read_group_desc(group, gd) || gd.size() < 12) return false;
387 uint64_t inode_bitmap_block = le32(gd.data() + 4);
388 if (desc_size_ >= 64 && gd.size() >= 40) {
389 inode_bitmap_block |= static_cast<uint64_t>(le32(gd.data() + 0x24)) << 32;
390 }
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;
398 }
399
400 bool read_inode(uint32_t inode_no, Ext4Inode& inode, std::string& error) {
401 if (inode_no == 0 || inode_no > total_inodes_) {
402 error = trs("Invalid ext4 inode number");
403 return false;
404 }
405 const uint32_t group = (inode_no - 1) / inodes_per_group_;
406 const uint32_t index = (inode_no - 1) % inodes_per_group_;
407 std::vector<unsigned char> gd;
408 if (!read_group_desc(group, gd)) {
409 error = trs("Failed to read ext4 group descriptor");
410 return false;
411 }
412 uint64_t inode_table_block = le32(gd.data() + 8);
413 if (desc_size_ >= 64) {
414 inode_table_block |= static_cast<uint64_t>(le32(gd.data() + 40)) << 32;
415 }
416 if (inode_table_block == 0) {
417 error = trs("Invalid ext4 inode table pointer");
418 return false;
419 }
420 const uint64_t inode_rel_offset = inode_table_block * block_size_ + static_cast<uint64_t>(index) * inode_size_;
421 std::vector<unsigned char> raw(inode_size_);
422 if (!read_volume_bytes_replayed(inode_rel_offset, raw.data(), raw.size())) {
423 error = trs("Failed to read ext4 inode");
424 return false;
425 }
426 inode.mode = le16(raw.data() + 0x0);
427 inode.uid = le16(raw.data() + 0x2);
428 inode.size_lo = le32(raw.data() + 0x4);
429 inode.atime = le32(raw.data() + 0x8);
430 inode.ctime = le32(raw.data() + 0xC);
431 inode.mtime = le32(raw.data() + 0x10);
432 inode.dtime = le32(raw.data() + 0x14);
433 inode.gid = le16(raw.data() + 0x18);
434 inode.links_count = le16(raw.data() + 0x1A);
435 inode.blocks_lo = le32(raw.data() + 0x1C);
436 inode.flags = le32(raw.data() + 0x20);
437 std::copy(raw.begin() + 0x28, raw.begin() + 0x28 + 60, inode.block.begin());
438 inode.generation = le32(raw.data() + 0x64);
439 inode.file_acl_lo = le32(raw.data() + 0x68);
440 inode.size_high = le32(raw.data() + 0x6C);
441 // i_extra_isize at offset 0x80: tells us how much of the
442 // post-128 area is reserved for extended inode core (vs
443 // available for in-inode xattrs).
444 if (inode_size_ >= 0x82) {
445 inode.extra_isize = le16(raw.data() + 0x80);
446 // Sanity-bound to inode_size - 128 - 4 (we need at least the
447 // xattr magic to fit)
448 if (inode.extra_isize > inode_size_ - 132) {
449 inode.extra_isize = 0;
450 }
451 }
452 // Save the post-128 region for xattr parsing
453 if (inode_size_ > 128) {
454 inode.extra_bytes.assign(raw.begin() + 128, raw.end());
455 }
456 // Bug #19 fix: track allocation status as a flag instead of rejecting.
457 // Recovery of DELETED files MUST read unallocated inodes (dtime != 0).
458 // The previous code aborted on exactly that case, making deleted-file
459 // recovery silently impossible on ext4. Now we report the status and
460 // let the caller decide.
461 inode.allocated = inode_marked_allocated(inode_no);
462 return true;
463 }
464
465 uint64_t inode_size_bytes(const Ext4Inode& inode) const {
466 // Bug #12 fix: size_high (i_size_high) is valid for any file/dir when the
467 // huge_file feature is on. The previous code restricted it to regular
468 // files only (mode == 0x8000), which gave wrong sizes for large dirs and
469 // for symlinks/special files >4GB on filesystems with that feature.
470 const uint64_t high = static_cast<uint64_t>(inode.size_high) << 32;
471 return high | inode.size_lo;
472 }
473
474 bool inode_is_dir(const Ext4Inode& inode) const { return (inode.mode & 0xF000) == 0x4000; }
475 bool inode_is_reg(const Ext4Inode& inode) const { return (inode.mode & 0xF000) == 0x8000; }
476 bool inode_uses_extents(const Ext4Inode& inode) const { return (inode.flags & 0x00080000u) != 0; }
477
478 // Find the value of the in-inode xattr "system.data" (used by ext4
479 // inline_data feature for the overflow beyond the first 60 bytes
480 // stored in i_block[]).
481 //
482 // The in-inode xattr region starts at:
483 // raw_inode + 128 + i_extra_isize (= start of xattr area)
484 // Header is __le32 magic = 0xEA020000, followed by xattr entries.
485 //
486 // Each entry is 16 bytes + name (padded to 4-byte boundary):
487 // __u8 e_name_len
488 // __u8 e_name_index
489 // __le16 e_value_offs (offset within xattr area, AFTER the
490 // entries-list end)
491 // __le32 e_value_inum (= 0 for inline value)
492 // __le32 e_value_size
493 // __le32 e_hash
494 // char e_name[e_name_len] (NOT null-terminated)
495 //
496 // For "system.data": e_name_index = 7 (system), e_name = "data"
497 // (4 bytes).
498 //
499 // Returns true and fills `out` on success.
501 std::vector<unsigned char>& out) const {
502 out.clear();
503 if (inode.extra_bytes.empty()) return false;
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;
508 // Magic check: 0xEA020000 little-endian = bytes EA 02 00 00
509 if (xattr_area[0] != 0xEA || xattr_area[1] != 0x02 ||
510 xattr_area[2] != 0x00 || xattr_area[3] != 0x00) return false;
511 // Entries start after the 4-byte magic
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);
520 // Zero-name terminator
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;
524 // Name padded to 4-byte boundary
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);
528 // Match "system.data": index 7, name "data"
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) {
533 // Value is at e_value_offs from the START of the xattr
534 // area (= where the magic is)
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);
538 return true;
539 }
540 return false;
541 }
542 entry_off += 16 + name_padded;
543 }
544 return false;
545 }
546
547 bool collect_extents_from_node(uint64_t node_phys_block,
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");
553 return false;
554 }
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");
560 return false;
561 }
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;
572 ExtentRef ref;
573 ref.physical_block = (static_cast<uint64_t>(ee_start_hi) << 32) | ee_start_lo;
574 ref.logical_block = ee_block;
575 ref.block_count = count;
576 ref.uninitialized = uninit;
577 extents.push_back(ref);
578 }
579 return true;
580 }
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;
585 if (!collect_extents_from_node(child, extents, error)) return false;
586 }
587 return true;
588 }
589
590 bool inode_extents(const Ext4Inode& inode, std::vector<ExtentRef>& extents, std::string& error) {
591 if (!inode_uses_extents(inode)) {
592 error = trs("ext4 inode does not use extents");
593 return false;
594 }
595 const unsigned char* p = inode.block.data();
596 const uint16_t magic = le16(p + 0);
597 const uint16_t entries = le16(p + 2);
598 const uint16_t depth = le16(p + 6);
599 if (magic != 0xF30A) {
600 error = trs("Invalid ext4 inode extent header");
601 return false;
602 }
603 p += 12;
604 if (depth == 0) {
605 for (uint16_t i = 0; i < entries; ++i, p += 12) {
606 const uint32_t ee_block = le32(p + 0);
607 const uint16_t ee_len = le16(p + 4);
608 const uint16_t ee_start_hi = le16(p + 6);
609 const uint32_t ee_start_lo = le32(p + 8);
610 const bool uninit = (ee_len & 0x8000u) != 0;
611 const uint32_t count = uninit ? (ee_len & 0x7FFFu) : ee_len;
612 if (count == 0) continue;
613 ExtentRef ref;
614 ref.physical_block = (static_cast<uint64_t>(ee_start_hi) << 32) | ee_start_lo;
615 ref.logical_block = ee_block;
616 ref.block_count = count;
617 ref.uninitialized = uninit;
618 extents.push_back(ref);
619 }
620 return true;
621 }
622 for (uint16_t i = 0; i < entries; ++i, p += 12) {
623 const uint32_t ei_leaf_lo = le32(p + 4);
624 const uint16_t ei_leaf_hi = le16(p + 8);
625 const uint64_t child = (static_cast<uint64_t>(ei_leaf_hi) << 32) | ei_leaf_lo;
626 if (!collect_extents_from_node(child, extents, error)) return false;
627 }
628 return true;
629 }
630
631 bool read_u32_block(uint64_t block_no, std::vector<uint32_t>& out, std::string& error) {
632 std::vector<unsigned char> raw(static_cast<size_t>(block_size_));
633 if (!read_fs_block(block_no, raw.data(), raw.size())) {
634 error = trs("Failed to read ext4 indirect block");
635 return false;
636 }
637 out.resize(raw.size() / 4);
638 for (size_t i = 0; i < out.size(); ++i) out[i] = le32(raw.data() + i * 4);
639 return true;
640 }
641
642 bool inode_block_map_classic(const Ext4Inode& inode, std::vector<uint64_t>& blocks, std::string& error) {
643 blocks.clear();
644 const uint64_t size_bytes = inode_size_bytes(inode);
645 const uint64_t needed_blocks = (size_bytes + block_size_ - 1) / block_size_;
646 if (needed_blocks == 0) return true;
647
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);
650
651 for (size_t i = 0; i < 12 && blocks.size() < needed_blocks; ++i) {
652 if (ptrs[i] != 0) blocks.push_back(ptrs[i]);
653 }
654
655 std::vector<uint32_t> level1;
656 std::vector<uint32_t> level2;
657 std::vector<uint32_t> level3;
658
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);
663 }
664 };
665
666 if (blocks.size() < needed_blocks && ptrs[12] != 0) {
667 if (!read_u32_block(ptrs[12], level1, error)) return false;
668 append_ptrs(level1);
669 }
670 if (blocks.size() < needed_blocks && ptrs[13] != 0) {
671 if (!read_u32_block(ptrs[13], level1, error)) return false;
672 for (uint32_t indirect : level1) {
673 if (indirect == 0 || blocks.size() >= needed_blocks) continue;
674 if (!read_u32_block(indirect, level2, error)) return false;
675 append_ptrs(level2);
676 }
677 }
678 if (blocks.size() < needed_blocks && ptrs[14] != 0) {
679 if (!read_u32_block(ptrs[14], level1, error)) return false;
680 for (uint32_t doubly : level1) {
681 if (doubly == 0 || blocks.size() >= needed_blocks) continue;
682 if (!read_u32_block(doubly, level2, error)) return false;
683 for (uint32_t indirect : level2) {
684 if (indirect == 0 || blocks.size() >= needed_blocks) continue;
685 if (!read_u32_block(indirect, level3, error)) return false;
686 append_ptrs(level3);
687 }
688 }
689 }
690
691 if (blocks.size() > needed_blocks) blocks.resize(static_cast<size_t>(needed_blocks));
692 return true;
693 }
694
695 bool inode_ranges(const Ext4Inode& inode,
696 std::vector<ExportRange>& ranges,
697 std::string& error) {
698 ranges.clear();
699 if (inode_uses_extents(inode)) {
700 std::vector<ExtentRef> extents;
701 if (!inode_extents(inode, extents, error)) return false;
702 std::sort(extents.begin(), extents.end(),
703 [](const auto& a, const auto& b){
704 return a.logical_block < b.logical_block;
705 });
706 // Walk extents in logical-block order. Fill any GAP between
707 // consecutive extents (= sparse hole) with a zero-fill range
708 // sized exactly hole_blocks * block_size_. Mark
709 // UNINITIALIZED extents as zero-fill (their physical content
710 // is undefined; the kernel returns zeros).
711 uint64_t logical_cursor = 0;
712 for (const auto& e : extents) {
713 if (e.logical_block > logical_cursor) {
714 // Sparse hole between previous extent and this one
715 const uint64_t hole_blocks = e.logical_block - logical_cursor;
716 ExportRange zr;
717 zr.length = hole_blocks * block_size_;
718 zr.zero_fill = true;
719 ranges.push_back(zr);
720 }
721 ExportRange r;
722 r.length = static_cast<uint64_t>(e.block_count) * block_size_;
723 if (e.uninitialized) {
724 r.zero_fill = true;
725 } else {
726 r.offset = block_to_offset(e.physical_block);
727 r.zero_fill = false;
728 }
729 ranges.push_back(r);
730 logical_cursor = e.logical_block + e.block_count;
731 }
732 return true;
733 }
734
735 std::vector<uint64_t> blocks;
736 if (!inode_block_map_classic(inode, blocks, error)) return false;
737 // For classic block map: a zero entry means a sparse hole.
738 for (uint64_t block : blocks) {
739 ExportRange r;
740 r.length = block_size_;
741 if (block == 0) {
742 r.zero_fill = true;
743 } else {
744 r.offset = block_to_offset(block);
745 r.zero_fill = false;
746 }
747 ranges.push_back(r);
748 }
749 return true;
750 }
751
752 bool export_file(uint32_t inode_no,
753 const std::string& name,
754 const Ext4Inode& inode,
755 const fs::path& out_dir,
756 std::vector<RecoveredFileRecord>& recovered,
757 std::vector<std::string>& logs,
758 std::string& error) {
759 if (!inode_is_reg(inode)) return true;
760 const uint64_t size_bytes = inode_size_bytes(inode);
761 if (size_bytes == 0) {
762 fs::create_directories(out_dir);
763 fs::path out_path = out_dir / sanitize_path_component(name);
764 std::ofstream(out_path, std::ios::binary);
765 recovered.push_back({out_path.string(), "ext4", name, 0, "empty", "zero-length file"});
766 return true;
767 }
768
769 // ext4 inline_data: when EXT4_INLINE_DATA_FL (0x10000000) is set,
770 // the file content lives in i_block[60] for the first 60 bytes
771 // (and optionally in an xattr 'system.data' for the overflow
772 // beyond 60 bytes).
773 constexpr uint32_t EXT4_INLINE_DATA_FL = 0x10000000u;
774 if (inode.flags & EXT4_INLINE_DATA_FL) {
775 fs::create_directories(out_dir);
776 fs::path out_path = out_dir / sanitize_path_component(name);
777 std::string unique = out_path.string();
778 int suffix = 1;
779 while (fs::exists(unique)) {
780 unique = (out_dir / (sanitize_path_component(name) + "_" +
781 std::to_string(suffix++))).string();
782 }
783 out_path = unique;
784
785 constexpr uint64_t inline_iblock_bytes = 60; // bytes in i_block[]
786
787 std::ofstream out(out_path, std::ios::binary | std::ios::trunc);
788 if (!out) {
789 error = trs("Cannot create ext4 file");
790 return false;
791 }
792
793 // First 60 bytes from i_block[]
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;
798
799 // Beyond 60 bytes: look up 'system.data' xattr
800 if (size_bytes > inline_iblock_bytes) {
801 std::vector<unsigned char> xattr_data;
802 if (find_inline_xattr_system_data(inode, 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));
807 written += can;
808 if (written == size_bytes) {
809 recovered.push_back({out_path.string(), "ext4", name,
810 written, "full",
811 "inline-data+xattr-overflow"});
812 logs.push_back(trs("Recovered ext4 inline-data file with "
813 "xattr overflow:") +
814 std::string(" ") + out_path.string());
815 } else {
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,
820 written, "partial",
821 "inline-data+xattr-partial"});
822 }
823 } else {
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"});
829 }
830 } else {
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());
835 }
836 return true;
837 }
838
839 std::vector<ExportRange> ranges;
840 if (!inode_ranges(inode, ranges, error)) {
841 logs.push_back(trs("Skipped ext4 file without supported data mapping") + std::string(": inode ") + std::to_string(inode_no));
842 error.clear();
843 return true;
844 }
845
846 fs::create_directories(out_dir);
847 fs::path out_path = out_dir / sanitize_path_component(name);
848 std::string unique = out_path.string();
849 int suffix = 1;
850 while (fs::exists(unique)) {
851 unique = (out_dir / (sanitize_path_component(name) + "_" + std::to_string(suffix++))).string();
852 }
853 out_path = unique;
854
855 if (!write_ranges_to_file([&](uint64_t offset, unsigned char* buffer, size_t len) {
856 return read_volume_bytes_replayed(offset - volume_offset_, buffer, len);
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());
860 return true;
861 }
862
863
864 static bool looks_like_printable_name(const unsigned char* p, size_t len) {
865 if (len == 0 || len > 255) return false;
866 for (size_t i = 0; i < len; ++i) {
867 unsigned char c = p[i];
868 if (c == 0) return false;
869 if (c < 32 || c == '/' || c == '\\') return false;
870 }
871 return true;
872 }
873
874 static std::string ext4_file_type_name(uint8_t t) {
875 switch (t) {
876 case 1: return "file";
877 case 2: return "dir";
878 case 3: return "char";
879 case 4: return "block";
880 case 5: return "fifo";
881 case 6: return "socket";
882 case 7: return "symlink";
883 default: return "unknown";
884 }
885 }
886
887 void scan_journal_dirents(const std::vector<unsigned char>& block,
888 uint32_t target_block,
889 uint32_t sequence,
890 std::vector<JournalDirentCandidate>& out) {
891 size_t pos = 0;
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) {
898 ++pos;
899 continue;
900 }
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});
905 }
906 }
907 pos += rec_len;
908 }
909 }
910
911 void export_journal(const fs::path& out_root,
912 std::vector<RecoveredFileRecord>& recovered,
913 std::vector<std::string>& logs) {
914 if (journal_inode_ == 0) return;
915
916 Ext4Inode journal_inode{};
917 std::string local_error;
918 if (!read_inode(journal_inode_, journal_inode, local_error)) {
919 logs.push_back(trs("ext4 journal inode could not be read"));
920 return;
921 }
922
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"));
926 return;
927 }
928
929 const fs::path journal_root = out_root / "journal";
930 fs::create_directories(journal_root);
931 const uint64_t journal_size = inode_size_bytes(journal_inode);
932 const fs::path journal_copy = journal_root / "journal_copy.bin";
933 if (!write_ranges_to_file([&](uint64_t offset, unsigned char* buffer, size_t len) {
934 return reader_.read_at(offset, buffer, len);
935 }, ranges, journal_copy, journal_size, local_error)) {
936 logs.push_back(trs("ext4 journal export failed") + std::string(": ") + local_error);
937 return;
938 }
939 recovered.push_back({journal_copy.string(), "ext4", "journal/journal_copy.bin", journal_size, "metadata", "journal copy"});
940
941 std::ofstream info(journal_root / "journal_info.txt", std::ios::binary);
942 if (!info) return;
943
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);
951
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);
961 while (remaining >= block_size_) {
962 if (!reader_.read_at(cursor, block.data(), block.size())) break;
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);
967 type_counts[type]++;
968 info << "block=" << block_index << " type=" << type << " sequence=" << sequence << "\n";
969 pending_tags.clear();
970 pending_sequence = sequence;
971 if (type == 1u) {
972 size_t pos = 12;
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";
980 pos += 8;
981 if ((flags & 0x8u) != 0) break;
982 }
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);
986 if (bout) {
987 bout.write(reinterpret_cast<const char*>(block.data()), static_cast<std::streamsize>(block.size()));
988 ++metadata_blocks_exported;
989 }
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";
994 }
995 }
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);
1004 if (bout) {
1005 bout.write(reinterpret_cast<const char*>(block.data()), static_cast<std::streamsize>(block.size()));
1006 ++payload_blocks_exported;
1007 }
1008 info << "payload[" << block_index << "].target_block=" << target_block
1009 << " flags=" << flags << " sequence=" << pending_sequence << "\n";
1010 scan_journal_dirents(block, target_block, pending_sequence, journal_dirents);
1011 }
1012 cursor += block_size_;
1013 remaining -= block_size_;
1014 remaining_total = remaining_total >= block_size_ ? remaining_total - block_size_ : 0;
1015 ++block_index;
1016 if (remaining_total < block_size_) break;
1017 }
1018 if (remaining_total < block_size_) break;
1019 }
1020
1021 info << "journal_inode=" << journal_inode_ << "\n";
1022 info << "journal_size=" << journal_size << "\n";
1023 for (const auto& [type, count] : type_counts) {
1024 info << "type_" << type << "=" << count << "\n";
1025 }
1026 info << "metadata_blocks_exported=" << metadata_blocks_exported << "\n";
1027 info << "payload_blocks_exported=" << payload_blocks_exported << "\n";
1028 info << "replay_overlay_blocks=" << journal_replay_blocks_.size() << "\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) {
1035 dirent_out << "inode=" << d.inode << " type=" << ext4_file_type_name(d.file_type)
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) {
1039 if (!inode_marked_allocated(d.inode)) continue;
1040 Ext4Inode cand{};
1041 std::string cand_error;
1042 if (read_inode(d.inode, cand, cand_error) && inode_is_reg(cand)) {
1043 export_file(d.inode, sanitize_path_component(d.name), cand, candidate_dir, recovered, logs, cand_error);
1044 }
1045 }
1046 }
1047 info << "dirent_candidates=" << journal_dirents.size() << "\n";
1048 } else {
1049 info << "dirent_candidates=0\n";
1050 }
1051 logs.push_back(trs("ext4 journal exported") + std::string(": ") + journal_copy.string());
1052 }
1053
1054 bool recover_directory(uint32_t inode_no,
1055 const fs::path& out_dir,
1056 std::vector<RecoveredFileRecord>& recovered,
1057 std::vector<std::string>& logs,
1058 std::string& error,
1059 std::set<uint32_t>& dir_guard,
1060 int depth) {
1061 if (depth > 64) return true;
1062 if (!dir_guard.insert(inode_no).second) return true;
1063
1064 Ext4Inode inode;
1065 if (!read_inode(inode_no, inode, error)) return false;
1066 if (!inode_is_dir(inode)) return true;
1067
1068 std::vector<ExportRange> ranges;
1069 if (!inode_ranges(inode, ranges, error)) {
1070 logs.push_back(trs("Skipped ext4 directory without supported data mapping") + std::string(": inode ") + std::to_string(inode_no));
1071 error.clear();
1072 return true;
1073 }
1074
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;
1081 while (remaining >= block_size_) {
1082 if (!read_volume_bytes_replayed(cursor - volume_offset_, block.data(), block.size())) break;
1083 size_t pos = 0;
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)));
1093 name = sanitize_path_component(name);
1094 if (name.empty() || name == "." || name == "..") {
1095 pos += rec_len;
1096 continue;
1097 }
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) {
1103 Ext4Inode child;
1104 if (read_inode(entry_inode, child, error)) {
1105 if (!export_file(entry_inode, name, child, out_dir, recovered, logs, error)) return false;
1106 } else {
1107 error.clear();
1108 }
1109 }
1110 pos += rec_len;
1111 }
1112 cursor += block_size_;
1113 remaining -= block_size_;
1114 }
1115 }
1116 return true;
1117 }
1118};
1119} // namespace
1120
1122
1123bool Ext4Recovery::recover(uint64_t volume_offset,
1124 const std::string& output_dir,
1125 std::vector<RecoveredFileRecord>& recovered,
1126 std::vector<std::string>& logs,
1127 std::string& error) {
1128 Ext4RecoveryImpl impl(reader_);
1129 return impl.recover(volume_offset, output_dir, recovered, logs, error);
1130}
bool read_at(uint64_t offset, void *buffer, size_t len)
bool recover(uint64_t volume_offset, const std::string &output_dir, std::vector< RecoveredFileRecord > &recovered, std::vector< std::string > &logs, std::string &error)
DiskReader & reader_
Ext4Recovery(DiskReader &reader)
void scan_journal_dirents(const std::vector< unsigned char > &block, uint32_t target_block, uint32_t sequence, std::vector< JournalDirentCandidate > &out)
bool read_volume_bytes_replayed(uint64_t volume_rel_offset, unsigned char *buffer, size_t len)
bool read_inode(uint32_t inode_no, Ext4Inode &inode, std::string &error)
bool find_inline_xattr_system_data(const Ext4Inode &inode, std::vector< unsigned char > &out) const
bool read_u32_block(uint64_t block_no, std::vector< uint32_t > &out, std::string &error)
bool recover_directory(uint32_t inode_no, const fs::path &out_dir, std::vector< RecoveredFileRecord > &recovered, std::vector< std::string > &logs, std::string &error, std::set< uint32_t > &dir_guard, int depth)
bool inode_extents(const Ext4Inode &inode, std::vector< ExtentRef > &extents, std::string &error)
bool export_file(uint32_t inode_no, const std::string &name, const Ext4Inode &inode, const fs::path &out_dir, std::vector< RecoveredFileRecord > &recovered, std::vector< std::string > &logs, std::string &error)
bool inode_ranges(const Ext4Inode &inode, std::vector< ExportRange > &ranges, std::string &error)
std::map< uint64_t, std::pair< uint32_t, std::vector< unsigned char > > > journal_replay_blocks_
bool read_fs_block(uint64_t block_no, unsigned char *buffer, size_t len)
bool recover(uint64_t volume_offset, const std::string &output_dir, std::vector< RecoveredFileRecord > &recovered, std::vector< std::string > &logs, std::string &error)
bool read_group_desc(uint32_t group, std::vector< unsigned char > &out)
bool collect_extents_from_node(uint64_t node_phys_block, std::vector< ExtentRef > &extents, std::string &error)
void build_journal_replay_map(std::vector< std::string > &logs)
static bool looks_like_printable_name(const unsigned char *p, size_t len)
uint64_t inode_size_bytes(const Ext4Inode &inode) const
bool inode_block_map_classic(const Ext4Inode &inode, std::vector< uint64_t > &blocks, std::string &error)
void export_journal(const fs::path &out_root, std::vector< RecoveredFileRecord > &recovered, std::vector< std::string > &logs)
std::string trs(const char *msgid)
Definition i18n.cpp:22
bool write_ranges_to_file(ReadFn &&read_fn, const std::vector< ExportRange > &ranges, const fs::path &out_path, uint64_t expected_size, std::string &error)
uint64_t le64(const unsigned char *p)
uint16_t le16(const unsigned char *p)
std::string sanitize_path_component(const std::string &in)
uint32_t be32(const unsigned char *p)
uint32_t le32(const unsigned char *p)
std::string sanitize_component(const std::string &in)