use super::*;
use std::ffi::c_void;
use std::os::windows::fs::{MetadataExt, OpenOptionsExt};
use std::os::windows::io::{AsRawHandle, FromRawHandle};
#[repr(C)]
struct UnicodeString {
length: u16,
maximum_length: u16,
buffer: *mut u16,
}
#[repr(C)]
struct ObjectAttributes {
length: u32,
root_directory: *mut c_void,
object_name: *mut UnicodeString,
attributes: u32,
security_descriptor: *mut c_void,
security_quality_of_service: *mut c_void,
}
#[repr(C)]
struct IoStatusBlock {
status_or_pointer: usize,
information: usize,
}
#[link(name = "ntdll")]
unsafe extern "system" {
#[link_name = "NtCreateFile"]
fn nt_create_file(
file_handle: *mut *mut c_void,
desired_access: u32,
object_attributes: *mut ObjectAttributes,
io_status: *mut IoStatusBlock,
allocation_size: *const i64,
file_attributes: u32,
share_access: u32,
create_disposition: u32,
create_options: u32,
ea_buffer: *const c_void,
ea_length: u32,
) -> i32;
#[link_name = "NtQueryDirectoryFile"]
fn nt_query_directory(
file: *mut c_void,
event: *mut c_void,
apc_routine: *mut c_void,
apc_context: *mut c_void,
status: *mut IoStatusBlock,
information: *mut c_void,
length: u32,
class: u32,
single_entry: u8,
name: *const UnicodeString,
restart: u8,
) -> i32;
#[link_name = "RtlNtStatusToDosError"]
fn status_to_error(status: i32) -> u32;
#[link_name = "NtSetInformationFile"]
fn nt_set_information_file(
file: *mut c_void,
io_status: *mut IoStatusBlock,
information: *const c_void,
bytes: u32,
class: i32,
) -> i32;
}
#[link(name = "kernel32")]
unsafe extern "system" {
#[link_name = "SetFileInformationByHandle"]
fn set_file_information(
file: *mut c_void,
class: i32,
information: *const c_void,
bytes: u32,
) -> i32;
}
const SYNCHRONIZE: u32 = 0x0010_0000;
const READ_ATTRIBUTES: u32 = 0x80;
const READ_DATA: u32 = 1;
const DELETE: u32 = 0x0001_0000;
const SHARE_ALL: u32 = 7;
const NON_DIRECTORY: u32 = 0x40;
const SYNCHRONOUS_NONALERT: u32 = 0x20;
const OPEN_REPARSE_POINT: u32 = 0x0020_0000;
const ATTRIBUTE_REPARSE_POINT: u32 = 0x400;
pub(super) fn create_directory(root: &std::fs::File, name: ChildName<'_>) -> io::Result<()> {
let mut encoded = [0_u16; 256];
for (slot, byte) in encoded.iter_mut().zip(name.0.bytes()) {
*slot = u16::from(byte);
}
let file = relative_open_encoded_raw(root, &mut encoded[..name.0.len()], 1, 2, 1)?;
drop(file);
Ok(())
}
pub(super) fn create_control_candidate(
root: &std::fs::File,
name: ChildName<'_>,
) -> io::Result<std::fs::File> {
let mut encoded = [0_u16; 256];
for (slot, byte) in encoded.iter_mut().zip(name.0.bytes()) {
*slot = u16::from(byte);
}
relative_open_encoded_raw(
root,
&mut encoded[..name.0.len()],
READ_DATA | 0x4000_0000 | DELETE,
2,
NON_DIRECTORY,
)
}
pub(super) fn replace_control(
root: &std::fs::File,
file: &std::fs::File,
_source: ChildName<'_>,
target: ChildName<'_>,
) -> io::Result<()> {
#[repr(C)]
struct RenameInfo {
flags: u32,
root: *mut c_void,
length: u32,
name: [u16; 256],
}
let mut info = RenameInfo {
flags: 3,
root: root.as_raw_handle(),
length: u32::try_from(target.0.len() * 2).expect("bounded component"),
name: [0; 256],
};
for (slot, byte) in info.name.iter_mut().zip(target.0.bytes()) {
*slot = u16::from(byte);
}
let mut status = IoStatusBlock {
status_or_pointer: 0,
information: 0,
};
let result = unsafe {
nt_set_information_file(
file.as_raw_handle(),
&mut status,
std::ptr::from_ref(&info).cast(),
u32::try_from(std::mem::size_of::<RenameInfo>()).expect("bounded rename buffer"),
65,
)
};
if result < 0 {
Err(io::Error::from_raw_os_error(
unsafe { status_to_error(result) } as i32,
))
} else {
Ok(())
}
}
pub(super) fn open_root(path: &Path) -> io::Result<std::fs::File> {
const BACKUP_SEMANTICS: u32 = 0x0200_0000;
std::fs::OpenOptions::new()
.read(true)
.share_mode(SHARE_ALL)
.custom_flags(BACKUP_SEMANTICS)
.open(path)
}
pub(super) fn open_wal_append_child(
root: &std::fs::File,
name: ChildName<'_>,
) -> io::Result<std::fs::File> {
relative_open(root, name, READ_DATA | 0x0012_0114, false)
}
pub(super) fn open_mutable_child(
root: &std::fs::File,
name: ChildName<'_>,
) -> io::Result<std::fs::File> {
relative_open(root, name, READ_DATA | 0x4000_0000, false)
}
fn relative_open(
root: &std::fs::File,
name: ChildName<'_>,
access: u32,
create: bool,
) -> io::Result<std::fs::File> {
relative_open_kind(root, name, access, create, NON_DIRECTORY)
}
pub(super) fn open_directory(
root: &std::fs::File,
name: ChildName<'_>,
) -> io::Result<std::fs::File> {
relative_open_kind(root, name, READ_DATA, false, 1)
}
pub(super) fn sync_directory(root: &std::fs::File, context: &FileOpenContext) -> io::Result<()> {
let _permit = context.acquire(DescriptorKind::Transient)?;
let file = relative_open_kind(root, ChildName(""), READ_DATA | 0x4000_0000, false, 1)?;
if crate::native_file_identity::of_file(&file)? != crate::native_file_identity::of_file(root)? {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"directory sync identity changed",
));
}
file.sync_all()
}
fn relative_open_kind(
root: &std::fs::File,
name: ChildName<'_>,
access: u32,
create: bool,
kind: u32,
) -> io::Result<std::fs::File> {
let mut encoded = [0_u16; 256];
for (slot, byte) in encoded.iter_mut().zip(name.0.bytes()) {
*slot = u16::from(byte);
}
relative_open_encoded(
root,
&mut encoded[..name.0.len()],
access,
if create { 3 } else { 1 },
kind,
)
}
fn relative_open_encoded(
root: &std::fs::File,
encoded: &mut [u16],
access: u32,
disposition: u32,
kind: u32,
) -> io::Result<std::fs::File> {
let file = relative_open_encoded_raw(root, encoded, access, disposition, kind)?;
let metadata = file.metadata()?;
if metadata.file_attributes() & ATTRIBUTE_REPARSE_POINT != 0
|| if kind == NON_DIRECTORY {
!metadata.is_file()
} else if kind == 1 {
!metadata.is_dir()
} else {
!metadata.is_file() && !metadata.is_dir()
}
{
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"source child is not a regular non-reparse file",
));
}
Ok(file)
}
fn relative_open_encoded_raw(
root: &std::fs::File,
encoded: &mut [u16],
access: u32,
disposition: u32,
kind: u32,
) -> io::Result<std::fs::File> {
let length = u16::try_from(encoded.len() * 2).expect("bounded name fits native scratch");
let mut unicode = UnicodeString {
length,
maximum_length: length,
buffer: encoded.as_mut_ptr(),
};
let mut attributes = ObjectAttributes {
length: u32::try_from(std::mem::size_of::<ObjectAttributes>())
.expect("native struct fits u32"),
root_directory: root.as_raw_handle(),
object_name: &mut unicode,
attributes: 0,
security_descriptor: std::ptr::null_mut(),
security_quality_of_service: std::ptr::null_mut(),
};
let mut status = IoStatusBlock {
status_or_pointer: 0,
information: 0,
};
let mut handle = std::ptr::null_mut();
let result = unsafe {
nt_create_file(
&mut handle,
access | READ_ATTRIBUTES | SYNCHRONIZE,
&mut attributes,
&mut status,
std::ptr::null(),
0,
SHARE_ALL,
disposition,
kind | SYNCHRONOUS_NONALERT | OPEN_REPARSE_POINT,
std::ptr::null(),
0,
)
};
if result < 0 {
return Err(io::Error::from_raw_os_error(
unsafe { status_to_error(result) } as i32,
));
}
if handle.is_null() || handle == std::ptr::without_provenance_mut(usize::MAX) {
return Err(io::Error::other(
"native source open returned an invalid handle",
));
}
Ok(unsafe { std::fs::File::from_raw_handle(handle) })
}
pub(super) fn open_child(root: &std::fs::File, name: ChildName<'_>) -> io::Result<std::fs::File> {
relative_open(root, name, READ_DATA, false)
}
pub(super) fn remove_child(
root: &std::fs::File,
name: ChildName<'_>,
context: &FileOpenContext,
) -> io::Result<()> {
remove_entry(root, name, context, NON_DIRECTORY)
}
pub(super) fn remove_directory(
root: &std::fs::File,
name: ChildName<'_>,
context: &FileOpenContext,
) -> io::Result<()> {
remove_entry(root, name, context, 1)
}
fn remove_entry(
root: &std::fs::File,
name: ChildName<'_>,
context: &FileOpenContext,
kind: u32,
) -> io::Result<()> {
let _permit = context.acquire(DescriptorKind::Transient)?;
let file = relative_open_kind(root, name, DELETE, false, kind)?;
const FILE_DISPOSITION_INFO_EX: i32 = 21;
const FILE_DISPOSITION_DELETE: u32 = 1;
const FILE_DISPOSITION_POSIX_SEMANTICS: u32 = 2;
let disposition = FILE_DISPOSITION_DELETE | FILE_DISPOSITION_POSIX_SEMANTICS;
let success = unsafe {
set_file_information(
file.as_raw_handle(),
FILE_DISPOSITION_INFO_EX,
std::ptr::from_ref(&disposition).cast(),
4,
)
};
if success == 0 {
Err(io::Error::last_os_error())
} else {
Ok(())
}
}
pub(super) fn open_lock_file(
root: &std::fs::File,
name: ChildName<'_>,
mode: SourceDirectoryLockMode,
) -> io::Result<std::fs::File> {
const WRITE_DATA: u32 = 2;
match mode {
SourceDirectoryLockMode::ReadExisting => relative_open(root, name, READ_DATA, false),
SourceDirectoryLockMode::ReadWriteCreate => {
relative_open(root, name, READ_DATA | WRITE_DATA, true)
}
}
}
pub(super) fn visit_children(
root: &std::fs::File,
visitor: &mut dyn FnMut(&str) -> io::Result<()>,
) -> io::Result<()> {
visit_native_units(root, &mut |units| {
let mut encoded = [0_u8; 255];
for (slot, unit) in encoded.iter_mut().zip(units) {
if *unit > 127 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"source directory contains a non-ASCII name",
));
}
*slot = *unit as u8;
}
let name = std::str::from_utf8(&encoded[..units.len()]).expect("ASCII directory name");
visitor(name)
})
}
#[cfg(any(test, feature = "test-support"))]
pub(super) fn visit_names(
root: &std::fs::File,
visitor: &mut dyn FnMut(&std::ffi::OsStr) -> io::Result<()>,
) -> io::Result<()> {
use std::os::windows::ffi::OsStringExt;
visit_native_units(root, &mut |units| {
visitor(&std::ffi::OsString::from_wide(units))
})
}
fn visit_native_units(
root: &std::fs::File,
visitor: &mut dyn FnMut(&[u16]) -> io::Result<()>,
) -> io::Result<()> {
let cursor = relative_open_kind(root, ChildName(""), READ_DATA, false, 1)
.map_err(|error| report_cursor_error("reopen held directory", error))?;
let cursor_identity = crate::native_file_identity::of_file(&cursor)
.map_err(|error| report_cursor_error("query cursor identity", error))?;
let root_identity = crate::native_file_identity::of_file(root)
.map_err(|error| report_cursor_error("query held directory identity", error))?;
if cursor_identity != root_identity {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"directory cursor identity differs from captured root",
));
}
let mut scratch = [0_u64; 512];
let mut restart = 1;
loop {
let mut status = IoStatusBlock {
status_or_pointer: 0,
information: 0,
};
let result = unsafe {
nt_query_directory(
cursor.as_raw_handle(),
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
&mut status,
scratch.as_mut_ptr().cast(),
4096,
12,
1,
std::ptr::null(),
restart,
)
};
restart = 0;
if result == 0x8000_0006_u32 as i32 {
return Ok(());
} if result != 0 {
return Err(report_cursor_error(
"query directory entries",
io::Error::from_raw_os_error(unsafe { status_to_error(result) } as i32),
));
}
let bytes = unsafe { std::slice::from_raw_parts(scratch.as_ptr().cast::<u8>(), 4096) };
let used = status.information;
if !(12..=bytes.len()).contains(&used) {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"incomplete native directory record",
));
}
let next = u32::from_ne_bytes(bytes[0..4].try_into().unwrap());
let length = u32::from_ne_bytes(bytes[8..12].try_into().unwrap()) as usize;
if next != 0 || !length.is_multiple_of(2) || length > used - 12 || length / 2 > 255 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"unsupported native directory record",
));
}
let mut name = [0_u16; 255];
for (slot, unit) in name.iter_mut().zip(bytes[12..12 + length].chunks_exact(2)) {
*slot = u16::from_ne_bytes([unit[0], unit[1]]);
}
let name = &name[..length / 2];
if !matches!(name, [46] | [46, 46]) {
visitor(name)?;
}
}
}
#[cfg(any(test, feature = "test-support"))]
pub(super) fn open_recorded_child(
root: &std::fs::File,
name: &std::ffi::OsStr,
) -> io::Result<std::fs::File> {
use std::os::windows::ffi::OsStrExt;
let mut encoded = [0_u16; 255];
let mut length = 0;
for unit in name.encode_wide() {
if length == encoded.len() || matches!(unit, 0 | 47 | 58 | 92) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"invalid native child name",
));
}
encoded[length] = unit;
length += 1;
}
if length == 0 || matches!(&encoded[..length], [46] | [46, 46]) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"invalid native child name",
));
}
relative_open_encoded(root, &mut encoded[..length], READ_DATA, 1, 0)
}
#[cfg(any(test, feature = "test-support"))]
pub(super) fn reopen_directory(root: &std::fs::File) -> io::Result<std::fs::File> {
let file = relative_open_kind(root, ChildName(""), READ_DATA, false, 1)?;
if crate::native_file_identity::of_file(&file)? != crate::native_file_identity::of_file(root)? {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"reopened directory identity changed",
));
}
Ok(file)
}
#[cfg(any(test, feature = "test-support"))]
pub(super) fn open_recorded_lock(
root: &std::fs::File,
name: ChildName<'_>,
create_new: bool,
) -> io::Result<std::fs::File> {
let mut encoded = [0_u16; 255];
for (slot, byte) in encoded.iter_mut().zip(name.0.bytes()) {
*slot = u16::from(byte);
}
relative_open_encoded(
root,
&mut encoded[..name.0.len()],
READ_DATA | 2,
if create_new { 2 } else { 1 },
NON_DIRECTORY,
)
}
fn report_cursor_error(stage: &str, error: io::Error) -> io::Error {
#[cfg(test)]
eprintln!(
"source-directory cursor {stage}: kind={:?} raw={:?} error={error}",
error.kind(),
error.raw_os_error()
);
#[cfg(not(test))]
let _ = stage;
error
}
pub(super) fn matches_child_file(
root: &std::fs::File,
name: ChildName<'_>,
file: &std::fs::File,
context: &FileOpenContext,
) -> io::Result<bool> {
let _permit = context.acquire(DescriptorKind::Transient)?;
let named = relative_open(root, name, 0x80, false)?;
Ok(
crate::native_file_identity::of_file(file)?
== crate::native_file_identity::of_file(&named)?,
)
}