ntfs_reader/
journal.rs

1// Copyright (c) 2022, Matteo Bernacchia <dev@kikijiki.com>. All rights reserved.
2// This project is dual licensed under the Apache License 2.0 and the MIT license.
3// See the LICENSE files in the project root for details.
4
5use std::collections::VecDeque;
6use std::ffi::{CString, OsString};
7use std::mem::size_of;
8use std::os::raw::c_void;
9use std::os::windows::ffi::OsStringExt;
10use std::path::{Path, PathBuf};
11
12use windows::core::PCSTR;
13use windows::Win32::Foundation::{self, ERROR_MORE_DATA};
14use windows::Win32::Storage::FileSystem::{self, FILE_FLAG_BACKUP_SEMANTICS};
15use windows::Win32::System::Ioctl;
16use windows::Win32::System::Threading::INFINITE;
17use windows::Win32::System::IO::{self, GetQueuedCompletionStatus};
18
19use crate::{api::FileId, volume::Volume};
20
21#[repr(align(64))]
22#[derive(Debug, Clone, Copy)]
23struct AlignedBuffer<const N: usize>([u8; N]);
24
25fn get_usn_record_time(timestamp: i64) -> std::time::Duration {
26    std::time::Duration::from_nanos(timestamp as u64 * 100u64)
27}
28
29fn get_usn_record_name(file_name_length: u16, file_name: *const u16) -> String {
30    let size = (file_name_length / 2) as usize;
31
32    if size > 0 {
33        unsafe {
34            let name_u16 = std::slice::from_raw_parts(file_name, size);
35            let name = std::ffi::OsString::from_wide(name_u16)
36                .to_string_lossy()
37                .into_owned();
38            return name;
39        }
40    }
41
42    String::new()
43}
44
45fn get_file_path(volume_handle: Foundation::HANDLE, file_id: FileId) -> Option<PathBuf> {
46    let (id, id_type) = match file_id {
47        FileId::Normal(id) => (
48            FileSystem::FILE_ID_DESCRIPTOR_0 { FileId: id as i64 },
49            FileSystem::FileIdType,
50        ),
51        FileId::Extended(id) => (
52            FileSystem::FILE_ID_DESCRIPTOR_0 { ExtendedFileId: id },
53            FileSystem::ExtendedFileIdType,
54        ),
55    };
56
57    let file_id_desc = FileSystem::FILE_ID_DESCRIPTOR {
58        Type: id_type,
59        dwSize: size_of::<FileSystem::FILE_ID_DESCRIPTOR>() as u32,
60        Anonymous: id,
61    };
62
63    unsafe {
64        let file_handle = FileSystem::OpenFileById(
65            volume_handle,
66            &file_id_desc,
67            0,
68            FileSystem::FILE_SHARE_READ
69                | FileSystem::FILE_SHARE_WRITE
70                | FileSystem::FILE_SHARE_DELETE,
71            None,
72            FILE_FLAG_BACKUP_SEMANTICS,
73        )
74        .unwrap_or(Foundation::INVALID_HANDLE_VALUE);
75
76        if file_handle.is_invalid() {
77            return None;
78        }
79
80        let mut info_buffer_size = size_of::<FileSystem::FILE_NAME_INFO>()
81            + (Foundation::MAX_PATH as usize) * size_of::<u16>();
82        let mut info_buffer = vec![0u8; info_buffer_size];
83
84        let result = loop {
85            let info_result = FileSystem::GetFileInformationByHandleEx(
86                file_handle,
87                FileSystem::FileNameInfo,
88                info_buffer.as_mut_ptr() as *mut _,
89                info_buffer_size as u32,
90            );
91
92            match info_result {
93                Ok(_) => {
94                    let (_, body, _) = info_buffer.align_to::<FileSystem::FILE_NAME_INFO>();
95                    let info = &body[0];
96                    let name_len = info.FileNameLength as usize / size_of::<u16>();
97                    let name_u16 = std::slice::from_raw_parts(info.FileName.as_ptr(), name_len);
98                    break Some(PathBuf::from(OsString::from_wide(name_u16)));
99                }
100                Err(err) => {
101                    if err.code() == ERROR_MORE_DATA.to_hresult() {
102                        // The buffer was too small, resize it and try again.
103                        let required_size = info_buffer.align_to::<FileSystem::FILE_NAME_INFO>().1
104                            [0]
105                        .FileNameLength as usize;
106
107                        info_buffer_size = size_of::<FileSystem::FILE_NAME_INFO>() + required_size;
108                        info_buffer.resize(info_buffer_size, 0);
109                    } else {
110                        break None;
111                    }
112                }
113            }
114        };
115
116        let _ = Foundation::CloseHandle(file_handle);
117        result
118    }
119}
120
121fn get_usn_record_path(
122    volume_path: &Path,
123    volume_handle: Foundation::HANDLE,
124    file_name: String,
125    file_id: FileId,
126    parent_id: FileId,
127) -> PathBuf {
128    // First try to get the full path from the parent.
129    // We do this because if the file was moved, computing the path from the file id
130    // could return the wrong path.
131    if let Some(parent_path) = get_file_path(volume_handle, parent_id) {
132        return volume_path.join(parent_path.join(&file_name));
133    } else {
134        // If we can't get the parent path, try to get the path from the file id.
135        // This can happen if the parent was deleted.
136        if let Some(path) = get_file_path(volume_handle, file_id) {
137            return volume_path.join(path);
138        }
139    }
140
141    //warn!("Could not get path: {}", file_name);
142    PathBuf::from(&file_name)
143}
144
145#[derive(Debug, Clone)]
146pub struct UsnRecord {
147    pub usn: i64,
148    pub timestamp: std::time::Duration,
149    pub file_id: FileId,
150    pub parent_id: FileId,
151    pub reason: u32,
152    pub path: PathBuf,
153}
154
155impl UsnRecord {
156    fn from_v2(journal: &Journal, rec: &Ioctl::USN_RECORD_V2) -> Self {
157        let usn = rec.Usn;
158        let timestamp = get_usn_record_time(rec.TimeStamp);
159        let file_id = FileId::Normal(rec.FileReferenceNumber);
160        let parent_id = FileId::Normal(rec.ParentFileReferenceNumber);
161        let reason = rec.Reason;
162        let name = get_usn_record_name(rec.FileNameLength, rec.FileName.as_ptr());
163        let path = get_usn_record_path(
164            &journal.volume.path,
165            journal.volume_handle,
166            name,
167            file_id,
168            parent_id,
169        );
170
171        UsnRecord {
172            usn,
173            timestamp,
174            file_id,
175            parent_id,
176            reason,
177            path,
178        }
179    }
180
181    fn from_v3(journal: &Journal, rec: &Ioctl::USN_RECORD_V3) -> Self {
182        let usn = rec.Usn;
183        let timestamp = get_usn_record_time(rec.TimeStamp);
184        let file_id = FileId::Extended(rec.FileReferenceNumber);
185        let parent_id = FileId::Extended(rec.ParentFileReferenceNumber);
186        let reason = rec.Reason;
187
188        let name = get_usn_record_name(rec.FileNameLength, rec.FileName.as_ptr());
189        let path = get_usn_record_path(
190            &journal.volume.path,
191            journal.volume_handle,
192            name,
193            file_id,
194            parent_id,
195        );
196
197        UsnRecord {
198            usn,
199            timestamp,
200            file_id,
201            parent_id,
202            reason,
203            path,
204        }
205    }
206}
207
208#[derive(Debug, Clone)]
209pub enum NextUsn {
210    First,
211    Next,
212    Custom(i64),
213}
214
215#[derive(Debug, Clone)]
216pub enum HistorySize {
217    Unlimited,
218    Limited(usize),
219}
220
221#[derive(Debug, Clone)]
222pub struct JournalOptions {
223    pub reason_mask: u32,
224    pub next_usn: NextUsn,
225    pub max_history_size: HistorySize,
226    pub version_range: (u16, u16),
227}
228
229impl Default for JournalOptions {
230    fn default() -> Self {
231        JournalOptions {
232            reason_mask: 0xFFFFFFFF,
233            next_usn: NextUsn::Next,
234            max_history_size: HistorySize::Unlimited,
235            version_range: (2, 3),
236        }
237    }
238}
239
240pub struct Journal {
241    volume: Volume,
242    volume_handle: Foundation::HANDLE,
243    port: Foundation::HANDLE,
244    journal: Ioctl::USN_JOURNAL_DATA_V2,
245    next_usn: i64,
246    reason_mask: u32, // Ioctl::USN_REASON_FILE_CREATE
247    history: VecDeque<UsnRecord>,
248    max_history_size: usize,
249    version_range: (u16, u16),
250}
251
252impl Journal {
253    pub fn new(volume: Volume, options: JournalOptions) -> Result<Journal, std::io::Error> {
254        let volume_handle: Foundation::HANDLE;
255
256        unsafe {
257            // Needs to be null terminated.
258            let path = CString::new(volume.path.to_str().unwrap()).unwrap();
259
260            volume_handle = FileSystem::CreateFileA(
261                PCSTR::from_raw(path.as_bytes_with_nul().as_ptr()),
262                (FileSystem::FILE_GENERIC_READ | FileSystem::FILE_GENERIC_WRITE).0,
263                FileSystem::FILE_SHARE_READ
264                    | FileSystem::FILE_SHARE_WRITE
265                    | FileSystem::FILE_SHARE_DELETE,
266                None,
267                FileSystem::OPEN_EXISTING,
268                FileSystem::FILE_FLAG_OVERLAPPED,
269                None,
270            )?;
271        }
272
273        let mut journal = Ioctl::USN_JOURNAL_DATA_V2::default();
274
275        unsafe {
276            let mut ioctl_bytes_returned = 0;
277            IO::DeviceIoControl(
278                volume_handle,
279                Ioctl::FSCTL_QUERY_USN_JOURNAL,
280                None,
281                0,
282                Some(&mut journal as *mut _ as *mut c_void),
283                size_of::<Ioctl::USN_JOURNAL_DATA_V2>() as u32,
284                Some(&mut ioctl_bytes_returned),
285                None,
286            )?;
287        }
288
289        let next_usn = match options.next_usn {
290            NextUsn::First => 0,
291            NextUsn::Next => journal.NextUsn,
292            NextUsn::Custom(usn) => usn,
293        };
294
295        let max_history_size = match options.max_history_size {
296            HistorySize::Unlimited => 0,
297            HistorySize::Limited(size) => size,
298        };
299
300        let port = unsafe { IO::CreateIoCompletionPort(volume_handle, None, 0, 1)? };
301
302        Ok(Journal {
303            volume,
304            volume_handle,
305            port,
306            journal,
307            next_usn,
308            reason_mask: options.reason_mask,
309            history: VecDeque::new(),
310            max_history_size,
311            version_range: options.version_range,
312        })
313    }
314
315    pub fn read(&mut self) -> Result<Vec<UsnRecord>, std::io::Error> {
316        self.read_sized::<4096>()
317    }
318
319    pub fn read_sized<const BUFFER_SIZE: usize>(
320        &mut self,
321    ) -> Result<Vec<UsnRecord>, std::io::Error> {
322        let mut results = Vec::<UsnRecord>::new();
323
324        let mut read = Ioctl::READ_USN_JOURNAL_DATA_V1 {
325            StartUsn: self.next_usn,
326            ReasonMask: self.reason_mask,
327            ReturnOnlyOnClose: 0,
328            Timeout: 0,
329            BytesToWaitFor: 0,
330            UsnJournalID: self.journal.UsnJournalID,
331            MinMajorVersion: u16::max(self.version_range.0, self.journal.MinSupportedMajorVersion),
332            MaxMajorVersion: u16::min(self.version_range.1, self.journal.MaxSupportedMajorVersion),
333        };
334
335        let mut buffer = AlignedBuffer::<BUFFER_SIZE>([0u8; BUFFER_SIZE]);
336
337        let mut bytes_returned = 0;
338        let mut overlapped = IO::OVERLAPPED {
339            ..Default::default()
340        };
341
342        unsafe {
343            IO::DeviceIoControl(
344                self.volume_handle,
345                Ioctl::FSCTL_READ_USN_JOURNAL,
346                Some(&mut read as *mut _ as *mut c_void),
347                size_of::<Ioctl::READ_USN_JOURNAL_DATA_V1>() as u32,
348                Some(&mut buffer as *mut _ as *mut c_void),
349                BUFFER_SIZE as u32,
350                Some(&mut bytes_returned),
351                Some(&mut overlapped),
352            )?;
353
354            // NOTE: Switched to overlapped IO while investigating a bug,
355            // but it's not needed (we just wait immediately anyway).
356
357            // Wait for the operation to complete.
358            let mut key = 0usize;
359            let mut overlapped = std::ptr::null_mut();
360            GetQueuedCompletionStatus(
361                self.port,
362                &mut bytes_returned,
363                &mut key,
364                &mut overlapped,
365                INFINITE,
366            )?;
367        }
368
369        let next_usn = i64::from_le_bytes(buffer.0[0..8].try_into().unwrap());
370        if next_usn == 0 || next_usn < self.next_usn {
371            return Ok(results);
372        } else {
373            self.next_usn = next_usn;
374        }
375
376        let mut offset = 8; // sizeof(USN)
377        while offset < bytes_returned {
378            let (record_len, record) = unsafe {
379                let record_ptr = std::mem::transmute::<*const u8, *const Ioctl::USN_RECORD_UNION>(
380                    buffer.0[offset as usize..].as_ptr(),
381                );
382
383                let record_len = (*record_ptr).Header.RecordLength;
384                if record_len == 0 {
385                    break;
386                }
387
388                let record = match (*record_ptr).Header.MajorVersion {
389                    2 => Some(UsnRecord::from_v2(self, &(*record_ptr).V2)),
390                    3 => Some(UsnRecord::from_v3(self, &(*record_ptr).V3)),
391                    _ => None,
392                };
393
394                (record_len, record)
395            };
396
397            if let Some(record) = record {
398                if record.reason
399                    & (Ioctl::USN_REASON_RENAME_OLD_NAME
400                        | Ioctl::USN_REASON_HARD_LINK_CHANGE
401                        | Ioctl::USN_REASON_REPARSE_POINT_CHANGE)
402                    != 0
403                {
404                    if self.max_history_size > 0 && self.history.len() >= self.max_history_size {
405                        self.history.pop_front();
406                    }
407                    self.history.push_back(record.clone());
408                }
409
410                results.push(record);
411            }
412
413            offset += record_len;
414        }
415
416        Ok(results)
417    }
418
419    pub fn match_rename(&self, record: &UsnRecord) -> Option<PathBuf> {
420        if record.reason & Ioctl::USN_REASON_RENAME_NEW_NAME == 0 {
421            return None;
422        }
423
424        self.history
425            .iter()
426            .find(|r| r.file_id == record.file_id && r.usn < record.usn)
427            .map(|r| r.path.clone())
428    }
429
430    pub fn trim_history(&mut self, min_usn: Option<i64>) {
431        match min_usn {
432            Some(usn) => self.history.retain(|r| r.usn > usn),
433            None => self.history.clear(),
434        }
435    }
436
437    pub fn get_next_usn(&self) -> i64 {
438        self.next_usn
439    }
440
441    pub fn get_reason_str(reason: u32) -> String {
442        let mut reason_str = String::new();
443
444        if reason & Ioctl::USN_REASON_BASIC_INFO_CHANGE != 0 {
445            reason_str.push_str("USN_REASON_BASIC_INFO_CHANGE ");
446        }
447        if reason & Ioctl::USN_REASON_CLOSE != 0 {
448            reason_str.push_str("USN_REASON_CLOSE ");
449        }
450        if reason & Ioctl::USN_REASON_COMPRESSION_CHANGE != 0 {
451            reason_str.push_str("USN_REASON_COMPRESSION_CHANGE ");
452        }
453        if reason & Ioctl::USN_REASON_DATA_EXTEND != 0 {
454            reason_str.push_str("USN_REASON_DATA_EXTEND ");
455        }
456        if reason & Ioctl::USN_REASON_DATA_OVERWRITE != 0 {
457            reason_str.push_str("USN_REASON_DATA_OVERWRITE ");
458        }
459        if reason & Ioctl::USN_REASON_DATA_TRUNCATION != 0 {
460            reason_str.push_str("USN_REASON_DATA_TRUNCATION ");
461        }
462        if reason & Ioctl::USN_REASON_DESIRED_STORAGE_CLASS_CHANGE != 0 {
463            reason_str.push_str("USN_REASON_DESIRED_STORAGE_CLASS_CHANGE ");
464        }
465        if reason & Ioctl::USN_REASON_EA_CHANGE != 0 {
466            reason_str.push_str("USN_REASON_EA_CHANGE ");
467        }
468        if reason & Ioctl::USN_REASON_ENCRYPTION_CHANGE != 0 {
469            reason_str.push_str("USN_REASON_ENCRYPTION_CHANGE ");
470        }
471        if reason & Ioctl::USN_REASON_FILE_CREATE != 0 {
472            reason_str.push_str("USN_REASON_FILE_CREATE ");
473        }
474        if reason & Ioctl::USN_REASON_FILE_DELETE != 0 {
475            reason_str.push_str("USN_REASON_FILE_DELETE ");
476        }
477        if reason & Ioctl::USN_REASON_HARD_LINK_CHANGE != 0 {
478            reason_str.push_str("USN_REASON_HARD_LINK_CHANGE ");
479        }
480        if reason & Ioctl::USN_REASON_INDEXABLE_CHANGE != 0 {
481            reason_str.push_str("USN_REASON_INDEXABLE_CHANGE ");
482        }
483        if reason & Ioctl::USN_REASON_INTEGRITY_CHANGE != 0 {
484            reason_str.push_str("USN_REASON_INTEGRITY_CHANGE ");
485        }
486        if reason & Ioctl::USN_REASON_NAMED_DATA_EXTEND != 0 {
487            reason_str.push_str("USN_REASON_NAMED_DATA_EXTEND ");
488        }
489        if reason & Ioctl::USN_REASON_NAMED_DATA_OVERWRITE != 0 {
490            reason_str.push_str("USN_REASON_NAMED_DATA_OVERWRITE ");
491        }
492        if reason & Ioctl::USN_REASON_NAMED_DATA_TRUNCATION != 0 {
493            reason_str.push_str("USN_REASON_NAMED_DATA_TRUNCATION ");
494        }
495        if reason & Ioctl::USN_REASON_OBJECT_ID_CHANGE != 0 {
496            reason_str.push_str("USN_REASON_OBJECT_ID_CHANGE ");
497        }
498        if reason & Ioctl::USN_REASON_RENAME_NEW_NAME != 0 {
499            reason_str.push_str("USN_REASON_RENAME_NEW_NAME ");
500        }
501        if reason & Ioctl::USN_REASON_RENAME_OLD_NAME != 0 {
502            reason_str.push_str("USN_REASON_RENAME_OLD_NAME ");
503        }
504        if reason & Ioctl::USN_REASON_REPARSE_POINT_CHANGE != 0 {
505            reason_str.push_str("USN_REASON_REPARSE_POINT_CHANGE ");
506        }
507        if reason & Ioctl::USN_REASON_SECURITY_CHANGE != 0 {
508            reason_str.push_str("USN_REASON_SECURITY_CHANGE ");
509        }
510        if reason & Ioctl::USN_REASON_STREAM_CHANGE != 0 {
511            reason_str.push_str("USN_REASON_STREAM_CHANGE ");
512        }
513        if reason & Ioctl::USN_REASON_TRANSACTED_CHANGE != 0 {
514            reason_str.push_str("USN_REASON_TRANSACTED_CHANGE ");
515        }
516
517        reason_str
518    }
519}
520
521impl Drop for Journal {
522    fn drop(&mut self) {
523        unsafe {
524            let _ = Foundation::CloseHandle(self.volume_handle);
525            let _ = Foundation::CloseHandle(self.port);
526        }
527    }
528}
529
530#[cfg(test)]
531mod test {
532    use core::panic;
533    use std::fs::File;
534    use std::io::Write;
535
536    use tracing_subscriber::FmtSubscriber;
537
538    use crate::errors::NtfsReaderResult;
539
540    use super::*;
541    use crate::test_utils::TEST_VOLUME_LETTER;
542
543    fn init_tracing() {
544        let subscriber = FmtSubscriber::builder()
545            .with_max_level(tracing::Level::TRACE)
546            .without_time()
547            .finish();
548        let _ = tracing::subscriber::set_global_default(subscriber);
549    }
550
551    fn make_journal(version: u16, reason_mask: u32) -> NtfsReaderResult<Journal> {
552        let volume = Volume::new(format!("\\\\?\\{}:", TEST_VOLUME_LETTER))?;
553        let options = JournalOptions {
554            version_range: (version, version),
555            reason_mask,
556            ..JournalOptions::default()
557        };
558        Ok(Journal::new(volume, options)?)
559    }
560
561    fn make_test_dir(name: &str, version: u16) -> NtfsReaderResult<PathBuf> {
562        let name = format!("{}-v{}", name, version);
563        let dir = PathBuf::from(format!("\\\\?\\{}:\\{}", TEST_VOLUME_LETTER, name));
564        let _ = std::fs::remove_dir_all(&dir);
565        std::fs::create_dir_all(&dir)?;
566        Ok(dir)
567    }
568
569    fn test_file_create(journal_version: u16) -> NtfsReaderResult<()> {
570        init_tracing();
571
572        let mut journal = make_journal(journal_version, Ioctl::USN_REASON_FILE_CREATE)?;
573        while !journal.read()?.is_empty() {}
574
575        /////////////////////////////////////////////////////////////////
576        // PREPARE DATA
577
578        let mut files = Vec::new();
579        let mut found = Vec::new();
580
581        let dir = make_test_dir("usn-journal-test-create", journal_version)?;
582
583        for x in 0..10 {
584            let path = dir.join(format!("usn-journal-test-create-{}.txt", x));
585            File::create(&path)?.write_all(b"test")?;
586            files.push(path);
587        }
588
589        /////////////////////////////////////////////////////////////////
590        // TEST JOURNAL
591
592        // Retry a few times in case there is a lot of unrelated activity.
593        for _ in 0..10 {
594            for result in journal.read()? {
595                found.push(result.path);
596            }
597
598            if files.iter().all(|f| found.contains(f)) {
599                return Ok(());
600            }
601        }
602
603        panic!("The file creation was not detected");
604    }
605
606    fn test_file_move(journal_version: u16) -> NtfsReaderResult<()> {
607        init_tracing();
608
609        /////////////////////////////////////////////////////////////////
610        // PREPARE DATA
611
612        let dir = make_test_dir("usn-journal-test-move", journal_version)?;
613
614        let path_old = dir.join("usn-journal-test-move.old");
615        let path_new = path_old.with_extension("new");
616
617        let _ = std::fs::remove_file(path_new.as_path());
618        let _ = std::fs::remove_file(path_old.as_path());
619
620        File::create(path_old.as_path())?.write_all(b"test")?;
621
622        /////////////////////////////////////////////////////////////////
623        // TEST JOURNAL
624
625        let mut journal = make_journal(
626            journal_version,
627            Ioctl::USN_REASON_RENAME_OLD_NAME | Ioctl::USN_REASON_RENAME_NEW_NAME,
628        )?;
629        while !journal.read()?.is_empty() {}
630
631        std::fs::rename(path_old.as_path(), path_new.as_path())?;
632
633        // Retry a few times in case there is a lot of unrelated activity.
634        for _ in 0..10 {
635            for result in journal.read()? {
636                if (result.path == path_new)
637                    && (result.reason & Ioctl::USN_REASON_RENAME_NEW_NAME != 0)
638                {
639                    if let Some(path) = journal.match_rename(&result) {
640                        assert_eq!(path, path_old);
641                        return Ok(());
642                    } else {
643                        panic!("No old path found for {}", result.path.to_str().unwrap());
644                    }
645                }
646            }
647        }
648
649        panic!("The file move was not detected");
650    }
651
652    fn test_file_delete(journal_version: u16) -> NtfsReaderResult<()> {
653        init_tracing();
654
655        /////////////////////////////////////////////////////////////////
656        // PREPARE DATA
657
658        let dir = make_test_dir("usn-journal-test-delete", journal_version)?;
659        let file_path = dir.join("usn-journal-test-delete.txt");
660        File::create(&file_path)?.write_all(b"test")?;
661
662        /////////////////////////////////////////////////////////////////
663        // TEST JOURNAL
664
665        let mut journal = make_journal(journal_version, Ioctl::USN_REASON_FILE_DELETE)?;
666        while !journal.read()?.is_empty() {}
667
668        // This will not work well for the files inside because the directory
669        // will be gone by the time the journal is processed.
670        //std::fs::remove_dir_all(&dir)?;
671
672        std::fs::remove_file(&file_path)?;
673
674        // Retry a few times in case there is a lot of unrelated activity.
675        for _ in 0..10 {
676            for result in journal.read()? {
677                if result.path == file_path {
678                    return Ok(());
679                }
680            }
681        }
682
683        panic!("The file deletion was not detected");
684    }
685
686    #[test]
687    fn file_create_v2() -> NtfsReaderResult<()> {
688        test_file_create(2)
689    }
690
691    #[test]
692    fn file_create_v3() -> NtfsReaderResult<()> {
693        test_file_create(3)
694    }
695
696    #[test]
697    fn file_move_v2() -> NtfsReaderResult<()> {
698        test_file_move(2)
699    }
700
701    #[test]
702    fn file_move_v3() -> NtfsReaderResult<()> {
703        test_file_move(3)
704    }
705
706    #[test]
707    fn file_delete_v2() -> NtfsReaderResult<()> {
708        test_file_delete(2)
709    }
710
711    #[test]
712    fn file_delete_v3() -> NtfsReaderResult<()> {
713        test_file_delete(3)
714    }
715}