use std::{
ffi::{OsStr, OsString},
fs::{File, OpenOptions},
io, mem,
os::windows::{
ffi::{OsStrExt, OsStringExt},
fs::{MetadataExt, OpenOptionsExt},
io::{AsRawHandle, FromRawHandle},
},
path::{Path, PathBuf},
ptr,
};
use windows_sys::{
Wdk::{
Foundation::OBJECT_ATTRIBUTES,
Storage::FileSystem::{
FILE_OPEN, FILE_OPEN_REPARSE_POINT, FILE_SYNCHRONOUS_IO_NONALERT, NtCreateFile,
},
},
Win32::{
Foundation::{
HANDLE, OBJ_CASE_INSENSITIVE, OBJ_DONT_REPARSE, RtlNtStatusToDosError,
STATUS_INVALID_PARAMETER, UNICODE_STRING,
},
Storage::FileSystem::{
FILE_ATTRIBUTE_DIRECTORY, FILE_ATTRIBUTE_REPARSE_POINT, FILE_ATTRIBUTE_TAG_INFO,
FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT, FILE_GENERIC_READ,
FILE_READ_ATTRIBUTES, FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE,
FileAttributeTagInfo, GetFileInformationByHandleEx, GetFinalPathNameByHandleW,
SYNCHRONIZE,
},
System::IO::IO_STATUS_BLOCK,
},
};
use super::FileKind;
mod directory;
mod path;
pub use directory::read_dir;
pub use path::canonicalize;
const NAME_SURROGATE: u32 = 0x2000_0000;
pub fn open_no_follow(options: &mut OpenOptions, path: &Path) -> io::Result<File> {
let file = options
.custom_flags(FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS)
.open(path)?;
if file.metadata()?.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
return Err(reparse_point());
}
Ok(file)
}
pub fn open_nonblocking(path: &Path) -> io::Result<File> {
OpenOptions::new()
.read(true)
.custom_flags(FILE_FLAG_BACKUP_SEMANTICS)
.open(path)
}
pub fn open_directory(path: &Path) -> io::Result<File> {
OpenOptions::new()
.read(true)
.custom_flags(FILE_FLAG_BACKUP_SEMANTICS)
.open(path)
}
pub struct EntryName(Vec<u16>);
impl EntryName {
pub fn new(name: &OsStr) -> Option<Self> {
let name: Vec<u16> = name.encode_wide().collect();
let dots = |count| name.len() == count && name.iter().all(|&unit| unit == u16::from(b'.'));
let valid = !name.is_empty()
&& name.len() <= usize::from(u16::MAX) / WIDE_UNIT_BYTES
&& !dots(1)
&& !dots(2)
&& !name
.iter()
.any(|&unit| [0, b'/', b':', b'\\'].map(u16::from).contains(&unit));
valid.then_some(Self(name))
}
}
pub fn open_entry(directory: &File, name: &EntryName) -> io::Result<File> {
let file = open_relative(directory, &name.0, FILE_GENERIC_READ)?;
if attribute_tag(&file)?.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
return Err(reparse_point());
}
Ok(file)
}
pub fn entry_kind(directory: &File, name: &OsStr) -> io::Result<FileKind> {
let name = EntryName::new(name).ok_or(io::ErrorKind::InvalidInput)?;
let file = open_relative(directory, &name.0, FILE_READ_ATTRIBUTES | SYNCHRONIZE)?;
let info = attribute_tag(&file)?;
Ok(kind(info.FileAttributes, info.ReparseTag))
}
fn open_relative(directory: &File, name: &[u16], access: u32) -> io::Result<File> {
let length = u16::try_from(mem::size_of_val(name)).map_err(|_| io::ErrorKind::InvalidInput)?;
let name = UNICODE_STRING {
Length: length,
MaximumLength: length,
Buffer: name.as_ptr().cast_mut(),
};
let mut attributes = OBJECT_ATTRIBUTES {
Length: mem::size_of::<OBJECT_ATTRIBUTES>() as u32,
RootDirectory: directory.as_raw_handle(),
ObjectName: &name,
Attributes: OBJ_CASE_INSENSITIVE | OBJ_DONT_REPARSE,
..Default::default()
};
let open = |attributes: &OBJECT_ATTRIBUTES, handle: &mut HANDLE| {
let mut status = IO_STATUS_BLOCK::default();
unsafe {
NtCreateFile(
handle,
access,
attributes,
&mut status,
ptr::null(),
0,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
FILE_OPEN,
FILE_OPEN_REPARSE_POINT | FILE_SYNCHRONOUS_IO_NONALERT,
ptr::null(),
0,
)
}
};
let mut handle = ptr::null_mut();
let mut status = open(&attributes, &mut handle);
if status == STATUS_INVALID_PARAMETER {
attributes.Attributes = OBJ_CASE_INSENSITIVE;
status = open(&attributes, &mut handle);
}
if status < 0 {
let error = unsafe { RtlNtStatusToDosError(status) };
return Err(io::Error::from_raw_os_error(error as i32));
}
Ok(unsafe { File::from_raw_handle(handle) })
}
fn attribute_tag(file: &File) -> io::Result<FILE_ATTRIBUTE_TAG_INFO> {
let mut info = FILE_ATTRIBUTE_TAG_INFO::default();
let ok = unsafe {
GetFileInformationByHandleEx(
file.as_raw_handle(),
FileAttributeTagInfo,
(&raw mut info).cast(),
mem::size_of::<FILE_ATTRIBUTE_TAG_INFO>() as u32,
)
};
if ok == 0 {
return Err(io::Error::last_os_error());
}
Ok(info)
}
fn kind(attributes: u32, reparse_tag: u32) -> FileKind {
if attributes & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
if reparse_tag & NAME_SURROGATE != 0 {
FileKind::Symlink
} else {
FileKind::Other
}
} else if attributes & FILE_ATTRIBUTE_DIRECTORY != 0 {
FileKind::Directory
} else {
FileKind::File
}
}
fn reparse_point() -> io::Error {
io::Error::new(io::ErrorKind::InvalidInput, ReparsePoint)
}
#[derive(Debug)]
struct ReparsePoint;
impl std::fmt::Display for ReparsePoint {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("refusing a symbolic link, junction or other reparse point")
}
}
impl std::error::Error for ReparsePoint {}
pub fn is_link_refusal(error: &io::Error) -> bool {
error
.get_ref()
.is_some_and(|inner| inner.is::<ReparsePoint>())
}
const WIDE_UNIT_BYTES: usize = mem::size_of::<u16>();
pub fn exact_name(file: &File, name: &OsStr) -> io::Result<bool> {
const MAX_PATH_UNITS: usize = 32_768;
let mut path = vec![0; MAX_PATH_UNITS];
let length = unsafe {
GetFinalPathNameByHandleW(
file.as_raw_handle(),
path.as_mut_ptr(),
path.len() as u32,
0,
)
} as usize;
if length == 0 {
return Err(io::Error::last_os_error());
}
if length >= path.len() {
return Err(io::ErrorKind::InvalidData.into());
}
let path = PathBuf::from(OsString::from_wide(&path[..length]));
Ok(path.file_name() == Some(name))
}
pub fn case_sensitive(_directory: &File) -> io::Result<bool> {
Ok(false)
}