use std::collections::VecDeque;
use std::ffi::{CString, OsString};
use std::mem::size_of;
use std::os::raw::c_void;
use std::os::windows::ffi::OsStringExt;
use std::path::{Path, PathBuf};
use windows::core::PCSTR;
use windows::Win32::Foundation::{self, ERROR_IO_PENDING, ERROR_MORE_DATA};
use windows::Win32::Storage::FileSystem::{self, FILE_FLAG_BACKUP_SEMANTICS};
use windows::Win32::System::Ioctl;
use windows::Win32::System::Threading::INFINITE;
use windows::Win32::System::IO::{self, GetQueuedCompletionStatus};
use crate::{api::FileId, errors::NtfsReaderResult, volume::Volume};
#[repr(align(64))]
#[derive(Debug, Clone, Copy)]
struct AlignedBuffer<const N: usize>([u8; N]);
fn get_usn_record_time(timestamp: i64) -> std::time::Duration {
if timestamp <= 0 {
return std::time::Duration::from_nanos(0);
}
let nanos = (timestamp as i128).saturating_mul(100);
let capped = nanos.min(u64::MAX as i128);
std::time::Duration::from_nanos(capped as u64)
}
fn get_usn_record_name(file_name_length: u16, file_name: *const u16) -> String {
let size = (file_name_length / 2) as usize;
if size > 0 {
unsafe {
let name_u16 = std::slice::from_raw_parts(file_name, size);
let name = std::ffi::OsString::from_wide(name_u16)
.to_string_lossy()
.into_owned();
return name;
}
}
String::new()
}
fn get_file_path(volume_handle: Foundation::HANDLE, file_id: FileId) -> Option<PathBuf> {
let (id, id_type) = match file_id {
FileId::Normal(id) => (
FileSystem::FILE_ID_DESCRIPTOR_0 { FileId: id as i64 },
FileSystem::FileIdType,
),
FileId::Extended(id) => (
FileSystem::FILE_ID_DESCRIPTOR_0 { ExtendedFileId: id },
FileSystem::ExtendedFileIdType,
),
};
let file_id_desc = FileSystem::FILE_ID_DESCRIPTOR {
Type: id_type,
dwSize: size_of::<FileSystem::FILE_ID_DESCRIPTOR>() as u32,
Anonymous: id,
};
unsafe {
let file_handle = FileSystem::OpenFileById(
volume_handle,
&file_id_desc,
0,
FileSystem::FILE_SHARE_READ
| FileSystem::FILE_SHARE_WRITE
| FileSystem::FILE_SHARE_DELETE,
None,
FILE_FLAG_BACKUP_SEMANTICS,
)
.unwrap_or(Foundation::INVALID_HANDLE_VALUE);
if file_handle.is_invalid() {
return None;
}
let mut info_buffer_size = size_of::<FileSystem::FILE_NAME_INFO>()
+ (Foundation::MAX_PATH as usize) * size_of::<u16>();
let mut info_buffer = vec![0u8; info_buffer_size];
let result = loop {
let info_result = FileSystem::GetFileInformationByHandleEx(
file_handle,
FileSystem::FileNameInfo,
info_buffer.as_mut_ptr() as *mut _,
info_buffer_size as u32,
);
match info_result {
Ok(_) => {
let (_, body, _) = info_buffer.align_to::<FileSystem::FILE_NAME_INFO>();
let info = &body[0];
let name_len = info.FileNameLength as usize / size_of::<u16>();
let name_u16 = std::slice::from_raw_parts(info.FileName.as_ptr(), name_len);
break Some(PathBuf::from(OsString::from_wide(name_u16)));
}
Err(err) => {
if err.code() == ERROR_MORE_DATA.to_hresult() {
let required_size = info_buffer.align_to::<FileSystem::FILE_NAME_INFO>().1
[0]
.FileNameLength as usize;
info_buffer_size = size_of::<FileSystem::FILE_NAME_INFO>() + required_size;
info_buffer.resize(info_buffer_size, 0);
} else {
break None;
}
}
}
};
let _ = Foundation::CloseHandle(file_handle);
result
}
}
fn get_usn_record_path(
volume_path: &Path,
volume_handle: Foundation::HANDLE,
file_name: String,
file_id: FileId,
parent_id: FileId,
) -> PathBuf {
if let Some(parent_path) = get_file_path(volume_handle, parent_id) {
return volume_path.join(parent_path.join(&file_name));
} else {
if let Some(path) = get_file_path(volume_handle, file_id) {
return volume_path.join(path);
}
}
tracing::debug!("Could not get path: {}", file_name);
PathBuf::from(&file_name)
}
#[derive(Debug, Clone)]
pub struct UsnRecord {
pub usn: i64,
pub timestamp: std::time::Duration,
pub file_id: FileId,
pub parent_id: FileId,
pub reason: u32,
pub path: PathBuf,
}
impl UsnRecord {
fn from_v2(journal: &Journal, rec: &Ioctl::USN_RECORD_V2) -> Self {
let usn = rec.Usn;
let timestamp = get_usn_record_time(rec.TimeStamp);
let file_id = FileId::Normal(rec.FileReferenceNumber);
let parent_id = FileId::Normal(rec.ParentFileReferenceNumber);
let reason = rec.Reason;
let name = get_usn_record_name(rec.FileNameLength, rec.FileName.as_ptr());
let path = get_usn_record_path(
&journal.volume.path,
journal.volume_handle,
name,
file_id,
parent_id,
);
UsnRecord {
usn,
timestamp,
file_id,
parent_id,
reason,
path,
}
}
fn from_v3(journal: &Journal, rec: &Ioctl::USN_RECORD_V3) -> Self {
let usn = rec.Usn;
let timestamp = get_usn_record_time(rec.TimeStamp);
let file_id = FileId::Extended(rec.FileReferenceNumber);
let parent_id = FileId::Extended(rec.ParentFileReferenceNumber);
let reason = rec.Reason;
let name = get_usn_record_name(rec.FileNameLength, rec.FileName.as_ptr());
let path = get_usn_record_path(
&journal.volume.path,
journal.volume_handle,
name,
file_id,
parent_id,
);
UsnRecord {
usn,
timestamp,
file_id,
parent_id,
reason,
path,
}
}
}
#[derive(Debug, Clone)]
pub enum NextUsn {
First,
Next,
Custom(i64),
}
#[derive(Debug, Clone)]
pub enum HistorySize {
Unlimited,
Limited(usize),
}
#[derive(Debug, Clone)]
pub struct JournalOptions {
pub reason_mask: u32,
pub next_usn: NextUsn,
pub max_history_size: HistorySize,
}
impl Default for JournalOptions {
fn default() -> Self {
JournalOptions {
reason_mask: 0xFFFFFFFF,
next_usn: NextUsn::Next,
max_history_size: HistorySize::Unlimited,
}
}
}
pub struct Journal {
volume: Volume,
volume_handle: Foundation::HANDLE,
port: Foundation::HANDLE,
journal: Ioctl::USN_JOURNAL_DATA_V2,
next_usn: i64,
reason_mask: u32, history: VecDeque<UsnRecord>,
max_history_size: usize,
}
impl Journal {
pub fn new(volume: Volume, options: JournalOptions) -> NtfsReaderResult<Journal> {
let volume_handle: Foundation::HANDLE;
unsafe {
let path = CString::new(volume.path.to_str().unwrap()).unwrap();
volume_handle = FileSystem::CreateFileA(
PCSTR::from_raw(path.as_bytes_with_nul().as_ptr()),
(FileSystem::FILE_GENERIC_READ | FileSystem::FILE_GENERIC_WRITE).0,
FileSystem::FILE_SHARE_READ
| FileSystem::FILE_SHARE_WRITE
| FileSystem::FILE_SHARE_DELETE,
None,
FileSystem::OPEN_EXISTING,
FileSystem::FILE_FLAG_OVERLAPPED,
None,
)?;
}
let mut journal = Ioctl::USN_JOURNAL_DATA_V2::default();
unsafe {
let mut ioctl_bytes_returned = 0;
IO::DeviceIoControl(
volume_handle,
Ioctl::FSCTL_QUERY_USN_JOURNAL,
None,
0,
Some(&mut journal as *mut _ as *mut c_void),
size_of::<Ioctl::USN_JOURNAL_DATA_V2>() as u32,
Some(&mut ioctl_bytes_returned),
None,
)?;
}
let next_usn = match options.next_usn {
NextUsn::First => 0,
NextUsn::Next => journal.NextUsn,
NextUsn::Custom(usn) => usn,
};
let max_history_size = match options.max_history_size {
HistorySize::Unlimited => 0,
HistorySize::Limited(size) => size,
};
let port = unsafe { IO::CreateIoCompletionPort(volume_handle, None, 0, 1)? };
Ok(Journal {
volume,
volume_handle,
port,
journal,
next_usn,
reason_mask: options.reason_mask,
history: VecDeque::new(),
max_history_size,
})
}
pub fn read(&mut self) -> NtfsReaderResult<Vec<UsnRecord>> {
self.read_sized::<4096>()
}
pub fn read_sized<const BUFFER_SIZE: usize>(&mut self) -> NtfsReaderResult<Vec<UsnRecord>> {
let mut results = Vec::<UsnRecord>::new();
let mut read = Ioctl::READ_USN_JOURNAL_DATA_V1 {
StartUsn: self.next_usn,
ReasonMask: self.reason_mask,
ReturnOnlyOnClose: 0,
Timeout: 0,
BytesToWaitFor: 0,
UsnJournalID: self.journal.UsnJournalID,
MinMajorVersion: 2,
MaxMajorVersion: u16::min(3, self.journal.MaxSupportedMajorVersion),
};
let mut buffer = AlignedBuffer::<BUFFER_SIZE>([0u8; BUFFER_SIZE]);
let mut bytes_returned = 0;
let mut overlapped = IO::OVERLAPPED {
..Default::default()
};
unsafe {
let result = IO::DeviceIoControl(
self.volume_handle,
Ioctl::FSCTL_READ_USN_JOURNAL,
Some(&mut read as *mut _ as *mut c_void),
size_of::<Ioctl::READ_USN_JOURNAL_DATA_V1>() as u32,
Some(&mut buffer as *mut _ as *mut c_void),
BUFFER_SIZE as u32,
Some(&mut bytes_returned),
Some(&mut overlapped),
);
match result {
Ok(_) => {}
Err(err) => {
if err.code() == ERROR_IO_PENDING.to_hresult() {
let mut key = 0usize;
let mut completed = std::ptr::null_mut();
GetQueuedCompletionStatus(
self.port,
&mut bytes_returned,
&mut key,
&mut completed,
INFINITE,
)?;
} else {
return Err(err.into());
}
}
}
}
let next_usn = i64::from_le_bytes(buffer.0[0..8].try_into().unwrap());
if next_usn == 0 || next_usn < self.next_usn {
return Ok(results);
} else {
self.next_usn = next_usn;
}
let mut offset = 8; while offset < bytes_returned {
let remaining = (bytes_returned - offset) as usize;
if remaining < size_of::<Ioctl::USN_RECORD_COMMON_HEADER>() {
break;
}
let (record_len, record) = unsafe {
let record_ptr =
buffer.0[offset as usize..].as_ptr() as *const Ioctl::USN_RECORD_UNION;
let record_len = (*record_ptr).Header.RecordLength;
if record_len == 0 || record_len as usize > remaining {
break;
}
let record = match (*record_ptr).Header.MajorVersion {
2 => Some(UsnRecord::from_v2(self, &(*record_ptr).V2)),
3 => Some(UsnRecord::from_v3(self, &(*record_ptr).V3)),
_ => None,
};
(record_len, record)
};
if let Some(record) = record {
if record.reason
& (Ioctl::USN_REASON_RENAME_OLD_NAME
| Ioctl::USN_REASON_HARD_LINK_CHANGE
| Ioctl::USN_REASON_REPARSE_POINT_CHANGE)
!= 0
{
if self.max_history_size > 0 && self.history.len() >= self.max_history_size {
self.history.pop_front();
}
self.history.push_back(record.clone());
}
results.push(record);
}
offset += record_len;
}
Ok(results)
}
pub fn match_rename(&self, record: &UsnRecord) -> Option<PathBuf> {
if record.reason & Ioctl::USN_REASON_RENAME_NEW_NAME == 0 {
return None;
}
self.history
.iter()
.find(|r| r.file_id == record.file_id && r.usn < record.usn)
.map(|r| r.path.clone())
}
pub fn trim_history(&mut self, min_usn: Option<i64>) {
match min_usn {
Some(usn) => self.history.retain(|r| r.usn > usn),
None => self.history.clear(),
}
}
pub fn get_next_usn(&self) -> i64 {
self.next_usn
}
pub fn get_reason_str(reason: u32) -> String {
let mut reason_str = String::new();
if reason & Ioctl::USN_REASON_BASIC_INFO_CHANGE != 0 {
reason_str.push_str("USN_REASON_BASIC_INFO_CHANGE ");
}
if reason & Ioctl::USN_REASON_CLOSE != 0 {
reason_str.push_str("USN_REASON_CLOSE ");
}
if reason & Ioctl::USN_REASON_COMPRESSION_CHANGE != 0 {
reason_str.push_str("USN_REASON_COMPRESSION_CHANGE ");
}
if reason & Ioctl::USN_REASON_DATA_EXTEND != 0 {
reason_str.push_str("USN_REASON_DATA_EXTEND ");
}
if reason & Ioctl::USN_REASON_DATA_OVERWRITE != 0 {
reason_str.push_str("USN_REASON_DATA_OVERWRITE ");
}
if reason & Ioctl::USN_REASON_DATA_TRUNCATION != 0 {
reason_str.push_str("USN_REASON_DATA_TRUNCATION ");
}
if reason & Ioctl::USN_REASON_DESIRED_STORAGE_CLASS_CHANGE != 0 {
reason_str.push_str("USN_REASON_DESIRED_STORAGE_CLASS_CHANGE ");
}
if reason & Ioctl::USN_REASON_EA_CHANGE != 0 {
reason_str.push_str("USN_REASON_EA_CHANGE ");
}
if reason & Ioctl::USN_REASON_ENCRYPTION_CHANGE != 0 {
reason_str.push_str("USN_REASON_ENCRYPTION_CHANGE ");
}
if reason & Ioctl::USN_REASON_FILE_CREATE != 0 {
reason_str.push_str("USN_REASON_FILE_CREATE ");
}
if reason & Ioctl::USN_REASON_FILE_DELETE != 0 {
reason_str.push_str("USN_REASON_FILE_DELETE ");
}
if reason & Ioctl::USN_REASON_HARD_LINK_CHANGE != 0 {
reason_str.push_str("USN_REASON_HARD_LINK_CHANGE ");
}
if reason & Ioctl::USN_REASON_INDEXABLE_CHANGE != 0 {
reason_str.push_str("USN_REASON_INDEXABLE_CHANGE ");
}
if reason & Ioctl::USN_REASON_INTEGRITY_CHANGE != 0 {
reason_str.push_str("USN_REASON_INTEGRITY_CHANGE ");
}
if reason & Ioctl::USN_REASON_NAMED_DATA_EXTEND != 0 {
reason_str.push_str("USN_REASON_NAMED_DATA_EXTEND ");
}
if reason & Ioctl::USN_REASON_NAMED_DATA_OVERWRITE != 0 {
reason_str.push_str("USN_REASON_NAMED_DATA_OVERWRITE ");
}
if reason & Ioctl::USN_REASON_NAMED_DATA_TRUNCATION != 0 {
reason_str.push_str("USN_REASON_NAMED_DATA_TRUNCATION ");
}
if reason & Ioctl::USN_REASON_OBJECT_ID_CHANGE != 0 {
reason_str.push_str("USN_REASON_OBJECT_ID_CHANGE ");
}
if reason & Ioctl::USN_REASON_RENAME_NEW_NAME != 0 {
reason_str.push_str("USN_REASON_RENAME_NEW_NAME ");
}
if reason & Ioctl::USN_REASON_RENAME_OLD_NAME != 0 {
reason_str.push_str("USN_REASON_RENAME_OLD_NAME ");
}
if reason & Ioctl::USN_REASON_REPARSE_POINT_CHANGE != 0 {
reason_str.push_str("USN_REASON_REPARSE_POINT_CHANGE ");
}
if reason & Ioctl::USN_REASON_SECURITY_CHANGE != 0 {
reason_str.push_str("USN_REASON_SECURITY_CHANGE ");
}
if reason & Ioctl::USN_REASON_STREAM_CHANGE != 0 {
reason_str.push_str("USN_REASON_STREAM_CHANGE ");
}
if reason & Ioctl::USN_REASON_TRANSACTED_CHANGE != 0 {
reason_str.push_str("USN_REASON_TRANSACTED_CHANGE ");
}
reason_str
}
}
impl Drop for Journal {
fn drop(&mut self) {
unsafe {
let _ = Foundation::CloseHandle(self.volume_handle);
let _ = Foundation::CloseHandle(self.port);
}
}
}