1use std::collections::HashMap;
11
12use chrono::{DateTime, Utc};
13
14use crate::error::Result;
15use crate::rewind::{EntryKey, RewindEngine};
16use crate::{
17 apply_fixup, parse_attributes, AttributeBody, FileName, Filetime, MftRecordHeader,
18 StandardInformation,
19};
20
21#[derive(Debug, Clone)]
23pub struct MftEntry {
24 pub entry_number: u64,
25 pub sequence_number: u16,
26 pub filename: String,
27 pub parent_entry: u64,
28 pub parent_sequence: u16,
29 pub is_directory: bool,
30 pub is_in_use: bool,
31 pub si_created: Option<DateTime<Utc>>,
33 pub si_modified: Option<DateTime<Utc>>,
34 pub si_mft_modified: Option<DateTime<Utc>>,
35 pub si_accessed: Option<DateTime<Utc>>,
36 pub fn_created: Option<DateTime<Utc>>,
38 pub fn_modified: Option<DateTime<Utc>>,
39 pub fn_mft_modified: Option<DateTime<Utc>>,
40 pub fn_accessed: Option<DateTime<Utc>>,
41 pub full_path: String,
43 pub file_size: u64,
45 pub has_ads: bool,
47}
48
49pub struct MftData {
51 pub entries: Vec<MftEntry>,
53 pub by_entry: HashMap<u64, usize>,
55 pub by_key: HashMap<EntryKey, usize>,
57}
58
59impl MftData {
60 pub fn parse(data: &[u8]) -> Result<Self> {
68 const REC: usize = 1024;
69 let mut entries = Vec::new();
70 let mut by_entry = HashMap::new();
71 let mut by_key = HashMap::new();
72
73 for chunk in data.chunks(REC) {
74 if chunk.len() < REC || chunk.get(0..4) != Some(b"FILE") {
76 continue;
77 }
78 let mut buf = chunk.to_vec();
79 if apply_fixup(&mut buf, 512).is_err() {
80 continue;
81 }
82 let Ok(header) = MftRecordHeader::parse(&buf) else {
83 continue; };
85 let Ok(attrs) = parse_attributes(&buf, header.first_attribute_offset as usize) else {
86 continue;
87 };
88
89 let (mut si_created, mut si_modified, mut si_mft_modified, mut si_accessed) =
91 (None, None, None, None);
92 for a in attrs.iter().filter(|a| a.type_code == 0x10) {
93 if let Some(si) = a
94 .resident_content(&buf)
95 .and_then(|c| StandardInformation::parse(c).ok())
96 {
97 si_created = to_datetime(si.created);
98 si_modified = to_datetime(si.modified);
99 si_mft_modified = to_datetime(si.mft_modified);
100 si_accessed = to_datetime(si.accessed);
101 }
102 }
103
104 let mut best: Option<(u8, FileName)> = None;
106 for a in attrs.iter().filter(|a| a.type_code == 0x30) {
107 if let Some(fnm) = a
108 .resident_content(&buf)
109 .and_then(|c| FileName::parse(c).ok())
110 {
111 let priority = match fnm.namespace {
112 1 | 3 => 3,
113 2 => 1,
114 _ => 2,
115 };
116 if best.as_ref().is_none_or(|(p, _)| priority > *p) {
117 best = Some((priority, fnm));
118 }
119 } }
121 let Some((_, best_fn)) = best else {
122 continue;
123 };
124
125 let has_ads = attrs
127 .iter()
128 .any(|a| a.type_code == 0x80 && a.name.is_some());
129 let file_size = attrs
130 .iter()
131 .filter(|a| a.type_code == 0x80 && a.name.is_none())
132 .map(|a| match &a.body {
133 AttributeBody::NonResident { real_size, .. } => *real_size,
134 AttributeBody::Resident { content_length, .. } => u64::from(*content_length),
135 })
136 .next()
137 .unwrap_or(0);
138
139 let entry_number = u64::from(header.record_number);
140 let sequence_number = header.sequence_number;
141 let idx = entries.len();
142 entries.push(MftEntry {
143 entry_number,
144 sequence_number,
145 filename: best_fn.name.clone(),
146 parent_entry: best_fn.parent.record_number,
147 parent_sequence: best_fn.parent.sequence,
148 is_directory: header.is_directory(),
149 is_in_use: header.is_in_use(),
150 si_created,
151 si_modified,
152 si_mft_modified,
153 si_accessed,
154 fn_created: to_datetime(best_fn.created),
155 fn_modified: to_datetime(best_fn.modified),
156 fn_mft_modified: to_datetime(best_fn.mft_modified),
157 fn_accessed: to_datetime(best_fn.accessed),
158 full_path: String::new(),
159 file_size,
160 has_ads,
161 });
162 by_entry.insert(entry_number, idx);
163 by_key.insert(EntryKey::new(entry_number, sequence_number), idx);
164 }
165
166 let paths: Vec<String> = (0..entries.len())
168 .map(|i| resolve_full_path(&entries, &by_entry, i))
169 .collect();
170 for (entry, path) in entries.iter_mut().zip(paths) {
171 entry.full_path = path;
172 }
173
174 Ok(Self {
175 entries,
176 by_entry,
177 by_key,
178 })
179 }
180
181 #[must_use]
183 pub fn seed_rewind(&self) -> RewindEngine {
184 let mft_iter = self.entries.iter().map(|e| {
185 (
186 e.entry_number,
187 e.sequence_number,
188 e.filename.clone(),
189 e.parent_entry,
190 e.parent_sequence,
191 )
192 });
193 RewindEngine::from_mft(mft_iter)
194 }
195
196 #[must_use]
198 pub fn detect_timestomping(&self) -> Vec<&MftEntry> {
199 self.entries
200 .iter()
201 .filter(|e| {
202 if let (Some(si_c), Some(fn_c)) = (e.si_created, e.fn_created) {
203 si_c < fn_c || {
204 if let Some(si_m) = e.si_modified {
205 si_m < fn_c
206 } else {
207 false
208 }
209 }
210 } else {
211 false
212 }
213 })
214 .collect()
215 }
216
217 #[must_use]
219 pub fn get_by_entry(&self, entry_number: u64) -> Option<&MftEntry> {
220 self.by_entry
221 .get(&entry_number)
222 .and_then(|&idx| self.entries.get(idx))
223 }
224
225 #[must_use]
227 pub fn get_by_key(&self, key: &EntryKey) -> Option<&MftEntry> {
228 self.by_key.get(key).and_then(|&idx| self.entries.get(idx))
229 }
230}
231
232fn to_datetime(ft: Filetime) -> Option<DateTime<Utc>> {
234 if ft.is_zero() {
235 return None;
236 }
237 let secs = ft.to_unix_seconds();
238 let sub_nanos = ft.to_unix_nanos().rem_euclid(1_000_000_000);
240 let nsec = u32::try_from(sub_nanos).unwrap_or(0);
241 DateTime::from_timestamp(secs, nsec)
242}
243
244fn resolve_full_path(entries: &[MftEntry], by_entry: &HashMap<u64, usize>, idx: usize) -> String {
246 let mut parts = Vec::new();
247 let mut cur = idx;
248 for _ in 0..256 {
250 let Some(e) = entries.get(cur) else {
251 break; };
253 parts.push(e.filename.clone());
254 if e.parent_entry == 5 || e.parent_entry == e.entry_number {
255 break;
256 }
257 match by_entry.get(&e.parent_entry) {
258 Some(&p) if p != cur => cur = p,
259 _ => break,
260 }
261 }
262 parts.reverse();
263 format!(".\\{}", parts.join("\\"))
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269
270 #[test]
271 fn test_mft_data_empty() {
272 let result = MftData::parse(&[]);
273 assert!(result.is_err() || result.unwrap().entries.is_empty());
274 }
275
276 #[test]
277 fn test_entry_key_equality() {
278 let k1 = EntryKey::new(100, 3);
279 let k2 = EntryKey::new(100, 3);
280 let k3 = EntryKey::new(100, 4);
281 assert_eq!(k1, k2);
282 assert_ne!(k1, k3);
283 }
284
285 #[test]
286 fn test_mft_data_get_by_entry_not_found() {
287 let mft_data = MftData {
289 entries: Vec::new(),
290 by_entry: HashMap::new(),
291 by_key: HashMap::new(),
292 };
293 assert!(mft_data.get_by_entry(100).is_none());
294 }
295
296 #[test]
297 fn test_mft_data_get_by_key_not_found() {
298 let mft_data = MftData {
299 entries: Vec::new(),
300 by_entry: HashMap::new(),
301 by_key: HashMap::new(),
302 };
303 let key = EntryKey::new(100, 3);
304 assert!(mft_data.get_by_key(&key).is_none());
305 }
306
307 fn make_mft_entry(
308 entry_number: u64,
309 sequence_number: u16,
310 filename: &str,
311 parent_entry: u64,
312 parent_sequence: u16,
313 is_dir: bool,
314 is_in_use: bool,
315 ) -> MftEntry {
316 MftEntry {
317 entry_number,
318 sequence_number,
319 filename: filename.to_string(),
320 parent_entry,
321 parent_sequence,
322 is_directory: is_dir,
323 is_in_use,
324 si_created: None,
325 si_modified: None,
326 si_mft_modified: None,
327 si_accessed: None,
328 fn_created: None,
329 fn_modified: None,
330 fn_mft_modified: None,
331 fn_accessed: None,
332 full_path: format!(".\\{filename}"),
333 file_size: 0,
334 has_ads: false,
335 }
336 }
337
338 #[test]
339 fn test_mft_data_get_by_entry_found() {
340 let entry = make_mft_entry(100, 3, "test.txt", 5, 5, false, true);
341 let mut by_entry = HashMap::new();
342 by_entry.insert(100u64, 0usize);
343 let mut by_key = HashMap::new();
344 by_key.insert(EntryKey::new(100, 3), 0usize);
345
346 let mft_data = MftData {
347 entries: vec![entry],
348 by_entry,
349 by_key,
350 };
351
352 let found = mft_data.get_by_entry(100);
353 assert!(found.is_some());
354 assert_eq!(found.unwrap().filename, "test.txt");
355 }
356
357 #[test]
358 fn parse_extracts_entry_fields_via_ntfs_forensic() {
359 let data = build_mft_entry_bytes(100, 1, 5, 5, "testfile.txt", 0x01);
360 let mft = MftData::parse(&data).expect("parse");
361 assert_eq!(mft.entries.len(), 1);
362 let e = &mft.entries[0];
363 assert_eq!(e.entry_number, 100);
364 assert_eq!(e.sequence_number, 1);
365 assert_eq!(e.filename, "testfile.txt");
366 assert_eq!(e.parent_entry, 5);
367 assert_eq!(e.parent_sequence, 5);
368 assert!(!e.is_directory);
369 assert!(e.is_in_use);
370 assert!(e.si_created.is_some());
371 assert!(e.fn_created.is_some());
372 assert!(!e.has_ads);
373 assert!(mft.by_entry.contains_key(&100));
374 assert!(mft.by_key.contains_key(&EntryKey::new(100, 1)));
375 }
376
377 #[test]
378 fn test_mft_data_get_by_key_found() {
379 let entry = make_mft_entry(100, 3, "test.txt", 5, 5, false, true);
380 let mut by_entry = HashMap::new();
381 by_entry.insert(100u64, 0usize);
382 let mut by_key = HashMap::new();
383 by_key.insert(EntryKey::new(100, 3), 0usize);
384
385 let mft_data = MftData {
386 entries: vec![entry],
387 by_entry,
388 by_key,
389 };
390
391 let key = EntryKey::new(100, 3);
392 let found = mft_data.get_by_key(&key);
393 assert!(found.is_some());
394 assert_eq!(found.unwrap().filename, "test.txt");
395 }
396
397 #[test]
398 fn test_detect_timestomping_si_before_fn() {
399 use chrono::DateTime;
400 let mut entry = make_mft_entry(100, 1, "suspicious.exe", 5, 5, false, true);
401 entry.si_created = Some(DateTime::from_timestamp(1_700_000_000, 0).unwrap());
403 entry.fn_created = Some(DateTime::from_timestamp(1_700_001_000, 0).unwrap());
404
405 let mut by_entry = HashMap::new();
406 by_entry.insert(100u64, 0usize);
407 let mft_data = MftData {
408 entries: vec![entry],
409 by_entry,
410 by_key: HashMap::new(),
411 };
412
413 let stomped = mft_data.detect_timestomping();
414 assert_eq!(stomped.len(), 1);
415 assert_eq!(stomped[0].filename, "suspicious.exe");
416 }
417
418 #[test]
419 fn test_detect_timestomping_si_modified_before_fn_created() {
420 use chrono::DateTime;
421 let mut entry = make_mft_entry(100, 1, "modified.exe", 5, 5, false, true);
422 entry.si_created = Some(DateTime::from_timestamp(1_700_001_000, 0).unwrap());
424 entry.si_modified = Some(DateTime::from_timestamp(1_700_000_000, 0).unwrap());
425 entry.fn_created = Some(DateTime::from_timestamp(1_700_001_000, 0).unwrap());
426
427 let mft_data = MftData {
428 entries: vec![entry],
429 by_entry: HashMap::new(),
430 by_key: HashMap::new(),
431 };
432
433 let stomped = mft_data.detect_timestomping();
434 assert_eq!(stomped.len(), 1);
435 }
436
437 #[test]
438 fn test_detect_timestomping_none_when_consistent() {
439 use chrono::DateTime;
440 let mut entry = make_mft_entry(100, 1, "normal.txt", 5, 5, false, true);
441 let ts = DateTime::from_timestamp(1_700_001_000, 0).unwrap();
442 entry.si_created = Some(ts);
443 entry.si_modified = Some(ts);
444 entry.fn_created = Some(ts);
445
446 let mft_data = MftData {
447 entries: vec![entry],
448 by_entry: HashMap::new(),
449 by_key: HashMap::new(),
450 };
451
452 let stomped = mft_data.detect_timestomping();
453 assert_eq!(stomped.len(), 0);
454 }
455
456 #[test]
457 fn test_detect_timestomping_no_timestamps() {
458 let entry = make_mft_entry(100, 1, "no_ts.txt", 5, 5, false, true);
459
460 let mft_data = MftData {
461 entries: vec![entry],
462 by_entry: HashMap::new(),
463 by_key: HashMap::new(),
464 };
465
466 let stomped = mft_data.detect_timestomping();
467 assert_eq!(stomped.len(), 0);
468 }
469
470 #[test]
471 fn test_seed_rewind() {
472 let entry = make_mft_entry(100, 1, "test.txt", 5, 5, false, true);
473 let mut by_entry = HashMap::new();
474 by_entry.insert(100u64, 0usize);
475 let mut by_key = HashMap::new();
476 by_key.insert(EntryKey::new(100, 1), 0usize);
477
478 let mft_data = MftData {
479 entries: vec![entry],
480 by_entry,
481 by_key,
482 };
483
484 let engine = mft_data.seed_rewind();
485 assert_eq!(engine.lookup_len(), 1);
486 let path = engine.resolve_path(&EntryKey::new(100, 1));
487 assert_eq!(path, ".\\test.txt");
488 }
489
490 #[test]
491 fn test_mft_data_multiple_entries() {
492 let e1 = make_mft_entry(100, 1, "file1.txt", 5, 5, false, true);
493 let e2 = make_mft_entry(200, 2, "file2.txt", 100, 1, false, true);
494 let e3 = make_mft_entry(300, 1, "dir1", 5, 5, true, true);
495
496 let mut by_entry = HashMap::new();
497 by_entry.insert(100u64, 0usize);
498 by_entry.insert(200u64, 1usize);
499 by_entry.insert(300u64, 2usize);
500
501 let mut by_key = HashMap::new();
502 by_key.insert(EntryKey::new(100, 1), 0usize);
503 by_key.insert(EntryKey::new(200, 2), 1usize);
504 by_key.insert(EntryKey::new(300, 1), 2usize);
505
506 let mft_data = MftData {
507 entries: vec![e1, e2, e3],
508 by_entry,
509 by_key,
510 };
511
512 assert_eq!(mft_data.entries.len(), 3);
513 assert_eq!(mft_data.get_by_entry(200).unwrap().filename, "file2.txt");
514 assert_eq!(
515 mft_data
516 .get_by_key(&EntryKey::new(300, 1))
517 .unwrap()
518 .filename,
519 "dir1"
520 );
521 assert!(
522 mft_data
523 .get_by_key(&EntryKey::new(300, 1))
524 .unwrap()
525 .is_directory
526 );
527 }
528
529 #[test]
530 fn test_detect_timestomping_si_modified_none() {
531 use chrono::DateTime;
532 let mut entry = make_mft_entry(100, 1, "check.exe", 5, 5, false, true);
533 let ts = DateTime::from_timestamp(1_700_001_000, 0).unwrap();
535 entry.si_created = Some(ts);
536 entry.si_modified = None;
537 entry.fn_created = Some(ts);
538
539 let mft_data = MftData {
540 entries: vec![entry],
541 by_entry: HashMap::new(),
542 by_key: HashMap::new(),
543 };
544
545 let stomped = mft_data.detect_timestomping();
546 assert_eq!(stomped.len(), 0);
547 }
548
549 #[test]
550 fn test_mft_entry_has_ads_field() {
551 let mut entry = make_mft_entry(100, 1, "ads.txt", 5, 5, false, true);
552 entry.has_ads = true;
553 assert!(entry.has_ads);
554 }
555
556 #[test]
557 fn test_mft_entry_file_size() {
558 let mut entry = make_mft_entry(100, 1, "big.bin", 5, 5, false, true);
559 entry.file_size = 1_048_576;
560 assert_eq!(entry.file_size, 1_048_576);
561 }
562
563 #[test]
564 fn test_mft_data_parse_invalid_data() {
565 let garbage = vec![0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02, 0x03, 0x04];
567 let result = MftData::parse(&garbage);
568 if let Ok(mft_data) = result {
570 assert!(mft_data.entries.is_empty());
571 } }
573
574 #[test]
575 fn test_mft_data_parse_short_data() {
576 let data = vec![0xAA; 512];
578 let result = MftData::parse(&data);
579 if let Ok(mft_data) = result {
580 assert!(mft_data.entries.is_empty());
581 } }
583
584 #[test]
585 fn test_mft_data_parse_corrupt_entries_skipped() {
586 let mut data = vec![0u8; 1024 * 4];
599 for i in 0..4 {
600 let o = i * 1024;
601 data[o..o + 4].copy_from_slice(b"FILE");
602 data[o + 0x04..o + 0x06].copy_from_slice(&0x30u16.to_le_bytes()); data[o + 0x06..o + 0x08].copy_from_slice(&3u16.to_le_bytes()); data[o + 0x14..o + 0x16].copy_from_slice(&0x38u16.to_le_bytes()); data[o + 0x16..o + 0x18].copy_from_slice(&0x01u16.to_le_bytes()); data[o + 0x18..o + 0x1C].copy_from_slice(&0x38u32.to_le_bytes()); data[o + 0x1C..o + 0x20].copy_from_slice(&1024u32.to_le_bytes()); data[o + 0x38..o + 0x3C].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
610 }
611 let mft_data = MftData::parse(&data).unwrap();
612 assert!(mft_data.entries.is_empty());
614 }
615
616 #[test]
617 fn test_mft_entry_ads_detection_field() {
618 let mut entry = make_mft_entry(100, 1, "file_with_ads.txt", 5, 5, false, true);
620 assert!(!entry.has_ads);
621 entry.has_ads = true;
622 assert!(entry.has_ads);
623
624 let mft_data = MftData {
626 entries: vec![entry],
627 by_entry: HashMap::new(),
628 by_key: HashMap::new(),
629 };
630 assert_eq!(mft_data.detect_timestomping().len(), 0);
632 }
633
634 #[test]
635 fn test_mft_data_seed_rewind_multiple() {
636 let e1 = make_mft_entry(10, 1, "Users", 5, 5, true, true);
638 let e2 = make_mft_entry(20, 1, "admin", 10, 1, true, true);
639 let e3 = make_mft_entry(30, 1, "Desktop", 20, 1, true, true);
640
641 let mut by_entry = HashMap::new();
642 by_entry.insert(10u64, 0usize);
643 by_entry.insert(20u64, 1usize);
644 by_entry.insert(30u64, 2usize);
645
646 let mut by_key = HashMap::new();
647 by_key.insert(EntryKey::new(10, 1), 0usize);
648 by_key.insert(EntryKey::new(20, 1), 1usize);
649 by_key.insert(EntryKey::new(30, 1), 2usize);
650
651 let mft_data = MftData {
652 entries: vec![e1, e2, e3],
653 by_entry,
654 by_key,
655 };
656
657 let engine = mft_data.seed_rewind();
658 assert_eq!(engine.lookup_len(), 3);
659 let path = engine.resolve_path(&EntryKey::new(30, 1));
660 assert_eq!(path, ".\\Users\\admin\\Desktop");
661 }
662
663 #[test]
664 fn test_mft_data_full_path_field() {
665 let entry = make_mft_entry(100, 1, "test.txt", 5, 5, false, true);
666 assert_eq!(entry.full_path, ".\\test.txt");
667 }
668
669 fn build_mft_entry_bytes(
677 entry_number: u32,
678 sequence: u16,
679 parent_entry: u64,
680 parent_seq: u16,
681 filename: &str,
682 flags: u16, ) -> Vec<u8> {
684 let name_utf16: Vec<u16> = filename.encode_utf16().collect();
685 let fn_name_len = name_utf16.len();
686
687 let si_data_size: u32 = 72;
691 let si_attr_header_size: u16 = 24;
692 let si_total_size: u32 = u32::from(si_attr_header_size) + si_data_size;
693 let si_total_aligned = (si_total_size + 7) & !7;
694
695 let fn_data_size: u32 = 66 + (fn_name_len as u32 * 2);
699 let fn_attr_header_size: u16 = 24;
700 let fn_total_size: u32 = u32::from(fn_attr_header_size) + fn_data_size;
701 let fn_total_aligned = (fn_total_size + 7) & !7;
702
703 let first_attr_offset: u16 = 0x38; let bytes_used: u32 =
706 u32::from(first_attr_offset) + si_total_aligned + fn_total_aligned + 8; let alloc_size: u32 = 1024;
708 let mut buf = vec![0u8; alloc_size as usize];
709
710 buf[0..4].copy_from_slice(b"FILE"); buf[0x04..0x06].copy_from_slice(&0x30u16.to_le_bytes()); buf[0x06..0x08].copy_from_slice(&3u16.to_le_bytes()); buf[0x08..0x10].copy_from_slice(&0u64.to_le_bytes()); buf[0x10..0x12].copy_from_slice(&sequence.to_le_bytes()); buf[0x12..0x14].copy_from_slice(&0u16.to_le_bytes()); buf[0x14..0x16].copy_from_slice(&first_attr_offset.to_le_bytes()); buf[0x16..0x18].copy_from_slice(&flags.to_le_bytes()); buf[0x18..0x1C].copy_from_slice(&bytes_used.to_le_bytes()); buf[0x1C..0x20].copy_from_slice(&alloc_size.to_le_bytes()); buf[0x20..0x28].copy_from_slice(&0u64.to_le_bytes()); buf[0x28..0x2C].copy_from_slice(&0u32.to_le_bytes()); buf[0x2C..0x30].copy_from_slice(&entry_number.to_le_bytes()); buf[0x30..0x32].copy_from_slice(&0x0001u16.to_le_bytes()); buf[0x32..0x34].copy_from_slice(&0x0000u16.to_le_bytes()); buf[0x34..0x36].copy_from_slice(&0x0000u16.to_le_bytes()); buf[0x1FE..0x200].copy_from_slice(&0x0001u16.to_le_bytes());
731 buf[0x3FE..0x400].copy_from_slice(&0x0001u16.to_le_bytes());
732
733 let mut off = first_attr_offset as usize;
734
735 buf[off..off + 4].copy_from_slice(&0x10u32.to_le_bytes()); buf[off + 4..off + 8].copy_from_slice(&si_total_aligned.to_le_bytes()); buf[off + 8] = 0; buf[off + 9] = 0; buf[off + 10..off + 12].copy_from_slice(&0u16.to_le_bytes()); buf[off + 12..off + 14].copy_from_slice(&0u16.to_le_bytes()); buf[off + 14..off + 16].copy_from_slice(&0u16.to_le_bytes()); buf[off + 16..off + 20].copy_from_slice(&si_data_size.to_le_bytes()); buf[off + 20..off + 22].copy_from_slice(&si_attr_header_size.to_le_bytes()); buf[off + 22..off + 24].copy_from_slice(&0u16.to_le_bytes()); let ts: i64 = 133_500_480_000_000_000; let si_data_off = off + si_attr_header_size as usize;
750 buf[si_data_off..si_data_off + 8].copy_from_slice(&ts.to_le_bytes()); buf[si_data_off + 8..si_data_off + 16].copy_from_slice(&ts.to_le_bytes()); buf[si_data_off + 16..si_data_off + 24].copy_from_slice(&ts.to_le_bytes()); buf[si_data_off + 24..si_data_off + 32].copy_from_slice(&ts.to_le_bytes()); off += si_total_aligned as usize;
756
757 buf[off..off + 4].copy_from_slice(&0x30u32.to_le_bytes()); buf[off + 4..off + 8].copy_from_slice(&fn_total_aligned.to_le_bytes()); buf[off + 8] = 0; buf[off + 9] = 0; buf[off + 10..off + 12].copy_from_slice(&0u16.to_le_bytes()); buf[off + 12..off + 14].copy_from_slice(&0u16.to_le_bytes()); buf[off + 14..off + 16].copy_from_slice(&1u16.to_le_bytes()); buf[off + 16..off + 20].copy_from_slice(&fn_data_size.to_le_bytes()); buf[off + 20..off + 22].copy_from_slice(&fn_attr_header_size.to_le_bytes()); buf[off + 22..off + 24].copy_from_slice(&0u16.to_le_bytes()); let fn_data_off = off + fn_attr_header_size as usize;
770 let parent_ref = parent_entry | (u64::from(parent_seq) << 48);
772 buf[fn_data_off..fn_data_off + 8].copy_from_slice(&parent_ref.to_le_bytes());
773 buf[fn_data_off + 8..fn_data_off + 16].copy_from_slice(&ts.to_le_bytes()); buf[fn_data_off + 16..fn_data_off + 24].copy_from_slice(&ts.to_le_bytes()); buf[fn_data_off + 24..fn_data_off + 32].copy_from_slice(&ts.to_le_bytes()); buf[fn_data_off + 32..fn_data_off + 40].copy_from_slice(&ts.to_le_bytes()); buf[fn_data_off + 40..fn_data_off + 48].copy_from_slice(&0u64.to_le_bytes());
780 buf[fn_data_off + 48..fn_data_off + 56].copy_from_slice(&0u64.to_le_bytes());
781 buf[fn_data_off + 56..fn_data_off + 60].copy_from_slice(&0u32.to_le_bytes());
783 buf[fn_data_off + 60..fn_data_off + 64].copy_from_slice(&0u32.to_le_bytes());
784 buf[fn_data_off + 64] = fn_name_len as u8;
786 buf[fn_data_off + 65] = 0x03;
788 for (i, &ch) in name_utf16.iter().enumerate() {
790 let name_off = fn_data_off + 66 + i * 2;
791 buf[name_off..name_off + 2].copy_from_slice(&ch.to_le_bytes());
792 }
793
794 off += fn_total_aligned as usize;
795
796 buf[off..off + 4].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
798
799 buf
800 }
801
802 #[test]
803 fn test_mft_data_parse_valid_entry() {
804 let entry_data = build_mft_entry_bytes(
811 100, 1, 5, 5, "testfile.txt",
816 0x01, );
818
819 let mft_data = MftData::parse(&entry_data).unwrap();
820 if !mft_data.entries.is_empty() {
822 let e = &mft_data.entries[0];
823 assert_eq!(e.filename, "testfile.txt");
824 assert_eq!(e.parent_entry, 5);
825 assert!(e.si_created.is_some());
826 assert!(e.fn_created.is_some());
827 assert!(!e.has_ads);
828 let entry_num = e.entry_number;
831 assert!(mft_data.by_entry.contains_key(&entry_num));
832 assert!(mft_data
833 .by_key
834 .contains_key(&EntryKey::new(entry_num, e.sequence_number)));
835 } }
837
838 #[test]
839 fn test_mft_data_parse_entry_with_ads() {
840 let mut entry_data = build_mft_entry_bytes(200, 1, 5, 5, "ads_file.txt", 0x01);
843
844 let first_attr_offset = 0x38usize;
847 let mut off = first_attr_offset;
848 loop {
850 if off + 4 > entry_data.len() {
851 break; }
853 let attr_type = u32::from_le_bytes([
854 entry_data[off],
855 entry_data[off + 1],
856 entry_data[off + 2],
857 entry_data[off + 3],
858 ]);
859 if attr_type == 0xFFFF_FFFF {
860 break;
861 }
862 let attr_size = u32::from_le_bytes([
863 entry_data[off + 4],
864 entry_data[off + 5],
865 entry_data[off + 6],
866 entry_data[off + 7],
867 ]) as usize;
868 if attr_size == 0 || off + attr_size > entry_data.len() {
869 break; }
871 off += attr_size;
872 }
873
874 let ads_name = "Zone.Identifier";
877 let ads_name_utf16: Vec<u16> = ads_name.encode_utf16().collect();
878 let ads_name_bytes = ads_name_utf16.len() * 2;
879 let ads_attr_header_size = 24u16;
880 let ads_content_size = 0u32; let ads_name_offset = ads_attr_header_size;
884 let ads_total =
885 (u32::from(ads_attr_header_size) + ads_name_bytes as u32 + ads_content_size + 7) & !7;
886
887 if off + ads_total as usize + 8 <= entry_data.len() {
888 entry_data[off..off + 4].copy_from_slice(&0x80u32.to_le_bytes()); entry_data[off + 4..off + 8].copy_from_slice(&ads_total.to_le_bytes());
890 entry_data[off + 8] = 0; entry_data[off + 9] = ads_name_utf16.len() as u8; entry_data[off + 10..off + 12].copy_from_slice(&ads_name_offset.to_le_bytes());
893 entry_data[off + 12..off + 14].copy_from_slice(&0u16.to_le_bytes());
894 entry_data[off + 14..off + 16].copy_from_slice(&2u16.to_le_bytes()); entry_data[off + 16..off + 20].copy_from_slice(&ads_content_size.to_le_bytes());
896 let content_off = ads_name_offset + ads_name_bytes as u16;
897 entry_data[off + 20..off + 22].copy_from_slice(&content_off.to_le_bytes());
898
899 let name_start = off + ads_name_offset as usize;
901 for (i, &ch) in ads_name_utf16.iter().enumerate() {
902 let pos = name_start + i * 2;
903 if pos + 2 <= entry_data.len() {
904 entry_data[pos..pos + 2].copy_from_slice(&ch.to_le_bytes());
905 }
906 }
907
908 let end_off = off + ads_total as usize;
909 if end_off + 4 <= entry_data.len() {
910 entry_data[end_off..end_off + 4].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
911 }
912
913 let new_bytes_used = (end_off + 8) as u32;
915 entry_data[0x18..0x1C].copy_from_slice(&new_bytes_used.to_le_bytes());
916 } let mft_data = MftData::parse(&entry_data).unwrap();
919 if !mft_data.entries.is_empty() {
920 let e = &mft_data.entries[0];
921 assert_eq!(e.filename, "ads_file.txt");
922 } }
928
929 #[test]
930 fn test_mft_data_parse_multiple_entries() {
931 let entry0 = build_mft_entry_bytes(0, 1, 5, 5, "root", 0x03); let entry1 = build_mft_entry_bytes(1, 1, 0, 1, "file1.txt", 0x01);
934 let entry2 = build_mft_entry_bytes(2, 1, 0, 1, "file2.doc", 0x01);
935
936 let mut data = Vec::new();
937 data.extend_from_slice(&entry0);
938 data.extend_from_slice(&entry1);
939 data.extend_from_slice(&entry2);
940
941 let mft_data = MftData::parse(&data).unwrap();
942 assert!(mft_data.entries.len() <= 3);
945 }
946
947 #[test]
948 fn test_mft_data_parse_entry_without_filename_skipped() {
949 let mut buf = vec![0u8; 1024];
951
952 buf[0..4].copy_from_slice(b"FILE");
953 buf[0x04..0x06].copy_from_slice(&0x30u16.to_le_bytes());
954 buf[0x06..0x08].copy_from_slice(&3u16.to_le_bytes());
955 buf[0x10..0x12].copy_from_slice(&1u16.to_le_bytes()); buf[0x14..0x16].copy_from_slice(&0x38u16.to_le_bytes()); buf[0x16..0x18].copy_from_slice(&0x01u16.to_le_bytes()); let si_size = 96u32;
959 let si_aligned = (si_size + 7) & !7;
960 buf[0x18..0x1C].copy_from_slice(&(0x38u32 + si_aligned + 8).to_le_bytes()); buf[0x1C..0x20].copy_from_slice(&1024u32.to_le_bytes()); buf[0x28..0x2C].copy_from_slice(&50u32.to_le_bytes()); buf[0x30..0x32].copy_from_slice(&0x0001u16.to_le_bytes());
966 buf[0x1FE..0x200].copy_from_slice(&0x0001u16.to_le_bytes());
967 buf[0x3FE..0x400].copy_from_slice(&0x0001u16.to_le_bytes());
968
969 let off = 0x38;
971 buf[off..off + 4].copy_from_slice(&0x10u32.to_le_bytes());
972 buf[off + 4..off + 8].copy_from_slice(&si_aligned.to_le_bytes());
973 buf[off + 8] = 0;
974 buf[off + 16..off + 20].copy_from_slice(&72u32.to_le_bytes());
975 buf[off + 20..off + 22].copy_from_slice(&24u16.to_le_bytes());
976
977 let end_off = off + si_aligned as usize;
978 buf[end_off..end_off + 4].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
979
980 let mft_data = MftData::parse(&buf).unwrap();
981 assert!(mft_data.entries.is_empty());
983 }
984
985 #[test]
986 fn test_mft_data_is_directory_and_in_use() {
987 let dir_entry = make_mft_entry(100, 1, "Documents", 5, 5, true, true);
988 assert!(dir_entry.is_directory);
989 assert!(dir_entry.is_in_use);
990
991 let deleted_entry = make_mft_entry(200, 1, "deleted.txt", 5, 5, false, false);
992 assert!(!deleted_entry.is_directory);
993 assert!(!deleted_entry.is_in_use);
994 }
995
996 fn build_raw_mft_entry_buf(seq: u16, flags: u16) -> Vec<u8> {
998 let mut buf = vec![0u8; 1024];
999
1000 buf[0..4].copy_from_slice(b"FILE");
1002 buf[0x04..0x06].copy_from_slice(&0x30u16.to_le_bytes());
1004 buf[0x06..0x08].copy_from_slice(&3u16.to_le_bytes());
1006 buf[0x08..0x10].copy_from_slice(&0u64.to_le_bytes());
1008 buf[0x10..0x12].copy_from_slice(&seq.to_le_bytes());
1010 buf[0x12..0x14].copy_from_slice(&1u16.to_le_bytes());
1012 buf[0x14..0x16].copy_from_slice(&0x38u16.to_le_bytes());
1014 buf[0x16..0x18].copy_from_slice(&flags.to_le_bytes());
1016 buf[0x18..0x1C].copy_from_slice(&512u32.to_le_bytes());
1018 buf[0x1C..0x20].copy_from_slice(&1024u32.to_le_bytes());
1020 buf[0x28..0x2A].copy_from_slice(&0u16.to_le_bytes());
1023
1024 let marker: u16 = 0x0001;
1026 buf[0x30..0x32].copy_from_slice(&marker.to_le_bytes());
1027 buf[0x32..0x34].copy_from_slice(&marker.to_le_bytes());
1028 buf[0x34..0x36].copy_from_slice(&marker.to_le_bytes());
1029
1030 buf[510..512].copy_from_slice(&marker.to_le_bytes());
1032 buf[1022..1024].copy_from_slice(&marker.to_le_bytes());
1033
1034 buf[0x38..0x3C].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
1036
1037 buf
1038 }
1039
1040 #[test]
1041 fn test_mft_parse_with_corrupt_entry() {
1042 let entry0 = build_raw_mft_entry_buf(1, 0x01);
1048
1049 let mut entry1 = vec![0u8; 1024];
1052 entry1[0..4].copy_from_slice(b"DEAD"); entry1[0x1C..0x20].copy_from_slice(&1024u32.to_le_bytes());
1054
1055 let mut data = Vec::new();
1056 data.extend_from_slice(&entry0);
1057 data.extend_from_slice(&entry1);
1058
1059 let mft_data = MftData::parse(&data).unwrap();
1061 let _ = mft_data.entries.len();
1062 }
1063
1064 #[test]
1065 fn parse_skips_entry_with_fixup_mismatch() {
1066 let mut e = build_mft_entry_bytes(202, 1, 5, 5, "x.txt", 0x01);
1068 e[0x1FE] = 0xFF;
1069 e[0x1FF] = 0xFF;
1070 let m = MftData::parse(&e).unwrap();
1071 assert!(m.entries.is_empty());
1072 }
1073
1074 #[test]
1075 fn parse_covers_filename_namespace_priorities() {
1076 const NS_OFF: usize = 0xF1;
1078
1079 let mut dos = build_mft_entry_bytes(210, 1, 5, 5, "DOS~1.TXT", 0x01);
1081 dos[NS_OFF] = 0x02;
1082 let m = MftData::parse(&dos).unwrap();
1083 assert_eq!(m.entries.len(), 1);
1084 assert_eq!(m.entries[0].filename, "DOS~1.TXT");
1085
1086 let mut posix = build_mft_entry_bytes(211, 1, 5, 5, "posix.txt", 0x01);
1088 posix[NS_OFF] = 0x00;
1089 let m2 = MftData::parse(&posix).unwrap();
1090 assert_eq!(m2.entries[0].filename, "posix.txt");
1091 }
1092
1093 #[test]
1094 fn parse_full_path_stops_on_missing_parent() {
1095 let e = build_mft_entry_bytes(300, 1, 999, 1, "orphan.txt", 0x01);
1098 let m = MftData::parse(&e).unwrap();
1099 assert_eq!(m.entries.len(), 1);
1100 assert!(m.entries[0].full_path.contains("orphan.txt"));
1101 }
1102
1103 fn entry_with_unnamed_data(
1106 entry_num: u32,
1107 name: &str,
1108 non_resident: bool,
1109 size: u64,
1110 ) -> Vec<u8> {
1111 let mut buf = build_mft_entry_bytes(entry_num, 1, 5, 5, name, 0x01);
1112 let mut off = 0x38usize;
1114 loop {
1115 let t = u32::from_le_bytes(buf[off..off + 4].try_into().unwrap());
1116 if t == 0xFFFF_FFFF {
1117 break;
1118 }
1119 let sz = u32::from_le_bytes(buf[off + 4..off + 8].try_into().unwrap()) as usize;
1120 off += sz;
1121 }
1122 let total: u32 = if non_resident {
1123 let total = ((0x40 + 8 + 7) & !7) as u32;
1125 buf[off..off + 4].copy_from_slice(&0x80u32.to_le_bytes());
1126 buf[off + 4..off + 8].copy_from_slice(&total.to_le_bytes());
1127 buf[off + 8] = 1; buf[off + 9] = 0; buf[off + 14..off + 16].copy_from_slice(&3u16.to_le_bytes()); buf[off + 0x20..off + 0x22].copy_from_slice(&0x40u16.to_le_bytes()); buf[off + 0x28..off + 0x30].copy_from_slice(&4096u64.to_le_bytes()); buf[off + 0x30..off + 0x38].copy_from_slice(&size.to_le_bytes()); buf[off + 0x38..off + 0x40].copy_from_slice(&size.to_le_bytes()); total
1135 } else {
1136 let content = size as usize;
1137 let total = ((24 + content + 7) & !7) as u32;
1138 buf[off..off + 4].copy_from_slice(&0x80u32.to_le_bytes());
1139 buf[off + 4..off + 8].copy_from_slice(&total.to_le_bytes());
1140 buf[off + 8] = 0; buf[off + 9] = 0; buf[off + 14..off + 16].copy_from_slice(&3u16.to_le_bytes());
1143 buf[off + 16..off + 20].copy_from_slice(&(content as u32).to_le_bytes()); buf[off + 20..off + 22].copy_from_slice(&24u16.to_le_bytes()); total
1146 };
1147 let end = off + total as usize;
1148 buf[end..end + 4].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
1149 buf[0x18..0x1C].copy_from_slice(&((end + 8) as u32).to_le_bytes()); buf
1151 }
1152
1153 #[test]
1154 fn parse_extracts_file_size_from_unnamed_data() {
1155 let res = entry_with_unnamed_data(400, "res.txt", false, 42);
1157 let m = MftData::parse(&res).unwrap();
1158 assert_eq!(m.entries.len(), 1);
1159 assert_eq!(m.entries[0].file_size, 42);
1160 let nr = entry_with_unnamed_data(401, "nr.txt", true, 123_456);
1162 let m2 = MftData::parse(&nr).unwrap();
1163 assert_eq!(m2.entries.len(), 1);
1164 assert_eq!(m2.entries[0].file_size, 123_456);
1165 }
1166
1167 #[test]
1168 fn parse_handles_out_of_bounds_attribute_offset_without_panic() {
1169 let mut e = build_mft_entry_bytes(310, 1, 5, 5, "x.txt", 0x01);
1173 e[0x14..0x16].copy_from_slice(&0x0410u16.to_le_bytes());
1174 let m = MftData::parse(&e).unwrap();
1175 assert!(m.entries.is_empty());
1176 }
1177
1178 #[test]
1179 fn parse_keeps_higher_priority_filename_over_later_lower_priority() {
1180 let base = build_mft_entry_bytes(330, 1, 5, 5, "WIN32.TXT", 0x01);
1187 let mut e = base.clone();
1188
1189 let mut off = 0x38usize;
1191 let mut fn_off = None;
1192 let mut fn_len = 0usize;
1193 loop {
1194 let t = u32::from_le_bytes(e[off..off + 4].try_into().unwrap());
1195 if t == 0xFFFF_FFFF {
1196 break;
1197 }
1198 let sz = u32::from_le_bytes(e[off + 4..off + 8].try_into().unwrap()) as usize;
1199 if t == 0x30 {
1200 fn_off = Some(off);
1201 fn_len = sz;
1202 }
1203 off += sz;
1204 }
1205 let fn_off = fn_off.unwrap();
1206 let end_marker = off;
1207
1208 let mut second: Vec<u8> = e[fn_off..fn_off + fn_len].to_vec();
1210 second[24 + 65] = 0x02; e[end_marker..end_marker + fn_len].copy_from_slice(&second);
1213 let new_end = end_marker + fn_len;
1215 e[new_end..new_end + 4].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
1216 e[0x18..0x1C].copy_from_slice(&((new_end + 8) as u32).to_le_bytes()); let m = MftData::parse(&e).unwrap();
1219 assert_eq!(m.entries.len(), 1);
1220 assert_eq!(m.entries[0].filename, "WIN32.TXT");
1221 }
1222
1223 #[test]
1224 fn parse_skips_entry_when_parse_attributes_errors() {
1225 let mut e = build_mft_entry_bytes(320, 1, 5, 5, "x.txt", 0x01);
1231 e[0x3C..0x40].copy_from_slice(&0x0000_0001u32.to_le_bytes());
1232 let m = MftData::parse(&e).unwrap();
1233 assert!(m.entries.is_empty());
1234 }
1235}