Skip to content
API Documentation
BastionGuard™
NtfsRecoveryImpl Class Reference

Classes

struct  I30Entry

Public Member Functions

 NtfsRecoveryImpl (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)

Private Member Functions

bool read_boot_sector (uint64_t volume_offset, std::string &error)
uint64_t cluster_to_offset (uint64_t cluster) const
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_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)
std::vector< NtfsDataRun > parse_runlist (const unsigned char *runlist, size_t max_len, uint64_t start_vcn, bool &has_sparse, bool &malformed)
NtfsDataStream & stream_for (NtfsFileRecordInfo &info, const std::string &name)
bool parse_attribute_list_entries (const unsigned char *value, uint32_t value_len, std::vector< NtfsAttributeListEntry > &out)
bool read_runs_to_buffer (const std::vector< NtfsDataRun > &runs, uint64_t expected_size, std::vector< unsigned char > &out, std::string &error)
bool decode_compressed_stream (const NtfsDataStream &stream, std::vector< unsigned char > &out, std::string &error)
bool process_attributes (uint64_t base_record_number, const std::vector< unsigned char > &record, NtfsFileRecordInfo &info, std::vector< NtfsAttributeListEntry > &attr_list, std::string &error)
bool load_attribute_list_extensions (uint64_t base_record_number, NtfsFileRecordInfo &info, const std::vector< NtfsAttributeListEntry > &attr_list, std::string &error)
bool parse_file_record (uint64_t record_number, const std::vector< unsigned char > &record, NtfsFileRecordInfo &info, std::string &error)
void parse_index_entries (const unsigned char *entries_data, size_t total_size, std::vector< I30Entry > &out)
bool read_indx_block (const std::vector< NtfsDataRun > &runs, uint64_t vcn, uint32_t irs, std::vector< unsigned char > &out, std::string &error)
void walk_i30 (const NtfsFileRecordInfo &dir_info, std::vector< I30Entry > &out_entries, std::vector< std::string > &logs)
fs::path build_relative_path (uint64_t record_number, const std::map< uint64_t, NtfsFileRecordInfo > &records)
bool stream_exportable (const NtfsDataStream &stream) const
fs::path output_path_for_stream (const fs::path &base, const std::string &stream_name)
fs::path unique_path (const fs::path &candidate)
void export_reparse_sidecar (const NtfsFileRecordInfo &info, const fs::path &base_path, std::vector< RecoveredFileRecord > &recovered, std::vector< std::string > &logs)
void export_usn_summary_if_needed (const NtfsFileRecordInfo &info, const fs::path &base_path, std::vector< RecoveredFileRecord > &recovered, std::vector< std::string > &logs)
bool read_stream_data (const NtfsDataStream &stream, std::vector< unsigned char > &data, std::string &error)
void write_usn_reason_flags (std::ostream &out, uint32_t reason)
bool export_stream (const NtfsDataStream &stream, const fs::path &out_path, 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)

Static Private Member Functions

static void merge_runs (std::vector< NtfsDataRun > &dst, const std::vector< NtfsDataRun > &src)

Private Attributes

DiskReader & reader_
NtfsVolumeInfo volume_
std::vector< std::pair< uint64_t, uint64_t > > mft_ranges_
std::vector< std::pair< uint64_t, uint64_t > > mftmirr_ranges_

Static Private Attributes

static constexpr uint32_t kLogPageMagicRSTR = 0x52545352u
static constexpr uint32_t kLogPageMagicRCRD = 0x44524352u
static constexpr uint16_t kRedoOpInitFileRecord = 0x0010

Detailed Description

Definition at line 201 of file ntfs_recovery.cpp.

Constructor & Destructor Documentation

◆ NtfsRecoveryImpl()

NtfsRecoveryImpl::NtfsRecoveryImpl ( DiskReader & reader)
inlineexplicit

Definition at line 203 of file ntfs_recovery.cpp.

203: reader_(reader) {}
DiskReader & reader_

References reader_.

Member Function Documentation

◆ apply_fixup()

bool NtfsRecoveryImpl::apply_fixup ( std::vector< unsigned char > & record,
std::string & error )
inlineprivate

Definition at line 533 of file ntfs_recovery.cpp.

533 {
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 }
std::string trs(const char *msgid)
Definition i18n.cpp:22
uint16_t le16(const unsigned char *p)

References anonymous_namespace{ntfs_recovery.cpp}::le16(), and trs().

Referenced by read_indx_block(), read_mft_record(), read_mft_record_from_mirror(), and scan_logfile().

◆ build_relative_path()

fs::path NtfsRecoveryImpl::build_relative_path ( uint64_t record_number,
const std::map< uint64_t, NtfsFileRecordInfo > & records )
inlineprivate

Definition at line 1259 of file ntfs_recovery.cpp.

1259 {
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 }
std::string sanitize_component(const std::string &in)

References anonymous_namespace{ntfs_recovery.cpp}::sanitize_component().

Referenced by recover().

◆ cluster_to_offset()

◆ decode_compressed_stream()

bool NtfsRecoveryImpl::decode_compressed_stream ( const NtfsDataStream & stream,
std::vector< unsigned char > & out,
std::string & error )
inlineprivate

Definition at line 790 of file ntfs_recovery.cpp.

792 {
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 }
bool read_runs_to_buffer(const std::vector< NtfsDataRun > &runs, uint64_t expected_size, std::vector< unsigned char > &out, std::string &error)
bool lznt1_decompress(const std::vector< unsigned char > &src, size_t expected_size, std::vector< unsigned char > &dst)

References anonymous_namespace{ntfs_recovery.cpp}::NtfsVolumeInfo::cluster_size, anonymous_namespace{ntfs_recovery.cpp}::NtfsDataStream::compression_unit_shift, anonymous_namespace{ntfs_recovery.cpp}::NtfsDataStream::data_size, anonymous_namespace{ntfs_recovery.cpp}::lznt1_decompress(), read_runs_to_buffer(), anonymous_namespace{ntfs_recovery.cpp}::NtfsDataStream::runs, trs(), and volume_.

Referenced by export_stream(), and read_stream_data().

◆ export_reparse_sidecar()

void NtfsRecoveryImpl::export_reparse_sidecar ( const NtfsFileRecordInfo & info,
const fs::path & base_path,
std::vector< RecoveredFileRecord > & recovered,
std::vector< std::string > & logs )
inlineprivate

Definition at line 1320 of file ntfs_recovery.cpp.

1323 {
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 }
fs::path unique_path(const fs::path &candidate)
std::string decode_utf16le_ascii(const unsigned char *data, size_t byte_count)
uint32_t le32(const unsigned char *p)

References anonymous_namespace{ntfs_recovery.cpp}::decode_utf16le_ascii(), anonymous_namespace{ntfs_recovery.cpp}::NtfsFileRecordInfo::has_reparse_point, anonymous_namespace{ntfs_recovery.cpp}::le16(), anonymous_namespace{ntfs_recovery.cpp}::le32(), anonymous_namespace{ntfs_recovery.cpp}::NtfsFileRecordInfo::reparse_data, trs(), and unique_path().

Referenced by recover().

◆ export_stream()

bool NtfsRecoveryImpl::export_stream ( const NtfsDataStream & stream,
const fs::path & out_path,
std::string & error )
inlineprivate

Definition at line 1508 of file ntfs_recovery.cpp.

1508 {
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 }
bool decode_compressed_stream(const NtfsDataStream &stream, std::vector< unsigned char > &out, std::string &error)

References anonymous_namespace{ntfs_recovery.cpp}::NtfsDataStream::compressed, anonymous_namespace{ntfs_recovery.cpp}::NtfsDataStream::data_size, decode_compressed_stream(), read_runs_to_buffer(), anonymous_namespace{ntfs_recovery.cpp}::NtfsDataStream::resident, anonymous_namespace{ntfs_recovery.cpp}::NtfsDataStream::resident_data, anonymous_namespace{ntfs_recovery.cpp}::NtfsDataStream::runs, and trs().

Referenced by recover(), and scan_logfile().

◆ export_usn_summary_if_needed()

void NtfsRecoveryImpl::export_usn_summary_if_needed ( const NtfsFileRecordInfo & info,
const fs::path & base_path,
std::vector< RecoveredFileRecord > & recovered,
std::vector< std::string > & logs )
inlineprivate

Definition at line 1436 of file ntfs_recovery.cpp.

1439 {
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 }

References anonymous_namespace{ntfs_recovery.cpp}::NtfsFileRecordInfo::name, anonymous_namespace{ntfs_recovery.cpp}::NtfsFileRecordInfo::streams, trs(), and unique_path().

Referenced by recover().

◆ load_attribute_list_extensions()

bool NtfsRecoveryImpl::load_attribute_list_extensions ( uint64_t base_record_number,
NtfsFileRecordInfo & info,
const std::vector< NtfsAttributeListEntry > & attr_list,
std::string & error )
inlineprivate

Definition at line 1017 of file ntfs_recovery.cpp.

1020 {
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 }
bool process_attributes(uint64_t base_record_number, const std::vector< unsigned char > &record, NtfsFileRecordInfo &info, std::vector< NtfsAttributeListEntry > &attr_list, std::string &error)
bool read_mft_record(uint64_t record_number, std::vector< unsigned char > &out, std::string &error)

References process_attributes(), and read_mft_record().

Referenced by parse_file_record().

◆ merge_runs()

void NtfsRecoveryImpl::merge_runs ( std::vector< NtfsDataRun > & dst,
const std::vector< NtfsDataRun > & src )
inlinestaticprivate

Definition at line 702 of file ntfs_recovery.cpp.

702 {
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 }

Referenced by process_attributes().

◆ output_path_for_stream()

fs::path NtfsRecoveryImpl::output_path_for_stream ( const fs::path & base,
const std::string & stream_name )
inlineprivate

Definition at line 1301 of file ntfs_recovery.cpp.

1301 {
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 }

References anonymous_namespace{ntfs_recovery.cpp}::sanitize_component(), and unique_path().

Referenced by recover().

◆ parse_attribute_list_entries()

bool NtfsRecoveryImpl::parse_attribute_list_entries ( const unsigned char * value,
uint32_t value_len,
std::vector< NtfsAttributeListEntry > & out )
inlineprivate

Definition at line 729 of file ntfs_recovery.cpp.

731 {
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;
740 NtfsAttributeListEntry entry;
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 }
uint64_t le64(const unsigned char *p)

References anonymous_namespace{ntfs_recovery.cpp}::NtfsAttributeListEntry::base_reference, anonymous_namespace{ntfs_recovery.cpp}::decode_utf16le_ascii(), anonymous_namespace{ntfs_recovery.cpp}::NtfsAttributeListEntry::instance, anonymous_namespace{ntfs_recovery.cpp}::le16(), anonymous_namespace{ntfs_recovery.cpp}::le32(), anonymous_namespace{ntfs_recovery.cpp}::le64(), anonymous_namespace{ntfs_recovery.cpp}::NtfsAttributeListEntry::name, anonymous_namespace{ntfs_recovery.cpp}::NtfsAttributeListEntry::name_length, anonymous_namespace{ntfs_recovery.cpp}::NtfsAttributeListEntry::name_offset, anonymous_namespace{ntfs_recovery.cpp}::NtfsAttributeListEntry::record_length, anonymous_namespace{ntfs_recovery.cpp}::NtfsAttributeListEntry::start_vcn, and anonymous_namespace{ntfs_recovery.cpp}::NtfsAttributeListEntry::type.

Referenced by process_attributes().

◆ parse_file_record()

bool NtfsRecoveryImpl::parse_file_record ( uint64_t record_number,
const std::vector< unsigned char > & record,
NtfsFileRecordInfo & info,
std::string & error )
inlineprivate

Definition at line 1039 of file ntfs_recovery.cpp.

1042 {
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 }
bool load_attribute_list_extensions(uint64_t base_record_number, NtfsFileRecordInfo &info, const std::vector< NtfsAttributeListEntry > &attr_list, std::string &error)

References load_attribute_list_extensions(), process_attributes(), anonymous_namespace{ntfs_recovery.cpp}::NtfsFileRecordInfo::streams, and trs().

Referenced by recover(), and scan_logfile().

◆ parse_index_entries()

void NtfsRecoveryImpl::parse_index_entries ( const unsigned char * entries_data,
size_t total_size,
std::vector< I30Entry > & out )
inlineprivate

Definition at line 1095 of file ntfs_recovery.cpp.

1097 {
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 }

References NtfsRecoveryImpl::I30Entry::child_record_number, NtfsRecoveryImpl::I30Entry::child_sequence_number, anonymous_namespace{ntfs_recovery.cpp}::decode_utf16le_ascii(), NtfsRecoveryImpl::I30Entry::filename, anonymous_namespace{ntfs_recovery.cpp}::le16(), anonymous_namespace{ntfs_recovery.cpp}::le32(), anonymous_namespace{ntfs_recovery.cpp}::le64(), and NtfsRecoveryImpl::I30Entry::name_namespace.

Referenced by walk_i30().

◆ parse_runlist()

std::vector< NtfsDataRun > NtfsRecoveryImpl::parse_runlist ( const unsigned char * runlist,
size_t max_len,
uint64_t start_vcn,
bool & has_sparse,
bool & malformed )
inlineprivate

Definition at line 641 of file ntfs_recovery.cpp.

645 {
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 }
uint64_t cluster_to_offset(uint64_t cluster) const

References anonymous_namespace{ntfs_recovery.cpp}::NtfsVolumeInfo::cluster_size, cluster_to_offset(), and volume_.

Referenced by process_attributes().

◆ process_attributes()

bool NtfsRecoveryImpl::process_attributes ( uint64_t base_record_number,
const std::vector< unsigned char > & record,
NtfsFileRecordInfo & info,
std::vector< NtfsAttributeListEntry > & attr_list,
std::string & error )
inlineprivate

Definition at line 846 of file ntfs_recovery.cpp.

850 {
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;
928 info.parent_sequence_number =
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 }
bool parse_attribute_list_entries(const unsigned char *value, uint32_t value_len, std::vector< NtfsAttributeListEntry > &out)
static void merge_runs(std::vector< NtfsDataRun > &dst, const std::vector< NtfsDataRun > &src)
NtfsDataStream & stream_for(NtfsFileRecordInfo &info, const std::string &name)
std::vector< NtfsDataRun > parse_runlist(const unsigned char *runlist, size_t max_len, uint64_t start_vcn, bool &has_sparse, bool &malformed)

References anonymous_namespace{ntfs_recovery.cpp}::NtfsDataStream::alloc_size, anonymous_namespace{ntfs_recovery.cpp}::NtfsDataStream::attr_flags, anonymous_namespace{ntfs_recovery.cpp}::NtfsDataStream::compressed, anonymous_namespace{ntfs_recovery.cpp}::NtfsDataStream::compression_unit_shift, anonymous_namespace{ntfs_recovery.cpp}::NtfsDataStream::data_size, anonymous_namespace{ntfs_recovery.cpp}::decode_utf16le_ascii(), anonymous_namespace{ntfs_recovery.cpp}::NtfsDataStream::encrypted, anonymous_namespace{ntfs_recovery.cpp}::NtfsFileRecordInfo::has_attribute_list, anonymous_namespace{ntfs_recovery.cpp}::NtfsFileRecordInfo::has_reparse_point, anonymous_namespace{ntfs_recovery.cpp}::NtfsFileRecordInfo::in_use, anonymous_namespace{ntfs_recovery.cpp}::NtfsFileRecordInfo::index_allocation_runs, anonymous_namespace{ntfs_recovery.cpp}::NtfsFileRecordInfo::index_record_size, anonymous_namespace{ntfs_recovery.cpp}::NtfsFileRecordInfo::index_root_data, anonymous_namespace{ntfs_recovery.cpp}::NtfsFileRecordInfo::is_directory, anonymous_namespace{ntfs_recovery.cpp}::le16(), anonymous_namespace{ntfs_recovery.cpp}::le32(), anonymous_namespace{ntfs_recovery.cpp}::le64(), merge_runs(), anonymous_namespace{ntfs_recovery.cpp}::NtfsFileRecordInfo::name, anonymous_namespace{ntfs_recovery.cpp}::NtfsFileRecordInfo::own_sequence_number, anonymous_namespace{ntfs_recovery.cpp}::NtfsFileRecordInfo::parent_record_number, anonymous_namespace{ntfs_recovery.cpp}::NtfsFileRecordInfo::parent_sequence_number, parse_attribute_list_entries(), parse_runlist(), read_runs_to_buffer(), anonymous_namespace{ntfs_recovery.cpp}::NtfsFileRecordInfo::reparse_data, anonymous_namespace{ntfs_recovery.cpp}::NtfsDataStream::resident, anonymous_namespace{ntfs_recovery.cpp}::NtfsDataStream::resident_data, anonymous_namespace{ntfs_recovery.cpp}::NtfsDataStream::runs, anonymous_namespace{ntfs_recovery.cpp}::NtfsDataStream::sparse, stream_for(), trs(), and anonymous_namespace{ntfs_recovery.cpp}::NtfsDataStream::unsupported_layout.

Referenced by load_attribute_list_extensions(), and parse_file_record().

◆ read_boot_sector()

bool NtfsRecoveryImpl::read_boot_sector ( uint64_t volume_offset,
std::string & error )
inlineprivate

Definition at line 453 of file ntfs_recovery.cpp.

453 {
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 }
bool read_at(uint64_t offset, void *buffer, size_t len)

References anonymous_namespace{ntfs_recovery.cpp}::NtfsVolumeInfo::bytes_per_sector, anonymous_namespace{ntfs_recovery.cpp}::NtfsVolumeInfo::cluster_size, anonymous_namespace{ntfs_recovery.cpp}::le16(), anonymous_namespace{ntfs_recovery.cpp}::le64(), anonymous_namespace{ntfs_recovery.cpp}::NtfsVolumeInfo::mft_lcn, anonymous_namespace{ntfs_recovery.cpp}::NtfsVolumeInfo::mft_record_size, anonymous_namespace{ntfs_recovery.cpp}::NtfsVolumeInfo::mftmirr_lcn, DiskReader::read_at(), reader_, anonymous_namespace{ntfs_recovery.cpp}::NtfsVolumeInfo::sectors_per_cluster, anonymous_namespace{ntfs_recovery.cpp}::NtfsVolumeInfo::total_sectors, trs(), volume_, and anonymous_namespace{ntfs_recovery.cpp}::NtfsVolumeInfo::volume_offset.

Referenced by recover().

◆ read_indx_block()

bool NtfsRecoveryImpl::read_indx_block ( const std::vector< NtfsDataRun > & runs,
uint64_t vcn,
uint32_t irs,
std::vector< unsigned char > & out,
std::string & error )
inlineprivate

Definition at line 1144 of file ntfs_recovery.cpp.

1148 {
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 }
bool apply_fixup(std::vector< unsigned char > &record, std::string &error)

References apply_fixup(), DiskReader::read_at(), and reader_.

Referenced by walk_i30().

◆ read_mft_record()

bool NtfsRecoveryImpl::read_mft_record ( uint64_t record_number,
std::vector< unsigned char > & out,
std::string & error )
inlineprivate

Definition at line 567 of file ntfs_recovery.cpp.

567 {
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 }
bool read_mft_record_from_mirror(uint64_t record_number, std::vector< unsigned char > &out, std::string &error)
bool read_raw_record(uint64_t record_number, std::vector< unsigned char > &out, std::string &error)

References apply_fixup(), read_mft_record_from_mirror(), read_raw_record(), and trs().

Referenced by load_attribute_list_extensions(), recover(), and scan_logfile().

◆ read_mft_record_from_mirror()

bool NtfsRecoveryImpl::read_mft_record_from_mirror ( uint64_t record_number,
std::vector< unsigned char > & out,
std::string & error )
inlineprivate

Definition at line 589 of file ntfs_recovery.cpp.

591 {
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 }
#define _(s)
std::vector< std::pair< uint64_t, uint64_t > > mftmirr_ranges_

References _, apply_fixup(), anonymous_namespace{ntfs_recovery.cpp}::NtfsVolumeInfo::mft_record_size, mftmirr_ranges_, DiskReader::read_at(), reader_, trs(), and volume_.

Referenced by read_mft_record().

◆ read_raw_record()

bool NtfsRecoveryImpl::read_raw_record ( uint64_t record_number,
std::vector< unsigned char > & out,
std::string & error )
inlineprivate

Definition at line 496 of file ntfs_recovery.cpp.

496 {
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 }
std::vector< std::pair< uint64_t, uint64_t > > mft_ranges_

References cluster_to_offset(), anonymous_namespace{ntfs_recovery.cpp}::NtfsVolumeInfo::mft_lcn, mft_ranges_, anonymous_namespace{ntfs_recovery.cpp}::NtfsVolumeInfo::mft_record_size, DiskReader::read_at(), reader_, trs(), and volume_.

Referenced by read_mft_record().

◆ read_runs_to_buffer()

bool NtfsRecoveryImpl::read_runs_to_buffer ( const std::vector< NtfsDataRun > & runs,
uint64_t expected_size,
std::vector< unsigned char > & out,
std::string & error )
inlineprivate

Definition at line 757 of file ntfs_recovery.cpp.

760 {
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 }

References DiskReader::read_at(), reader_, and trs().

Referenced by decode_compressed_stream(), export_stream(), process_attributes(), read_stream_data(), and scan_logfile().

◆ read_stream_data()

bool NtfsRecoveryImpl::read_stream_data ( const NtfsDataStream & stream,
std::vector< unsigned char > & data,
std::string & error )
inlineprivate

Definition at line 1457 of file ntfs_recovery.cpp.

1459 {
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 }

References anonymous_namespace{ntfs_recovery.cpp}::NtfsDataStream::compressed, anonymous_namespace{ntfs_recovery.cpp}::NtfsDataStream::data_size, decode_compressed_stream(), read_runs_to_buffer(), anonymous_namespace{ntfs_recovery.cpp}::NtfsDataStream::resident, anonymous_namespace{ntfs_recovery.cpp}::NtfsDataStream::resident_data, and anonymous_namespace{ntfs_recovery.cpp}::NtfsDataStream::runs.

◆ recover()

bool NtfsRecoveryImpl::recover ( uint64_t volume_offset,
const std::string & output_dir,
std::vector< RecoveredFileRecord > & recovered,
std::vector< std::string > & logs,
std::string & error )
inline

Definition at line 205 of file ntfs_recovery.cpp.

209 {
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 }
287 NtfsFileRecordInfo info;
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 }
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 stream_exportable(const NtfsDataStream &stream) const
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)
bool read_boot_sector(uint64_t volume_offset, std::string &error)
void export_usn_summary_if_needed(const NtfsFileRecordInfo &info, const fs::path &base_path, std::vector< RecoveredFileRecord > &recovered, std::vector< std::string > &logs)
bool parse_file_record(uint64_t record_number, const std::vector< unsigned char > &record, NtfsFileRecordInfo &info, std::string &error)
fs::path output_path_for_stream(const fs::path &base, const std::string &stream_name)
void export_reparse_sidecar(const NtfsFileRecordInfo &info, const fs::path &base_path, std::vector< RecoveredFileRecord > &recovered, std::vector< std::string > &logs)

References _, build_relative_path(), anonymous_namespace{ntfs_recovery.cpp}::NtfsFileRecordInfo::deleted_candidate, export_reparse_sidecar(), export_stream(), export_usn_summary_if_needed(), anonymous_namespace{ntfs_recovery.cpp}::NtfsFileRecordInfo::in_use, mft_ranges_, anonymous_namespace{ntfs_recovery.cpp}::NtfsVolumeInfo::mft_record_size, mftmirr_ranges_, anonymous_namespace{ntfs_recovery.cpp}::NtfsFileRecordInfo::name, output_path_for_stream(), anonymous_namespace{ntfs_recovery.cpp}::NtfsFileRecordInfo::own_sequence_number, anonymous_namespace{ntfs_recovery.cpp}::NtfsFileRecordInfo::parent_record_number, anonymous_namespace{ntfs_recovery.cpp}::NtfsFileRecordInfo::parent_sequence_number, parse_file_record(), read_boot_sector(), read_mft_record(), anonymous_namespace{ntfs_recovery.cpp}::NtfsFileRecordInfo::record_number, anonymous_namespace{ntfs_recovery.cpp}::sanitize_component(), scan_logfile(), stream_exportable(), anonymous_namespace{ntfs_recovery.cpp}::NtfsFileRecordInfo::streams, trs(), volume_, and walk_i30().

Referenced by NtfsRecovery::recover().

◆ scan_logfile()

void NtfsRecoveryImpl::scan_logfile ( const fs::path & out_root,
const std::map< uint64_t, NtfsFileRecordInfo > & live_records,
std::vector< RecoveredFileRecord > & recovered,
std::vector< std::string > & logs )
inlineprivate

Definition at line 1566 of file ntfs_recovery.cpp.

1569 {
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 }
static constexpr uint16_t kRedoOpInitFileRecord
static constexpr uint32_t kLogPageMagicRCRD

References apply_fixup(), anonymous_namespace{ntfs_recovery.cpp}::NtfsDataStream::data_size, export_stream(), anonymous_namespace{ntfs_recovery.cpp}::NtfsFileRecordInfo::is_directory, kLogPageMagicRCRD, kRedoOpInitFileRecord, anonymous_namespace{ntfs_recovery.cpp}::le16(), anonymous_namespace{ntfs_recovery.cpp}::le32(), anonymous_namespace{ntfs_recovery.cpp}::NtfsFileRecordInfo::name, parse_file_record(), read_mft_record(), read_runs_to_buffer(), anonymous_namespace{ntfs_recovery.cpp}::NtfsFileRecordInfo::record_number, anonymous_namespace{ntfs_recovery.cpp}::NtfsDataStream::runs, anonymous_namespace{ntfs_recovery.cpp}::sanitize_component(), stream_exportable(), anonymous_namespace{ntfs_recovery.cpp}::NtfsFileRecordInfo::streams, and trs().

Referenced by recover().

◆ stream_exportable()

bool NtfsRecoveryImpl::stream_exportable ( const NtfsDataStream & stream) const
inlineprivate

◆ stream_for()

NtfsDataStream & NtfsRecoveryImpl::stream_for ( NtfsFileRecordInfo & info,
const std::string & name )
inlineprivate

Definition at line 695 of file ntfs_recovery.cpp.

695 {
696 auto [it, inserted] = info.streams.try_emplace(name);
697 (void)inserted;
698 it->second.name = name;
699 return it->second;
700 }

References anonymous_namespace{ntfs_recovery.cpp}::NtfsFileRecordInfo::streams.

Referenced by process_attributes().

◆ unique_path()

fs::path NtfsRecoveryImpl::unique_path ( const fs::path & candidate)
inlineprivate

Definition at line 1309 of file ntfs_recovery.cpp.

1309 {
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 }

Referenced by export_reparse_sidecar(), export_usn_summary_if_needed(), and output_path_for_stream().

◆ walk_i30()

void NtfsRecoveryImpl::walk_i30 ( const NtfsFileRecordInfo & dir_info,
std::vector< I30Entry > & out_entries,
std::vector< std::string > & logs )
inlineprivate

Definition at line 1186 of file ntfs_recovery.cpp.

1188 {
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;
1225 if (!read_indx_block(dir_info.index_allocation_runs,
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 }
void parse_index_entries(const unsigned char *entries_data, size_t total_size, std::vector< I30Entry > &out)
bool read_indx_block(const std::vector< NtfsDataRun > &runs, uint64_t vcn, uint32_t irs, std::vector< unsigned char > &out, std::string &error)

References anonymous_namespace{ntfs_recovery.cpp}::NtfsFileRecordInfo::index_allocation_runs, anonymous_namespace{ntfs_recovery.cpp}::NtfsFileRecordInfo::index_record_size, anonymous_namespace{ntfs_recovery.cpp}::NtfsFileRecordInfo::index_root_data, anonymous_namespace{ntfs_recovery.cpp}::le32(), parse_index_entries(), read_indx_block(), and anonymous_namespace{ntfs_recovery.cpp}::NtfsFileRecordInfo::record_number.

Referenced by recover().

◆ write_usn_reason_flags()

void NtfsRecoveryImpl::write_usn_reason_flags ( std::ostream & out,
uint32_t reason )
inlineprivate

Definition at line 1471 of file ntfs_recovery.cpp.

1471 {
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 }

Member Data Documentation

◆ kLogPageMagicRCRD

uint32_t NtfsRecoveryImpl::kLogPageMagicRCRD = 0x44524352u
staticconstexprprivate

Definition at line 1563 of file ntfs_recovery.cpp.

Referenced by scan_logfile().

◆ kLogPageMagicRSTR

uint32_t NtfsRecoveryImpl::kLogPageMagicRSTR = 0x52545352u
staticconstexprprivate

Definition at line 1562 of file ntfs_recovery.cpp.

◆ kRedoOpInitFileRecord

uint16_t NtfsRecoveryImpl::kRedoOpInitFileRecord = 0x0010
staticconstexprprivate

Definition at line 1564 of file ntfs_recovery.cpp.

Referenced by scan_logfile().

◆ mft_ranges_

std::vector<std::pair<uint64_t, uint64_t> > NtfsRecoveryImpl::mft_ranges_
private

Definition at line 446 of file ntfs_recovery.cpp.

Referenced by read_raw_record(), and recover().

◆ mftmirr_ranges_

std::vector<std::pair<uint64_t, uint64_t> > NtfsRecoveryImpl::mftmirr_ranges_
private

Definition at line 451 of file ntfs_recovery.cpp.

Referenced by read_mft_record_from_mirror(), and recover().

◆ reader_

◆ volume_


The documentation for this class was generated from the following file: