Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
9 changes: 8 additions & 1 deletion crates/fspy_nostd/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,14 @@ 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_LibraryLoader",
"Win32_System_Memory",
] }

# Cross-validates the page-size probe against rustix's auxv-based answer.
[target.'cfg(target_os = "linux")'.dev-dependencies]
Expand Down
4 changes: 2 additions & 2 deletions crates/fspy_nostd/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
28 changes: 28 additions & 0 deletions crates/fspy_nostd/src/c_str.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self> {
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
Expand Down Expand Up @@ -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::<Fat>::from_units_with_nul(b"abc\0").unwrap();

assert_eq!(checked.as_units(), b"abc");
assert_eq!(checked.len_with_nul(), 4);
assert!(CStr::<Fat>::from_units_with_nul(b"").is_none());
assert!(CStr::<Fat>::from_units_with_nul(b"abc").is_none());
assert!(CStr::<Fat>::from_units_with_nul(b"a\0c\0").is_none());
assert!(WideCStr::<Fat>::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();
Expand Down
67 changes: 9 additions & 58 deletions crates/fspy_nostd/src/fs/mod.rs
Original file line number Diff line number Diff line change
@@ -1,67 +1,18 @@
//! Filesystem calls with caller-owned storage.

use core::mem::MaybeUninit;

pub use rustix::fs::{AtFlags, Mode, OFlags, fstat, ftruncate};

use crate::{BorrowedFd, CStr, Fat, OwnedFd, Result};

#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "macos")]
mod mac;
#[cfg(unix)]
mod unix;
#[cfg(windows)]
mod windows;

#[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.
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<R>(
dirfd: BorrowedFd<'_>,
path: CStr<'_, R>,
flags: OFlags,
mode: Mode,
) -> Result<OwnedFd> {
imp::openat(dirfd, path, flags, mode)
}

/// Removes `path` relative to `dirfd`.
///
/// # Errors
///
/// Returns the error reported by `unlinkat`.
pub fn unlinkat<R>(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<u8>]) -> Result<CStr<'_, Fat>> {
imp::getcwd(buf)
}
#[cfg(unix)]
pub use unix::*;
#[cfg(windows)]
pub use windows::*;

#[cfg(test)]
#[cfg(all(test, unix))]
mod tests;
56 changes: 56 additions & 0 deletions crates/fspy_nostd/src/fs/unix.rs
Original file line number Diff line number Diff line change
@@ -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<R>(
dirfd: BorrowedFd<'_>,
path: CStr<'_, R>,
flags: OFlags,
mode: Mode,
) -> Result<OwnedFd> {
imp::openat(dirfd, path, flags, mode)
}

/// Removes `path` relative to `dirfd`.
///
/// # Errors
///
/// Returns the error reported by `unlinkat`.
pub fn unlinkat<R>(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<u8>]) -> Result<CStr<'_, Fat>> {
imp::getcwd(buf)
}
115 changes: 115 additions & 0 deletions crates/fspy_nostd/src/fs/windows.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
use core::ptr;

use bitflags::bitflags;
use windows_sys::Win32::{
Foundation::{GENERIC_READ, GENERIC_WRITE, INVALID_HANDLE_VALUE},
Storage::FileSystem::{
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,
},
};

use crate::{BorrowedHandle, 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;
/// 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.
///
/// `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,
/// Open an existing file and fail if it does not exist.
OpenExisting = OPEN_EXISTING,
/// Open and truncate an existing file.
TruncateExisting = TRUNCATE_EXISTING,
}

/// 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 create_file<R>(
path: WideCStr<'_, R>,
access: FileAccess,
share: FileShare,
security_attributes: Option<&SecurityAttributes>,
disposition: CreationDisposition,
options: FileOptions,
template_file: Option<BorrowedHandle<'_>>,
) -> Result<OwnedHandle> {
let security_attributes = security_attributes.map_or(ptr::null(), SecurityAttributes::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.
// Windows validates the template's object type and all scalar options.
let handle = unsafe {
CreateFileW(
path.as_ptr(),
access.bits(),
share.bits(),
security_attributes,
disposition as u32,
options.bits(),
template_file,
)
};
if handle == INVALID_HANDLE_VALUE {
Err(crate::windows::last_error())
} else {
// SAFETY: `CreateFileW` returned a valid, newly owned handle.
Ok(unsafe { OwnedHandle::from_raw_handle(handle) })
}
}

/// Calls `GetFileSizeEx`.
///
/// # Errors
///
/// Returns the error reported by `GetFileSizeEx`.
pub fn get_file_size(file: BorrowedHandle<'_>) -> Result<i64> {
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_handle(), &raw mut size) })?;
Ok(size)
}
9 changes: 6 additions & 3 deletions crates/fspy_nostd/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,19 @@ 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::{
BorrowedHandle, OwnedHandle, RawHandle, SecurityAttributes, bool_result, get_module_handle,
last_error,
};

#[cfg(windows)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
Expand Down
16 changes: 7 additions & 9 deletions crates/fspy_nostd/src/mm.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
//! Anonymous memory mappings.
//!
//! Re-exposed from rustix as-is: these 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;
#[cfg(windows)]
pub use windows::*;
Loading
Loading