use std::{
ffi::{OsStr, OsString},
fs::{File, OpenOptions},
io, mem,
os::windows::{
ffi::{OsStrExt, OsStringExt},
fs::{MetadataExt, OpenOptionsExt},
io::{AsRawHandle, FromRawHandle},
},
path::{Component, Path, PathBuf, Prefix},
ptr, slice,
};
use windows_sys::{
Wdk::{
Foundation::OBJECT_ATTRIBUTES,
Storage::FileSystem::{
FILE_OPEN, FILE_OPEN_REPARSE_POINT, FILE_SYNCHRONOUS_IO_NONALERT, NtCreateFile,
},
},
Win32::{
Foundation::{
ERROR_NO_MORE_FILES, 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_ID_BOTH_DIR_INFO, FILE_READ_ATTRIBUTES, FILE_SHARE_DELETE, FILE_SHARE_READ,
FILE_SHARE_WRITE, FileAttributeTagInfo, FileIdBothDirectoryInfo,
FileIdBothDirectoryRestartInfo, GetFileInformationByHandleEx,
GetFinalPathNameByHandleW, SYNCHRONIZE,
},
System::IO::IO_STATUS_BLOCK,
},
};
use super::FileKind;
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 canonicalize(path: &Path) -> io::Result<PathBuf> {
let path = std::fs::canonicalize(path)?;
Ok(plain(&path).unwrap_or(path))
}
fn plain(path: &Path) -> Option<PathBuf> {
let mut components = path.components();
let Some(Component::Prefix(prefix)) = components.next() else {
return None;
};
let mut plain = match prefix.kind() {
Prefix::VerbatimDisk(drive) => OsString::from(format!("{}:", char::from(drive))),
Prefix::VerbatimUNC(server, share) => {
let mut plain = OsString::from(r"\\");
plain.push(server);
plain.push(r"\");
plain.push(share);
plain
}
_ => return None,
};
if components.next() != Some(Component::RootDir) {
return None;
}
plain.push(r"\");
for (index, component) in components.enumerate() {
let Component::Normal(name) = component else {
return None;
};
if !plain_name(name) {
return None;
}
if index > 0 {
plain.push(r"\");
}
plain.push(name);
}
const MAX_PLAIN_PATH_UNITS: usize = 260;
(plain.encode_wide().count() < MAX_PLAIN_PATH_UNITS).then(|| PathBuf::from(plain))
}
fn plain_name(name: &OsStr) -> bool {
const MAX_PLAIN_NAME_BYTES: usize = 255;
let Some(name) = name.to_str() else {
return false;
};
let stem = name
.split('.')
.next()
.unwrap_or_default()
.trim_end_matches(' ');
let device = match stem.to_ascii_uppercase().as_str() {
"CON" | "PRN" | "AUX" | "NUL" | "CONIN$" | "CONOUT$" => true,
port => {
(port.starts_with("COM") || port.starts_with("LPT"))
&& port[3..].chars().count() == 1
&& port[3..]
.chars()
.all(|digit| "0123456789¹²³".contains(digit))
}
};
!name.is_empty()
&& name.len() <= MAX_PLAIN_NAME_BYTES
&& !name.ends_with(['.', ' '])
&& !name
.chars()
.any(|c| c < ' ' || matches!(c, '<' | '>' | ':' | '"' | '/' | '\\' | '|' | '?' | '*'))
&& !device
}
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 DIRECTORY_BUFFER_WORDS: usize = 8 * 1024;
const DIRECTORY_RECORD_ALIGNMENT: usize = 8;
const WIDE_UNIT_BYTES: usize = mem::size_of::<u16>();
pub fn read_dir(directory: &File) -> io::Result<impl Iterator<Item = io::Result<DirEntry>> + '_> {
Ok(ReadDir {
directory,
buffer: vec![0; DIRECTORY_BUFFER_WORDS],
next: None,
scan: super::directory_scan::DirectoryScan::First,
})
}
pub struct DirEntry {
name: OsString,
kind: FileKind,
}
impl DirEntry {
pub fn file_name(&self) -> OsString {
self.name.clone()
}
pub fn file_type(&self) -> io::Result<FileKind> {
Ok(self.kind)
}
}
struct ReadDir<'a> {
directory: &'a File,
buffer: Vec<u64>,
next: Option<usize>,
scan: super::directory_scan::DirectoryScan,
}
impl Iterator for ReadDir<'_> {
type Item = io::Result<DirEntry>;
fn next(&mut self) -> Option<Self::Item> {
loop {
if let Some(offset) = self.next {
match self.record(offset) {
Ok(Some(entry)) => return Some(Ok(entry)),
Ok(None) => continue,
Err(error) => return Some(Err(error)),
}
}
let class = match self.scan.query()? {
super::directory_scan::Query::Restart => FileIdBothDirectoryRestartInfo,
super::directory_scan::Query::Continue => FileIdBothDirectoryInfo,
};
let ok = unsafe {
GetFileInformationByHandleEx(
self.directory.as_raw_handle(),
class,
self.buffer.as_mut_ptr().cast(),
mem::size_of_val(self.buffer.as_slice()) as u32,
)
};
if ok == 0 {
self.scan.finish();
let error = io::Error::last_os_error();
if error.raw_os_error() == Some(ERROR_NO_MORE_FILES as i32) {
return None;
}
return Some(Err(error));
}
self.next = Some(0);
}
}
}
impl ReadDir<'_> {
fn record(&mut self, offset: usize) -> io::Result<Option<DirEntry>> {
let size = mem::size_of_val(self.buffer.as_slice());
let header = mem::offset_of!(FILE_ID_BOTH_DIR_INFO, FileName);
if !offset.is_multiple_of(DIRECTORY_RECORD_ALIGNMENT) || offset + header > size {
self.scan.finish();
self.next = None;
return Err(io::Error::other("malformed directory listing"));
}
let info = unsafe {
self.buffer
.as_ptr()
.cast::<u8>()
.add(offset)
.cast::<FILE_ID_BOTH_DIR_INFO>()
};
let (next, attributes, tag, length) = unsafe {
(
(*info).NextEntryOffset as usize,
(*info).FileAttributes,
(*info).EaSize,
(*info).FileNameLength as usize,
)
};
if offset + header + length > size {
self.scan.finish();
self.next = None;
return Err(io::Error::other("malformed directory listing"));
}
let name = unsafe {
slice::from_raw_parts(
(&raw const (*info).FileName).cast::<u16>(),
length / WIDE_UNIT_BYTES,
)
};
self.next = (next != 0).then_some(offset + next);
let dot = u16::from(b'.');
if name == [dot] || name == [dot, dot] {
return Ok(None);
}
Ok(Some(DirEntry {
name: OsString::from_wide(name),
kind: kind(attributes, tag),
}))
}
}
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)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn directory_records_drain_then_continue_and_malformed_record_ends_iteration() {
use super::super::directory_scan::{DirectoryScan, Query};
let directory = crate::test_os::tempdir();
let handle = open_directory(directory.path()).unwrap();
let mut reader = ReadDir {
directory: &handle,
buffer: vec![0; DIRECTORY_BUFFER_WORDS],
next: None,
scan: DirectoryScan::First,
};
assert_eq!(reader.scan.query(), Some(Query::Restart));
let header = mem::offset_of!(FILE_ID_BOTH_DIR_INFO, FileName);
let next_offset = (header + WIDE_UNIT_BYTES).next_multiple_of(DIRECTORY_RECORD_ALIGNMENT);
for (offset, next, character) in [(0, next_offset, b'a'), (next_offset, 0, b'b')] {
let info = unsafe {
reader
.buffer
.as_mut_ptr()
.cast::<u8>()
.add(offset)
.cast::<FILE_ID_BOTH_DIR_INFO>()
};
unsafe {
(*info).NextEntryOffset = next as u32;
(*info).FileNameLength = WIDE_UNIT_BYTES as u32;
(*info).FileAttributes = 0;
(*info).FileName[0] = u16::from(character);
}
}
reader.next = Some(0);
assert_eq!(
reader.next().unwrap().unwrap().file_name(),
OsString::from("a")
);
assert_eq!(
reader.next().unwrap().unwrap().file_name(),
OsString::from("b")
);
assert_eq!(reader.next, None);
assert_eq!(reader.scan.query(), Some(Query::Continue));
reader.next = Some(1); assert!(reader.next().unwrap().is_err());
assert_eq!(reader.scan, DirectoryScan::Done);
assert!(reader.next().is_none());
assert!(reader.next().is_none());
}
}