From bb38a3a55038c7f0b26e4dfb5f2a439317bb9020 Mon Sep 17 00:00:00 2001 From: wan9chi Date: Thu, 13 Aug 2026 13:04:55 +0800 Subject: [PATCH 01/18] refactor(fspy-shm): adopt fspy_nostd on Windows Move Windows file handles, sparse-file operations, and mapped views behind direct no-std Win32 wrappers. Keep the existing fspy_shm public API while removing its production memmap2 and direct windows-sys use. Co-authored-by: GPT-5 Codex --- Cargo.lock | 2 +- crates/fspy_nostd/Cargo.toml | 11 +- crates/fspy_nostd/README.md | 4 +- crates/fspy_nostd/src/fs/mod.rs | 16 ++- crates/fspy_nostd/src/fs/windows.rs | 154 ++++++++++++++++++++++++++ crates/fspy_nostd/src/lib.rs | 6 +- crates/fspy_nostd/src/mm.rs | 83 +++++++++++++- crates/fspy_nostd/src/windows.rs | 44 ++++++++ crates/fspy_shm/Cargo.toml | 14 +-- crates/fspy_shm/src/windows.rs | 161 ++++++++++++++-------------- 10 files changed, 396 insertions(+), 99 deletions(-) create mode 100644 crates/fspy_nostd/src/fs/windows.rs diff --git a/Cargo.lock b/Cargo.lock index 22ed69117..b6a18250e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1345,6 +1345,7 @@ name = "fspy_nostd" version = "0.0.0" dependencies = [ "atoi", + "bitflags 2.10.0", "bstr", "libc", "rustix", @@ -1458,7 +1459,6 @@ version = "0.0.0" dependencies = [ "ctor", "fspy_nostd", - "memmap2", "subprocess_test", "uuid", "windows-sys 0.61.2", diff --git a/crates/fspy_nostd/Cargo.toml b/crates/fspy_nostd/Cargo.toml index 02cce3f40..9fbef3962 100644 --- a/crates/fspy_nostd/Cargo.toml +++ b/crates/fspy_nostd/Cargo.toml @@ -32,7 +32,16 @@ rustix = { workspace = true, features = ["runtime"] } syscalls = { workspace = true } [target.'cfg(windows)'.dependencies] -windows-sys = { workspace = true, features = ["Win32_Foundation", "Win32_System_LibraryLoader"] } +bitflags = { workspace = true } +windows-sys = { workspace = true, features = [ + "Win32_Foundation", + "Win32_Security", + "Win32_Storage_FileSystem", + "Win32_System_IO", + "Win32_System_Ioctl", + "Win32_System_LibraryLoader", + "Win32_System_Memory", +] } # Cross-validates the page-size probe against rustix's auxv-based answer. [target.'cfg(target_os = "linux")'.dev-dependencies] diff --git a/crates/fspy_nostd/README.md b/crates/fspy_nostd/README.md index 3404b5c86..9e918ca5e 100644 --- a/crates/fspy_nostd/README.md +++ b/crates/fspy_nostd/README.md @@ -54,8 +54,8 @@ Code that needs allocation uses an explicit allocator. [`fspy_nostd_alloc`](../f ## Modules -- `mm`: anonymous memory mapping and protection operations. +- `mm`: memory mapping and protection operations. - `env`: allocation-free process argument and environment iteration. -- `fs`: filesystem operations with caller-owned buffers. +- `fs`: filesystem operations with caller-owned paths and buffers. - `param`: page-size access. - `get_module_handle`: allocation-free lookup of an already-loaded Windows module. diff --git a/crates/fspy_nostd/src/fs/mod.rs b/crates/fspy_nostd/src/fs/mod.rs index 3720e13bd..88594a223 100644 --- a/crates/fspy_nostd/src/fs/mod.rs +++ b/crates/fspy_nostd/src/fs/mod.rs @@ -1,11 +1,21 @@ //! Filesystem calls with caller-owned storage. +#[cfg(unix)] use core::mem::MaybeUninit; +#[cfg(unix)] pub use rustix::fs::{AtFlags, Mode, OFlags, fstat, ftruncate}; +#[cfg(unix)] use crate::{BorrowedFd, CStr, Fat, OwnedFd, Result}; +#[cfg(windows)] +mod windows; +#[cfg(windows)] +pub use windows::{ + Access, CreationDisposition, OpenFlags, ShareMode, file_size, open, remove, set_len, set_sparse, +}; + #[cfg(target_os = "linux")] mod linux; #[cfg(target_os = "macos")] @@ -21,6 +31,7 @@ use mac as imp; pub use mac::fcntl_getpath; /// The platform's maximum pathname size, including the terminating NUL. +#[cfg(unix)] pub const PATH_MAX: usize = imp::PATH_MAX; /// Opens `path` relative to `dirfd` and returns its owned descriptor. @@ -28,6 +39,7 @@ pub const PATH_MAX: usize = imp::PATH_MAX; /// # Errors /// /// Returns the error reported by `openat`. +#[cfg(unix)] pub fn openat( dirfd: BorrowedFd<'_>, path: CStr<'_, R>, @@ -42,6 +54,7 @@ pub fn openat( /// # Errors /// /// Returns the error reported by `unlinkat`. +#[cfg(unix)] pub fn unlinkat(dirfd: BorrowedFd<'_>, path: CStr<'_, R>, flags: AtFlags) -> Result<()> { imp::unlinkat(dirfd, path, flags) } @@ -59,9 +72,10 @@ pub fn unlinkat(dirfd: BorrowedFd<'_>, path: CStr<'_, R>, flags: AtFlags) -> /// # Errors /// /// Returns the error reported while resolving the current working directory. +#[cfg(unix)] pub fn getcwd(buf: &mut [MaybeUninit]) -> Result> { imp::getcwd(buf) } -#[cfg(test)] +#[cfg(all(test, unix))] mod tests; diff --git a/crates/fspy_nostd/src/fs/windows.rs b/crates/fspy_nostd/src/fs/windows.rs new file mode 100644 index 000000000..f0207fa2c --- /dev/null +++ b/crates/fspy_nostd/src/fs/windows.rs @@ -0,0 +1,154 @@ +use core::{ffi::c_void, mem::size_of, ptr}; + +use bitflags::bitflags; +use windows_sys::Win32::{ + Foundation::{GENERIC_READ, GENERIC_WRITE, INVALID_HANDLE_VALUE}, + Storage::FileSystem::{ + CREATE_NEW, CreateFileW, DELETE, DeleteFileW, FILE_ATTRIBUTE_TEMPORARY, + FILE_END_OF_FILE_INFO, FILE_FLAG_DELETE_ON_CLOSE, FILE_SHARE_DELETE, FILE_SHARE_READ, + FILE_SHARE_WRITE, FileEndOfFileInfo, GetFileSizeEx, OPEN_EXISTING, + SetFileInformationByHandle, + }, + System::{IO::DeviceIoControl, Ioctl::FSCTL_SET_SPARSE}, +}; + +use crate::{OwnedHandle, Result, WideCStr}; + +bitflags! { + /// Access rights requested when opening a file. + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub struct Access: u32 { + const READ = GENERIC_READ; + const WRITE = GENERIC_WRITE; + const DELETE = DELETE; + } + + /// Operations that other handles may perform while a file is open. + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub struct ShareMode: u32 { + const READ = FILE_SHARE_READ; + const WRITE = FILE_SHARE_WRITE; + const DELETE = FILE_SHARE_DELETE; + } + + /// File attributes and flags applied while opening a file. + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub struct OpenFlags: u32 { + const TEMPORARY = FILE_ATTRIBUTE_TEMPORARY; + const DELETE_ON_CLOSE = FILE_FLAG_DELETE_ON_CLOSE; + } +} + +/// Controls whether opening a file creates it or requires it to exist. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct CreationDisposition(u32); + +impl CreationDisposition { + pub const CREATE_NEW: Self = Self(CREATE_NEW); + pub const OPEN_EXISTING: Self = Self(OPEN_EXISTING); +} + +/// Opens a file and returns its owned handle. +/// +/// The returned handle is non-inheritable because this function supplies no +/// security attributes. +/// +/// # Errors +/// +/// Returns the error reported by `CreateFileW`. +#[expect(clippy::needless_pass_by_value, reason = "CStr is a borrowed value type")] +pub fn open( + path: WideCStr<'_, R>, + access: Access, + share_mode: ShareMode, + creation: CreationDisposition, + flags: OpenFlags, +) -> Result { + // SAFETY: `path` is NUL-terminated. Null security attributes make the + // handle non-inheritable, and the null template is permitted. + let handle = unsafe { + CreateFileW( + path.as_ptr(), + access.bits(), + share_mode.bits(), + ptr::null(), + creation.0, + flags.bits(), + ptr::null_mut(), + ) + }; + if handle == INVALID_HANDLE_VALUE || handle.is_null() { + Err(crate::windows::last_error()) + } else { + // SAFETY: `CreateFileW` returned a valid, newly owned handle. + Ok(unsafe { OwnedHandle::from_raw(handle) }) + } +} + +/// Removes a file name. +/// +/// # Errors +/// +/// Returns the error reported by `DeleteFileW`. +#[expect(clippy::needless_pass_by_value, reason = "CStr is a borrowed value type")] +pub fn remove(path: WideCStr<'_, R>) -> Result<()> { + // SAFETY: `path` is a valid NUL-terminated wide string. + crate::windows::bool_result(unsafe { DeleteFileW(path.as_ptr()) }) +} + +/// Returns a file's logical length. +/// +/// # Errors +/// +/// Returns the error reported by `GetFileSizeEx`. +pub fn file_size(file: &OwnedHandle) -> Result { + let mut size = 0; + // SAFETY: `file` is valid and `size` is writable for the call. + crate::windows::bool_result(unsafe { GetFileSizeEx(file.as_raw(), &raw mut size) })?; + Ok(size) +} + +/// Sets a file's logical length. +/// +/// # Errors +/// +/// Returns the error reported by `SetFileInformationByHandle`. +pub fn set_len(file: &OwnedHandle, len: i64) -> Result<()> { + const INFO_SIZE: u32 = 8; + const _: [(); 8] = [(); size_of::()]; + + let info = FILE_END_OF_FILE_INFO { EndOfFile: len }; + // SAFETY: `file` is valid and `info` has the type and exact size required + // by `FileEndOfFileInfo`. + crate::windows::bool_result(unsafe { + SetFileInformationByHandle( + file.as_raw(), + FileEndOfFileInfo, + (&raw const info).cast::(), + INFO_SIZE, + ) + }) +} + +/// Marks a file sparse. +/// +/// # Errors +/// +/// Returns the error reported by `FSCTL_SET_SPARSE`. +pub fn set_sparse(file: &OwnedHandle) -> Result<()> { + let mut bytes_returned = 0; + // SAFETY: `file` is valid and synchronous. `FSCTL_SET_SPARSE` needs no + // input or output buffer, and `bytes_returned` is writable for the call. + crate::windows::bool_result(unsafe { + DeviceIoControl( + file.as_raw(), + FSCTL_SET_SPARSE, + ptr::null(), + 0, + ptr::null_mut(), + 0, + &raw mut bytes_returned, + ptr::null_mut(), + ) + }) +} diff --git a/crates/fspy_nostd/src/lib.rs b/crates/fspy_nostd/src/lib.rs index 779889d51..3d1669bcf 100644 --- a/crates/fspy_nostd/src/lib.rs +++ b/crates/fspy_nostd/src/lib.rs @@ -13,16 +13,16 @@ mod windows; #[cfg(unix)] pub mod env; -#[cfg(unix)] +#[cfg(any(unix, windows))] pub mod fs; -#[cfg(unix)] +#[cfg(any(unix, windows))] pub mod mm; #[cfg(unix)] pub mod param; pub use c_str::{CStr, CStrUnit, Fat, Thin, Units, WideCStr}; #[cfg(windows)] -pub use windows::get_module_handle; +pub use windows::{OwnedHandle, get_module_handle}; #[cfg(windows)] #[derive(Clone, Copy, Debug, Eq, PartialEq)] diff --git a/crates/fspy_nostd/src/mm.rs b/crates/fspy_nostd/src/mm.rs index 7de3763f5..3bb846128 100644 --- a/crates/fspy_nostd/src/mm.rs +++ b/crates/fspy_nostd/src/mm.rs @@ -1,6 +1,6 @@ -//! Anonymous memory mappings. +//! Memory mappings. //! -//! Re-exposed from rustix as-is: these are single syscalls against the +//! On Unix, these are re-exposed from rustix as-is: they are single syscalls against the //! kernel's own address-space bookkeeping — no libc state, no locks, no //! allocation — so they already meet this crate's rules everywhere it //! promises to work. What this module adds is the curation (being listed @@ -8,4 +8,83 @@ //! pre-libc startup) and the crate-level backend check, which guarantees //! they cannot silently turn into libc calls on Linux. +#[cfg(unix)] pub use rustix::mm::{MapFlags, MprotectFlags, ProtFlags, mmap, mmap_anonymous, mprotect, munmap}; + +#[cfg(windows)] +mod windows { + use core::{ffi::c_void, num::NonZeroUsize, ptr}; + + use windows_sys::Win32::System::Memory::{ + CreateFileMappingW, FILE_MAP_READ, FILE_MAP_WRITE, MEMORY_MAPPED_VIEW_ADDRESS, + MapViewOfFile, PAGE_READWRITE, UnmapViewOfFile, + }; + + use crate::{OwnedHandle, Result}; + + /// An owned writable view of a shared file mapping. + pub struct MappedView { + ptr: core::ptr::NonNull, + } + + // SAFETY: a view owns no thread-affine state. Synchronization of accesses + // to its shared bytes is the caller's responsibility. + unsafe impl Send for MappedView {} + // SAFETY: sharing the view does not itself access the mapped bytes. + unsafe impl Sync for MappedView {} + + impl MappedView { + /// Returns a raw pointer to the first mapped byte. + #[must_use] + pub const fn as_ptr(&self) -> *mut u8 { + self.ptr.as_ptr() + } + } + + impl Drop for MappedView { + fn drop(&mut self) { + // SAFETY: this is the complete view owned by `self` and is + // unmapped exactly once. + let _ = unsafe { + UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS { + Value: self.ptr.as_ptr().cast::(), + }) + }; + } + } + + /// Maps the first `len` bytes of `file` as shared readable and writable + /// memory. + /// + /// # Errors + /// + /// Returns the error reported while creating or mapping the view. + pub fn map_file(file: &OwnedHandle, len: NonZeroUsize) -> Result { + // SAFETY: `file` is valid. Null security attributes and name request + // an unnamed, non-inheritable mapping object backed by the file's + // current size. + let mapping = unsafe { + CreateFileMappingW(file.as_raw(), ptr::null(), PAGE_READWRITE, 0, 0, ptr::null()) + }; + let Some(mapping) = core::ptr::NonNull::new(mapping) else { + return Err(crate::windows::last_error()); + }; + // SAFETY: `CreateFileMappingW` returned a valid, newly owned handle. + let mapping = unsafe { OwnedHandle::from_raw(mapping.as_ptr()) }; + + // SAFETY: `mapping` is a valid file-mapping object, the requested + // access matches its protection, and `len` is nonzero. + let view = unsafe { + MapViewOfFile(mapping.as_raw(), FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, len.get()) + }; + let Some(ptr) = core::ptr::NonNull::new(view.Value.cast::()) else { + return Err(crate::windows::last_error()); + }; + + // A mapped view remains valid after its mapping-object handle closes. + Ok(MappedView { ptr }) + } +} + +#[cfg(windows)] +pub use windows::{MappedView, map_file}; diff --git a/crates/fspy_nostd/src/windows.rs b/crates/fspy_nostd/src/windows.rs index b2b4d369f..c01149805 100644 --- a/crates/fspy_nostd/src/windows.rs +++ b/crates/fspy_nostd/src/windows.rs @@ -4,6 +4,50 @@ use windows_sys::Win32::{Foundation::GetLastError, System::LibraryLoader::GetMod use crate::{Result, WideCStr}; +/// An owned Windows kernel handle. +pub struct OwnedHandle(NonNull); + +impl OwnedHandle { + /// Creates an owned handle after the caller has validated the raw value. + /// + /// # Safety + /// + /// `handle` must be a valid, non-null, uniquely owned handle that may be + /// closed with `CloseHandle`. + pub(crate) const unsafe fn from_raw(handle: *mut c_void) -> Self { + // SAFETY: the caller guarantees that the handle is non-null. + Self(unsafe { NonNull::new_unchecked(handle) }) + } + + /// Returns the raw Windows handle without transferring ownership. + pub(crate) const fn as_raw(&self) -> *mut c_void { + self.0.as_ptr() + } +} + +// SAFETY: Windows kernel handles are not thread-affine. The operations exposed +// by this crate provide their own synchronization or do not mutate the handle. +unsafe impl Send for OwnedHandle {} +// SAFETY: as above; sharing the value does not itself access the referenced +// kernel object. +unsafe impl Sync for OwnedHandle {} + +impl Drop for OwnedHandle { + fn drop(&mut self) { + // SAFETY: this type owns a valid handle and closes it exactly once. + let _ = unsafe { windows_sys::Win32::Foundation::CloseHandle(self.as_raw()) }; + } +} + +pub fn last_error() -> crate::Error { + // SAFETY: `GetLastError` reads thread-local error state. + crate::Error::from_raw_os_error(unsafe { GetLastError() }) +} + +pub fn bool_result(result: i32) -> Result<()> { + if result == 0 { Err(last_error()) } else { Ok(()) } +} + /// Returns a handle to the loaded module named by `name`. /// /// This does not load the module or increment its loader reference count. The diff --git a/crates/fspy_shm/Cargo.toml b/crates/fspy_shm/Cargo.toml index 70115da56..7f42fa906 100644 --- a/crates/fspy_shm/Cargo.toml +++ b/crates/fspy_shm/Cargo.toml @@ -10,19 +10,11 @@ rust-version.workspace = true [dependencies] uuid = { workspace = true, features = ["v4"] } -[target.'cfg(unix)'.dependencies] +[target.'cfg(any(unix, windows))'.dependencies] fspy_nostd = { workspace = true } -[target.'cfg(windows)'.dependencies] -memmap2 = { workspace = true } - -[target.'cfg(target_os = "windows")'.dependencies] -windows-sys = { workspace = true, features = [ - "Win32_Foundation", - "Win32_Storage_FileSystem", - "Win32_System_IO", - "Win32_System_Ioctl", -] } +[target.'cfg(target_os = "windows")'.dev-dependencies] +windows-sys = { workspace = true, features = ["Win32_Foundation", "Win32_Storage_FileSystem"] } [dev-dependencies] ctor = { workspace = true } diff --git a/crates/fspy_shm/src/windows.rs b/crates/fspy_shm/src/windows.rs index c7c47446b..d4f52d957 100644 --- a/crates/fspy_shm/src/windows.rs +++ b/crates/fspy_shm/src/windows.rs @@ -2,34 +2,23 @@ //! its path. use std::{ - env::temp_dir, - ffi::OsStr, - fs::{self, File, OpenOptions}, - io, - os::windows::{fs::OpenOptionsExt as _, io::AsRawHandle as _}, + env::temp_dir, ffi::OsStr, io, num::NonZeroUsize, os::windows::ffi::OsStrExt as _, path::PathBuf, }; +#[cfg(test)] +use std::{fs::File, os::windows::io::AsRawHandle as _}; -use memmap2::{MmapOptions, MmapRaw}; use uuid::Uuid; #[cfg(test)] use windows_sys::Win32::Storage::FileSystem::{ FILE_STANDARD_INFO, FileStandardInfo, GetFileInformationByHandleEx, }; -use windows_sys::Win32::{ - Storage::FileSystem::{ - FILE_ATTRIBUTE_TEMPORARY, FILE_FLAG_DELETE_ON_CLOSE, FILE_SHARE_DELETE, FILE_SHARE_READ, - FILE_SHARE_WRITE, - }, - System::{IO::DeviceIoControl, Ioctl::FSCTL_SET_SPARSE}, -}; use crate::BACKING_PREFIX; -const SHARE_ALL: u32 = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE; -const TEMPORARY: u32 = FILE_ATTRIBUTE_TEMPORARY; -const DELETE_ON_CLOSE: u32 = FILE_FLAG_DELETE_ON_CLOSE; -const DELETE_ACCESS: u32 = windows_sys::Win32::Storage::FileSystem::DELETE; +const SHARE_ALL: fspy_nostd::fs::ShareMode = fspy_nostd::fs::ShareMode::READ + .union(fspy_nostd::fs::ShareMode::WRITE) + .union(fspy_nostd::fs::ShareMode::DELETE); /// Keeps the shared memory's identifier alive and removes it on drop. /// @@ -45,8 +34,8 @@ pub struct ShmKeeper { /// [`map`](Self::map) can be called more than once; every call returns another /// view of the same bytes. Drop the handle once the mappings exist. pub struct ShmHandle { - file: File, - size: usize, + file: fspy_nostd::OwnedHandle, + size: NonZeroUsize, } /// The mapped shared bytes. @@ -54,7 +43,8 @@ pub struct ShmHandle { /// A `Mapping` keeps the bytes alive until it is dropped and cannot affect the /// shared memory's identifier. pub struct Mapping { - raw: MmapRaw, + raw: fspy_nostd::mm::MappedView, + len: NonZeroUsize, } /// Creates `size` bytes of zero-initialized shared memory. @@ -70,37 +60,33 @@ pub struct Mapping { /// Returns an error if the shared memory cannot be created or sized. Creation /// fails on volumes without sparse-file support. pub fn create(size: usize) -> io::Result<(ShmKeeper, ShmHandle)> { - if size == 0 { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "shared-memory size must be nonzero", - )); - } - let size_u64 = u64::try_from(size).map_err(|_| { - io::Error::new(io::ErrorKind::InvalidInput, "shared-memory size exceeds u64") + let size = NonZeroUsize::new(size).ok_or_else(|| { + io::Error::new(io::ErrorKind::InvalidInput, "shared-memory size must be nonzero") + })?; + let size_i64 = i64::try_from(size.get()).map_err(|_| { + io::Error::new(io::ErrorKind::InvalidInput, "shared-memory size exceeds i64") })?; // The per-user `%TEMP%` ACL provides same-user gating. The identifier is // absolute so it keeps working after a working-directory change. let path = std::path::absolute(temp_dir())? .join(format!("{BACKING_PREFIX}{}.shm", Uuid::new_v4().simple())); - let file = OpenOptions::new() - .read(true) - .write(true) - .create_new(true) - .share_mode(SHARE_ALL) + let file = open_file( + path.as_os_str(), + fspy_nostd::fs::Access::READ | fspy_nostd::fs::Access::WRITE, + fspy_nostd::fs::CreationDisposition::CREATE_NEW, // Ask Windows to keep the data in memory when it can. - .attributes(TEMPORARY) - .open(&path)?; + fspy_nostd::fs::OpenFlags::TEMPORARY, + )?; // The keeper exists from here on, so every error path below cleans up. let keeper = ShmKeeper { path }; // NTFS allocates clusters for the whole logical size unless the file is // marked sparse first, which would turn the capacity into real disk usage. // Volumes without sparse-file support fail here. - set_sparse(&file)?; + fspy_nostd::fs::set_sparse(&file).map_err(error_to_io)?; // Every byte reads as zero because the file is all holes. - file.set_len(size_u64)?; + fspy_nostd::fs::set_len(&file, size_i64).map_err(error_to_io)?; Ok((keeper, ShmHandle { file, size })) } @@ -115,32 +101,68 @@ pub fn create(size: usize) -> io::Result<(ShmKeeper, ShmHandle)> { /// Returns an error if the shared memory is unavailable, which is the common /// case once its keeper has been dropped. pub fn open(id: &OsStr) -> io::Result { - // Rust handles are non-inheritable, and its default share mode permits - // concurrent read, write and delete access. - let file = OpenOptions::new().read(true).write(true).open(id)?; + let file = open_file( + id, + fspy_nostd::fs::Access::READ | fspy_nostd::fs::Access::WRITE, + fspy_nostd::fs::CreationDisposition::OPEN_EXISTING, + fspy_nostd::fs::OpenFlags::empty(), + )?; // If another process shrinks the file before `map`, mapping fails. If it // resizes afterwards, nothing here touches the mapped pages. A concurrent // resize cannot make a mapping access invalid memory. - let size = usize::try_from(file.metadata()?.len()) + let size = usize::try_from(fspy_nostd::fs::file_size(&file).map_err(error_to_io)?) .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "invalid shared-memory size"))?; - if size == 0 { - return Err(io::Error::new(io::ErrorKind::InvalidData, "shared-memory size is zero")); - } + let size = NonZeroUsize::new(size) + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "shared-memory size is zero"))?; Ok(ShmHandle { file, size }) } +fn open_file( + path: &OsStr, + access: fspy_nostd::fs::Access, + creation: fspy_nostd::fs::CreationDisposition, + flags: fspy_nostd::fs::OpenFlags, +) -> io::Result { + let path = copy_path(path)?; + fspy_nostd::fs::open(as_nostd_path(&path), access, SHARE_ALL, creation, flags) + .map_err(error_to_io) +} + +fn copy_path(path: &OsStr) -> io::Result> { + let mut units: Vec<_> = path.encode_wide().collect(); + if units.contains(&0) { + return Err(io::Error::new(io::ErrorKind::InvalidInput, "path contains NUL")); + } + units.push(0); + Ok(units) +} + +const fn as_nostd_path(path: &[u16]) -> fspy_nostd::WideCStr<'_, fspy_nostd::Fat> { + // SAFETY: `copy_path` rejects interior NUL and appends one terminator. + unsafe { fspy_nostd::WideCStr::from_units_with_nul_unchecked(path) } +} + +fn error_to_io(error: fspy_nostd::Error) -> io::Error { + io::Error::from_raw_os_error(error.raw_os_error().cast_signed()) +} + impl Drop for ShmKeeper { fn drop(&mut self) { // Windows versions without POSIX delete refuse to remove the name of a // mapped file. Arm the deferred delete instead: a handle opened with // `FILE_FLAG_DELETE_ON_CLOSE` deletes the file once every handle to it // is closed. - if fs::remove_file(&self.path).is_err() { - let _ = OpenOptions::new() - .access_mode(DELETE_ACCESS) - .share_mode(SHARE_ALL) - .custom_flags(DELETE_ON_CLOSE) - .open(&self.path); + let Ok(path) = copy_path(self.path.as_os_str()) else { + return; + }; + if fspy_nostd::fs::remove(as_nostd_path(&path)).is_err() { + let _ = fspy_nostd::fs::open( + as_nostd_path(&path), + fspy_nostd::fs::Access::DELETE, + SHARE_ALL, + fspy_nostd::fs::CreationDisposition::OPEN_EXISTING, + fspy_nostd::fs::OpenFlags::DELETE_ON_CLOSE, + ); } } } @@ -161,7 +183,11 @@ impl ShmHandle { /// /// Returns an error if the mapping cannot be established. pub fn map(&self) -> io::Result { - Ok(Mapping { raw: MmapOptions::new().len(self.size).map_raw(&self.file)? }) + let _slice_len = isize::try_from(self.size.get()).map_err(|_| { + io::Error::new(io::ErrorKind::InvalidData, "shared-memory size exceeds isize") + })?; + let raw = fspy_nostd::mm::map_file(&self.file, self.size).map_err(error_to_io)?; + Ok(Mapping { raw, len: self.size }) } } @@ -169,14 +195,14 @@ impl ShmHandle { impl Mapping { /// Returns the mapped length in bytes. #[must_use] - pub fn len(&self) -> usize { - self.raw.len() + pub const fn len(&self) -> usize { + self.len.get() } /// Returns a raw pointer to the first mapped byte. #[must_use] - pub fn as_ptr(&self) -> *mut u8 { - self.raw.as_mut_ptr() + pub const fn as_ptr(&self) -> *mut u8 { + self.raw.as_ptr() } /// Returns the mapped bytes as a shared slice. @@ -186,34 +212,13 @@ impl Mapping { /// The caller must ensure that no process or thread mutates the mapping for /// the lifetime of the returned slice. #[must_use] - pub unsafe fn as_slice(&self) -> &[u8] { + pub const unsafe fn as_slice(&self) -> &[u8] { // SAFETY: The mapping is valid for its full length, and the caller // guarantees that it is not mutated while the slice is borrowed. - unsafe { std::slice::from_raw_parts(self.as_ptr().cast_const(), self.len()) } + unsafe { core::slice::from_raw_parts(self.as_ptr().cast_const(), self.len()) } } } -/// Marks `file` sparse so that setting its length reserves no clusters. -fn set_sparse(file: &File) -> io::Result<()> { - let mut bytes_returned = 0; - // SAFETY: `file` supplies a valid synchronous file handle. FSCTL_SET_SPARSE - // requires no input or output buffers, and `bytes_returned` is writable for - // the duration of the call. - let result = unsafe { - DeviceIoControl( - file.as_raw_handle().cast(), - FSCTL_SET_SPARSE, - std::ptr::null(), - 0, - std::ptr::null_mut(), - 0, - &raw mut bytes_returned, - std::ptr::null_mut(), - ) - }; - if result == 0 { Err(io::Error::last_os_error()) } else { Ok(()) } -} - /// Returns the backing file's logical size and allocated byte count. #[cfg(test)] pub fn file_sizes(file: &File) -> io::Result<(u64, u64)> { From 173bbfb185f9589b147dca6b5a16190ebfb2eae4 Mon Sep 17 00:00:00 2001 From: wan9chi Date: Thu, 13 Aug 2026 13:09:37 +0800 Subject: [PATCH 02/18] fix(fspy-nostd): preserve POSIX Windows deletion Mirror the std remove_file fallback to FileDispositionInfoEx when DeleteFileW is denied, so dropping a shared-memory keeper removes the pathname while existing handles remain usable. Co-authored-by: GPT-5 Codex --- crates/fspy_nostd/src/fs/windows.rs | 59 +++++++++++++++++++++++++---- 1 file changed, 52 insertions(+), 7 deletions(-) diff --git a/crates/fspy_nostd/src/fs/windows.rs b/crates/fspy_nostd/src/fs/windows.rs index f0207fa2c..1b2d82bf8 100644 --- a/crates/fspy_nostd/src/fs/windows.rs +++ b/crates/fspy_nostd/src/fs/windows.rs @@ -2,12 +2,14 @@ use core::{ffi::c_void, mem::size_of, ptr}; use bitflags::bitflags; use windows_sys::Win32::{ - Foundation::{GENERIC_READ, GENERIC_WRITE, INVALID_HANDLE_VALUE}, + Foundation::{ERROR_ACCESS_DENIED, GENERIC_READ, GENERIC_WRITE, INVALID_HANDLE_VALUE}, Storage::FileSystem::{ CREATE_NEW, CreateFileW, DELETE, DeleteFileW, FILE_ATTRIBUTE_TEMPORARY, - FILE_END_OF_FILE_INFO, FILE_FLAG_DELETE_ON_CLOSE, FILE_SHARE_DELETE, FILE_SHARE_READ, - FILE_SHARE_WRITE, FileEndOfFileInfo, GetFileSizeEx, OPEN_EXISTING, - SetFileInformationByHandle, + FILE_DISPOSITION_FLAG_DELETE, FILE_DISPOSITION_FLAG_IGNORE_READONLY_ATTRIBUTE, + FILE_DISPOSITION_FLAG_POSIX_SEMANTICS, FILE_DISPOSITION_INFO_EX, FILE_END_OF_FILE_INFO, + FILE_FLAG_DELETE_ON_CLOSE, FILE_FLAG_OPEN_REPARSE_POINT, FILE_SHARE_DELETE, + FILE_SHARE_READ, FILE_SHARE_WRITE, FileDispositionInfoEx, FileEndOfFileInfo, GetFileSizeEx, + OPEN_EXISTING, SetFileInformationByHandle, }, System::{IO::DeviceIoControl, Ioctl::FSCTL_SET_SPARSE}, }; @@ -36,6 +38,7 @@ bitflags! { pub struct OpenFlags: u32 { const TEMPORARY = FILE_ATTRIBUTE_TEMPORARY; const DELETE_ON_CLOSE = FILE_FLAG_DELETE_ON_CLOSE; + const OPEN_REPARSE_POINT = FILE_FLAG_OPEN_REPARSE_POINT; } } @@ -89,11 +92,53 @@ pub fn open( /// /// # Errors /// -/// Returns the error reported by `DeleteFileW`. -#[expect(clippy::needless_pass_by_value, reason = "CStr is a borrowed value type")] +/// Returns the error reported while removing the file. pub fn remove(path: WideCStr<'_, R>) -> Result<()> { // SAFETY: `path` is a valid NUL-terminated wide string. - crate::windows::bool_result(unsafe { DeleteFileW(path.as_ptr()) }) + if unsafe { DeleteFileW(path.as_ptr()) } != 0 { + return Ok(()); + } + + let error = crate::windows::last_error(); + if error.raw_os_error() == ERROR_ACCESS_DENIED { + const SHARE_ALL: ShareMode = + ShareMode::READ.union(ShareMode::WRITE).union(ShareMode::DELETE); + if let Ok(file) = open( + path, + Access::DELETE, + SHARE_ALL, + CreationDisposition::OPEN_EXISTING, + OpenFlags::OPEN_REPARSE_POINT, + ) && set_posix_delete(&file).is_ok() + { + return Ok(()); + } + } + + // Match `std::fs::remove_file`: if the POSIX fallback is unavailable, + // preserve the original `DeleteFileW` error. + Err(error) +} + +fn set_posix_delete(file: &OwnedHandle) -> Result<()> { + const INFO_SIZE: u32 = 4; + const _: [(); 4] = [(); size_of::()]; + + let info = FILE_DISPOSITION_INFO_EX { + Flags: FILE_DISPOSITION_FLAG_DELETE + | FILE_DISPOSITION_FLAG_POSIX_SEMANTICS + | FILE_DISPOSITION_FLAG_IGNORE_READONLY_ATTRIBUTE, + }; + // SAFETY: `file` is valid and `info` has the type and exact size required + // by `FileDispositionInfoEx`. + crate::windows::bool_result(unsafe { + SetFileInformationByHandle( + file.as_raw(), + FileDispositionInfoEx, + (&raw const info).cast::(), + INFO_SIZE, + ) + }) } /// Returns a file's logical length. From 9757e413522fb0daf423d3c8f1fe173a7b4bbaf7 Mon Sep 17 00:00:00 2001 From: wan9chi Date: Thu, 13 Aug 2026 13:34:02 +0800 Subject: [PATCH 03/18] refactor(fspy-nostd): expose direct Windows mapping calls Replace the policy-heavy map_file helper with one-to-one CreateFileMappingW and MapViewOfFile wrappers. Keep MappingView as the owned RAII view and move shared-memory policy into fspy_shm. Co-authored-by: GPT-5 Codex --- crates/fspy_nostd/src/mm.rs | 87 ++++++++++++++++++++++++---------- crates/fspy_shm/src/windows.rs | 31 ++++++++++-- 2 files changed, 89 insertions(+), 29 deletions(-) diff --git a/crates/fspy_nostd/src/mm.rs b/crates/fspy_nostd/src/mm.rs index 3bb846128..5d212d268 100644 --- a/crates/fspy_nostd/src/mm.rs +++ b/crates/fspy_nostd/src/mm.rs @@ -13,27 +13,32 @@ pub use rustix::mm::{MapFlags, MprotectFlags, ProtFlags, mmap, mmap_anonymous, m #[cfg(windows)] mod windows { - use core::{ffi::c_void, num::NonZeroUsize, ptr}; + use core::ffi::c_void; use windows_sys::Win32::System::Memory::{ - CreateFileMappingW, FILE_MAP_READ, FILE_MAP_WRITE, MEMORY_MAPPED_VIEW_ADDRESS, - MapViewOfFile, PAGE_READWRITE, UnmapViewOfFile, + CreateFileMappingW, MEMORY_MAPPED_VIEW_ADDRESS, MapViewOfFile, UnmapViewOfFile, + }; + pub use windows_sys::Win32::{ + Security::SECURITY_ATTRIBUTES as SecurityAttributes, + System::Memory::{ + FILE_MAP, FILE_MAP_READ, FILE_MAP_WRITE, PAGE_PROTECTION_FLAGS, PAGE_READWRITE, + }, }; use crate::{OwnedHandle, Result}; - /// An owned writable view of a shared file mapping. - pub struct MappedView { + /// An owned view of a file-mapping object. + pub struct MappingView { ptr: core::ptr::NonNull, } // SAFETY: a view owns no thread-affine state. Synchronization of accesses // to its shared bytes is the caller's responsibility. - unsafe impl Send for MappedView {} + unsafe impl Send for MappingView {} // SAFETY: sharing the view does not itself access the mapped bytes. - unsafe impl Sync for MappedView {} + unsafe impl Sync for MappingView {} - impl MappedView { + impl MappingView { /// Returns a raw pointer to the first mapped byte. #[must_use] pub const fn as_ptr(&self) -> *mut u8 { @@ -41,7 +46,7 @@ mod windows { } } - impl Drop for MappedView { + impl Drop for MappingView { fn drop(&mut self) { // SAFETY: this is the complete view owned by `self` and is // unmapped exactly once. @@ -53,38 +58,70 @@ mod windows { } } - /// Maps the first `len` bytes of `file` as shared readable and writable - /// memory. + /// Calls `CreateFileMappingW` and returns the new mapping-object handle. /// /// # Errors /// - /// Returns the error reported while creating or mapping the view. - pub fn map_file(file: &OwnedHandle, len: NonZeroUsize) -> Result { - // SAFETY: `file` is valid. Null security attributes and name request - // an unnamed, non-inheritable mapping object backed by the file's - // current size. + /// Returns the error reported by `CreateFileMappingW`. + /// + /// # Safety + /// + /// `mapping_attributes` must be null or point to a valid + /// [`SecurityAttributes`] value for the duration of the call. `name` must + /// be null or point to a valid NUL-terminated UTF-16 string. + pub unsafe fn create_file_mapping( + file: &OwnedHandle, + mapping_attributes: *const SecurityAttributes, + protection: PAGE_PROTECTION_FLAGS, + maximum_size_high: u32, + maximum_size_low: u32, + name: *const u16, + ) -> Result { + // SAFETY: `file` is valid and the caller upholds both pointer + // contracts. The remaining values are passed through unchanged. let mapping = unsafe { - CreateFileMappingW(file.as_raw(), ptr::null(), PAGE_READWRITE, 0, 0, ptr::null()) + CreateFileMappingW( + file.as_raw(), + mapping_attributes, + protection, + maximum_size_high, + maximum_size_low, + name, + ) }; let Some(mapping) = core::ptr::NonNull::new(mapping) else { return Err(crate::windows::last_error()); }; // SAFETY: `CreateFileMappingW` returned a valid, newly owned handle. - let mapping = unsafe { OwnedHandle::from_raw(mapping.as_ptr()) }; + Ok(unsafe { OwnedHandle::from_raw(mapping.as_ptr()) }) + } - // SAFETY: `mapping` is a valid file-mapping object, the requested - // access matches its protection, and `len` is nonzero. + /// Calls `MapViewOfFile` and returns the new owned view. + /// + /// # Errors + /// + /// Returns the error reported by `MapViewOfFile`. + pub fn map_view_of_file( + mapping: &OwnedHandle, + access: FILE_MAP, + file_offset_high: u32, + file_offset_low: u32, + bytes_to_map: usize, + ) -> Result { + // SAFETY: `mapping` is a valid file-mapping object. Windows validates + // the requested access, offset, and size against that object. let view = unsafe { - MapViewOfFile(mapping.as_raw(), FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, len.get()) + MapViewOfFile(mapping.as_raw(), access, file_offset_high, file_offset_low, bytes_to_map) }; let Some(ptr) = core::ptr::NonNull::new(view.Value.cast::()) else { return Err(crate::windows::last_error()); }; - - // A mapped view remains valid after its mapping-object handle closes. - Ok(MappedView { ptr }) + Ok(MappingView { ptr }) } } #[cfg(windows)] -pub use windows::{MappedView, map_file}; +pub use windows::{ + FILE_MAP, FILE_MAP_READ, FILE_MAP_WRITE, MappingView, PAGE_PROTECTION_FLAGS, PAGE_READWRITE, + SecurityAttributes, create_file_mapping, map_view_of_file, +}; diff --git a/crates/fspy_shm/src/windows.rs b/crates/fspy_shm/src/windows.rs index d4f52d957..62fb507fd 100644 --- a/crates/fspy_shm/src/windows.rs +++ b/crates/fspy_shm/src/windows.rs @@ -1,6 +1,7 @@ //! Windows shared memory backed by a sparse temporary file and identified by //! its path. +use core::ptr; use std::{ env::temp_dir, ffi::OsStr, io, num::NonZeroUsize, os::windows::ffi::OsStrExt as _, path::PathBuf, @@ -43,7 +44,7 @@ pub struct ShmHandle { /// A `Mapping` keeps the bytes alive until it is dropped and cannot affect the /// shared memory's identifier. pub struct Mapping { - raw: fspy_nostd::mm::MappedView, + view: fspy_nostd::mm::MappingView, len: NonZeroUsize, } @@ -186,8 +187,30 @@ impl ShmHandle { let _slice_len = isize::try_from(self.size.get()).map_err(|_| { io::Error::new(io::ErrorKind::InvalidData, "shared-memory size exceeds isize") })?; - let raw = fspy_nostd::mm::map_file(&self.file, self.size).map_err(error_to_io)?; - Ok(Mapping { raw, len: self.size }) + // SAFETY: null attributes create a non-inheritable mapping object and + // a null name creates an unnamed one. Both maximum-size halves are + // zero, so Windows uses the current file size. + let mapping = unsafe { + fspy_nostd::mm::create_file_mapping( + &self.file, + ptr::null(), + fspy_nostd::mm::PAGE_READWRITE, + 0, + 0, + ptr::null(), + ) + } + .map_err(error_to_io)?; + let view = fspy_nostd::mm::map_view_of_file( + &mapping, + fspy_nostd::mm::FILE_MAP_READ | fspy_nostd::mm::FILE_MAP_WRITE, + 0, + 0, + self.size.get(), + ) + .map_err(error_to_io)?; + // The view remains valid after its mapping-object handle closes. + Ok(Mapping { view, len: self.size }) } } @@ -202,7 +225,7 @@ impl Mapping { /// Returns a raw pointer to the first mapped byte. #[must_use] pub const fn as_ptr(&self) -> *mut u8 { - self.raw.as_ptr() + self.view.as_ptr() } /// Returns the mapped bytes as a shared slice. From fd82a42da0be79cea295e7b17685ff3f87b40aba Mon Sep 17 00:00:00 2001 From: wan9chi Date: Thu, 13 Aug 2026 13:39:52 +0800 Subject: [PATCH 04/18] refactor(fspy-nostd): expose direct Windows deletion calls Co-authored-by: GPT-5 Codex --- crates/fspy_nostd/src/fs/mod.rs | 5 +- crates/fspy_nostd/src/fs/windows.rs | 94 +++++++++++++---------------- crates/fspy_shm/src/windows.rs | 48 ++++++++++++++- 3 files changed, 93 insertions(+), 54 deletions(-) diff --git a/crates/fspy_nostd/src/fs/mod.rs b/crates/fspy_nostd/src/fs/mod.rs index 88594a223..f9e9e1fca 100644 --- a/crates/fspy_nostd/src/fs/mod.rs +++ b/crates/fspy_nostd/src/fs/mod.rs @@ -13,7 +13,10 @@ use crate::{BorrowedFd, CStr, Fat, OwnedFd, Result}; mod windows; #[cfg(windows)] pub use windows::{ - Access, CreationDisposition, OpenFlags, ShareMode, file_size, open, remove, set_len, set_sparse, + Access, CreationDisposition, ERROR_ACCESS_DENIED, FILE_DISPOSITION_FLAG_DELETE, + FILE_DISPOSITION_FLAG_IGNORE_READONLY_ATTRIBUTE, FILE_DISPOSITION_FLAG_POSIX_SEMANTICS, + FILE_DISPOSITION_INFO_EX, FILE_INFO_BY_HANDLE_CLASS, FileDispositionInfoEx, OpenFlags, + ShareMode, delete_file, file_size, open, set_file_information_by_handle, set_len, set_sparse, }; #[cfg(target_os = "linux")] diff --git a/crates/fspy_nostd/src/fs/windows.rs b/crates/fspy_nostd/src/fs/windows.rs index 1b2d82bf8..f37dcceb8 100644 --- a/crates/fspy_nostd/src/fs/windows.rs +++ b/crates/fspy_nostd/src/fs/windows.rs @@ -1,14 +1,20 @@ use core::{ffi::c_void, mem::size_of, ptr}; use bitflags::bitflags; +pub use windows_sys::Win32::{ + Foundation::ERROR_ACCESS_DENIED, + Storage::FileSystem::{ + FILE_DISPOSITION_FLAG_DELETE, FILE_DISPOSITION_FLAG_IGNORE_READONLY_ATTRIBUTE, + FILE_DISPOSITION_FLAG_POSIX_SEMANTICS, FILE_DISPOSITION_INFO_EX, FILE_INFO_BY_HANDLE_CLASS, + FileDispositionInfoEx, + }, +}; use windows_sys::Win32::{ - Foundation::{ERROR_ACCESS_DENIED, GENERIC_READ, GENERIC_WRITE, INVALID_HANDLE_VALUE}, + Foundation::{GENERIC_READ, GENERIC_WRITE, INVALID_HANDLE_VALUE}, Storage::FileSystem::{ CREATE_NEW, CreateFileW, DELETE, DeleteFileW, FILE_ATTRIBUTE_TEMPORARY, - FILE_DISPOSITION_FLAG_DELETE, FILE_DISPOSITION_FLAG_IGNORE_READONLY_ATTRIBUTE, - FILE_DISPOSITION_FLAG_POSIX_SEMANTICS, FILE_DISPOSITION_INFO_EX, FILE_END_OF_FILE_INFO, - FILE_FLAG_DELETE_ON_CLOSE, FILE_FLAG_OPEN_REPARSE_POINT, FILE_SHARE_DELETE, - FILE_SHARE_READ, FILE_SHARE_WRITE, FileDispositionInfoEx, FileEndOfFileInfo, GetFileSizeEx, + FILE_END_OF_FILE_INFO, FILE_FLAG_DELETE_ON_CLOSE, FILE_FLAG_OPEN_REPARSE_POINT, + FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE, FileEndOfFileInfo, GetFileSizeEx, OPEN_EXISTING, SetFileInformationByHandle, }, System::{IO::DeviceIoControl, Ioctl::FSCTL_SET_SPARSE}, @@ -88,55 +94,41 @@ pub fn open( } } -/// Removes a file name. +/// Calls `DeleteFileW`. /// /// # Errors /// -/// Returns the error reported while removing the file. -pub fn remove(path: WideCStr<'_, R>) -> Result<()> { +/// Returns the error reported by `DeleteFileW`. +#[expect(clippy::needless_pass_by_value, reason = "CStr is a borrowed value type")] +pub fn delete_file(path: WideCStr<'_, R>) -> Result<()> { // SAFETY: `path` is a valid NUL-terminated wide string. - if unsafe { DeleteFileW(path.as_ptr()) } != 0 { - return Ok(()); - } - - let error = crate::windows::last_error(); - if error.raw_os_error() == ERROR_ACCESS_DENIED { - const SHARE_ALL: ShareMode = - ShareMode::READ.union(ShareMode::WRITE).union(ShareMode::DELETE); - if let Ok(file) = open( - path, - Access::DELETE, - SHARE_ALL, - CreationDisposition::OPEN_EXISTING, - OpenFlags::OPEN_REPARSE_POINT, - ) && set_posix_delete(&file).is_ok() - { - return Ok(()); - } - } - - // Match `std::fs::remove_file`: if the POSIX fallback is unavailable, - // preserve the original `DeleteFileW` error. - Err(error) + crate::windows::bool_result(unsafe { DeleteFileW(path.as_ptr()) }) } -fn set_posix_delete(file: &OwnedHandle) -> Result<()> { - const INFO_SIZE: u32 = 4; - const _: [(); 4] = [(); size_of::()]; - - let info = FILE_DISPOSITION_INFO_EX { - Flags: FILE_DISPOSITION_FLAG_DELETE - | FILE_DISPOSITION_FLAG_POSIX_SEMANTICS - | FILE_DISPOSITION_FLAG_IGNORE_READONLY_ATTRIBUTE, - }; - // SAFETY: `file` is valid and `info` has the type and exact size required - // by `FileDispositionInfoEx`. +/// Calls `SetFileInformationByHandle`. +/// +/// # Errors +/// +/// Returns the error reported by `SetFileInformationByHandle`. +/// +/// # Safety +/// +/// `file_information` must point to an initialized buffer whose type and size +/// match `file_information_class`, and it must remain valid for the call. +pub unsafe fn set_file_information_by_handle( + file: &OwnedHandle, + file_information_class: FILE_INFO_BY_HANDLE_CLASS, + file_information: *const c_void, + buffer_size: u32, +) -> Result<()> { + // SAFETY: `file` is valid and the caller upholds the information-buffer + // contract. The remaining values are passed through unchanged. crate::windows::bool_result(unsafe { SetFileInformationByHandle( file.as_raw(), - FileDispositionInfoEx, - (&raw const info).cast::(), - INFO_SIZE, + file_information_class, + file_information, + buffer_size, ) }) } @@ -163,16 +155,16 @@ pub fn set_len(file: &OwnedHandle, len: i64) -> Result<()> { const _: [(); 8] = [(); size_of::()]; let info = FILE_END_OF_FILE_INFO { EndOfFile: len }; - // SAFETY: `file` is valid and `info` has the type and exact size required - // by `FileEndOfFileInfo`. - crate::windows::bool_result(unsafe { - SetFileInformationByHandle( - file.as_raw(), + // SAFETY: `info` has the type and exact size required by + // `FileEndOfFileInfo`. + unsafe { + set_file_information_by_handle( + file, FileEndOfFileInfo, (&raw const info).cast::(), INFO_SIZE, ) - }) + } } /// Marks a file sparse. diff --git a/crates/fspy_shm/src/windows.rs b/crates/fspy_shm/src/windows.rs index 62fb507fd..794e4178b 100644 --- a/crates/fspy_shm/src/windows.rs +++ b/crates/fspy_shm/src/windows.rs @@ -1,7 +1,7 @@ //! Windows shared memory backed by a sparse temporary file and identified by //! its path. -use core::ptr; +use core::{ffi::c_void, mem::size_of, ptr}; use std::{ env::temp_dir, ffi::OsStr, io, num::NonZeroUsize, os::windows::ffi::OsStrExt as _, path::PathBuf, @@ -147,6 +147,50 @@ fn error_to_io(error: fspy_nostd::Error) -> io::Error { io::Error::from_raw_os_error(error.raw_os_error().cast_signed()) } +fn remove_file(path: fspy_nostd::WideCStr<'_, fspy_nostd::Fat>) -> fspy_nostd::Result<()> { + let error = match fspy_nostd::fs::delete_file(path) { + Ok(()) => return Ok(()), + Err(error) => error, + }; + if error.raw_os_error() == fspy_nostd::fs::ERROR_ACCESS_DENIED + && let Ok(file) = fspy_nostd::fs::open( + path, + fspy_nostd::fs::Access::DELETE, + SHARE_ALL, + fspy_nostd::fs::CreationDisposition::OPEN_EXISTING, + fspy_nostd::fs::OpenFlags::OPEN_REPARSE_POINT, + ) + && set_posix_delete(&file).is_ok() + { + return Ok(()); + } + + // Preserve the original `DeleteFileW` error when POSIX deletion is not + // available. + Err(error) +} + +fn set_posix_delete(file: &fspy_nostd::OwnedHandle) -> fspy_nostd::Result<()> { + const INFO_SIZE: u32 = 4; + const _: [(); 4] = [(); size_of::()]; + + let info = fspy_nostd::fs::FILE_DISPOSITION_INFO_EX { + Flags: fspy_nostd::fs::FILE_DISPOSITION_FLAG_DELETE + | fspy_nostd::fs::FILE_DISPOSITION_FLAG_POSIX_SEMANTICS + | fspy_nostd::fs::FILE_DISPOSITION_FLAG_IGNORE_READONLY_ATTRIBUTE, + }; + // SAFETY: `info` has the type and exact size required by + // `FileDispositionInfoEx`. + unsafe { + fspy_nostd::fs::set_file_information_by_handle( + file, + fspy_nostd::fs::FileDispositionInfoEx, + (&raw const info).cast::(), + INFO_SIZE, + ) + } +} + impl Drop for ShmKeeper { fn drop(&mut self) { // Windows versions without POSIX delete refuse to remove the name of a @@ -156,7 +200,7 @@ impl Drop for ShmKeeper { let Ok(path) = copy_path(self.path.as_os_str()) else { return; }; - if fspy_nostd::fs::remove(as_nostd_path(&path)).is_err() { + if remove_file(as_nostd_path(&path)).is_err() { let _ = fspy_nostd::fs::open( as_nostd_path(&path), fspy_nostd::fs::Access::DELETE, From 4276b9e6aa6a947e49a20ab0607440c724cc1560 Mon Sep 17 00:00:00 2001 From: wan9chi Date: Thu, 13 Aug 2026 13:57:10 +0800 Subject: [PATCH 05/18] refactor(fspy-nostd): make file mapping safe Co-authored-by: GPT-5 Codex --- crates/fspy_nostd/src/mm.rs | 43 ++++++++++++++++++++-------------- crates/fspy_shm/src/windows.rs | 24 +++++++++---------- 2 files changed, 37 insertions(+), 30 deletions(-) diff --git a/crates/fspy_nostd/src/mm.rs b/crates/fspy_nostd/src/mm.rs index 5d212d268..62994553a 100644 --- a/crates/fspy_nostd/src/mm.rs +++ b/crates/fspy_nostd/src/mm.rs @@ -13,19 +13,27 @@ pub use rustix::mm::{MapFlags, MprotectFlags, ProtFlags, mmap, mmap_anonymous, m #[cfg(windows)] mod windows { - use core::ffi::c_void; + use core::{ffi::c_void, ptr}; - use windows_sys::Win32::System::Memory::{ - CreateFileMappingW, MEMORY_MAPPED_VIEW_ADDRESS, MapViewOfFile, UnmapViewOfFile, + pub use windows_sys::Win32::System::Memory::{ + FILE_MAP, FILE_MAP_READ, FILE_MAP_WRITE, PAGE_PROTECTION_FLAGS, PAGE_READWRITE, }; - pub use windows_sys::Win32::{ - Security::SECURITY_ATTRIBUTES as SecurityAttributes, + use windows_sys::Win32::{ + Security::SECURITY_ATTRIBUTES, System::Memory::{ - FILE_MAP, FILE_MAP_READ, FILE_MAP_WRITE, PAGE_PROTECTION_FLAGS, PAGE_READWRITE, + CreateFileMappingW, MEMORY_MAPPED_VIEW_ADDRESS, MapViewOfFile, UnmapViewOfFile, }, }; - use crate::{OwnedHandle, Result}; + use crate::{OwnedHandle, Result, WideCStr}; + + /// Opaque security attributes accepted by `CreateFileMappingW`. + /// + /// This type cannot be constructed outside `fspy_nostd`. No constructor + /// is exposed until a caller needs non-default security attributes and + /// their embedded security descriptor can be represented safely. + #[repr(transparent)] + pub struct SecurityAttributes(SECURITY_ATTRIBUTES); /// An owned view of a file-mapping object. pub struct MappingView { @@ -64,21 +72,22 @@ mod windows { /// /// Returns the error reported by `CreateFileMappingW`. /// - /// # Safety - /// - /// `mapping_attributes` must be null or point to a valid - /// [`SecurityAttributes`] value for the duration of the call. `name` must - /// be null or point to a valid NUL-terminated UTF-16 string. - pub unsafe fn create_file_mapping( + pub fn create_file_mapping( file: &OwnedHandle, - mapping_attributes: *const SecurityAttributes, + mapping_attributes: Option<&SecurityAttributes>, protection: PAGE_PROTECTION_FLAGS, maximum_size_high: u32, maximum_size_low: u32, - name: *const u16, + name: Option>, ) -> Result { - // SAFETY: `file` is valid and the caller upholds both pointer - // contracts. The remaining values are passed through unchanged. + let mapping_attributes = + mapping_attributes.map_or(ptr::null(), |attributes| ptr::from_ref(&attributes.0)); + let name = name.map_or(ptr::null(), |name| name.as_ptr()); + + // SAFETY: `file` is valid. Security attributes are either null or a + // valid opaque value owned by this module, and `name` is either null + // or backed by a valid borrowed wide C string. The remaining values + // are passed through unchanged. let mapping = unsafe { CreateFileMappingW( file.as_raw(), diff --git a/crates/fspy_shm/src/windows.rs b/crates/fspy_shm/src/windows.rs index 794e4178b..a8d04ab4c 100644 --- a/crates/fspy_shm/src/windows.rs +++ b/crates/fspy_shm/src/windows.rs @@ -1,7 +1,7 @@ //! Windows shared memory backed by a sparse temporary file and identified by //! its path. -use core::{ffi::c_void, mem::size_of, ptr}; +use core::{ffi::c_void, mem::size_of}; use std::{ env::temp_dir, ffi::OsStr, io, num::NonZeroUsize, os::windows::ffi::OsStrExt as _, path::PathBuf, @@ -231,19 +231,17 @@ impl ShmHandle { let _slice_len = isize::try_from(self.size.get()).map_err(|_| { io::Error::new(io::ErrorKind::InvalidData, "shared-memory size exceeds isize") })?; - // SAFETY: null attributes create a non-inheritable mapping object and - // a null name creates an unnamed one. Both maximum-size halves are + // Default security attributes create a non-inheritable mapping object + // and no name creates an unnamed one. Both maximum-size halves are // zero, so Windows uses the current file size. - let mapping = unsafe { - fspy_nostd::mm::create_file_mapping( - &self.file, - ptr::null(), - fspy_nostd::mm::PAGE_READWRITE, - 0, - 0, - ptr::null(), - ) - } + let mapping = fspy_nostd::mm::create_file_mapping::( + &self.file, + None, + fspy_nostd::mm::PAGE_READWRITE, + 0, + 0, + None, + ) .map_err(error_to_io)?; let view = fspy_nostd::mm::map_view_of_file( &mapping, From 554f1457825347e8cd52608cec00f96761485488 Mon Sep 17 00:00:00 2001 From: wan9chi Date: Thu, 13 Aug 2026 14:05:35 +0800 Subject: [PATCH 06/18] refactor(fspy-nostd): expose safe low-level Windows APIs Co-authored-by: GPT-5 Codex --- Cargo.lock | 1 - crates/fspy_nostd/Cargo.toml | 1 - crates/fspy_nostd/src/fs/mod.rs | 11 +- crates/fspy_nostd/src/fs/windows.rs | 250 +++++++++++++++------------- crates/fspy_nostd/src/lib.rs | 2 +- crates/fspy_nostd/src/mm.rs | 24 +-- crates/fspy_nostd/src/windows.rs | 33 +++- crates/fspy_shm/src/windows.rs | 98 ++++++----- 8 files changed, 243 insertions(+), 177 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b6a18250e..590408765 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1345,7 +1345,6 @@ name = "fspy_nostd" version = "0.0.0" dependencies = [ "atoi", - "bitflags 2.10.0", "bstr", "libc", "rustix", diff --git a/crates/fspy_nostd/Cargo.toml b/crates/fspy_nostd/Cargo.toml index 9fbef3962..85dcf7c5d 100644 --- a/crates/fspy_nostd/Cargo.toml +++ b/crates/fspy_nostd/Cargo.toml @@ -32,7 +32,6 @@ rustix = { workspace = true, features = ["runtime"] } syscalls = { workspace = true } [target.'cfg(windows)'.dependencies] -bitflags = { workspace = true } windows-sys = { workspace = true, features = [ "Win32_Foundation", "Win32_Security", diff --git a/crates/fspy_nostd/src/fs/mod.rs b/crates/fspy_nostd/src/fs/mod.rs index f9e9e1fca..999099b47 100644 --- a/crates/fspy_nostd/src/fs/mod.rs +++ b/crates/fspy_nostd/src/fs/mod.rs @@ -13,10 +13,15 @@ use crate::{BorrowedFd, CStr, Fat, OwnedFd, Result}; mod windows; #[cfg(windows)] pub use windows::{ - Access, CreationDisposition, ERROR_ACCESS_DENIED, FILE_DISPOSITION_FLAG_DELETE, + CREATE_NEW, DELETE, DeviceIoControlCode, ERROR_ACCESS_DENIED, FILE_ATTRIBUTE_TEMPORARY, + FILE_CREATION_DISPOSITION, FILE_DISPOSITION_FLAG_DELETE, FILE_DISPOSITION_FLAG_IGNORE_READONLY_ATTRIBUTE, FILE_DISPOSITION_FLAG_POSIX_SEMANTICS, - FILE_DISPOSITION_INFO_EX, FILE_INFO_BY_HANDLE_CLASS, FileDispositionInfoEx, OpenFlags, - ShareMode, delete_file, file_size, open, set_file_information_by_handle, set_len, set_sparse, + FILE_DISPOSITION_INFO_EX, FILE_END_OF_FILE_INFO, FILE_FLAG_DELETE_ON_CLOSE, + FILE_FLAG_OPEN_REPARSE_POINT, FILE_FLAGS_AND_ATTRIBUTES, FILE_INFO_BY_HANDLE_CLASS, + FILE_SET_SPARSE_BUFFER, FILE_SHARE_DELETE, FILE_SHARE_MODE, FILE_SHARE_READ, FILE_SHARE_WRITE, + FSCTL_SET_SPARSE, FileDispositionInfoEx, FileEndOfFileInfo, FileInformationClass, GENERIC_READ, + GENERIC_WRITE, OPEN_EXISTING, create_file, delete_file, device_io_control, get_file_size, + set_file_information_by_handle, }; #[cfg(target_os = "linux")] diff --git a/crates/fspy_nostd/src/fs/windows.rs b/crates/fspy_nostd/src/fs/windows.rs index f37dcceb8..c88ebe4ac 100644 --- a/crates/fspy_nostd/src/fs/windows.rs +++ b/crates/fspy_nostd/src/fs/windows.rs @@ -1,89 +1,120 @@ -use core::{ffi::c_void, mem::size_of, ptr}; +use core::{ffi::c_void, marker::PhantomData, mem::size_of, ptr}; -use bitflags::bitflags; -pub use windows_sys::Win32::{ - Foundation::ERROR_ACCESS_DENIED, +use windows_sys::Win32::{ + Foundation::INVALID_HANDLE_VALUE, Storage::FileSystem::{ - FILE_DISPOSITION_FLAG_DELETE, FILE_DISPOSITION_FLAG_IGNORE_READONLY_ATTRIBUTE, - FILE_DISPOSITION_FLAG_POSIX_SEMANTICS, FILE_DISPOSITION_INFO_EX, FILE_INFO_BY_HANDLE_CLASS, - FileDispositionInfoEx, + CreateFileW, DeleteFileW, FileDispositionInfoEx as RAW_FILE_DISPOSITION_INFO_EX, + FileEndOfFileInfo as RAW_FILE_END_OF_FILE_INFO, GetFileSizeEx, SetFileInformationByHandle, }, + System::{IO::DeviceIoControl, Ioctl::FSCTL_SET_SPARSE as RAW_FSCTL_SET_SPARSE}, }; -use windows_sys::Win32::{ - Foundation::{GENERIC_READ, GENERIC_WRITE, INVALID_HANDLE_VALUE}, +pub use windows_sys::Win32::{ + Foundation::{ERROR_ACCESS_DENIED, GENERIC_READ, GENERIC_WRITE}, Storage::FileSystem::{ - CREATE_NEW, CreateFileW, DELETE, DeleteFileW, FILE_ATTRIBUTE_TEMPORARY, - FILE_END_OF_FILE_INFO, FILE_FLAG_DELETE_ON_CLOSE, FILE_FLAG_OPEN_REPARSE_POINT, - FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE, FileEndOfFileInfo, GetFileSizeEx, - OPEN_EXISTING, SetFileInformationByHandle, + CREATE_NEW, DELETE, FILE_ATTRIBUTE_TEMPORARY, FILE_CREATION_DISPOSITION, + FILE_DISPOSITION_FLAG_DELETE, FILE_DISPOSITION_FLAG_IGNORE_READONLY_ATTRIBUTE, + FILE_DISPOSITION_FLAG_POSIX_SEMANTICS, FILE_DISPOSITION_INFO_EX, FILE_END_OF_FILE_INFO, + FILE_FLAG_DELETE_ON_CLOSE, FILE_FLAG_OPEN_REPARSE_POINT, FILE_FLAGS_AND_ATTRIBUTES, + FILE_INFO_BY_HANDLE_CLASS, FILE_SHARE_DELETE, FILE_SHARE_MODE, FILE_SHARE_READ, + FILE_SHARE_WRITE, OPEN_EXISTING, }, - System::{IO::DeviceIoControl, Ioctl::FSCTL_SET_SPARSE}, + System::Ioctl::FILE_SET_SPARSE_BUFFER, }; -use crate::{OwnedHandle, Result, WideCStr}; +use crate::{Overlapped, OwnedHandle, Result, SecurityAttributes, WideCStr}; -bitflags! { - /// Access rights requested when opening a file. - #[derive(Clone, Copy, Debug, Eq, PartialEq)] - pub struct Access: u32 { - const READ = GENERIC_READ; - const WRITE = GENERIC_WRITE; - const DELETE = DELETE; - } +/// A file-information class tied to its buffer type and size. +/// +/// Values are provided by this module for the Win32 classes it supports. The +/// private fields prevent safe code from pairing a class with the wrong +/// buffer representation. +pub struct FileInformationClass { + raw: FILE_INFO_BY_HANDLE_CLASS, + size: u32, + marker: PhantomData, +} - /// Operations that other handles may perform while a file is open. - #[derive(Clone, Copy, Debug, Eq, PartialEq)] - pub struct ShareMode: u32 { - const READ = FILE_SHARE_READ; - const WRITE = FILE_SHARE_WRITE; - const DELETE = FILE_SHARE_DELETE; +impl Clone for FileInformationClass { + fn clone(&self) -> Self { + *self } +} + +impl Copy for FileInformationClass {} + +const _: [(); 4] = [(); size_of::()]; +const _: [(); 8] = [(); size_of::()]; + +/// The `FileDispositionInfoEx` class and its buffer representation. +#[expect(non_upper_case_globals, reason = "matches the Win32 class name")] +pub const FileDispositionInfoEx: FileInformationClass = + FileInformationClass { raw: RAW_FILE_DISPOSITION_INFO_EX, size: 4, marker: PhantomData }; + +/// The `FileEndOfFileInfo` class and its buffer representation. +#[expect(non_upper_case_globals, reason = "matches the Win32 class name")] +pub const FileEndOfFileInfo: FileInformationClass = + FileInformationClass { raw: RAW_FILE_END_OF_FILE_INFO, size: 8, marker: PhantomData }; - /// File attributes and flags applied while opening a file. - #[derive(Clone, Copy, Debug, Eq, PartialEq)] - pub struct OpenFlags: u32 { - const TEMPORARY = FILE_ATTRIBUTE_TEMPORARY; - const DELETE_ON_CLOSE = FILE_FLAG_DELETE_ON_CLOSE; - const OPEN_REPARSE_POINT = FILE_FLAG_OPEN_REPARSE_POINT; +/// A device-control code tied to its input and output buffer types and sizes. +/// +/// Values are provided by this module for the controls it supports. The +/// private fields prevent safe code from changing the code or its buffer +/// contract. +pub struct DeviceIoControlCode { + raw: u32, + input_size: u32, + output_size: u32, + marker: PhantomData, +} + +impl Clone for DeviceIoControlCode { + fn clone(&self) -> Self { + *self } } -/// Controls whether opening a file creates it or requires it to exist. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct CreationDisposition(u32); +impl Copy for DeviceIoControlCode {} -impl CreationDisposition { - pub const CREATE_NEW: Self = Self(CREATE_NEW); - pub const OPEN_EXISTING: Self = Self(OPEN_EXISTING); -} +const _: [(); 1] = [(); size_of::()]; -/// Opens a file and returns its owned handle. -/// -/// The returned handle is non-inheritable because this function supplies no -/// security attributes. +/// The `FSCTL_SET_SPARSE` control and its buffer representations. +pub const FSCTL_SET_SPARSE: DeviceIoControlCode = DeviceIoControlCode { + raw: RAW_FSCTL_SET_SPARSE, + input_size: 1, + output_size: 0, + marker: PhantomData, +}; + +/// Calls `CreateFileW` and returns the new owned handle. /// /// # Errors /// /// Returns the error reported by `CreateFileW`. #[expect(clippy::needless_pass_by_value, reason = "CStr is a borrowed value type")] -pub fn open( +pub fn create_file( path: WideCStr<'_, R>, - access: Access, - share_mode: ShareMode, - creation: CreationDisposition, - flags: OpenFlags, + desired_access: u32, + share_mode: FILE_SHARE_MODE, + security_attributes: Option<&SecurityAttributes>, + creation_disposition: FILE_CREATION_DISPOSITION, + flags_and_attributes: FILE_FLAGS_AND_ATTRIBUTES, + template_file: Option<&OwnedHandle>, ) -> Result { - // SAFETY: `path` is NUL-terminated. Null security attributes make the - // handle non-inheritable, and the null template is permitted. + let security_attributes = security_attributes.map_or(ptr::null(), SecurityAttributes::as_raw); + let template_file = template_file.map_or(ptr::null_mut(), OwnedHandle::as_raw); + + // SAFETY: `path` is NUL-terminated. Both optional pointers are either null + // or backed by their valid borrowed wrapper types. Other arguments are + // passed through unchanged for Windows to validate. let handle = unsafe { CreateFileW( path.as_ptr(), - access.bits(), - share_mode.bits(), - ptr::null(), - creation.0, - flags.bits(), - ptr::null_mut(), + desired_access, + share_mode, + security_attributes, + creation_disposition, + flags_and_attributes, + template_file, ) }; if handle == INVALID_HANDLE_VALUE || handle.is_null() { @@ -110,82 +141,75 @@ pub fn delete_file(path: WideCStr<'_, R>) -> Result<()> { /// # Errors /// /// Returns the error reported by `SetFileInformationByHandle`. -/// -/// # Safety -/// -/// `file_information` must point to an initialized buffer whose type and size -/// match `file_information_class`, and it must remain valid for the call. -pub unsafe fn set_file_information_by_handle( +pub fn set_file_information_by_handle( file: &OwnedHandle, - file_information_class: FILE_INFO_BY_HANDLE_CLASS, - file_information: *const c_void, - buffer_size: u32, + class: FileInformationClass, + information: &I, ) -> Result<()> { - // SAFETY: `file` is valid and the caller upholds the information-buffer - // contract. The remaining values are passed through unchanged. + // SAFETY: `file` is valid. An unforgeable `class` value ties the class and + // byte count to `I`, and `information` remains readable for the call. crate::windows::bool_result(unsafe { SetFileInformationByHandle( file.as_raw(), - file_information_class, - file_information, - buffer_size, + class.raw, + ptr::from_ref(information).cast::(), + class.size, ) }) } -/// Returns a file's logical length. +/// Calls `GetFileSizeEx`. /// /// # Errors /// /// Returns the error reported by `GetFileSizeEx`. -pub fn file_size(file: &OwnedHandle) -> Result { - let mut size = 0; +pub fn get_file_size(file: &OwnedHandle, size: &mut i64) -> Result<()> { // SAFETY: `file` is valid and `size` is writable for the call. - crate::windows::bool_result(unsafe { GetFileSizeEx(file.as_raw(), &raw mut size) })?; - Ok(size) + crate::windows::bool_result(unsafe { GetFileSizeEx(file.as_raw(), size) }) } -/// Sets a file's logical length. +/// Calls `DeviceIoControl`. /// /// # Errors /// -/// Returns the error reported by `SetFileInformationByHandle`. -pub fn set_len(file: &OwnedHandle, len: i64) -> Result<()> { - const INFO_SIZE: u32 = 8; - const _: [(); 8] = [(); size_of::()]; - - let info = FILE_END_OF_FILE_INFO { EndOfFile: len }; - // SAFETY: `info` has the type and exact size required by - // `FileEndOfFileInfo`. - unsafe { - set_file_information_by_handle( - file, - FileEndOfFileInfo, - (&raw const info).cast::(), - INFO_SIZE, - ) - } -} +/// Returns the error reported by `DeviceIoControl`. +pub fn device_io_control( + device: &OwnedHandle, + control_code: DeviceIoControlCode, + input: Option<&I>, + output: Option<&mut O>, + bytes_returned: &mut u32, + overlapped: Option<&mut Overlapped>, +) -> Result<()> { + let (input, input_size) = match input { + Some(input) if control_code.input_size != 0 => { + (ptr::from_ref(input).cast::(), control_code.input_size) + } + _ => (ptr::null(), 0), + }; + let (output, output_size) = match output { + Some(output) if control_code.output_size != 0 => { + (ptr::from_mut(output).cast::(), control_code.output_size) + } + _ => (ptr::null_mut(), 0), + }; + let overlapped = overlapped.map_or(ptr::null_mut(), Overlapped::as_raw_mut); -/// Marks a file sparse. -/// -/// # Errors -/// -/// Returns the error reported by `FSCTL_SET_SPARSE`. -pub fn set_sparse(file: &OwnedHandle) -> Result<()> { - let mut bytes_returned = 0; - // SAFETY: `file` is valid and synchronous. `FSCTL_SET_SPARSE` needs no - // input or output buffer, and `bytes_returned` is writable for the call. + // SAFETY: `device` is valid. The unforgeable control-code value ties the + // control to the input and output buffer types and sizes. Every non-null + // pointer is backed by its mutable or shared borrow for the call. Safe + // callers cannot construct overlapped state, so their calls are + // synchronous. crate::windows::bool_result(unsafe { DeviceIoControl( - file.as_raw(), - FSCTL_SET_SPARSE, - ptr::null(), - 0, - ptr::null_mut(), - 0, - &raw mut bytes_returned, - ptr::null_mut(), + device.as_raw(), + control_code.raw, + input, + input_size, + output, + output_size, + bytes_returned, + overlapped, ) }) } diff --git a/crates/fspy_nostd/src/lib.rs b/crates/fspy_nostd/src/lib.rs index 3d1669bcf..82ca959ed 100644 --- a/crates/fspy_nostd/src/lib.rs +++ b/crates/fspy_nostd/src/lib.rs @@ -22,7 +22,7 @@ pub mod param; pub use c_str::{CStr, CStrUnit, Fat, Thin, Units, WideCStr}; #[cfg(windows)] -pub use windows::{OwnedHandle, get_module_handle}; +pub use windows::{Overlapped, OwnedHandle, SecurityAttributes, get_module_handle}; #[cfg(windows)] #[derive(Clone, Copy, Debug, Eq, PartialEq)] diff --git a/crates/fspy_nostd/src/mm.rs b/crates/fspy_nostd/src/mm.rs index 62994553a..15c58747d 100644 --- a/crates/fspy_nostd/src/mm.rs +++ b/crates/fspy_nostd/src/mm.rs @@ -15,25 +15,14 @@ pub use rustix::mm::{MapFlags, MprotectFlags, ProtFlags, mmap, mmap_anonymous, m mod windows { use core::{ffi::c_void, ptr}; + use windows_sys::Win32::System::Memory::{ + CreateFileMappingW, MEMORY_MAPPED_VIEW_ADDRESS, MapViewOfFile, UnmapViewOfFile, + }; pub use windows_sys::Win32::System::Memory::{ FILE_MAP, FILE_MAP_READ, FILE_MAP_WRITE, PAGE_PROTECTION_FLAGS, PAGE_READWRITE, }; - use windows_sys::Win32::{ - Security::SECURITY_ATTRIBUTES, - System::Memory::{ - CreateFileMappingW, MEMORY_MAPPED_VIEW_ADDRESS, MapViewOfFile, UnmapViewOfFile, - }, - }; - use crate::{OwnedHandle, Result, WideCStr}; - - /// Opaque security attributes accepted by `CreateFileMappingW`. - /// - /// This type cannot be constructed outside `fspy_nostd`. No constructor - /// is exposed until a caller needs non-default security attributes and - /// their embedded security descriptor can be represented safely. - #[repr(transparent)] - pub struct SecurityAttributes(SECURITY_ATTRIBUTES); + use crate::{OwnedHandle, Result, SecurityAttributes, WideCStr}; /// An owned view of a file-mapping object. pub struct MappingView { @@ -80,8 +69,7 @@ mod windows { maximum_size_low: u32, name: Option>, ) -> Result { - let mapping_attributes = - mapping_attributes.map_or(ptr::null(), |attributes| ptr::from_ref(&attributes.0)); + let mapping_attributes = mapping_attributes.map_or(ptr::null(), SecurityAttributes::as_raw); let name = name.map_or(ptr::null(), |name| name.as_ptr()); // SAFETY: `file` is valid. Security attributes are either null or a @@ -132,5 +120,5 @@ mod windows { #[cfg(windows)] pub use windows::{ FILE_MAP, FILE_MAP_READ, FILE_MAP_WRITE, MappingView, PAGE_PROTECTION_FLAGS, PAGE_READWRITE, - SecurityAttributes, create_file_mapping, map_view_of_file, + create_file_mapping, map_view_of_file, }; diff --git a/crates/fspy_nostd/src/windows.rs b/crates/fspy_nostd/src/windows.rs index c01149805..08b4eeedd 100644 --- a/crates/fspy_nostd/src/windows.rs +++ b/crates/fspy_nostd/src/windows.rs @@ -1,12 +1,43 @@ use core::{ffi::c_void, ptr::NonNull}; -use windows_sys::Win32::{Foundation::GetLastError, System::LibraryLoader::GetModuleHandleW}; +use windows_sys::Win32::{ + Foundation::GetLastError, + Security::SECURITY_ATTRIBUTES, + System::{IO::OVERLAPPED, LibraryLoader::GetModuleHandleW}, +}; use crate::{Result, WideCStr}; /// An owned Windows kernel handle. pub struct OwnedHandle(NonNull); +/// Opaque security attributes accepted by Win32 creation functions. +/// +/// This type has no public constructor. Non-default security attributes will +/// remain unavailable to safe callers until their embedded security +/// descriptor can be represented safely. +#[repr(transparent)] +pub struct SecurityAttributes(SECURITY_ATTRIBUTES); + +impl SecurityAttributes { + pub(crate) const fn as_raw(&self) -> *const SECURITY_ATTRIBUTES { + &raw const self.0 + } +} + +/// Opaque state for overlapped Win32 I/O. +/// +/// This type has no public constructor because an asynchronous operation must +/// tie its lifetime to the state and every borrowed buffer. +#[repr(transparent)] +pub struct Overlapped(OVERLAPPED); + +impl Overlapped { + pub(crate) const fn as_raw_mut(&mut self) -> *mut OVERLAPPED { + &raw mut self.0 + } +} + impl OwnedHandle { /// Creates an owned handle after the caller has validated the raw value. /// diff --git a/crates/fspy_shm/src/windows.rs b/crates/fspy_shm/src/windows.rs index a8d04ab4c..4667c4765 100644 --- a/crates/fspy_shm/src/windows.rs +++ b/crates/fspy_shm/src/windows.rs @@ -1,7 +1,6 @@ //! Windows shared memory backed by a sparse temporary file and identified by //! its path. -use core::{ffi::c_void, mem::size_of}; use std::{ env::temp_dir, ffi::OsStr, io, num::NonZeroUsize, os::windows::ffi::OsStrExt as _, path::PathBuf, @@ -17,9 +16,9 @@ use windows_sys::Win32::Storage::FileSystem::{ use crate::BACKING_PREFIX; -const SHARE_ALL: fspy_nostd::fs::ShareMode = fspy_nostd::fs::ShareMode::READ - .union(fspy_nostd::fs::ShareMode::WRITE) - .union(fspy_nostd::fs::ShareMode::DELETE); +const SHARE_ALL: fspy_nostd::fs::FILE_SHARE_MODE = fspy_nostd::fs::FILE_SHARE_READ + | fspy_nostd::fs::FILE_SHARE_WRITE + | fspy_nostd::fs::FILE_SHARE_DELETE; /// Keeps the shared memory's identifier alive and removes it on drop. /// @@ -74,10 +73,10 @@ pub fn create(size: usize) -> io::Result<(ShmKeeper, ShmHandle)> { .join(format!("{BACKING_PREFIX}{}.shm", Uuid::new_v4().simple())); let file = open_file( path.as_os_str(), - fspy_nostd::fs::Access::READ | fspy_nostd::fs::Access::WRITE, - fspy_nostd::fs::CreationDisposition::CREATE_NEW, + fspy_nostd::fs::GENERIC_READ | fspy_nostd::fs::GENERIC_WRITE, + fspy_nostd::fs::CREATE_NEW, // Ask Windows to keep the data in memory when it can. - fspy_nostd::fs::OpenFlags::TEMPORARY, + fspy_nostd::fs::FILE_ATTRIBUTE_TEMPORARY, )?; // The keeper exists from here on, so every error path below cleans up. let keeper = ShmKeeper { path }; @@ -85,9 +84,24 @@ pub fn create(size: usize) -> io::Result<(ShmKeeper, ShmHandle)> { // NTFS allocates clusters for the whole logical size unless the file is // marked sparse first, which would turn the capacity into real disk usage. // Volumes without sparse-file support fail here. - fspy_nostd::fs::set_sparse(&file).map_err(error_to_io)?; + let mut bytes_returned = 0; + fspy_nostd::fs::device_io_control( + &file, + fspy_nostd::fs::FSCTL_SET_SPARSE, + None, + None, + &mut bytes_returned, + None, + ) + .map_err(error_to_io)?; // Every byte reads as zero because the file is all holes. - fspy_nostd::fs::set_len(&file, size_i64).map_err(error_to_io)?; + let end_of_file = fspy_nostd::fs::FILE_END_OF_FILE_INFO { EndOfFile: size_i64 }; + fspy_nostd::fs::set_file_information_by_handle( + &file, + fspy_nostd::fs::FileEndOfFileInfo, + &end_of_file, + ) + .map_err(error_to_io)?; Ok((keeper, ShmHandle { file, size })) } @@ -104,14 +118,16 @@ pub fn create(size: usize) -> io::Result<(ShmKeeper, ShmHandle)> { pub fn open(id: &OsStr) -> io::Result { let file = open_file( id, - fspy_nostd::fs::Access::READ | fspy_nostd::fs::Access::WRITE, - fspy_nostd::fs::CreationDisposition::OPEN_EXISTING, - fspy_nostd::fs::OpenFlags::empty(), + fspy_nostd::fs::GENERIC_READ | fspy_nostd::fs::GENERIC_WRITE, + fspy_nostd::fs::OPEN_EXISTING, + 0, )?; // If another process shrinks the file before `map`, mapping fails. If it // resizes afterwards, nothing here touches the mapped pages. A concurrent // resize cannot make a mapping access invalid memory. - let size = usize::try_from(fspy_nostd::fs::file_size(&file).map_err(error_to_io)?) + let mut size = 0; + fspy_nostd::fs::get_file_size(&file, &mut size).map_err(error_to_io)?; + let size = usize::try_from(size) .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "invalid shared-memory size"))?; let size = NonZeroUsize::new(size) .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "shared-memory size is zero"))?; @@ -120,13 +136,21 @@ pub fn open(id: &OsStr) -> io::Result { fn open_file( path: &OsStr, - access: fspy_nostd::fs::Access, - creation: fspy_nostd::fs::CreationDisposition, - flags: fspy_nostd::fs::OpenFlags, + desired_access: u32, + creation_disposition: fspy_nostd::fs::FILE_CREATION_DISPOSITION, + flags_and_attributes: fspy_nostd::fs::FILE_FLAGS_AND_ATTRIBUTES, ) -> io::Result { let path = copy_path(path)?; - fspy_nostd::fs::open(as_nostd_path(&path), access, SHARE_ALL, creation, flags) - .map_err(error_to_io) + fspy_nostd::fs::create_file( + as_nostd_path(&path), + desired_access, + SHARE_ALL, + None, + creation_disposition, + flags_and_attributes, + None, + ) + .map_err(error_to_io) } fn copy_path(path: &OsStr) -> io::Result> { @@ -153,12 +177,14 @@ fn remove_file(path: fspy_nostd::WideCStr<'_, fspy_nostd::Fat>) -> fspy_nostd::R Err(error) => error, }; if error.raw_os_error() == fspy_nostd::fs::ERROR_ACCESS_DENIED - && let Ok(file) = fspy_nostd::fs::open( + && let Ok(file) = fspy_nostd::fs::create_file( path, - fspy_nostd::fs::Access::DELETE, + fspy_nostd::fs::DELETE, SHARE_ALL, - fspy_nostd::fs::CreationDisposition::OPEN_EXISTING, - fspy_nostd::fs::OpenFlags::OPEN_REPARSE_POINT, + None, + fspy_nostd::fs::OPEN_EXISTING, + fspy_nostd::fs::FILE_FLAG_OPEN_REPARSE_POINT, + None, ) && set_posix_delete(&file).is_ok() { @@ -171,24 +197,16 @@ fn remove_file(path: fspy_nostd::WideCStr<'_, fspy_nostd::Fat>) -> fspy_nostd::R } fn set_posix_delete(file: &fspy_nostd::OwnedHandle) -> fspy_nostd::Result<()> { - const INFO_SIZE: u32 = 4; - const _: [(); 4] = [(); size_of::()]; - let info = fspy_nostd::fs::FILE_DISPOSITION_INFO_EX { Flags: fspy_nostd::fs::FILE_DISPOSITION_FLAG_DELETE | fspy_nostd::fs::FILE_DISPOSITION_FLAG_POSIX_SEMANTICS | fspy_nostd::fs::FILE_DISPOSITION_FLAG_IGNORE_READONLY_ATTRIBUTE, }; - // SAFETY: `info` has the type and exact size required by - // `FileDispositionInfoEx`. - unsafe { - fspy_nostd::fs::set_file_information_by_handle( - file, - fspy_nostd::fs::FileDispositionInfoEx, - (&raw const info).cast::(), - INFO_SIZE, - ) - } + fspy_nostd::fs::set_file_information_by_handle( + file, + fspy_nostd::fs::FileDispositionInfoEx, + &info, + ) } impl Drop for ShmKeeper { @@ -201,12 +219,14 @@ impl Drop for ShmKeeper { return; }; if remove_file(as_nostd_path(&path)).is_err() { - let _ = fspy_nostd::fs::open( + let _ = fspy_nostd::fs::create_file( as_nostd_path(&path), - fspy_nostd::fs::Access::DELETE, + fspy_nostd::fs::DELETE, SHARE_ALL, - fspy_nostd::fs::CreationDisposition::OPEN_EXISTING, - fspy_nostd::fs::OpenFlags::DELETE_ON_CLOSE, + None, + fspy_nostd::fs::OPEN_EXISTING, + fspy_nostd::fs::FILE_FLAG_DELETE_ON_CLOSE, + None, ); } } From 513d4985e7fa39aa6fc988cf525d103043744f25 Mon Sep 17 00:00:00 2001 From: wan9chi Date: Thu, 13 Aug 2026 14:17:03 +0800 Subject: [PATCH 07/18] refactor(fspy-shm): simplify Windows syscall boundary Co-authored-by: GPT-5 Codex --- crates/fspy_nostd/Cargo.toml | 2 - crates/fspy_nostd/src/fs/mod.rs | 12 +-- crates/fspy_nostd/src/fs/windows.rs | 157 ++-------------------------- crates/fspy_nostd/src/lib.rs | 2 +- crates/fspy_nostd/src/mm.rs | 11 +- crates/fspy_nostd/src/windows.rs | 21 +--- crates/fspy_shm/Cargo.toml | 10 +- crates/fspy_shm/src/windows.rs | 151 +++++++++++++++++--------- 8 files changed, 124 insertions(+), 242 deletions(-) diff --git a/crates/fspy_nostd/Cargo.toml b/crates/fspy_nostd/Cargo.toml index 85dcf7c5d..b414a5546 100644 --- a/crates/fspy_nostd/Cargo.toml +++ b/crates/fspy_nostd/Cargo.toml @@ -36,8 +36,6 @@ windows-sys = { workspace = true, features = [ "Win32_Foundation", "Win32_Security", "Win32_Storage_FileSystem", - "Win32_System_IO", - "Win32_System_Ioctl", "Win32_System_LibraryLoader", "Win32_System_Memory", ] } diff --git a/crates/fspy_nostd/src/fs/mod.rs b/crates/fspy_nostd/src/fs/mod.rs index 999099b47..d0164e062 100644 --- a/crates/fspy_nostd/src/fs/mod.rs +++ b/crates/fspy_nostd/src/fs/mod.rs @@ -12,17 +12,7 @@ use crate::{BorrowedFd, CStr, Fat, OwnedFd, Result}; #[cfg(windows)] mod windows; #[cfg(windows)] -pub use windows::{ - CREATE_NEW, DELETE, DeviceIoControlCode, ERROR_ACCESS_DENIED, FILE_ATTRIBUTE_TEMPORARY, - FILE_CREATION_DISPOSITION, FILE_DISPOSITION_FLAG_DELETE, - FILE_DISPOSITION_FLAG_IGNORE_READONLY_ATTRIBUTE, FILE_DISPOSITION_FLAG_POSIX_SEMANTICS, - FILE_DISPOSITION_INFO_EX, FILE_END_OF_FILE_INFO, FILE_FLAG_DELETE_ON_CLOSE, - FILE_FLAG_OPEN_REPARSE_POINT, FILE_FLAGS_AND_ATTRIBUTES, FILE_INFO_BY_HANDLE_CLASS, - FILE_SET_SPARSE_BUFFER, FILE_SHARE_DELETE, FILE_SHARE_MODE, FILE_SHARE_READ, FILE_SHARE_WRITE, - FSCTL_SET_SPARSE, FileDispositionInfoEx, FileEndOfFileInfo, FileInformationClass, GENERIC_READ, - GENERIC_WRITE, OPEN_EXISTING, create_file, delete_file, device_io_control, get_file_size, - set_file_information_by_handle, -}; +pub use windows::{create_file, delete_file, get_file_size}; #[cfg(target_os = "linux")] mod linux; diff --git a/crates/fspy_nostd/src/fs/windows.rs b/crates/fspy_nostd/src/fs/windows.rs index c88ebe4ac..b4213e2d8 100644 --- a/crates/fspy_nostd/src/fs/windows.rs +++ b/crates/fspy_nostd/src/fs/windows.rs @@ -1,89 +1,14 @@ -use core::{ffi::c_void, marker::PhantomData, mem::size_of, ptr}; +use core::ptr; use windows_sys::Win32::{ Foundation::INVALID_HANDLE_VALUE, Storage::FileSystem::{ - CreateFileW, DeleteFileW, FileDispositionInfoEx as RAW_FILE_DISPOSITION_INFO_EX, - FileEndOfFileInfo as RAW_FILE_END_OF_FILE_INFO, GetFileSizeEx, SetFileInformationByHandle, + CreateFileW, DeleteFileW, FILE_CREATION_DISPOSITION, FILE_FLAGS_AND_ATTRIBUTES, + FILE_SHARE_MODE, GetFileSizeEx, }, - System::{IO::DeviceIoControl, Ioctl::FSCTL_SET_SPARSE as RAW_FSCTL_SET_SPARSE}, }; -pub use windows_sys::Win32::{ - Foundation::{ERROR_ACCESS_DENIED, GENERIC_READ, GENERIC_WRITE}, - Storage::FileSystem::{ - CREATE_NEW, DELETE, FILE_ATTRIBUTE_TEMPORARY, FILE_CREATION_DISPOSITION, - FILE_DISPOSITION_FLAG_DELETE, FILE_DISPOSITION_FLAG_IGNORE_READONLY_ATTRIBUTE, - FILE_DISPOSITION_FLAG_POSIX_SEMANTICS, FILE_DISPOSITION_INFO_EX, FILE_END_OF_FILE_INFO, - FILE_FLAG_DELETE_ON_CLOSE, FILE_FLAG_OPEN_REPARSE_POINT, FILE_FLAGS_AND_ATTRIBUTES, - FILE_INFO_BY_HANDLE_CLASS, FILE_SHARE_DELETE, FILE_SHARE_MODE, FILE_SHARE_READ, - FILE_SHARE_WRITE, OPEN_EXISTING, - }, - System::Ioctl::FILE_SET_SPARSE_BUFFER, -}; - -use crate::{Overlapped, OwnedHandle, Result, SecurityAttributes, WideCStr}; - -/// A file-information class tied to its buffer type and size. -/// -/// Values are provided by this module for the Win32 classes it supports. The -/// private fields prevent safe code from pairing a class with the wrong -/// buffer representation. -pub struct FileInformationClass { - raw: FILE_INFO_BY_HANDLE_CLASS, - size: u32, - marker: PhantomData, -} - -impl Clone for FileInformationClass { - fn clone(&self) -> Self { - *self - } -} - -impl Copy for FileInformationClass {} - -const _: [(); 4] = [(); size_of::()]; -const _: [(); 8] = [(); size_of::()]; - -/// The `FileDispositionInfoEx` class and its buffer representation. -#[expect(non_upper_case_globals, reason = "matches the Win32 class name")] -pub const FileDispositionInfoEx: FileInformationClass = - FileInformationClass { raw: RAW_FILE_DISPOSITION_INFO_EX, size: 4, marker: PhantomData }; - -/// The `FileEndOfFileInfo` class and its buffer representation. -#[expect(non_upper_case_globals, reason = "matches the Win32 class name")] -pub const FileEndOfFileInfo: FileInformationClass = - FileInformationClass { raw: RAW_FILE_END_OF_FILE_INFO, size: 8, marker: PhantomData }; - -/// A device-control code tied to its input and output buffer types and sizes. -/// -/// Values are provided by this module for the controls it supports. The -/// private fields prevent safe code from changing the code or its buffer -/// contract. -pub struct DeviceIoControlCode { - raw: u32, - input_size: u32, - output_size: u32, - marker: PhantomData, -} - -impl Clone for DeviceIoControlCode { - fn clone(&self) -> Self { - *self - } -} - -impl Copy for DeviceIoControlCode {} -const _: [(); 1] = [(); size_of::()]; - -/// The `FSCTL_SET_SPARSE` control and its buffer representations. -pub const FSCTL_SET_SPARSE: DeviceIoControlCode = DeviceIoControlCode { - raw: RAW_FSCTL_SET_SPARSE, - input_size: 1, - output_size: 0, - marker: PhantomData, -}; +use crate::{OwnedHandle, Result, SecurityAttributes, WideCStr}; /// Calls `CreateFileW` and returns the new owned handle. /// @@ -136,80 +61,14 @@ pub fn delete_file(path: WideCStr<'_, R>) -> Result<()> { crate::windows::bool_result(unsafe { DeleteFileW(path.as_ptr()) }) } -/// Calls `SetFileInformationByHandle`. -/// -/// # Errors -/// -/// Returns the error reported by `SetFileInformationByHandle`. -pub fn set_file_information_by_handle( - file: &OwnedHandle, - class: FileInformationClass, - information: &I, -) -> Result<()> { - // SAFETY: `file` is valid. An unforgeable `class` value ties the class and - // byte count to `I`, and `information` remains readable for the call. - crate::windows::bool_result(unsafe { - SetFileInformationByHandle( - file.as_raw(), - class.raw, - ptr::from_ref(information).cast::(), - class.size, - ) - }) -} - /// Calls `GetFileSizeEx`. /// /// # Errors /// /// Returns the error reported by `GetFileSizeEx`. -pub fn get_file_size(file: &OwnedHandle, size: &mut i64) -> Result<()> { +pub fn get_file_size(file: &OwnedHandle) -> Result { + let mut size = 0; // SAFETY: `file` is valid and `size` is writable for the call. - crate::windows::bool_result(unsafe { GetFileSizeEx(file.as_raw(), size) }) -} - -/// Calls `DeviceIoControl`. -/// -/// # Errors -/// -/// Returns the error reported by `DeviceIoControl`. -pub fn device_io_control( - device: &OwnedHandle, - control_code: DeviceIoControlCode, - input: Option<&I>, - output: Option<&mut O>, - bytes_returned: &mut u32, - overlapped: Option<&mut Overlapped>, -) -> Result<()> { - let (input, input_size) = match input { - Some(input) if control_code.input_size != 0 => { - (ptr::from_ref(input).cast::(), control_code.input_size) - } - _ => (ptr::null(), 0), - }; - let (output, output_size) = match output { - Some(output) if control_code.output_size != 0 => { - (ptr::from_mut(output).cast::(), control_code.output_size) - } - _ => (ptr::null_mut(), 0), - }; - let overlapped = overlapped.map_or(ptr::null_mut(), Overlapped::as_raw_mut); - - // SAFETY: `device` is valid. The unforgeable control-code value ties the - // control to the input and output buffer types and sizes. Every non-null - // pointer is backed by its mutable or shared borrow for the call. Safe - // callers cannot construct overlapped state, so their calls are - // synchronous. - crate::windows::bool_result(unsafe { - DeviceIoControl( - device.as_raw(), - control_code.raw, - input, - input_size, - output, - output_size, - bytes_returned, - overlapped, - ) - }) + crate::windows::bool_result(unsafe { GetFileSizeEx(file.as_raw(), &raw mut size) })?; + Ok(size) } diff --git a/crates/fspy_nostd/src/lib.rs b/crates/fspy_nostd/src/lib.rs index 82ca959ed..c7237b235 100644 --- a/crates/fspy_nostd/src/lib.rs +++ b/crates/fspy_nostd/src/lib.rs @@ -22,7 +22,7 @@ pub mod param; pub use c_str::{CStr, CStrUnit, Fat, Thin, Units, WideCStr}; #[cfg(windows)] -pub use windows::{Overlapped, OwnedHandle, SecurityAttributes, get_module_handle}; +pub use windows::{OwnedHandle, SecurityAttributes, get_module_handle}; #[cfg(windows)] #[derive(Clone, Copy, Debug, Eq, PartialEq)] diff --git a/crates/fspy_nostd/src/mm.rs b/crates/fspy_nostd/src/mm.rs index 15c58747d..7557e03c6 100644 --- a/crates/fspy_nostd/src/mm.rs +++ b/crates/fspy_nostd/src/mm.rs @@ -16,10 +16,8 @@ mod windows { use core::{ffi::c_void, ptr}; use windows_sys::Win32::System::Memory::{ - CreateFileMappingW, MEMORY_MAPPED_VIEW_ADDRESS, MapViewOfFile, UnmapViewOfFile, - }; - pub use windows_sys::Win32::System::Memory::{ - FILE_MAP, FILE_MAP_READ, FILE_MAP_WRITE, PAGE_PROTECTION_FLAGS, PAGE_READWRITE, + CreateFileMappingW, FILE_MAP, MEMORY_MAPPED_VIEW_ADDRESS, MapViewOfFile, + PAGE_PROTECTION_FLAGS, UnmapViewOfFile, }; use crate::{OwnedHandle, Result, SecurityAttributes, WideCStr}; @@ -118,7 +116,4 @@ mod windows { } #[cfg(windows)] -pub use windows::{ - FILE_MAP, FILE_MAP_READ, FILE_MAP_WRITE, MappingView, PAGE_PROTECTION_FLAGS, PAGE_READWRITE, - create_file_mapping, map_view_of_file, -}; +pub use windows::{MappingView, create_file_mapping, map_view_of_file}; diff --git a/crates/fspy_nostd/src/windows.rs b/crates/fspy_nostd/src/windows.rs index 08b4eeedd..0c2d88c12 100644 --- a/crates/fspy_nostd/src/windows.rs +++ b/crates/fspy_nostd/src/windows.rs @@ -1,9 +1,8 @@ use core::{ffi::c_void, ptr::NonNull}; use windows_sys::Win32::{ - Foundation::GetLastError, - Security::SECURITY_ATTRIBUTES, - System::{IO::OVERLAPPED, LibraryLoader::GetModuleHandleW}, + Foundation::GetLastError, Security::SECURITY_ATTRIBUTES, + System::LibraryLoader::GetModuleHandleW, }; use crate::{Result, WideCStr}; @@ -25,19 +24,6 @@ impl SecurityAttributes { } } -/// Opaque state for overlapped Win32 I/O. -/// -/// This type has no public constructor because an asynchronous operation must -/// tie its lifetime to the state and every borrowed buffer. -#[repr(transparent)] -pub struct Overlapped(OVERLAPPED); - -impl Overlapped { - pub(crate) const fn as_raw_mut(&mut self) -> *mut OVERLAPPED { - &raw mut self.0 - } -} - impl OwnedHandle { /// Creates an owned handle after the caller has validated the raw value. /// @@ -51,7 +37,8 @@ impl OwnedHandle { } /// Returns the raw Windows handle without transferring ownership. - pub(crate) const fn as_raw(&self) -> *mut c_void { + #[must_use] + pub const fn as_raw(&self) -> *mut c_void { self.0.as_ptr() } } diff --git a/crates/fspy_shm/Cargo.toml b/crates/fspy_shm/Cargo.toml index 7f42fa906..34d3793b7 100644 --- a/crates/fspy_shm/Cargo.toml +++ b/crates/fspy_shm/Cargo.toml @@ -13,8 +13,14 @@ uuid = { workspace = true, features = ["v4"] } [target.'cfg(any(unix, windows))'.dependencies] fspy_nostd = { workspace = true } -[target.'cfg(target_os = "windows")'.dev-dependencies] -windows-sys = { workspace = true, features = ["Win32_Foundation", "Win32_Storage_FileSystem"] } +[target.'cfg(target_os = "windows")'.dependencies] +windows-sys = { workspace = true, features = [ + "Win32_Foundation", + "Win32_Storage_FileSystem", + "Win32_System_IO", + "Win32_System_Ioctl", + "Win32_System_Memory", +] } [dev-dependencies] ctor = { workspace = true } diff --git a/crates/fspy_shm/src/windows.rs b/crates/fspy_shm/src/windows.rs index 4667c4765..2b6ee84a0 100644 --- a/crates/fspy_shm/src/windows.rs +++ b/crates/fspy_shm/src/windows.rs @@ -1,6 +1,7 @@ //! Windows shared memory backed by a sparse temporary file and identified by //! its path. +use core::{ffi::c_void, mem::size_of, ptr}; use std::{ env::temp_dir, ffi::OsStr, io, num::NonZeroUsize, os::windows::ffi::OsStrExt as _, path::PathBuf, @@ -13,12 +14,26 @@ use uuid::Uuid; use windows_sys::Win32::Storage::FileSystem::{ FILE_STANDARD_INFO, FileStandardInfo, GetFileInformationByHandleEx, }; +use windows_sys::Win32::{ + Foundation::{ERROR_ACCESS_DENIED, GENERIC_READ, GENERIC_WRITE, GetLastError}, + Storage::FileSystem::{ + CREATE_NEW, DELETE, FILE_ATTRIBUTE_TEMPORARY, FILE_CREATION_DISPOSITION, + FILE_DISPOSITION_FLAG_DELETE, FILE_DISPOSITION_FLAG_IGNORE_READONLY_ATTRIBUTE, + FILE_DISPOSITION_FLAG_POSIX_SEMANTICS, FILE_DISPOSITION_INFO_EX, FILE_END_OF_FILE_INFO, + FILE_FLAG_DELETE_ON_CLOSE, FILE_FLAG_OPEN_REPARSE_POINT, FILE_FLAGS_AND_ATTRIBUTES, + FILE_SHARE_DELETE, FILE_SHARE_MODE, FILE_SHARE_READ, FILE_SHARE_WRITE, + FileDispositionInfoEx, FileEndOfFileInfo, OPEN_EXISTING, SetFileInformationByHandle, + }, + System::{ + IO::DeviceIoControl, + Ioctl::FSCTL_SET_SPARSE, + Memory::{FILE_MAP_READ, FILE_MAP_WRITE, PAGE_READWRITE}, + }, +}; use crate::BACKING_PREFIX; -const SHARE_ALL: fspy_nostd::fs::FILE_SHARE_MODE = fspy_nostd::fs::FILE_SHARE_READ - | fspy_nostd::fs::FILE_SHARE_WRITE - | fspy_nostd::fs::FILE_SHARE_DELETE; +const SHARE_ALL: FILE_SHARE_MODE = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE; /// Keeps the shared memory's identifier alive and removes it on drop. /// @@ -73,10 +88,10 @@ pub fn create(size: usize) -> io::Result<(ShmKeeper, ShmHandle)> { .join(format!("{BACKING_PREFIX}{}.shm", Uuid::new_v4().simple())); let file = open_file( path.as_os_str(), - fspy_nostd::fs::GENERIC_READ | fspy_nostd::fs::GENERIC_WRITE, - fspy_nostd::fs::CREATE_NEW, + GENERIC_READ | GENERIC_WRITE, + CREATE_NEW, // Ask Windows to keep the data in memory when it can. - fspy_nostd::fs::FILE_ATTRIBUTE_TEMPORARY, + FILE_ATTRIBUTE_TEMPORARY, )?; // The keeper exists from here on, so every error path below cleans up. let keeper = ShmKeeper { path }; @@ -84,24 +99,9 @@ pub fn create(size: usize) -> io::Result<(ShmKeeper, ShmHandle)> { // NTFS allocates clusters for the whole logical size unless the file is // marked sparse first, which would turn the capacity into real disk usage. // Volumes without sparse-file support fail here. - let mut bytes_returned = 0; - fspy_nostd::fs::device_io_control( - &file, - fspy_nostd::fs::FSCTL_SET_SPARSE, - None, - None, - &mut bytes_returned, - None, - ) - .map_err(error_to_io)?; + set_sparse(&file).map_err(error_to_io)?; // Every byte reads as zero because the file is all holes. - let end_of_file = fspy_nostd::fs::FILE_END_OF_FILE_INFO { EndOfFile: size_i64 }; - fspy_nostd::fs::set_file_information_by_handle( - &file, - fspy_nostd::fs::FileEndOfFileInfo, - &end_of_file, - ) - .map_err(error_to_io)?; + set_end_of_file(&file, size_i64).map_err(error_to_io)?; Ok((keeper, ShmHandle { file, size })) } @@ -116,18 +116,11 @@ pub fn create(size: usize) -> io::Result<(ShmKeeper, ShmHandle)> { /// Returns an error if the shared memory is unavailable, which is the common /// case once its keeper has been dropped. pub fn open(id: &OsStr) -> io::Result { - let file = open_file( - id, - fspy_nostd::fs::GENERIC_READ | fspy_nostd::fs::GENERIC_WRITE, - fspy_nostd::fs::OPEN_EXISTING, - 0, - )?; + let file = open_file(id, GENERIC_READ | GENERIC_WRITE, OPEN_EXISTING, 0)?; // If another process shrinks the file before `map`, mapping fails. If it // resizes afterwards, nothing here touches the mapped pages. A concurrent // resize cannot make a mapping access invalid memory. - let mut size = 0; - fspy_nostd::fs::get_file_size(&file, &mut size).map_err(error_to_io)?; - let size = usize::try_from(size) + let size = usize::try_from(fspy_nostd::fs::get_file_size(&file).map_err(error_to_io)?) .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "invalid shared-memory size"))?; let size = NonZeroUsize::new(size) .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "shared-memory size is zero"))?; @@ -137,8 +130,8 @@ pub fn open(id: &OsStr) -> io::Result { fn open_file( path: &OsStr, desired_access: u32, - creation_disposition: fspy_nostd::fs::FILE_CREATION_DISPOSITION, - flags_and_attributes: fspy_nostd::fs::FILE_FLAGS_AND_ATTRIBUTES, + creation_disposition: FILE_CREATION_DISPOSITION, + flags_and_attributes: FILE_FLAGS_AND_ATTRIBUTES, ) -> io::Result { let path = copy_path(path)?; fspy_nostd::fs::create_file( @@ -171,19 +164,65 @@ fn error_to_io(error: fspy_nostd::Error) -> io::Error { io::Error::from_raw_os_error(error.raw_os_error().cast_signed()) } +fn bool_result(result: i32) -> fspy_nostd::Result<()> { + if result == 0 { + // SAFETY: the failing Win32 call immediately precedes this read of the + // thread-local error code. + Err(fspy_nostd::Error::from_raw_os_error(unsafe { GetLastError() })) + } else { + Ok(()) + } +} + +fn set_sparse(file: &fspy_nostd::OwnedHandle) -> fspy_nostd::Result<()> { + let mut bytes_returned = 0; + // SAFETY: `file` is valid and synchronous. `FSCTL_SET_SPARSE` accepts null + // input and output buffers to mark the file sparse, and `bytes_returned` + // is writable for the call. + bool_result(unsafe { + DeviceIoControl( + file.as_raw(), + FSCTL_SET_SPARSE, + ptr::null(), + 0, + ptr::null_mut(), + 0, + &raw mut bytes_returned, + ptr::null_mut(), + ) + }) +} + +fn set_end_of_file(file: &fspy_nostd::OwnedHandle, len: i64) -> fspy_nostd::Result<()> { + const INFO_SIZE: u32 = 8; + const _: [(); 8] = [(); size_of::()]; + + let info = FILE_END_OF_FILE_INFO { EndOfFile: len }; + // SAFETY: `file` is valid and `info` has the type and exact size required + // by `FileEndOfFileInfo`. + bool_result(unsafe { + SetFileInformationByHandle( + file.as_raw(), + FileEndOfFileInfo, + (&raw const info).cast::(), + INFO_SIZE, + ) + }) +} + fn remove_file(path: fspy_nostd::WideCStr<'_, fspy_nostd::Fat>) -> fspy_nostd::Result<()> { let error = match fspy_nostd::fs::delete_file(path) { Ok(()) => return Ok(()), Err(error) => error, }; - if error.raw_os_error() == fspy_nostd::fs::ERROR_ACCESS_DENIED + if error.raw_os_error() == ERROR_ACCESS_DENIED && let Ok(file) = fspy_nostd::fs::create_file( path, - fspy_nostd::fs::DELETE, + DELETE, SHARE_ALL, None, - fspy_nostd::fs::OPEN_EXISTING, - fspy_nostd::fs::FILE_FLAG_OPEN_REPARSE_POINT, + OPEN_EXISTING, + FILE_FLAG_OPEN_REPARSE_POINT, None, ) && set_posix_delete(&file).is_ok() @@ -197,16 +236,24 @@ fn remove_file(path: fspy_nostd::WideCStr<'_, fspy_nostd::Fat>) -> fspy_nostd::R } fn set_posix_delete(file: &fspy_nostd::OwnedHandle) -> fspy_nostd::Result<()> { - let info = fspy_nostd::fs::FILE_DISPOSITION_INFO_EX { - Flags: fspy_nostd::fs::FILE_DISPOSITION_FLAG_DELETE - | fspy_nostd::fs::FILE_DISPOSITION_FLAG_POSIX_SEMANTICS - | fspy_nostd::fs::FILE_DISPOSITION_FLAG_IGNORE_READONLY_ATTRIBUTE, + const INFO_SIZE: u32 = 4; + const _: [(); 4] = [(); size_of::()]; + + let info = FILE_DISPOSITION_INFO_EX { + Flags: FILE_DISPOSITION_FLAG_DELETE + | FILE_DISPOSITION_FLAG_POSIX_SEMANTICS + | FILE_DISPOSITION_FLAG_IGNORE_READONLY_ATTRIBUTE, }; - fspy_nostd::fs::set_file_information_by_handle( - file, - fspy_nostd::fs::FileDispositionInfoEx, - &info, - ) + // SAFETY: `file` is valid and `info` has the type and exact size required + // by `FileDispositionInfoEx`. + bool_result(unsafe { + SetFileInformationByHandle( + file.as_raw(), + FileDispositionInfoEx, + (&raw const info).cast::(), + INFO_SIZE, + ) + }) } impl Drop for ShmKeeper { @@ -221,11 +268,11 @@ impl Drop for ShmKeeper { if remove_file(as_nostd_path(&path)).is_err() { let _ = fspy_nostd::fs::create_file( as_nostd_path(&path), - fspy_nostd::fs::DELETE, + DELETE, SHARE_ALL, None, - fspy_nostd::fs::OPEN_EXISTING, - fspy_nostd::fs::FILE_FLAG_DELETE_ON_CLOSE, + OPEN_EXISTING, + FILE_FLAG_DELETE_ON_CLOSE, None, ); } @@ -257,7 +304,7 @@ impl ShmHandle { let mapping = fspy_nostd::mm::create_file_mapping::( &self.file, None, - fspy_nostd::mm::PAGE_READWRITE, + PAGE_READWRITE, 0, 0, None, @@ -265,7 +312,7 @@ impl ShmHandle { .map_err(error_to_io)?; let view = fspy_nostd::mm::map_view_of_file( &mapping, - fspy_nostd::mm::FILE_MAP_READ | fspy_nostd::mm::FILE_MAP_WRITE, + FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, self.size.get(), From c2ac2da1d61911a0727ae74f4d9d72debae2cea2 Mon Sep 17 00:00:00 2001 From: wan9chi Date: Thu, 13 Aug 2026 16:31:18 +0800 Subject: [PATCH 08/18] refactor(fspy-shm): type Windows flags Co-authored-by: GPT-5 Codex --- Cargo.lock | 1 + crates/fspy_nostd/Cargo.toml | 1 + crates/fspy_nostd/src/fs/mod.rs | 73 ++-------------- crates/fspy_nostd/src/fs/unix.rs | 56 +++++++++++++ crates/fspy_nostd/src/fs/windows.rs | 87 ++++++++++++++++--- crates/fspy_nostd/src/mm.rs | 116 +------------------------- crates/fspy_nostd/src/mm/windows.rs | 124 ++++++++++++++++++++++++++++ crates/fspy_shm/Cargo.toml | 1 - crates/fspy_shm/src/windows.rs | 60 +++++++------- 9 files changed, 298 insertions(+), 221 deletions(-) create mode 100644 crates/fspy_nostd/src/fs/unix.rs create mode 100644 crates/fspy_nostd/src/mm/windows.rs diff --git a/Cargo.lock b/Cargo.lock index 590408765..b6a18250e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1345,6 +1345,7 @@ name = "fspy_nostd" version = "0.0.0" dependencies = [ "atoi", + "bitflags 2.10.0", "bstr", "libc", "rustix", diff --git a/crates/fspy_nostd/Cargo.toml b/crates/fspy_nostd/Cargo.toml index b414a5546..0691c189b 100644 --- a/crates/fspy_nostd/Cargo.toml +++ b/crates/fspy_nostd/Cargo.toml @@ -32,6 +32,7 @@ rustix = { workspace = true, features = ["runtime"] } syscalls = { workspace = true } [target.'cfg(windows)'.dependencies] +bitflags = { workspace = true } windows-sys = { workspace = true, features = [ "Win32_Foundation", "Win32_Security", diff --git a/crates/fspy_nostd/src/fs/mod.rs b/crates/fspy_nostd/src/fs/mod.rs index d0164e062..29197830c 100644 --- a/crates/fspy_nostd/src/fs/mod.rs +++ b/crates/fspy_nostd/src/fs/mod.rs @@ -1,79 +1,18 @@ //! Filesystem calls with caller-owned storage. -#[cfg(unix)] -use core::mem::MaybeUninit; - -#[cfg(unix)] -pub use rustix::fs::{AtFlags, Mode, OFlags, fstat, ftruncate}; - -#[cfg(unix)] -use crate::{BorrowedFd, CStr, Fat, OwnedFd, Result}; - -#[cfg(windows)] -mod windows; -#[cfg(windows)] -pub use windows::{create_file, delete_file, get_file_size}; - #[cfg(target_os = "linux")] mod linux; #[cfg(target_os = "macos")] mod mac; - -#[cfg(target_os = "linux")] -use linux as imp; -#[cfg(target_os = "linux")] -pub use linux::readlinkat; -#[cfg(target_os = "macos")] -use mac as imp; -#[cfg(target_os = "macos")] -pub use mac::fcntl_getpath; - -/// The platform's maximum pathname size, including the terminating NUL. -#[cfg(unix)] -pub const PATH_MAX: usize = imp::PATH_MAX; - -/// Opens `path` relative to `dirfd` and returns its owned descriptor. -/// -/// # Errors -/// -/// Returns the error reported by `openat`. #[cfg(unix)] -pub fn openat( - dirfd: BorrowedFd<'_>, - path: CStr<'_, R>, - flags: OFlags, - mode: Mode, -) -> Result { - imp::openat(dirfd, path, flags, mode) -} - -/// Removes `path` relative to `dirfd`. -/// -/// # Errors -/// -/// Returns the error reported by `unlinkat`. -#[cfg(unix)] -pub fn unlinkat(dirfd: BorrowedFd<'_>, path: CStr<'_, R>, flags: AtFlags) -> Result<()> { - imp::unlinkat(dirfd, path, flags) -} +mod unix; +#[cfg(windows)] +mod windows; -/// Writes the absolute pathname of the current working directory into `buf`. -/// -/// The returned C string borrows `buf`, starts at the same address as `buf`, -/// and includes a terminating NUL. Bytes after that terminator have an -/// unspecified initialization state. -/// -/// This function performs one resolution attempt and does not allocate, grow, -/// or retry. No buffer size guarantees success, including one larger than -/// [`PATH_MAX`]. -/// -/// # Errors -/// -/// Returns the error reported while resolving the current working directory. #[cfg(unix)] -pub fn getcwd(buf: &mut [MaybeUninit]) -> Result> { - imp::getcwd(buf) -} +pub use unix::*; +#[cfg(windows)] +pub use windows::*; #[cfg(all(test, unix))] mod tests; diff --git a/crates/fspy_nostd/src/fs/unix.rs b/crates/fspy_nostd/src/fs/unix.rs new file mode 100644 index 000000000..05279a393 --- /dev/null +++ b/crates/fspy_nostd/src/fs/unix.rs @@ -0,0 +1,56 @@ +use core::mem::MaybeUninit; + +pub use rustix::fs::{AtFlags, Mode, OFlags, fstat, ftruncate}; + +#[cfg(target_os = "linux")] +use super::linux as imp; +#[cfg(target_os = "linux")] +pub use super::linux::readlinkat; +#[cfg(target_os = "macos")] +use super::mac as imp; +#[cfg(target_os = "macos")] +pub use super::mac::fcntl_getpath; +use crate::{BorrowedFd, CStr, Fat, OwnedFd, Result}; + +/// The platform's maximum pathname size, including the terminating NUL. +pub const PATH_MAX: usize = imp::PATH_MAX; + +/// Opens `path` relative to `dirfd` and returns its owned descriptor. +/// +/// # Errors +/// +/// Returns the error reported by `openat`. +pub fn openat( + dirfd: BorrowedFd<'_>, + path: CStr<'_, R>, + flags: OFlags, + mode: Mode, +) -> Result { + imp::openat(dirfd, path, flags, mode) +} + +/// Removes `path` relative to `dirfd`. +/// +/// # Errors +/// +/// Returns the error reported by `unlinkat`. +pub fn unlinkat(dirfd: BorrowedFd<'_>, path: CStr<'_, R>, flags: AtFlags) -> Result<()> { + imp::unlinkat(dirfd, path, flags) +} + +/// Writes the absolute pathname of the current working directory into `buf`. +/// +/// The returned C string borrows `buf`, starts at the same address as `buf`, +/// and includes a terminating NUL. Bytes after that terminator have an +/// unspecified initialization state. +/// +/// This function performs one resolution attempt and does not allocate, grow, +/// or retry. No buffer size guarantees success, including one larger than +/// [`PATH_MAX`]. +/// +/// # Errors +/// +/// Returns the error reported while resolving the current working directory. +pub fn getcwd(buf: &mut [MaybeUninit]) -> Result> { + imp::getcwd(buf) +} diff --git a/crates/fspy_nostd/src/fs/windows.rs b/crates/fspy_nostd/src/fs/windows.rs index b4213e2d8..6d0e25eed 100644 --- a/crates/fspy_nostd/src/fs/windows.rs +++ b/crates/fspy_nostd/src/fs/windows.rs @@ -1,15 +1,80 @@ use core::ptr; +use bitflags::bitflags; use windows_sys::Win32::{ - Foundation::INVALID_HANDLE_VALUE, + Foundation::{GENERIC_READ, GENERIC_WRITE, INVALID_HANDLE_VALUE}, Storage::FileSystem::{ - CreateFileW, DeleteFileW, FILE_CREATION_DISPOSITION, FILE_FLAGS_AND_ATTRIBUTES, - FILE_SHARE_MODE, GetFileSizeEx, + CREATE_ALWAYS, CREATE_NEW, CreateFileW, DELETE, DeleteFileW, FILE_ATTRIBUTE_TEMPORARY, + FILE_FLAG_DELETE_ON_CLOSE, FILE_FLAG_OPEN_REPARSE_POINT, FILE_SHARE_DELETE, + FILE_SHARE_READ, FILE_SHARE_WRITE, GetFileSizeEx, OPEN_ALWAYS, OPEN_EXISTING, + TRUNCATE_EXISTING, }, }; use crate::{OwnedHandle, Result, SecurityAttributes, WideCStr}; +bitflags! { + /// Access rights requested for a file handle. + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub struct FileAccess: u32 { + /// Generic read access. + const GENERIC_READ = GENERIC_READ; + /// Generic write access. + const GENERIC_WRITE = GENERIC_WRITE; + /// Permission to delete the file. + const DELETE = DELETE; + } + + /// Operations that other handles may perform while a file is open. + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub struct FileShare: u32 { + /// Permit subsequent opens for reading. + const READ = FILE_SHARE_READ; + /// Permit subsequent opens for writing. + const WRITE = FILE_SHARE_WRITE; + /// Permit subsequent opens for deletion. + const DELETE = FILE_SHARE_DELETE; + } + + /// File attributes and creation options accepted by `CreateFileW`. + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub struct FileOptions: u32 { + /// Hint that the file should be kept in memory when possible. + const TEMPORARY = FILE_ATTRIBUTE_TEMPORARY; + /// Delete the file after its last handle closes. + const DELETE_ON_CLOSE = FILE_FLAG_DELETE_ON_CLOSE; + /// Open a reparse point rather than its target. + const OPEN_REPARSE_POINT = FILE_FLAG_OPEN_REPARSE_POINT; + } +} + +/// How `CreateFileW` handles an existing or missing file. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum CreationDisposition { + /// Create a new file and fail if it already exists. + CreateNew, + /// Create a new file or replace an existing file. + CreateAlways, + /// Open an existing file and fail if it does not exist. + OpenExisting, + /// Open an existing file or create a new file. + OpenAlways, + /// Open and truncate an existing file. + TruncateExisting, +} + +impl CreationDisposition { + const fn into_raw(self) -> u32 { + match self { + Self::CreateNew => CREATE_NEW, + Self::CreateAlways => CREATE_ALWAYS, + Self::OpenExisting => OPEN_EXISTING, + Self::OpenAlways => OPEN_ALWAYS, + Self::TruncateExisting => TRUNCATE_EXISTING, + } + } +} + /// Calls `CreateFileW` and returns the new owned handle. /// /// # Errors @@ -18,11 +83,11 @@ use crate::{OwnedHandle, Result, SecurityAttributes, WideCStr}; #[expect(clippy::needless_pass_by_value, reason = "CStr is a borrowed value type")] pub fn create_file( path: WideCStr<'_, R>, - desired_access: u32, - share_mode: FILE_SHARE_MODE, + access: FileAccess, + share: FileShare, security_attributes: Option<&SecurityAttributes>, - creation_disposition: FILE_CREATION_DISPOSITION, - flags_and_attributes: FILE_FLAGS_AND_ATTRIBUTES, + disposition: CreationDisposition, + options: FileOptions, template_file: Option<&OwnedHandle>, ) -> Result { let security_attributes = security_attributes.map_or(ptr::null(), SecurityAttributes::as_raw); @@ -34,11 +99,11 @@ pub fn create_file( let handle = unsafe { CreateFileW( path.as_ptr(), - desired_access, - share_mode, + access.bits(), + share.bits(), security_attributes, - creation_disposition, - flags_and_attributes, + disposition.into_raw(), + options.bits(), template_file, ) }; diff --git a/crates/fspy_nostd/src/mm.rs b/crates/fspy_nostd/src/mm.rs index 7557e03c6..ce98132ea 100644 --- a/crates/fspy_nostd/src/mm.rs +++ b/crates/fspy_nostd/src/mm.rs @@ -1,119 +1,9 @@ -//! Memory mappings. -//! -//! On Unix, these are re-exposed from rustix as-is: they are single syscalls against the -//! kernel's own address-space bookkeeping — no libc state, no locks, no -//! allocation — so they already meet this crate's rules everywhere it -//! promises to work. What this module adds is the curation (being listed -//! here is what marks them safe for signal handlers, fork children, and -//! pre-libc startup) and the crate-level backend check, which guarantees -//! they cannot silently turn into libc calls on Linux. +//! Memory mappings with no process-runtime dependency. #[cfg(unix)] pub use rustix::mm::{MapFlags, MprotectFlags, ProtFlags, mmap, mmap_anonymous, mprotect, munmap}; #[cfg(windows)] -mod windows { - use core::{ffi::c_void, ptr}; - - use windows_sys::Win32::System::Memory::{ - CreateFileMappingW, FILE_MAP, MEMORY_MAPPED_VIEW_ADDRESS, MapViewOfFile, - PAGE_PROTECTION_FLAGS, UnmapViewOfFile, - }; - - use crate::{OwnedHandle, Result, SecurityAttributes, WideCStr}; - - /// An owned view of a file-mapping object. - pub struct MappingView { - ptr: core::ptr::NonNull, - } - - // SAFETY: a view owns no thread-affine state. Synchronization of accesses - // to its shared bytes is the caller's responsibility. - unsafe impl Send for MappingView {} - // SAFETY: sharing the view does not itself access the mapped bytes. - unsafe impl Sync for MappingView {} - - impl MappingView { - /// Returns a raw pointer to the first mapped byte. - #[must_use] - pub const fn as_ptr(&self) -> *mut u8 { - self.ptr.as_ptr() - } - } - - impl Drop for MappingView { - fn drop(&mut self) { - // SAFETY: this is the complete view owned by `self` and is - // unmapped exactly once. - let _ = unsafe { - UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS { - Value: self.ptr.as_ptr().cast::(), - }) - }; - } - } - - /// Calls `CreateFileMappingW` and returns the new mapping-object handle. - /// - /// # Errors - /// - /// Returns the error reported by `CreateFileMappingW`. - /// - pub fn create_file_mapping( - file: &OwnedHandle, - mapping_attributes: Option<&SecurityAttributes>, - protection: PAGE_PROTECTION_FLAGS, - maximum_size_high: u32, - maximum_size_low: u32, - name: Option>, - ) -> Result { - let mapping_attributes = mapping_attributes.map_or(ptr::null(), SecurityAttributes::as_raw); - let name = name.map_or(ptr::null(), |name| name.as_ptr()); - - // SAFETY: `file` is valid. Security attributes are either null or a - // valid opaque value owned by this module, and `name` is either null - // or backed by a valid borrowed wide C string. The remaining values - // are passed through unchanged. - let mapping = unsafe { - CreateFileMappingW( - file.as_raw(), - mapping_attributes, - protection, - maximum_size_high, - maximum_size_low, - name, - ) - }; - let Some(mapping) = core::ptr::NonNull::new(mapping) else { - return Err(crate::windows::last_error()); - }; - // SAFETY: `CreateFileMappingW` returned a valid, newly owned handle. - Ok(unsafe { OwnedHandle::from_raw(mapping.as_ptr()) }) - } - - /// Calls `MapViewOfFile` and returns the new owned view. - /// - /// # Errors - /// - /// Returns the error reported by `MapViewOfFile`. - pub fn map_view_of_file( - mapping: &OwnedHandle, - access: FILE_MAP, - file_offset_high: u32, - file_offset_low: u32, - bytes_to_map: usize, - ) -> Result { - // SAFETY: `mapping` is a valid file-mapping object. Windows validates - // the requested access, offset, and size against that object. - let view = unsafe { - MapViewOfFile(mapping.as_raw(), access, file_offset_high, file_offset_low, bytes_to_map) - }; - let Some(ptr) = core::ptr::NonNull::new(view.Value.cast::()) else { - return Err(crate::windows::last_error()); - }; - Ok(MappingView { ptr }) - } -} - +mod windows; #[cfg(windows)] -pub use windows::{MappingView, create_file_mapping, map_view_of_file}; +pub use windows::*; diff --git a/crates/fspy_nostd/src/mm/windows.rs b/crates/fspy_nostd/src/mm/windows.rs new file mode 100644 index 000000000..b9cbd4324 --- /dev/null +++ b/crates/fspy_nostd/src/mm/windows.rs @@ -0,0 +1,124 @@ +use core::{ffi::c_void, ptr}; + +use bitflags::bitflags; +use windows_sys::Win32::System::Memory::{ + CreateFileMappingW, FILE_MAP_READ, FILE_MAP_WRITE, MEMORY_MAPPED_VIEW_ADDRESS, MapViewOfFile, + PAGE_READWRITE, UnmapViewOfFile, +}; + +use crate::{OwnedHandle, Result, SecurityAttributes, WideCStr}; + +bitflags! { + /// Page protection for a file mapping. + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub struct PageProtection: u32 { + /// Permit pages to be read and written. + const READ_WRITE = PAGE_READWRITE; + } + + /// Access requested for a mapped view. + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub struct MappingAccess: u32 { + /// Permit reads from the view. + const READ = FILE_MAP_READ; + /// Permit writes to the view. + const WRITE = FILE_MAP_WRITE; + } +} + +/// An owned view of a file-mapping object. +pub struct MappingView { + ptr: core::ptr::NonNull, +} + +// SAFETY: a view owns no thread-affine state. Synchronization of accesses to +// its shared bytes is the caller's responsibility. +unsafe impl Send for MappingView {} +// SAFETY: sharing the view does not itself access the mapped bytes. +unsafe impl Sync for MappingView {} + +impl MappingView { + /// Returns a raw pointer to the first mapped byte. + #[must_use] + pub const fn as_ptr(&self) -> *mut u8 { + self.ptr.as_ptr() + } +} + +impl Drop for MappingView { + fn drop(&mut self) { + // SAFETY: this is the complete view owned by `self` and is unmapped + // exactly once. + let _ = unsafe { + UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS { + Value: self.ptr.as_ptr().cast::(), + }) + }; + } +} + +/// Calls `CreateFileMappingW` and returns the new mapping-object handle. +/// +/// # Errors +/// +/// Returns the error reported by `CreateFileMappingW`. +pub fn create_file_mapping( + file: &OwnedHandle, + mapping_attributes: Option<&SecurityAttributes>, + protection: PageProtection, + maximum_size_high: u32, + maximum_size_low: u32, + name: Option>, +) -> Result { + let mapping_attributes = mapping_attributes.map_or(ptr::null(), SecurityAttributes::as_raw); + let name = name.map_or(ptr::null(), |name| name.as_ptr()); + + // SAFETY: `file` is valid. Security attributes are either null or a valid + // opaque value owned by this module, and `name` is either null or backed + // by a valid borrowed wide C string. Windows validates the protection and + // size arguments. + let mapping = unsafe { + CreateFileMappingW( + file.as_raw(), + mapping_attributes, + protection.bits(), + maximum_size_high, + maximum_size_low, + name, + ) + }; + let Some(mapping) = core::ptr::NonNull::new(mapping) else { + return Err(crate::windows::last_error()); + }; + // SAFETY: `CreateFileMappingW` returned a valid, newly owned handle. + Ok(unsafe { OwnedHandle::from_raw(mapping.as_ptr()) }) +} + +/// Calls `MapViewOfFile` and returns the new owned view. +/// +/// # Errors +/// +/// Returns the error reported by `MapViewOfFile`. +pub fn map_view_of_file( + mapping: &OwnedHandle, + access: MappingAccess, + file_offset_high: u32, + file_offset_low: u32, + bytes_to_map: usize, +) -> Result { + // SAFETY: `mapping` is a valid file-mapping object. Windows validates the + // requested access, offset, and size against that object. + let view = unsafe { + MapViewOfFile( + mapping.as_raw(), + access.bits(), + file_offset_high, + file_offset_low, + bytes_to_map, + ) + }; + let Some(ptr) = core::ptr::NonNull::new(view.Value.cast::()) else { + return Err(crate::windows::last_error()); + }; + Ok(MappingView { ptr }) +} diff --git a/crates/fspy_shm/Cargo.toml b/crates/fspy_shm/Cargo.toml index 34d3793b7..a0aaa45c4 100644 --- a/crates/fspy_shm/Cargo.toml +++ b/crates/fspy_shm/Cargo.toml @@ -19,7 +19,6 @@ windows-sys = { workspace = true, features = [ "Win32_Storage_FileSystem", "Win32_System_IO", "Win32_System_Ioctl", - "Win32_System_Memory", ] } [dev-dependencies] diff --git a/crates/fspy_shm/src/windows.rs b/crates/fspy_shm/src/windows.rs index 2b6ee84a0..e9ebc9dea 100644 --- a/crates/fspy_shm/src/windows.rs +++ b/crates/fspy_shm/src/windows.rs @@ -9,31 +9,28 @@ use std::{ #[cfg(test)] use std::{fs::File, os::windows::io::AsRawHandle as _}; +use fspy_nostd::{ + fs::{CreationDisposition, FileAccess, FileOptions, FileShare}, + mm::{MappingAccess, PageProtection}, +}; use uuid::Uuid; #[cfg(test)] use windows_sys::Win32::Storage::FileSystem::{ FILE_STANDARD_INFO, FileStandardInfo, GetFileInformationByHandleEx, }; use windows_sys::Win32::{ - Foundation::{ERROR_ACCESS_DENIED, GENERIC_READ, GENERIC_WRITE, GetLastError}, + Foundation::{ERROR_ACCESS_DENIED, GetLastError}, Storage::FileSystem::{ - CREATE_NEW, DELETE, FILE_ATTRIBUTE_TEMPORARY, FILE_CREATION_DISPOSITION, FILE_DISPOSITION_FLAG_DELETE, FILE_DISPOSITION_FLAG_IGNORE_READONLY_ATTRIBUTE, FILE_DISPOSITION_FLAG_POSIX_SEMANTICS, FILE_DISPOSITION_INFO_EX, FILE_END_OF_FILE_INFO, - FILE_FLAG_DELETE_ON_CLOSE, FILE_FLAG_OPEN_REPARSE_POINT, FILE_FLAGS_AND_ATTRIBUTES, - FILE_SHARE_DELETE, FILE_SHARE_MODE, FILE_SHARE_READ, FILE_SHARE_WRITE, - FileDispositionInfoEx, FileEndOfFileInfo, OPEN_EXISTING, SetFileInformationByHandle, - }, - System::{ - IO::DeviceIoControl, - Ioctl::FSCTL_SET_SPARSE, - Memory::{FILE_MAP_READ, FILE_MAP_WRITE, PAGE_READWRITE}, + FileDispositionInfoEx, FileEndOfFileInfo, SetFileInformationByHandle, }, + System::{IO::DeviceIoControl, Ioctl::FSCTL_SET_SPARSE}, }; use crate::BACKING_PREFIX; -const SHARE_ALL: FILE_SHARE_MODE = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE; +const SHARE_ALL: FileShare = FileShare::READ.union(FileShare::WRITE).union(FileShare::DELETE); /// Keeps the shared memory's identifier alive and removes it on drop. /// @@ -88,10 +85,10 @@ pub fn create(size: usize) -> io::Result<(ShmKeeper, ShmHandle)> { .join(format!("{BACKING_PREFIX}{}.shm", Uuid::new_v4().simple())); let file = open_file( path.as_os_str(), - GENERIC_READ | GENERIC_WRITE, - CREATE_NEW, + FileAccess::GENERIC_READ | FileAccess::GENERIC_WRITE, + CreationDisposition::CreateNew, // Ask Windows to keep the data in memory when it can. - FILE_ATTRIBUTE_TEMPORARY, + FileOptions::TEMPORARY, )?; // The keeper exists from here on, so every error path below cleans up. let keeper = ShmKeeper { path }; @@ -116,7 +113,12 @@ pub fn create(size: usize) -> io::Result<(ShmKeeper, ShmHandle)> { /// Returns an error if the shared memory is unavailable, which is the common /// case once its keeper has been dropped. pub fn open(id: &OsStr) -> io::Result { - let file = open_file(id, GENERIC_READ | GENERIC_WRITE, OPEN_EXISTING, 0)?; + let file = open_file( + id, + FileAccess::GENERIC_READ | FileAccess::GENERIC_WRITE, + CreationDisposition::OpenExisting, + FileOptions::empty(), + )?; // If another process shrinks the file before `map`, mapping fails. If it // resizes afterwards, nothing here touches the mapped pages. A concurrent // resize cannot make a mapping access invalid memory. @@ -129,18 +131,18 @@ pub fn open(id: &OsStr) -> io::Result { fn open_file( path: &OsStr, - desired_access: u32, - creation_disposition: FILE_CREATION_DISPOSITION, - flags_and_attributes: FILE_FLAGS_AND_ATTRIBUTES, + access: FileAccess, + disposition: CreationDisposition, + options: FileOptions, ) -> io::Result { let path = copy_path(path)?; fspy_nostd::fs::create_file( as_nostd_path(&path), - desired_access, + access, SHARE_ALL, None, - creation_disposition, - flags_and_attributes, + disposition, + options, None, ) .map_err(error_to_io) @@ -218,11 +220,11 @@ fn remove_file(path: fspy_nostd::WideCStr<'_, fspy_nostd::Fat>) -> fspy_nostd::R if error.raw_os_error() == ERROR_ACCESS_DENIED && let Ok(file) = fspy_nostd::fs::create_file( path, - DELETE, + FileAccess::DELETE, SHARE_ALL, None, - OPEN_EXISTING, - FILE_FLAG_OPEN_REPARSE_POINT, + CreationDisposition::OpenExisting, + FileOptions::OPEN_REPARSE_POINT, None, ) && set_posix_delete(&file).is_ok() @@ -268,11 +270,11 @@ impl Drop for ShmKeeper { if remove_file(as_nostd_path(&path)).is_err() { let _ = fspy_nostd::fs::create_file( as_nostd_path(&path), - DELETE, + FileAccess::DELETE, SHARE_ALL, None, - OPEN_EXISTING, - FILE_FLAG_DELETE_ON_CLOSE, + CreationDisposition::OpenExisting, + FileOptions::DELETE_ON_CLOSE, None, ); } @@ -304,7 +306,7 @@ impl ShmHandle { let mapping = fspy_nostd::mm::create_file_mapping::( &self.file, None, - PAGE_READWRITE, + PageProtection::READ_WRITE, 0, 0, None, @@ -312,7 +314,7 @@ impl ShmHandle { .map_err(error_to_io)?; let view = fspy_nostd::mm::map_view_of_file( &mapping, - FILE_MAP_READ | FILE_MAP_WRITE, + MappingAccess::READ | MappingAccess::WRITE, 0, 0, self.size.get(), From 8c31846117fe42c916c6747dfdb038cd9190c673 Mon Sep 17 00:00:00 2001 From: wan9chi Date: Thu, 13 Aug 2026 16:48:09 +0800 Subject: [PATCH 09/18] docs(fspy-shm): clarify Windows safety invariants Co-authored-by: GPT-5 Codex --- crates/fspy_nostd/src/fs/windows.rs | 9 +++++---- crates/fspy_nostd/src/mm/windows.rs | 12 ++++++------ crates/fspy_nostd/src/windows.rs | 8 ++++---- crates/fspy_shm/src/windows.rs | 22 +++++++++++++--------- 4 files changed, 28 insertions(+), 23 deletions(-) diff --git a/crates/fspy_nostd/src/fs/windows.rs b/crates/fspy_nostd/src/fs/windows.rs index 6d0e25eed..8b5c16cc5 100644 --- a/crates/fspy_nostd/src/fs/windows.rs +++ b/crates/fspy_nostd/src/fs/windows.rs @@ -93,9 +93,9 @@ pub fn create_file( let security_attributes = security_attributes.map_or(ptr::null(), SecurityAttributes::as_raw); let template_file = template_file.map_or(ptr::null_mut(), OwnedHandle::as_raw); - // SAFETY: `path` is NUL-terminated. Both optional pointers are either null - // or backed by their valid borrowed wrapper types. Other arguments are - // passed through unchanged for Windows to validate. + // SAFETY: `path` and the optional security-attributes pointer remain + // readable for the call, and the optional template handle remains open. + // Windows validates the template's object type and all scalar options. let handle = unsafe { CreateFileW( path.as_ptr(), @@ -133,7 +133,8 @@ pub fn delete_file(path: WideCStr<'_, R>) -> Result<()> { /// Returns the error reported by `GetFileSizeEx`. pub fn get_file_size(file: &OwnedHandle) -> Result { let mut size = 0; - // SAFETY: `file` is valid and `size` is writable for the call. + // SAFETY: `file` keeps the opaque handle open and `size` is writable for + // the call. Windows rejects handles that do not support a size query. crate::windows::bool_result(unsafe { GetFileSizeEx(file.as_raw(), &raw mut size) })?; Ok(size) } diff --git a/crates/fspy_nostd/src/mm/windows.rs b/crates/fspy_nostd/src/mm/windows.rs index b9cbd4324..cddd12204 100644 --- a/crates/fspy_nostd/src/mm/windows.rs +++ b/crates/fspy_nostd/src/mm/windows.rs @@ -73,10 +73,9 @@ pub fn create_file_mapping( let mapping_attributes = mapping_attributes.map_or(ptr::null(), SecurityAttributes::as_raw); let name = name.map_or(ptr::null(), |name| name.as_ptr()); - // SAFETY: `file` is valid. Security attributes are either null or a valid - // opaque value owned by this module, and `name` is either null or backed - // by a valid borrowed wide C string. Windows validates the protection and - // size arguments. + // SAFETY: `file` keeps the opaque handle open. The optional security + // attributes and name are either null or readable for the call. Windows + // validates the handle's object type, protection, and sizes. let mapping = unsafe { CreateFileMappingW( file.as_raw(), @@ -106,8 +105,9 @@ pub fn map_view_of_file( file_offset_low: u32, bytes_to_map: usize, ) -> Result { - // SAFETY: `mapping` is a valid file-mapping object. Windows validates the - // requested access, offset, and size against that object. + // SAFETY: `mapping` keeps the opaque handle open for the call. Windows + // verifies that it names a file-mapping object and validates the access, + // offset, and size; any mismatch is reported as a null result. let view = unsafe { MapViewOfFile( mapping.as_raw(), diff --git a/crates/fspy_nostd/src/windows.rs b/crates/fspy_nostd/src/windows.rs index 0c2d88c12..84c49e071 100644 --- a/crates/fspy_nostd/src/windows.rs +++ b/crates/fspy_nostd/src/windows.rs @@ -43,11 +43,11 @@ impl OwnedHandle { } } -// SAFETY: Windows kernel handles are not thread-affine. The operations exposed -// by this crate provide their own synchronization or do not mutate the handle. +// SAFETY: Windows kernel handles are process-scoped and may be moved between +// threads. Moving this value does not access the referenced kernel object. unsafe impl Send for OwnedHandle {} -// SAFETY: as above; sharing the value does not itself access the referenced -// kernel object. +// SAFETY: sharing a handle value does not access the kernel object. Each +// operation is responsible for the synchronization required by that object. unsafe impl Sync for OwnedHandle {} impl Drop for OwnedHandle { diff --git a/crates/fspy_shm/src/windows.rs b/crates/fspy_shm/src/windows.rs index e9ebc9dea..6b7980ae3 100644 --- a/crates/fspy_shm/src/windows.rs +++ b/crates/fspy_shm/src/windows.rs @@ -178,9 +178,10 @@ fn bool_result(result: i32) -> fspy_nostd::Result<()> { fn set_sparse(file: &fspy_nostd::OwnedHandle) -> fspy_nostd::Result<()> { let mut bytes_returned = 0; - // SAFETY: `file` is valid and synchronous. `FSCTL_SET_SPARSE` accepts null - // input and output buffers to mark the file sparse, and `bytes_returned` - // is writable for the call. + // SAFETY: `file` keeps the handle open, and every caller supplies a file + // opened without `FILE_FLAG_OVERLAPPED`. `FSCTL_SET_SPARSE` accepts null + // input and output buffers, and `bytes_returned` is writable. Windows + // validates that the handle supports this control code. bool_result(unsafe { DeviceIoControl( file.as_raw(), @@ -200,8 +201,9 @@ fn set_end_of_file(file: &fspy_nostd::OwnedHandle, len: i64) -> fspy_nostd::Resu const _: [(); 8] = [(); size_of::()]; let info = FILE_END_OF_FILE_INFO { EndOfFile: len }; - // SAFETY: `file` is valid and `info` has the type and exact size required - // by `FileEndOfFileInfo`. + // SAFETY: `file` keeps the opaque handle open. `info` is initialized and + // has the type and exact size required by `FileEndOfFileInfo`; Windows + // validates the handle type and access rights. bool_result(unsafe { SetFileInformationByHandle( file.as_raw(), @@ -246,8 +248,9 @@ fn set_posix_delete(file: &fspy_nostd::OwnedHandle) -> fspy_nostd::Result<()> { | FILE_DISPOSITION_FLAG_POSIX_SEMANTICS | FILE_DISPOSITION_FLAG_IGNORE_READONLY_ATTRIBUTE, }; - // SAFETY: `file` is valid and `info` has the type and exact size required - // by `FileDispositionInfoEx`. + // SAFETY: `file` keeps the opaque handle open. `info` is initialized and + // has the type and exact size required by `FileDispositionInfoEx`; Windows + // validates the handle type, access rights, and supported flags. bool_result(unsafe { SetFileInformationByHandle( file.as_raw(), @@ -359,8 +362,9 @@ pub fn file_sizes(file: &File) -> io::Result<(u64, u64)> { let mut info = FILE_STANDARD_INFO::default(); let info_size = u32::try_from(std::mem::size_of::()) .map_err(|_| io::Error::other("file size information is too large"))?; - // SAFETY: `file` supplies a valid handle and `info` is a writable - // FILE_STANDARD_INFO buffer of exactly `info_size` bytes. + // SAFETY: `file` keeps its handle open and `info` is a writable + // `FILE_STANDARD_INFO` buffer of exactly `info_size` bytes. Windows + // validates that the handle supports this information class. let result = unsafe { GetFileInformationByHandleEx( file.as_raw_handle().cast(), From ce8efb78fde51ab78beca1e673bd2138b25536cb Mon Sep 17 00:00:00 2001 From: wan9chi Date: Thu, 13 Aug 2026 16:55:32 +0800 Subject: [PATCH 10/18] refactor(fspy-nostd): encode creation disposition values Co-authored-by: GPT-5 Codex --- crates/fspy_nostd/src/fs/windows.rs | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/crates/fspy_nostd/src/fs/windows.rs b/crates/fspy_nostd/src/fs/windows.rs index 8b5c16cc5..146014fdc 100644 --- a/crates/fspy_nostd/src/fs/windows.rs +++ b/crates/fspy_nostd/src/fs/windows.rs @@ -50,29 +50,18 @@ bitflags! { /// How `CreateFileW` handles an existing or missing file. #[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(u32)] pub enum CreationDisposition { /// Create a new file and fail if it already exists. - CreateNew, + CreateNew = CREATE_NEW, /// Create a new file or replace an existing file. - CreateAlways, + CreateAlways = CREATE_ALWAYS, /// Open an existing file and fail if it does not exist. - OpenExisting, + OpenExisting = OPEN_EXISTING, /// Open an existing file or create a new file. - OpenAlways, + OpenAlways = OPEN_ALWAYS, /// Open and truncate an existing file. - TruncateExisting, -} - -impl CreationDisposition { - const fn into_raw(self) -> u32 { - match self { - Self::CreateNew => CREATE_NEW, - Self::CreateAlways => CREATE_ALWAYS, - Self::OpenExisting => OPEN_EXISTING, - Self::OpenAlways => OPEN_ALWAYS, - Self::TruncateExisting => TRUNCATE_EXISTING, - } - } + TruncateExisting = TRUNCATE_EXISTING, } /// Calls `CreateFileW` and returns the new owned handle. @@ -102,7 +91,7 @@ pub fn create_file( access.bits(), share.bits(), security_attributes, - disposition.into_raw(), + disposition as u32, options.bits(), template_file, ) From 9ad61d40dedf955cb3cdfda9c3851db696fd1423 Mon Sep 17 00:00:00 2001 From: wan9chi Date: Thu, 13 Aug 2026 17:06:28 +0800 Subject: [PATCH 11/18] refactor(fspy-nostd): borrow Windows handles Co-authored-by: GPT-5 Codex --- crates/fspy_nostd/src/fs/windows.rs | 12 ++-- crates/fspy_nostd/src/lib.rs | 5 +- crates/fspy_nostd/src/mm/windows.rs | 12 ++-- crates/fspy_nostd/src/windows.rs | 89 +++++++++++++++++++++++++++-- crates/fspy_shm/src/windows.rs | 30 +++++----- 5 files changed, 116 insertions(+), 32 deletions(-) diff --git a/crates/fspy_nostd/src/fs/windows.rs b/crates/fspy_nostd/src/fs/windows.rs index 146014fdc..fd611dbdd 100644 --- a/crates/fspy_nostd/src/fs/windows.rs +++ b/crates/fspy_nostd/src/fs/windows.rs @@ -11,7 +11,7 @@ use windows_sys::Win32::{ }, }; -use crate::{OwnedHandle, Result, SecurityAttributes, WideCStr}; +use crate::{AsRawHandle as _, BorrowedHandle, OwnedHandle, Result, SecurityAttributes, WideCStr}; bitflags! { /// Access rights requested for a file handle. @@ -77,10 +77,10 @@ pub fn create_file( security_attributes: Option<&SecurityAttributes>, disposition: CreationDisposition, options: FileOptions, - template_file: Option<&OwnedHandle>, + template_file: Option>, ) -> Result { let security_attributes = security_attributes.map_or(ptr::null(), SecurityAttributes::as_raw); - let template_file = template_file.map_or(ptr::null_mut(), OwnedHandle::as_raw); + let template_file = template_file.map_or(ptr::null_mut(), |file| file.as_raw_handle()); // SAFETY: `path` and the optional security-attributes pointer remain // readable for the call, and the optional template handle remains open. @@ -100,7 +100,7 @@ pub fn create_file( Err(crate::windows::last_error()) } else { // SAFETY: `CreateFileW` returned a valid, newly owned handle. - Ok(unsafe { OwnedHandle::from_raw(handle) }) + Ok(unsafe { OwnedHandle::from_raw_handle(handle) }) } } @@ -120,10 +120,10 @@ pub fn delete_file(path: WideCStr<'_, R>) -> Result<()> { /// # Errors /// /// Returns the error reported by `GetFileSizeEx`. -pub fn get_file_size(file: &OwnedHandle) -> Result { +pub fn get_file_size(file: BorrowedHandle<'_>) -> Result { let mut size = 0; // SAFETY: `file` keeps the opaque handle open and `size` is writable for // the call. Windows rejects handles that do not support a size query. - crate::windows::bool_result(unsafe { GetFileSizeEx(file.as_raw(), &raw mut size) })?; + crate::windows::bool_result(unsafe { GetFileSizeEx(file.as_raw_handle(), &raw mut size) })?; Ok(size) } diff --git a/crates/fspy_nostd/src/lib.rs b/crates/fspy_nostd/src/lib.rs index c7237b235..53dc804cb 100644 --- a/crates/fspy_nostd/src/lib.rs +++ b/crates/fspy_nostd/src/lib.rs @@ -22,7 +22,10 @@ pub mod param; pub use c_str::{CStr, CStrUnit, Fat, Thin, Units, WideCStr}; #[cfg(windows)] -pub use windows::{OwnedHandle, SecurityAttributes, get_module_handle}; +pub use windows::{ + AsHandle, AsRawHandle, BorrowedHandle, OwnedHandle, RawHandle, SecurityAttributes, + get_module_handle, +}; #[cfg(windows)] #[derive(Clone, Copy, Debug, Eq, PartialEq)] diff --git a/crates/fspy_nostd/src/mm/windows.rs b/crates/fspy_nostd/src/mm/windows.rs index cddd12204..2d461516b 100644 --- a/crates/fspy_nostd/src/mm/windows.rs +++ b/crates/fspy_nostd/src/mm/windows.rs @@ -6,7 +6,7 @@ use windows_sys::Win32::System::Memory::{ PAGE_READWRITE, UnmapViewOfFile, }; -use crate::{OwnedHandle, Result, SecurityAttributes, WideCStr}; +use crate::{AsRawHandle as _, BorrowedHandle, OwnedHandle, Result, SecurityAttributes, WideCStr}; bitflags! { /// Page protection for a file mapping. @@ -63,7 +63,7 @@ impl Drop for MappingView { /// /// Returns the error reported by `CreateFileMappingW`. pub fn create_file_mapping( - file: &OwnedHandle, + file: BorrowedHandle<'_>, mapping_attributes: Option<&SecurityAttributes>, protection: PageProtection, maximum_size_high: u32, @@ -78,7 +78,7 @@ pub fn create_file_mapping( // validates the handle's object type, protection, and sizes. let mapping = unsafe { CreateFileMappingW( - file.as_raw(), + file.as_raw_handle(), mapping_attributes, protection.bits(), maximum_size_high, @@ -90,7 +90,7 @@ pub fn create_file_mapping( return Err(crate::windows::last_error()); }; // SAFETY: `CreateFileMappingW` returned a valid, newly owned handle. - Ok(unsafe { OwnedHandle::from_raw(mapping.as_ptr()) }) + Ok(unsafe { OwnedHandle::from_raw_handle(mapping.as_ptr()) }) } /// Calls `MapViewOfFile` and returns the new owned view. @@ -99,7 +99,7 @@ pub fn create_file_mapping( /// /// Returns the error reported by `MapViewOfFile`. pub fn map_view_of_file( - mapping: &OwnedHandle, + mapping: BorrowedHandle<'_>, access: MappingAccess, file_offset_high: u32, file_offset_low: u32, @@ -110,7 +110,7 @@ pub fn map_view_of_file( // offset, and size; any mismatch is reported as a null result. let view = unsafe { MapViewOfFile( - mapping.as_raw(), + mapping.as_raw_handle(), access.bits(), file_offset_high, file_offset_low, diff --git a/crates/fspy_nostd/src/windows.rs b/crates/fspy_nostd/src/windows.rs index 84c49e071..05bc86ec2 100644 --- a/crates/fspy_nostd/src/windows.rs +++ b/crates/fspy_nostd/src/windows.rs @@ -1,4 +1,4 @@ -use core::{ffi::c_void, ptr::NonNull}; +use core::{ffi::c_void, marker::PhantomData, ptr::NonNull}; use windows_sys::Win32::{ Foundation::GetLastError, Security::SECURITY_ATTRIBUTES, @@ -7,9 +7,35 @@ use windows_sys::Win32::{ use crate::{Result, WideCStr}; +/// A raw Windows kernel handle. +pub type RawHandle = *mut c_void; + +/// A borrowed Windows kernel handle. +/// +/// Its lifetime is tied to the value that keeps the handle open. +#[derive(Clone, Copy)] +#[repr(transparent)] +pub struct BorrowedHandle<'handle> { + handle: NonNull, + lifetime: PhantomData<&'handle OwnedHandle>, +} + /// An owned Windows kernel handle. +#[repr(transparent)] pub struct OwnedHandle(NonNull); +/// A type that can lend a Windows kernel handle. +pub trait AsHandle { + /// Borrows the handle. + fn as_handle(&self) -> BorrowedHandle<'_>; +} + +/// A type that exposes a raw Windows kernel handle. +pub trait AsRawHandle { + /// Returns the raw handle without transferring ownership. + fn as_raw_handle(&self) -> RawHandle; +} + /// Opaque security attributes accepted by Win32 creation functions. /// /// This type has no public constructor. Non-default security attributes will @@ -24,6 +50,21 @@ impl SecurityAttributes { } } +impl BorrowedHandle<'_> { + /// Borrows a raw handle. + /// + /// # Safety + /// + /// `handle` must be a valid, non-null, open handle and remain open for the + /// lifetime of the returned value. + #[must_use] + pub const unsafe fn borrow_raw(handle: RawHandle) -> Self { + // SAFETY: the caller guarantees that the handle is non-null. + let handle = unsafe { NonNull::new_unchecked(handle) }; + Self { handle, lifetime: PhantomData } + } +} + impl OwnedHandle { /// Creates an owned handle after the caller has validated the raw value. /// @@ -31,14 +72,46 @@ impl OwnedHandle { /// /// `handle` must be a valid, non-null, uniquely owned handle that may be /// closed with `CloseHandle`. - pub(crate) const unsafe fn from_raw(handle: *mut c_void) -> Self { + pub(crate) const unsafe fn from_raw_handle(handle: RawHandle) -> Self { // SAFETY: the caller guarantees that the handle is non-null. Self(unsafe { NonNull::new_unchecked(handle) }) } +} - /// Returns the raw Windows handle without transferring ownership. - #[must_use] - pub const fn as_raw(&self) -> *mut c_void { +impl AsHandle for BorrowedHandle<'_> { + fn as_handle(&self) -> BorrowedHandle<'_> { + *self + } +} + +impl AsHandle for OwnedHandle { + fn as_handle(&self) -> BorrowedHandle<'_> { + // SAFETY: `self` keeps the same valid handle open for the returned + // borrow's lifetime. + unsafe { BorrowedHandle::borrow_raw(self.as_raw_handle()) } + } +} + +impl AsHandle for &T { + fn as_handle(&self) -> BorrowedHandle<'_> { + T::as_handle(self) + } +} + +impl AsHandle for &mut T { + fn as_handle(&self) -> BorrowedHandle<'_> { + T::as_handle(self) + } +} + +impl AsRawHandle for BorrowedHandle<'_> { + fn as_raw_handle(&self) -> RawHandle { + self.handle.as_ptr() + } +} + +impl AsRawHandle for OwnedHandle { + fn as_raw_handle(&self) -> RawHandle { self.0.as_ptr() } } @@ -46,14 +119,18 @@ impl OwnedHandle { // SAFETY: Windows kernel handles are process-scoped and may be moved between // threads. Moving this value does not access the referenced kernel object. unsafe impl Send for OwnedHandle {} +// SAFETY: as above; the borrow does not add thread affinity. +unsafe impl Send for BorrowedHandle<'_> {} // SAFETY: sharing a handle value does not access the kernel object. Each // operation is responsible for the synchronization required by that object. unsafe impl Sync for OwnedHandle {} +// SAFETY: as above; sharing the borrowed value does not access the object. +unsafe impl Sync for BorrowedHandle<'_> {} impl Drop for OwnedHandle { fn drop(&mut self) { // SAFETY: this type owns a valid handle and closes it exactly once. - let _ = unsafe { windows_sys::Win32::Foundation::CloseHandle(self.as_raw()) }; + let _ = unsafe { windows_sys::Win32::Foundation::CloseHandle(self.as_raw_handle()) }; } } diff --git a/crates/fspy_shm/src/windows.rs b/crates/fspy_shm/src/windows.rs index 6b7980ae3..822167cef 100644 --- a/crates/fspy_shm/src/windows.rs +++ b/crates/fspy_shm/src/windows.rs @@ -10,6 +10,7 @@ use std::{ use std::{fs::File, os::windows::io::AsRawHandle as _}; use fspy_nostd::{ + AsHandle as _, AsRawHandle as _, BorrowedHandle, fs::{CreationDisposition, FileAccess, FileOptions, FileShare}, mm::{MappingAccess, PageProtection}, }; @@ -96,9 +97,9 @@ pub fn create(size: usize) -> io::Result<(ShmKeeper, ShmHandle)> { // NTFS allocates clusters for the whole logical size unless the file is // marked sparse first, which would turn the capacity into real disk usage. // Volumes without sparse-file support fail here. - set_sparse(&file).map_err(error_to_io)?; + set_sparse(file.as_handle()).map_err(error_to_io)?; // Every byte reads as zero because the file is all holes. - set_end_of_file(&file, size_i64).map_err(error_to_io)?; + set_end_of_file(file.as_handle(), size_i64).map_err(error_to_io)?; Ok((keeper, ShmHandle { file, size })) } @@ -122,8 +123,11 @@ pub fn open(id: &OsStr) -> io::Result { // If another process shrinks the file before `map`, mapping fails. If it // resizes afterwards, nothing here touches the mapped pages. A concurrent // resize cannot make a mapping access invalid memory. - let size = usize::try_from(fspy_nostd::fs::get_file_size(&file).map_err(error_to_io)?) - .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "invalid shared-memory size"))?; + let size = + usize::try_from(fspy_nostd::fs::get_file_size(file.as_handle()).map_err(error_to_io)?) + .map_err(|_| { + io::Error::new(io::ErrorKind::InvalidData, "invalid shared-memory size") + })?; let size = NonZeroUsize::new(size) .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "shared-memory size is zero"))?; Ok(ShmHandle { file, size }) @@ -176,7 +180,7 @@ fn bool_result(result: i32) -> fspy_nostd::Result<()> { } } -fn set_sparse(file: &fspy_nostd::OwnedHandle) -> fspy_nostd::Result<()> { +fn set_sparse(file: BorrowedHandle<'_>) -> fspy_nostd::Result<()> { let mut bytes_returned = 0; // SAFETY: `file` keeps the handle open, and every caller supplies a file // opened without `FILE_FLAG_OVERLAPPED`. `FSCTL_SET_SPARSE` accepts null @@ -184,7 +188,7 @@ fn set_sparse(file: &fspy_nostd::OwnedHandle) -> fspy_nostd::Result<()> { // validates that the handle supports this control code. bool_result(unsafe { DeviceIoControl( - file.as_raw(), + file.as_raw_handle(), FSCTL_SET_SPARSE, ptr::null(), 0, @@ -196,7 +200,7 @@ fn set_sparse(file: &fspy_nostd::OwnedHandle) -> fspy_nostd::Result<()> { }) } -fn set_end_of_file(file: &fspy_nostd::OwnedHandle, len: i64) -> fspy_nostd::Result<()> { +fn set_end_of_file(file: BorrowedHandle<'_>, len: i64) -> fspy_nostd::Result<()> { const INFO_SIZE: u32 = 8; const _: [(); 8] = [(); size_of::()]; @@ -206,7 +210,7 @@ fn set_end_of_file(file: &fspy_nostd::OwnedHandle, len: i64) -> fspy_nostd::Resu // validates the handle type and access rights. bool_result(unsafe { SetFileInformationByHandle( - file.as_raw(), + file.as_raw_handle(), FileEndOfFileInfo, (&raw const info).cast::(), INFO_SIZE, @@ -229,7 +233,7 @@ fn remove_file(path: fspy_nostd::WideCStr<'_, fspy_nostd::Fat>) -> fspy_nostd::R FileOptions::OPEN_REPARSE_POINT, None, ) - && set_posix_delete(&file).is_ok() + && set_posix_delete(file.as_handle()).is_ok() { return Ok(()); } @@ -239,7 +243,7 @@ fn remove_file(path: fspy_nostd::WideCStr<'_, fspy_nostd::Fat>) -> fspy_nostd::R Err(error) } -fn set_posix_delete(file: &fspy_nostd::OwnedHandle) -> fspy_nostd::Result<()> { +fn set_posix_delete(file: BorrowedHandle<'_>) -> fspy_nostd::Result<()> { const INFO_SIZE: u32 = 4; const _: [(); 4] = [(); size_of::()]; @@ -253,7 +257,7 @@ fn set_posix_delete(file: &fspy_nostd::OwnedHandle) -> fspy_nostd::Result<()> { // validates the handle type, access rights, and supported flags. bool_result(unsafe { SetFileInformationByHandle( - file.as_raw(), + file.as_raw_handle(), FileDispositionInfoEx, (&raw const info).cast::(), INFO_SIZE, @@ -307,7 +311,7 @@ impl ShmHandle { // and no name creates an unnamed one. Both maximum-size halves are // zero, so Windows uses the current file size. let mapping = fspy_nostd::mm::create_file_mapping::( - &self.file, + self.file.as_handle(), None, PageProtection::READ_WRITE, 0, @@ -316,7 +320,7 @@ impl ShmHandle { ) .map_err(error_to_io)?; let view = fspy_nostd::mm::map_view_of_file( - &mapping, + mapping.as_handle(), MappingAccess::READ | MappingAccess::WRITE, 0, 0, From b5415295326c36250ec80d284a0d5f8e2ad40976 Mon Sep 17 00:00:00 2001 From: wan9chi Date: Thu, 13 Aug 2026 17:20:54 +0800 Subject: [PATCH 12/18] fix(fspy-nostd): permit zero Windows handles Co-authored-by: GPT-5 Codex --- crates/fspy_nostd/src/fs/windows.rs | 2 +- crates/fspy_nostd/src/mm/windows.rs | 6 +++--- crates/fspy_nostd/src/windows.rs | 26 ++++++++++++++------------ 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/crates/fspy_nostd/src/fs/windows.rs b/crates/fspy_nostd/src/fs/windows.rs index fd611dbdd..d9579c436 100644 --- a/crates/fspy_nostd/src/fs/windows.rs +++ b/crates/fspy_nostd/src/fs/windows.rs @@ -96,7 +96,7 @@ pub fn create_file( template_file, ) }; - if handle == INVALID_HANDLE_VALUE || handle.is_null() { + if handle == INVALID_HANDLE_VALUE { Err(crate::windows::last_error()) } else { // SAFETY: `CreateFileW` returned a valid, newly owned handle. diff --git a/crates/fspy_nostd/src/mm/windows.rs b/crates/fspy_nostd/src/mm/windows.rs index 2d461516b..c4ba82d5b 100644 --- a/crates/fspy_nostd/src/mm/windows.rs +++ b/crates/fspy_nostd/src/mm/windows.rs @@ -86,11 +86,11 @@ pub fn create_file_mapping( name, ) }; - let Some(mapping) = core::ptr::NonNull::new(mapping) else { + if mapping.is_null() { return Err(crate::windows::last_error()); - }; + } // SAFETY: `CreateFileMappingW` returned a valid, newly owned handle. - Ok(unsafe { OwnedHandle::from_raw_handle(mapping.as_ptr()) }) + Ok(unsafe { OwnedHandle::from_raw_handle(mapping) }) } /// Calls `MapViewOfFile` and returns the new owned view. diff --git a/crates/fspy_nostd/src/windows.rs b/crates/fspy_nostd/src/windows.rs index 05bc86ec2..d4ae71c56 100644 --- a/crates/fspy_nostd/src/windows.rs +++ b/crates/fspy_nostd/src/windows.rs @@ -13,16 +13,21 @@ pub type RawHandle = *mut c_void; /// A borrowed Windows kernel handle. /// /// Its lifetime is tied to the value that keeps the handle open. +/// `NULL` and `-1` are permitted because their validity is API-specific. #[derive(Clone, Copy)] #[repr(transparent)] pub struct BorrowedHandle<'handle> { - handle: NonNull, + handle: RawHandle, lifetime: PhantomData<&'handle OwnedHandle>, } /// An owned Windows kernel handle. +/// +/// `NULL` and `-1` are permitted because their validity is API-specific. #[repr(transparent)] -pub struct OwnedHandle(NonNull); +pub struct OwnedHandle { + handle: RawHandle, +} /// A type that can lend a Windows kernel handle. pub trait AsHandle { @@ -55,12 +60,10 @@ impl BorrowedHandle<'_> { /// /// # Safety /// - /// `handle` must be a valid, non-null, open handle and remain open for the - /// lifetime of the returned value. + /// `handle` must be a valid open handle and remain open for the lifetime of + /// the returned value. #[must_use] pub const unsafe fn borrow_raw(handle: RawHandle) -> Self { - // SAFETY: the caller guarantees that the handle is non-null. - let handle = unsafe { NonNull::new_unchecked(handle) }; Self { handle, lifetime: PhantomData } } } @@ -70,11 +73,10 @@ impl OwnedHandle { /// /// # Safety /// - /// `handle` must be a valid, non-null, uniquely owned handle that may be - /// closed with `CloseHandle`. + /// `handle` must be a valid, uniquely owned handle that may be closed with + /// `CloseHandle`. pub(crate) const unsafe fn from_raw_handle(handle: RawHandle) -> Self { - // SAFETY: the caller guarantees that the handle is non-null. - Self(unsafe { NonNull::new_unchecked(handle) }) + Self { handle } } } @@ -106,13 +108,13 @@ impl AsHandle for &mut T { impl AsRawHandle for BorrowedHandle<'_> { fn as_raw_handle(&self) -> RawHandle { - self.handle.as_ptr() + self.handle } } impl AsRawHandle for OwnedHandle { fn as_raw_handle(&self) -> RawHandle { - self.0.as_ptr() + self.handle } } From 904c1d316df90099245e94497943ca3db86e29c0 Mon Sep 17 00:00:00 2001 From: wan9chi Date: Thu, 13 Aug 2026 17:26:25 +0800 Subject: [PATCH 13/18] refactor(fspy-nostd): remove handle conversion traits Co-authored-by: GPT-5 Codex --- crates/fspy_nostd/src/fs/windows.rs | 2 +- crates/fspy_nostd/src/lib.rs | 5 +-- crates/fspy_nostd/src/mm/windows.rs | 2 +- crates/fspy_nostd/src/windows.rs | 58 ++++++----------------------- crates/fspy_shm/src/windows.rs | 2 +- 5 files changed, 15 insertions(+), 54 deletions(-) diff --git a/crates/fspy_nostd/src/fs/windows.rs b/crates/fspy_nostd/src/fs/windows.rs index d9579c436..c8f163d0e 100644 --- a/crates/fspy_nostd/src/fs/windows.rs +++ b/crates/fspy_nostd/src/fs/windows.rs @@ -11,7 +11,7 @@ use windows_sys::Win32::{ }, }; -use crate::{AsRawHandle as _, BorrowedHandle, OwnedHandle, Result, SecurityAttributes, WideCStr}; +use crate::{BorrowedHandle, OwnedHandle, Result, SecurityAttributes, WideCStr}; bitflags! { /// Access rights requested for a file handle. diff --git a/crates/fspy_nostd/src/lib.rs b/crates/fspy_nostd/src/lib.rs index 53dc804cb..b1e68f976 100644 --- a/crates/fspy_nostd/src/lib.rs +++ b/crates/fspy_nostd/src/lib.rs @@ -22,10 +22,7 @@ pub mod param; pub use c_str::{CStr, CStrUnit, Fat, Thin, Units, WideCStr}; #[cfg(windows)] -pub use windows::{ - AsHandle, AsRawHandle, BorrowedHandle, OwnedHandle, RawHandle, SecurityAttributes, - get_module_handle, -}; +pub use windows::{BorrowedHandle, OwnedHandle, RawHandle, SecurityAttributes, get_module_handle}; #[cfg(windows)] #[derive(Clone, Copy, Debug, Eq, PartialEq)] diff --git a/crates/fspy_nostd/src/mm/windows.rs b/crates/fspy_nostd/src/mm/windows.rs index c4ba82d5b..e1bc2b274 100644 --- a/crates/fspy_nostd/src/mm/windows.rs +++ b/crates/fspy_nostd/src/mm/windows.rs @@ -6,7 +6,7 @@ use windows_sys::Win32::System::Memory::{ PAGE_READWRITE, UnmapViewOfFile, }; -use crate::{AsRawHandle as _, BorrowedHandle, OwnedHandle, Result, SecurityAttributes, WideCStr}; +use crate::{BorrowedHandle, OwnedHandle, Result, SecurityAttributes, WideCStr}; bitflags! { /// Page protection for a file mapping. diff --git a/crates/fspy_nostd/src/windows.rs b/crates/fspy_nostd/src/windows.rs index d4ae71c56..ff43152e4 100644 --- a/crates/fspy_nostd/src/windows.rs +++ b/crates/fspy_nostd/src/windows.rs @@ -29,18 +29,6 @@ pub struct OwnedHandle { handle: RawHandle, } -/// A type that can lend a Windows kernel handle. -pub trait AsHandle { - /// Borrows the handle. - fn as_handle(&self) -> BorrowedHandle<'_>; -} - -/// A type that exposes a raw Windows kernel handle. -pub trait AsRawHandle { - /// Returns the raw handle without transferring ownership. - fn as_raw_handle(&self) -> RawHandle; -} - /// Opaque security attributes accepted by Win32 creation functions. /// /// This type has no public constructor. Non-default security attributes will @@ -66,6 +54,12 @@ impl BorrowedHandle<'_> { pub const unsafe fn borrow_raw(handle: RawHandle) -> Self { Self { handle, lifetime: PhantomData } } + + /// Returns the raw handle without transferring ownership. + #[must_use] + pub const fn as_raw_handle(&self) -> RawHandle { + self.handle + } } impl OwnedHandle { @@ -78,43 +72,13 @@ impl OwnedHandle { pub(crate) const unsafe fn from_raw_handle(handle: RawHandle) -> Self { Self { handle } } -} -impl AsHandle for BorrowedHandle<'_> { - fn as_handle(&self) -> BorrowedHandle<'_> { - *self - } -} - -impl AsHandle for OwnedHandle { - fn as_handle(&self) -> BorrowedHandle<'_> { + /// Borrows this handle. + #[must_use] + pub const fn as_handle(&self) -> BorrowedHandle<'_> { // SAFETY: `self` keeps the same valid handle open for the returned // borrow's lifetime. - unsafe { BorrowedHandle::borrow_raw(self.as_raw_handle()) } - } -} - -impl AsHandle for &T { - fn as_handle(&self) -> BorrowedHandle<'_> { - T::as_handle(self) - } -} - -impl AsHandle for &mut T { - fn as_handle(&self) -> BorrowedHandle<'_> { - T::as_handle(self) - } -} - -impl AsRawHandle for BorrowedHandle<'_> { - fn as_raw_handle(&self) -> RawHandle { - self.handle - } -} - -impl AsRawHandle for OwnedHandle { - fn as_raw_handle(&self) -> RawHandle { - self.handle + unsafe { BorrowedHandle::borrow_raw(self.handle) } } } @@ -132,7 +96,7 @@ unsafe impl Sync for BorrowedHandle<'_> {} impl Drop for OwnedHandle { fn drop(&mut self) { // SAFETY: this type owns a valid handle and closes it exactly once. - let _ = unsafe { windows_sys::Win32::Foundation::CloseHandle(self.as_raw_handle()) }; + let _ = unsafe { windows_sys::Win32::Foundation::CloseHandle(self.handle) }; } } diff --git a/crates/fspy_shm/src/windows.rs b/crates/fspy_shm/src/windows.rs index 822167cef..bbdaa3765 100644 --- a/crates/fspy_shm/src/windows.rs +++ b/crates/fspy_shm/src/windows.rs @@ -10,7 +10,7 @@ use std::{ use std::{fs::File, os::windows::io::AsRawHandle as _}; use fspy_nostd::{ - AsHandle as _, AsRawHandle as _, BorrowedHandle, + BorrowedHandle, fs::{CreationDisposition, FileAccess, FileOptions, FileShare}, mm::{MappingAccess, PageProtection}, }; From ad516739893cd12122d4c63187b762f09bab8583 Mon Sep 17 00:00:00 2001 From: wan9chi Date: Thu, 13 Aug 2026 17:36:09 +0800 Subject: [PATCH 14/18] refactor(fspy-shm): centralize Windows file cleanup Co-authored-by: GPT-5 Codex --- crates/fspy_shm/src/windows.rs | 65 ++++++++++++++++------------------ 1 file changed, 30 insertions(+), 35 deletions(-) diff --git a/crates/fspy_shm/src/windows.rs b/crates/fspy_shm/src/windows.rs index bbdaa3765..a1b7164a3 100644 --- a/crates/fspy_shm/src/windows.rs +++ b/crates/fspy_shm/src/windows.rs @@ -140,16 +140,16 @@ fn open_file( options: FileOptions, ) -> io::Result { let path = copy_path(path)?; - fspy_nostd::fs::create_file( - as_nostd_path(&path), - access, - SHARE_ALL, - None, - disposition, - options, - None, - ) - .map_err(error_to_io) + open_file_wide(as_nostd_path(&path), access, disposition, options).map_err(error_to_io) +} + +fn open_file_wide( + path: fspy_nostd::WideCStr<'_, fspy_nostd::Fat>, + access: FileAccess, + disposition: CreationDisposition, + options: FileOptions, +) -> fspy_nostd::Result { + fspy_nostd::fs::create_file(path, access, SHARE_ALL, None, disposition, options, None) } fn copy_path(path: &OsStr) -> io::Result> { @@ -218,29 +218,41 @@ fn set_end_of_file(file: BorrowedHandle<'_>, len: i64) -> fspy_nostd::Result<()> }) } -fn remove_file(path: fspy_nostd::WideCStr<'_, fspy_nostd::Fat>) -> fspy_nostd::Result<()> { +fn remove_file(path: &OsStr) -> io::Result<()> { + let path = copy_path(path)?; + let path = as_nostd_path(&path); let error = match fspy_nostd::fs::delete_file(path) { Ok(()) => return Ok(()), Err(error) => error, }; if error.raw_os_error() == ERROR_ACCESS_DENIED - && let Ok(file) = fspy_nostd::fs::create_file( + && let Ok(file) = open_file_wide( path, FileAccess::DELETE, - SHARE_ALL, - None, CreationDisposition::OpenExisting, FileOptions::OPEN_REPARSE_POINT, - None, ) && set_posix_delete(file.as_handle()).is_ok() { return Ok(()); } + // Windows versions without POSIX delete refuse to remove the name of a + // mapped file. Opening with `FILE_FLAG_DELETE_ON_CLOSE` and immediately + // closing that handle arms deletion once every other handle is closed. + if let Ok(file) = open_file_wide( + path, + FileAccess::DELETE, + CreationDisposition::OpenExisting, + FileOptions::DELETE_ON_CLOSE, + ) { + drop(file); + return Ok(()); + } + // Preserve the original `DeleteFileW` error when POSIX deletion is not - // available. - Err(error) + // available and deferred deletion cannot be armed. + Err(error_to_io(error)) } fn set_posix_delete(file: BorrowedHandle<'_>) -> fspy_nostd::Result<()> { @@ -267,24 +279,7 @@ fn set_posix_delete(file: BorrowedHandle<'_>) -> fspy_nostd::Result<()> { impl Drop for ShmKeeper { fn drop(&mut self) { - // Windows versions without POSIX delete refuse to remove the name of a - // mapped file. Arm the deferred delete instead: a handle opened with - // `FILE_FLAG_DELETE_ON_CLOSE` deletes the file once every handle to it - // is closed. - let Ok(path) = copy_path(self.path.as_os_str()) else { - return; - }; - if remove_file(as_nostd_path(&path)).is_err() { - let _ = fspy_nostd::fs::create_file( - as_nostd_path(&path), - FileAccess::DELETE, - SHARE_ALL, - None, - CreationDisposition::OpenExisting, - FileOptions::DELETE_ON_CLOSE, - None, - ); - } + let _ = remove_file(self.path.as_os_str()); } } From 5b598f7aee9680d9da5810f89e4df32f8cc2cbe9 Mon Sep 17 00:00:00 2001 From: wan9chi Date: Thu, 13 Aug 2026 18:47:52 +0800 Subject: [PATCH 15/18] fix(fspy-shm): restore std Windows path and deletion semantics Address review findings on the no-std adoption: - Convert long backing paths to verbatim form in copy_path, matching the std conversion the raw CreateFileW/DeleteFileW calls no longer get. - Give every Mapping a duplicated file handle closed only after its view unmaps, so the DELETE_ON_CLOSE fallback removal fires once the last view is gone on volumes without POSIX delete. - Open reparse points rather than their targets when creating the backing file and in the delete-on-close fallback, as std does. - Add the checked WideCStr::from_units_with_nul constructor and use it instead of a safe wrapper around the unchecked one. - Export fspy_nostd::{bool_result, last_error} and drop the duplicate in fspy_shm. - Add BorrowedHandle::try_clone_to_owned and reconcile the handle sentinel docs with their constructor contracts. - Model PageProtection as an enum, drop create_file_mapping's unused name parameter, and omit the CreateAlways/OpenAlways dispositions whose already-existed signal create_file does not surface. Co-Authored-By: Claude Fable 5 --- crates/fspy_nostd/Cargo.toml | 1 + crates/fspy_nostd/src/c_str.rs | 28 ++++++ crates/fspy_nostd/src/fs/windows.rs | 13 ++- crates/fspy_nostd/src/lib.rs | 5 +- crates/fspy_nostd/src/mm/windows.rs | 39 +++++--- crates/fspy_nostd/src/windows.rs | 59 +++++++++-- crates/fspy_shm/src/windows.rs | 146 +++++++++++++++++++++++----- 7 files changed, 233 insertions(+), 58 deletions(-) diff --git a/crates/fspy_nostd/Cargo.toml b/crates/fspy_nostd/Cargo.toml index 0691c189b..ec2dc7ea7 100644 --- a/crates/fspy_nostd/Cargo.toml +++ b/crates/fspy_nostd/Cargo.toml @@ -39,6 +39,7 @@ windows-sys = { workspace = true, features = [ "Win32_Storage_FileSystem", "Win32_System_LibraryLoader", "Win32_System_Memory", + "Win32_System_Threading", ] } # Cross-validates the page-size probe against rustix's auxv-based answer. diff --git a/crates/fspy_nostd/src/c_str.rs b/crates/fspy_nostd/src/c_str.rs index 265250199..a8893f896 100644 --- a/crates/fspy_nostd/src/c_str.rs +++ b/crates/fspy_nostd/src/c_str.rs @@ -152,6 +152,22 @@ impl<'a, U: CStrUnit> CStr<'a, Thin, U> { } impl<'a, U: CStrUnit> CStr<'a, Fat, U> { + /// Creates a length-retaining C string from code units that end with the + /// single NUL terminator. + /// + /// Returns [`None`] when `units` is empty, does not end with a NUL code + /// unit, or contains an interior NUL code unit. + #[must_use] + pub fn from_units_with_nul(units: &'a [U]) -> Option { + let (last, rest) = units.split_last()?; + if *last != U::NUL || rest.contains(&U::NUL) { + return None; + } + // SAFETY: `units` ends with exactly one NUL code unit and contains no + // other NUL code units. + Some(unsafe { Self::from_units_with_nul_unchecked(units) }) + } + /// Creates a length-retaining C string from code units without validation. /// /// # Safety @@ -215,6 +231,18 @@ mod tests { assert_eq!(counted.as_units_with_nul(), fat.as_units_with_nul()); } + #[test] + fn checked_construction_accepts_only_a_single_trailing_nul() { + let checked = CStr::::from_units_with_nul(b"abc\0").unwrap(); + + assert_eq!(checked.as_units(), b"abc"); + assert_eq!(checked.len_with_nul(), 4); + assert!(CStr::::from_units_with_nul(b"").is_none()); + assert!(CStr::::from_units_with_nul(b"abc").is_none()); + assert!(CStr::::from_units_with_nul(b"a\0c\0").is_none()); + assert!(WideCStr::::from_units_with_nul(&[0u16]).is_some()); + } + #[test] fn thin_view_accepts_a_checked_non_null_pointer() { let ptr = NonNull::new(c"abc".as_ptr().cast_mut().cast()).unwrap(); diff --git a/crates/fspy_nostd/src/fs/windows.rs b/crates/fspy_nostd/src/fs/windows.rs index c8f163d0e..af34bd970 100644 --- a/crates/fspy_nostd/src/fs/windows.rs +++ b/crates/fspy_nostd/src/fs/windows.rs @@ -4,10 +4,9 @@ use bitflags::bitflags; use windows_sys::Win32::{ Foundation::{GENERIC_READ, GENERIC_WRITE, INVALID_HANDLE_VALUE}, Storage::FileSystem::{ - CREATE_ALWAYS, CREATE_NEW, CreateFileW, DELETE, DeleteFileW, FILE_ATTRIBUTE_TEMPORARY, + CREATE_NEW, CreateFileW, DELETE, DeleteFileW, FILE_ATTRIBUTE_TEMPORARY, FILE_FLAG_DELETE_ON_CLOSE, FILE_FLAG_OPEN_REPARSE_POINT, FILE_SHARE_DELETE, - FILE_SHARE_READ, FILE_SHARE_WRITE, GetFileSizeEx, OPEN_ALWAYS, OPEN_EXISTING, - TRUNCATE_EXISTING, + FILE_SHARE_READ, FILE_SHARE_WRITE, GetFileSizeEx, OPEN_EXISTING, TRUNCATE_EXISTING, }, }; @@ -49,17 +48,17 @@ bitflags! { } /// How `CreateFileW` handles an existing or missing file. +/// +/// `CREATE_ALWAYS` and `OPEN_ALWAYS` are omitted: their success reports +/// whether the file already existed only through `GetLastError`, which +/// [`create_file`] does not surface. #[derive(Clone, Copy, Debug, Eq, PartialEq)] #[repr(u32)] pub enum CreationDisposition { /// Create a new file and fail if it already exists. CreateNew = CREATE_NEW, - /// Create a new file or replace an existing file. - CreateAlways = CREATE_ALWAYS, /// Open an existing file and fail if it does not exist. OpenExisting = OPEN_EXISTING, - /// Open an existing file or create a new file. - OpenAlways = OPEN_ALWAYS, /// Open and truncate an existing file. TruncateExisting = TRUNCATE_EXISTING, } diff --git a/crates/fspy_nostd/src/lib.rs b/crates/fspy_nostd/src/lib.rs index b1e68f976..746d2a982 100644 --- a/crates/fspy_nostd/src/lib.rs +++ b/crates/fspy_nostd/src/lib.rs @@ -22,7 +22,10 @@ pub mod param; pub use c_str::{CStr, CStrUnit, Fat, Thin, Units, WideCStr}; #[cfg(windows)] -pub use windows::{BorrowedHandle, OwnedHandle, RawHandle, SecurityAttributes, get_module_handle}; +pub use windows::{ + BorrowedHandle, OwnedHandle, RawHandle, SecurityAttributes, bool_result, get_module_handle, + last_error, +}; #[cfg(windows)] #[derive(Clone, Copy, Debug, Eq, PartialEq)] diff --git a/crates/fspy_nostd/src/mm/windows.rs b/crates/fspy_nostd/src/mm/windows.rs index e1bc2b274..60df93f42 100644 --- a/crates/fspy_nostd/src/mm/windows.rs +++ b/crates/fspy_nostd/src/mm/windows.rs @@ -6,16 +6,20 @@ use windows_sys::Win32::System::Memory::{ PAGE_READWRITE, UnmapViewOfFile, }; -use crate::{BorrowedHandle, OwnedHandle, Result, SecurityAttributes, WideCStr}; +use crate::{BorrowedHandle, OwnedHandle, Result, SecurityAttributes}; -bitflags! { - /// Page protection for a file mapping. - #[derive(Clone, Copy, Debug, Eq, PartialEq)] - pub struct PageProtection: u32 { - /// Permit pages to be read and written. - const READ_WRITE = PAGE_READWRITE; - } +/// Page protection for a file mapping. +/// +/// Protection values are mutually exclusive, so they are modeled as an enum +/// rather than as flags. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(u32)] +pub enum PageProtection { + /// Permit pages to be read and written. + ReadWrite = PAGE_READWRITE, +} +bitflags! { /// Access requested for a mapped view. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct MappingAccess: u32 { @@ -57,33 +61,36 @@ impl Drop for MappingView { } } -/// Calls `CreateFileMappingW` and returns the new mapping-object handle. +/// Calls `CreateFileMappingW` and returns the new unnamed mapping object's +/// handle. +/// +/// Named mappings are unsupported: for a name that already exists, +/// `CreateFileMappingW` reports the pre-existing object only through +/// `GetLastError`, which this wrapper does not surface. /// /// # Errors /// /// Returns the error reported by `CreateFileMappingW`. -pub fn create_file_mapping( +pub fn create_file_mapping( file: BorrowedHandle<'_>, mapping_attributes: Option<&SecurityAttributes>, protection: PageProtection, maximum_size_high: u32, maximum_size_low: u32, - name: Option>, ) -> Result { let mapping_attributes = mapping_attributes.map_or(ptr::null(), SecurityAttributes::as_raw); - let name = name.map_or(ptr::null(), |name| name.as_ptr()); // SAFETY: `file` keeps the opaque handle open. The optional security - // attributes and name are either null or readable for the call. Windows - // validates the handle's object type, protection, and sizes. + // attributes are either null or readable for the call. Windows validates + // the handle's object type, protection, and sizes. let mapping = unsafe { CreateFileMappingW( file.as_raw_handle(), mapping_attributes, - protection.bits(), + protection as u32, maximum_size_high, maximum_size_low, - name, + ptr::null(), ) }; if mapping.is_null() { diff --git a/crates/fspy_nostd/src/windows.rs b/crates/fspy_nostd/src/windows.rs index ff43152e4..0640dc037 100644 --- a/crates/fspy_nostd/src/windows.rs +++ b/crates/fspy_nostd/src/windows.rs @@ -1,8 +1,9 @@ -use core::{ffi::c_void, marker::PhantomData, ptr::NonNull}; +use core::{ffi::c_void, marker::PhantomData, ptr, ptr::NonNull}; use windows_sys::Win32::{ - Foundation::GetLastError, Security::SECURITY_ATTRIBUTES, - System::LibraryLoader::GetModuleHandleW, + Foundation::{DUPLICATE_SAME_ACCESS, DuplicateHandle, GetLastError}, + Security::SECURITY_ATTRIBUTES, + System::{LibraryLoader::GetModuleHandleW, Threading::GetCurrentProcess}, }; use crate::{Result, WideCStr}; @@ -12,8 +13,9 @@ pub type RawHandle = *mut c_void; /// A borrowed Windows kernel handle. /// -/// Its lifetime is tied to the value that keeps the handle open. -/// `NULL` and `-1` are permitted because their validity is API-specific. +/// Its lifetime is tied to the value that keeps the handle open. The sentinel +/// values `NULL` and `-1` are also permitted because some Win32 calls define +/// them as meaningful arguments. #[derive(Clone, Copy)] #[repr(transparent)] pub struct BorrowedHandle<'handle> { @@ -23,7 +25,8 @@ pub struct BorrowedHandle<'handle> { /// An owned Windows kernel handle. /// -/// `NULL` and `-1` are permitted because their validity is API-specific. +/// The handle is closed on drop, so sentinel values such as `NULL` and `-1` +/// are never permitted here. #[repr(transparent)] pub struct OwnedHandle { handle: RawHandle, @@ -48,8 +51,9 @@ impl BorrowedHandle<'_> { /// /// # Safety /// - /// `handle` must be a valid open handle and remain open for the lifetime of - /// the returned value. + /// `handle` must be a valid open handle that remains open for the lifetime + /// of the returned value, or a sentinel value (`NULL` or `-1`) that every + /// call receiving the borrow defines as meaningful. #[must_use] pub const unsafe fn borrow_raw(handle: RawHandle) -> Self { Self { handle, lifetime: PhantomData } @@ -60,6 +64,32 @@ impl BorrowedHandle<'_> { pub const fn as_raw_handle(&self) -> RawHandle { self.handle } + + /// Duplicates this handle into a new non-inheritable owned handle with the + /// same access. + /// + /// # Errors + /// + /// Returns the error reported by `DuplicateHandle`. + pub fn try_clone_to_owned(&self) -> Result { + let mut duplicated = ptr::null_mut(); + // SAFETY: `self` keeps the source handle open for the call, the + // current-process pseudo handle is always valid, and `duplicated` is + // writable. Windows validates that the handle can be duplicated. + bool_result(unsafe { + DuplicateHandle( + GetCurrentProcess(), + self.handle, + GetCurrentProcess(), + &raw mut duplicated, + 0, + 0, + DUPLICATE_SAME_ACCESS, + ) + })?; + // SAFETY: `DuplicateHandle` returned a valid, newly owned handle. + Ok(unsafe { OwnedHandle::from_raw_handle(duplicated) }) + } } impl OwnedHandle { @@ -100,11 +130,24 @@ impl Drop for OwnedHandle { } } +/// Returns the calling thread's last Win32 error. +/// +/// Call this immediately after the failing Win32 call: anything in between, +/// including drops, can overwrite the thread-local error code. +#[must_use] pub fn last_error() -> crate::Error { // SAFETY: `GetLastError` reads thread-local error state. crate::Error::from_raw_os_error(unsafe { GetLastError() }) } +/// Converts a Win32 `BOOL` result into a [`Result`]. +/// +/// As with [`last_error`], call this immediately after the Win32 call whose +/// result it receives. +/// +/// # Errors +/// +/// Returns the calling thread's last Win32 error when `result` is zero. pub fn bool_result(result: i32) -> Result<()> { if result == 0 { Err(last_error()) } else { Ok(()) } } diff --git a/crates/fspy_shm/src/windows.rs b/crates/fspy_shm/src/windows.rs index a1b7164a3..fc08c9c1e 100644 --- a/crates/fspy_shm/src/windows.rs +++ b/crates/fspy_shm/src/windows.rs @@ -10,7 +10,7 @@ use std::{ use std::{fs::File, os::windows::io::AsRawHandle as _}; use fspy_nostd::{ - BorrowedHandle, + BorrowedHandle, bool_result, fs::{CreationDisposition, FileAccess, FileOptions, FileShare}, mm::{MappingAccess, PageProtection}, }; @@ -20,7 +20,7 @@ use windows_sys::Win32::Storage::FileSystem::{ FILE_STANDARD_INFO, FileStandardInfo, GetFileInformationByHandleEx, }; use windows_sys::Win32::{ - Foundation::{ERROR_ACCESS_DENIED, GetLastError}, + Foundation::ERROR_ACCESS_DENIED, Storage::FileSystem::{ FILE_DISPOSITION_FLAG_DELETE, FILE_DISPOSITION_FLAG_IGNORE_READONLY_ATTRIBUTE, FILE_DISPOSITION_FLAG_POSIX_SEMANTICS, FILE_DISPOSITION_INFO_EX, FILE_END_OF_FILE_INFO, @@ -56,8 +56,13 @@ pub struct ShmHandle { /// A `Mapping` keeps the bytes alive until it is dropped and cannot affect the /// shared memory's identifier. pub struct Mapping { + // Field order matters: the view must unmap before `_file` closes. view: fspy_nostd::mm::MappingView, len: NonZeroUsize, + /// Keeps a file handle open until the view above is unmapped, so that a + /// deferred `FILE_FLAG_DELETE_ON_CLOSE` removal armed by [`ShmKeeper`] + /// fires once the last view is gone. + _file: fspy_nostd::OwnedHandle, } /// Creates `size` bytes of zero-initialized shared memory. @@ -88,8 +93,10 @@ pub fn create(size: usize) -> io::Result<(ShmKeeper, ShmHandle)> { path.as_os_str(), FileAccess::GENERIC_READ | FileAccess::GENERIC_WRITE, CreationDisposition::CreateNew, - // Ask Windows to keep the data in memory when it can. - FileOptions::TEMPORARY, + // Ask Windows to keep the data in memory when it can. Opening the + // reparse point itself makes a link planted at the fresh path fail + // instead of redirecting the file, as std does for `create_new`. + FileOptions::TEMPORARY | FileOptions::OPEN_REPARSE_POINT, )?; // The keeper exists from here on, so every error path below cleans up. let keeper = ShmKeeper { path }; @@ -152,32 +159,62 @@ fn open_file_wide( fspy_nostd::fs::create_file(path, access, SHARE_ALL, None, disposition, options, None) } +/// The length at which std's own Windows path conversion switches to a +/// verbatim path. +const VERBATIM_THRESHOLD: usize = 248; + +const SEP: u16 = b'\\' as u16; +const COLON: u16 = b':' as u16; +/// `\\?\` +const VERBATIM_PREFIX: [u16; 4] = [SEP, SEP, b'?' as u16, SEP]; +/// `\??\` +const NT_PREFIX: [u16; 4] = [SEP, b'?' as u16, b'?' as u16, SEP]; +/// `\\.\` +const DEVICE_PREFIX: [u16; 4] = [SEP, SEP, b'.' as u16, SEP]; +/// `UNC\` +const UNC_INFIX: [u16; 4] = [b'U' as u16, b'N' as u16, b'C' as u16, SEP]; + fn copy_path(path: &OsStr) -> io::Result> { let mut units: Vec<_> = path.encode_wide().collect(); if units.contains(&0) { return Err(io::Error::new(io::ErrorKind::InvalidInput, "path contains NUL")); } + // std converts long paths to verbatim form before every `CreateFileW`; + // without that, paths at or beyond the legacy `MAX_PATH` limit fail + // regardless of the system's long-path opt-in, which the arbitrary + // processes opening shared memory could not rely on anyway. + if units.len() >= VERBATIM_THRESHOLD { + add_verbatim_prefix(&mut units); + } units.push(0); Ok(units) } -const fn as_nostd_path(path: &[u16]) -> fspy_nostd::WideCStr<'_, fspy_nostd::Fat> { - // SAFETY: `copy_path` rejects interior NUL and appends one terminator. - unsafe { fspy_nostd::WideCStr::from_units_with_nul_unchecked(path) } +fn add_verbatim_prefix(units: &mut Vec) { + if units.starts_with(&VERBATIM_PREFIX) + || units.starts_with(&NT_PREFIX) + || units.starts_with(&DEVICE_PREFIX) + { + return; + } + if units.starts_with(&[SEP, SEP]) { + // `\\server\share\...` becomes `\\?\UNC\server\share\...`. + drop(units.splice(0..2, VERBATIM_PREFIX.into_iter().chain(UNC_INFIX))); + } else if units.get(1) == Some(&COLON) && units.get(2) == Some(&SEP) { + // `C:\...` becomes `\\?\C:\...`. + drop(units.splice(0..0, VERBATIM_PREFIX)); + } + // Every other shape is left alone: the backing paths this crate produces + // are always fully qualified. } -fn error_to_io(error: fspy_nostd::Error) -> io::Error { - io::Error::from_raw_os_error(error.raw_os_error().cast_signed()) +fn as_nostd_path(path: &[u16]) -> fspy_nostd::WideCStr<'_, fspy_nostd::Fat> { + fspy_nostd::WideCStr::from_units_with_nul(path) + .expect("copy_path rejects interior NUL and appends one terminator") } -fn bool_result(result: i32) -> fspy_nostd::Result<()> { - if result == 0 { - // SAFETY: the failing Win32 call immediately precedes this read of the - // thread-local error code. - Err(fspy_nostd::Error::from_raw_os_error(unsafe { GetLastError() })) - } else { - Ok(()) - } +fn error_to_io(error: fspy_nostd::Error) -> io::Error { + io::Error::from_raw_os_error(error.raw_os_error().cast_signed()) } fn set_sparse(file: BorrowedHandle<'_>) -> fspy_nostd::Result<()> { @@ -239,12 +276,14 @@ fn remove_file(path: &OsStr) -> io::Result<()> { // Windows versions without POSIX delete refuse to remove the name of a // mapped file. Opening with `FILE_FLAG_DELETE_ON_CLOSE` and immediately - // closing that handle arms deletion once every other handle is closed. + // closing that handle arms deletion once every other handle is closed; + // every [`Mapping`] holds a file handle until its view unmaps, so the + // deletion fires once the last view is gone. if let Ok(file) = open_file_wide( path, FileAccess::DELETE, CreationDisposition::OpenExisting, - FileOptions::DELETE_ON_CLOSE, + FileOptions::DELETE_ON_CLOSE | FileOptions::OPEN_REPARSE_POINT, ) { drop(file); return Ok(()); @@ -302,16 +341,21 @@ impl ShmHandle { let _slice_len = isize::try_from(self.size.get()).map_err(|_| { io::Error::new(io::ErrorKind::InvalidData, "shared-memory size exceeds isize") })?; - // Default security attributes create a non-inheritable mapping object - // and no name creates an unnamed one. Both maximum-size halves are - // zero, so Windows uses the current file size. - let mapping = fspy_nostd::mm::create_file_mapping::( + // A deferred `FILE_FLAG_DELETE_ON_CLOSE` removal is processed when the + // last file handle closes and is silently discarded while mapped views + // remain. Give every mapping its own handle, closed only after its + // view unmaps, so [`ShmKeeper`]'s fallback removal fires once the last + // view is gone. + let file = self.file.as_handle().try_clone_to_owned().map_err(error_to_io)?; + // Default security attributes create a non-inheritable mapping object. + // Both maximum-size halves are zero, so Windows uses the current file + // size. + let mapping = fspy_nostd::mm::create_file_mapping( self.file.as_handle(), None, - PageProtection::READ_WRITE, + PageProtection::ReadWrite, 0, 0, - None, ) .map_err(error_to_io)?; let view = fspy_nostd::mm::map_view_of_file( @@ -323,7 +367,7 @@ impl ShmHandle { ) .map_err(error_to_io)?; // The view remains valid after its mapping-object handle closes. - Ok(Mapping { view, len: self.size }) + Ok(Mapping { view, len: self.size, _file: file }) } } @@ -382,3 +426,53 @@ pub fn file_sizes(file: &File) -> io::Result<(u64, u64)> { .map_err(|_| io::Error::other("file has a negative allocated size"))?; Ok((logical_size, allocated_size)) } + +#[cfg(test)] +mod tests { + use std::ffi::OsStr; + + use super::copy_path; + + fn units_with_nul(path: &str) -> Vec { + let mut units: Vec = path.encode_utf16().collect(); + units.push(0); + units + } + + #[test] + fn short_paths_stay_unprefixed() { + let path = r"C:\Temp\file.shm"; + + assert_eq!(copy_path(OsStr::new(path)).unwrap(), units_with_nul(path)); + } + + #[test] + fn long_drive_paths_gain_the_verbatim_prefix() { + let path = format!(r"C:\{}\file.shm", "a".repeat(300)); + + let converted = copy_path(OsStr::new(&path)).unwrap(); + + assert_eq!(converted, units_with_nul(&format!(r"\\?\{path}"))); + } + + #[test] + fn long_unc_paths_gain_the_unc_verbatim_prefix() { + let path = format!(r"\\server\share\{}\file.shm", "a".repeat(300)); + + let converted = copy_path(OsStr::new(&path)).unwrap(); + + assert_eq!(converted, units_with_nul(&format!(r"\\?\UNC\{}", &path[2..]))); + } + + #[test] + fn long_verbatim_paths_are_left_alone() { + let path = format!(r"\\?\C:\{}\file.shm", "a".repeat(300)); + + assert_eq!(copy_path(OsStr::new(&path)).unwrap(), units_with_nul(&path)); + } + + #[test] + fn interior_nul_is_rejected() { + assert!(copy_path(OsStr::new("a\0b")).is_err()); + } +} From dbf1fc57c992d781b9ab0ddeca848c2edc8b9cb5 Mon Sep 17 00:00:00 2001 From: wan9chi Date: Thu, 13 Aug 2026 18:59:36 +0800 Subject: [PATCH 16/18] refactor(fspy-shm): rely on POSIX delete for Windows removal Assume NTFS on Windows 10 1607+, where POSIX delete unlinks a name even while views of the file remain mapped: - remove_file now opens the backing file and sets FileDispositionInfoEx with POSIX semantics directly, instead of trying DeleteFileW first. - Drop the DELETE_ON_CLOSE fallback, and with it the duplicated file handle each Mapping held only to make that fallback fire after the last view unmapped. - Remove the now-unused fspy_nostd delete_file wrapper, FileOptions::DELETE_ON_CLOSE, BorrowedHandle::try_clone_to_owned, and the Win32_System_Threading feature. - Document the platform floor on ShmKeeper. Co-Authored-By: Claude Fable 5 --- crates/fspy_nostd/Cargo.toml | 1 - crates/fspy_nostd/src/fs/windows.rs | 19 ++------- crates/fspy_nostd/src/windows.rs | 33 ++-------------- crates/fspy_shm/src/windows.rs | 60 +++++++---------------------- 4 files changed, 20 insertions(+), 93 deletions(-) diff --git a/crates/fspy_nostd/Cargo.toml b/crates/fspy_nostd/Cargo.toml index ec2dc7ea7..0691c189b 100644 --- a/crates/fspy_nostd/Cargo.toml +++ b/crates/fspy_nostd/Cargo.toml @@ -39,7 +39,6 @@ windows-sys = { workspace = true, features = [ "Win32_Storage_FileSystem", "Win32_System_LibraryLoader", "Win32_System_Memory", - "Win32_System_Threading", ] } # Cross-validates the page-size probe against rustix's auxv-based answer. diff --git a/crates/fspy_nostd/src/fs/windows.rs b/crates/fspy_nostd/src/fs/windows.rs index af34bd970..6b29b1d2f 100644 --- a/crates/fspy_nostd/src/fs/windows.rs +++ b/crates/fspy_nostd/src/fs/windows.rs @@ -4,9 +4,9 @@ use bitflags::bitflags; use windows_sys::Win32::{ Foundation::{GENERIC_READ, GENERIC_WRITE, INVALID_HANDLE_VALUE}, Storage::FileSystem::{ - CREATE_NEW, CreateFileW, DELETE, DeleteFileW, FILE_ATTRIBUTE_TEMPORARY, - FILE_FLAG_DELETE_ON_CLOSE, FILE_FLAG_OPEN_REPARSE_POINT, FILE_SHARE_DELETE, - FILE_SHARE_READ, FILE_SHARE_WRITE, GetFileSizeEx, OPEN_EXISTING, TRUNCATE_EXISTING, + CREATE_NEW, CreateFileW, DELETE, FILE_ATTRIBUTE_TEMPORARY, FILE_FLAG_OPEN_REPARSE_POINT, + FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE, GetFileSizeEx, OPEN_EXISTING, + TRUNCATE_EXISTING, }, }; @@ -40,8 +40,6 @@ bitflags! { pub struct FileOptions: u32 { /// Hint that the file should be kept in memory when possible. const TEMPORARY = FILE_ATTRIBUTE_TEMPORARY; - /// Delete the file after its last handle closes. - const DELETE_ON_CLOSE = FILE_FLAG_DELETE_ON_CLOSE; /// Open a reparse point rather than its target. const OPEN_REPARSE_POINT = FILE_FLAG_OPEN_REPARSE_POINT; } @@ -103,17 +101,6 @@ pub fn create_file( } } -/// Calls `DeleteFileW`. -/// -/// # Errors -/// -/// Returns the error reported by `DeleteFileW`. -#[expect(clippy::needless_pass_by_value, reason = "CStr is a borrowed value type")] -pub fn delete_file(path: WideCStr<'_, R>) -> Result<()> { - // SAFETY: `path` is a valid NUL-terminated wide string. - crate::windows::bool_result(unsafe { DeleteFileW(path.as_ptr()) }) -} - /// Calls `GetFileSizeEx`. /// /// # Errors diff --git a/crates/fspy_nostd/src/windows.rs b/crates/fspy_nostd/src/windows.rs index 0640dc037..1a70e5d4a 100644 --- a/crates/fspy_nostd/src/windows.rs +++ b/crates/fspy_nostd/src/windows.rs @@ -1,9 +1,8 @@ -use core::{ffi::c_void, marker::PhantomData, ptr, ptr::NonNull}; +use core::{ffi::c_void, marker::PhantomData, ptr::NonNull}; use windows_sys::Win32::{ - Foundation::{DUPLICATE_SAME_ACCESS, DuplicateHandle, GetLastError}, - Security::SECURITY_ATTRIBUTES, - System::{LibraryLoader::GetModuleHandleW, Threading::GetCurrentProcess}, + Foundation::GetLastError, Security::SECURITY_ATTRIBUTES, + System::LibraryLoader::GetModuleHandleW, }; use crate::{Result, WideCStr}; @@ -64,32 +63,6 @@ impl BorrowedHandle<'_> { pub const fn as_raw_handle(&self) -> RawHandle { self.handle } - - /// Duplicates this handle into a new non-inheritable owned handle with the - /// same access. - /// - /// # Errors - /// - /// Returns the error reported by `DuplicateHandle`. - pub fn try_clone_to_owned(&self) -> Result { - let mut duplicated = ptr::null_mut(); - // SAFETY: `self` keeps the source handle open for the call, the - // current-process pseudo handle is always valid, and `duplicated` is - // writable. Windows validates that the handle can be duplicated. - bool_result(unsafe { - DuplicateHandle( - GetCurrentProcess(), - self.handle, - GetCurrentProcess(), - &raw mut duplicated, - 0, - 0, - DUPLICATE_SAME_ACCESS, - ) - })?; - // SAFETY: `DuplicateHandle` returned a valid, newly owned handle. - Ok(unsafe { OwnedHandle::from_raw_handle(duplicated) }) - } } impl OwnedHandle { diff --git a/crates/fspy_shm/src/windows.rs b/crates/fspy_shm/src/windows.rs index fc08c9c1e..fa1940605 100644 --- a/crates/fspy_shm/src/windows.rs +++ b/crates/fspy_shm/src/windows.rs @@ -20,7 +20,6 @@ use windows_sys::Win32::Storage::FileSystem::{ FILE_STANDARD_INFO, FileStandardInfo, GetFileInformationByHandleEx, }; use windows_sys::Win32::{ - Foundation::ERROR_ACCESS_DENIED, Storage::FileSystem::{ FILE_DISPOSITION_FLAG_DELETE, FILE_DISPOSITION_FLAG_IGNORE_READONLY_ATTRIBUTE, FILE_DISPOSITION_FLAG_POSIX_SEMANTICS, FILE_DISPOSITION_INFO_EX, FILE_END_OF_FILE_INFO, @@ -35,6 +34,10 @@ const SHARE_ALL: FileShare = FileShare::READ.union(FileShare::WRITE).union(FileS /// Keeps the shared memory's identifier alive and removes it on drop. /// +/// Removal relies on POSIX delete semantics, which requires NTFS on Windows +/// 10 1607 or newer: the name is unlinked immediately even while views of the +/// backing file remain mapped. +/// /// Removal is cleanup, not a stop signal: later opens fail, but existing /// [`ShmHandle`]s and [`Mapping`]s keep reading and writing. To stop them, /// store a flag in the shared bytes, as the fspy channel's close gate does. @@ -56,13 +59,8 @@ pub struct ShmHandle { /// A `Mapping` keeps the bytes alive until it is dropped and cannot affect the /// shared memory's identifier. pub struct Mapping { - // Field order matters: the view must unmap before `_file` closes. view: fspy_nostd::mm::MappingView, len: NonZeroUsize, - /// Keeps a file handle open until the view above is unmapped, so that a - /// deferred `FILE_FLAG_DELETE_ON_CLOSE` removal armed by [`ShmKeeper`] - /// fires once the last view is gone. - _file: fspy_nostd::OwnedHandle, } /// Creates `size` bytes of zero-initialized shared memory. @@ -257,41 +255,17 @@ fn set_end_of_file(file: BorrowedHandle<'_>, len: i64) -> fspy_nostd::Result<()> fn remove_file(path: &OsStr) -> io::Result<()> { let path = copy_path(path)?; - let path = as_nostd_path(&path); - let error = match fspy_nostd::fs::delete_file(path) { - Ok(()) => return Ok(()), - Err(error) => error, - }; - if error.raw_os_error() == ERROR_ACCESS_DENIED - && let Ok(file) = open_file_wide( - path, - FileAccess::DELETE, - CreationDisposition::OpenExisting, - FileOptions::OPEN_REPARSE_POINT, - ) - && set_posix_delete(file.as_handle()).is_ok() - { - return Ok(()); - } - - // Windows versions without POSIX delete refuse to remove the name of a - // mapped file. Opening with `FILE_FLAG_DELETE_ON_CLOSE` and immediately - // closing that handle arms deletion once every other handle is closed; - // every [`Mapping`] holds a file handle until its view unmaps, so the - // deletion fires once the last view is gone. - if let Ok(file) = open_file_wide( - path, + // Opening the reparse point itself removes a link rather than its target. + let file = open_file_wide( + as_nostd_path(&path), FileAccess::DELETE, CreationDisposition::OpenExisting, - FileOptions::DELETE_ON_CLOSE | FileOptions::OPEN_REPARSE_POINT, - ) { - drop(file); - return Ok(()); - } - - // Preserve the original `DeleteFileW` error when POSIX deletion is not - // available and deferred deletion cannot be armed. - Err(error_to_io(error)) + FileOptions::OPEN_REPARSE_POINT, + ) + .map_err(error_to_io)?; + // POSIX delete removes the name as soon as `file` closes below, while + // existing handles and mapped views keep working until they are dropped. + set_posix_delete(file.as_handle()).map_err(error_to_io) } fn set_posix_delete(file: BorrowedHandle<'_>) -> fspy_nostd::Result<()> { @@ -341,12 +315,6 @@ impl ShmHandle { let _slice_len = isize::try_from(self.size.get()).map_err(|_| { io::Error::new(io::ErrorKind::InvalidData, "shared-memory size exceeds isize") })?; - // A deferred `FILE_FLAG_DELETE_ON_CLOSE` removal is processed when the - // last file handle closes and is silently discarded while mapped views - // remain. Give every mapping its own handle, closed only after its - // view unmaps, so [`ShmKeeper`]'s fallback removal fires once the last - // view is gone. - let file = self.file.as_handle().try_clone_to_owned().map_err(error_to_io)?; // Default security attributes create a non-inheritable mapping object. // Both maximum-size halves are zero, so Windows uses the current file // size. @@ -367,7 +335,7 @@ impl ShmHandle { ) .map_err(error_to_io)?; // The view remains valid after its mapping-object handle closes. - Ok(Mapping { view, len: self.size, _file: file }) + Ok(Mapping { view, len: self.size }) } } From 078af4f53320a386fc43dfcb358f8d38f802ac4a Mon Sep 17 00:00:00 2001 From: wan9chi Date: Thu, 13 Aug 2026 22:57:59 +0800 Subject: [PATCH 17/18] refactor(fspy-shm): use omnipath for verbatim path conversion Replace the hand-rolled `\\?\` prefix logic with omnipath's to_verbatim, which also normalizes through GetFullPathNameW first and covers the device-path and drive-relative shapes the local version left alone. omnipath is dependency-free and maintained by the author of std's own Windows path handling. The copy_path unit tests are kept and now pin the integration's behavior. Co-Authored-By: Claude Fable 5 --- Cargo.lock | 7 ++++++ Cargo.toml | 1 + crates/fspy_shm/Cargo.toml | 1 + crates/fspy_shm/src/windows.rs | 40 +++++++--------------------------- 4 files changed, 17 insertions(+), 32 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b6a18250e..be7e52c36 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1459,6 +1459,7 @@ version = "0.0.0" dependencies = [ "ctor", "fspy_nostd", + "omnipath", "subprocess_test", "uuid", "windows-sys 0.61.2", @@ -2464,6 +2465,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "omnipath" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80adb31078122c880307e9cdfd4e3361e6545c319f9b9dcafcb03acd3b51a575" + [[package]] name = "once_cell" version = "1.21.3" diff --git a/Cargo.toml b/Cargo.toml index 6c96c1867..3b76cb483 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -102,6 +102,7 @@ fspy_ipc_str = { path = "crates/fspy_ipc_str" } nix = { version = "0.31.2", features = ["dir", "signal"] } ntapi = "0.4.1" nucleo-matcher = "0.3.1" +omnipath = "0.1.6" once_cell = "1.19" os_str_bytes = "7.1.1" ouroboros = "0.18.5" diff --git a/crates/fspy_shm/Cargo.toml b/crates/fspy_shm/Cargo.toml index a0aaa45c4..e53ea56b0 100644 --- a/crates/fspy_shm/Cargo.toml +++ b/crates/fspy_shm/Cargo.toml @@ -14,6 +14,7 @@ uuid = { workspace = true, features = ["v4"] } fspy_nostd = { workspace = true } [target.'cfg(target_os = "windows")'.dependencies] +omnipath = { workspace = true } windows-sys = { workspace = true, features = [ "Win32_Foundation", "Win32_Storage_FileSystem", diff --git a/crates/fspy_shm/src/windows.rs b/crates/fspy_shm/src/windows.rs index fa1940605..ee53ff80a 100644 --- a/crates/fspy_shm/src/windows.rs +++ b/crates/fspy_shm/src/windows.rs @@ -3,8 +3,12 @@ use core::{ffi::c_void, mem::size_of, ptr}; use std::{ - env::temp_dir, ffi::OsStr, io, num::NonZeroUsize, os::windows::ffi::OsStrExt as _, - path::PathBuf, + env::temp_dir, + ffi::OsStr, + io, + num::NonZeroUsize, + os::windows::ffi::OsStrExt as _, + path::{Path, PathBuf}, }; #[cfg(test)] use std::{fs::File, os::windows::io::AsRawHandle as _}; @@ -14,6 +18,7 @@ use fspy_nostd::{ fs::{CreationDisposition, FileAccess, FileOptions, FileShare}, mm::{MappingAccess, PageProtection}, }; +use omnipath::windows::WinPathExt as _; use uuid::Uuid; #[cfg(test)] use windows_sys::Win32::Storage::FileSystem::{ @@ -161,17 +166,6 @@ fn open_file_wide( /// verbatim path. const VERBATIM_THRESHOLD: usize = 248; -const SEP: u16 = b'\\' as u16; -const COLON: u16 = b':' as u16; -/// `\\?\` -const VERBATIM_PREFIX: [u16; 4] = [SEP, SEP, b'?' as u16, SEP]; -/// `\??\` -const NT_PREFIX: [u16; 4] = [SEP, b'?' as u16, b'?' as u16, SEP]; -/// `\\.\` -const DEVICE_PREFIX: [u16; 4] = [SEP, SEP, b'.' as u16, SEP]; -/// `UNC\` -const UNC_INFIX: [u16; 4] = [b'U' as u16, b'N' as u16, b'C' as u16, SEP]; - fn copy_path(path: &OsStr) -> io::Result> { let mut units: Vec<_> = path.encode_wide().collect(); if units.contains(&0) { @@ -182,30 +176,12 @@ fn copy_path(path: &OsStr) -> io::Result> { // regardless of the system's long-path opt-in, which the arbitrary // processes opening shared memory could not rely on anyway. if units.len() >= VERBATIM_THRESHOLD { - add_verbatim_prefix(&mut units); + units = Path::new(path).to_verbatim()?.as_os_str().encode_wide().collect(); } units.push(0); Ok(units) } -fn add_verbatim_prefix(units: &mut Vec) { - if units.starts_with(&VERBATIM_PREFIX) - || units.starts_with(&NT_PREFIX) - || units.starts_with(&DEVICE_PREFIX) - { - return; - } - if units.starts_with(&[SEP, SEP]) { - // `\\server\share\...` becomes `\\?\UNC\server\share\...`. - drop(units.splice(0..2, VERBATIM_PREFIX.into_iter().chain(UNC_INFIX))); - } else if units.get(1) == Some(&COLON) && units.get(2) == Some(&SEP) { - // `C:\...` becomes `\\?\C:\...`. - drop(units.splice(0..0, VERBATIM_PREFIX)); - } - // Every other shape is left alone: the backing paths this crate produces - // are always fully qualified. -} - fn as_nostd_path(path: &[u16]) -> fspy_nostd::WideCStr<'_, fspy_nostd::Fat> { fspy_nostd::WideCStr::from_units_with_nul(path) .expect("copy_path rejects interior NUL and appends one terminator") From ae5bca7ed25528448fcc91a8674540ae78e8d295 Mon Sep 17 00:00:00 2001 From: wan9chi Date: Thu, 13 Aug 2026 22:59:46 +0800 Subject: [PATCH 18/18] test(fspy-shm): drop copy_path unit tests Co-Authored-By: Claude Fable 5 --- crates/fspy_shm/src/windows.rs | 50 ---------------------------------- 1 file changed, 50 deletions(-) diff --git a/crates/fspy_shm/src/windows.rs b/crates/fspy_shm/src/windows.rs index ee53ff80a..8a7582a8d 100644 --- a/crates/fspy_shm/src/windows.rs +++ b/crates/fspy_shm/src/windows.rs @@ -370,53 +370,3 @@ pub fn file_sizes(file: &File) -> io::Result<(u64, u64)> { .map_err(|_| io::Error::other("file has a negative allocated size"))?; Ok((logical_size, allocated_size)) } - -#[cfg(test)] -mod tests { - use std::ffi::OsStr; - - use super::copy_path; - - fn units_with_nul(path: &str) -> Vec { - let mut units: Vec = path.encode_utf16().collect(); - units.push(0); - units - } - - #[test] - fn short_paths_stay_unprefixed() { - let path = r"C:\Temp\file.shm"; - - assert_eq!(copy_path(OsStr::new(path)).unwrap(), units_with_nul(path)); - } - - #[test] - fn long_drive_paths_gain_the_verbatim_prefix() { - let path = format!(r"C:\{}\file.shm", "a".repeat(300)); - - let converted = copy_path(OsStr::new(&path)).unwrap(); - - assert_eq!(converted, units_with_nul(&format!(r"\\?\{path}"))); - } - - #[test] - fn long_unc_paths_gain_the_unc_verbatim_prefix() { - let path = format!(r"\\server\share\{}\file.shm", "a".repeat(300)); - - let converted = copy_path(OsStr::new(&path)).unwrap(); - - assert_eq!(converted, units_with_nul(&format!(r"\\?\UNC\{}", &path[2..]))); - } - - #[test] - fn long_verbatim_paths_are_left_alone() { - let path = format!(r"\\?\C:\{}\file.shm", "a".repeat(300)); - - assert_eq!(copy_path(OsStr::new(&path)).unwrap(), units_with_nul(&path)); - } - - #[test] - fn interior_nul_is_rejected() { - assert!(copy_path(OsStr::new("a\0b")).is_err()); - } -}