1use 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 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 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 let Some(path) = get_file_path(volume_handle, file_id) {
137 return volume_path.join(path);
138 }
139 }
140
141 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, 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 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 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; 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 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 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 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 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 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 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 let mut journal = make_journal(journal_version, Ioctl::USN_REASON_FILE_DELETE)?;
666 while !journal.read()?.is_empty() {}
667
668 std::fs::remove_file(&file_path)?;
673
674 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}