1use std::collections::HashMap;
23
24use crate::carve::CarvedMftEntry;
25use crate::usn::{UsnReason, UsnRecord};
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
29pub struct EntryKey {
30 pub entry: u64,
31 pub sequence: u16,
32}
33
34impl EntryKey {
35 pub fn new(entry: u64, sequence: u16) -> Self {
36 Self { entry, sequence }
37 }
38
39 pub fn root() -> Self {
41 Self {
42 entry: 5,
43 sequence: 5,
44 }
45 }
46
47 pub fn is_root(&self) -> bool {
48 self.entry == 5
49 }
50}
51
52#[derive(Debug, Clone)]
54pub struct EntryInfo {
55 pub name: String,
57 pub parent: EntryKey,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum RecordSource {
64 Allocated,
66 Carved,
68 Ghost,
70}
71
72impl RecordSource {
73 pub fn as_str(&self) -> &'static str {
75 match self {
76 RecordSource::Allocated => "allocated",
77 RecordSource::Carved => "entry-carved",
78 RecordSource::Ghost => "ghost",
79 }
80 }
81}
82
83#[derive(Debug, Clone)]
85pub struct ResolvedRecord {
86 pub record: UsnRecord,
88 pub full_path: String,
90 pub parent_path: String,
92 pub source: RecordSource,
94}
95
96pub struct RewindEngine {
98 lookup: HashMap<EntryKey, EntryInfo>,
100}
101
102impl RewindEngine {
103 pub fn new() -> Self {
105 Self {
106 lookup: HashMap::new(),
107 }
108 }
109
110 pub fn from_mft<I>(mft_entries: I) -> Self
115 where
116 I: IntoIterator<Item = (u64, u16, String, u64, u16)>,
117 {
118 let mut engine = Self::new();
119 for (entry, seq, name, parent_entry, parent_seq) in mft_entries {
120 engine.lookup.insert(
121 EntryKey::new(entry, seq),
122 EntryInfo {
123 name,
124 parent: EntryKey::new(parent_entry, parent_seq),
125 },
126 );
127 }
128 engine
129 }
130
131 pub fn lookup_len(&self) -> usize {
133 self.lookup.len()
134 }
135
136 pub fn seed_from_carved(&mut self, entries: &[CarvedMftEntry]) {
142 for e in entries {
143 let key = EntryKey::new(e.entry_number, e.sequence_number);
144 self.lookup.entry(key).or_insert(EntryInfo {
145 name: e.filename.clone(),
146 parent: EntryKey::new(e.parent_entry, e.parent_sequence),
147 });
148 }
149 }
150
151 pub fn insert(&mut self, key: EntryKey, info: EntryInfo) {
153 self.lookup.insert(key, info);
154 }
155
156 pub fn resolve_path(&self, key: &EntryKey) -> String {
161 self.resolve_path_inner(key, 0)
162 }
163
164 fn resolve_path_inner(&self, key: &EntryKey, depth: usize) -> String {
165 if depth > 256 {
167 return format!("UNRESOLVED({}:{})", key.entry, key.sequence);
168 }
169
170 if key.is_root() {
171 return ".".to_string();
172 }
173
174 if let Some(info) = self.lookup.get(key) {
175 let parent_path = self.resolve_path_inner(&info.parent, depth + 1);
176 format!("{}\\{}", parent_path, info.name)
177 } else {
178 format!("UNKNOWN({}:{})", key.entry, key.sequence)
179 }
180 }
181
182 pub fn rewind(&mut self, records: &[UsnRecord]) -> Vec<ResolvedRecord> {
190 for record in records.iter().rev() {
194 let key = EntryKey::new(record.mft_entry, record.mft_sequence);
195 let parent_key = EntryKey::new(record.parent_mft_entry, record.parent_mft_sequence);
196
197 if record.reason.contains(UsnReason::RENAME_OLD_NAME) {
201 self.lookup.insert(
206 key,
207 EntryInfo {
208 name: record.filename.clone(),
209 parent: parent_key,
210 },
211 );
212 } else {
213 self.lookup.entry(key).or_insert(EntryInfo {
215 name: record.filename.clone(),
216 parent: parent_key,
217 });
218 }
219
220 }
223
224 let mut forward_lookup = self.lookup.clone();
231 let mut results = Vec::with_capacity(records.len());
232
233 for record in records {
234 let key = EntryKey::new(record.mft_entry, record.mft_sequence);
235 let parent_key = EntryKey::new(record.parent_mft_entry, record.parent_mft_sequence);
236
237 if record.reason.contains(UsnReason::RENAME_NEW_NAME) {
239 forward_lookup.insert(
241 key,
242 EntryInfo {
243 name: record.filename.clone(),
244 parent: parent_key,
245 },
246 );
247 } else if record.reason.contains(UsnReason::FILE_CREATE) {
248 forward_lookup.insert(
250 key,
251 EntryInfo {
252 name: record.filename.clone(),
253 parent: parent_key,
254 },
255 );
256 } else {
257 forward_lookup.entry(key).or_insert(EntryInfo {
258 name: record.filename.clone(),
259 parent: parent_key,
260 });
261 }
262
263 let parent_path = resolve_path_from(&forward_lookup, &parent_key);
265 let full_path = format!("{}\\{}", parent_path, record.filename);
266
267 results.push(ResolvedRecord {
268 record: record.clone(),
269 full_path,
270 parent_path,
271 source: RecordSource::Allocated,
272 });
273 }
274
275 results
276 }
277}
278
279fn resolve_path_from(lookup: &HashMap<EntryKey, EntryInfo>, key: &EntryKey) -> String {
281 resolve_path_from_inner(lookup, key, 0)
282}
283
284fn resolve_path_from_inner(
285 lookup: &HashMap<EntryKey, EntryInfo>,
286 key: &EntryKey,
287 depth: usize,
288) -> String {
289 if depth > 256 {
290 return format!("UNRESOLVED({}:{})", key.entry, key.sequence);
291 }
292 if key.is_root() {
293 return ".".to_string();
294 }
295 if let Some(info) = lookup.get(key) {
296 let parent_path = resolve_path_from_inner(lookup, &info.parent, depth + 1);
297 format!("{}\\{}", parent_path, info.name)
298 } else {
299 format!("UNKNOWN({}:{})", key.entry, key.sequence)
300 }
301}
302
303impl Default for RewindEngine {
304 fn default() -> Self {
305 Self::new()
306 }
307}
308
309#[cfg(test)]
312mod tests {
313 use super::*;
314 use crate::usn::{FileAttributes, UsnReason};
315 use chrono::DateTime;
316
317 fn make_record(
318 entry: u64,
319 seq: u16,
320 parent_entry: u64,
321 parent_seq: u16,
322 reason: UsnReason,
323 filename: &str,
324 usn: i64,
325 ) -> UsnRecord {
326 UsnRecord {
327 mft_entry: entry,
328 mft_sequence: seq,
329 parent_mft_entry: parent_entry,
330 parent_mft_sequence: parent_seq,
331 usn,
332 timestamp: DateTime::from_timestamp(1_700_000_000, 0).unwrap(),
333 reason,
334 filename: filename.to_string(),
335 file_attributes: FileAttributes::ARCHIVE,
336 source_info: 0,
337 security_id: 0,
338 major_version: 2,
339 }
340 }
341
342 #[test]
343 fn test_entry_key_root() {
344 let root = EntryKey::root();
345 assert!(root.is_root());
346 assert_eq!(root.entry, 5);
347 }
348
349 #[test]
350 fn test_record_source_as_str() {
351 assert_eq!(RecordSource::Allocated.as_str(), "allocated");
353 assert_eq!(RecordSource::Carved.as_str(), "entry-carved");
354 assert_eq!(RecordSource::Ghost.as_str(), "ghost");
355 }
356
357 #[test]
358 fn test_resolve_path_simple() {
359 let engine = RewindEngine::from_mft(vec![
361 (100, 1, "Users".into(), 5, 5),
362 (200, 1, "admin".into(), 100, 1),
363 ]);
364
365 let path = engine.resolve_path(&EntryKey::new(200, 1));
366 assert_eq!(path, ".\\Users\\admin");
367 }
368
369 #[test]
370 fn test_resolve_path_root() {
371 let engine = RewindEngine::new();
372 let path = engine.resolve_path(&EntryKey::root());
373 assert_eq!(path, ".");
374 }
375
376 #[test]
377 fn test_resolve_path_unknown_entry() {
378 let engine = RewindEngine::new();
379 let path = engine.resolve_path(&EntryKey::new(999, 1));
380 assert!(path.contains("UNKNOWN"));
381 }
382
383 #[test]
384 fn test_rewind_simple_create() {
385 let mut engine = RewindEngine::from_mft(vec![(50, 1, "temp".into(), 5, 5)]);
387
388 let records = vec![make_record(
389 100,
390 1,
391 50,
392 1,
393 UsnReason::FILE_CREATE,
394 "malware.exe",
395 100,
396 )];
397
398 let resolved = engine.rewind(&records);
399 assert_eq!(resolved.len(), 1);
400 assert_eq!(resolved[0].full_path, ".\\temp\\malware.exe");
401 assert_eq!(resolved[0].parent_path, ".\\temp");
402 }
403
404 #[test]
405 fn test_rewind_resolves_unknown_parent_via_journal() {
406 let mut engine = RewindEngine::from_mft(vec![(30, 1, "Intel".into(), 5, 5)]);
416
417 let records = vec![
425 make_record(500, 1, 30, 1, UsnReason::FILE_CREATE, "Drivers", 10),
426 make_record(983, 4, 500, 1, UsnReason::FILE_CREATE, "ip_scanner", 20),
427 make_record(1500, 1, 983, 4, UsnReason::FILE_CREATE, "data.txt", 30),
428 make_record(1500, 1, 983, 4, UsnReason::FILE_DELETE, "data.txt", 40),
429 make_record(983, 4, 500, 1, UsnReason::FILE_DELETE, "ip_scanner", 50),
430 make_record(983, 6, 5, 5, UsnReason::FILE_CREATE, "NewFolder", 60),
431 ];
432
433 let resolved = engine.rewind(&records);
434
435 assert_eq!(
437 resolved[2].full_path,
438 ".\\Intel\\Drivers\\ip_scanner\\data.txt"
439 );
440 assert_eq!(resolved[5].full_path, ".\\NewFolder");
442 }
443
444 #[test]
445 fn test_rewind_handles_rename() {
446 let mut engine = RewindEngine::from_mft(vec![]);
450
451 let records = vec![
452 make_record(100, 1, 5, 5, UsnReason::FILE_CREATE, "old_name", 10),
454 make_record(200, 1, 100, 1, UsnReason::FILE_CREATE, "before.txt", 20),
456 make_record(100, 1, 5, 5, UsnReason::RENAME_OLD_NAME, "old_name", 30),
458 make_record(100, 1, 5, 5, UsnReason::RENAME_NEW_NAME, "new_name", 31),
459 make_record(300, 1, 100, 1, UsnReason::FILE_CREATE, "after.txt", 40),
461 ];
462
463 let resolved = engine.rewind(&records);
464
465 assert_eq!(resolved[1].full_path, ".\\old_name\\before.txt");
467 assert_eq!(resolved[4].full_path, ".\\new_name\\after.txt");
469 }
470
471 #[test]
472 fn test_rewind_multiple_reuse() {
473 let mut engine = RewindEngine::from_mft(vec![]);
475
476 let records = vec![
477 make_record(50, 2, 5, 5, UsnReason::FILE_CREATE, "first_life", 10),
478 make_record(50, 2, 5, 5, UsnReason::FILE_DELETE, "first_life", 20),
479 make_record(50, 4, 5, 5, UsnReason::FILE_CREATE, "second_life", 30),
480 make_record(50, 4, 5, 5, UsnReason::FILE_DELETE, "second_life", 40),
481 make_record(50, 6, 5, 5, UsnReason::FILE_CREATE, "third_life", 50),
482 ];
483
484 let resolved = engine.rewind(&records);
485 assert_eq!(resolved[0].full_path, ".\\first_life");
486 assert_eq!(resolved[2].full_path, ".\\second_life");
487 assert_eq!(resolved[4].full_path, ".\\third_life");
488 }
489
490 #[test]
491 fn test_rewind_deep_path_reconstruction() {
492 let mut engine = RewindEngine::from_mft(vec![]);
494
495 let records = vec![
496 make_record(10, 1, 5, 5, UsnReason::FILE_CREATE, "A", 10),
497 make_record(20, 1, 10, 1, UsnReason::FILE_CREATE, "B", 20),
498 make_record(30, 1, 20, 1, UsnReason::FILE_CREATE, "C", 30),
499 make_record(40, 1, 30, 1, UsnReason::FILE_CREATE, "D", 40),
500 make_record(50, 1, 40, 1, UsnReason::FILE_CREATE, "file.txt", 50),
501 ];
502
503 let resolved = engine.rewind(&records);
504 assert_eq!(resolved[4].full_path, ".\\A\\B\\C\\D\\file.txt");
505 }
506
507 #[test]
508 fn test_from_mft_seeding() {
509 let engine = RewindEngine::from_mft(vec![
510 (100, 1, "Users".into(), 5, 5),
511 (200, 1, "admin".into(), 100, 1),
512 (300, 1, "Desktop".into(), 200, 1),
513 ]);
514 assert_eq!(engine.lookup_len(), 3);
515 let path = engine.resolve_path(&EntryKey::new(300, 1));
516 assert_eq!(path, ".\\Users\\admin\\Desktop");
517 }
518
519 #[test]
520 fn test_rewind_engine_default() {
521 let engine = RewindEngine::default();
522 assert_eq!(engine.lookup_len(), 0);
523 }
524
525 #[test]
526 fn test_rewind_engine_insert() {
527 let mut engine = RewindEngine::new();
528 engine.insert(
529 EntryKey::new(100, 1),
530 EntryInfo {
531 name: "inserted.txt".to_string(),
532 parent: EntryKey::root(),
533 },
534 );
535 assert_eq!(engine.lookup_len(), 1);
536 let path = engine.resolve_path(&EntryKey::new(100, 1));
537 assert_eq!(path, ".\\inserted.txt");
538 }
539
540 #[test]
541 fn test_resolve_path_circular_reference() {
542 let mut engine = RewindEngine::new();
544 engine.insert(
545 EntryKey::new(100, 1),
546 EntryInfo {
547 name: "A".to_string(),
548 parent: EntryKey::new(200, 1),
549 },
550 );
551 engine.insert(
552 EntryKey::new(200, 1),
553 EntryInfo {
554 name: "B".to_string(),
555 parent: EntryKey::new(100, 1),
556 },
557 );
558
559 let path = engine.resolve_path(&EntryKey::new(100, 1));
560 assert!(path.contains("UNRESOLVED"));
562 }
563
564 #[test]
565 fn test_entry_key_not_root() {
566 let key = EntryKey::new(100, 1);
567 assert!(!key.is_root());
568 }
569
570 #[test]
571 fn test_rewind_empty_records() {
572 let mut engine = RewindEngine::new();
573 let resolved = engine.rewind(&[]);
574 assert!(resolved.is_empty());
575 }
576
577 #[test]
578 fn test_rewind_data_extend_and_truncation() {
579 let mut engine = RewindEngine::from_mft(vec![(50, 1, "data".into(), 5, 5)]);
580
581 let records = vec![
582 make_record(100, 1, 50, 1, UsnReason::FILE_CREATE, "log.txt", 10),
583 make_record(100, 1, 50, 1, UsnReason::DATA_EXTEND, "log.txt", 20),
584 make_record(100, 1, 50, 1, UsnReason::DATA_TRUNCATION, "log.txt", 30),
585 ];
586
587 let resolved = engine.rewind(&records);
588 assert_eq!(resolved.len(), 3);
589 assert_eq!(resolved[0].full_path, ".\\data\\log.txt");
590 assert_eq!(resolved[1].full_path, ".\\data\\log.txt");
591 assert_eq!(resolved[2].full_path, ".\\data\\log.txt");
592 }
593
594 #[test]
595 fn test_resolve_path_hits_depth_limit_linear_chain() {
596 let mut engine = RewindEngine::new();
600 let chain_length = 258;
601 for i in 0..chain_length {
602 let entry_num = 1000 + i as u64;
603 let parent_num = 1001 + i as u64; engine.insert(
605 EntryKey::new(entry_num, 1),
606 EntryInfo {
607 name: format!("dir_{i}"),
608 parent: EntryKey::new(parent_num, 1),
609 },
610 );
611 }
612
613 let path = engine.resolve_path(&EntryKey::new(1000, 1));
615 assert!(path.contains("UNRESOLVED") || path.contains("UNKNOWN"));
616 }
617
618 #[test]
619 fn test_resolve_path_from_hits_depth_limit_in_rewind() {
620 let mut engine = RewindEngine::new();
624 engine.insert(
625 EntryKey::new(100, 1),
626 EntryInfo {
627 name: "A".to_string(),
628 parent: EntryKey::new(200, 1),
629 },
630 );
631 engine.insert(
632 EntryKey::new(200, 1),
633 EntryInfo {
634 name: "B".to_string(),
635 parent: EntryKey::new(300, 1),
636 },
637 );
638 engine.insert(
639 EntryKey::new(300, 1),
640 EntryInfo {
641 name: "C".to_string(),
642 parent: EntryKey::new(100, 1),
643 },
644 );
645
646 let records = vec![make_record(
648 400,
649 1,
650 100,
651 1,
652 UsnReason::FILE_CREATE,
653 "trapped.txt",
654 10,
655 )];
656
657 let resolved = engine.rewind(&records);
658 assert_eq!(resolved.len(), 1);
659 assert!(resolved[0].parent_path.contains("UNRESOLVED"));
661 }
662
663 #[test]
664 fn test_rewind_forward_pass_unseen_entry() {
665 let mut engine = RewindEngine::from_mft(vec![(50, 1, "data".into(), 5, 5)]);
669
670 let records = vec![
671 make_record(100, 1, 50, 1, UsnReason::DATA_EXTEND, "log.txt", 10),
674 ];
675
676 let resolved = engine.rewind(&records);
677 assert_eq!(resolved.len(), 1);
678 assert_eq!(resolved[0].full_path, ".\\data\\log.txt");
679 }
680
681 #[test]
682 fn test_resolve_path_from_unknown_parent_in_forward() {
683 let mut engine = RewindEngine::new();
687
688 let records = vec![
689 make_record(100, 1, 999, 1, UsnReason::FILE_CREATE, "orphan.txt", 10),
691 ];
692
693 let resolved = engine.rewind(&records);
694 assert_eq!(resolved.len(), 1);
695 assert!(resolved[0].parent_path.contains("UNKNOWN(999:1)"));
696 }
697
698 #[test]
699 fn test_seed_from_carved_adds_entries() {
700 use crate::carve::CarvedMftEntry;
701
702 let mut engine = RewindEngine::from_mft(vec![
703 (5, 5, ".".into(), 5, 5), (10, 1, "Users".into(), 5, 5),
705 ]);
706 assert_eq!(engine.lookup_len(), 2);
707
708 let carved = vec![
709 CarvedMftEntry {
710 offset: 0,
711 entry_number: 20,
712 sequence_number: 1,
713 filename: "admin".to_string(),
714 parent_entry: 10,
715 parent_sequence: 1,
716 is_directory: true,
717 is_in_use: false, },
719 CarvedMftEntry {
720 offset: 1024,
721 entry_number: 30,
722 sequence_number: 1,
723 filename: "Desktop".to_string(),
724 parent_entry: 20,
725 parent_sequence: 1,
726 is_directory: true,
727 is_in_use: false,
728 },
729 ];
730
731 engine.seed_from_carved(&carved);
732
733 assert_eq!(engine.lookup_len(), 4);
734 let path = engine.resolve_path(&EntryKey::new(30, 1));
735 assert_eq!(path, ".\\Users\\admin\\Desktop");
736 }
737
738 #[test]
739 fn test_seed_from_carved_does_not_overwrite_allocated() {
740 use crate::carve::CarvedMftEntry;
741
742 let mut engine = RewindEngine::from_mft(vec![(100, 1, "current.txt".into(), 5, 5)]);
744
745 let carved = vec![CarvedMftEntry {
747 offset: 0,
748 entry_number: 100,
749 sequence_number: 1,
750 filename: "old_name.txt".to_string(),
751 parent_entry: 5,
752 parent_sequence: 5,
753 is_directory: false,
754 is_in_use: false,
755 }];
756
757 engine.seed_from_carved(&carved);
758
759 assert_eq!(engine.lookup_len(), 1);
761 let path = engine.resolve_path(&EntryKey::new(100, 1));
762 assert_eq!(path, ".\\current.txt");
763 }
764
765 #[test]
766 fn test_seed_from_carved_adds_historical_sequence() {
767 use crate::carve::CarvedMftEntry;
768
769 let mut engine = RewindEngine::from_mft(vec![(100, 3, "new_file.txt".into(), 5, 5)]);
771
772 let carved = vec![CarvedMftEntry {
774 offset: 0,
775 entry_number: 100,
776 sequence_number: 1,
777 filename: "old_file.txt".to_string(),
778 parent_entry: 5,
779 parent_sequence: 5,
780 is_directory: false,
781 is_in_use: false,
782 }];
783
784 engine.seed_from_carved(&carved);
785
786 assert_eq!(engine.lookup_len(), 2);
788 assert_eq!(
789 engine.resolve_path(&EntryKey::new(100, 3)),
790 ".\\new_file.txt"
791 );
792 assert_eq!(
793 engine.resolve_path(&EntryKey::new(100, 1)),
794 ".\\old_file.txt"
795 );
796 }
797
798 #[test]
799 fn test_resolve_path_from_standalone() {
800 let mut engine = RewindEngine::new();
802 let records = vec![make_record(
803 10,
804 1,
805 5,
806 5,
807 UsnReason::FILE_CREATE,
808 "root_file.txt",
809 10,
810 )];
811
812 let resolved = engine.rewind(&records);
813 assert_eq!(resolved.len(), 1);
814 assert_eq!(resolved[0].parent_path, ".");
815 assert_eq!(resolved[0].full_path, ".\\root_file.txt");
816 }
817
818 #[test]
821 fn test_carved_records_resolve_paths_via_carved_mft() {
822 use crate::carve::CarvedMftEntry;
823 use crate::usn::CarvedRecord;
824
825 let mut engine = RewindEngine::from_mft(vec![]);
830
831 let carved_mft = vec![
832 CarvedMftEntry {
833 offset: 0,
834 entry_number: 10,
835 sequence_number: 1,
836 filename: "Users".to_string(),
837 parent_entry: 5,
838 parent_sequence: 5,
839 is_directory: true,
840 is_in_use: false,
841 },
842 CarvedMftEntry {
843 offset: 1024,
844 entry_number: 20,
845 sequence_number: 1,
846 filename: "admin".to_string(),
847 parent_entry: 10,
848 parent_sequence: 1,
849 is_directory: true,
850 is_in_use: false,
851 },
852 CarvedMftEntry {
853 offset: 2048,
854 entry_number: 30,
855 sequence_number: 1,
856 filename: "Temp".to_string(),
857 parent_entry: 20,
858 parent_sequence: 1,
859 is_directory: true,
860 is_in_use: false,
861 },
862 ];
863
864 engine.seed_from_carved(&carved_mft);
865
866 let carved_usn = vec![CarvedRecord {
868 offset: 50000,
869 record: make_record(500, 1, 30, 1, UsnReason::FILE_CREATE, "malware.exe", 99999),
870 }];
871
872 let mut all_records: Vec<UsnRecord> = Vec::new();
874 all_records.extend(carved_usn.into_iter().map(|c| c.record));
875
876 let resolved = engine.rewind(&all_records);
877 assert_eq!(resolved.len(), 1);
878 assert_eq!(resolved[0].full_path, ".\\Users\\admin\\Temp\\malware.exe");
879 }
880
881 #[test]
882 fn test_carved_and_allocated_records_merge_in_pipeline() {
883 use crate::carve::CarvedMftEntry;
884 use crate::usn::CarvedRecord;
885
886 let mut engine = RewindEngine::from_mft(vec![
888 (10, 1, "Windows".into(), 5, 5),
889 (20, 1, "System32".into(), 10, 1),
890 ]);
891
892 let carved_mft = vec![CarvedMftEntry {
894 offset: 0,
895 entry_number: 50,
896 sequence_number: 2,
897 filename: "HackTools".to_string(),
898 parent_entry: 5,
899 parent_sequence: 5,
900 is_directory: true,
901 is_in_use: false,
902 }];
903 engine.seed_from_carved(&carved_mft);
904
905 let allocated = vec![make_record(
907 100,
908 1,
909 20,
910 1,
911 UsnReason::FILE_CREATE,
912 "cmd.exe",
913 1000,
914 )];
915
916 let carved_usn = vec![CarvedRecord {
918 offset: 80000,
919 record: make_record(
920 200,
921 1,
922 50,
923 2,
924 UsnReason::FILE_CREATE,
925 "mimikatz.exe",
926 500, ),
928 }];
929
930 let mut all_records = allocated;
932 all_records.extend(carved_usn.into_iter().map(|c| c.record));
933 all_records.sort_by_key(|r| r.usn);
934
935 let resolved = engine.rewind(&all_records);
936 assert_eq!(resolved.len(), 2);
937
938 assert_eq!(resolved[0].full_path, ".\\HackTools\\mimikatz.exe");
940 assert_eq!(resolved[1].full_path, ".\\Windows\\System32\\cmd.exe");
941 }
942}