21namespace fs = std::filesystem;
24uint16_t
le16(
const unsigned char* p) {
25 return static_cast<uint16_t
>(p[0]) | (
static_cast<uint16_t
>(p[1]) << 8);
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);
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);
98 std::vector<NtfsDataRun>
runs;
127 std::map<std::string, NtfsDataStream>
streams;
145 size_t expected_size,
146 std::vector<unsigned char>& dst) {
149 while (pos + 2 <= src.size() && dst.size() < expected_size) {
150 const uint16_t header =
le16(src.data() + pos);
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;
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));
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++]);
172 if (pos + 1 >= chunk_end)
return false;
173 const uint16_t token =
le16(src.data() + pos);
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]);
186 if (chunk_out.size() >= 4096)
break;
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);
195 if (dst.size() < expected_size) dst.resize(expected_size, 0);
206 const std::string& output_dir,
207 std::vector<RecoveredFileRecord>& recovered,
208 std::vector<std::string>& logs,
209 std::string& error) {
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));
216 std::vector<unsigned char> mft_record;
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");
228 for (
const auto& run : mft_it->second.runs) {
229 if (!run.sparse)
mft_ranges_.push_back({run.offset, run.length});
247 std::vector<unsigned char> mirr_rec;
248 std::string mirr_err;
252 auto it = mirr_info.
streams.find(
"");
253 if (it != mirr_info.
streams.end()) {
254 for (
const auto& run : it->second.runs) {
260 logs.push_back(
trs(
"NTFS $MFTMirr loaded as fallback "
261 "for the first MFT records "
262 "(typically records 0-3)"));
268 logs.push_back(
trs(
"NTFS $MFTMirr not loadable -- primary MFT "
269 "must be intact for recovery"));
276 constexpr uint64_t MAX_MFT_RECORDS = 8ull * 1000ull * 1000ull;
277 const uint64_t record_count = std::min<uint64_t>(
280 std::map<uint64_t, NtfsFileRecordInfo> records;
281 for (uint64_t i = 0; i < record_count; ++i) {
282 std::vector<unsigned char> rec;
294 records[i] = std::move(info);
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;
323 for (
const auto& ie : entries) {
324 auto cit = records.find(ie.child_record_number);
325 if (cit == records.end())
continue;
330 if (ie.child_sequence_number != 0 &&
336 if (child.
name.empty() && !ie.filename.empty()) {
337 child.
name = ie.filename;
338 ++i30_filenames_recovered;
345 ++i30_parent_updates;
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());
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;
365 for (
const auto& [
id, info] : records) {
366 if (info.is_directory || info.name.empty())
continue;
371 bool any_encrypted =
false;
372 for (
const auto& [
_, stream] : info.streams) {
373 if (stream.encrypted) { any_encrypted =
true;
break; }
375 if (any_encrypted) ++files_with_efs;
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());
383 bool exported_any =
false;
384 for (
const auto& [stream_name, stream] : info.streams) {
387 if (stream.encrypted) ++skipped_encrypted;
388 else if (stream.unsupported_layout) ++skipped_malformed;
392 std::string local_error;
394 logs.push_back(
trs(
"NTFS stream export failed") + std::string(
": ") + rel.string() +
" - " + local_error);
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"});
399 if (!stream_name.empty()) ++ads_exported_count;
406 if (info.deleted_candidate) ++deleted_exported_count;
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));
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));
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));
454 std::array<unsigned char, 512> b{};
456 error =
trs(
"Unable to read NTFS boot sector");
459 if (std::string(
reinterpret_cast<const char*
>(b.data() + 3), 8) !=
"NTFS ") {
460 error =
trs(
"Invalid NTFS boot sector");
470 const int8_t rec_desc =
static_cast<int8_t
>(b[64]);
474 }
else if (rec_desc == 0) {
475 error =
trs(
"Invalid NTFS MFT record descriptor (zero)");
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)");
486 error =
trs(
"Invalid NTFS geometry");
496 bool read_raw_record(uint64_t record_number, std::vector<unsigned char>& out, std::string& error) {
501 error =
trs(
"Unable to read MFT record");
509 out.resize(
static_cast<size_t>(remaining));
513 if (logical_offset >= cursor + len) {
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");
523 remaining -= can_read;
524 out_pos +=
static_cast<size_t>(can_read);
525 logical_offset += can_read;
527 if (remaining == 0)
return true;
529 error =
trs(
"Incomplete read of MFT record");
533 bool apply_fixup(std::vector<unsigned char>& record, std::string& error) {
534 if (record.size() < 512) {
535 error =
trs(
"Invalid MFT record size");
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");
544 const uint16_t usn =
le16(record.data() + usa_offset);
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);
556 record[sector_end] = record[usa_offset + i * 2];
557 record[sector_end + 1] = record[usa_offset + i * 2 + 1];
560 if (usa_count > 1 && bad_sectors >=
static_cast<size_t>(usa_count - 1)) {
561 error =
trs(
"NTFS fixup mismatch on all sectors");
567 bool read_mft_record(uint64_t record_number, std::vector<unsigned char>& out, std::string& error) {
572 if (out.size() < 4 || std::string(
reinterpret_cast<const char*
>(out.data()), 4) !=
"FILE") {
575 std::string mirror_err;
579 error =
trs(
"Invalid MFT record signature");
590 std::vector<unsigned char>& out,
591 std::string& error) {
593 error =
trs(
"NTFS $MFTMirr not available");
597 uint64_t mirror_size = 0;
601 error =
trs(
"Record number out of $MFTMirr coverage");
605 uint64_t logical = logical_offset;
607 out.resize(
static_cast<size_t>(remaining));
611 if (logical >= cursor + len) {
615 const uint64_t inner = logical > cursor ? logical - cursor : 0;
616 const uint64_t can_read = std::min<uint64_t>(remaining, len - inner);
618 static_cast<size_t>(can_read))) {
619 error =
trs(
"Mirror read failed");
622 remaining -= can_read;
623 out_pos +=
static_cast<size_t>(can_read);
626 if (remaining == 0)
break;
628 if (remaining != 0) {
629 error =
trs(
"Incomplete mirror read");
633 if (out.size() < 4 ||
634 std::string(
reinterpret_cast<const char*
>(out.data()), 4) !=
"FILE") {
635 error =
trs(
"Mirror MFT record signature mismatch");
646 std::vector<NtfsDataRun> out;
647 int64_t current_lcn = 0;
648 uint64_t current_vcn = start_vcn;
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) {
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);
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));
676 run.vcn = current_vcn;
678 if (off_bytes == 0) {
682 current_lcn += run_off;
683 if (current_lcn < 0) {
690 current_vcn += run_len;
696 auto [it, inserted] = info.
streams.try_emplace(name);
698 it->second.name = name;
702 static void merge_runs(std::vector<NtfsDataRun>& dst,
const std::vector<NtfsDataRun>& src) {
703 dst.insert(dst.end(), src.begin(), src.end());
705 if (a.vcn != b.vcn) return a.vcn < b.vcn;
707 if (a.sparse != b.sparse) return !a.sparse;
708 return a.offset < b.offset;
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) {
719 if (merged.back().sparse && !run.sparse) {
724 merged.push_back(run);
731 std::vector<NtfsAttributeListEntry>& out) {
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;
748 if (name_length > 0 && name_offset +
static_cast<uint16_t
>(name_length) * 2 <= record_length) {
751 out.push_back(std::move(entry));
752 pos += record_length;
758 uint64_t expected_size,
759 std::vector<unsigned char>& out,
760 std::string& error) {
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;
769 out.insert(out.end(),
static_cast<size_t>(remain), 0);
773 uint64_t cur = run.offset;
775 const size_t chunk =
static_cast<size_t>(std::min<uint64_t>(buffer.size(), remain));
777 error =
trs(
"Read error while exporting recovered file");
780 out.insert(out.end(), buffer.begin(), buffer.begin() +
static_cast<long>(chunk));
786 if (out.size() < expected_size) out.resize(
static_cast<size_t>(expected_size), 0);
791 std::vector<unsigned char>& out,
792 std::string& error) {
795 if (unit_size == 0) {
796 error =
trs(
"Invalid NTFS compression unit");
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];
811 unit_bytes += run.length;
812 unit_sparse = unit_sparse || run.sparse;
814 if (unit_bytes >= unit_size)
break;
817 if (unit_sparse && unit_bytes == unit_size) {
818 out.insert(out.end(),
static_cast<size_t>(expected), 0);
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;
827 out.insert(out.end(), raw.begin(), raw.begin() +
static_cast<long>(expected));
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();
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");
840 out.insert(out.end(), decomp.begin(), decomp.begin() +
static_cast<long>(expected));
847 const std::vector<unsigned char>& record,
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;
862 if (attrs_offset >= record.size()) {
863 error =
trs(
"Invalid NTFS attribute offset");
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");
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);
886 if (attr_type == 0x20) {
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) {
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;
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);
922 if (name_ns == 1 || name_ns == 3) score = 3;
923 else if (name_ns == 0) score = 2;
925 if (!candidate_name.empty() && score >= best_name_score) {
926 best_name_score = score;
929 static_cast<uint16_t
>(parent_ref >> 48);
930 info.
name = candidate_name;
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) {
939 info.
reparse_data.assign(record.begin() +
static_cast<long>(pos + value_off),
940 record.begin() +
static_cast<long>(pos + value_off + value_len));
942 }
else if (attr_type == 0x90 && attr_name ==
"$I30") {
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) {
957 record.begin() +
static_cast<long>(pos + value_off),
958 record.begin() +
static_cast<long>(pos + value_off + value_len));
963 }
else if (attr_type == 0xA0 && attr_name ==
"$I30") {
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;
973 record.data() + pos + run_off,
974 attr_len - run_off, start_vcn,
975 has_sparse, malformed);
981 }
else if (attr_type == 0x80) {
986 stream.
sparse = stream.
sparse || ((attr_flags & 0x8000) != 0);
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) {
992 stream.
resident_data.assign(record.begin() +
static_cast<long>(pos + value_off),
993 record.begin() +
static_cast<long>(pos + value_off + value_len));
997 const uint64_t start_vcn =
le64(record.data() + pos + 16);
998 const uint16_t run_off =
le16(record.data() + pos + 32);
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);
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;
1030 std::vector<NtfsAttributeListEntry> nested;
1040 const std::vector<unsigned char>& record,
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");
1047 std::vector<NtfsAttributeListEntry> attr_list;
1048 if (!
process_attributes(record_number, record, info, attr_list, error))
return false;
1049 if (!attr_list.empty()) {
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;
1097 std::vector<I30Entry>& out) {
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;
1107 const bool is_last = (flags & 0x02) != 0;
1111 const uint64_t fref =
le64(e + 0);
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) {
1128 static_cast<size_t>(name_chars) * 2);
1147 std::vector<unsigned char>& out,
1148 std::string& error) {
1149 if (irs == 0)
return false;
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) {
1160 error =
"indx block in sparse run";
1163 const uint64_t inner = logical_off - cursor;
1164 const uint64_t phys = run.offset + inner;
1166 error =
"read failure on INDX block";
1170 if (out.size() < 4 || std::memcmp(out.data(),
"INDX", 4) != 0) {
1171 error =
"INDX magic mismatch";
1178 cursor += run_bytes;
1180 error =
"VCN out of allocation range";
1187 std::vector<I30Entry>& out_entries,
1188 std::vector<std::string>& logs) {
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);
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;
1219 const uint64_t num_blocks = total / irs;
1220 constexpr uint64_t kMaxBlocksPerDir = 100000;
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;
1226 vcn, irs, block, err)) {
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;
1249 if (!out_entries.empty()) {
1250 std::ostringstream m;
1251 m <<
"NTFS $I30 walker: directory MFT#"
1253 << out_entries.size() <<
" index entries";
1254 logs.push_back(m.str());
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;
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;
1283 current = info.parent_record_number;
1284 if (current == 5)
break;
1286 if (parts.empty())
return {};
1288 for (
auto it = parts.rbegin(); it != parts.rend(); ++it) out /= *it;
1291 if (sequence_mismatch) {
1292 out = fs::path(
"orphaned-seq-mismatch") / out;
1302 if (stream_name.empty())
return unique_path(base);
1304 fs::path named = base.parent_path() /
1305 fs::path(base.stem().string() + marker + base.extension().string());
1310 fs::path unique = candidate;
1312 while (fs::exists(unique)) {
1313 unique = candidate.parent_path() /
1314 fs::path(candidate.stem().string() +
"_" + std::to_string(suffix++) + candidate.extension().string());
1321 const fs::path& base_path,
1322 std::vector<RecoveredFileRecord>& recovered,
1323 std::vector<std::string>& logs) {
1330 constexpr uint32_t IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003;
1331 constexpr uint32_t IO_REPARSE_TAG_SYMLINK = 0xA000000C;
1335 std::string target_str;
1336 bool target_extracted =
false;
1337 if (tag == IO_REPARSE_TAG_SYMLINK && info.
reparse_data.size() >= 20) {
1351 const size_t buf_start = 20;
1352 if (print_len > 0 &&
1353 buf_start + print_off + print_len <= info.
reparse_data.size()) {
1357 target_extracted = !target_str.empty();
1358 }
else if (sub_len > 0 &&
1359 buf_start + sub_off + sub_len <= info.
reparse_data.size()) {
1365 if (target_str.rfind(
"\\??\\", 0) == 0 ||
1366 target_str.rfind(
"\\\\?\\", 0) == 0) {
1367 target_str = target_str.substr(4);
1369 target_extracted = !target_str.empty();
1371 }
else if (tag == IO_REPARSE_TAG_MOUNT_POINT &&
1381 const size_t buf_start = 16;
1382 if (print_len > 0 &&
1383 buf_start + print_off + print_len <= info.
reparse_data.size()) {
1387 target_extracted = !target_str.empty();
1391 if (target_extracted) {
1393 std::replace(target_str.begin(), target_str.end(),
'\\',
'/');
1396 const fs::path link_path =
unique_path(base_path);
1397 fs::create_symlink(target_str, link_path, 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);
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);
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";
1426 out <<
"raw_bytes=";
1427 static const char* hex =
"0123456789ABCDEF";
1429 out << hex[(c >> 4) & 0xF] << hex[c & 0xF];
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());
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);
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());
1458 std::vector<unsigned char>& data,
1459 std::string& error) {
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);
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"},
1499 for (
const auto& entry : names) {
1500 if ((reason & entry.flag) == 0)
continue;
1501 if (!first) out <<
'|';
1505 if (first) out <<
'0';
1509 std::vector<unsigned char> data;
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);
1520 error =
trs(
"Unable to create output file") + std::string(
": ") + out_path.string();
1523 if (!data.empty()) out.write(
reinterpret_cast<const char*
>(data.data()),
static_cast<std::streamsize
>(data.size()));
1524 return static_cast<bool>(out);
1567 const std::map<uint64_t, NtfsFileRecordInfo>& live_records,
1568 std::vector<RecoveredFileRecord>& recovered,
1569 std::vector<std::string>& logs) {
1571 std::vector<unsigned char> logrec;
1574 logs.push_back(
trs(
"NTFS $LogFile: cannot read MFT record 2 (skipping)"));
1579 logs.push_back(
trs(
"NTFS $LogFile: parse failed (skipping)"));
1582 auto data_it = loginfo.
streams.find(
"");
1583 if (data_it == loginfo.
streams.end()) {
1584 logs.push_back(
trs(
"NTFS $LogFile: no $DATA stream"));
1591 constexpr uint64_t kLogFileMaxBytes = 256ull * 1024 * 1024;
1592 const uint64_t to_read = std::min<uint64_t>(logdata.
data_size,
1594 if (to_read < 8192) {
1595 logs.push_back(
trs(
"NTFS $LogFile: too small (<8 KiB)"));
1598 std::vector<unsigned char> logbuf;
1599 logbuf.reserve(
static_cast<size_t>(to_read));
1602 logs.push_back(
trs(
"NTFS $LogFile: cannot read content: ") + rs_err);
1605 logs.push_back(
trs(
"NTFS $LogFile: loaded ") +
1606 std::to_string(logbuf.size()) +
1607 trs(
" bytes; scanning for recoverable file records"));
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;
1616 fs::path log_out = out_root /
"logfile_recovered";
1617 bool log_out_created =
false;
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);
1642 const uint16_t next_rec_off =
le16(page + 0x18);
1643 if (next_rec_off < 0x40 || next_rec_off >= kPageSize)
continue;
1679 const size_t page_end = next_rec_off;
1680 while (pos + 0x40 <= page_end) {
1681 const unsigned char* lr = page + pos;
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);
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;
1697 redo_len >= 1024 && redo_len <= 4096) {
1698 ++init_redo_records_seen;
1701 if (redo_off + redo_len > entry_total) {
1702 pos += entry_total > 0x30 ? entry_total : 0x30;
1705 const unsigned char* mftrec_raw = lr + redo_off;
1706 std::vector<unsigned char> mftrec(
1707 mftrec_raw, mftrec_raw + redo_len);
1710 std::string fix_err;
1720 const uint64_t mft_no = target_attr;
1728 if (live_records.count(mft_no) > 0)
continue;
1731 bool exported_any =
false;
1732 for (
const auto& [stream_name, stream] : info.
streams) {
1734 if (!log_out_created) {
1736 fs::create_directories(log_out, ec);
1737 log_out_created =
true;
1741 fname =
"mft" + std::to_string(mft_no) +
"_" + fname;
1742 const fs::path target = log_out / fname;
1745 recovered.push_back({
1746 target.string(),
"NTFS",
1747 (
"logfile_recovered/" + fname),
1748 stream.data_size,
"partial",
1750 exported_any =
true;
1753 if (exported_any) ++recovered_from_log;
1756 pos += entry_total > 0x30 ? entry_total : 0x30;
1758 pos = (pos + 7u) & ~
size_t{7u};
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());
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);
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
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)
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)
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)
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)
uint16_t child_sequence_number
uint64_t child_record_number
std::vector< unsigned char > resident_data
std::vector< NtfsDataRun > runs
uint8_t compression_unit_shift
std::vector< NtfsDataRun > index_allocation_runs
std::vector< unsigned char > index_root_data
uint16_t parent_sequence_number
std::map< std::string, NtfsDataStream > streams
uint64_t parent_record_number
uint16_t own_sequence_number
uint32_t index_record_size
std::vector< unsigned char > reparse_data
uint8_t sectors_per_cluster
uint16_t bytes_per_sector