Skip to main content

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, errors::NtfsReaderResult, 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    tracing::debug!("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) -> NtfsReaderResult<Journal> {
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) -> NtfsReaderResult<Vec<UsnRecord>> {
318        self.read_sized::<4096>()
319    }
320
321    pub fn read_sized<const BUFFER_SIZE: usize>(&mut self) -> NtfsReaderResult<Vec<UsnRecord>> {
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: 2,
332            MaxMajorVersion: u16::min(3, 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            let result = 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            match result {
355                Ok(_) => {}
356                Err(err) => {
357                    if err.code() == ERROR_IO_PENDING.to_hresult() {
358                        let mut key = 0usize;
359                        let mut completed = std::ptr::null_mut();
360                        GetQueuedCompletionStatus(
361                            self.port,
362                            &mut bytes_returned,
363                            &mut key,
364                            &mut completed,
365                            INFINITE,
366                        )?;
367                    } else {
368                        return Err(err.into());
369                    }
370                }
371            }
372        }
373
374        let next_usn = i64::from_le_bytes(buffer.0[0..8].try_into().unwrap());
375        if next_usn == 0 || next_usn < self.next_usn {
376            return Ok(results);
377        } else {
378            self.next_usn = next_usn;
379        }
380
381        let mut offset = 8; // sizeof(USN)
382        while offset < bytes_returned {
383            let remaining = (bytes_returned - offset) as usize;
384            if remaining < size_of::<Ioctl::USN_RECORD_COMMON_HEADER>() {
385                break;
386            }
387
388            let (record_len, record) = unsafe {
389                let record_ptr =
390                    buffer.0[offset as usize..].as_ptr() as *const Ioctl::USN_RECORD_UNION;
391
392                let record_len = (*record_ptr).Header.RecordLength;
393                if record_len == 0 || record_len as usize > remaining {
394                    break;
395                }
396
397                let record = match (*record_ptr).Header.MajorVersion {
398                    2 => Some(UsnRecord::from_v2(self, &(*record_ptr).V2)),
399                    3 => Some(UsnRecord::from_v3(self, &(*record_ptr).V3)),
400                    _ => None,
401                };
402
403                (record_len, record)
404            };
405
406            if let Some(record) = record {
407                if record.reason
408                    & (Ioctl::USN_REASON_RENAME_OLD_NAME
409                        | Ioctl::USN_REASON_HARD_LINK_CHANGE
410                        | Ioctl::USN_REASON_REPARSE_POINT_CHANGE)
411                    != 0
412                {
413                    if self.max_history_size > 0 && self.history.len() >= self.max_history_size {
414                        self.history.pop_front();
415                    }
416                    self.history.push_back(record.clone());
417                }
418
419                results.push(record);
420            }
421
422            offset += record_len;
423        }
424
425        Ok(results)
426    }
427
428    pub fn match_rename(&self, record: &UsnRecord) -> Option<PathBuf> {
429        if record.reason & Ioctl::USN_REASON_RENAME_NEW_NAME == 0 {
430            return None;
431        }
432
433        self.history
434            .iter()
435            .find(|r| r.file_id == record.file_id && r.usn < record.usn)
436            .map(|r| r.path.clone())
437    }
438
439    pub fn trim_history(&mut self, min_usn: Option<i64>) {
440        match min_usn {
441            Some(usn) => self.history.retain(|r| r.usn > usn),
442            None => self.history.clear(),
443        }
444    }
445
446    pub fn get_next_usn(&self) -> i64 {
447        self.next_usn
448    }
449
450    pub fn get_reason_str(reason: u32) -> String {
451        let mut reason_str = String::new();
452
453        if reason & Ioctl::USN_REASON_BASIC_INFO_CHANGE != 0 {
454            reason_str.push_str("USN_REASON_BASIC_INFO_CHANGE ");
455        }
456        if reason & Ioctl::USN_REASON_CLOSE != 0 {
457            reason_str.push_str("USN_REASON_CLOSE ");
458        }
459        if reason & Ioctl::USN_REASON_COMPRESSION_CHANGE != 0 {
460            reason_str.push_str("USN_REASON_COMPRESSION_CHANGE ");
461        }
462        if reason & Ioctl::USN_REASON_DATA_EXTEND != 0 {
463            reason_str.push_str("USN_REASON_DATA_EXTEND ");
464        }
465        if reason & Ioctl::USN_REASON_DATA_OVERWRITE != 0 {
466            reason_str.push_str("USN_REASON_DATA_OVERWRITE ");
467        }
468        if reason & Ioctl::USN_REASON_DATA_TRUNCATION != 0 {
469            reason_str.push_str("USN_REASON_DATA_TRUNCATION ");
470        }
471        if reason & Ioctl::USN_REASON_DESIRED_STORAGE_CLASS_CHANGE != 0 {
472            reason_str.push_str("USN_REASON_DESIRED_STORAGE_CLASS_CHANGE ");
473        }
474        if reason & Ioctl::USN_REASON_EA_CHANGE != 0 {
475            reason_str.push_str("USN_REASON_EA_CHANGE ");
476        }
477        if reason & Ioctl::USN_REASON_ENCRYPTION_CHANGE != 0 {
478            reason_str.push_str("USN_REASON_ENCRYPTION_CHANGE ");
479        }
480        if reason & Ioctl::USN_REASON_FILE_CREATE != 0 {
481            reason_str.push_str("USN_REASON_FILE_CREATE ");
482        }
483        if reason & Ioctl::USN_REASON_FILE_DELETE != 0 {
484            reason_str.push_str("USN_REASON_FILE_DELETE ");
485        }
486        if reason & Ioctl::USN_REASON_HARD_LINK_CHANGE != 0 {
487            reason_str.push_str("USN_REASON_HARD_LINK_CHANGE ");
488        }
489        if reason & Ioctl::USN_REASON_INDEXABLE_CHANGE != 0 {
490            reason_str.push_str("USN_REASON_INDEXABLE_CHANGE ");
491        }
492        if reason & Ioctl::USN_REASON_INTEGRITY_CHANGE != 0 {
493            reason_str.push_str("USN_REASON_INTEGRITY_CHANGE ");
494        }
495        if reason & Ioctl::USN_REASON_NAMED_DATA_EXTEND != 0 {
496            reason_str.push_str("USN_REASON_NAMED_DATA_EXTEND ");
497        }
498        if reason & Ioctl::USN_REASON_NAMED_DATA_OVERWRITE != 0 {
499            reason_str.push_str("USN_REASON_NAMED_DATA_OVERWRITE ");
500        }
501        if reason & Ioctl::USN_REASON_NAMED_DATA_TRUNCATION != 0 {
502            reason_str.push_str("USN_REASON_NAMED_DATA_TRUNCATION ");
503        }
504        if reason & Ioctl::USN_REASON_OBJECT_ID_CHANGE != 0 {
505            reason_str.push_str("USN_REASON_OBJECT_ID_CHANGE ");
506        }
507        if reason & Ioctl::USN_REASON_RENAME_NEW_NAME != 0 {
508            reason_str.push_str("USN_REASON_RENAME_NEW_NAME ");
509        }
510        if reason & Ioctl::USN_REASON_RENAME_OLD_NAME != 0 {
511            reason_str.push_str("USN_REASON_RENAME_OLD_NAME ");
512        }
513        if reason & Ioctl::USN_REASON_REPARSE_POINT_CHANGE != 0 {
514            reason_str.push_str("USN_REASON_REPARSE_POINT_CHANGE ");
515        }
516        if reason & Ioctl::USN_REASON_SECURITY_CHANGE != 0 {
517            reason_str.push_str("USN_REASON_SECURITY_CHANGE ");
518        }
519        if reason & Ioctl::USN_REASON_STREAM_CHANGE != 0 {
520            reason_str.push_str("USN_REASON_STREAM_CHANGE ");
521        }
522        if reason & Ioctl::USN_REASON_TRANSACTED_CHANGE != 0 {
523            reason_str.push_str("USN_REASON_TRANSACTED_CHANGE ");
524        }
525
526        reason_str
527    }
528}
529
530impl Drop for Journal {
531    fn drop(&mut self) {
532        unsafe {
533            let _ = Foundation::CloseHandle(self.volume_handle);
534            let _ = Foundation::CloseHandle(self.port);
535        }
536    }
537}