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_IO_PENDING, 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    if timestamp <= 0 {
27        return std::time::Duration::from_nanos(0);
28    }
29
30    let nanos = (timestamp as i128).saturating_mul(100);
31    let capped = nanos.min(u64::MAX as i128);
32    std::time::Duration::from_nanos(capped as u64)
33}
34
35fn get_usn_record_name(file_name_length: u16, file_name: *const u16) -> String {
36    let size = (file_name_length / 2) as usize;
37
38    if size > 0 {
39        unsafe {
40            let name_u16 = std::slice::from_raw_parts(file_name, size);
41            let name = std::ffi::OsString::from_wide(name_u16)
42                .to_string_lossy()
43                .into_owned();
44            return name;
45        }
46    }
47
48    String::new()
49}
50
51fn get_file_path(volume_handle: Foundation::HANDLE, file_id: FileId) -> Option<PathBuf> {
52    let (id, id_type) = match file_id {
53        FileId::Normal(id) => (
54            FileSystem::FILE_ID_DESCRIPTOR_0 { FileId: id as i64 },
55            FileSystem::FileIdType,
56        ),
57        FileId::Extended(id) => (
58            FileSystem::FILE_ID_DESCRIPTOR_0 { ExtendedFileId: id },
59            FileSystem::ExtendedFileIdType,
60        ),
61    };
62
63    let file_id_desc = FileSystem::FILE_ID_DESCRIPTOR {
64        Type: id_type,
65        dwSize: size_of::<FileSystem::FILE_ID_DESCRIPTOR>() as u32,
66        Anonymous: id,
67    };
68
69    unsafe {
70        let file_handle = FileSystem::OpenFileById(
71            volume_handle,
72            &file_id_desc,
73            0,
74            FileSystem::FILE_SHARE_READ
75                | FileSystem::FILE_SHARE_WRITE
76                | FileSystem::FILE_SHARE_DELETE,
77            None,
78            FILE_FLAG_BACKUP_SEMANTICS,
79        )
80        .unwrap_or(Foundation::INVALID_HANDLE_VALUE);
81
82        if file_handle.is_invalid() {
83            return None;
84        }
85
86        let mut info_buffer_size = size_of::<FileSystem::FILE_NAME_INFO>()
87            + (Foundation::MAX_PATH as usize) * size_of::<u16>();
88        let mut info_buffer = vec![0u8; info_buffer_size];
89
90        let result = loop {
91            let info_result = FileSystem::GetFileInformationByHandleEx(
92                file_handle,
93                FileSystem::FileNameInfo,
94                info_buffer.as_mut_ptr() as *mut _,
95                info_buffer_size as u32,
96            );
97
98            match info_result {
99                Ok(_) => {
100                    let (_, body, _) = info_buffer.align_to::<FileSystem::FILE_NAME_INFO>();
101                    let info = &body[0];
102                    let name_len = info.FileNameLength as usize / size_of::<u16>();
103                    let name_u16 = std::slice::from_raw_parts(info.FileName.as_ptr(), name_len);
104                    break Some(PathBuf::from(OsString::from_wide(name_u16)));
105                }
106                Err(err) => {
107                    if err.code() == ERROR_MORE_DATA.to_hresult() {
108                        // The buffer was too small, resize it and try again.
109                        let required_size = info_buffer.align_to::<FileSystem::FILE_NAME_INFO>().1
110                            [0]
111                        .FileNameLength as usize;
112
113                        info_buffer_size = size_of::<FileSystem::FILE_NAME_INFO>() + required_size;
114                        info_buffer.resize(info_buffer_size, 0);
115                    } else {
116                        break None;
117                    }
118                }
119            }
120        };
121
122        let _ = Foundation::CloseHandle(file_handle);
123        result
124    }
125}
126
127fn get_usn_record_path(
128    volume_path: &Path,
129    volume_handle: Foundation::HANDLE,
130    file_name: String,
131    file_id: FileId,
132    parent_id: FileId,
133) -> PathBuf {
134    // First try to get the full path from the parent.
135    // We do this because if the file was moved, computing the path from the file id
136    // could return the wrong path.
137    if let Some(parent_path) = get_file_path(volume_handle, parent_id) {
138        return volume_path.join(parent_path.join(&file_name));
139    } else {
140        // If we can't get the parent path, try to get the path from the file id.
141        // This can happen if the parent was deleted.
142        if let Some(path) = get_file_path(volume_handle, file_id) {
143            return volume_path.join(path);
144        }
145    }
146
147    //warn!("Could not get path: {}", file_name);
148    PathBuf::from(&file_name)
149}
150
151#[derive(Debug, Clone)]
152pub struct UsnRecord {
153    pub usn: i64,
154    pub timestamp: std::time::Duration,
155    pub file_id: FileId,
156    pub parent_id: FileId,
157    pub reason: u32,
158    pub path: PathBuf,
159}
160
161impl UsnRecord {
162    fn from_v2(journal: &Journal, rec: &Ioctl::USN_RECORD_V2) -> Self {
163        let usn = rec.Usn;
164        let timestamp = get_usn_record_time(rec.TimeStamp);
165        let file_id = FileId::Normal(rec.FileReferenceNumber);
166        let parent_id = FileId::Normal(rec.ParentFileReferenceNumber);
167        let reason = rec.Reason;
168        let name = get_usn_record_name(rec.FileNameLength, rec.FileName.as_ptr());
169        let path = get_usn_record_path(
170            &journal.volume.path,
171            journal.volume_handle,
172            name,
173            file_id,
174            parent_id,
175        );
176
177        UsnRecord {
178            usn,
179            timestamp,
180            file_id,
181            parent_id,
182            reason,
183            path,
184        }
185    }
186
187    fn from_v3(journal: &Journal, rec: &Ioctl::USN_RECORD_V3) -> Self {
188        let usn = rec.Usn;
189        let timestamp = get_usn_record_time(rec.TimeStamp);
190        let file_id = FileId::Extended(rec.FileReferenceNumber);
191        let parent_id = FileId::Extended(rec.ParentFileReferenceNumber);
192        let reason = rec.Reason;
193
194        let name = get_usn_record_name(rec.FileNameLength, rec.FileName.as_ptr());
195        let path = get_usn_record_path(
196            &journal.volume.path,
197            journal.volume_handle,
198            name,
199            file_id,
200            parent_id,
201        );
202
203        UsnRecord {
204            usn,
205            timestamp,
206            file_id,
207            parent_id,
208            reason,
209            path,
210        }
211    }
212}
213
214#[derive(Debug, Clone)]
215pub enum NextUsn {
216    First,
217    Next,
218    Custom(i64),
219}
220
221#[derive(Debug, Clone)]
222pub enum HistorySize {
223    Unlimited,
224    Limited(usize),
225}
226
227#[derive(Debug, Clone)]
228pub struct JournalOptions {
229    pub reason_mask: u32,
230    pub next_usn: NextUsn,
231    pub max_history_size: HistorySize,
232}
233
234impl Default for JournalOptions {
235    fn default() -> Self {
236        JournalOptions {
237            reason_mask: 0xFFFFFFFF,
238            next_usn: NextUsn::Next,
239            max_history_size: HistorySize::Unlimited,
240        }
241    }
242}
243
244pub struct Journal {
245    volume: Volume,
246    volume_handle: Foundation::HANDLE,
247    port: Foundation::HANDLE,
248    journal: Ioctl::USN_JOURNAL_DATA_V2,
249    next_usn: i64,
250    reason_mask: u32, // Ioctl::USN_REASON_FILE_CREATE
251    history: VecDeque<UsnRecord>,
252    max_history_size: usize,
253}
254
255impl Journal {
256    pub fn new(volume: Volume, options: JournalOptions) -> Result<Journal, std::io::Error> {
257        let volume_handle: Foundation::HANDLE;
258
259        unsafe {
260            // Needs to be null terminated.
261            let path = CString::new(volume.path.to_str().unwrap()).unwrap();
262
263            volume_handle = FileSystem::CreateFileA(
264                PCSTR::from_raw(path.as_bytes_with_nul().as_ptr()),
265                (FileSystem::FILE_GENERIC_READ | FileSystem::FILE_GENERIC_WRITE).0,
266                FileSystem::FILE_SHARE_READ
267                    | FileSystem::FILE_SHARE_WRITE
268                    | FileSystem::FILE_SHARE_DELETE,
269                None,
270                FileSystem::OPEN_EXISTING,
271                FileSystem::FILE_FLAG_OVERLAPPED,
272                None,
273            )?;
274        }
275
276        let mut journal = Ioctl::USN_JOURNAL_DATA_V2::default();
277
278        unsafe {
279            let mut ioctl_bytes_returned = 0;
280            IO::DeviceIoControl(
281                volume_handle,
282                Ioctl::FSCTL_QUERY_USN_JOURNAL,
283                None,
284                0,
285                Some(&mut journal as *mut _ as *mut c_void),
286                size_of::<Ioctl::USN_JOURNAL_DATA_V2>() as u32,
287                Some(&mut ioctl_bytes_returned),
288                None,
289            )?;
290        }
291
292        let next_usn = match options.next_usn {
293            NextUsn::First => 0,
294            NextUsn::Next => journal.NextUsn,
295            NextUsn::Custom(usn) => usn,
296        };
297
298        let max_history_size = match options.max_history_size {
299            HistorySize::Unlimited => 0,
300            HistorySize::Limited(size) => size,
301        };
302
303        let port = unsafe { IO::CreateIoCompletionPort(volume_handle, None, 0, 1)? };
304
305        Ok(Journal {
306            volume,
307            volume_handle,
308            port,
309            journal,
310            next_usn,
311            reason_mask: options.reason_mask,
312            history: VecDeque::new(),
313            max_history_size,
314        })
315    }
316
317    pub fn read(&mut self) -> Result<Vec<UsnRecord>, std::io::Error> {
318        self.read_sized::<4096>()
319    }
320
321    pub fn read_sized<const BUFFER_SIZE: usize>(
322        &mut self,
323    ) -> Result<Vec<UsnRecord>, std::io::Error> {
324        let mut results = Vec::<UsnRecord>::new();
325
326        let mut read = Ioctl::READ_USN_JOURNAL_DATA_V1 {
327            StartUsn: self.next_usn,
328            ReasonMask: self.reason_mask,
329            ReturnOnlyOnClose: 0,
330            Timeout: 0,
331            BytesToWaitFor: 0,
332            UsnJournalID: self.journal.UsnJournalID,
333            MinMajorVersion: 2,
334            MaxMajorVersion: u16::min(3, self.journal.MaxSupportedMajorVersion),
335        };
336
337        let mut buffer = AlignedBuffer::<BUFFER_SIZE>([0u8; BUFFER_SIZE]);
338
339        let mut bytes_returned = 0;
340        let mut overlapped = IO::OVERLAPPED {
341            ..Default::default()
342        };
343
344        unsafe {
345            let result = IO::DeviceIoControl(
346                self.volume_handle,
347                Ioctl::FSCTL_READ_USN_JOURNAL,
348                Some(&mut read as *mut _ as *mut c_void),
349                size_of::<Ioctl::READ_USN_JOURNAL_DATA_V1>() as u32,
350                Some(&mut buffer as *mut _ as *mut c_void),
351                BUFFER_SIZE as u32,
352                Some(&mut bytes_returned),
353                Some(&mut overlapped),
354            );
355
356            match result {
357                Ok(_) => {}
358                Err(err) => {
359                    if err.code() == ERROR_IO_PENDING.to_hresult() {
360                        let mut key = 0usize;
361                        let mut completed = std::ptr::null_mut();
362                        GetQueuedCompletionStatus(
363                            self.port,
364                            &mut bytes_returned,
365                            &mut key,
366                            &mut completed,
367                            INFINITE,
368                        )?;
369                    } else {
370                        return Err(err.into());
371                    }
372                }
373            }
374        }
375
376        let next_usn = i64::from_le_bytes(buffer.0[0..8].try_into().unwrap());
377        if next_usn == 0 || next_usn < self.next_usn {
378            return Ok(results);
379        } else {
380            self.next_usn = next_usn;
381        }
382
383        let mut offset = 8; // sizeof(USN)
384        while offset < bytes_returned {
385            let (record_len, record) = unsafe {
386                let record_ptr = std::mem::transmute::<*const u8, *const Ioctl::USN_RECORD_UNION>(
387                    buffer.0[offset as usize..].as_ptr(),
388                );
389
390                let record_len = (*record_ptr).Header.RecordLength;
391                if record_len == 0 {
392                    break;
393                }
394
395                let record = match (*record_ptr).Header.MajorVersion {
396                    2 => Some(UsnRecord::from_v2(self, &(*record_ptr).V2)),
397                    3 => Some(UsnRecord::from_v3(self, &(*record_ptr).V3)),
398                    _ => None,
399                };
400
401                (record_len, record)
402            };
403
404            if let Some(record) = record {
405                if record.reason
406                    & (Ioctl::USN_REASON_RENAME_OLD_NAME
407                        | Ioctl::USN_REASON_HARD_LINK_CHANGE
408                        | Ioctl::USN_REASON_REPARSE_POINT_CHANGE)
409                    != 0
410                {
411                    if self.max_history_size > 0 && self.history.len() >= self.max_history_size {
412                        self.history.pop_front();
413                    }
414                    self.history.push_back(record.clone());
415                }
416
417                results.push(record);
418            }
419
420            offset += record_len;
421        }
422
423        Ok(results)
424    }
425
426    pub fn match_rename(&self, record: &UsnRecord) -> Option<PathBuf> {
427        if record.reason & Ioctl::USN_REASON_RENAME_NEW_NAME == 0 {
428            return None;
429        }
430
431        self.history
432            .iter()
433            .find(|r| r.file_id == record.file_id && r.usn < record.usn)
434            .map(|r| r.path.clone())
435    }
436
437    pub fn trim_history(&mut self, min_usn: Option<i64>) {
438        match min_usn {
439            Some(usn) => self.history.retain(|r| r.usn > usn),
440            None => self.history.clear(),
441        }
442    }
443
444    pub fn get_next_usn(&self) -> i64 {
445        self.next_usn
446    }
447
448    pub fn get_reason_str(reason: u32) -> String {
449        let mut reason_str = String::new();
450
451        if reason & Ioctl::USN_REASON_BASIC_INFO_CHANGE != 0 {
452            reason_str.push_str("USN_REASON_BASIC_INFO_CHANGE ");
453        }
454        if reason & Ioctl::USN_REASON_CLOSE != 0 {
455            reason_str.push_str("USN_REASON_CLOSE ");
456        }
457        if reason & Ioctl::USN_REASON_COMPRESSION_CHANGE != 0 {
458            reason_str.push_str("USN_REASON_COMPRESSION_CHANGE ");
459        }
460        if reason & Ioctl::USN_REASON_DATA_EXTEND != 0 {
461            reason_str.push_str("USN_REASON_DATA_EXTEND ");
462        }
463        if reason & Ioctl::USN_REASON_DATA_OVERWRITE != 0 {
464            reason_str.push_str("USN_REASON_DATA_OVERWRITE ");
465        }
466        if reason & Ioctl::USN_REASON_DATA_TRUNCATION != 0 {
467            reason_str.push_str("USN_REASON_DATA_TRUNCATION ");
468        }
469        if reason & Ioctl::USN_REASON_DESIRED_STORAGE_CLASS_CHANGE != 0 {
470            reason_str.push_str("USN_REASON_DESIRED_STORAGE_CLASS_CHANGE ");
471        }
472        if reason & Ioctl::USN_REASON_EA_CHANGE != 0 {
473            reason_str.push_str("USN_REASON_EA_CHANGE ");
474        }
475        if reason & Ioctl::USN_REASON_ENCRYPTION_CHANGE != 0 {
476            reason_str.push_str("USN_REASON_ENCRYPTION_CHANGE ");
477        }
478        if reason & Ioctl::USN_REASON_FILE_CREATE != 0 {
479            reason_str.push_str("USN_REASON_FILE_CREATE ");
480        }
481        if reason & Ioctl::USN_REASON_FILE_DELETE != 0 {
482            reason_str.push_str("USN_REASON_FILE_DELETE ");
483        }
484        if reason & Ioctl::USN_REASON_HARD_LINK_CHANGE != 0 {
485            reason_str.push_str("USN_REASON_HARD_LINK_CHANGE ");
486        }
487        if reason & Ioctl::USN_REASON_INDEXABLE_CHANGE != 0 {
488            reason_str.push_str("USN_REASON_INDEXABLE_CHANGE ");
489        }
490        if reason & Ioctl::USN_REASON_INTEGRITY_CHANGE != 0 {
491            reason_str.push_str("USN_REASON_INTEGRITY_CHANGE ");
492        }
493        if reason & Ioctl::USN_REASON_NAMED_DATA_EXTEND != 0 {
494            reason_str.push_str("USN_REASON_NAMED_DATA_EXTEND ");
495        }
496        if reason & Ioctl::USN_REASON_NAMED_DATA_OVERWRITE != 0 {
497            reason_str.push_str("USN_REASON_NAMED_DATA_OVERWRITE ");
498        }
499        if reason & Ioctl::USN_REASON_NAMED_DATA_TRUNCATION != 0 {
500            reason_str.push_str("USN_REASON_NAMED_DATA_TRUNCATION ");
501        }
502        if reason & Ioctl::USN_REASON_OBJECT_ID_CHANGE != 0 {
503            reason_str.push_str("USN_REASON_OBJECT_ID_CHANGE ");
504        }
505        if reason & Ioctl::USN_REASON_RENAME_NEW_NAME != 0 {
506            reason_str.push_str("USN_REASON_RENAME_NEW_NAME ");
507        }
508        if reason & Ioctl::USN_REASON_RENAME_OLD_NAME != 0 {
509            reason_str.push_str("USN_REASON_RENAME_OLD_NAME ");
510        }
511        if reason & Ioctl::USN_REASON_REPARSE_POINT_CHANGE != 0 {
512            reason_str.push_str("USN_REASON_REPARSE_POINT_CHANGE ");
513        }
514        if reason & Ioctl::USN_REASON_SECURITY_CHANGE != 0 {
515            reason_str.push_str("USN_REASON_SECURITY_CHANGE ");
516        }
517        if reason & Ioctl::USN_REASON_STREAM_CHANGE != 0 {
518            reason_str.push_str("USN_REASON_STREAM_CHANGE ");
519        }
520        if reason & Ioctl::USN_REASON_TRANSACTED_CHANGE != 0 {
521            reason_str.push_str("USN_REASON_TRANSACTED_CHANGE ");
522        }
523
524        reason_str
525    }
526}
527
528impl Drop for Journal {
529    fn drop(&mut self) {
530        unsafe {
531            let _ = Foundation::CloseHandle(self.volume_handle);
532            let _ = Foundation::CloseHandle(self.port);
533        }
534    }
535}