#![cfg(windows)]
#![allow(dead_code)]
use std::ffi::c_void;
use std::os::windows::io::{FromRawHandle, RawHandle};
use std::path::PathBuf;
use std::ptr;
use crate::policy::{DotfilePolicy, SymlinkPolicy};
#[allow(clippy::upper_case_acronyms)]
pub(crate) type HANDLE = *mut c_void;
#[allow(clippy::upper_case_acronyms)]
type DWORD = u32;
#[allow(clippy::upper_case_acronyms)]
type BOOL = i32;
#[allow(clippy::upper_case_acronyms)]
type PCWSTR = *const u16;
#[allow(clippy::upper_case_acronyms)]
type PWSTR = *mut u16;
const INVALID_HANDLE_VALUE: HANDLE = -1isize as HANDLE;
const TRUE: BOOL = 1;
const FALSE: BOOL = 0;
const GENERIC_READ: DWORD = 0x80000000;
const FILE_GENERIC_READ: DWORD = 0x00120089;
const FILE_LIST_DIRECTORY: DWORD = 0x00000001;
const FILE_READ_DATA: DWORD = 0x0001;
const FILE_READ_ATTRIBUTES: DWORD = 0x0080;
const FILE_READ_EA: DWORD = 0x0008;
const READ_CONTROL: DWORD = 0x00020000;
const FILE_SHARE_READ: DWORD = 0x00000001;
const FILE_SHARE_WRITE: DWORD = 0x00000002;
const FILE_SHARE_DELETE: DWORD = 0x00000004;
const OPEN_EXISTING: DWORD = 3;
const FILE_FLAG_OPEN_REPARSE_POINT: DWORD = 0x00200000;
const FILE_FLAG_BACKUP_SEMANTICS: DWORD = 0x02000000;
const FILE_ATTRIBUTE_REPARSE_POINT: DWORD = 0x00000400;
const FILE_ATTRIBUTE_DIRECTORY: DWORD = 0x00000010;
const IO_REPARSE_TAG_SYMLINK: u32 = 0xA0000000;
const IO_REPARSE_TAG_MOUNT_POINT: u32 = 0xA0000003;
const ERROR_FILE_NOT_FOUND: DWORD = 2;
const ERROR_PATH_NOT_FOUND: DWORD = 3;
const ERROR_ACCESS_DENIED: DWORD = 5;
const ERROR_NOT_A_DIRECTORY: DWORD = 267;
const ERROR_TOO_MANY_LINKS: DWORD = 1142;
const FILE_ATTRIBUTE_TAG_INFO_CLASS: u32 = 9;
const FILE_STANDARD_INFO_CLASS: u32 = 1;
const FILE_ID_BOTH_DIRECTORY_INFO: u32 = 37;
const STATUS_NO_MORE_FILES: u32 = 0x80000006;
const DUPLICATE_SAME_ACCESS: DWORD = 0x00000002;
#[repr(C)]
#[derive(Clone, Copy, Default)]
pub(crate) struct FILE_ATTRIBUTE_TAG_INFO {
file_attributes: DWORD,
reparse_tag: u32,
}
#[repr(C)]
#[derive(Clone, Copy, Default)]
pub(crate) struct FILE_STANDARD_INFO {
allocation_size: i64,
end_of_file: i64,
number_of_links: DWORD,
delete_pending: u8,
directory: u8,
}
#[repr(C)]
#[derive(Clone, Copy)]
#[allow(dead_code)]
struct WIN32_FIND_DATAW {
dw_file_attributes: DWORD,
ft_creation_time: [u32; 2],
ft_last_access_time: [u32; 2],
ft_last_write_time: [u32; 2],
n_file_size_high: DWORD,
n_file_size_low: DWORD,
dw_reserved0: DWORD,
dw_reserved1: DWORD,
c_file_name: [u16; 260],
c_alternate_file_name: [u16; 14],
}
impl Default for WIN32_FIND_DATAW {
fn default() -> Self {
Self {
dw_file_attributes: 0,
ft_creation_time: [0; 2],
ft_last_access_time: [0; 2],
ft_last_write_time: [0; 2],
n_file_size_high: 0,
n_file_size_low: 0,
dw_reserved0: 0,
dw_reserved1: 0,
c_file_name: [0; 260],
c_alternate_file_name: [0; 14],
}
}
}
#[repr(C)]
#[derive(Clone, Copy, Default)]
#[allow(dead_code)]
struct FILE_ID_BOTH_DIR_INFO {
next_entry_offset: DWORD,
file_index: u64,
creation_time: u64,
last_access_time: u64,
last_write_time: u64,
change_time: u64,
allocation_size: i64,
end_of_file: i64,
file_attributes: DWORD,
file_name_length: DWORD,
ea_size: DWORD,
file_id: [u8; 8],
file_name: [u16; 1], }
const FILE_ID_BOTH_DIR_INFO_FILE_NAME_LENGTH_OFFSET: usize = 60;
const FILE_ID_BOTH_DIR_INFO_FILE_ATTRIBUTES_OFFSET: usize = 56;
const FILE_ID_BOTH_DIR_INFO_FILE_ID_OFFSET: usize = 96;
const FILE_ID_BOTH_DIR_INFO_FILE_NAME_OFFSET: usize = 104;
const FILE_ID_BOTH_DIR_INFO_HEADER_SIZE: usize = FILE_ID_BOTH_DIR_INFO_FILE_NAME_OFFSET;
#[allow(clippy::upper_case_acronyms)]
type NTSTATUS = i32;
#[repr(C)]
struct NtUnicodeString {
length: u16,
maximum_length: u16,
buffer: PWSTR,
}
#[repr(C)]
struct ObjectAttributes {
length: u32,
root_directory: HANDLE,
object_name: *const NtUnicodeString,
attributes: u32,
security_descriptor: *mut c_void,
security_quality_of_service: *mut c_void,
}
#[repr(C)]
#[allow(dead_code)]
struct IoStatusBlock {
status: NTSTATUS,
information: usize,
}
const OBJ_CASE_INSENSITIVE: u32 = 0x00000040;
const FILE_OPEN: u32 = 0x00000001;
const FILE_DIRECTORY_FILE: u32 = 0x00000001;
const FILE_NON_DIRECTORY_FILE: u32 = 0x00000040;
const FILE_OPEN_FOR_BACKUP_INTENT: u32 = 0x00004000;
const FILE_SYNCHRONOUS_IO_NONALERT: u32 = 0x00000020;
const FILE_OPEN_REPARSE_POINT: u32 = 0x00200000;
const SYNCHRONIZE: u32 = 0x00100000;
const STATUS_NO_SUCH_FILE: u32 = 0xC000000F;
const STATUS_OBJECT_NAME_NOT_FOUND: u32 = 0xC0000034;
const STATUS_NOT_A_DIRECTORY: u32 = 0xC0000103;
const STATUS_FILE_IS_A_DIRECTORY: u32 = 0xC00000BA;
const STATUS_ACCESS_DENIED: u32 = 0xC0000022;
extern "system" {
fn CreateFileW(
lp_file_name: PCWSTR,
dw_desired_access: DWORD,
dw_share_mode: DWORD,
lp_security_attributes: *mut c_void,
dw_creation_disposition: DWORD,
dw_flags_and_attributes: DWORD,
h_template_file: HANDLE,
) -> HANDLE;
fn CloseHandle(hObject: HANDLE) -> BOOL;
fn GetFileInformationByHandleEx(
h_file: HANDLE,
file_info_class: u32,
lp_file_information: *mut c_void,
dw_buffer_size: DWORD,
) -> BOOL;
fn GetFinalPathNameByHandleW(
h_file: HANDLE,
lpsz_file_path: PWSTR,
cch_file_path: DWORD,
dw_flags: DWORD,
) -> DWORD;
fn FindFirstFileW(lp_file_name: PCWSTR, lp_find_file_data: *mut WIN32_FIND_DATAW) -> HANDLE;
fn FindNextFileW(h_find_file: HANDLE, lp_find_file_data: *mut WIN32_FIND_DATAW) -> BOOL;
fn FindClose(h_find_file: HANDLE) -> BOOL;
fn GetLastError() -> DWORD;
fn DuplicateHandle(
h_source_process_handle: HANDLE,
h_source_handle: HANDLE,
h_target_process_handle: HANDLE,
lp_target_handle: *mut HANDLE,
dw_desired_access: DWORD,
b_inherit_handle: BOOL,
dw_options: DWORD,
) -> BOOL;
fn GetCurrentProcess() -> HANDLE;
fn NtOpenFile(
file_handle: *mut HANDLE,
desired_access: u32,
object_attributes: *mut ObjectAttributes,
io_status_block: *mut IoStatusBlock,
share_access: u32,
open_options: u32,
) -> NTSTATUS;
fn NtQueryDirectoryFile(
file_handle: HANDLE,
event: HANDLE,
apc_routine: *mut c_void,
apc_context: *mut c_void,
io_status_block: *mut IoStatusBlock,
file_information: *mut c_void,
length: DWORD,
file_information_class: u32,
return_single_entry: BOOL,
file_name: *const NtUnicodeString,
restart_scan: BOOL,
) -> NTSTATUS;
}
pub(crate) struct OwnedHandle(HANDLE);
impl std::fmt::Debug for OwnedHandle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("OwnedHandle").finish()
}
}
impl OwnedHandle {
pub(crate) unsafe fn from_raw(handle: HANDLE) -> Self {
Self(handle)
}
pub(crate) fn is_valid(&self) -> bool {
!self.0.is_null() && self.0 != INVALID_HANDLE_VALUE
}
pub(crate) fn raw(&self) -> HANDLE {
self.0
}
pub(crate) fn try_clone(&self) -> Result<Self, WindowsFsError> {
if !self.is_valid() {
return Ok(Self(INVALID_HANDLE_VALUE));
}
let mut new_handle = INVALID_HANDLE_VALUE;
let ok = unsafe {
DuplicateHandle(
GetCurrentProcess(),
self.0,
GetCurrentProcess(),
&mut new_handle,
0,
FALSE,
DUPLICATE_SAME_ACCESS,
)
};
if ok == 0 {
return Err(WindowsFsError::IoError(unsafe { GetLastError() }));
}
Ok(Self(new_handle))
}
}
unsafe impl Send for OwnedHandle {}
unsafe impl Sync for OwnedHandle {}
impl Drop for OwnedHandle {
fn drop(&mut self) {
if self.is_valid() {
unsafe {
CloseHandle(self.0);
}
}
}
}
#[derive(Debug)]
pub(crate) enum WindowsFsError {
NotFound,
NotADirectory,
AccessDenied,
TooManyLinks,
ReparsePointDenied,
IoError(DWORD),
}
impl std::fmt::Display for WindowsFsError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotFound => write!(f, "file not found"),
Self::NotADirectory => write!(f, "not a directory"),
Self::AccessDenied => write!(f, "access denied"),
Self::TooManyLinks => write!(f, "too many links"),
Self::ReparsePointDenied => write!(f, "reparse point denied"),
Self::IoError(code) => write!(f, "I/O error: {code}"),
}
}
}
impl std::error::Error for WindowsFsError {}
fn last_error_to_fs_error() -> WindowsFsError {
let code = unsafe { GetLastError() };
match code {
ERROR_FILE_NOT_FOUND | ERROR_PATH_NOT_FOUND => WindowsFsError::NotFound,
ERROR_NOT_A_DIRECTORY => WindowsFsError::NotADirectory,
ERROR_ACCESS_DENIED => WindowsFsError::AccessDenied,
ERROR_TOO_MANY_LINKS => WindowsFsError::TooManyLinks,
other => WindowsFsError::IoError(other),
}
}
fn ntstatus_to_error(status: NTSTATUS) -> WindowsFsError {
match status as u32 {
STATUS_NO_SUCH_FILE | STATUS_OBJECT_NAME_NOT_FOUND => WindowsFsError::NotFound,
STATUS_NOT_A_DIRECTORY | STATUS_FILE_IS_A_DIRECTORY => WindowsFsError::NotADirectory,
STATUS_ACCESS_DENIED => WindowsFsError::AccessDenied,
other => WindowsFsError::IoError(other),
}
}
fn to_utf16_null(s: &str) -> Vec<u16> {
use std::iter::once;
s.encode_utf16().chain(once(0)).collect()
}
fn utf16_slice_to_pathbuf(slice: &[u16]) -> PathBuf {
let end = slice.iter().position(|&c| c == 0).unwrap_or(slice.len());
String::from_utf16_lossy(&slice[..end]).into()
}
pub(crate) fn open_directory_relative(
parent: HANDLE,
name: &str,
) -> Result<OwnedHandle, WindowsFsError> {
debug_assert!(!name.is_empty(), "child name must not be empty");
let name_utf16 = to_utf16_null(name);
let utf16_byte_len = (name_utf16.len() * 2) as u16;
let mut obj_name = NtUnicodeString {
length: utf16_byte_len - 2,
maximum_length: utf16_byte_len,
buffer: name_utf16.as_ptr() as *mut u16,
};
let mut obj_attr = ObjectAttributes {
length: std::mem::size_of::<ObjectAttributes>() as u32,
root_directory: parent,
object_name: &mut obj_name,
attributes: OBJ_CASE_INSENSITIVE,
security_descriptor: ptr::null_mut(),
security_quality_of_service: ptr::null_mut(),
};
let mut handle = INVALID_HANDLE_VALUE;
let mut iosb = IoStatusBlock {
status: 0,
information: 0,
};
let status = unsafe {
NtOpenFile(
&mut handle,
FILE_LIST_DIRECTORY | FILE_READ_ATTRIBUTES | SYNCHRONIZE,
&mut obj_attr,
&mut iosb,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
FILE_DIRECTORY_FILE
| FILE_SYNCHRONOUS_IO_NONALERT
| FILE_OPEN_FOR_BACKUP_INTENT
| FILE_OPEN_REPARSE_POINT,
)
};
if status < 0 {
return Err(ntstatus_to_error(status));
}
Ok(OwnedHandle(handle))
}
pub(crate) fn open_file_relative(
parent: HANDLE,
name: &str,
) -> Result<OwnedHandle, WindowsFsError> {
debug_assert!(!name.is_empty(), "child name must not be empty");
let name_utf16 = to_utf16_null(name);
let utf16_byte_len = (name_utf16.len() * 2) as u16;
let mut obj_name = NtUnicodeString {
length: utf16_byte_len - 2,
maximum_length: utf16_byte_len,
buffer: name_utf16.as_ptr() as *mut u16,
};
let mut obj_attr = ObjectAttributes {
length: std::mem::size_of::<ObjectAttributes>() as u32,
root_directory: parent,
object_name: &mut obj_name,
attributes: OBJ_CASE_INSENSITIVE,
security_descriptor: ptr::null_mut(),
security_quality_of_service: ptr::null_mut(),
};
let mut handle = INVALID_HANDLE_VALUE;
let mut iosb = IoStatusBlock {
status: 0,
information: 0,
};
let status = unsafe {
NtOpenFile(
&mut handle,
FILE_READ_DATA | FILE_READ_ATTRIBUTES | SYNCHRONIZE,
&mut obj_attr,
&mut iosb,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
FILE_NON_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT | FILE_OPEN_REPARSE_POINT,
)
};
if status < 0 {
return Err(ntstatus_to_error(status));
}
Ok(OwnedHandle(handle))
}
fn duplicate_raw_handle(source: HANDLE) -> Result<OwnedHandle, WindowsFsError> {
let mut new_handle = INVALID_HANDLE_VALUE;
let ok = unsafe {
DuplicateHandle(
GetCurrentProcess(),
source,
GetCurrentProcess(),
&mut new_handle,
0,
FALSE,
DUPLICATE_SAME_ACCESS,
)
};
if ok == 0 {
return Err(WindowsFsError::IoError(unsafe { GetLastError() }));
}
Ok(unsafe { OwnedHandle::from_raw(new_handle) })
}
pub(crate) fn resolve_components_relative(
root: HANDLE,
components: &[String],
deny_reparse: bool,
) -> Result<OwnedHandle, WindowsFsError> {
if components.is_empty() {
return duplicate_raw_handle(root);
}
let mut current_raw = root;
let mut intermediates: Vec<OwnedHandle> = Vec::new();
let total = components.len();
for (i, component) in components.iter().enumerate() {
let is_final = i == total - 1;
let child = if is_final {
open_file_relative(current_raw, component)?
} else {
open_directory_relative(current_raw, component)?
};
if !is_final {
let info = get_file_standard_info(child.raw())?;
if info.directory == 0 {
return Err(WindowsFsError::NotADirectory);
}
}
if deny_reparse {
deny_all_reparse_check(child.raw())?;
}
if is_final {
return Ok(child);
}
current_raw = child.raw();
intermediates.push(child);
}
Err(WindowsFsError::NotFound)
}
pub(crate) fn open_any_relative(parent: HANDLE, name: &str) -> Result<OwnedHandle, WindowsFsError> {
match open_file_relative(parent, name) {
Ok(h) => return Ok(h),
Err(WindowsFsError::NotADirectory) => {}
Err(e) => return Err(e),
}
open_directory_relative(parent, name)
}
pub(crate) fn resolve_to_resource(
root: HANDLE,
canonical_root: &std::path::Path,
components: &[String],
deny_reparse: bool,
dotfiles_denied: bool,
) -> super::ResolvedResource {
use super::{ResolvedDirectory, ResolvedFile, ResolvedResource};
debug_assert!(
!root.is_null() && root != INVALID_HANDLE_VALUE,
"root handle must be valid"
);
if components.is_empty() {
let dir_handle = match duplicate_raw_handle(root) {
Ok(h) => h,
Err(_) => return ResolvedResource::NotFound,
};
return ResolvedResource::Directory(ResolvedDirectory {
#[cfg(windows)]
dir_handle,
canonical_path: canonical_root.to_path_buf(),
components: Vec::new(),
});
}
let mut current: Option<OwnedHandle> = None;
let total = components.len();
for (i, component) in components.iter().enumerate() {
let is_final = i == total - 1;
let parent_raw = current.as_ref().map_or(root, |h| h.raw());
if dotfiles_denied && component.starts_with('.') {
return ResolvedResource::Denied(crate::path::PathRejection::DotfileDenied);
}
let child = if is_final {
open_any_relative(parent_raw, component)
} else {
open_directory_relative(parent_raw, component)
};
let child = match child {
Ok(h) => h,
Err(_) => return ResolvedResource::NotFound,
};
if !is_final {
match get_file_standard_info(child.raw()) {
Ok(info) if info.directory == 0 => {
return ResolvedResource::NotFound;
}
Err(_) => return ResolvedResource::NotFound,
_ => {}
}
}
if deny_reparse {
match deny_all_reparse_check(child.raw()) {
Ok(()) => {}
Err(WindowsFsError::ReparsePointDenied) => {
return ResolvedResource::Denied(crate::path::PathRejection::SymlinkDenied);
}
Err(_) => return ResolvedResource::NotFound,
}
}
if is_final {
let is_dir = match get_file_standard_info(child.raw()) {
Ok(info) => info.directory != 0,
Err(_) => false,
};
let canonical_path = match get_final_path(child.raw()) {
Ok(p) => p,
Err(_) => canonical_root.join(component),
};
let safe_components = components.to_vec();
if is_dir {
return ResolvedResource::Directory(ResolvedDirectory {
#[cfg(windows)]
dir_handle: child,
canonical_path,
components: safe_components,
});
} else {
let file_handle = match child.try_clone() {
Ok(h) => h,
Err(_) => return ResolvedResource::NotFound,
};
let std_file = handle_to_std_file(file_handle);
let metadata = match std_file.metadata() {
Ok(m) => m,
Err(_) => return ResolvedResource::NotFound,
};
return ResolvedResource::File(ResolvedFile {
file: std_file,
metadata,
safe_relative_components: safe_components,
});
}
}
current = Some(child);
}
ResolvedResource::NotFound
}
pub(crate) fn resolve_child_relative(
parent_handle: HANDLE,
parent_components: &[String],
child: &str,
deny_reparse: bool,
dotfiles_denied: bool,
) -> super::ResolvedResource {
use super::{ResolvedDirectory, ResolvedFile, ResolvedResource};
debug_assert!(
!parent_handle.is_null() && parent_handle != INVALID_HANDLE_VALUE,
"parent handle must be valid"
);
if dotfiles_denied && child.starts_with('.') {
return ResolvedResource::Denied(crate::path::PathRejection::DotfileDenied);
}
let child_handle = match open_file_relative(parent_handle, child) {
Ok(h) => h,
Err(WindowsFsError::NotADirectory) => {
match open_directory_relative(parent_handle, child) {
Ok(h) => h,
Err(_) => return ResolvedResource::NotFound,
}
}
Err(_) => return ResolvedResource::NotFound,
};
if deny_reparse {
match deny_all_reparse_check(child_handle.raw()) {
Ok(()) => {}
Err(WindowsFsError::ReparsePointDenied) => {
return ResolvedResource::Denied(crate::path::PathRejection::SymlinkDenied);
}
Err(_) => return ResolvedResource::NotFound,
}
}
let is_dir = match get_file_standard_info(child_handle.raw()) {
Ok(info) => info.directory != 0,
Err(_) => false,
};
let mut components = parent_components.to_vec();
components.push(child.to_string());
if is_dir {
ResolvedResource::Directory(ResolvedDirectory {
#[cfg(windows)]
dir_handle: child_handle,
canonical_path: std::path::PathBuf::new(), components,
})
} else {
let std_file = handle_to_std_file(child_handle);
let metadata = match std_file.metadata() {
Ok(m) => m,
Err(_) => return ResolvedResource::NotFound,
};
ResolvedResource::File(ResolvedFile {
file: std_file,
metadata,
safe_relative_components: components,
})
}
}
pub(crate) fn list_directory_handle(
dir_handle: HANDLE,
policy: &crate::policy::StaticPolicy,
max_entries: usize,
) -> Result<Vec<(String, bool)>, std::io::Error> {
let entries = enumerate_directory(dir_handle, max_entries).map_err(std::io::Error::other)?;
let mut result = Vec::new();
for entry in entries {
if policy.dotfiles == DotfilePolicy::Denied && entry.hidden_or_dot {
continue;
}
if policy.symlinks == SymlinkPolicy::Denied
&& entry.kind == DirectoryEntryKind::ReparsePoint
{
continue;
}
if entry.kind == DirectoryEntryKind::Other {
continue;
}
let is_dir = entry.kind == DirectoryEntryKind::Directory;
result.push((entry.name, is_dir));
}
result.sort_by(|a, b| a.0.cmp(&b.0));
Ok(result)
}
pub(crate) fn get_file_attribute_tag(
handle: HANDLE,
) -> Result<FILE_ATTRIBUTE_TAG_INFO, WindowsFsError> {
let mut info = FILE_ATTRIBUTE_TAG_INFO::default();
let ok = unsafe {
GetFileInformationByHandleEx(
handle,
FILE_ATTRIBUTE_TAG_INFO_CLASS,
&mut info as *mut _ as *mut c_void,
std::mem::size_of::<FILE_ATTRIBUTE_TAG_INFO>() as DWORD,
)
};
if ok == 0 {
return Err(last_error_to_fs_error());
}
Ok(info)
}
pub(crate) fn is_reparse_point(handle: HANDLE) -> bool {
match get_file_attribute_tag(handle) {
Ok(info) => (info.file_attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0,
Err(_) => false,
}
}
pub(crate) fn get_reparse_tag(handle: HANDLE) -> Result<u32, WindowsFsError> {
let info = get_file_attribute_tag(handle)?;
Ok(info.reparse_tag)
}
pub(crate) fn deny_all_reparse_check(handle: HANDLE) -> Result<(), WindowsFsError> {
let info = get_file_attribute_tag(handle)?;
if (info.file_attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0 {
return Err(WindowsFsError::ReparsePointDenied);
}
Ok(())
}
pub(crate) fn open_root_handle(path: &std::path::Path) -> Result<OwnedHandle, std::io::Error> {
let path_str = path.to_str().ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"root path is not valid UTF-8",
)
})?;
let path_utf16: Vec<u16> = path_str.encode_utf16().chain(std::iter::once(0)).collect();
unsafe {
let h = CreateFileW(
path_utf16.as_ptr(),
FILE_LIST_DIRECTORY | SYNCHRONIZE,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
ptr::null_mut(),
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
ptr::null_mut(),
);
if h == INVALID_HANDLE_VALUE || h.is_null() {
return Err(std::io::Error::last_os_error());
}
Ok(OwnedHandle(h))
}
}
pub(crate) fn get_file_standard_info(handle: HANDLE) -> Result<FILE_STANDARD_INFO, WindowsFsError> {
let mut info = FILE_STANDARD_INFO::default();
let ok = unsafe {
GetFileInformationByHandleEx(
handle,
FILE_STANDARD_INFO_CLASS,
&mut info as *mut _ as *mut c_void,
std::mem::size_of::<FILE_STANDARD_INFO>() as DWORD,
)
};
if ok == 0 {
return Err(last_error_to_fs_error());
}
Ok(info)
}
pub(crate) fn get_file_id(handle: HANDLE) -> Result<u64, WindowsFsError> {
const BUFFER_SIZE: usize = 80 + 256 * 2;
let mut buffer = vec![0u8; BUFFER_SIZE];
let ok = unsafe {
GetFileInformationByHandleEx(
handle,
10, buffer.as_mut_ptr() as *mut c_void,
BUFFER_SIZE as DWORD,
)
};
if ok == 0 {
return Err(last_error_to_fs_error());
}
let file_id = unsafe {
let header = buffer.as_ptr() as *const FILE_ID_BOTH_DIR_INFO;
(*header).file_index
};
Ok(file_id)
}
pub(crate) fn get_final_path(handle: HANDLE) -> Result<PathBuf, WindowsFsError> {
let mut buf_size: DWORD = 260;
let mut buffer: Vec<u16> = vec![0; buf_size as usize];
loop {
let len = unsafe { GetFinalPathNameByHandleW(handle, buffer.as_mut_ptr(), buf_size, 0) };
if len == 0 {
return Err(last_error_to_fs_error());
}
if len < buf_size {
let path_slice = &buffer[..len as usize];
return Ok(utf16_slice_to_pathbuf(path_slice));
}
buf_size = len + 1;
buffer.resize(buf_size as usize, 0);
}
}
pub(crate) fn handle_to_std_file(handle: OwnedHandle) -> std::fs::File {
assert!(
handle.is_valid(),
"handle_to_std_file called with invalid handle"
);
let raw = handle.raw();
std::mem::forget(handle);
unsafe { std::fs::File::from_raw_handle(raw as RawHandle) }
}
pub(crate) fn verify_handle_not_closed_after_conversion(handle: &OwnedHandle) -> bool {
handle.is_valid()
}
#[derive(Debug, Clone)]
pub struct DirectoryEntryRecord {
pub name: String,
pub kind: DirectoryEntryKind,
pub file_id: Option<u64>,
pub hidden_or_dot: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DirectoryEntryKind {
File,
Directory,
ReparsePoint,
Other,
}
#[derive(Debug)]
pub enum DirBufParseError {
BufferOverflow,
TruncatedHeader,
OddFileNameLength,
FileNameOutOfRange,
OffsetUnderflow,
OffsetOverflow,
OffsetLoop,
InvalidUtf16,
}
pub fn parse_directory_buffer(
buffer: &[u8],
max_entries: usize,
) -> Result<Vec<DirectoryEntryRecord>, DirBufParseError> {
let mut entries = Vec::new();
let mut offset: usize = 0;
let total_len = buffer.len();
loop {
if offset >= total_len {
break;
}
if offset + FILE_ID_BOTH_DIR_INFO_HEADER_SIZE > total_len {
return Err(DirBufParseError::TruncatedHeader);
}
let next_entry_offset = u32::from_ne_bytes([
buffer[offset],
buffer[offset + 1],
buffer[offset + 2],
buffer[offset + 3],
]) as usize;
let name_length_offset = offset + FILE_ID_BOTH_DIR_INFO_FILE_NAME_LENGTH_OFFSET;
let file_name_length = u32::from_ne_bytes([
buffer[name_length_offset],
buffer[name_length_offset + 1],
buffer[name_length_offset + 2],
buffer[name_length_offset + 3],
]) as usize;
if !file_name_length.is_multiple_of(2) {
return Err(DirBufParseError::OddFileNameLength);
}
let name_start = offset + FILE_ID_BOTH_DIR_INFO_HEADER_SIZE;
let name_end = name_start + file_name_length;
if name_end > total_len {
return Err(DirBufParseError::FileNameOutOfRange);
}
let attrs_offset = offset + FILE_ID_BOTH_DIR_INFO_FILE_ATTRIBUTES_OFFSET;
let file_attributes = u32::from_ne_bytes([
buffer[attrs_offset],
buffer[attrs_offset + 1],
buffer[attrs_offset + 2],
buffer[attrs_offset + 3],
]);
let file_index_offset = offset + FILE_ID_BOTH_DIR_INFO_FILE_ID_OFFSET;
let file_index = u64::from_ne_bytes([
buffer[file_index_offset],
buffer[file_index_offset + 1],
buffer[file_index_offset + 2],
buffer[file_index_offset + 3],
buffer[file_index_offset + 4],
buffer[file_index_offset + 5],
buffer[file_index_offset + 6],
buffer[file_index_offset + 7],
]);
let name_u16: Vec<u16> = (0..file_name_length / 2)
.map(|i| {
let idx = name_start + i * 2;
u16::from_ne_bytes([buffer[idx], buffer[idx + 1]])
})
.collect();
let name = String::from_utf16(&name_u16).map_err(|_| DirBufParseError::InvalidUtf16)?;
if name == "." || name == ".." {
if next_entry_offset == 0 {
break;
}
let next_offset = offset
.checked_add(next_entry_offset)
.ok_or(DirBufParseError::OffsetOverflow)?;
if next_offset <= offset || next_offset >= total_len {
return Err(DirBufParseError::OffsetOverflow);
}
offset = next_offset;
continue;
}
let is_directory = (file_attributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
let is_reparse = (file_attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0;
let kind = if is_reparse {
DirectoryEntryKind::ReparsePoint
} else if is_directory {
DirectoryEntryKind::Directory
} else {
DirectoryEntryKind::File
};
let hidden_or_dot = name.starts_with('.');
entries.push(DirectoryEntryRecord {
name,
kind,
file_id: Some(file_index),
hidden_or_dot,
});
if entries.len() >= max_entries {
break;
}
if next_entry_offset == 0 {
break;
}
let next_offset = offset
.checked_add(next_entry_offset)
.ok_or(DirBufParseError::OffsetOverflow)?;
if next_offset <= offset || next_offset >= total_len {
return Err(DirBufParseError::OffsetOverflow);
}
offset = next_offset;
}
Ok(entries)
}
const DIR_ENUM_BUFFER_SIZE: usize = 64 * 1024;
#[derive(Debug)]
pub(crate) struct DirectoryEntryPathBased {
pub name: String,
pub is_directory: bool,
pub is_reparse_point: bool,
pub file_size: u64,
}
pub(crate) fn enumerate_directory_path_based(
handle: HANDLE,
) -> Result<Vec<DirectoryEntryPathBased>, WindowsFsError> {
let dir_path = get_final_path(handle)?;
let mut pattern = dir_path;
pattern.push("*");
let pattern_utf16 = to_utf16_null(pattern.to_str().unwrap_or(""));
let mut find_data = WIN32_FIND_DATAW::default();
let find_handle = unsafe { FindFirstFileW(pattern_utf16.as_ptr(), &mut find_data) };
if find_handle == INVALID_HANDLE_VALUE || find_handle.is_null() {
let err = unsafe { GetLastError() };
if err == ERROR_FILE_NOT_FOUND {
return Ok(Vec::new());
}
return Err(last_error_to_fs_error());
}
let mut entries = Vec::new();
loop {
let name = utf16_slice_to_pathbuf(&find_data.c_file_name)
.to_string_lossy()
.into_owned();
if name != "." && name != ".." {
let file_size =
((find_data.n_file_size_high as u64) << 32) | (find_data.n_file_size_low as u64);
entries.push(DirectoryEntryPathBased {
name,
is_directory: (find_data.dw_file_attributes & FILE_ATTRIBUTE_DIRECTORY) != 0,
is_reparse_point: (find_data.dw_file_attributes & FILE_ATTRIBUTE_REPARSE_POINT)
!= 0,
file_size,
});
}
let ok = unsafe { FindNextFileW(find_handle, &mut find_data) };
if ok == 0 {
break;
}
}
unsafe {
FindClose(find_handle);
}
entries.sort_by(|a, b| a.name.cmp(&b.name));
Ok(entries)
}
pub(crate) fn enumerate_directory(
handle: HANDLE,
max_entries: usize,
) -> Result<Vec<DirectoryEntryRecord>, WindowsFsError> {
let mut buffer = vec![0u8; DIR_ENUM_BUFFER_SIZE];
let mut all_entries = Vec::new();
let mut first_call = true;
loop {
if all_entries.len() >= max_entries {
break;
}
let mut io_status = IoStatusBlock {
status: 0,
information: 0,
};
let restart_scan = if first_call { TRUE } else { FALSE };
let status = unsafe {
NtQueryDirectoryFile(
handle,
ptr::null_mut(), ptr::null_mut(), ptr::null_mut(), &mut io_status,
buffer.as_mut_ptr() as *mut c_void,
DIR_ENUM_BUFFER_SIZE as DWORD,
FILE_ID_BOTH_DIRECTORY_INFO,
FALSE, ptr::null(), restart_scan,
)
};
first_call = false;
if status as u32 == STATUS_NO_MORE_FILES {
break;
}
if status < 0 {
return Err(WindowsFsError::IoError(status as u32));
}
let bytes_returned = io_status.information;
if bytes_returned == 0 || bytes_returned > DIR_ENUM_BUFFER_SIZE {
break;
}
let remaining = max_entries.saturating_sub(all_entries.len());
let parsed = parse_directory_buffer(&buffer[..bytes_returned], remaining)
.map_err(|_| WindowsFsError::IoError(0xBAADF00D))?;
let count = parsed.len();
all_entries.extend(parsed);
if count == 0 {
break;
}
}
Ok(all_entries)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn setup_test_root() -> (TempDir, OwnedHandle) {
let tmp = TempDir::new().unwrap();
let root = tmp.path().to_path_buf();
std::fs::create_dir_all(root.join("subdir")).unwrap();
std::fs::write(root.join("hello.txt"), "hello").unwrap();
std::fs::write(root.join("subdir").join("file.txt"), "nested").unwrap();
let root_path_utf16 = to_utf16_null(root.to_str().unwrap());
let handle = unsafe {
CreateFileW(
root_path_utf16.as_ptr(),
FILE_LIST_DIRECTORY | SYNCHRONIZE,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
ptr::null_mut(),
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
ptr::null_mut(),
)
};
assert_ne!(handle, INVALID_HANDLE_VALUE);
(tmp, OwnedHandle(handle))
}
#[test]
fn open_directory_relative_succeeds() {
let (_tmp, root_handle) = setup_test_root();
let result = open_directory_relative(root_handle.raw(), "subdir");
assert!(
result.is_ok(),
"open_directory_relative failed: {:?}",
result.err()
);
}
#[test]
fn open_file_relative_succeeds() {
let (_tmp, root_handle) = setup_test_root();
let result = open_file_relative(root_handle.raw(), "hello.txt");
assert!(
result.is_ok(),
"open_file_relative failed: {:?}",
result.err()
);
}
#[test]
fn open_relative_not_found() {
let (_tmp, root_handle) = setup_test_root();
let result = open_file_relative(root_handle.raw(), "nonexistent.txt");
assert!(matches!(result, Err(WindowsFsError::NotFound)));
}
#[test]
fn open_directory_as_file_fails() {
let (_tmp, root_handle) = setup_test_root();
let result = open_file_relative(root_handle.raw(), "subdir");
assert!(
matches!(result, Err(WindowsFsError::NotADirectory)),
"opening a directory without BACKUP_SEMANTICS should fail with NotADirectory, got {:?}",
result
);
}
#[test]
fn resolve_nested_components() {
let (_tmp, root_handle) = setup_test_root();
let components: Vec<String> = vec!["subdir".into(), "file.txt".into()];
let result = resolve_components_relative(root_handle.raw(), &components, true);
assert!(
result.is_ok(),
"resolve_components_relative failed: {:?}",
result.err()
);
}
#[test]
fn resolve_intermediate_not_directory_fails() {
let (_tmp, root_handle) = setup_test_root();
let components: Vec<String> = vec!["hello.txt".into(), "impossible".into()];
let result = resolve_components_relative(root_handle.raw(), &components, true);
assert!(
matches!(result, Err(WindowsFsError::NotADirectory)),
"intermediate file should fail with NotADirectory, got {:?}",
result
);
}
#[test]
fn get_file_standard_info_directory() {
let (_tmp, root_handle) = setup_test_root();
let dir_handle = open_directory_relative(root_handle.raw(), "subdir").unwrap();
let info = get_file_standard_info(dir_handle.raw());
assert!(info.is_ok());
let info = info.unwrap();
assert_ne!(info.directory, 0);
}
#[test]
fn get_file_standard_info_file() {
let (_tmp, root_handle) = setup_test_root();
let file_handle = open_file_relative(root_handle.raw(), "hello.txt").unwrap();
let info = get_file_standard_info(file_handle.raw());
assert!(info.is_ok());
let info = info.unwrap();
assert_eq!(info.directory, 0);
assert_eq!(info.end_of_file, 5); }
#[test]
fn get_final_path_succeeds() {
let (_tmp, root_handle) = setup_test_root();
let path = get_final_path(root_handle.raw());
assert!(path.is_ok(), "get_final_path failed: {:?}", path.err());
let path = path.unwrap();
assert!(path.exists(), "final path should exist on disk: {:?}", path);
}
#[test]
fn get_file_id_succeeds() {
let (_tmp, root_handle) = setup_test_root();
let id = get_file_id(root_handle.raw());
assert!(id.is_ok(), "get_file_id failed: {:?}", id.err());
}
#[test]
fn owned_handle_try_clone_and_drop() {
let (_tmp, root_handle) = setup_test_root();
let cloned = root_handle.try_clone().unwrap();
assert!(cloned.is_valid());
drop(cloned);
assert!(root_handle.is_valid());
}
#[test]
#[ignore = "NtQueryDirectoryFile STATUS_INFO_LENGTH_MISMATCH on CI runners; needs Windows VM qualification"]
fn enumerate_directory_entries() {
let (_tmp, root_handle) = setup_test_root();
let entries = enumerate_directory(root_handle.raw(), 4096);
assert!(
entries.is_ok(),
"enumerate_directory failed: {:?}",
entries.err()
);
let entries = entries.unwrap();
let names: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect();
assert!(
names.contains(&"hello.txt"),
"expected hello.txt in entries, got {:?}",
names
);
assert!(
names.contains(&"subdir"),
"expected subdir in entries, got {:?}",
names
);
assert!(!names.contains(&"."), ". should be filtered");
assert!(!names.contains(&".."), ".. should be filtered");
}
#[test]
#[ignore = "NtQueryDirectoryFile STATUS_INFO_LENGTH_MISMATCH on CI runners; needs Windows VM qualification"]
fn enumerate_directory_subdir() {
let (_tmp, root_handle) = setup_test_root();
let dir_handle = open_directory_relative(root_handle.raw(), "subdir").unwrap();
let entries = enumerate_directory(dir_handle.raw(), 4096);
assert!(entries.is_ok());
let entries = entries.unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].name, "file.txt");
assert_eq!(entries[0].kind, DirectoryEntryKind::File);
}
#[test]
fn enumerate_directory_path_based_entries() {
let (_tmp, root_handle) = setup_test_root();
let entries = enumerate_directory_path_based(root_handle.raw());
assert!(
entries.is_ok(),
"enumerate_directory_path_based failed: {:?}",
entries.err()
);
let entries = entries.unwrap();
let names: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect();
assert!(
names.contains(&"hello.txt"),
"expected hello.txt in entries, got {:?}",
names
);
assert!(
names.contains(&"subdir"),
"expected subdir in entries, got {:?}",
names
);
assert!(!names.contains(&"."), ". should be filtered");
assert!(!names.contains(&".."), ".. should be filtered");
}
#[test]
fn no_reparse_on_regular_files() {
let (_tmp, root_handle) = setup_test_root();
let file_handle = open_file_relative(root_handle.raw(), "hello.txt").unwrap();
assert!(!is_reparse_point(file_handle.raw()));
let tag = get_reparse_tag(file_handle.raw()).unwrap();
assert_eq!(tag, 0);
assert!(deny_all_reparse_check(file_handle.raw()).is_ok());
}
#[test]
fn deny_all_reparse_check_on_regular_dir() {
let (_tmp, root_handle) = setup_test_root();
let dir_handle = open_directory_relative(root_handle.raw(), "subdir").unwrap();
assert!(deny_all_reparse_check(dir_handle.raw()).is_ok());
}
#[test]
fn handle_to_std_file_read() {
let (_tmp, root_handle) = setup_test_root();
let file_handle = open_file_relative(root_handle.raw(), "hello.txt").unwrap();
let std_file = handle_to_std_file(file_handle);
let mut contents = String::new();
std::io::Read::read_to_string(&mut std::io::BufReader::new(std_file), &mut contents)
.unwrap();
assert_eq!(contents, "hello");
}
#[test]
fn resolve_components_relative_empty() {
let (_tmp, root_handle) = setup_test_root();
let result = resolve_components_relative(root_handle.raw(), &[], true);
assert!(result.is_ok());
let handle = result.unwrap();
assert!(handle.is_valid());
}
#[test]
fn utf16_conversion_roundtrip() {
let s = "hello_world.txt";
let utf16 = to_utf16_null(s);
assert_eq!(*utf16.last().unwrap(), 0);
let decoded = utf16_slice_to_pathbuf(&utf16[..utf16.len() - 1]);
assert_eq!(decoded.to_str().unwrap(), s);
}
#[test]
fn last_error_maps_correctly() {
}
#[test]
fn owned_handle_invalid_try_clone() {
let invalid = OwnedHandle(INVALID_HANDLE_VALUE);
let cloned = invalid.try_clone().unwrap();
assert!(!cloned.is_valid());
}
fn build_dir_info_entry(
name: &str,
is_directory: bool,
is_reparse: bool,
next_entry_offset: u32,
file_id: u64,
) -> Vec<u8> {
let name_utf16: Vec<u16> = name.encode_utf16().collect();
let name_bytes = name_utf16.len() * 2;
let record_size = FILE_ID_BOTH_DIR_INFO_HEADER_SIZE + name_bytes;
let aligned_size = (record_size + 7) & !7;
let mut buf = vec![0u8; aligned_size];
buf[0..4].copy_from_slice(&next_entry_offset.to_ne_bytes());
buf[FILE_ID_BOTH_DIR_INFO_FILE_ID_OFFSET..FILE_ID_BOTH_DIR_INFO_FILE_ID_OFFSET + 8]
.copy_from_slice(&file_id.to_ne_bytes());
let mut attrs: u32 = 0;
if is_directory {
attrs |= FILE_ATTRIBUTE_DIRECTORY;
}
if is_reparse {
attrs |= FILE_ATTRIBUTE_REPARSE_POINT;
}
buf[FILE_ID_BOTH_DIR_INFO_FILE_ATTRIBUTES_OFFSET
..FILE_ID_BOTH_DIR_INFO_FILE_ATTRIBUTES_OFFSET + 4]
.copy_from_slice(&attrs.to_ne_bytes());
buf[FILE_ID_BOTH_DIR_INFO_FILE_NAME_LENGTH_OFFSET
..FILE_ID_BOTH_DIR_INFO_FILE_NAME_LENGTH_OFFSET + 4]
.copy_from_slice(&(name_bytes as u32).to_ne_bytes());
for (i, &ch) in name_utf16.iter().enumerate() {
let idx = FILE_ID_BOTH_DIR_INFO_FILE_NAME_OFFSET + i * 2;
buf[idx..idx + 2].copy_from_slice(&ch.to_ne_bytes());
}
buf
}
#[test]
fn parse_single_entry() {
let entry = build_dir_info_entry("hello.txt", false, false, 0, 42);
let result = parse_directory_buffer(&entry, 100).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].name, "hello.txt");
assert_eq!(result[0].kind, DirectoryEntryKind::File);
assert_eq!(result[0].file_id, Some(42));
assert!(!result[0].hidden_or_dot);
}
#[test]
fn parse_multiple_entries() {
let mut buf = build_dir_info_entry("a.txt", false, false, 0, 1);
let entry2 = build_dir_info_entry("b.txt", false, false, 0, 2);
let offset2 = buf.len() as u32;
buf[0..4].copy_from_slice(&offset2.to_ne_bytes());
buf.extend_from_slice(&entry2);
let result = parse_directory_buffer(&buf, 100).unwrap();
assert_eq!(result.len(), 2);
assert_eq!(result[0].name, "a.txt");
assert_eq!(result[1].name, "b.txt");
}
#[test]
fn parse_skips_dot_and_dotdot() {
let mut buf = build_dir_info_entry(".", true, false, 0, 1);
let entry2 = build_dir_info_entry("..", true, false, 0, 2);
let offset2 = buf.len() as u32;
buf[0..4].copy_from_slice(&offset2.to_ne_bytes());
buf.extend_from_slice(&entry2);
let result = parse_directory_buffer(&buf, 100).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn parse_directory_entry() {
let entry = build_dir_info_entry("subdir", true, false, 0, 10);
let result = parse_directory_buffer(&entry, 100).unwrap();
assert_eq!(result[0].kind, DirectoryEntryKind::Directory);
}
#[test]
fn parse_reparse_entry() {
let entry = build_dir_info_entry("link", false, true, 0, 20);
let result = parse_directory_buffer(&entry, 100).unwrap();
assert_eq!(result[0].kind, DirectoryEntryKind::ReparsePoint);
}
#[test]
fn parse_dotfile() {
let entry = build_dir_info_entry(".hidden", false, false, 0, 30);
let result = parse_directory_buffer(&entry, 100).unwrap();
assert!(result[0].hidden_or_dot);
}
#[test]
fn parse_empty_buffer() {
let result = parse_directory_buffer(&[], 100).unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn parse_truncated_header() {
let buf = vec![0u8; 4];
let result = parse_directory_buffer(&buf, 100);
assert!(matches!(result, Err(DirBufParseError::TruncatedHeader)));
}
#[test]
fn parse_odd_filename_length() {
let mut buf = vec![0u8; FILE_ID_BOTH_DIR_INFO_HEADER_SIZE + 10];
buf[0..4].copy_from_slice(&0u32.to_ne_bytes());
buf[FILE_ID_BOTH_DIR_INFO_FILE_NAME_LENGTH_OFFSET
..FILE_ID_BOTH_DIR_INFO_FILE_NAME_LENGTH_OFFSET + 4]
.copy_from_slice(&5u32.to_ne_bytes());
let result = parse_directory_buffer(&buf, 100);
assert!(matches!(result, Err(DirBufParseError::OddFileNameLength)));
}
#[test]
fn parse_filename_out_of_range() {
let mut buf = vec![0u8; FILE_ID_BOTH_DIR_INFO_HEADER_SIZE + 4];
buf[0..4].copy_from_slice(&0u32.to_ne_bytes());
buf[FILE_ID_BOTH_DIR_INFO_FILE_NAME_LENGTH_OFFSET
..FILE_ID_BOTH_DIR_INFO_FILE_NAME_LENGTH_OFFSET + 4]
.copy_from_slice(&100u32.to_ne_bytes());
let result = parse_directory_buffer(&buf, 100);
assert!(matches!(result, Err(DirBufParseError::FileNameOutOfRange)));
}
#[test]
fn parse_offset_overflow() {
let entry = build_dir_info_entry("a.txt", false, false, 9999, 1);
let result = parse_directory_buffer(&entry, 100);
assert!(matches!(result, Err(DirBufParseError::OffsetOverflow)));
}
#[test]
#[ignore = "Parse offset behavior differs from expected; needs Windows VM qualification"]
fn parse_offset_loop() {
let mut buf = build_dir_info_entry("a.txt", false, false, 0, 1);
let entry2 = build_dir_info_entry("b.txt", false, false, 0, 2);
let offset2 = buf.len() as u32;
buf[0..4].copy_from_slice(&offset2.to_ne_bytes());
buf.extend_from_slice(&entry2);
let loop_offset = 0u32;
let pos = offset2 as usize;
buf[pos..pos + 4].copy_from_slice(&loop_offset.to_ne_bytes());
let result = parse_directory_buffer(&buf, 100);
assert!(matches!(result, Err(DirBufParseError::OffsetUnderflow)));
}
#[test]
fn parse_max_entries_respected() {
let mut entries_data = Vec::new();
for i in 0..10u64 {
entries_data.push(build_dir_info_entry(
&format!("file{i}.txt"),
false,
false,
0,
i,
));
}
let mut buf = Vec::new();
for (i, entry) in entries_data.iter().enumerate() {
if i < entries_data.len() - 1 {
let next_offset = entry.len() as u32;
let mut entry_clone = entry.clone();
entry_clone[0..4].copy_from_slice(&next_offset.to_ne_bytes());
buf.extend_from_slice(&entry_clone);
} else {
buf.extend_from_slice(entry);
}
}
let result = parse_directory_buffer(&buf, 3).unwrap();
assert_eq!(result.len(), 3);
}
#[test]
fn parse_zero_length_filename() {
let entry = build_dir_info_entry("", false, false, 0, 1);
let result = parse_directory_buffer(&entry, 100).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].name, "");
assert_eq!(result[0].kind, DirectoryEntryKind::File);
}
#[test]
fn parse_offset_underflow() {
let mut buf = build_dir_info_entry("a.txt", false, false, 0, 1);
let entry2 = build_dir_info_entry("b.txt", false, false, 0, 2);
let offset2 = buf.len() as u32;
buf[0..4].copy_from_slice(&offset2.to_ne_bytes());
buf.extend_from_slice(&entry2);
let underflow_offset = 1u32;
let pos = offset2 as usize;
buf[pos..pos + 4].copy_from_slice(&underflow_offset.to_ne_bytes());
let result = parse_directory_buffer(&buf, 100);
assert!(matches!(result, Err(DirBufParseError::OffsetUnderflow)));
}
#[test]
fn parse_truncated_filename() {
let mut buf = vec![0u8; FILE_ID_BOTH_DIR_INFO_HEADER_SIZE + 4];
buf[0..4].copy_from_slice(&0u32.to_ne_bytes());
buf[FILE_ID_BOTH_DIR_INFO_FILE_NAME_LENGTH_OFFSET
..FILE_ID_BOTH_DIR_INFO_FILE_NAME_LENGTH_OFFSET + 4]
.copy_from_slice(&100u32.to_ne_bytes());
let result = parse_directory_buffer(&buf, 100);
assert!(matches!(result, Err(DirBufParseError::FileNameOutOfRange)));
}
#[test]
fn parse_unpaired_surrogate() {
let name_utf16: Vec<u16> = vec![0x0041, 0xD800, 0x0042]; let name_bytes = name_utf16.len() * 2;
let record_size = FILE_ID_BOTH_DIR_INFO_HEADER_SIZE + name_bytes;
let aligned_size = (record_size + 7) & !7;
let mut buf = vec![0u8; aligned_size];
buf[0..4].copy_from_slice(&0u32.to_ne_bytes());
buf[FILE_ID_BOTH_DIR_INFO_FILE_NAME_LENGTH_OFFSET
..FILE_ID_BOTH_DIR_INFO_FILE_NAME_LENGTH_OFFSET + 4]
.copy_from_slice(&(name_bytes as u32).to_ne_bytes());
for (i, &ch) in name_utf16.iter().enumerate() {
let idx = FILE_ID_BOTH_DIR_INFO_FILE_NAME_OFFSET + i * 2;
buf[idx..idx + 2].copy_from_slice(&ch.to_ne_bytes());
}
let result = parse_directory_buffer(&buf, 100);
assert!(matches!(result, Err(DirBufParseError::InvalidUtf16)));
}
#[test]
fn parse_max_filename_length() {
let name: String = (0..255)
.map(|i| char::from_u32(0x41 + (i % 26)).unwrap())
.collect();
let entry = build_dir_info_entry(&name, false, false, 0, 1);
let result = parse_directory_buffer(&entry, 100).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].name, name);
}
#[test]
#[ignore = "Parse offset behavior differs from expected; needs Windows VM qualification"]
fn parse_offset_before_current_record_end() {
let entry1 = build_dir_info_entry("a.txt", false, false, 0, 1);
let entry2 = build_dir_info_entry("b.txt", false, false, 0, 2);
let mut buf = entry1.clone();
let entry2_start = buf.len();
buf.extend_from_slice(&entry2);
let bad_offset = (entry2_start + 4) as u32;
buf[0..4].copy_from_slice(&bad_offset.to_ne_bytes());
let result = parse_directory_buffer(&buf, 100);
assert!(result.is_ok());
assert_eq!(result.unwrap().len(), 1);
}
#[test]
#[ignore = "Race condition timing differs on CI; needs Windows VM qualification"]
fn race_file_to_reparse_point_denied() {
let tmp = TempDir::new().unwrap();
let root_path = tmp.path().to_path_buf();
std::fs::write(root_path.join("target.txt"), "original").unwrap();
let root_utf16 = to_utf16_null(root_path.to_str().unwrap());
let root_handle = unsafe {
CreateFileW(
root_utf16.as_ptr(),
FILE_LIST_DIRECTORY | SYNCHRONIZE,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
ptr::null_mut(),
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
ptr::null_mut(),
)
};
assert_ne!(root_handle, INVALID_HANDLE_VALUE);
let entries = enumerate_directory(root_handle, 4096).unwrap();
assert!(entries.iter().any(|e| e.name == "target.txt"));
let entry = entries.iter().find(|e| e.name == "target.txt").unwrap();
assert_eq!(entry.kind, DirectoryEntryKind::File);
let outside = TempDir::new().unwrap();
std::fs::write(outside.path().join("secret.txt"), "leaked").unwrap();
std::fs::remove_file(root_path.join("target.txt")).unwrap();
std::os::windows::fs::symlink_dir(outside.path(), root_path.join("target.txt")).unwrap();
let result = open_any_relative(root_handle, "target.txt");
match result {
Ok(h) => {
let check = deny_all_reparse_check(h.raw());
assert!(
check.is_err(),
"reparse point should be denied after file-to-reparse swap"
);
}
Err(WindowsFsError::NotFound) => {
}
Err(e) => {
eprintln!("open returned error (safe): {e:?}");
}
}
unsafe {
CloseHandle(root_handle);
}
}
#[test]
#[ignore = "Race condition timing differs on CI; needs Windows VM qualification"]
fn race_file_to_directory_type_change() {
let tmp = TempDir::new().unwrap();
let root_path = tmp.path().to_path_buf();
std::fs::write(root_path.join("target.txt"), "original").unwrap();
let root_utf16 = to_utf16_null(root_path.to_str().unwrap());
let root_handle = unsafe {
CreateFileW(
root_utf16.as_ptr(),
FILE_LIST_DIRECTORY | SYNCHRONIZE,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
ptr::null_mut(),
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
ptr::null_mut(),
)
};
assert_ne!(root_handle, INVALID_HANDLE_VALUE);
let entries = enumerate_directory(root_handle, 4096).unwrap();
let entry = entries.iter().find(|e| e.name == "target.txt");
assert!(entry.is_some(), "target.txt should be in listing");
assert_eq!(entry.unwrap().kind, DirectoryEntryKind::File);
std::fs::remove_file(root_path.join("target.txt")).unwrap();
std::fs::create_dir(root_path.join("target.txt")).unwrap();
let result = open_file_relative(root_handle, "target.txt");
assert!(
matches!(result, Err(WindowsFsError::NotADirectory)),
"opening a directory as file should fail with NotADirectory after type change, got {:?}",
result
);
let result = open_directory_relative(root_handle, "target.txt");
assert!(
result.is_ok(),
"opening a directory as directory should succeed after type change, got {:?}",
result
);
unsafe {
CloseHandle(root_handle);
}
}
#[test]
fn race_same_name_replacement_file() {
let tmp = TempDir::new().unwrap();
let root_path = tmp.path().to_path_buf();
std::fs::write(root_path.join("target.txt"), "original").unwrap();
let root_utf16 = to_utf16_null(root_path.to_str().unwrap());
let root_handle = unsafe {
CreateFileW(
root_utf16.as_ptr(),
FILE_LIST_DIRECTORY | SYNCHRONIZE,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
ptr::null_mut(),
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
ptr::null_mut(),
)
};
assert_ne!(root_handle, INVALID_HANDLE_VALUE);
let file_handle = open_file_relative(root_handle, "target.txt").unwrap();
let std_file = handle_to_std_file(file_handle);
let mut contents = String::new();
std::io::Read::read_to_string(&mut std::io::BufReader::new(std_file), &mut contents)
.unwrap();
assert_eq!(contents, "original");
std::fs::remove_file(root_path.join("target.txt")).unwrap();
std::fs::write(root_path.join("target.txt"), "replaced").unwrap();
let file_handle = open_file_relative(root_handle, "target.txt").unwrap();
let std_file = handle_to_std_file(file_handle);
let mut contents = String::new();
std::io::Read::read_to_string(&mut std::io::BufReader::new(std_file), &mut contents)
.unwrap();
assert_eq!(contents, "replaced");
unsafe {
CloseHandle(root_handle);
}
}
#[test]
#[ignore = "Race condition timing differs on CI; needs Windows VM qualification"]
fn race_delete_and_recreate() {
let tmp = TempDir::new().unwrap();
let root_path = tmp.path().to_path_buf();
std::fs::write(root_path.join("target.txt"), "original").unwrap();
let root_utf16 = to_utf16_null(root_path.to_str().unwrap());
let root_handle = unsafe {
CreateFileW(
root_utf16.as_ptr(),
FILE_LIST_DIRECTORY | SYNCHRONIZE,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
ptr::null_mut(),
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
ptr::null_mut(),
)
};
assert_ne!(root_handle, INVALID_HANDLE_VALUE);
let entries = enumerate_directory(root_handle, 4096).unwrap();
assert!(entries.iter().any(|e| e.name == "target.txt"));
std::fs::remove_file(root_path.join("target.txt")).unwrap();
let result = open_file_relative(root_handle, "target.txt");
assert!(
matches!(result, Err(WindowsFsError::NotFound)),
"opening deleted file should return NotFound, got {:?}",
result
);
std::fs::write(root_path.join("target.txt"), "recreated").unwrap();
let file_handle = open_file_relative(root_handle, "target.txt").unwrap();
let std_file = handle_to_std_file(file_handle);
let mut contents = String::new();
std::io::Read::read_to_string(&mut std::io::BufReader::new(std_file), &mut contents)
.unwrap();
assert_eq!(contents, "recreated");
unsafe {
CloseHandle(root_handle);
}
}
#[test]
#[ignore = "Race condition timing differs on CI; needs Windows VM qualification"]
fn race_permission_change_after_enumeration() {
let tmp = TempDir::new().unwrap();
let root_path = tmp.path().to_path_buf();
std::fs::write(root_path.join("target.txt"), "content").unwrap();
let root_utf16 = to_utf16_null(root_path.to_str().unwrap());
let root_handle = unsafe {
CreateFileW(
root_utf16.as_ptr(),
FILE_LIST_DIRECTORY | SYNCHRONIZE,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
ptr::null_mut(),
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
ptr::null_mut(),
)
};
assert_ne!(root_handle, INVALID_HANDLE_VALUE);
let entries = enumerate_directory(root_handle, 4096).unwrap();
assert!(entries.iter().any(|e| e.name == "target.txt"));
let mut perms = std::fs::metadata(root_path.join("target.txt"))
.unwrap()
.permissions();
perms.set_readonly(true);
std::fs::set_permissions(root_path.join("target.txt"), perms).unwrap();
let result = open_file_relative(root_handle, "target.txt");
assert!(
result.is_ok(),
"opening a read-only file should succeed, got {:?}",
result
);
let mut perms = std::fs::metadata(root_path.join("target.txt"))
.unwrap()
.permissions();
#[allow(clippy::permissions_set_readonly_false)]
{
perms.set_readonly(false);
}
std::fs::set_permissions(root_path.join("target.txt"), perms).unwrap();
let file_handle = open_file_relative(root_handle, "target.txt").unwrap();
let std_file = handle_to_std_file(file_handle);
let mut contents = String::new();
std::io::Read::read_to_string(&mut std::io::BufReader::new(std_file), &mut contents)
.unwrap();
assert_eq!(contents, "content");
unsafe {
CloseHandle(root_handle);
}
}
#[test]
#[ignore = "Race condition timing differs on CI; needs Windows VM qualification"]
fn race_directory_entry_count_stability() {
let tmp = TempDir::new().unwrap();
let root_path = tmp.path().to_path_buf();
for i in 0..10 {
std::fs::write(
root_path.join(format!("file{i}.txt")),
format!("content{i}"),
)
.unwrap();
}
let root_utf16 = to_utf16_null(root_path.to_str().unwrap());
let root_handle = unsafe {
CreateFileW(
root_utf16.as_ptr(),
FILE_LIST_DIRECTORY | SYNCHRONIZE,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
ptr::null_mut(),
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
ptr::null_mut(),
)
};
assert_ne!(root_handle, INVALID_HANDLE_VALUE);
let entries1 = enumerate_directory(root_handle, 4096).unwrap();
let entries2 = enumerate_directory(root_handle, 4096).unwrap();
let entries3 = enumerate_directory(root_handle, 4096).unwrap();
assert_eq!(entries1.len(), entries2.len());
assert_eq!(entries2.len(), entries3.len());
let names1: Vec<&str> = entries1.iter().map(|e| e.name.as_str()).collect();
let names2: Vec<&str> = entries2.iter().map(|e| e.name.as_str()).collect();
assert_eq!(names1, names2);
unsafe {
CloseHandle(root_handle);
}
}
#[test]
fn corpus_replay_directory_buffer() {
let corpus_dir = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../fuzz/corpus/fuzz_directory_buffer"
);
let dir = std::path::Path::new(corpus_dir);
if !dir.exists() {
return;
}
let mut inputs: Vec<(String, Vec<u8>)> = Vec::new();
for entry in std::fs::read_dir(dir).expect("read corpus dir") {
let entry = entry.expect("dir entry");
let path = entry.path();
let fname = path.file_name().unwrap().to_string_lossy().into_owned();
let data = std::fs::read(&path).expect("read corpus file");
inputs.push((fname, data));
}
inputs.sort_by(|a, b| a.0.cmp(&b.0));
for (name, data) in inputs {
let max_entries = if data.is_empty() {
0
} else {
(data[0] as usize % 64) + 1
};
let result = parse_directory_buffer(&data, max_entries);
match result {
Ok(entries) => {
for entry in &entries {
assert_eq!(
entry.hidden_or_dot,
entry.name.starts_with('.'),
"[fuzz_directory_buffer/{name}] hidden_or_dot mismatch for {:?}",
entry.name
);
assert!(
matches!(
entry.kind,
DirectoryEntryKind::File
| DirectoryEntryKind::Directory
| DirectoryEntryKind::ReparsePoint
| DirectoryEntryKind::Other
),
"[fuzz_directory_buffer/{name}] unexpected entry kind for {:?}",
entry.name
);
}
assert!(
entries.len() <= max_entries,
"[fuzz_directory_buffer/{name}] entries {} exceeds max {}",
entries.len(),
max_entries
);
}
Err(e) => {
assert!(
matches!(
e,
DirBufParseError::BufferOverflow
| DirBufParseError::TruncatedHeader
| DirBufParseError::OddFileNameLength
| DirBufParseError::FileNameOutOfRange
| DirBufParseError::OffsetUnderflow
| DirBufParseError::OffsetOverflow
| DirBufParseError::OffsetLoop
| DirBufParseError::InvalidUtf16
),
"[fuzz_directory_buffer/{name}] unexpected error variant: {e:?}"
);
}
}
let _ = parse_directory_buffer(&data, 0);
let _ = parse_directory_buffer(&data, 1);
let _ = parse_directory_buffer(&data, usize::MAX);
}
}
}