diff --git a/Cargo.lock b/Cargo.lock index e3071154d..bceabfee1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1416,6 +1416,7 @@ dependencies = [ name = "fspy_shared" version = "0.0.0" dependencies = [ + "allocator-api2", "assert2", "bitflags 2.10.0", "bstr", @@ -1423,6 +1424,8 @@ dependencies = [ "bytemuck", "ctor", "fspy_ipc_str", + "fspy_nostd", + "fspy_nostd_alloc", "fspy_shm", "omnipath", "rustc-hash", @@ -1461,7 +1464,7 @@ dependencies = [ "ctor", "fspy_nostd", "subprocess_test", - "uuid", + "tempfile", "windows-sys 0.61.2", ] diff --git a/crates/fspy_client_unix/src/convert.rs b/crates/fspy_client_unix/src/convert.rs index 89045e814..f286b7e0c 100644 --- a/crates/fspy_client_unix/src/convert.rs +++ b/crates/fspy_client_unix/src/convert.rs @@ -11,7 +11,7 @@ fn get_fd_path(allocator: A, fd: BorrowedFd<'_>) -> nix::Result(allocator: A, fd: BorrowedFd<'_>) -> nix::Result(allocator: A, fd: BorrowedFd<'_>) -> nix::Result { // `F_GETPATH` does not return a length. Count at this caller before // converting its allocation into the returned path. - Ok(Some(path.count().into_bytes())) + Ok(Some(path.count().into_units())) } Err(fspy_nostd::Error::BADF | fspy_nostd::Error::NOENT) => Ok(None), Err(errno) => Err(nix::errno::Errno::from_raw(errno.raw_os_error())), diff --git a/crates/fspy_nostd/src/c_str.rs b/crates/fspy_nostd/src/c_str.rs index a8893f896..9fda1971d 100644 --- a/crates/fspy_nostd/src/c_str.rs +++ b/crates/fspy_nostd/src/c_str.rs @@ -46,6 +46,15 @@ pub struct CStr<'a, R, U: CStrUnit = u8> { /// A borrowed NUL-terminated string of `u16` code units. pub type WideCStr<'a, R> = CStr<'a, R, u16>; +/// A borrowed NUL-terminated string of the platform's native path code +/// units: bytes on Unix and wide (`u16`) code units on Windows. +#[cfg(unix)] +pub type OsCStr<'a, R> = CStr<'a, R>; +/// A borrowed NUL-terminated string of the platform's native path code +/// units: bytes on Unix and wide (`u16`) code units on Windows. +#[cfg(windows)] +pub type OsCStr<'a, R> = WideCStr<'a, R>; + /// An iterator over the non-NUL code units of a thin C string. #[derive(Clone)] pub struct Units<'a, U: CStrUnit> { @@ -187,6 +196,13 @@ impl<'a, U: CStrUnit> CStr<'a, Fat, U> { } } + /// Discards the retained length and returns a thin view of the same + /// string. + #[must_use] + pub const fn as_thin(self) -> CStr<'a, Thin, U> { + CStr { ptr: self.ptr, repr: Thin { _private: () }, lifetime: PhantomData } + } + /// Returns the string's code units without the terminating NUL. #[must_use] pub const fn as_units(&self) -> &'a [U] { diff --git a/crates/fspy_nostd/src/lib.rs b/crates/fspy_nostd/src/lib.rs index 746d2a982..1541e11c2 100644 --- a/crates/fspy_nostd/src/lib.rs +++ b/crates/fspy_nostd/src/lib.rs @@ -20,7 +20,7 @@ pub mod mm; #[cfg(unix)] pub mod param; -pub use c_str::{CStr, CStrUnit, Fat, Thin, Units, WideCStr}; +pub use c_str::{CStr, CStrUnit, Fat, OsCStr, Thin, Units, WideCStr}; #[cfg(windows)] pub use windows::{ BorrowedHandle, OwnedHandle, RawHandle, SecurityAttributes, bool_result, get_module_handle, diff --git a/crates/fspy_nostd_alloc/Cargo.toml b/crates/fspy_nostd_alloc/Cargo.toml index 1fc070e15..3c5e55cd8 100644 --- a/crates/fspy_nostd_alloc/Cargo.toml +++ b/crates/fspy_nostd_alloc/Cargo.toml @@ -7,10 +7,12 @@ publish = false [lib] doctest = false -[target.'cfg(unix)'.dependencies] +[dependencies] allocator-api2 = { workspace = true, features = ["alloc"] } -bump-scope = { workspace = true } fspy_nostd = { workspace = true } +[target.'cfg(unix)'.dependencies] +bump-scope = { workspace = true } + [lints] workspace = true diff --git a/crates/fspy_nostd_alloc/src/c_string.rs b/crates/fspy_nostd_alloc/src/c_string.rs index 29b00bf67..b2e38e6d1 100644 --- a/crates/fspy_nostd_alloc/src/c_string.rs +++ b/crates/fspy_nostd_alloc/src/c_string.rs @@ -1,114 +1,137 @@ use core::{mem::MaybeUninit, ptr, slice}; use allocator_api2::{alloc::Allocator, boxed::Box, vec::Vec}; -use fspy_nostd::{CStr, Fat, Thin}; +use fspy_nostd::{CStr, CStrUnit, Fat, Thin}; /// An allocator-backed owned C string. -pub struct CString { - bytes: Box<[MaybeUninit], A>, +pub struct CString { + units: Box<[MaybeUninit], A>, repr: R, } -impl CString { - /// Converts a byte vector to a C string without checking its contents. +/// An allocator-backed owned C string of the platform's native path code +/// units: bytes on Unix and wide (`u16`) code units on Windows. +#[cfg(unix)] +pub type OsCString = CString; +/// An allocator-backed owned C string of the platform's native path code +/// units: bytes on Unix and wide (`u16`) code units on Windows. +#[cfg(windows)] +pub type OsCString = CString; + +impl CString { + /// Converts a code-unit vector that ends with the single NUL terminator + /// to a C string. + /// + /// 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_vec_with_nul(units: Vec) -> Option { + CStr::::from_units_with_nul(&units)?; + // SAFETY: validated just above. + Some(unsafe { Self::from_vec_with_nul_unchecked(units) }) + } + + /// Converts a code-unit vector to a C string without checking its + /// contents. /// /// # Safety /// - /// `bytes` must end with exactly one NUL byte and contain no other NUL - /// bytes. + /// `units` must end with exactly one NUL code unit and contain no other + /// NUL code units. #[must_use] - pub unsafe fn from_vec_with_nul_unchecked(bytes: Vec) -> Self { + pub unsafe fn from_vec_with_nul_unchecked(units: Vec) -> Self { // SAFETY: upheld by the caller. - let repr = unsafe { CStr::::from_units_with_nul_unchecked(&bytes) }.into_repr(); - let (bytes, _len, capacity, allocator) = bytes.into_raw_parts_with_alloc(); - let bytes = ptr::slice_from_raw_parts_mut(bytes.cast::>(), capacity); + let repr = unsafe { CStr::::from_units_with_nul_unchecked(&units) }.into_repr(); + let (units, _len, capacity, allocator) = units.into_raw_parts_with_alloc(); + let units = ptr::slice_from_raw_parts_mut(units.cast::>(), capacity); Self { // SAFETY: this uses the vector's original pointer, capacity, and // allocator. Its initialized prefix is described by `repr`; any - // spare capacity is valid as `MaybeUninit`. - bytes: unsafe { Box::from_raw_in(bytes, allocator) }, + // spare capacity is valid as `MaybeUninit`. + units: unsafe { Box::from_raw_in(units, allocator) }, repr, } } } -impl CString { +impl CString { /// Converts boxed storage to a thin C string without checking its /// contents. /// /// # Safety /// - /// `bytes` must begin with a valid NUL-terminated C string. Bytes after - /// that string may be uninitialized. + /// `units` must begin with a valid NUL-terminated C string. Code units + /// after that string may be uninitialized. #[must_use] - pub unsafe fn from_boxed_with_nul_unchecked(bytes: Box<[MaybeUninit], A>) -> Self { + pub unsafe fn from_boxed_with_nul_unchecked(units: Box<[MaybeUninit], A>) -> Self { // SAFETY: upheld by the caller. Creating a thin view does not scan the // allocation or retain the string length. - let repr = unsafe { CStr::::from_ptr(bytes.as_ptr().cast()) }.into_repr(); - Self { bytes, repr } + let repr = unsafe { CStr::::from_ptr(units.as_ptr().cast()) }.into_repr(); + Self { units, repr } } /// Returns a thin borrowed view of this C string. #[must_use] - pub fn as_c_str(&self) -> CStr<'_, Thin> { + pub fn as_c_str(&self) -> CStr<'_, Thin, U> { // SAFETY: upheld by the constructor; `self` owns the storage. - unsafe { CStr::from_ptr(self.bytes.as_ptr().cast()) } + unsafe { CStr::from_ptr(self.units.as_ptr().cast()) } } /// Counts through the terminating NUL and returns a length-retaining C /// string using the same allocation. #[must_use] - pub fn count(self) -> CString { + pub fn count(self) -> CString { let repr = self.as_c_str().count().into_repr(); - CString { bytes: self.bytes, repr } + CString { units: self.units, repr } } } -impl CString { +impl CString { /// Extracts a borrowed C string view containing the entire string. #[must_use] - pub fn as_c_str(&self) -> CStr<'_, Fat> { + pub fn as_c_str(&self) -> CStr<'_, Fat, U> { // SAFETY: the initialized prefix described by `repr` is a C string. - unsafe { CStr::from_units_with_nul_unchecked(self.as_bytes_with_nul()) } + unsafe { CStr::from_units_with_nul_unchecked(self.as_units_with_nul()) } } /// Returns the contents of this C string without the terminating NUL. #[must_use] - pub fn as_bytes(&self) -> &[u8] { - let bytes = self.as_bytes_with_nul(); - bytes.split_at(bytes.len() - 1).0 + pub fn as_units(&self) -> &[U] { + let units = self.as_units_with_nul(); + units.split_at(units.len() - 1).0 } /// Returns the contents of this C string, including the terminating NUL. #[must_use] - pub fn as_bytes_with_nul(&self) -> &[u8] { + pub fn as_units_with_nul(&self) -> &[U] { // SAFETY: `repr` describes the initialized C string prefix. - unsafe { slice::from_raw_parts(self.bytes.as_ptr().cast(), self.repr.len_with_nul()) } + unsafe { slice::from_raw_parts(self.units.as_ptr().cast(), self.repr.len_with_nul()) } } - /// Consumes this C string and returns its bytes without the terminating NUL. + /// Consumes this C string and returns its code units without the + /// terminating NUL. #[must_use = "`self` will be dropped if the result is not used"] - pub fn into_bytes(self) -> Vec { + pub fn into_units(self) -> Vec { let len = self.repr.len_with_nul() - 1; self.into_vec(len) } - /// Consumes this C string and returns its bytes, including the terminating - /// NUL. + /// Consumes this C string and returns its code units, including the + /// terminating NUL. #[must_use = "`self` will be dropped if the result is not used"] - pub fn into_bytes_with_nul(self) -> Vec { + pub fn into_units_with_nul(self) -> Vec { let len = self.repr.len_with_nul(); self.into_vec(len) } - fn into_vec(self, len: usize) -> Vec { - let Self { bytes, .. } = self; - let capacity = bytes.len(); - let (bytes, allocator) = Box::into_raw_with_allocator(bytes); + fn into_vec(self, len: usize) -> Vec { + let Self { units, .. } = self; + let capacity = units.len(); + let (units, allocator) = Box::into_raw_with_allocator(units); - // SAFETY: the box was allocated for `capacity` bytes with `allocator`. - // The first `len` bytes are initialized. - unsafe { Vec::from_raw_parts_in(bytes.cast::(), len, capacity, allocator) } + // SAFETY: the box was allocated for `capacity` units with `allocator`. + // The first `len` units are initialized. + unsafe { Vec::from_raw_parts_in(units.cast::(), len, capacity, allocator) } } } diff --git a/crates/fspy_nostd_alloc/src/fs.rs b/crates/fspy_nostd_alloc/src/fs.rs index 54d0cbef7..3295a1364 100644 --- a/crates/fspy_nostd_alloc/src/fs.rs +++ b/crates/fspy_nostd_alloc/src/fs.rs @@ -115,12 +115,12 @@ mod tests { let expected = expected.as_units_with_nul(); assert_eq!(path.as_c_str().as_units_with_nul(), expected); - assert_eq!(path.as_bytes(), &expected[..expected.len() - 1]); - assert_eq!(path.as_bytes_with_nul(), expected); - assert_eq!(path.into_bytes().as_slice(), &expected[..expected.len() - 1]); + assert_eq!(path.as_units(), &expected[..expected.len() - 1]); + assert_eq!(path.as_units_with_nul(), expected); + assert_eq!(path.into_units().as_slice(), &expected[..expected.len() - 1]); let path = super::getcwd(Global).unwrap(); - assert_eq!(path.into_bytes_with_nul().as_slice(), expected); + assert_eq!(path.into_units_with_nul().as_slice(), expected); } #[cfg(target_os = "macos")] diff --git a/crates/fspy_nostd_alloc/src/lib.rs b/crates/fspy_nostd_alloc/src/lib.rs index a322fb3b3..cbf04000e 100644 --- a/crates/fspy_nostd_alloc/src/lib.rs +++ b/crates/fspy_nostd_alloc/src/lib.rs @@ -11,28 +11,35 @@ //! intercepted call, drawing its chunks from the process-wide pool and //! returning them on drop. -#![cfg(unix)] #![cfg_attr(not(test), no_std)] mod c_string; +#[cfg(unix)] pub mod fs; +#[cfg(unix)] mod mmap; +#[cfg(unix)] mod pool; +#[cfg(unix)] use allocator_api2::alloc::Allocator; +#[cfg(unix)] use bump_scope::{ Bump, alloc::compat::AllocatorApi2V02Compat, settings::{BumpAllocatorSettings, BumpSettings}, }; -pub use c_string::CString; +pub use c_string::{CString, OsCString}; +#[cfg(unix)] use mmap::MmapAllocator; +#[cfg(unix)] use pool::ChunkPool; /// Every cached chunk is 64 KiB: a whole multiple of the page size on all /// supported targets, and big enough that most intercepted calls fit their /// allocations into a single chunk. [`ArenaSettings`] pins the arenas' own /// chunk sizing to this same value. +#[cfg(unix)] const CHUNK_SIZE: usize = 64 * 1024; /// The alignment chunks are allocated with. Must be at least the alignment /// `bump_scope::Bump` uses for its chunk requests — 16 (see @@ -40,13 +47,16 @@ const CHUNK_SIZE: usize = 64 * 1024; /// constant, so the `bump_chunk_requests_fit_the_pool_gates` test pins the /// fit instead: it fails if a bump-scope upgrade ever requests chunks the /// pool would refuse. +#[cfg(unix)] const CHUNK_ALIGN: usize = 16; /// At most this many chunks stay cached, capping retained memory at /// `SLOTS * CHUNK_SIZE` = 4 MiB. +#[cfg(unix)] const SLOTS: usize = 64; /// The process-wide chunk pool. `const`-initialized, so it works from the /// first allocation on — even before any constructor has run. +#[cfg(unix)] static CHUNK_POOL: ChunkPool = ChunkPool::new(); /// The `Bump` settings the arenas use — the defaults, with two changes: @@ -63,6 +73,7 @@ static CHUNK_POOL: ChunkPool = Ch /// minimum — which is exactly what keeps a minimum-sized request within /// the pool's `size <= CHUNK_SIZE` gate; the /// `bump_chunk_requests_fit_the_pool_gates` test pins that fit. +#[cfg(unix)] type ArenaSettings = <::WithGuaranteedAllocated as BumpAllocatorSettings>::WithMinimumChunkSize; /// `Bump::unallocated` requires its base allocator to implement `Default` @@ -70,6 +81,7 @@ type ArenaSettings = <::WithGuaranteedAll /// conjures one on first use). Point defaulted references at the /// process-wide pool. As an allocator, `&ChunkPool` already works through /// allocator-api2's blanket `impl Allocator for &A`. +#[cfg(unix)] impl Default for &'static ChunkPool { fn default() -> Self { &CHUNK_POOL @@ -89,6 +101,7 @@ impl Default for &'static ChunkPool impl Allocator { Bump::< @@ -97,7 +110,7 @@ pub fn arena() -> impl Allocator { >::unallocated() } -#[cfg(test)] +#[cfg(all(test, unix))] mod tests { use core::alloc::Layout; diff --git a/crates/fspy_shared/Cargo.toml b/crates/fspy_shared/Cargo.toml index f5ed6c105..071210f63 100644 --- a/crates/fspy_shared/Cargo.toml +++ b/crates/fspy_shared/Cargo.toml @@ -7,10 +7,13 @@ publish = false [dependencies] wincode = { workspace = true, features = ["derive"] } +allocator-api2 = { workspace = true, features = ["alloc"] } bitflags = { workspace = true } bumpalo = { workspace = true } bstr = { workspace = true, features = ["alloc", "std"] } bytemuck = { workspace = true, features = ["must_cast", "derive"] } +fspy_nostd = { workspace = true } +fspy_nostd_alloc = { workspace = true } fspy_shm = { workspace = true } fspy_ipc_str = { workspace = true } thiserror = { workspace = true } diff --git a/crates/fspy_shared/src/ipc/channel/mod.rs b/crates/fspy_shared/src/ipc/channel/mod.rs index 396fbc620..9cad595b2 100644 --- a/crates/fspy_shared/src/ipc/channel/mod.rs +++ b/crates/fspy_shared/src/ipc/channel/mod.rs @@ -2,8 +2,11 @@ mod shm_io; -use std::{env::temp_dir, fs::File, io, ops::Deref, path::PathBuf}; +use std::{env::temp_dir, ffi::OsStr, fs::File, io, ops::Deref, path::PathBuf}; +use allocator_api2::alloc::Global; +use fspy_nostd::Fat; +use fspy_nostd_alloc::OsCString; use fspy_shm::Mapping; pub use shm_io::FrameMut; use shm_io::{ShmReader, ShmWriter}; @@ -35,20 +38,60 @@ pub fn channel(capacity: usize) -> io::Result<(ChannelConf, Receiver)> { let lock_file_path = temp_dir().join(format!("fspy_ipc_{}.lock", Uuid::new_v4())); let shm_path = shm_backing_path()?; - let handle = fspy_shm::create(shm_path.as_os_str(), capacity)?; + let shm_c_path = os_c_string(shm_path.as_os_str())?; + let handle = + fspy_shm::create(shm_c_path.as_c_str().as_thin(), capacity).map_err(shm_error_to_io)?; // The keeper exists from here on, so every error path below cleans up. - let keeper = ShmKeeper { path: shm_path }; - let mapping = handle.map()?; + let keeper = ShmKeeper { path: shm_c_path }; + let mapping = handle.map().map_err(shm_error_to_io)?; let conf = ChannelConf { lock_file_path: lock_file_path.as_os_str().into(), - shm_id: keeper.path.as_os_str().into(), + shm_id: shm_path.as_os_str().into(), }; let receiver = Receiver::new(lock_file_path, keeper, mapping)?; Ok((conf, receiver)) } +/// Encodes `path` as an owned NUL-terminated platform C string. +fn os_c_string(path: &OsStr) -> io::Result> { + let mut units = os_units(path); + units.push(0); + OsCString::from_vec_with_nul(units) + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "path contains NUL")) +} + +#[cfg(unix)] +fn os_units(path: &OsStr) -> allocator_api2::vec::Vec { + use std::os::unix::ffi::OsStrExt as _; + + let mut units = allocator_api2::vec::Vec::with_capacity(path.len() + 1); + units.extend_from_slice(path.as_bytes()); + units +} + +#[cfg(windows)] +fn os_units(path: &OsStr) -> allocator_api2::vec::Vec { + use std::os::windows::ffi::OsStrExt as _; + + let mut units = allocator_api2::vec::Vec::with_capacity(path.len() + 1); + for unit in path.encode_wide() { + units.push(unit); + } + units +} + +#[cfg(unix)] +fn shm_error_to_io(error: fspy_nostd::Error) -> io::Error { + io::Error::from_raw_os_error(error.raw_os_error()) +} + +#[cfg(windows)] +fn shm_error_to_io(error: fspy_nostd::Error) -> io::Error { + io::Error::from_raw_os_error(error.raw_os_error().cast_signed()) +} + /// Returns a fresh absolute path for a shared-memory backing file. fn shm_backing_path() -> io::Result { // `temp_dir` reflects `TMPDIR` verbatim, which may be relative. The path @@ -87,12 +130,12 @@ fn to_verbatim_if_long(path: PathBuf) -> io::Result { /// Removal is cleanup, not a stop signal: later opens fail, but existing /// handles and mappings keep reading and writing; see [`fspy_shm::remove`]. struct ShmKeeper { - path: PathBuf, + path: OsCString, } impl Drop for ShmKeeper { fn drop(&mut self) { - let _ = fspy_shm::remove(self.path.as_os_str()); + let _ = fspy_shm::remove(self.path.as_c_str().as_thin()); } } @@ -108,7 +151,11 @@ impl ChannelConf { let lock_file = File::open(self.lock_file_path.to_cow_os_str())?; lock_file.try_lock_shared()?; - let mapping = fspy_shm::open(&self.shm_id.to_cow_os_str())?.map()?; + let shm_path = os_c_string(&self.shm_id.to_cow_os_str())?; + let mapping = fspy_shm::open(shm_path.as_c_str().as_thin()) + .map_err(shm_error_to_io)? + .map() + .map_err(shm_error_to_io)?; // SAFETY: `mapping` is a freshly mapped shared memory region with valid // pointer and size. Exclusive write access is ensured by the shared // file lock held by this sender. diff --git a/crates/fspy_shared/src/ipc/channel/shm_io.rs b/crates/fspy_shared/src/ipc/channel/shm_io.rs index 59ebe83e8..916df30d7 100644 --- a/crates/fspy_shared/src/ipc/channel/shm_io.rs +++ b/crates/fspy_shared/src/ipc/channel/shm_io.rs @@ -673,9 +673,10 @@ mod tests { const SHM_SIZE: usize = 1024 * 1024; let shm_path = crate::ipc::channel::shm_backing_path().unwrap(); - let handle = fspy_shm::create(shm_path.as_os_str(), SHM_SIZE).unwrap(); - let _keeper = crate::ipc::channel::ShmKeeper { path: shm_path.clone() }; let shm_name = shm_path.to_str().expect("test temp dir is UTF-8").to_owned(); + let c_path = crate::ipc::channel::os_c_string(shm_path.as_os_str()).unwrap(); + let handle = fspy_shm::create(c_path.as_c_str().as_thin(), SHM_SIZE).unwrap(); + let _keeper = crate::ipc::channel::ShmKeeper { path: c_path }; // Map before the children run. Windows keeps views coherent while they // exist at the same time; a view created after every writer exited can // observe the file before the writers' dirty pages reach it. @@ -686,8 +687,11 @@ mod tests { let cmd = command_for_fn!( (shm_name.clone(), child_index), |(shm_name, child_index): (String, usize)| { + let c_path = + crate::ipc::channel::os_c_string(std::ffi::OsStr::new(&shm_name)) + .unwrap(); let mapping = - fspy_shm::open(std::ffi::OsStr::new(&shm_name)).unwrap().map().unwrap(); + fspy_shm::open(c_path.as_c_str().as_thin()).unwrap().map().unwrap(); // SAFETY: `mapping` is a freshly mapped shared memory region with a // valid pointer and size. Concurrent write access is safe because // `ShmWriter` uses atomic operations. diff --git a/crates/fspy_shm/Cargo.toml b/crates/fspy_shm/Cargo.toml index ad38ff3d5..9b6da1115 100644 --- a/crates/fspy_shm/Cargo.toml +++ b/crates/fspy_shm/Cargo.toml @@ -21,7 +21,7 @@ windows-sys = { workspace = true, features = [ [dev-dependencies] ctor = { workspace = true } subprocess_test = { workspace = true } -uuid = { workspace = true, features = ["v4"] } +tempfile = { workspace = true } [lints] workspace = true diff --git a/crates/fspy_shm/README.md b/crates/fspy_shm/README.md index c3113abe1..8d845c060 100644 --- a/crates/fspy_shm/README.md +++ b/crates/fspy_shm/README.md @@ -28,7 +28,7 @@ One implementation serves every platform: a sparse file at the caller's path. An Only written pages ever occupy memory or disk. The multi-gigabyte capacity fspy asks for therefore costs about as much as the data a run actually records. -Every operation goes through [`fspy_nostd`](../fspy_nostd) wrappers or direct Win32 calls. The platform-specific parts are three short passages: +The crate is `no_std`: paths arrive as platform C strings and errors are raw OS error codes, so it stays usable from the preload contexts described in [`fspy_nostd`](../fspy_nostd)'s README. Every operation goes through `fspy_nostd` wrappers or direct Win32 calls. The platform-specific parts are three short passages: | Concern | Unix | Windows | | ---------------- | ---------------------------------------- | --------------------------------------------------------------------------- | diff --git a/crates/fspy_shm/src/lib.rs b/crates/fspy_shm/src/lib.rs index 65126f27b..05a245e7a 100644 --- a/crates/fspy_shm/src/lib.rs +++ b/crates/fspy_shm/src/lib.rs @@ -1,4 +1,5 @@ #![doc = include_str!("../README.md")] +#![cfg_attr(not(test), no_std)] #[cfg(unix)] mod unix; @@ -11,14 +12,25 @@ use unix as platform; #[cfg(windows)] use windows as platform; +// Sizes travel as `u64` through the file APIs and as `usize` through the +// mapping APIs. Supported targets give both the same width, so conversions +// between them are lossless. +const _: () = assert!(usize::BITS == u64::BITS); + +/// Converts a backing file's size to a mapping length. +#[expect(clippy::cast_possible_truncation, reason = "lossless; see the width assert above")] +const fn file_size_to_len(size: u64) -> usize { + size as usize +} + #[cfg(test)] mod tests { #[cfg(windows)] use std::fs::File; - use std::{env::temp_dir, ffi::OsStr, mem::align_of, path::PathBuf, process::Command}; + use std::{ffi::OsStr, mem::align_of, path::PathBuf, process::Command}; + use fspy_nostd::{OsCStr, Thin}; use subprocess_test::command_for_fn; - use uuid::Uuid; use super::{Mapping, create, open, remove}; @@ -27,43 +39,63 @@ mod tests { // Use one byte more than 64 KiB to test multiple pages and a partial last page. const ZERO_INITIALIZED_SIZE: usize = SIZE + 1; - /// A fresh backing path that removes its file when the test ends, even on - /// panic — the job the fspy channel's keeper does in production. - struct BackingPath(PathBuf); + #[cfg(unix)] + fn encode(path: &OsStr) -> Vec { + use std::os::unix::ffi::OsStrExt as _; + + let mut units = path.as_bytes().to_vec(); + units.push(0); + units + } + + #[cfg(windows)] + fn encode(path: &OsStr) -> Vec { + use std::os::windows::ffi::OsStrExt as _; + + let mut units: Vec = path.encode_wide().collect(); + units.push(0); + units + } + + /// A fresh backing path whose directory removes the file when the test + /// ends, even on panic — the job the fspy channel's keeper does in + /// production. + struct BackingPath { + _dir: tempfile::TempDir, + path: PathBuf, + #[cfg(unix)] + units: Vec, + #[cfg(windows)] + units: Vec, + } impl BackingPath { fn new() -> Self { - let path = std::path::absolute(temp_dir()) - .unwrap() - .join(format!("fspy-shm-test-{}.shm", Uuid::new_v4().simple())); - Self(path) + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("backing.shm"); + let units = encode(path.as_os_str()); + Self { _dir: dir, path, units } } - fn as_os_str(&self) -> &OsStr { - self.0.as_os_str() + fn as_c_str(&self) -> OsCStr<'_, Thin> { + fspy_nostd::OsCStr::from_units_with_nul(&self.units).unwrap().as_thin() } fn exists(&self) -> bool { - self.0.exists() + self.path.exists() } fn to_str(&self) -> String { - self.0.to_str().expect("test temp dir is UTF-8").to_owned() - } - } - - impl Drop for BackingPath { - fn drop(&mut self) { - let _ = remove(self.0.as_os_str()); + self.path.to_str().expect("test temp dir is UTF-8").to_owned() } } #[test] fn new_mapping_is_zero_initialized_in_all_views() { let path = BackingPath::new(); - let handle = create(path.as_os_str(), ZERO_INITIALIZED_SIZE).unwrap(); + let handle = create(path.as_c_str(), ZERO_INITIALIZED_SIZE).unwrap(); let first = handle.map().unwrap(); - let second = open(path.as_os_str()).unwrap().map().unwrap(); + let second = open(path.as_c_str()).unwrap().map().unwrap(); assert_zero_initialized(&first); assert_zero_initialized(&second); @@ -72,12 +104,12 @@ mod tests { #[test] fn mappings_of_one_backing_file_are_shared() { let path = BackingPath::new(); - let handle = create(path.as_os_str(), SIZE).unwrap(); + let handle = create(path.as_c_str(), SIZE).unwrap(); let first = handle.map().unwrap(); assert_eq!(first.len(), SIZE); assert_eq!(first.as_ptr() as usize % align_of::(), 0); - let second = open(path.as_os_str()).unwrap().map().unwrap(); + let second = open(path.as_c_str()).unwrap().map().unwrap(); assert_eq!(second.len(), SIZE); write_byte(&first, 0, 17); @@ -89,7 +121,7 @@ mod tests { #[test] fn one_handle_maps_repeatedly() { let path = BackingPath::new(); - let handle = create(path.as_os_str(), SIZE).unwrap(); + let handle = create(path.as_c_str(), SIZE).unwrap(); let first = handle.map().unwrap(); let second = handle.map().unwrap(); @@ -100,20 +132,22 @@ mod tests { #[test] fn create_rejects_an_existing_path() { let path = BackingPath::new(); - let _handle = create(path.as_os_str(), SIZE).unwrap(); + let _handle = create(path.as_c_str(), SIZE).unwrap(); - assert!(create(path.as_os_str(), SIZE).is_err()); + assert!(create(path.as_c_str(), SIZE).is_err()); } #[test] fn mapping_is_visible_across_processes() { let path = BackingPath::new(); - let handle = create(path.as_os_str(), SIZE).unwrap(); + let handle = create(path.as_c_str(), SIZE).unwrap(); let mapping = handle.map().unwrap(); write_byte(&mapping, 0, 17); let command = command_for_fn!(path.to_str(), |path: String| { - let opened = open(OsStr::new(&path)).unwrap().map().unwrap(); + let units = encode(OsStr::new(&path)); + let path = fspy_nostd::OsCStr::from_units_with_nul(&units).unwrap().as_thin(); + let opened = open(path).unwrap().map().unwrap(); assert_eq!(read_byte(&opened, 0), 17); write_byte(&opened, SIZE - 1, 29); }); @@ -124,25 +158,25 @@ mod tests { #[test] fn remove_prevents_new_opens() { let path = BackingPath::new(); - let handle = create(path.as_os_str(), SIZE).unwrap(); + let handle = create(path.as_c_str(), SIZE).unwrap(); drop(handle); - remove(path.as_os_str()).unwrap(); + remove(path.as_c_str()).unwrap(); - assert!(open(path.as_os_str()).is_err()); + assert!(open(path.as_c_str()).is_err()); } #[test] fn opened_mapping_survives_remove() { let path = BackingPath::new(); - let handle = create(path.as_os_str(), SIZE).unwrap(); - let opened = open(path.as_os_str()).unwrap().map().unwrap(); + let handle = create(path.as_c_str(), SIZE).unwrap(); + let opened = open(path.as_c_str()).unwrap().map().unwrap(); write_byte(&opened, 0, 17); drop(handle); - remove(path.as_os_str()).unwrap(); + remove(path.as_c_str()).unwrap(); - assert!(open(path.as_os_str()).is_err()); + assert!(open(path.as_c_str()).is_err()); assert_eq!(read_byte(&opened, 0), 17); write_byte(&opened, SIZE - 1, 29); assert_eq!(read_byte(&opened, SIZE - 1), 29); @@ -153,15 +187,15 @@ mod tests { #[test] fn remove_deletes_backing_file_and_preserves_existing_mappings() { let path = BackingPath::new(); - let handle = create(path.as_os_str(), SIZE).unwrap(); - let opened = open(path.as_os_str()).unwrap().map().unwrap(); + let handle = create(path.as_c_str(), SIZE).unwrap(); + let opened = open(path.as_c_str()).unwrap().map().unwrap(); drop(handle); assert!(path.exists()); - remove(path.as_os_str()).unwrap(); + remove(path.as_c_str()).unwrap(); assert!(!path.exists()); - assert!(open(path.as_os_str()).is_err()); + assert!(open(path.as_c_str()).is_err()); write_byte(&opened, 0, 17); assert_eq!(read_byte(&opened, 0), 17); } @@ -171,14 +205,14 @@ mod tests { #[test] fn remove_with_open_handle_removes_name_and_handle_still_maps() { let path = BackingPath::new(); - let handle = create(path.as_os_str(), SIZE).unwrap(); + let handle = create(path.as_c_str(), SIZE).unwrap(); let before = handle.map().unwrap(); write_byte(&before, 0, 17); - remove(path.as_os_str()).unwrap(); + remove(path.as_c_str()).unwrap(); assert!(!path.exists()); - assert!(open(path.as_os_str()).is_err()); + assert!(open(path.as_c_str()).is_err()); let after = handle.map().unwrap(); assert_eq!(read_byte(&after, 0), 17); @@ -194,16 +228,16 @@ mod tests { const MAX_ENDPOINT_ALLOCATION: u64 = 16 * 1024 * 1024; let path = BackingPath::new(); - let handle = create(path.as_os_str(), PRODUCTION_SIZE).unwrap(); + let handle = create(path.as_c_str(), PRODUCTION_SIZE).unwrap(); #[cfg(windows)] { - let (logical_size, initial_allocation) = backing_file_sizes(path.as_os_str()); + let (logical_size, initial_allocation) = backing_file_sizes(&path); assert_eq!(logical_size, PRODUCTION_SIZE as u64); assert!(initial_allocation < MAX_ENDPOINT_ALLOCATION); } let first = handle.map().unwrap(); - let opened = open(path.as_os_str()).unwrap().map().unwrap(); + let opened = open(path.as_c_str()).unwrap().map().unwrap(); write_byte(&first, 0, 17); write_byte(&first, PRODUCTION_SIZE - 1, 29); assert_eq!(read_byte(&opened, 0), 17); @@ -212,15 +246,15 @@ mod tests { // Touching both endpoints must not have allocated the range between them. #[cfg(windows)] { - let (logical_size, endpoint_allocation) = backing_file_sizes(path.as_os_str()); + let (logical_size, endpoint_allocation) = backing_file_sizes(&path); assert_eq!(logical_size, PRODUCTION_SIZE as u64); assert!(endpoint_allocation < MAX_ENDPOINT_ALLOCATION); } } #[cfg(windows)] - fn backing_file_sizes(path: &OsStr) -> (u64, u64) { - let file = File::open(path).unwrap(); + fn backing_file_sizes(path: &BackingPath) -> (u64, u64) { + let file = File::open(&path.path).unwrap(); super::windows::file_sizes(&file).unwrap() } diff --git a/crates/fspy_shm/src/unix.rs b/crates/fspy_shm/src/unix.rs index 22200abac..f2a1075ed 100644 --- a/crates/fspy_shm/src/unix.rs +++ b/crates/fspy_shm/src/unix.rs @@ -1,13 +1,9 @@ //! Unix shared memory backed by a sparse temporary file and identified by its //! path. -use std::{ - ffi::{CString, OsStr}, - io, - num::NonZeroUsize, - os::unix::ffi::OsStrExt as _, - ptr::{self, NonNull}, -}; +use core::ptr::{self, NonNull}; + +use fspy_nostd::{OsCStr, Result, Thin}; /// Opened shared memory that is not mapped yet. /// @@ -15,7 +11,7 @@ use std::{ /// view of the same bytes. Drop the handle once the mappings exist. pub struct ShmHandle { file: fspy_nostd::OwnedFd, - size: NonZeroUsize, + size: usize, } /// The mapped shared bytes. @@ -24,7 +20,7 @@ pub struct ShmHandle { /// shared memory's identifier. pub struct Mapping { ptr: NonNull, - len: NonZeroUsize, + len: usize, } // SAFETY: a mapping owns no thread-affine state; access synchronization is @@ -47,19 +43,16 @@ unsafe impl Sync for Mapping {} /// Only pages that are actually written occupy memory or disk, so a large /// capacity is cheap. /// +/// A `size` of zero is not rejected here: mapping the empty file fails with +/// the OS's own error. +/// /// # Errors /// -/// Returns an error if the shared memory cannot be created or sized. A file -/// created by the failing call is removed before it returns. -pub fn create(path: &OsStr, size: usize) -> io::Result { - let size = NonZeroUsize::new(size).ok_or_else(|| { - io::Error::new(io::ErrorKind::InvalidInput, "shared-memory size must be nonzero") - })?; - let size_u64 = u64::try_from(size.get()).map_err(|_| { - io::Error::new(io::ErrorKind::InvalidInput, "shared-memory size exceeds u64") - })?; - - let file = open_file( +/// Returns the error reported while creating or sizing the shared memory. A +/// file created by the failing call is removed before it returns. +pub fn create(path: OsCStr<'_, Thin>, size: usize) -> Result { + let file = fspy_nostd::fs::openat( + fspy_nostd::CWD, path, fspy_nostd::fs::OFlags::RDWR | fspy_nostd::fs::OFlags::CREATE @@ -70,10 +63,10 @@ pub fn create(path: &OsStr, size: usize) -> io::Result { )?; // Every byte reads as zero because the file is all holes. - if let Err(error) = fspy_nostd::fs::ftruncate(&file, size_u64) { + if let Err(error) = fspy_nostd::fs::ftruncate(&file, size as u64) { // Do not hand the caller an unusable partial file to clean up. let _ = remove(path); - return Err(error_to_io(error)); + return Err(error); } Ok(ShmHandle { file, size }) @@ -81,12 +74,16 @@ pub fn create(path: &OsStr, size: usize) -> io::Result { /// Opens the shared memory backed by the file at `path`. /// +/// The file's size is read here but validated by [`map`](ShmHandle::map): +/// mapping an empty or oversized file fails there with the OS's own error. +/// /// # Errors /// -/// Returns an error if the shared memory is unavailable, which is the common +/// Returns the error reported while opening the file, which is the common /// case once the backing file has been removed. -pub fn open(path: &OsStr) -> io::Result { - let file = open_file( +pub fn open(path: OsCStr<'_, Thin>) -> Result { + let file = fspy_nostd::fs::openat( + fspy_nostd::CWD, path, fspy_nostd::fs::OFlags::RDWR | fspy_nostd::fs::OFlags::CLOEXEC, fspy_nostd::fs::Mode::empty(), @@ -94,22 +91,12 @@ pub fn open(path: &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::fstat(&file).map_err(error_to_io)?.st_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"))?; + // + // A regular file's size is never negative. + let size = crate::file_size_to_len(fspy_nostd::fs::fstat(&file)?.st_size.cast_unsigned()); Ok(ShmHandle { file, size }) } -fn open_file( - path: &OsStr, - flags: fspy_nostd::fs::OFlags, - mode: fspy_nostd::fs::Mode, -) -> io::Result { - let path = CString::new(path.as_bytes())?; - fspy_nostd::fs::openat(fspy_nostd::CWD, as_nostd_path(&path), flags, mode).map_err(error_to_io) -} - /// Removes the shared memory at `path`. /// /// Removal is cleanup, not a stop signal: later opens fail, but existing @@ -119,24 +106,8 @@ fn open_file( /// # Errors /// /// Returns the error reported while unlinking the path. -pub fn remove(path: &OsStr) -> io::Result<()> { - let path = CString::new(path.as_bytes())?; - fspy_nostd::fs::unlinkat( - fspy_nostd::CWD, - as_nostd_path(&path), - fspy_nostd::fs::AtFlags::empty(), - ) - .map_err(error_to_io) -} - -fn as_nostd_path(path: &CString) -> fspy_nostd::CStr<'_, fspy_nostd::Fat> { - // SAFETY: `CString` contains no interior NUL and includes one terminating - // NUL; the returned view borrows it. - unsafe { fspy_nostd::CStr::from_units_with_nul_unchecked(path.as_bytes_with_nul()) } -} - -fn error_to_io(error: fspy_nostd::Error) -> io::Error { - io::Error::from_raw_os_error(error.raw_os_error()) +pub fn remove(path: OsCStr<'_, Thin>) -> Result<()> { + fspy_nostd::fs::unlinkat(fspy_nostd::CWD, path, fspy_nostd::fs::AtFlags::empty()) } impl ShmHandle { @@ -145,33 +116,30 @@ impl ShmHandle { /// # Errors /// /// Returns an error if the mapping cannot be established. - pub fn map(&self) -> io::Result { - let _slice_len = isize::try_from(self.size.get()).map_err(|_| { - io::Error::new(io::ErrorKind::InvalidData, "shared-memory size exceeds isize") - })?; - // SAFETY: the address is only a hint, the validated nonzero length is - // representable as a Rust slice, the descriptor remains borrowed, and - // the resulting shared mapping is owned by `Mapping`. + pub fn map(&self) -> Result { + let len = self.size; + // SAFETY: the address is only a hint, the descriptor remains + // borrowed, and the resulting shared mapping is owned by `Mapping`. + // The kernel rejects zero and address-space-exceeding lengths. let mapped = unsafe { fspy_nostd::mm::mmap( ptr::null_mut(), - self.size.get(), + len, fspy_nostd::mm::ProtFlags::READ | fspy_nostd::mm::ProtFlags::WRITE, fspy_nostd::mm::MapFlags::SHARED, &self.file, 0, ) - } - .map_err(error_to_io)?; + }?; let Some(ptr) = NonNull::new(mapped.cast()) else { // `mmap` reports failure with `MAP_FAILED`, not null, so this is a // successful mapping at address zero. Rust references cannot // represent it. // SAFETY: release that complete mapping before returning an error. - let _ = unsafe { fspy_nostd::mm::munmap(mapped, self.size.get()) }; - return Err(io::Error::other("mmap returned address zero")); + let _ = unsafe { fspy_nostd::mm::munmap(mapped, len) }; + return Err(fspy_nostd::Error::INVAL); }; - Ok(Mapping { ptr, len: self.size }) + Ok(Mapping { ptr, len }) } } @@ -179,7 +147,7 @@ impl Drop for Mapping { fn drop(&mut self) { // SAFETY: this is the complete mapping owned by `self`, and dropping // it proves that no safe borrow through `self` remains. - let _ = unsafe { fspy_nostd::mm::munmap(self.ptr.as_ptr().cast(), self.len.get()) }; + let _ = unsafe { fspy_nostd::mm::munmap(self.ptr.as_ptr().cast(), self.len) }; } } @@ -188,7 +156,7 @@ impl Mapping { /// Returns the mapped length in bytes. #[must_use] pub const fn len(&self) -> usize { - self.len.get() + self.len } /// Returns a raw pointer to the first mapped byte. @@ -205,8 +173,10 @@ impl Mapping { /// the lifetime of the returned slice. #[must_use] pub const unsafe fn as_slice(&self) -> &[u8] { - // SAFETY: The mapping is valid for its full length, and the caller + // SAFETY: The mapping is valid for its full length, which fits the + // virtual address space and is therefore far below the `isize::MAX` + // slice bound on every supported 64-bit target, 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()) } } } diff --git a/crates/fspy_shm/src/windows.rs b/crates/fspy_shm/src/windows.rs index 0fcb19da0..a8c5142b0 100644 --- a/crates/fspy_shm/src/windows.rs +++ b/crates/fspy_shm/src/windows.rs @@ -2,12 +2,11 @@ //! its path. use core::{ffi::c_void, mem::size_of, ptr}; -use std::{ffi::OsStr, io, num::NonZeroUsize, os::windows::ffi::OsStrExt as _}; #[cfg(test)] -use std::{fs::File, os::windows::io::AsRawHandle as _}; +use std::{fs::File, io, os::windows::io::AsRawHandle as _}; use fspy_nostd::{ - BorrowedHandle, bool_result, + BorrowedHandle, OsCStr, Result, Thin, bool_result, fs::{CreationDisposition, FileAccess, FileOptions, FileShare}, mm::{MappingAccess, PageProtection}, }; @@ -32,7 +31,7 @@ const SHARE_ALL: FileShare = FileShare::READ.union(FileShare::WRITE).union(FileS /// view of the same bytes. Drop the handle once the mappings exist. pub struct ShmHandle { file: fspy_nostd::OwnedHandle, - size: NonZeroUsize, + size: usize, } /// The mapped shared bytes. @@ -41,7 +40,7 @@ pub struct ShmHandle { /// shared memory's identifier. pub struct Mapping { view: fspy_nostd::mm::MappingView, - len: NonZeroUsize, + len: usize, } /// Creates `size` bytes of zero-initialized shared memory backed by the file @@ -60,19 +59,15 @@ pub struct Mapping { /// Only pages that are actually written occupy memory or disk, so a large /// capacity is cheap. /// +/// A `size` of zero is not rejected here: mapping the empty file fails with +/// the OS's own error. +/// /// # Errors /// -/// Returns an error if the shared memory cannot be created or sized. Creation -/// fails on volumes without sparse-file support. A file created by the -/// failing call is removed before it returns. -pub fn create(path: &OsStr, size: usize) -> io::Result { - 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") - })?; - +/// Returns the error reported while creating or sizing the shared memory. +/// Creation fails on volumes without sparse-file support. A file created by +/// the failing call is removed before it returns. +pub fn create(path: OsCStr<'_, Thin>, size: usize) -> Result { let file = open_file( path, FileAccess::GENERIC_READ | FileAccess::GENERIC_WRITE, @@ -83,16 +78,18 @@ pub fn create(path: &OsStr, size: usize) -> io::Result { FileOptions::TEMPORARY | FileOptions::OPEN_REPARSE_POINT, )?; - if let Err(error) = size_backing_file(file.as_handle(), size_i64) { + // A size above `i64::MAX` turns negative here, and Windows rejects a + // negative end of file with its own error. + if let Err(error) = size_backing_file(file.as_handle(), (size as u64).cast_signed()) { // Do not hand the caller an unusable partial file to clean up. let _ = remove(path); - return Err(error_to_io(error)); + return Err(error); } Ok(ShmHandle { file, size }) } -fn size_backing_file(file: BorrowedHandle<'_>, len: i64) -> fspy_nostd::Result<()> { +fn size_backing_file(file: BorrowedHandle<'_>, len: i64) -> Result<()> { // 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. @@ -103,11 +100,14 @@ fn size_backing_file(file: BorrowedHandle<'_>, len: i64) -> fspy_nostd::Result<( /// Opens the shared memory backed by the file at `path`. /// +/// The file's size is read here but validated by [`map`](ShmHandle::map): +/// mapping an empty or oversized file fails there with the OS's own error. +/// /// # Errors /// -/// Returns an error if the shared memory is unavailable, which is the common +/// Returns the error reported while opening the file, which is the common /// case once the backing file has been removed. -pub fn open(path: &OsStr) -> io::Result { +pub fn open(path: OsCStr<'_, Thin>) -> Result { let file = open_file( path, FileAccess::GENERIC_READ | FileAccess::GENERIC_WRITE, @@ -117,54 +117,23 @@ pub fn open(path: &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. + // + // A regular file's size is never negative. 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"))?; + crate::file_size_to_len(fspy_nostd::fs::get_file_size(file.as_handle())?.cast_unsigned()); Ok(ShmHandle { file, size }) } fn open_file( - path: &OsStr, - access: FileAccess, - disposition: CreationDisposition, - options: FileOptions, -) -> io::Result { - let path = copy_path(path)?; - 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>, + path: OsCStr<'_, Thin>, access: FileAccess, disposition: CreationDisposition, options: FileOptions, -) -> fspy_nostd::Result { +) -> Result { fspy_nostd::fs::create_file(path, access, SHARE_ALL, None, disposition, options, None) } -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) -} - -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 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<()> { +fn set_sparse(file: BorrowedHandle<'_>) -> 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 +153,7 @@ fn set_sparse(file: BorrowedHandle<'_>) -> fspy_nostd::Result<()> { }) } -fn set_end_of_file(file: BorrowedHandle<'_>, len: i64) -> fspy_nostd::Result<()> { +fn set_end_of_file(file: BorrowedHandle<'_>, len: i64) -> Result<()> { const INFO_SIZE: u32 = 8; const _: [(); 8] = [(); size_of::()]; @@ -215,22 +184,20 @@ fn set_end_of_file(file: BorrowedHandle<'_>, len: i64) -> fspy_nostd::Result<()> /// # Errors /// /// Returns the error reported while unlinking the path. -pub fn remove(path: &OsStr) -> io::Result<()> { - let path = copy_path(path)?; +pub fn remove(path: OsCStr<'_, Thin>) -> Result<()> { // Opening the reparse point itself removes a link rather than its target. - let file = open_file_wide( - as_nostd_path(&path), + let file = open_file( + path, FileAccess::DELETE, CreationDisposition::OpenExisting, 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) + set_posix_delete(file.as_handle()) } -fn set_posix_delete(file: BorrowedHandle<'_>) -> fspy_nostd::Result<()> { +fn set_posix_delete(file: BorrowedHandle<'_>) -> Result<()> { const INFO_SIZE: u32 = 4; const _: [(); 4] = [(); size_of::()]; @@ -258,31 +225,27 @@ impl ShmHandle { /// # Errors /// /// Returns an error if the mapping cannot be established. - pub fn map(&self) -> io::Result { - let _slice_len = isize::try_from(self.size.get()).map_err(|_| { - io::Error::new(io::ErrorKind::InvalidData, "shared-memory size exceeds isize") - })?; + pub fn map(&self) -> Result { + let len = self.size; // Default security attributes create a non-inheritable mapping object. // Both maximum-size halves are zero, so Windows uses the current file - // size. + // size and rejects an empty file with its own error. let mapping = fspy_nostd::mm::create_file_mapping( self.file.as_handle(), None, PageProtection::ReadWrite, 0, 0, - ) - .map_err(error_to_io)?; + )?; let view = fspy_nostd::mm::map_view_of_file( mapping.as_handle(), MappingAccess::READ | MappingAccess::WRITE, 0, 0, - self.size.get(), - ) - .map_err(error_to_io)?; + len, + )?; // The view remains valid after its mapping-object handle closes. - Ok(Mapping { view, len: self.size }) + Ok(Mapping { view, len }) } } @@ -291,7 +254,7 @@ impl Mapping { /// Returns the mapped length in bytes. #[must_use] pub const fn len(&self) -> usize { - self.len.get() + self.len } /// Returns a raw pointer to the first mapped byte. @@ -308,7 +271,9 @@ impl Mapping { /// the lifetime of the returned slice. #[must_use] pub const unsafe fn as_slice(&self) -> &[u8] { - // SAFETY: The mapping is valid for its full length, and the caller + // SAFETY: The mapping is valid for its full length, which fits the + // virtual address space and is therefore far below the `isize::MAX` + // slice bound on every supported 64-bit target, and the caller // guarantees that it is not mutated while the slice is borrowed. unsafe { core::slice::from_raw_parts(self.as_ptr().cast_const(), self.len()) } }