Skip to content
API Documentation
BastionGuard™
filesystem_recovery.cpp
Go to the documentation of this file.
2#include "i18n.hpp"
3#include "ntfs_recovery.hpp"
4#include "ext4_recovery.hpp"
5#include "btrfs_recovery.hpp"
6#include "xfs_recovery.hpp"
7#include "hfs_recovery.hpp"
8#include "carving.hpp"
9#include "text_utils.hpp"
10
11#include <algorithm>
12#include <array>
13#include <cctype>
14#include <cstdlib>
15#include <filesystem>
16#include <fstream>
17#include <map>
18#include <optional>
19#include <set>
20#include <sstream>
21#include <string>
22#include <vector>
23
24namespace fs = std::filesystem;
25
26namespace {
27uint16_t le16(const unsigned char* p) {
28 return static_cast<uint16_t>(p[0]) | (static_cast<uint16_t>(p[1]) << 8);
29}
30
31uint32_t le32(const unsigned char* p) {
32 return static_cast<uint32_t>(p[0]) |
33 (static_cast<uint32_t>(p[1]) << 8) |
34 (static_cast<uint32_t>(p[2]) << 16) |
35 (static_cast<uint32_t>(p[3]) << 24);
36}
37
38uint64_t le64(const unsigned char* p) {
39 return static_cast<uint64_t>(p[0]) |
40 (static_cast<uint64_t>(p[1]) << 8) |
41 (static_cast<uint64_t>(p[2]) << 16) |
42 (static_cast<uint64_t>(p[3]) << 24) |
43 (static_cast<uint64_t>(p[4]) << 32) |
44 (static_cast<uint64_t>(p[5]) << 40) |
45 (static_cast<uint64_t>(p[6]) << 48) |
46 (static_cast<uint64_t>(p[7]) << 56);
47}
48
49// Bug #14, #18 fix: previous implementation silently DROPPED illegal characters
50// (e.g. "file:test.txt" -> "filetesttxt") instead of replacing them. It also
51// mangled accented characters into '_'. Now uses the shared UTF-8 aware helper.
52inline std::string sanitize_path_component(const std::string& in) {
54}
55
57 const std::vector<std::pair<uint64_t, uint64_t>>& ranges,
58 const fs::path& out_path,
59 uint64_t expected_size,
60 std::string& error) {
61 std::ofstream out(out_path, std::ios::binary);
62 if (!out) {
63 error = trs("Unable to create output file") + std::string(": ") + out_path.string();
64 return false;
65 }
66
67 std::vector<unsigned char> buffer(1024 * 1024);
68 uint64_t written = 0;
69 for (const auto& [offset, length] : ranges) {
70 uint64_t remaining = length;
71 uint64_t cursor = offset;
72 while (remaining > 0 && written < expected_size) {
73 const uint64_t allowed = std::min<uint64_t>(remaining, expected_size - written);
74 const size_t chunk = static_cast<size_t>(std::min<uint64_t>(buffer.size(), allowed));
75 if (!reader.read_at(cursor, buffer.data(), chunk)) {
76 error = trs("Read error while exporting recovered file");
77 return false;
78 }
79 out.write(reinterpret_cast<const char*>(buffer.data()), static_cast<std::streamsize>(chunk));
80 if (!out) {
81 error = trs("Write error while exporting recovered file");
82 return false;
83 }
84 remaining -= chunk;
85 cursor += chunk;
86 written += chunk;
87 }
88 if (written >= expected_size) break;
89 }
90 return true;
91}
92
94public:
95 explicit Fat32Recovery(DiskReader& reader) : reader_(reader) {}
96
97 bool recover(uint64_t volume_offset,
98 const fs::path& out_root,
99 std::vector<RecoveredFileRecord>& recovered,
100 std::vector<std::string>& logs,
101 std::string& error) {
102 if (!load_bpb(volume_offset)) return false;
103 fs::create_directories(out_root);
104 logs.push_back(trs("FAT32 recovery started at offset") + std::string(" ") + std::to_string(volume_offset));
105 std::set<uint32_t> dir_guard;
106 if (!recover_directory(root_cluster_, out_root, recovered, logs, error, dir_guard, 0)) {
107 return false;
108 }
109 logs.push_back(trs("FAT32 recovery completed."));
110 return true;
111 }
112
113private:
115 uint64_t volume_offset_ {0};
116 uint16_t bytes_per_sector_ {0};
118 uint16_t reserved_sectors_ {0};
119 uint8_t fat_count_ {0};
120 uint32_t sectors_per_fat_ {0};
121 uint32_t root_cluster_ {2};
122 uint64_t fat_offset_bytes_ {0};
124 uint64_t cluster_size_ {0};
125
126 bool load_bpb(uint64_t volume_offset) {
127 std::array<unsigned char, 512> b{};
128 if (!reader_.read_at(volume_offset, b.data(), b.size())) return false;
129 if (std::string(reinterpret_cast<const char*>(b.data() + 82), 8) != "FAT32 ") return false;
130 volume_offset_ = volume_offset;
131 bytes_per_sector_ = le16(b.data() + 11);
132 sectors_per_cluster_ = b[13];
133 reserved_sectors_ = le16(b.data() + 14);
134 fat_count_ = b[16];
135 sectors_per_fat_ = le32(b.data() + 36);
136 root_cluster_ = le32(b.data() + 44);
137 if (bytes_per_sector_ == 0 || sectors_per_cluster_ == 0 || sectors_per_fat_ == 0 || fat_count_ == 0) return false;
138 cluster_size_ = static_cast<uint64_t>(bytes_per_sector_) * sectors_per_cluster_;
141 return true;
142 }
143
144 uint64_t cluster_to_offset(uint32_t cluster) const {
145 return data_region_offset_ + static_cast<uint64_t>(cluster - 2) * cluster_size_;
146 }
147
148 bool read_cluster(uint32_t cluster, std::vector<unsigned char>& out) {
149 out.resize(static_cast<size_t>(cluster_size_));
150 return reader_.read_at(cluster_to_offset(cluster), out.data(), out.size());
151 }
152
153 bool next_cluster(uint32_t current, uint32_t& next) {
154 const uint64_t fat_entry_offset = fat_offset_bytes_ + static_cast<uint64_t>(current) * 4;
155 std::array<unsigned char, 4> entry{};
156 if (!reader_.read_at(fat_entry_offset, entry.data(), entry.size())) return false;
157 next = le32(entry.data()) & 0x0FFFFFFF;
158 return true;
159 }
160
161 std::vector<uint32_t> cluster_chain(uint32_t start, size_t hard_limit = 1 << 20) {
162 std::vector<uint32_t> chain;
163 if (start < 2) return chain;
164 std::set<uint32_t> seen;
165 uint32_t current = start;
166 while (current >= 2 && current < 0x0FFFFFF8 && chain.size() < hard_limit && seen.insert(current).second) {
167 chain.push_back(current);
168 uint32_t next = 0x0FFFFFFF;
169 if (!next_cluster(current, next)) break;
170 if (next == 0x0FFFFFF7 || next == 0) break;
171 current = next;
172 }
173 return chain;
174 }
175
176 static std::string decode_lfn_part(const unsigned char* entry) {
177 // Bug #14 fix: FAT32 long filenames are UTF-16LE, not ASCII.
178 // The previous version replaced any non-ASCII char with '_',
179 // mangling all Italian/European/Asian filenames.
180 // We extract the 26 bytes of UTF-16LE name fragment from one LFN slot
181 // (positions 1, 3, 5, 7, 9 / 14..24 / 28, 30) into a flat buffer,
182 // then run it through the shared UTF-8 decoder.
183 static const int positions[] = {1,3,5,7,9,14,16,18,20,22,24,28,30};
184 std::vector<unsigned char> raw;
185 raw.reserve(26);
186 for (int pos : positions) {
187 const uint16_t ch = le16(entry + pos);
188 if (ch == 0x0000 || ch == 0xFFFF) {
189 // Append null-terminator-equivalent and stop
190 raw.push_back(0);
191 raw.push_back(0);
192 break;
193 }
194 raw.push_back(static_cast<unsigned char>(ch & 0xFF));
195 raw.push_back(static_cast<unsigned char>((ch >> 8) & 0xFF));
196 }
197 return text_utils::utf16le_to_utf8(raw.data(), raw.size());
198 }
199
200 static std::string short_name(const unsigned char* entry) {
201 std::string base;
202 for (int i = 0; i < 8; ++i) {
203 const unsigned char c = entry[i];
204 if (c == ' ') break;
205 base.push_back(static_cast<char>(c));
206 }
207 std::string ext;
208 for (int i = 8; i < 11; ++i) {
209 const unsigned char c = entry[i];
210 if (c == ' ') break;
211 ext.push_back(static_cast<char>(c));
212 }
213 if (!ext.empty()) return base + "." + ext;
214 return base;
215 }
216
217 bool export_file(const std::string& name,
218 uint32_t start_cluster,
219 uint32_t size_bytes,
220 const fs::path& out_dir,
221 std::vector<RecoveredFileRecord>& recovered,
222 std::vector<std::string>& logs,
223 std::string& error) {
224 if (name.empty() || name == "." || name == "..") return true;
225 fs::create_directories(out_dir);
226 const auto chain = cluster_chain(start_cluster);
227 if (chain.empty() && size_bytes > 0) {
228 logs.push_back(trs("Skipped FAT32 file with invalid cluster chain") + std::string(": ") + name);
229 return true;
230 }
231
232 std::vector<std::pair<uint64_t, uint64_t>> ranges;
233 for (uint32_t cluster : chain) {
234 ranges.push_back({cluster_to_offset(cluster), cluster_size_});
235 }
236
237 fs::path out_path = out_dir / sanitize_path_component(name);
238 std::string unique = out_path.string();
239 int suffix = 1;
240 while (fs::exists(unique)) {
241 unique = (out_dir / (sanitize_path_component(name) + "_" + std::to_string(suffix++))).string();
242 }
243 out_path = unique;
244 if (!write_data_to_file(reader_, ranges, out_path, size_bytes, error)) {
245 return false;
246 }
247 recovered.push_back({out_path.string(), "FAT32", name, size_bytes, "full", "filesystem export"});
248 logs.push_back(trs("Recovered FAT32 file:") + std::string(" ") + out_path.string());
249 return true;
250 }
251
252 bool recover_directory(uint32_t dir_cluster,
253 const fs::path& out_dir,
254 std::vector<RecoveredFileRecord>& recovered,
255 std::vector<std::string>& logs,
256 std::string& error,
257 std::set<uint32_t>& dir_guard,
258 int depth) {
259 if (depth > 32) return true;
260 if (!dir_guard.insert(dir_cluster).second) return true;
261
262 const auto chain = cluster_chain(dir_cluster, 4096);
263 std::vector<unsigned char> cluster_buf;
264 std::vector<std::string> lfn_parts;
265 // Track LFN slots for deleted entries too. The LFN parts of a
266 // deleted file have their first byte set to 0xE5 too -- but we
267 // want to RECOVER the original characters when possible.
268 std::vector<std::string> deleted_lfn_parts;
269
270 for (uint32_t cluster : chain) {
271 if (!read_cluster(cluster, cluster_buf)) continue;
272 for (size_t off = 0; off + 32 <= cluster_buf.size(); off += 32) {
273 const unsigned char* e = cluster_buf.data() + off;
274 if (e[0] == 0x00) return true;
275
276 const unsigned char attr = e[11];
277
278 // ---- DELETED entry (first byte = 0xE5) ----
279 //
280 // A 0xE5 first-byte marks the dirent as freed. The rest
281 // of the bytes (cluster pointers, size, LFN parts) are
282 // intact unless the slot has been reused.
283 //
284 // Note that the FIRST char of the short name was
285 // overwritten by 0xE5, so we can't recover it from the
286 // 8.3 alone. If we have LFN parts, those are intact.
287 if (e[0] == 0xE5) {
288 if (attr == 0x0F) {
289 // Deleted LFN slot -- save the chars; the first
290 // byte (sequence number) was the one overwritten
291 // with 0xE5, the unicode chars are intact.
292 deleted_lfn_parts.push_back(decode_lfn_part(e));
293 continue;
294 }
295 if ((attr & 0x08) != 0) { deleted_lfn_parts.clear(); continue; }
296 // Skip directory entries that were deleted (rare,
297 // and recovering them recursively is risky on a
298 // possibly-corrupted slot).
299 if ((attr & 0x10) != 0) {
300 deleted_lfn_parts.clear();
301 continue;
302 }
303
304 // Build name from deleted LFN parts if available,
305 // otherwise reconstruct best-effort short name.
306 std::string del_name;
307 if (!deleted_lfn_parts.empty()) {
308 for (auto it = deleted_lfn_parts.rbegin();
309 it != deleted_lfn_parts.rend(); ++it) del_name += *it;
310 } else {
311 // First byte was overwritten by 0xE5. Use '_'
312 // as placeholder for the lost first character.
313 std::string sn = short_name(e);
314 if (!sn.empty() && static_cast<unsigned char>(sn[0]) == 0xE5) {
315 sn[0] = '_';
316 }
317 del_name = sn;
318 }
319 deleted_lfn_parts.clear();
320 del_name = sanitize_path_component(del_name);
321 if (del_name.empty()) del_name = "deleted_unnamed";
322
323 const uint32_t dh = le16(e + 20);
324 const uint32_t dl = le16(e + 26);
325 const uint32_t del_cluster = (dh << 16) | dl;
326 const uint32_t del_size = le32(e + 28);
327
328 // Heuristic: skip clearly-bogus entries
329 if (del_size == 0) {
330 // Empty deleted file -- still create it
331 const fs::path deleted_dir = out_dir / "deleted";
332 std::error_code ec;
333 fs::create_directories(deleted_dir, ec);
334 const fs::path out_path = deleted_dir / del_name;
335 std::ofstream(out_path, std::ios::binary);
336 recovered.push_back({out_path.string(), "FAT32",
337 ("deleted/" + del_name), 0, "partial",
338 "deleted-empty"});
339 continue;
340 }
341 if (del_cluster < 2 || del_cluster >= 0x0FFFFFF7) continue;
342
343 // Try to recover by following the FAT chain. The
344 // chain may have been re-used (= the file's data is
345 // gone). We do best-effort: if the chain reads back
346 // and decodes to size_bytes worth of data, save it.
347 const fs::path deleted_dir = out_dir / "deleted";
348 std::error_code ec;
349 fs::create_directories(deleted_dir, ec);
350 std::string ex_err;
351 if (export_file_to_dir(del_name, del_cluster, del_size,
352 deleted_dir, recovered, logs, ex_err,
353 /*is_deleted=*/true)) {
354 // recorded inside export_file_to_dir
355 }
356 continue;
357 }
358
359 if (attr == 0x0F) {
360 lfn_parts.push_back(decode_lfn_part(e));
361 continue;
362 }
363 if ((attr & 0x08) != 0) { lfn_parts.clear(); continue; }
364
365 std::string name;
366 if (!lfn_parts.empty()) {
367 for (auto it = lfn_parts.rbegin(); it != lfn_parts.rend(); ++it) name += *it;
368 } else {
369 name = short_name(e);
370 }
371 lfn_parts.clear();
372 name = sanitize_path_component(name);
373 if (name.empty()) name = "unnamed";
374
375 const uint32_t high = le16(e + 20);
376 const uint32_t low = le16(e + 26);
377 const uint32_t start_cluster = (high << 16) | low;
378 const uint32_t size_bytes = le32(e + 28);
379 const bool is_dir = (attr & 0x10) != 0;
380
381 if (is_dir) {
382 if (name != "." && name != ".." && start_cluster >= 2) {
383 const fs::path subdir = out_dir / name;
384 fs::create_directories(subdir);
385 if (!recover_directory(start_cluster, subdir, recovered, logs, error, dir_guard, depth + 1)) {
386 return false;
387 }
388 }
389 } else {
390 if (!export_file(name, start_cluster, size_bytes, out_dir, recovered, logs, error)) {
391 return false;
392 }
393 }
394 }
395 }
396 return true;
397 }
398
399 // Helper used by the deleted-entry path. Same as export_file but
400 // tags the resulting record with "deleted-recovered" + integrity
401 // "partial" because the cluster chain may have been reused.
402 bool export_file_to_dir(const std::string& name,
403 uint32_t start_cluster,
404 uint32_t size_bytes,
405 const fs::path& out_dir,
406 std::vector<RecoveredFileRecord>& recovered,
407 std::vector<std::string>& logs,
408 std::string& /*error*/,
409 bool is_deleted) {
410 if (start_cluster < 2 || size_bytes == 0) return false;
411 // Compute how many clusters we need
412 const uint32_t cluster_bytes = static_cast<uint32_t>(cluster_size_);
413 if (cluster_bytes == 0) return false;
414 const uint64_t need_clusters = (size_bytes + cluster_bytes - 1) / cluster_bytes;
415
416 // For deleted files we don't have a reliable FAT chain (it may
417 // point into reused clusters). Heuristic: assume the file was
418 // CONTIGUOUS starting at start_cluster -- this is true for ~80%
419 // of FAT files in practice (defragmented).
420 std::vector<uint32_t> chain;
421 if (is_deleted) {
422 chain.reserve(static_cast<size_t>(need_clusters));
423 for (uint64_t i = 0; i < need_clusters; ++i) {
424 chain.push_back(static_cast<uint32_t>(start_cluster + i));
425 }
426 } else {
427 chain = cluster_chain(start_cluster);
428 }
429 if (chain.empty()) return false;
430
431 const fs::path out_path = out_dir / sanitize_path_component(name);
432 std::ofstream out(out_path, std::ios::binary | std::ios::trunc);
433 if (!out) return false;
434 uint64_t written = 0;
435 std::vector<unsigned char> cb;
436 for (uint32_t cl : chain) {
437 if (written >= size_bytes) break;
438 if (!read_cluster(cl, cb)) break;
439 const uint64_t want = std::min<uint64_t>(cluster_bytes, size_bytes - written);
440 out.write(reinterpret_cast<const char*>(cb.data()),
441 static_cast<std::streamsize>(want));
442 written += want;
443 }
444 const std::string note = is_deleted
445 ? "deleted-contiguous-guess"
446 : "filesystem export";
447 const std::string integ = is_deleted ? "partial" : "full";
448 recovered.push_back({out_path.string(), "FAT32",
449 (is_deleted ? "deleted/" : "") + name,
450 written, integ, note});
451 if (is_deleted) {
452 logs.push_back(trs("Recovered FAT32 deleted file (best-effort, "
453 "assumes contiguous):") +
454 std::string(" ") + out_path.string());
455 }
456 return true;
457 }
458};
459
461public:
462 explicit ExfatRecovery(DiskReader& reader) : reader_(reader) {}
463
464 bool recover(uint64_t volume_offset,
465 const fs::path& out_root,
466 std::vector<RecoveredFileRecord>& recovered,
467 std::vector<std::string>& logs,
468 std::string& error) {
469 if (!load_vbr(volume_offset)) return false;
470 fs::create_directories(out_root);
471 logs.push_back(trs("exFAT recovery started at offset") + std::string(" ") + std::to_string(volume_offset));
473 logs.push_back(trs("exFAT allocation bitmap loaded") + std::string(": ") + std::to_string(allocation_bitmap_length_) + " bytes");
474 }
475 std::set<uint32_t> dir_guard;
476 if (!recover_directory(root_dir_cluster_, out_root, recovered, logs, error, dir_guard, 0)) {
477 return false;
478 }
479 logs.push_back(trs("exFAT recovery completed."));
480 return true;
481 }
482
483private:
484 struct StreamInfo {
485 bool valid {false};
486 bool no_fat_chain {false};
487 uint32_t first_cluster {0};
488 uint64_t data_length {0};
489 std::string name;
490 bool is_directory {false};
491 bool is_deleted {false};
492 };
493
495 uint64_t volume_offset_ {0};
501 uint32_t cluster_count_ {0};
502 uint32_t root_dir_cluster_ {2};
503 uint32_t serial_number_ {0};
506 uint64_t bytes_per_sector_ {0};
508 uint64_t cluster_size_ {0};
509 uint64_t fat_offset_bytes_ {0};
513 std::vector<unsigned char> allocation_bitmap_;
514
515 bool load_vbr(uint64_t volume_offset) {
516 std::array<unsigned char, 512> b{};
517 if (!reader_.read_at(volume_offset, b.data(), b.size())) return false;
518 if (std::string(reinterpret_cast<const char*>(b.data() + 3), 8) != "EXFAT ") return false;
519 volume_offset_ = volume_offset;
520 partition_offset_sectors_ = le64(b.data() + 64);
521 volume_length_sectors_ = le64(b.data() + 72);
522 fat_offset_sectors_ = le32(b.data() + 80);
523 fat_length_sectors_ = le32(b.data() + 84);
524 cluster_heap_offset_sectors_ = le32(b.data() + 88);
525 cluster_count_ = le32(b.data() + 92);
526 root_dir_cluster_ = le32(b.data() + 96);
527 serial_number_ = le32(b.data() + 100);
537 allocation_bitmap_.clear();
538 return bytes_per_sector_ >= 512 && cluster_size_ > 0 && cluster_count_ > 0;
539 }
540
541 bool cluster_allocated(uint32_t cluster) const {
542 if (cluster < 2 || cluster >= cluster_count_ + 2) return false;
543 if (allocation_bitmap_.empty()) return true;
544 const uint64_t idx = static_cast<uint64_t>(cluster - 2);
545 const uint64_t byte_index = idx / 8;
546 const uint8_t bit = static_cast<uint8_t>(idx % 8);
547 if (byte_index >= allocation_bitmap_.size()) return false;
548 return (allocation_bitmap_[static_cast<size_t>(byte_index)] & (1u << bit)) != 0;
549 }
550
552 if (root_dir_cluster_ < 2) return false;
553 const auto chain = cluster_chain(root_dir_cluster_, true, cluster_size_ * 64, 4096);
554 std::vector<unsigned char> cluster_buf;
555 for (uint32_t cluster : chain) {
556 if (!read_cluster(cluster, cluster_buf)) continue;
557 for (size_t off = 0; off + 32 <= cluster_buf.size(); off += 32) {
558 const unsigned char* e = cluster_buf.data() + off;
559 const uint8_t type = e[0];
560 if (type == 0x00) break;
561 if (type == 0x81) {
564 break;
565 }
566 }
567 if (allocation_bitmap_cluster_ != 0) break;
568 }
569 if (allocation_bitmap_cluster_ < 2 || allocation_bitmap_length_ == 0) return false;
570 const auto bitmap_chain = cluster_chain(allocation_bitmap_cluster_, true, allocation_bitmap_length_, 4096);
571 if (bitmap_chain.empty()) return false;
572 std::vector<std::pair<uint64_t, uint64_t>> ranges;
573 for (uint32_t cluster : bitmap_chain) ranges.push_back({cluster_to_offset(cluster), cluster_size_});
574 fs::path tmp = fs::temp_directory_path() / ("exfat_bitmap_" + std::to_string(serial_number_) + ".bin");
575 std::string local_error;
576 if (!write_data_to_file(reader_, ranges, tmp, allocation_bitmap_length_, local_error)) return false;
577 std::ifstream in(tmp, std::ios::binary);
578 allocation_bitmap_.assign(std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>());
579 std::error_code ec;
580 fs::remove(tmp, ec);
581 return !allocation_bitmap_.empty();
582 }
583
584 uint64_t cluster_to_offset(uint32_t cluster) const {
585 return cluster_heap_offset_bytes_ + static_cast<uint64_t>(cluster - 2) * cluster_size_;
586 }
587
588 bool next_cluster(uint32_t current, uint32_t& next) {
589 const uint64_t fat_entry_offset = fat_offset_bytes_ + static_cast<uint64_t>(current) * 4;
590 std::array<unsigned char, 4> entry{};
591 if (!reader_.read_at(fat_entry_offset, entry.data(), entry.size())) return false;
592 next = le32(entry.data()) & 0x0FFFFFFF;
593 return true;
594 }
595
596 std::vector<uint32_t> cluster_chain(uint32_t start, bool no_fat_chain, uint64_t data_length, size_t hard_limit = 1 << 20) {
597 std::vector<uint32_t> chain;
598 if (start < 2) return chain;
599 if (no_fat_chain) {
600 const uint64_t clusters_needed = data_length == 0 ? 1 : (data_length + cluster_size_ - 1) / cluster_size_;
601 for (uint64_t i = 0; i < clusters_needed; ++i) {
602 const uint32_t cluster = start + static_cast<uint32_t>(i);
603 if (cluster < 2 || cluster >= cluster_count_ + 2) break;
604 if (!cluster_allocated(cluster)) {
605 chain.clear();
606 break;
607 }
608 chain.push_back(cluster);
609 }
610 return chain;
611 }
612 std::set<uint32_t> seen;
613 uint32_t current = start;
614 while (current >= 2 && current < 0x0FFFFFF8 && chain.size() < hard_limit && seen.insert(current).second) {
615 chain.push_back(current);
616 uint32_t next = 0x0FFFFFFF;
617 if (!next_cluster(current, next)) break;
618 if (next == 0x0FFFFFF7 || next == 0) break;
619 current = next;
620 }
621 return chain;
622 }
623
624 bool read_cluster(uint32_t cluster, std::vector<unsigned char>& out) {
625 out.resize(static_cast<size_t>(cluster_size_));
626 return reader_.read_at(cluster_to_offset(cluster), out.data(), out.size());
627 }
628
629 static std::string decode_utf16_name(const std::vector<unsigned char>& bytes) {
630 // Bug #14 fix: exFAT filenames are UTF-16LE; decode as proper UTF-8
631 // instead of replacing non-ASCII with '_'.
632 return text_utils::utf16le_to_utf8(bytes.data(), bytes.size());
633 }
634
635 bool export_stream(const StreamInfo& info,
636 const fs::path& out_dir,
637 std::vector<RecoveredFileRecord>& recovered,
638 std::vector<std::string>& logs,
639 std::string& error) {
640 if (!info.valid || info.is_directory) return true;
641 fs::create_directories(out_dir);
642 const auto chain = cluster_chain(info.first_cluster, info.no_fat_chain, info.data_length);
643 if (chain.empty() && info.data_length > 0) {
644 logs.push_back(trs("Skipped exFAT file with invalid cluster chain") + std::string(": ") + info.name);
645 return true;
646 }
647 std::vector<std::pair<uint64_t, uint64_t>> ranges;
648 for (uint32_t cluster : chain) {
649 ranges.push_back({cluster_to_offset(cluster), cluster_size_});
650 }
651 fs::path out_path = out_dir / sanitize_path_component(info.name);
652 std::string unique = out_path.string();
653 int suffix = 1;
654 while (fs::exists(unique)) {
655 unique = (out_dir / (sanitize_path_component(info.name) + "_" + std::to_string(suffix++))).string();
656 }
657 out_path = unique;
658 if (!write_data_to_file(reader_, ranges, out_path, info.data_length, error)) return false;
659 recovered.push_back({out_path.string(), "exFAT", info.name, info.data_length, "full", info.no_fat_chain ? "bitmap-validated contiguous export" : "filesystem export"});
660 logs.push_back(trs("Recovered exFAT file:") + std::string(" ") + out_path.string());
661 return true;
662 }
663
664 // Deleted-file export. Same as export_stream but:
665 // - never trusts the FAT chain (it may have been re-allocated)
666 // - always assumes a CONTIGUOUS layout starting at first_cluster
667 // (= true for ~80% of exFAT files; many file systems
668 // allocate contiguously when they can)
669 // - tags the result as "partial" with note "deleted-contiguous-guess"
670 // - never recurses into deleted directories
672 const fs::path& out_dir,
673 std::vector<RecoveredFileRecord>& recovered,
674 std::vector<std::string>& logs,
675 std::string& error) {
676 if (!info.valid || info.is_directory) return true;
677 if (info.first_cluster < 2 || info.data_length == 0) {
678 // Empty file -- still create a placeholder
679 const fs::path out_path = out_dir / sanitize_path_component(info.name);
680 std::ofstream(out_path, std::ios::binary);
681 recovered.push_back({out_path.string(), "exFAT",
682 ("deleted/" + info.name), 0, "partial",
683 "deleted-empty"});
684 return true;
685 }
686 // Build a contiguous "guess" range
687 const uint64_t need_clusters =
689 std::vector<std::pair<uint64_t, uint64_t>> ranges;
690 ranges.reserve(static_cast<size_t>(need_clusters));
691 for (uint64_t i = 0; i < need_clusters; ++i) {
692 const uint32_t cl = info.first_cluster + static_cast<uint32_t>(i);
693 // Bound check
694 if (cl < 2 || cl >= cluster_count_ + 2) break;
695 ranges.push_back({cluster_to_offset(cl), cluster_size_});
696 }
697 if (ranges.empty()) return true;
698
699 fs::path out_path = out_dir / sanitize_path_component(info.name);
700 std::string unique = out_path.string();
701 int suffix = 1;
702 while (fs::exists(unique)) {
703 unique = (out_dir / (sanitize_path_component(info.name) + "_"
704 + std::to_string(suffix++))).string();
705 }
706 out_path = unique;
707 if (!write_data_to_file(reader_, ranges, out_path,
708 info.data_length, error)) return false;
709 recovered.push_back({out_path.string(), "exFAT",
710 ("deleted/" + info.name), info.data_length, "partial",
711 "deleted-contiguous-guess"});
712 logs.push_back(trs("Recovered exFAT deleted file (best-effort, "
713 "assumes contiguous):") +
714 std::string(" ") + out_path.string());
715 return true;
716 }
717
718 bool recover_directory(uint32_t start_cluster,
719 const fs::path& out_dir,
720 std::vector<RecoveredFileRecord>& recovered,
721 std::vector<std::string>& logs,
722 std::string& error,
723 std::set<uint32_t>& dir_guard,
724 int depth) {
725 if (depth > 32) return true;
726 if (!dir_guard.insert(start_cluster).second) return true;
727
728 const auto chain = cluster_chain(start_cluster, true, cluster_size_ * 16, 4096);
729 std::vector<unsigned char> cluster_buf;
730 StreamInfo current;
731 int remaining_secondary = 0;
732 std::vector<unsigned char> name_bytes;
733
734 auto finalize = [&](bool force_dir_recurse) -> bool {
735 if (!current.valid) return true;
736 current.name = sanitize_path_component(current.name.empty() ? "unnamed" : current.name);
737 // Deleted entries: route to "deleted/" subdir, never recurse
738 // into them (the directory's children entries are likely
739 // already overwritten or pointing into reused clusters).
740 if (current.is_deleted) {
741 if (current.is_directory) return true; // skip deleted dirs
742 const fs::path deleted_dir = out_dir / "deleted";
743 std::error_code ec;
744 fs::create_directories(deleted_dir, ec);
745 return export_stream_deleted(current, deleted_dir, recovered, logs, error);
746 }
747 if (current.is_directory) {
748 if (current.first_cluster >= 2) {
749 const fs::path subdir = out_dir / current.name;
750 fs::create_directories(subdir);
751 return recover_directory(current.first_cluster, subdir, recovered, logs, error, dir_guard, depth + 1);
752 }
753 } else {
754 return export_stream(current, out_dir, recovered, logs, error);
755 }
756 (void)force_dir_recurse;
757 return true;
758 };
759
760 for (uint32_t cluster : chain) {
761 if (!read_cluster(cluster, cluster_buf)) continue;
762 for (size_t off = 0; off + 32 <= cluster_buf.size(); off += 32) {
763 const unsigned char* e = cluster_buf.data() + off;
764 const uint8_t type = e[0];
765 if (type == 0x00) {
766 return finalize(false);
767 }
768 // ---- exFAT entry types ----
769 //
770 // Bit 7 of the type byte = "InUse". When clear, the
771 // entry is DELETED but otherwise intact. The 3 entry
772 // types we care about have these (in-use, deleted)
773 // pairs:
774 // File Directory Entry: 0x85 / 0x05
775 // Stream Extension: 0xC0 / 0x40
776 // Filename: 0xC1 / 0x41
777 //
778 // We accept BOTH variants. We track is_deleted on the
779 // current pseudo-entry so finalize() can route the
780 // file to a "deleted/" subdirectory.
781 const bool is_file_de = (type == 0x85 || type == 0x05);
782 const bool is_stream_de = (type == 0xC0 || type == 0x40);
783 const bool is_name_de = (type == 0xC1 || type == 0x41);
784 const bool entry_deleted = (type == 0x05 || type == 0x40 ||
785 type == 0x41);
786
787 if (is_file_de) {
788 if (!finalize(false)) return false;
789 current = {};
790 current.valid = true;
791 current.is_directory = (e[4] & 0x10) != 0;
792 current.is_deleted = entry_deleted;
793 remaining_secondary = e[1];
794 name_bytes.clear();
795 } else if (current.valid && is_stream_de) {
796 current.no_fat_chain = (e[1] & 0x02) != 0;
797 current.name = std::string();
798 const uint8_t name_len = e[3];
799 current.first_cluster = le32(e + 20);
800 current.data_length = le64(e + 24);
801 name_bytes.reserve(static_cast<size_t>(name_len) * 2);
802 if (entry_deleted) current.is_deleted = true;
803 --remaining_secondary;
804 } else if (current.valid && is_name_de) {
805 name_bytes.insert(name_bytes.end(), e + 2, e + 32);
806 current.name = decode_utf16_name(name_bytes);
807 if (entry_deleted) current.is_deleted = true;
808 --remaining_secondary;
809 if (remaining_secondary <= 0) {
810 if (!finalize(true)) return false;
811 current = {};
812 }
813 } else if (current.valid && remaining_secondary > 0) {
814 --remaining_secondary;
815 if (remaining_secondary <= 0) {
816 if (!finalize(false)) return false;
817 current = {};
818 }
819 } else {
820 if (!finalize(false)) return false;
821 current = {};
822 }
823 }
824 }
825 return finalize(false);
826 }
827};
828
829}
830
832 const ScanReport& report,
833 const std::string& output_dir,
834 std::vector<RecoveredFileRecord>& recovered,
835 std::vector<std::string>& logs,
836 std::string& error) {
837 std::set<uint64_t> ntfs_offsets;
838 std::set<uint64_t> fat32_offsets;
839 std::set<uint64_t> exfat_offsets;
840 std::set<uint64_t> ext4_offsets;
841 std::set<uint64_t> btrfs_offsets;
842 std::set<uint64_t> xfs_offsets;
843 std::set<uint64_t> hfs_offsets;
844 for (const auto& r : report.results) {
845 if (r.type == "NTFS volume" || r.type == "Lost NTFS partition candidate" || r.type == "NTFS candidate") ntfs_offsets.insert(r.offset_bytes);
846 if (r.type == "FAT32 volume" || r.type == "Lost FAT32 partition candidate") fat32_offsets.insert(r.offset_bytes);
847 if (r.type == "exFAT volume" || r.type == "Lost exFAT partition candidate") exfat_offsets.insert(r.offset_bytes);
848 if (r.type == "ext4 volume" || r.type == "Lost ext4 partition candidate" || r.type == "ext4 candidate") ext4_offsets.insert(r.offset_bytes);
849 if (r.type == "Btrfs volume" || r.type == "Lost Btrfs partition candidate" || r.type == "Btrfs candidate") btrfs_offsets.insert(r.offset_bytes);
850 if (r.type == "XFS volume" || r.type == "Lost XFS partition candidate") xfs_offsets.insert(r.offset_bytes);
851 if (r.type == "HFS+ volume" || r.type == "HFSX volume" || r.type == "Lost HFS+ partition candidate") hfs_offsets.insert(r.offset_bytes);
852 }
853
854 try {
855 fs::create_directories(fs::path(output_dir) / "filesystem_recovered" / "ntfs");
856 fs::create_directories(fs::path(output_dir) / "filesystem_recovered" / "fat32");
857 fs::create_directories(fs::path(output_dir) / "filesystem_recovered" / "exfat");
858 fs::create_directories(fs::path(output_dir) / "filesystem_recovered" / "ext4");
859 fs::create_directories(fs::path(output_dir) / "filesystem_recovered" / "btrfs");
860 fs::create_directories(fs::path(output_dir) / "filesystem_recovered" / "xfs");
861 fs::create_directories(fs::path(output_dir) / "filesystem_recovered" / "hfs");
862 } catch (const std::exception& ex) {
863 error = ex.what();
864 return false;
865 }
866
867
868 for (uint64_t offset : ntfs_offsets) {
869 NtfsRecovery ntfs(reader);
870 std::string local_error;
871 if (!ntfs.recover(offset, output_dir, recovered, logs, local_error)) {
872 logs.push_back(trs("NTFS recovery skipped or failed at offset") + std::string(" ") + std::to_string(offset) + ": " + local_error);
873 }
874 }
875
876 for (uint64_t offset : fat32_offsets) {
877 Fat32Recovery fat32(reader);
878 const fs::path out_root = fs::path(output_dir) / "filesystem_recovered" / "fat32" / ("offset_" + std::to_string(offset));
879 std::string local_error;
880 if (!fat32.recover(offset, out_root, recovered, logs, local_error)) {
881 logs.push_back(trs("FAT32 recovery skipped or failed at offset") + std::string(" ") + std::to_string(offset) + ": " + local_error);
882 }
883 }
884
885 for (uint64_t offset : exfat_offsets) {
886 ExfatRecovery exfat(reader);
887 const fs::path out_root = fs::path(output_dir) / "filesystem_recovered" / "exfat" / ("offset_" + std::to_string(offset));
888 std::string local_error;
889 if (!exfat.recover(offset, out_root, recovered, logs, local_error)) {
890 logs.push_back(trs("exFAT recovery skipped or failed at offset") + std::string(" ") + std::to_string(offset) + ": " + local_error);
891 }
892 }
893
894 for (uint64_t offset : ext4_offsets) {
895 Ext4Recovery ext4(reader);
896 std::string local_error;
897 if (!ext4.recover(offset, output_dir, recovered, logs, local_error)) {
898 logs.push_back(trs("ext4 recovery skipped or failed at offset") + std::string(" ") + std::to_string(offset) + ": " + local_error);
899 }
900 }
901
902 for (uint64_t offset : btrfs_offsets) {
903 BtrfsRecovery btrfs(reader);
904 std::string local_error;
905 if (!btrfs.recover(offset, output_dir, recovered, logs, local_error)) {
906 logs.push_back(trs("Btrfs recovery skipped or failed at offset") + std::string(" ") + std::to_string(offset) + ": " + local_error);
907 }
908 }
909
910 for (uint64_t offset : xfs_offsets) {
911 XfsRecovery xfs(reader);
912 std::string local_error;
913 if (!xfs.recover(offset, output_dir, recovered, logs, local_error)) {
914 logs.push_back(trs("XFS recovery skipped or failed at offset") + std::string(" ") + std::to_string(offset) + ": " + local_error);
915 }
916 }
917
918 for (uint64_t offset : hfs_offsets) {
919 HfsRecovery hfs(reader);
920 std::string local_error;
921 if (!hfs.recover(offset, output_dir, recovered, logs, local_error)) {
922 logs.push_back(trs("HFS+ recovery skipped or failed at offset") + std::string(" ") + std::to_string(offset) + ": " + local_error);
923 }
924 }
925
926 // ---- Optional file carving pass ----
927 //
928 // Carving is opt-in via environment variable BASTIONGUARD_CARVING=1.
929 // It runs AFTER all filesystem-specific recovery has been tried,
930 // so it complements (not replaces) the per-FS results. The carving
931 // pass is filesystem-agnostic: it recognises file content directly
932 // by magic-byte signatures and is the right tool for cases where
933 // filesystem metadata (MFT/inode table/catalog) is destroyed but
934 // content blocks remain intact. It also recovers files with any
935 // FS where deletion zeros the metadata but not the data.
936 //
937 // We make it opt-in (rather than always-on) because:
938 // * On a healthy disk, every file is also recovered by FS-aware
939 // recovery -- carving would produce duplicates.
940 // * Carving has a non-zero false-positive rate.
941 // * Carving is slow on big disks (full sequential scan).
942 {
943 const char* env = std::getenv("BASTIONGUARD_CARVING");
944 if (env != nullptr && env[0] == '1') {
946 // Defaults are reasonable (16 GiB scan limit, 500 MiB per
947 // file). The user can override these by editing the source
948 // for now; a CLI flag is a future addition.
949 std::string carve_err;
950 const size_t before = recovered.size();
951 carving::run_carving(reader, output_dir, opts, recovered,
952 logs, carve_err);
953 const size_t carved = recovered.size() - before;
954 if (!carve_err.empty()) {
955 logs.push_back(trs("Carving error: ") + carve_err);
956 }
957 logs.push_back(trs("Carving pass added ") +
958 std::to_string(carved) + trs(" file(s)"));
959 }
960 }
961
962 return true;
963}
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_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)
static bool recover_supported_filesystems(DiskReader &reader, const ScanReport &report, const std::string &output_dir, std::vector< RecoveredFileRecord > &recovered, std::vector< std::string > &logs, std::string &error)
bool recover(uint64_t volume_offset, const std::string &output_dir, std::vector< RecoveredFileRecord > &recovered, std::vector< std::string > &logs, std::string &error)
bool export_stream(const StreamInfo &info, const fs::path &out_dir, std::vector< RecoveredFileRecord > &recovered, std::vector< std::string > &logs, std::string &error)
std::vector< uint32_t > cluster_chain(uint32_t start, bool no_fat_chain, uint64_t data_length, size_t hard_limit=1<< 20)
static std::string decode_utf16_name(const std::vector< unsigned char > &bytes)
bool recover(uint64_t volume_offset, const fs::path &out_root, std::vector< RecoveredFileRecord > &recovered, std::vector< std::string > &logs, std::string &error)
bool read_cluster(uint32_t cluster, std::vector< unsigned char > &out)
bool recover_directory(uint32_t start_cluster, 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 export_stream_deleted(const StreamInfo &info, const fs::path &out_dir, std::vector< RecoveredFileRecord > &recovered, std::vector< std::string > &logs, std::string &error)
bool read_cluster(uint32_t cluster, std::vector< unsigned char > &out)
std::vector< uint32_t > cluster_chain(uint32_t start, size_t hard_limit=1<< 20)
bool recover(uint64_t volume_offset, const fs::path &out_root, std::vector< RecoveredFileRecord > &recovered, std::vector< std::string > &logs, std::string &error)
bool export_file(const std::string &name, uint32_t start_cluster, uint32_t size_bytes, const fs::path &out_dir, std::vector< RecoveredFileRecord > &recovered, std::vector< std::string > &logs, std::string &error)
static std::string decode_lfn_part(const unsigned char *entry)
static std::string short_name(const unsigned char *entry)
bool recover_directory(uint32_t dir_cluster, 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 export_file_to_dir(const std::string &name, uint32_t start_cluster, uint32_t size_bytes, const fs::path &out_dir, std::vector< RecoveredFileRecord > &recovered, std::vector< std::string > &logs, std::string &, bool is_deleted)
std::string trs(const char *msgid)
Definition i18n.cpp:22
bool write_data_to_file(DiskReader &reader, const std::vector< std::pair< uint64_t, uint64_t > > &ranges, const fs::path &out_path, uint64_t expected_size, std::string &error)
std::string sanitize_path_component(const std::string &in)
bool run_carving(DiskReader &reader, const std::string &output_dir, const CarvingOptions &opts, std::vector< RecoveredFileRecord > &recovered, std::vector< std::string > &logs, std::string &error)
Definition carving.cpp:954
std::string sanitize_component(const std::string &in)
std::string utf16le_to_utf8(const unsigned char *data, std::size_t byte_count)
std::vector< ScanResult > results
Definition models.hpp:40