Skip to content
API Documentation
BastionGuard™
ntfs_recovery.cpp
Go to the documentation of this file.
1#include "ntfs_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 <cstring>
10#include <filesystem>
11#include <fstream>
12#include <limits>
13#include <map>
14#include <optional>
15#include <set>
16#include <sstream>
17#include <string>
18#include <utility>
19#include <vector>
20
21namespace fs = std::filesystem;
22
23namespace {
24uint16_t le16(const unsigned char* p) {
25 return static_cast<uint16_t>(p[0]) | (static_cast<uint16_t>(p[1]) << 8);
26}
27
28uint32_t le32(const unsigned char* p) {
29 return static_cast<uint32_t>(p[0]) |
30 (static_cast<uint32_t>(p[1]) << 8) |
31 (static_cast<uint32_t>(p[2]) << 16) |
32 (static_cast<uint32_t>(p[3]) << 24);
33}
34
35uint64_t le64(const unsigned char* p) {
36 return static_cast<uint64_t>(p[0]) |
37 (static_cast<uint64_t>(p[1]) << 8) |
38 (static_cast<uint64_t>(p[2]) << 16) |
39 (static_cast<uint64_t>(p[3]) << 24) |
40 (static_cast<uint64_t>(p[4]) << 32) |
41 (static_cast<uint64_t>(p[5]) << 40) |
42 (static_cast<uint64_t>(p[6]) << 48) |
43 (static_cast<uint64_t>(p[7]) << 56);
44}
45
46// Bug #14, #18 fix: replaced the old broken implementations with the
47// shared text_utils helpers. We keep the names here for minimal diff.
48inline std::string sanitize_component(const std::string& in) {
50}
51
52inline std::string decode_utf16le_ascii(const unsigned char* data, size_t byte_count) {
53 // Function name kept for backwards compatibility, but it now produces
54 // proper UTF-8 output (the previous version replaced every non-ASCII
55 // character with '_').
56 return text_utils::utf16le_to_utf8(data, byte_count);
57}
58
59struct DiskSpan {
60 uint64_t offset {0};
61 uint64_t length {0};
62 bool sparse {false};
63};
64
66 uint64_t vcn {0};
67 uint64_t offset {0};
68 uint64_t length {0};
69 bool sparse {false};
70};
71
73 uint64_t volume_offset {0};
74 uint16_t bytes_per_sector {0};
76 uint64_t cluster_size {0};
77 uint64_t total_sectors {0};
78 uint64_t mft_lcn {0};
79 uint64_t mftmirr_lcn {0};
80 uint64_t mft_record_size {0};
81};
82
84 uint32_t type {0};
85 uint16_t record_length {0};
86 uint8_t name_length {0};
87 uint8_t name_offset {0};
88 uint64_t start_vcn {0};
89 uint64_t base_reference {0};
90 uint16_t instance {0};
91 std::string name;
92};
93
95 std::string name;
96 bool resident {false};
97 std::vector<unsigned char> resident_data;
98 std::vector<NtfsDataRun> runs;
99 uint64_t data_size {0};
100 uint64_t alloc_size {0};
101 uint16_t attr_flags {0};
102 bool compressed {false};
103 bool encrypted {false};
104 bool sparse {false};
105 bool unsupported_layout {false};
107};
108
110 uint64_t record_number {0};
112 // Sequence number this record had when the parent's $FILE_NAME was
113 // written. If it doesn't match the parent's CURRENT sequence
114 // number, the parent record was reused for a different file.
116 // This record's own sequence number (from the FILE record header
117 // at offset 0x10). Incremented every time the slot is freed and
118 // reallocated.
120 bool in_use {false};
121 bool is_directory {false};
122 std::string name;
123 bool deleted_candidate {false};
124 bool has_attribute_list {false};
125 bool has_reparse_point {false};
126 std::vector<unsigned char> reparse_data;
127 std::map<std::string, NtfsDataStream> streams;
128 // $I30 directory index: when this record is a directory, we collect
129 // these so the tree walker can use them as the AUTHORITATIVE source
130 // of (filename, child MFT ref) pairs -- bypassing the MFT scan's
131 // ambiguous parent inference.
132 //
133 // - index_root_data: bytes of $INDEX_ROOT (resident attribute,
134 // type 0x90, name "$I30"). Contains the small/initial index.
135 // - index_allocation_runs: run list of $INDEX_ALLOCATION (type
136 // 0xA0, name "$I30"). Contains the larger INDX node blocks
137 // for big directories.
138 // - index_record_size: bytes per INDX block (typically 4096).
139 std::vector<unsigned char> index_root_data;
140 std::vector<NtfsDataRun> index_allocation_runs;
141 uint32_t index_record_size {0};
142};
143
144bool lznt1_decompress(const std::vector<unsigned char>& src,
145 size_t expected_size,
146 std::vector<unsigned char>& dst) {
147 dst.clear();
148 size_t pos = 0;
149 while (pos + 2 <= src.size() && dst.size() < expected_size) {
150 const uint16_t header = le16(src.data() + pos);
151 pos += 2;
152 const size_t chunk_size = (header & 0x0FFF) + 1;
153 const bool is_compressed = (header & 0x8000) != 0;
154 if (pos + chunk_size > src.size()) return false;
155
156 if (!is_compressed) {
157 const size_t take = std::min(chunk_size, expected_size - dst.size());
158 dst.insert(dst.end(), src.begin() + static_cast<long>(pos), src.begin() + static_cast<long>(pos + take));
159 pos += chunk_size;
160 continue;
161 }
162
163 const size_t chunk_end = pos + chunk_size;
164 std::vector<unsigned char> chunk_out;
165 chunk_out.reserve(4096);
166 while (pos < chunk_end && chunk_out.size() < 4096) {
167 const uint8_t flags = src[pos++];
168 for (int bit = 0; bit < 8 && pos < chunk_end; ++bit) {
169 if ((flags & (1u << bit)) == 0) {
170 chunk_out.push_back(src[pos++]);
171 } else {
172 if (pos + 1 >= chunk_end) return false;
173 const uint16_t token = le16(src.data() + pos);
174 pos += 2;
175
176 int length_bits = 4;
177 while ((1u << (length_bits + 4)) < chunk_out.size() && length_bits < 12) ++length_bits;
178 const size_t length_mask = (1u << length_bits) - 1u;
179 const size_t offset = (token >> length_bits) + 1u;
180 const size_t length = (token & length_mask) + 3u;
181 if (offset == 0 || offset > chunk_out.size()) return false;
182 for (size_t i = 0; i < length && chunk_out.size() < 4096; ++i) {
183 chunk_out.push_back(chunk_out[chunk_out.size() - offset]);
184 }
185 }
186 if (chunk_out.size() >= 4096) break;
187 }
188 }
189 const size_t take = std::min(chunk_out.size(), expected_size - dst.size());
190 dst.insert(dst.end(), chunk_out.begin(), chunk_out.begin() + static_cast<long>(take));
191 if (chunk_out.size() < 4096 && dst.size() < expected_size) {
192 dst.resize(std::min(expected_size, dst.size() + (4096 - chunk_out.size())), 0);
193 }
194 }
195 if (dst.size() < expected_size) dst.resize(expected_size, 0);
196 return true;
197}
198
199} // namespace
200
202public:
203 explicit NtfsRecoveryImpl(DiskReader& reader) : reader_(reader) {}
204
205 bool recover(uint64_t volume_offset,
206 const std::string& output_dir,
207 std::vector<RecoveredFileRecord>& recovered,
208 std::vector<std::string>& logs,
209 std::string& error) {
210 if (!read_boot_sector(volume_offset, error)) return false;
211
212 fs::path out_root = fs::path(output_dir) / "filesystem_recovered" / "ntfs" / ("offset_" + std::to_string(volume_offset));
213 fs::create_directories(out_root);
214 logs.push_back(trs("NTFS recovery started at offset") + std::string(" ") + std::to_string(volume_offset));
215
216 std::vector<unsigned char> mft_record;
217 if (!read_mft_record(0, mft_record, error)) return false;
218
219 NtfsFileRecordInfo mft_meta;
220 if (!parse_file_record(0, mft_record, mft_meta, error)) return false;
221 auto mft_it = mft_meta.streams.find("");
222 if (mft_it == mft_meta.streams.end() || (mft_it->second.runs.empty() && mft_it->second.data_size == 0)) {
223 error = trs("Unable to parse NTFS $MFT data runs");
224 return false;
225 }
226
227 mft_ranges_.clear();
228 for (const auto& run : mft_it->second.runs) {
229 if (!run.sparse) mft_ranges_.push_back({run.offset, run.length});
230 }
231
232 // ---- Load $MFTMirr (MFT record 1) for fallback ----
233 //
234 // $MFTMirr is a mirror of the FIRST FOUR MFT records ($MFT,
235 // $MFTMirr, $LogFile, $Volume) plus a few more on modern
236 // versions. When the primary $MFT is corrupt, we can fall
237 // back to the mirror to recover at least these 4 critical
238 // records -- which often unblocks recovery of everything else
239 // because $MFT itself is among them.
240 //
241 // We read MFT record 1 from the PRIMARY $MFT, get its runs,
242 // and store them. If the primary read fails entirely (very
243 // corrupt MFT), we skip the mirror -- by then the volume is
244 // probably beyond software recovery.
245 mftmirr_ranges_.clear();
246 {
247 std::vector<unsigned char> mirr_rec;
248 std::string mirr_err;
249 if (read_mft_record(1, mirr_rec, mirr_err)) {
250 NtfsFileRecordInfo mirr_info;
251 if (parse_file_record(1, mirr_rec, mirr_info, mirr_err)) {
252 auto it = mirr_info.streams.find("");
253 if (it != mirr_info.streams.end()) {
254 for (const auto& run : it->second.runs) {
255 if (!run.sparse) {
256 mftmirr_ranges_.push_back({run.offset, run.length});
257 }
258 }
259 if (!mftmirr_ranges_.empty()) {
260 logs.push_back(trs("NTFS $MFTMirr loaded as fallback "
261 "for the first MFT records "
262 "(typically records 0-3)"));
263 }
264 }
265 }
266 }
267 if (mftmirr_ranges_.empty()) {
268 logs.push_back(trs("NTFS $MFTMirr not loadable -- primary MFT "
269 "must be intact for recovery"));
270 }
271 }
272
273 // Bug #6 fix: 300k was way too low for enterprise volumes (NAS, servers).
274 // Cap at 8M records or actual data size, whichever is smaller.
275 // Hard upper bound prevents runaway scan on corrupt $MFT data_size.
276 constexpr uint64_t MAX_MFT_RECORDS = 8ull * 1000ull * 1000ull;
277 const uint64_t record_count = std::min<uint64_t>(
278 mft_it->second.data_size / volume_.mft_record_size,
279 MAX_MFT_RECORDS);
280 std::map<uint64_t, NtfsFileRecordInfo> records;
281 for (uint64_t i = 0; i < record_count; ++i) {
282 std::vector<unsigned char> rec;
283 if (!read_mft_record(i, rec, error)) {
284 error.clear();
285 continue;
286 }
288 if (!parse_file_record(i, rec, info, error)) {
289 error.clear();
290 continue;
291 }
292 info.record_number = i;
293 info.deleted_candidate = !info.in_use;
294 records[i] = std::move(info);
295 }
296
297 // ---- $I30 directory index pass ----
298 //
299 // For every record that's a directory and has $I30 data, walk
300 // the index and use the resulting (filename, child_ref) pairs
301 // to:
302 // 1. Recover filenames for child records that lost their
303 // $FILE_NAME (= info.name was empty after parse).
304 // 2. Upgrade child records' parent_record_number when the
305 // $I30 entry agrees with a different parent than what
306 // the child's $FILE_NAME claimed -- this happens when
307 // the child's $FILE_NAME is stale (file was renamed or
308 // moved AFTER the dirent was last written; the dirent
309 // is authoritative).
310 // 3. Validate sequence numbers against $I30's child_ref:
311 // if the dirent's child_seq matches the record's
312 // own_seq, the (parent, child) link is genuine.
313 size_t i30_walks = 0;
314 size_t i30_filenames_recovered = 0;
315 size_t i30_parent_updates = 0;
316 for (auto& [parent_id, parent_info] : records) {
317 if (!parent_info.is_directory) continue;
318 if (parent_info.index_root_data.empty()) continue;
319 std::vector<I30Entry> entries;
320 walk_i30(parent_info, entries, logs);
321 if (entries.empty()) continue;
322 ++i30_walks;
323 for (const auto& ie : entries) {
324 auto cit = records.find(ie.child_record_number);
325 if (cit == records.end()) continue;
326 NtfsFileRecordInfo& child = cit->second;
327 // Sequence check: if the dirent's recorded child_seq
328 // doesn't match the record's actual seq, the dirent
329 // is stale (child slot was reused). Skip.
330 if (ie.child_sequence_number != 0 &&
331 child.own_sequence_number != 0 &&
332 ie.child_sequence_number != child.own_sequence_number) {
333 continue;
334 }
335 // Recover lost filename
336 if (child.name.empty() && !ie.filename.empty()) {
337 child.name = ie.filename;
338 ++i30_filenames_recovered;
339 }
340 // Upgrade parent link if we found a valid I30 entry
341 // pointing here -- the parent_id here IS authoritative.
342 if (child.parent_record_number != parent_id) {
343 child.parent_record_number = parent_id;
344 child.parent_sequence_number = parent_info.own_sequence_number;
345 ++i30_parent_updates;
346 }
347 }
348 }
349 if (i30_walks > 0) {
350 std::ostringstream m;
351 m << "NTFS $I30 indexes walked: " << i30_walks
352 << ", filenames recovered: " << i30_filenames_recovered
353 << ", parent links updated: " << i30_parent_updates;
354 logs.push_back(m.str());
355 }
356
357 size_t exported_count = 0;
358 size_t deleted_exported_count = 0;
359 size_t ads_exported_count = 0;
360 size_t skipped_count = 0;
361 size_t skipped_encrypted = 0;
362 size_t skipped_malformed = 0;
363 size_t files_with_efs = 0;
364
365 for (const auto& [id, info] : records) {
366 if (info.is_directory || info.name.empty()) continue;
367
368 // Track files that have ANY encrypted stream (EFS = Encrypting
369 // File System). These are reported separately so the user
370 // understands why content is unrecoverable without keys.
371 bool any_encrypted = false;
372 for (const auto& [_, stream] : info.streams) {
373 if (stream.encrypted) { any_encrypted = true; break; }
374 }
375 if (any_encrypted) ++files_with_efs;
376
377 fs::path rel = build_relative_path(id, records);
378 if (rel.empty()) rel = fs::path("orphaned") / sanitize_component(info.name);
379 rel = info.deleted_candidate ? (fs::path("deleted") / rel) : (fs::path("live") / rel);
380 const fs::path base_path = out_root / rel;
381 fs::create_directories(base_path.parent_path());
382
383 bool exported_any = false;
384 for (const auto& [stream_name, stream] : info.streams) {
385 if (!stream_exportable(stream)) {
386 ++skipped_count;
387 if (stream.encrypted) ++skipped_encrypted;
388 else if (stream.unsupported_layout) ++skipped_malformed;
389 continue;
390 }
391 const fs::path target = output_path_for_stream(base_path, stream_name);
392 std::string local_error;
393 if (!export_stream(stream, target, local_error)) {
394 logs.push_back(trs("NTFS stream export failed") + std::string(": ") + rel.string() + " - " + local_error);
395 continue;
396 }
397 recovered.push_back({target.string(), "NTFS", target.lexically_relative(out_root).string(), stream.data_size, "full", stream_name.empty() ? "filesystem export" : "alternate data stream"});
398 exported_any = true;
399 if (!stream_name.empty()) ++ads_exported_count;
400 }
401
402 if (exported_any) {
403 export_reparse_sidecar(info, base_path, recovered, logs);
404 export_usn_summary_if_needed(info, base_path, recovered, logs);
405 ++exported_count;
406 if (info.deleted_candidate) ++deleted_exported_count;
407 } else {
408 export_usn_summary_if_needed(info, base_path, recovered, logs);
409 }
410 }
411
412 logs.push_back(trs("NTFS recovery completed.") + std::string(" ") + trs("Files exported") + ": " + std::to_string(exported_count));
413 logs.push_back(trs("NTFS deleted files exported") + std::string(": ") + std::to_string(deleted_exported_count));
414 logs.push_back(trs("NTFS alternate data streams exported") + std::string(": ") + std::to_string(ads_exported_count));
415 logs.push_back(trs("NTFS unsupported streams skipped") + std::string(": ") + std::to_string(skipped_count));
416 if (skipped_encrypted > 0) {
417 logs.push_back(trs("NTFS streams skipped because EFS-encrypted "
418 "(content cannot be decrypted without "
419 "Windows account keys):") +
420 std::string(" ") + std::to_string(skipped_encrypted));
421 }
422 if (skipped_malformed > 0) {
423 logs.push_back(trs("NTFS streams skipped because runlist "
424 "malformed (likely corrupted records):") +
425 std::string(" ") + std::to_string(skipped_malformed));
426 }
427 if (files_with_efs > 0) {
428 logs.push_back(trs("NTFS files with EFS-encrypted data "
429 "detected (any stream encrypted):") +
430 std::string(" ") + std::to_string(files_with_efs));
431 }
432
433 // ---- $LogFile pass (MFT record 2) ----
434 //
435 // The transaction journal can hold "before images" of MFT records
436 // that have since been overwritten or freed. We try to recover
437 // additional file records from there.
438 scan_logfile(out_root, records, recovered, logs);
439
440 return true;
441 }
442
443private:
446 std::vector<std::pair<uint64_t, uint64_t>> mft_ranges_;
447 // $MFTMirr ranges -- a mirror of the first few MFT records (record
448 // 0 through ~3 typically). Used as fallback when the primary
449 // read returns invalid data. May be empty on volumes where we
450 // could not load record 1 from the primary.
451 std::vector<std::pair<uint64_t, uint64_t>> mftmirr_ranges_;
452
453 bool read_boot_sector(uint64_t volume_offset, std::string& error) {
454 std::array<unsigned char, 512> b{};
455 if (!reader_.read_at(volume_offset, b.data(), b.size())) {
456 error = trs("Unable to read NTFS boot sector");
457 return false;
458 }
459 if (std::string(reinterpret_cast<const char*>(b.data() + 3), 8) != "NTFS ") {
460 error = trs("Invalid NTFS boot sector");
461 return false;
462 }
463 volume_.volume_offset = volume_offset;
464 volume_.bytes_per_sector = le16(b.data() + 11);
467 volume_.total_sectors = le64(b.data() + 40);
468 volume_.mft_lcn = le64(b.data() + 48);
469 volume_.mftmirr_lcn = le64(b.data() + 56);
470 const int8_t rec_desc = static_cast<int8_t>(b[64]);
471 // Bug #3/#4 fix: descriptor 0 invalid; negative descriptor must be in 1..30 range
472 if (rec_desc > 0) {
473 volume_.mft_record_size = static_cast<uint64_t>(rec_desc) * volume_.cluster_size;
474 } else if (rec_desc == 0) {
475 error = trs("Invalid NTFS MFT record descriptor (zero)");
476 return false;
477 } else {
478 const int exponent = -static_cast<int>(rec_desc);
479 if (exponent < 1 || exponent > 30) {
480 error = trs("Invalid NTFS MFT record descriptor (out of range)");
481 return false;
482 }
483 volume_.mft_record_size = 1ull << exponent;
484 }
486 error = trs("Invalid NTFS geometry");
487 return false;
488 }
489 return true;
490 }
491
492 uint64_t cluster_to_offset(uint64_t cluster) const {
493 return volume_.volume_offset + cluster * volume_.cluster_size;
494 }
495
496 bool read_raw_record(uint64_t record_number, std::vector<unsigned char>& out, std::string& error) {
497 if (mft_ranges_.empty()) {
498 const uint64_t off = cluster_to_offset(volume_.mft_lcn) + record_number * volume_.mft_record_size;
499 out.resize(static_cast<size_t>(volume_.mft_record_size));
500 if (!reader_.read_at(off, out.data(), out.size())) {
501 error = trs("Unable to read MFT record");
502 return false;
503 }
504 return true;
505 }
506
507 uint64_t logical_offset = record_number * volume_.mft_record_size;
508 uint64_t remaining = volume_.mft_record_size;
509 out.resize(static_cast<size_t>(remaining));
510 size_t out_pos = 0;
511 uint64_t cursor = 0;
512 for (const auto& [phys_off, len] : mft_ranges_) {
513 if (logical_offset >= cursor + len) {
514 cursor += len;
515 continue;
516 }
517 const uint64_t inner = logical_offset > cursor ? logical_offset - cursor : 0;
518 const uint64_t can_read = std::min<uint64_t>(remaining, len - inner);
519 if (!reader_.read_at(phys_off + inner, out.data() + out_pos, static_cast<size_t>(can_read))) {
520 error = trs("Unable to read MFT record from data runs");
521 return false;
522 }
523 remaining -= can_read;
524 out_pos += static_cast<size_t>(can_read);
525 logical_offset += can_read;
526 cursor += len;
527 if (remaining == 0) return true;
528 }
529 error = trs("Incomplete read of MFT record");
530 return false;
531 }
532
533 bool apply_fixup(std::vector<unsigned char>& record, std::string& error) {
534 if (record.size() < 512) {
535 error = trs("Invalid MFT record size");
536 return false;
537 }
538 const uint16_t usa_offset = le16(record.data() + 4);
539 const uint16_t usa_count = le16(record.data() + 6);
540 if (static_cast<size_t>(usa_offset) + static_cast<size_t>(usa_count) * 2 > record.size() || usa_count == 0) {
541 error = trs("Invalid NTFS update sequence array");
542 return false;
543 }
544 const uint16_t usn = le16(record.data() + usa_offset);
545 // Bug #5 fix: per-sector mismatch is degraded read, not full reject.
546 // Apply fixup where it matches; leave bad sectors untouched and set warning flag.
547 size_t bad_sectors = 0;
548 for (uint16_t i = 1; i < usa_count; ++i) {
549 const size_t sector_end = i * 512 - 2;
550 if (sector_end + 2 > record.size()) break;
551 const uint16_t cur = le16(record.data() + sector_end);
552 if (cur != usn) {
553 ++bad_sectors;
554 continue; // skip this sector but continue with the others
555 }
556 record[sector_end] = record[usa_offset + i * 2];
557 record[sector_end + 1] = record[usa_offset + i * 2 + 1];
558 }
559 // If ALL sectors fail (record completely garbage) reject; otherwise return ok.
560 if (usa_count > 1 && bad_sectors >= static_cast<size_t>(usa_count - 1)) {
561 error = trs("NTFS fixup mismatch on all sectors");
562 return false;
563 }
564 return true;
565 }
566
567 bool read_mft_record(uint64_t record_number, std::vector<unsigned char>& out, std::string& error) {
568 if (!read_raw_record(record_number, out, error)) {
569 // Try mirror as a fallback for low record numbers
570 return read_mft_record_from_mirror(record_number, out, error);
571 }
572 if (out.size() < 4 || std::string(reinterpret_cast<const char*>(out.data()), 4) != "FILE") {
573 // Primary read succeeded but signature is wrong (possibly
574 // overwritten or non-MFT data on disk). Try mirror.
575 std::string mirror_err;
576 if (read_mft_record_from_mirror(record_number, out, mirror_err)) {
577 return true;
578 }
579 error = trs("Invalid MFT record signature");
580 return false;
581 }
582 return apply_fixup(out, error);
583 }
584
585 // $MFTMirr fallback. Returns true and fills `out` if a valid
586 // mirrored copy of the requested MFT record is available.
587 // $MFTMirr typically covers only records 0..3 (sometimes a few
588 // more). Records beyond the mirror's coverage trivially fail.
589 bool read_mft_record_from_mirror(uint64_t record_number,
590 std::vector<unsigned char>& out,
591 std::string& error) {
592 if (mftmirr_ranges_.empty()) {
593 error = trs("NTFS $MFTMirr not available");
594 return false;
595 }
596 // Compute total mirror size
597 uint64_t mirror_size = 0;
598 for (const auto& [_, len] : mftmirr_ranges_) mirror_size += len;
599 const uint64_t logical_offset = record_number * volume_.mft_record_size;
600 if (logical_offset + volume_.mft_record_size > mirror_size) {
601 error = trs("Record number out of $MFTMirr coverage");
602 return false;
603 }
604 // Walk the mirror ranges to read this record's bytes
605 uint64_t logical = logical_offset;
606 uint64_t remaining = volume_.mft_record_size;
607 out.resize(static_cast<size_t>(remaining));
608 size_t out_pos = 0;
609 uint64_t cursor = 0;
610 for (const auto& [phys_off, len] : mftmirr_ranges_) {
611 if (logical >= cursor + len) {
612 cursor += len;
613 continue;
614 }
615 const uint64_t inner = logical > cursor ? logical - cursor : 0;
616 const uint64_t can_read = std::min<uint64_t>(remaining, len - inner);
617 if (!reader_.read_at(phys_off + inner, out.data() + out_pos,
618 static_cast<size_t>(can_read))) {
619 error = trs("Mirror read failed");
620 return false;
621 }
622 remaining -= can_read;
623 out_pos += static_cast<size_t>(can_read);
624 logical += can_read;
625 cursor += len;
626 if (remaining == 0) break;
627 }
628 if (remaining != 0) {
629 error = trs("Incomplete mirror read");
630 return false;
631 }
632 // Verify signature + apply fixup
633 if (out.size() < 4 ||
634 std::string(reinterpret_cast<const char*>(out.data()), 4) != "FILE") {
635 error = trs("Mirror MFT record signature mismatch");
636 return false;
637 }
638 return apply_fixup(out, error);
639 }
640
641 std::vector<NtfsDataRun> parse_runlist(const unsigned char* runlist,
642 size_t max_len,
643 uint64_t start_vcn,
644 bool& has_sparse,
645 bool& malformed) {
646 std::vector<NtfsDataRun> out;
647 int64_t current_lcn = 0;
648 uint64_t current_vcn = start_vcn;
649 size_t pos = 0;
650 malformed = false;
651 has_sparse = false;
652 while (pos < max_len && runlist[pos] != 0x00) {
653 const uint8_t header = runlist[pos++];
654 const uint8_t len_bytes = header & 0x0F;
655 const uint8_t off_bytes = (header >> 4) & 0x0F;
656 if (len_bytes == 0 || pos + len_bytes + off_bytes > max_len) {
657 malformed = true;
658 break;
659 }
660 uint64_t run_len = 0;
661 for (uint8_t i = 0; i < len_bytes; ++i) run_len |= static_cast<uint64_t>(runlist[pos + i]) << (8 * i);
662 pos += len_bytes;
663 int64_t run_off = 0;
664 if (off_bytes > 0) {
665 for (uint8_t i = 0; i < off_bytes; ++i) run_off |= static_cast<int64_t>(runlist[pos + i]) << (8 * i);
666 if ((runlist[pos + off_bytes - 1] & 0x80) != 0) {
667 run_off |= -((int64_t)1 << (off_bytes * 8));
668 }
669 }
670 pos += off_bytes;
671 if (run_len == 0) {
672 malformed = true;
673 break;
674 }
675 NtfsDataRun run;
676 run.vcn = current_vcn;
677 run.length = run_len * volume_.cluster_size;
678 if (off_bytes == 0) {
679 has_sparse = true;
680 run.sparse = true;
681 } else {
682 current_lcn += run_off;
683 if (current_lcn < 0) {
684 malformed = true;
685 break;
686 }
687 run.offset = cluster_to_offset(static_cast<uint64_t>(current_lcn));
688 }
689 out.push_back(run);
690 current_vcn += run_len;
691 }
692 return out;
693 }
694
695 NtfsDataStream& stream_for(NtfsFileRecordInfo& info, const std::string& name) {
696 auto [it, inserted] = info.streams.try_emplace(name);
697 (void)inserted;
698 it->second.name = name;
699 return it->second;
700 }
701
702 static void merge_runs(std::vector<NtfsDataRun>& dst, const std::vector<NtfsDataRun>& src) {
703 dst.insert(dst.end(), src.begin(), src.end());
704 std::sort(dst.begin(), dst.end(), [](const NtfsDataRun& a, const NtfsDataRun& b) {
705 if (a.vcn != b.vcn) return a.vcn < b.vcn;
706 // At equal VCN: prefer non-sparse (real data) first
707 if (a.sparse != b.sparse) return !a.sparse;
708 return a.offset < b.offset;
709 });
710 // Bug #7 fix: dedup by VCN keeping the BEST candidate (non-sparse > sparse,
711 // earliest offset among real runs). The previous code dropped silently and
712 // could truncate heavily fragmented files when attribute extensions overlapped.
713 std::vector<NtfsDataRun> merged;
714 merged.reserve(dst.size());
715 for (const auto& run : dst) {
716 if (!merged.empty() && merged.back().vcn == run.vcn) {
717 // Keep the existing one (already best after sort), unless current is non-sparse
718 // and existing is sparse (sort already orders this correctly, but be safe).
719 if (merged.back().sparse && !run.sparse) {
720 merged.back() = run;
721 }
722 continue;
723 }
724 merged.push_back(run);
725 }
726 dst.swap(merged);
727 }
728
729 bool parse_attribute_list_entries(const unsigned char* value,
730 uint32_t value_len,
731 std::vector<NtfsAttributeListEntry>& out) {
732 uint32_t pos = 0;
733 while (pos + 26 <= value_len) {
734 const uint32_t type = le32(value + pos);
735 if (type == 0xFFFFFFFF) break;
736 const uint16_t record_length = le16(value + pos + 4);
737 const uint8_t name_length = value[pos + 6];
738 const uint8_t name_offset = value[pos + 7];
739 if (record_length < 26 || pos + record_length > value_len) break;
741 entry.type = type;
742 entry.record_length = record_length;
743 entry.name_length = name_length;
744 entry.name_offset = name_offset;
745 entry.start_vcn = le64(value + pos + 8);
746 entry.base_reference = le64(value + pos + 16) & 0x0000FFFFFFFFFFFFull;
747 entry.instance = le16(value + pos + 24);
748 if (name_length > 0 && name_offset + static_cast<uint16_t>(name_length) * 2 <= record_length) {
749 entry.name = decode_utf16le_ascii(value + pos + name_offset, static_cast<size_t>(name_length) * 2);
750 }
751 out.push_back(std::move(entry));
752 pos += record_length;
753 }
754 return !out.empty();
755 }
756
757 bool read_runs_to_buffer(const std::vector<NtfsDataRun>& runs,
758 uint64_t expected_size,
759 std::vector<unsigned char>& out,
760 std::string& error) {
761 out.clear();
762 out.reserve(static_cast<size_t>(std::min<uint64_t>(expected_size, 64ull * 1024ull * 1024ull)));
763 std::vector<unsigned char> buffer(1024 * 1024);
764 uint64_t written = 0;
765 for (const auto& run : runs) {
766 uint64_t remain = std::min<uint64_t>(run.length, expected_size > written ? expected_size - written : 0);
767 if (remain == 0) break;
768 if (run.sparse) {
769 out.insert(out.end(), static_cast<size_t>(remain), 0);
770 written += remain;
771 continue;
772 }
773 uint64_t cur = run.offset;
774 while (remain > 0) {
775 const size_t chunk = static_cast<size_t>(std::min<uint64_t>(buffer.size(), remain));
776 if (!reader_.read_at(cur, buffer.data(), chunk)) {
777 error = trs("Read error while exporting recovered file");
778 return false;
779 }
780 out.insert(out.end(), buffer.begin(), buffer.begin() + static_cast<long>(chunk));
781 cur += chunk;
782 remain -= chunk;
783 written += chunk;
784 }
785 }
786 if (out.size() < expected_size) out.resize(static_cast<size_t>(expected_size), 0);
787 return true;
788 }
789
791 std::vector<unsigned char>& out,
792 std::string& error) {
793 out.clear();
794 const uint64_t unit_size = volume_.cluster_size * (1ull << stream.compression_unit_shift);
795 if (unit_size == 0) {
796 error = trs("Invalid NTFS compression unit");
797 return false;
798 }
799
800 const uint64_t total_units = stream.data_size == 0 ? 0 : ((stream.data_size + unit_size - 1) / unit_size);
801 size_t run_index = 0;
802 std::vector<unsigned char> compressed_bytes;
803 for (uint64_t unit = 0; unit < total_units; ++unit) {
804 const uint64_t expected = std::min<uint64_t>(unit_size, stream.data_size - unit * unit_size);
805 uint64_t unit_bytes = 0;
806 bool unit_sparse = false;
807 size_t begin = run_index;
808 while (run_index < stream.runs.size()) {
809 const auto& run = stream.runs[run_index];
810 if (run.vcn >= (unit + 1) * (unit_size / volume_.cluster_size)) break;
811 unit_bytes += run.length;
812 unit_sparse = unit_sparse || run.sparse;
813 ++run_index;
814 if (unit_bytes >= unit_size) break;
815 }
816
817 if (unit_sparse && unit_bytes == unit_size) {
818 out.insert(out.end(), static_cast<size_t>(expected), 0);
819 continue;
820 }
821
822 if (unit_bytes == unit_size && !unit_sparse) {
823 std::vector<NtfsDataRun> unit_runs(stream.runs.begin() + static_cast<long>(begin),
824 stream.runs.begin() + static_cast<long>(run_index));
825 std::vector<unsigned char> raw;
826 if (!read_runs_to_buffer(unit_runs, expected, raw, error)) return false;
827 out.insert(out.end(), raw.begin(), raw.begin() + static_cast<long>(expected));
828 continue;
829 }
830
831 std::vector<NtfsDataRun> unit_runs(stream.runs.begin() + static_cast<long>(begin),
832 stream.runs.begin() + static_cast<long>(run_index));
833 compressed_bytes.clear();
834 if (!read_runs_to_buffer(unit_runs, unit_bytes, compressed_bytes, error)) return false;
835 std::vector<unsigned char> decomp;
836 if (!lznt1_decompress(compressed_bytes, static_cast<size_t>(expected), decomp)) {
837 error = trs("Unsupported or invalid NTFS compressed stream");
838 return false;
839 }
840 out.insert(out.end(), decomp.begin(), decomp.begin() + static_cast<long>(expected));
841 }
842 if (out.size() > stream.data_size) out.resize(static_cast<size_t>(stream.data_size));
843 return true;
844 }
845
846 bool process_attributes(uint64_t base_record_number,
847 const std::vector<unsigned char>& record,
848 NtfsFileRecordInfo& info,
849 std::vector<NtfsAttributeListEntry>& attr_list,
850 std::string& error) {
851 (void)base_record_number;
852 const uint16_t flags = le16(record.data() + 22);
853 const uint16_t attrs_offset = le16(record.data() + 20);
854 info.in_use = (flags & 0x0001) != 0;
855 info.is_directory = (flags & 0x0002) != 0;
856 // Sequence number at offset 0x10 of the FILE record header.
857 // We read it here so that the caller can later compare it
858 // against parent_sequence_number (= what the parent dirent
859 // recorded when it linked to this child).
860 info.own_sequence_number = le16(record.data() + 0x10);
861
862 if (attrs_offset >= record.size()) {
863 error = trs("Invalid NTFS attribute offset");
864 return false;
865 }
866
867 int best_name_score = -1;
868 size_t pos = attrs_offset;
869 while (pos + 8 <= record.size()) {
870 const uint32_t attr_type = le32(record.data() + pos);
871 if (attr_type == 0xFFFFFFFF) break;
872 const uint32_t attr_len = le32(record.data() + pos + 4);
873 if (attr_len < 24 || pos + attr_len > record.size()) {
874 error = trs("Invalid NTFS attribute length");
875 return false;
876 }
877 const bool nonresident = record[pos + 8] != 0;
878 const uint8_t attr_name_len = record[pos + 9];
879 const uint16_t attr_name_off = le16(record.data() + pos + 10);
880 const uint16_t attr_flags = le16(record.data() + pos + 12);
881 std::string attr_name;
882 if (attr_name_len > 0 && attr_name_off + static_cast<uint32_t>(attr_name_len) * 2 <= attr_len) {
883 attr_name = decode_utf16le_ascii(record.data() + pos + attr_name_off, static_cast<size_t>(attr_name_len) * 2);
884 }
885
886 if (attr_type == 0x20) {
887 info.has_attribute_list = true;
888 if (!nonresident) {
889 const uint32_t value_len = le32(record.data() + pos + 16);
890 const uint16_t value_off = le16(record.data() + pos + 20);
891 if (value_off + value_len <= attr_len) {
892 parse_attribute_list_entries(record.data() + pos + value_off, value_len, attr_list);
893 }
894 } else {
895 const uint16_t run_off = le16(record.data() + pos + 32);
896 const uint64_t real_size = le64(record.data() + pos + 48);
897 const uint64_t start_vcn = le64(record.data() + pos + 16);
898 if (run_off < attr_len) {
899 bool has_sparse = false;
900 bool malformed = false;
901 auto runs = parse_runlist(record.data() + pos + run_off, attr_len - run_off, start_vcn, has_sparse, malformed);
902 std::vector<unsigned char> buffer;
903 if (!malformed && read_runs_to_buffer(runs, real_size, buffer, error)) {
904 parse_attribute_list_entries(buffer.data(), static_cast<uint32_t>(buffer.size()), attr_list);
905 } else {
906 error.clear();
907 }
908 }
909 }
910 } else if (attr_type == 0x30) {
911 const uint32_t value_len = le32(record.data() + pos + 16);
912 const uint16_t value_off = le16(record.data() + pos + 20);
913 if (!nonresident && value_off + value_len <= attr_len && value_len >= 66) {
914 const unsigned char* v = record.data() + pos + value_off;
915 const uint64_t parent_ref = le64(v);
916 const uint8_t name_chars = v[64];
917 const uint8_t name_ns = v[65];
918 const unsigned char* name_ptr = v + 66;
919 if (66u + static_cast<uint32_t>(name_chars) * 2 <= value_len) {
920 const std::string candidate_name = decode_utf16le_ascii(name_ptr, static_cast<size_t>(name_chars) * 2);
921 int score = 0;
922 if (name_ns == 1 || name_ns == 3) score = 3;
923 else if (name_ns == 0) score = 2;
924 else score = 1;
925 if (!candidate_name.empty() && score >= best_name_score) {
926 best_name_score = score;
927 info.parent_record_number = parent_ref & 0x0000FFFFFFFFFFFFull;
929 static_cast<uint16_t>(parent_ref >> 48);
930 info.name = candidate_name;
931 }
932 }
933 }
934 } else if (attr_type == 0xC0) {
935 const uint32_t value_len = le32(record.data() + pos + 16);
936 const uint16_t value_off = le16(record.data() + pos + 20);
937 if (!nonresident && value_off + value_len <= attr_len) {
938 info.has_reparse_point = true;
939 info.reparse_data.assign(record.begin() + static_cast<long>(pos + value_off),
940 record.begin() + static_cast<long>(pos + value_off + value_len));
941 }
942 } else if (attr_type == 0x90 && attr_name == "$I30") {
943 // $INDEX_ROOT for the directory index. Always resident.
944 // Layout (within the attribute value):
945 // __le32 attribute_type (= 0x30 for $FILE_NAME)
946 // __le32 collation_rule
947 // __le32 index_record_size (typically 4096)
948 // __u8 clusters_per_index_record
949 // __u8 reserved[3]
950 // INDEX_HEADER (16 bytes)
951 // INDEX_ENTRY[] entries...
952 if (!nonresident) {
953 const uint32_t value_len = le32(record.data() + pos + 16);
954 const uint16_t value_off = le16(record.data() + pos + 20);
955 if (value_off + value_len <= attr_len && value_len >= 16) {
956 info.index_root_data.assign(
957 record.begin() + static_cast<long>(pos + value_off),
958 record.begin() + static_cast<long>(pos + value_off + value_len));
959 // Index record size at offset 8
960 info.index_record_size = le32(info.index_root_data.data() + 8);
961 }
962 }
963 } else if (attr_type == 0xA0 && attr_name == "$I30") {
964 // $INDEX_ALLOCATION: non-resident attribute holding the
965 // INDX node blocks for large directories.
966 if (nonresident) {
967 const uint16_t run_off = le16(record.data() + pos + 32);
968 const uint64_t start_vcn = le64(record.data() + pos + 16);
969 if (run_off >= 32 && run_off < attr_len) {
970 bool has_sparse = false;
971 bool malformed = false;
972 auto runs = parse_runlist(
973 record.data() + pos + run_off,
974 attr_len - run_off, start_vcn,
975 has_sparse, malformed);
976 if (!malformed) {
977 info.index_allocation_runs = std::move(runs);
978 }
979 }
980 }
981 } else if (attr_type == 0x80) {
982 NtfsDataStream& stream = stream_for(info, attr_name);
983 stream.attr_flags |= attr_flags;
984 stream.compressed = stream.compressed || ((attr_flags & 0x0001) != 0);
985 stream.encrypted = stream.encrypted || ((attr_flags & 0x4000) != 0);
986 stream.sparse = stream.sparse || ((attr_flags & 0x8000) != 0);
987 if (!nonresident) {
988 const uint32_t value_len = le32(record.data() + pos + 16);
989 const uint16_t value_off = le16(record.data() + pos + 20);
990 if (value_off + value_len <= attr_len) {
991 stream.resident = true;
992 stream.resident_data.assign(record.begin() + static_cast<long>(pos + value_off),
993 record.begin() + static_cast<long>(pos + value_off + value_len));
994 stream.data_size = std::max<uint64_t>(stream.data_size, value_len);
995 }
996 } else {
997 const uint64_t start_vcn = le64(record.data() + pos + 16);
998 const uint16_t run_off = le16(record.data() + pos + 32);
999 stream.compression_unit_shift = record[pos + 34];
1000 stream.alloc_size = std::max<uint64_t>(stream.alloc_size, le64(record.data() + pos + 40));
1001 stream.data_size = std::max<uint64_t>(stream.data_size, le64(record.data() + pos + 48));
1002 if (run_off < attr_len) {
1003 bool has_sparse = false;
1004 bool malformed = false;
1005 auto runs = parse_runlist(record.data() + pos + run_off, attr_len - run_off, start_vcn, has_sparse, malformed);
1006 stream.sparse = stream.sparse || has_sparse;
1007 stream.unsupported_layout = stream.unsupported_layout || malformed;
1008 merge_runs(stream.runs, runs);
1009 }
1010 }
1011 }
1012 pos += attr_len;
1013 }
1014 return true;
1015 }
1016
1017 bool load_attribute_list_extensions(uint64_t base_record_number,
1018 NtfsFileRecordInfo& info,
1019 const std::vector<NtfsAttributeListEntry>& attr_list,
1020 std::string& error) {
1021 std::set<uint64_t> processed;
1022 for (const auto& entry : attr_list) {
1023 if (entry.base_reference == 0 || entry.base_reference == base_record_number) continue;
1024 if (!processed.insert(entry.base_reference).second) continue;
1025 std::vector<unsigned char> ext_record;
1026 if (!read_mft_record(entry.base_reference, ext_record, error)) {
1027 error.clear();
1028 continue;
1029 }
1030 std::vector<NtfsAttributeListEntry> nested;
1031 if (!process_attributes(base_record_number, ext_record, info, nested, error)) {
1032 error.clear();
1033 continue;
1034 }
1035 }
1036 return true;
1037 }
1038
1039 bool parse_file_record(uint64_t record_number,
1040 const std::vector<unsigned char>& record,
1041 NtfsFileRecordInfo& info,
1042 std::string& error) {
1043 if (record.size() < 56 || std::string(reinterpret_cast<const char*>(record.data()), 4) != "FILE") {
1044 error = trs("Invalid MFT record signature");
1045 return false;
1046 }
1047 std::vector<NtfsAttributeListEntry> attr_list;
1048 if (!process_attributes(record_number, record, info, attr_list, error)) return false;
1049 if (!attr_list.empty()) {
1050 if (!load_attribute_list_extensions(record_number, info, attr_list, error)) return false;
1051 }
1052 for (auto& [name, stream] : info.streams) {
1053 if (stream.resident && stream.data_size == 0) stream.data_size = stream.resident_data.size();
1054 if (!stream.resident && stream.data_size == 0) {
1055 for (const auto& run : stream.runs) stream.data_size += run.length;
1056 }
1057 }
1058 return true;
1059 }
1060
1061 // ============================================================
1062 // $I30 directory index walker
1063 // ============================================================
1064 //
1065 // Each NTFS directory has a B+tree index keyed by filename. Small
1066 // directories fit entirely in $INDEX_ROOT (resident attribute, type
1067 // 0x90, name "$I30"). Larger ones spill into $INDEX_ALLOCATION
1068 // (non-resident, type 0xA0, name "$I30") which holds INDX node
1069 // blocks of typically 4 KiB each.
1070 //
1071 // Each INDEX_ENTRY contains:
1072 // __le64 file_ref // MFT record number + sequence (high 16)
1073 // __le16 entry_size
1074 // __le16 name_attr_size // size of $FILE_NAME content below
1075 // __le32 flags // 0x01 = has child node, 0x02 = last
1076 // $FILE_NAME attribute (parent_ref + timestamps + name UTF-16)
1077 // [__le64 child_vcn] // present if flags & 0x01
1078 //
1079 // We extract (child_record_number, child_sequence_number, filename)
1080 // for every leaf entry. The filename here is the AUTHORITATIVE name
1081 // the parent uses to reference the child -- bypassing any out-of-date
1082 // $FILE_NAME in the child's MFT record.
1083
1084 struct I30Entry {
1087 std::string filename;
1088 uint8_t name_namespace {0}; // 0=POSIX, 1=Win32, 2=DOS, 3=both
1089 };
1090
1091 // Parse all leaf entries from a buffer of INDEX_ENTRY records.
1092 // `entries_data` is the byte range starting at the first entry;
1093 // `total_size` is its length. Walks until it hits an entry with
1094 // flag 0x02 (= last) or runs out of bytes.
1095 void parse_index_entries(const unsigned char* entries_data,
1096 size_t total_size,
1097 std::vector<I30Entry>& out) {
1098 size_t off = 0;
1099 while (off + 16 <= total_size) {
1100 const unsigned char* e = entries_data + off;
1101 const uint16_t entry_size = le16(e + 8);
1102 const uint32_t flags = le32(e + 12);
1103 if (entry_size < 16 || off + entry_size > total_size) break;
1104
1105 // The "last" flag (0x02) marks the end-of-entries sentinel
1106 // which has NO real data, just the optional child VCN.
1107 const bool is_last = (flags & 0x02) != 0;
1108
1109 if (!is_last) {
1110 // file_ref at offset 0..7
1111 const uint64_t fref = le64(e + 0);
1112 I30Entry ie;
1113 ie.child_record_number = fref & 0x0000FFFFFFFFFFFFull;
1114 ie.child_sequence_number = static_cast<uint16_t>(fref >> 48);
1115 // $FILE_NAME starts at offset 16 (after the 16-byte
1116 // INDEX_ENTRY header). Layout:
1117 // parent_ref (8) + 4*timestamp (32) + alloc_size (8)
1118 // + real_size (8) + flags (4) + reparse_or_eas (4)
1119 // + name_length (1) + name_namespace (1)
1120 // + name UTF-16LE [name_length]
1121 const uint16_t name_attr_size = le16(e + 10);
1122 if (name_attr_size >= 66 && 16 + name_attr_size <= entry_size) {
1123 const unsigned char* fn = e + 16;
1124 const uint8_t name_chars = fn[64];
1125 const uint8_t name_ns = fn[65];
1126 if (66 + name_chars * 2u <= name_attr_size) {
1127 ie.filename = decode_utf16le_ascii(fn + 66,
1128 static_cast<size_t>(name_chars) * 2);
1129 ie.name_namespace = name_ns;
1130 }
1131 }
1132 if (!ie.filename.empty()) {
1133 out.push_back(ie);
1134 }
1135 }
1136
1137 off += entry_size;
1138 if (is_last) break;
1139 }
1140 }
1141
1142 // Read one INDX block from $INDEX_ALLOCATION runs at logical
1143 // VCN `vcn`. `irs` is the index record size in bytes.
1144 bool read_indx_block(const std::vector<NtfsDataRun>& runs,
1145 uint64_t vcn,
1146 uint32_t irs,
1147 std::vector<unsigned char>& out,
1148 std::string& error) {
1149 if (irs == 0) return false;
1150 out.clear();
1151 out.resize(irs);
1152 // VCN-to-LCN translation: walk runs, find which run covers
1153 // the byte offset = vcn * irs.
1154 const uint64_t logical_off = vcn * irs;
1155 uint64_t cursor = 0;
1156 for (const auto& run : runs) {
1157 const uint64_t run_bytes = run.length;
1158 if (logical_off < cursor + run_bytes) {
1159 if (run.sparse) {
1160 error = "indx block in sparse run";
1161 return false;
1162 }
1163 const uint64_t inner = logical_off - cursor;
1164 const uint64_t phys = run.offset + inner;
1165 if (!reader_.read_at(phys, out.data(), irs)) {
1166 error = "read failure on INDX block";
1167 return false;
1168 }
1169 // Validate magic
1170 if (out.size() < 4 || std::memcmp(out.data(), "INDX", 4) != 0) {
1171 error = "INDX magic mismatch";
1172 return false;
1173 }
1174 // Apply USN/USA fixup (same algorithm as MFT)
1175 if (!apply_fixup(out, error)) return false;
1176 return true;
1177 }
1178 cursor += run_bytes;
1179 }
1180 error = "VCN out of allocation range";
1181 return false;
1182 }
1183
1184 // Walks $INDEX_ROOT + (recursively) $INDEX_ALLOCATION blocks for a
1185 // directory record, collecting all (filename, child_ref) pairs.
1186 void walk_i30(const NtfsFileRecordInfo& dir_info,
1187 std::vector<I30Entry>& out_entries,
1188 std::vector<std::string>& logs) {
1189 if (dir_info.index_root_data.size() < 32) return;
1190 const unsigned char* root = dir_info.index_root_data.data();
1191 // After the 16-byte index attribute header (type+collation+
1192 // size+clusters_per_record+reserved), the INDEX_HEADER starts:
1193 // __le32 first_entry_offset (relative to INDEX_HEADER start)
1194 // __le32 used_size
1195 // __le32 total_size
1196 // __u8 flags (bit 0 = "has child VCN" entries)
1197 // __u8 reserved[3]
1198 const unsigned char* ihdr = root + 16;
1199 const uint32_t first_off = le32(ihdr + 0);
1200 const uint32_t used_sz = le32(ihdr + 4);
1201 if (16 + first_off > dir_info.index_root_data.size() ||
1202 16 + used_sz > dir_info.index_root_data.size()) return;
1203 const unsigned char* root_entries = ihdr + first_off;
1204 const size_t root_entries_size = used_sz > first_off
1205 ? used_sz - first_off : 0;
1206 parse_index_entries(root_entries, root_entries_size, out_entries);
1207
1208 // Recurse into INDEX_ALLOCATION blocks. For simplicity we
1209 // walk the runlist linearly and parse every block whose magic
1210 // is "INDX" (we don't follow the B+tree pointer structure;
1211 // we just enumerate all leaves). This works because each
1212 // INDX block is self-describing.
1213 if (dir_info.index_allocation_runs.empty() ||
1214 dir_info.index_record_size == 0) return;
1215 const uint32_t irs = dir_info.index_record_size;
1216 // Total size = sum of run lengths. Walk every irs-sized block.
1217 uint64_t total = 0;
1218 for (const auto& r : dir_info.index_allocation_runs) total += r.length;
1219 const uint64_t num_blocks = total / irs;
1220 constexpr uint64_t kMaxBlocksPerDir = 100000; // sanity cap
1221 const uint64_t cap = std::min<uint64_t>(num_blocks, kMaxBlocksPerDir);
1222 for (uint64_t vcn = 0; vcn < cap; ++vcn) {
1223 std::vector<unsigned char> block;
1224 std::string err;
1226 vcn, irs, block, err)) {
1227 // Not all VCNs in the allocation range are necessarily
1228 // valid INDX blocks (they may be unallocated). The
1229 // $BITMAP would tell us which are; we just skip on
1230 // failure here.
1231 continue;
1232 }
1233 // INDX block layout (after the 24-byte header):
1234 // __le32 first_entry_offset (relative to INDEX_HEADER)
1235 // __le32 used_size
1236 // __le32 total_size
1237 // __u8 flags
1238 // __u8 reserved[3]
1239 // INDEX_HEADER starts at offset 24 of the INDX block.
1240 if (block.size() < 24 + 16) continue;
1241 const unsigned char* h = block.data() + 24;
1242 const uint32_t fo = le32(h + 0);
1243 const uint32_t us = le32(h + 4);
1244 if (24 + fo > block.size() || 24 + us > block.size()) continue;
1245 const unsigned char* ents = h + fo;
1246 const size_t ents_sz = us > fo ? us - fo : 0;
1247 parse_index_entries(ents, ents_sz, out_entries);
1248 }
1249 if (!out_entries.empty()) {
1250 std::ostringstream m;
1251 m << "NTFS $I30 walker: directory MFT#"
1252 << dir_info.record_number << " yielded "
1253 << out_entries.size() << " index entries";
1254 logs.push_back(m.str());
1255 }
1256 }
1257
1258
1259 fs::path build_relative_path(uint64_t record_number, const std::map<uint64_t, NtfsFileRecordInfo>& records) {
1260 std::vector<std::string> parts;
1261 std::set<uint64_t> seen;
1262 uint64_t current = record_number;
1263 bool sequence_mismatch = false;
1264 while (seen.insert(current).second) {
1265 auto it = records.find(current);
1266 if (it == records.end()) break;
1267 const auto& info = it->second;
1268 if (!info.name.empty() && info.name != ".") parts.push_back(sanitize_component(info.name));
1269 if (current == 5 || info.parent_record_number == current || info.parent_record_number == 0) break;
1270 // Sequence number check: the child claims its parent had
1271 // sequence `parent_sequence_number` when the dirent was
1272 // written. If the parent's CURRENT own_sequence_number
1273 // differs, the parent record was reused for a different
1274 // file/directory; our path is mythical.
1275 const uint16_t expected_parent_seq = info.parent_sequence_number;
1276 auto pit = records.find(info.parent_record_number);
1277 if (pit != records.end() && expected_parent_seq != 0 &&
1278 pit->second.own_sequence_number != 0 &&
1279 pit->second.own_sequence_number != expected_parent_seq) {
1280 sequence_mismatch = true;
1281 break;
1282 }
1283 current = info.parent_record_number;
1284 if (current == 5) break;
1285 }
1286 if (parts.empty()) return {};
1287 fs::path out;
1288 for (auto it = parts.rbegin(); it != parts.rend(); ++it) out /= *it;
1289 // Mark seq-mismatched paths so the caller can route them to
1290 // a separate "orphaned-seq-mismatch" subdir if desired.
1291 if (sequence_mismatch) {
1292 out = fs::path("orphaned-seq-mismatch") / out;
1293 }
1294 return out;
1295 }
1296
1297 bool stream_exportable(const NtfsDataStream& stream) const {
1298 return !stream.encrypted && !stream.unsupported_layout && (stream.resident || !stream.runs.empty() || stream.data_size == 0);
1299 }
1300
1301 fs::path output_path_for_stream(const fs::path& base, const std::string& stream_name) {
1302 if (stream_name.empty()) return unique_path(base);
1303 const std::string marker = "__ADS_" + sanitize_component(stream_name);
1304 fs::path named = base.parent_path() /
1305 fs::path(base.stem().string() + marker + base.extension().string());
1306 return unique_path(named);
1307 }
1308
1309 fs::path unique_path(const fs::path& candidate) {
1310 fs::path unique = candidate;
1311 int suffix = 1;
1312 while (fs::exists(unique)) {
1313 unique = candidate.parent_path() /
1314 fs::path(candidate.stem().string() + "_" + std::to_string(suffix++) + candidate.extension().string());
1315 }
1316 return unique;
1317 }
1318
1319
1321 const fs::path& base_path,
1322 std::vector<RecoveredFileRecord>& recovered,
1323 std::vector<std::string>& logs) {
1324 if (!info.has_reparse_point || info.reparse_data.size() < 8) return;
1325
1326 const uint32_t tag = le32(info.reparse_data.data());
1327 const uint16_t data_len = le16(info.reparse_data.data() + 4);
1328
1329 // Common reparse tags:
1330 constexpr uint32_t IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003; // junction
1331 constexpr uint32_t IO_REPARSE_TAG_SYMLINK = 0xA000000C;
1332
1333 // Try to extract a target path string from the reparse data
1334 // and create a real symlink at base_path.
1335 std::string target_str;
1336 bool target_extracted = false;
1337 if (tag == IO_REPARSE_TAG_SYMLINK && info.reparse_data.size() >= 20) {
1338 // Layout (after the 8-byte common header):
1339 // __le16 substitute_name_offset
1340 // __le16 substitute_name_length
1341 // __le16 print_name_offset
1342 // __le16 print_name_length
1343 // __le32 flags
1344 // path_buffer[...]
1345 const uint16_t sub_off = le16(info.reparse_data.data() + 8);
1346 const uint16_t sub_len = le16(info.reparse_data.data() + 10);
1347 const uint16_t print_off = le16(info.reparse_data.data() + 12);
1348 const uint16_t print_len = le16(info.reparse_data.data() + 14);
1349 // Path buffer is at +20 (after flags). Prefer "print name"
1350 // (= what the user sees, no \??\ prefix).
1351 const size_t buf_start = 20;
1352 if (print_len > 0 &&
1353 buf_start + print_off + print_len <= info.reparse_data.size()) {
1354 target_str = decode_utf16le_ascii(
1355 info.reparse_data.data() + buf_start + print_off,
1356 print_len);
1357 target_extracted = !target_str.empty();
1358 } else if (sub_len > 0 &&
1359 buf_start + sub_off + sub_len <= info.reparse_data.size()) {
1360 target_str = decode_utf16le_ascii(
1361 info.reparse_data.data() + buf_start + sub_off,
1362 sub_len);
1363 // Strip leading "\??\" or "\\?\" prefix (Windows
1364 // namespace artefacts)
1365 if (target_str.rfind("\\??\\", 0) == 0 ||
1366 target_str.rfind("\\\\?\\", 0) == 0) {
1367 target_str = target_str.substr(4);
1368 }
1369 target_extracted = !target_str.empty();
1370 }
1371 } else if (tag == IO_REPARSE_TAG_MOUNT_POINT &&
1372 info.reparse_data.size() >= 16) {
1373 // Junction layout (no flags field):
1374 // __le16 substitute_name_offset
1375 // __le16 substitute_name_length
1376 // __le16 print_name_offset
1377 // __le16 print_name_length
1378 // path_buffer[...]
1379 const uint16_t print_off = le16(info.reparse_data.data() + 12);
1380 const uint16_t print_len = le16(info.reparse_data.data() + 14);
1381 const size_t buf_start = 16;
1382 if (print_len > 0 &&
1383 buf_start + print_off + print_len <= info.reparse_data.size()) {
1384 target_str = decode_utf16le_ascii(
1385 info.reparse_data.data() + buf_start + print_off,
1386 print_len);
1387 target_extracted = !target_str.empty();
1388 }
1389 }
1390
1391 if (target_extracted) {
1392 // Convert backslashes to forward slashes for POSIX
1393 std::replace(target_str.begin(), target_str.end(), '\\', '/');
1394 // Try to create a real symlink at base_path
1395 std::error_code ec;
1396 const fs::path link_path = unique_path(base_path);
1397 fs::create_symlink(target_str, link_path, ec);
1398 if (!ec) {
1399 recovered.push_back({
1400 link_path.string(), "NTFS",
1401 link_path.filename().string(),
1402 static_cast<uint64_t>(target_str.size()),
1403 "full", tag == IO_REPARSE_TAG_SYMLINK
1404 ? "symlink (recreated)"
1405 : "junction (recreated)"});
1406 logs.push_back(trs("Recovered NTFS reparse as symlink:") +
1407 std::string(" ") + link_path.string() +
1408 " -> " + target_str);
1409 return;
1410 }
1411 // If symlink creation failed (filesystem doesn't support
1412 // it, permission denied, or target name has illegal chars),
1413 // fall through to sidecar.
1414 }
1415
1416 // Sidecar fallback: write the metadata as a text file
1417 const fs::path sidecar = unique_path(base_path.parent_path() /
1418 fs::path(base_path.stem().string() + "__reparse.txt"));
1419 std::ofstream out(sidecar, std::ios::binary);
1420 if (!out) return;
1421 out << "reparse_tag=" << tag << "\n";
1422 out << "reparse_data_length=" << data_len << "\n";
1423 if (target_extracted) {
1424 out << "extracted_target=" << target_str << "\n";
1425 }
1426 out << "raw_bytes=";
1427 static const char* hex = "0123456789ABCDEF";
1428 for (unsigned char c : info.reparse_data) {
1429 out << hex[(c >> 4) & 0xF] << hex[c & 0xF];
1430 }
1431 out << "\n";
1432 recovered.push_back({sidecar.string(), "NTFS", sidecar.filename().string(), static_cast<uint64_t>(info.reparse_data.size()), "metadata", "reparse metadata"});
1433 logs.push_back(trs("Recovered NTFS reparse metadata:") + std::string(" ") + sidecar.string());
1434 }
1435
1437 const fs::path& base_path,
1438 std::vector<RecoveredFileRecord>& recovered,
1439 std::vector<std::string>& logs) {
1440 if (info.name != "$UsnJrnl") return;
1441 auto it = info.streams.find("$J");
1442 if (it == info.streams.end()) return;
1443 const fs::path summary = unique_path(base_path.parent_path() /
1444 fs::path(base_path.stem().string() + "__usn_summary.txt"));
1445 std::ofstream out(summary, std::ios::binary);
1446 if (!out) return;
1447 out << "stream=$J\n";
1448 out << "size=" << it->second.data_size << "\n";
1449 out << "resident=" << (it->second.resident ? 1 : 0) << "\n";
1450 out << "compressed=" << (it->second.compressed ? 1 : 0) << "\n";
1451 out << "sparse=" << (it->second.sparse ? 1 : 0) << "\n";
1452 recovered.push_back({summary.string(), "NTFS", summary.filename().string(), 0, "metadata", "USN summary"});
1453 logs.push_back(trs("Recovered NTFS USN summary:") + std::string(" ") + summary.string());
1454 }
1455
1456
1458 std::vector<unsigned char>& data,
1459 std::string& error) {
1460 data.clear();
1461 if (stream.resident) {
1462 data = stream.resident_data;
1463 if (data.size() > stream.data_size) data.resize(static_cast<size_t>(stream.data_size));
1464 if (data.size() < stream.data_size) data.resize(static_cast<size_t>(stream.data_size), 0);
1465 return true;
1466 }
1467 if (stream.compressed) return decode_compressed_stream(stream, data, error);
1468 return read_runs_to_buffer(stream.runs, stream.data_size, data, error);
1469 }
1470
1471 void write_usn_reason_flags(std::ostream& out, uint32_t reason) {
1472 struct FlagName { uint32_t flag; const char* name; };
1473 static constexpr FlagName names[] = {
1474 {0x00000001u, "DATA_OVERWRITE"},
1475 {0x00000002u, "DATA_EXTEND"},
1476 {0x00000004u, "DATA_TRUNCATION"},
1477 {0x00000010u, "NAMED_DATA_OVERWRITE"},
1478 {0x00000020u, "NAMED_DATA_EXTEND"},
1479 {0x00000040u, "NAMED_DATA_TRUNCATION"},
1480 {0x00000100u, "FILE_CREATE"},
1481 {0x00000200u, "FILE_DELETE"},
1482 {0x00000400u, "EA_CHANGE"},
1483 {0x00000800u, "SECURITY_CHANGE"},
1484 {0x00001000u, "RENAME_OLD_NAME"},
1485 {0x00002000u, "RENAME_NEW_NAME"},
1486 {0x00004000u, "INDEXABLE_CHANGE"},
1487 {0x00008000u, "BASIC_INFO_CHANGE"},
1488 {0x00010000u, "HARD_LINK_CHANGE"},
1489 {0x00020000u, "COMPRESSION_CHANGE"},
1490 {0x00040000u, "ENCRYPTION_CHANGE"},
1491 {0x00080000u, "OBJECT_ID_CHANGE"},
1492 {0x00100000u, "REPARSE_POINT_CHANGE"},
1493 {0x00200000u, "STREAM_CHANGE"},
1494 {0x00400000u, "TRANSACTED_CHANGE"},
1495 {0x00800000u, "INTEGRITY_CHANGE"},
1496 {0x80000000u, "CLOSE"},
1497 };
1498 bool first = true;
1499 for (const auto& entry : names) {
1500 if ((reason & entry.flag) == 0) continue;
1501 if (!first) out << '|';
1502 out << entry.name;
1503 first = false;
1504 }
1505 if (first) out << '0';
1506 }
1507
1508 bool export_stream(const NtfsDataStream& stream, const fs::path& out_path, std::string& error) {
1509 std::vector<unsigned char> data;
1510 if (stream.resident) {
1511 data = stream.resident_data;
1512 } else if (stream.compressed) {
1513 if (!decode_compressed_stream(stream, data, error)) return false;
1514 } else {
1515 if (!read_runs_to_buffer(stream.runs, stream.data_size, data, error)) return false;
1516 }
1517 if (data.size() > stream.data_size) data.resize(static_cast<size_t>(stream.data_size));
1518 std::ofstream out(out_path, std::ios::binary);
1519 if (!out) {
1520 error = trs("Unable to create output file") + std::string(": ") + out_path.string();
1521 return false;
1522 }
1523 if (!data.empty()) out.write(reinterpret_cast<const char*>(data.data()), static_cast<std::streamsize>(data.size()));
1524 return static_cast<bool>(out);
1525 }
1526
1527 // ============================================================
1528 // $LogFile transaction journal pass (NTFS log records recovery)
1529 // ============================================================
1530 //
1531 // The $LogFile (MFT record 2) is NTFS's WAL transaction journal.
1532 // Its layout (from the Linux NTFS-3G/Tuxera driver source and
1533 // [Russinovich, Solomon, "Windows Internals"]):
1534 //
1535 // First 8 KiB: two RESTART AREAS (4 KiB each, alternating).
1536 // Each starts with "RSTR" magic (0x52 0x53 0x54 0x52),
1537 // followed by a fixup, then RESTART_AREA + LOG_CLIENT_RECORD.
1538 //
1539 // Rest of the file (= 64 MiB typically): RCRD pages of 4 KiB,
1540 // each starting with "RCRD" magic (0x52 0x43 0x52 0x44).
1541 // Each page contains a 64-byte header + a sequence of LSN
1542 // records (LOG_RECORD_HEADER + redo/undo data).
1543 //
1544 // We do NOT do a real transaction replay -- that would require
1545 // tracking Open Attribute Tables, dirty pages, transaction
1546 // commit/abort state, and is hundreds more lines. Instead we do
1547 // a SIMPLE-AND-USEFUL pass:
1548 //
1549 // For each LSN record whose redo_op is InitializeFileRecordSegment
1550 // (== 0x10), the redo_data IS a complete MFT file record. If that
1551 // record number is now FREE in the live MFT (= file was deleted)
1552 // OR the live record has a different sequence number (= MFT slot
1553 // was reused for another file), we save the journal copy as a
1554 // recovered file. This catches recently-deleted files that didn't
1555 // make it to a checkpoint flush yet.
1556 //
1557 // Scope limits:
1558 // - We only handle the InitializeFileRecordSegment opcode.
1559 // - We do not follow redo/undo chains (= no rollback semantics).
1560 // - LZNT1-compressed log records are not decompressed (rare).
1561
1562 static constexpr uint32_t kLogPageMagicRSTR = 0x52545352u; // "RSTR" little-endian
1563 static constexpr uint32_t kLogPageMagicRCRD = 0x44524352u; // "RCRD" little-endian
1564 static constexpr uint16_t kRedoOpInitFileRecord = 0x0010;
1565
1566 void scan_logfile(const fs::path& out_root,
1567 const std::map<uint64_t, NtfsFileRecordInfo>& live_records,
1568 std::vector<RecoveredFileRecord>& recovered,
1569 std::vector<std::string>& logs) {
1570 // Step 1: read $LogFile (MFT record 2)
1571 std::vector<unsigned char> logrec;
1572 std::string err;
1573 if (!read_mft_record(2, logrec, err)) {
1574 logs.push_back(trs("NTFS $LogFile: cannot read MFT record 2 (skipping)"));
1575 return;
1576 }
1577 NtfsFileRecordInfo loginfo;
1578 if (!parse_file_record(2, logrec, loginfo, err)) {
1579 logs.push_back(trs("NTFS $LogFile: parse failed (skipping)"));
1580 return;
1581 }
1582 auto data_it = loginfo.streams.find("");
1583 if (data_it == loginfo.streams.end()) {
1584 logs.push_back(trs("NTFS $LogFile: no $DATA stream"));
1585 return;
1586 }
1587 const NtfsDataStream& logdata = data_it->second;
1588
1589 // Cap LogFile size we read at 256 MB (typical is 64 MB). This
1590 // protects us from runaway corrupt sizes.
1591 constexpr uint64_t kLogFileMaxBytes = 256ull * 1024 * 1024;
1592 const uint64_t to_read = std::min<uint64_t>(logdata.data_size,
1593 kLogFileMaxBytes);
1594 if (to_read < 8192) {
1595 logs.push_back(trs("NTFS $LogFile: too small (<8 KiB)"));
1596 return;
1597 }
1598 std::vector<unsigned char> logbuf;
1599 logbuf.reserve(static_cast<size_t>(to_read));
1600 std::string rs_err;
1601 if (!read_runs_to_buffer(logdata.runs, to_read, logbuf, rs_err)) {
1602 logs.push_back(trs("NTFS $LogFile: cannot read content: ") + rs_err);
1603 return;
1604 }
1605 logs.push_back(trs("NTFS $LogFile: loaded ") +
1606 std::to_string(logbuf.size()) +
1607 trs(" bytes; scanning for recoverable file records"));
1608
1609 // Step 2: iterate RCRD pages (4 KiB each), starting after the
1610 // restart area (= first 8 KiB).
1611 constexpr size_t kPageSize = 4096;
1612 size_t init_redo_records_seen = 0;
1613 size_t recovered_from_log = 0;
1614 size_t pages_scanned = 0;
1615
1616 fs::path log_out = out_root / "logfile_recovered";
1617 bool log_out_created = false;
1618
1619 for (size_t page_off = 8 * 1024; page_off + kPageSize <= logbuf.size();
1620 page_off += kPageSize) {
1621 const unsigned char* page = logbuf.data() + page_off;
1622 const uint32_t magic = le32(page);
1623 if (magic != kLogPageMagicRCRD) continue;
1624 ++pages_scanned;
1625
1626 // RCRD page header is 64 bytes. After fixup, the rest is
1627 // a sequence of LSN records. Layout (per ntfs-3g logfile.h):
1628 // 0x00 __le32 magic ('RCRD')
1629 // 0x04 __le16 usa_ofs (update sequence array offset)
1630 // 0x06 __le16 usa_count
1631 // 0x08 __le64 lsn (chkdsk lsn)
1632 // 0x10 __le32 flags
1633 // 0x14 __le16 page_count
1634 // 0x16 __le16 page_position
1635 // 0x18 __le16 next_record_offset (relative to page start)
1636 // 0x1A ... unused
1637 //
1638 // We don't apply fixup to the WHOLE page -- the LogFile
1639 // fixup logic is more complex (per-record). We rely on
1640 // best-effort scanning instead.
1641
1642 const uint16_t next_rec_off = le16(page + 0x18);
1643 if (next_rec_off < 0x40 || next_rec_off >= kPageSize) continue;
1644
1645 // Walk LOG_RECORD_HEADER entries within the page.
1646 // Each header is 0x30 bytes:
1647 // 0x00 __le64 this_lsn
1648 // 0x08 __le64 client_previous_lsn
1649 // 0x10 __le64 client_undo_next_lsn
1650 // 0x18 __le32 client_data_length (= length AFTER header)
1651 // 0x1C __le16 client_id (seq + index)
1652 // 0x1E __le16 reserved
1653 // 0x20 __le32 record_type (1=normal, 2=checkpoint)
1654 // 0x24 __le32 transaction_id
1655 // 0x28 __le16 flags (LOG_RECORD_MULTI_PAGE etc.)
1656 // 0x2A __le16 reserved x3
1657 // 0x30 __le16 redo_op
1658 // 0x32 __le16 undo_op
1659 // 0x34 __le16 redo_offset (= offset of redo_data
1660 // within this entry)
1661 // 0x36 __le16 redo_length
1662 // 0x38 __le16 undo_offset
1663 // 0x3A __le16 undo_length
1664 // 0x3C __le16 target_attribute
1665 // 0x3E __le16 lcns_to_follow
1666 // 0x40 __le16 record_offset
1667 // 0x42 __le16 attribute_offset
1668 // 0x44 __le16 cluster_block_offset
1669 // 0x46 __le16 reserved
1670 // 0x48 __le64 target_vcn
1671 // 0x50 __le64 lcn_list[lcns_to_follow]
1672 // Then redo_data and undo_data at the offsets given.
1673 //
1674 // Total log-record header = 0x30 bytes BEFORE LCN list.
1675 // For our purposes (find redo_op = InitFileRecord) we only
1676 // need the first ~0x40 bytes.
1677
1678 size_t pos = 0x40; // first LSN record after page header
1679 const size_t page_end = next_rec_off;
1680 while (pos + 0x40 <= page_end) {
1681 const unsigned char* lr = page + pos;
1682 // client_data_length includes the body but NOT the
1683 // 0x30-byte header. Sanity-check it.
1684 const uint32_t client_data_length = le32(lr + 0x18);
1685 const uint16_t redo_op = le16(lr + 0x30);
1686 const uint16_t redo_off = le16(lr + 0x34);
1687 const uint16_t redo_len = le16(lr + 0x36);
1688 const uint16_t target_attr = le16(lr + 0x3C);
1689
1690 // Plausibility: the entry must fit within the page
1691 if (client_data_length > 65536) break;
1692 const size_t entry_total =
1693 0x30 + static_cast<size_t>(client_data_length);
1694 if (entry_total > 65536 || pos + entry_total > kPageSize) break;
1695
1696 if (redo_op == kRedoOpInitFileRecord &&
1697 redo_len >= 1024 && redo_len <= 4096) {
1698 ++init_redo_records_seen;
1699 // The redo_data IS a complete MFT file record.
1700 // Locate it within the LSN entry.
1701 if (redo_off + redo_len > entry_total) {
1702 pos += entry_total > 0x30 ? entry_total : 0x30;
1703 continue;
1704 }
1705 const unsigned char* mftrec_raw = lr + redo_off;
1706 std::vector<unsigned char> mftrec(
1707 mftrec_raw, mftrec_raw + redo_len);
1708
1709 // Apply fixup (USN/USA) to recover original sectors
1710 std::string fix_err;
1711 if (!apply_fixup(mftrec, fix_err)) continue;
1712
1713 // Parse it as a regular MFT record
1714 NtfsFileRecordInfo info;
1715 if (!parse_file_record(0, mftrec, info, fix_err)) continue;
1716 if (info.is_directory || info.name.empty()) continue;
1717
1718 // target_attribute is the MFT record number this log
1719 // entry refers to. Use it as our "record number".
1720 const uint64_t mft_no = target_attr;
1721 info.record_number = mft_no;
1722
1723 // Skip if the live MFT already has this record
1724 // (with same seq number). We just check presence
1725 // here -- the seq comparison would require parsing
1726 // the seq field in both records, which we skip for
1727 // simplicity.
1728 if (live_records.count(mft_no) > 0) continue;
1729
1730 // Try to export every $DATA stream we find
1731 bool exported_any = false;
1732 for (const auto& [stream_name, stream] : info.streams) {
1733 if (!stream_exportable(stream)) continue;
1734 if (!log_out_created) {
1735 std::error_code ec;
1736 fs::create_directories(log_out, ec);
1737 log_out_created = true;
1738 }
1739 std::string fname = sanitize_component(info.name);
1740 if (!stream_name.empty()) fname += "_" + sanitize_component(stream_name);
1741 fname = "mft" + std::to_string(mft_no) + "_" + fname;
1742 const fs::path target = log_out / fname;
1743 std::string ex_err;
1744 if (export_stream(stream, target, ex_err)) {
1745 recovered.push_back({
1746 target.string(), "NTFS",
1747 ("logfile_recovered/" + fname),
1748 stream.data_size, "partial",
1749 "$LogFile-redo"});
1750 exported_any = true;
1751 }
1752 }
1753 if (exported_any) ++recovered_from_log;
1754 }
1755
1756 pos += entry_total > 0x30 ? entry_total : 0x30;
1757 // Round to 8-byte alignment (LSN records are 8-aligned)
1758 pos = (pos + 7u) & ~size_t{7u};
1759 }
1760 }
1761
1762 std::ostringstream m;
1763 m << trs("NTFS $LogFile: ") << pages_scanned
1764 << trs(" RCRD pages scanned, ") << init_redo_records_seen
1765 << trs(" InitFileRecord redo entries found, ") << recovered_from_log
1766 << trs(" file records recovered to logfile_recovered/");
1767 logs.push_back(m.str());
1768 }
1769};
1770
1772
1773bool NtfsRecovery::recover(uint64_t volume_offset,
1774 const std::string& output_dir,
1775 std::vector<RecoveredFileRecord>& recovered,
1776 std::vector<std::string>& logs,
1777 std::string& error) {
1779 return impl.recover(volume_offset, output_dir, recovered, logs, error);
1780}
#define _(s)
bool read_at(uint64_t offset, void *buffer, size_t len)
bool export_stream(const NtfsDataStream &stream, const fs::path &out_path, std::string &error)
fs::path build_relative_path(uint64_t record_number, const std::map< uint64_t, NtfsFileRecordInfo > &records)
bool parse_attribute_list_entries(const unsigned char *value, uint32_t value_len, std::vector< NtfsAttributeListEntry > &out)
void write_usn_reason_flags(std::ostream &out, uint32_t reason)
bool load_attribute_list_extensions(uint64_t base_record_number, NtfsFileRecordInfo &info, const std::vector< NtfsAttributeListEntry > &attr_list, std::string &error)
bool stream_exportable(const NtfsDataStream &stream) const
DiskReader & reader_
bool process_attributes(uint64_t base_record_number, const std::vector< unsigned char > &record, NtfsFileRecordInfo &info, std::vector< NtfsAttributeListEntry > &attr_list, std::string &error)
static constexpr uint16_t kRedoOpInitFileRecord
static void merge_runs(std::vector< NtfsDataRun > &dst, const std::vector< NtfsDataRun > &src)
bool read_stream_data(const NtfsDataStream &stream, std::vector< unsigned char > &data, std::string &error)
void scan_logfile(const fs::path &out_root, const std::map< uint64_t, NtfsFileRecordInfo > &live_records, std::vector< RecoveredFileRecord > &recovered, std::vector< std::string > &logs)
void walk_i30(const NtfsFileRecordInfo &dir_info, std::vector< I30Entry > &out_entries, std::vector< std::string > &logs)
static constexpr uint32_t kLogPageMagicRCRD
bool read_boot_sector(uint64_t volume_offset, std::string &error)
NtfsDataStream & stream_for(NtfsFileRecordInfo &info, const std::string &name)
void export_usn_summary_if_needed(const NtfsFileRecordInfo &info, const fs::path &base_path, std::vector< RecoveredFileRecord > &recovered, std::vector< std::string > &logs)
std::vector< std::pair< uint64_t, uint64_t > > mft_ranges_
bool decode_compressed_stream(const NtfsDataStream &stream, std::vector< unsigned char > &out, std::string &error)
std::vector< std::pair< uint64_t, uint64_t > > mftmirr_ranges_
bool read_mft_record(uint64_t record_number, std::vector< unsigned char > &out, std::string &error)
bool read_mft_record_from_mirror(uint64_t record_number, std::vector< unsigned char > &out, std::string &error)
NtfsVolumeInfo volume_
uint64_t cluster_to_offset(uint64_t cluster) const
void parse_index_entries(const unsigned char *entries_data, size_t total_size, std::vector< I30Entry > &out)
bool parse_file_record(uint64_t record_number, const std::vector< unsigned char > &record, NtfsFileRecordInfo &info, std::string &error)
NtfsRecoveryImpl(DiskReader &reader)
bool read_raw_record(uint64_t record_number, std::vector< unsigned char > &out, std::string &error)
bool apply_fixup(std::vector< unsigned char > &record, std::string &error)
bool read_indx_block(const std::vector< NtfsDataRun > &runs, uint64_t vcn, uint32_t irs, std::vector< unsigned char > &out, 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)
fs::path output_path_for_stream(const fs::path &base, const std::string &stream_name)
bool read_runs_to_buffer(const std::vector< NtfsDataRun > &runs, uint64_t expected_size, std::vector< unsigned char > &out, std::string &error)
void export_reparse_sidecar(const NtfsFileRecordInfo &info, const fs::path &base_path, std::vector< RecoveredFileRecord > &recovered, std::vector< std::string > &logs)
static constexpr uint32_t kLogPageMagicRSTR
std::vector< NtfsDataRun > parse_runlist(const unsigned char *runlist, size_t max_len, uint64_t start_vcn, bool &has_sparse, bool &malformed)
fs::path unique_path(const fs::path &candidate)
DiskReader & reader_
NtfsRecovery(DiskReader &reader)
bool recover(uint64_t volume_offset, const std::string &output_dir, std::vector< RecoveredFileRecord > &recovered, std::vector< std::string > &logs, std::string &error)
std::string trs(const char *msgid)
Definition i18n.cpp:22
uint64_t le64(const unsigned char *p)
std::string decode_utf16le_ascii(const unsigned char *data, size_t byte_count)
bool lznt1_decompress(const std::vector< unsigned char > &src, size_t expected_size, std::vector< unsigned char > &dst)
std::string sanitize_component(const std::string &in)
uint32_t le32(const unsigned char *p)
uint16_t le16(const unsigned char *p)
std::string sanitize_component(const std::string &in)
std::string utf16le_to_utf8(const unsigned char *data, std::size_t byte_count)