Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
26 changes: 13 additions & 13 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ monostate = "1.0.2"
napi = "3"
napi-build = "2"
napi-derive = "3"
native_str = { path = "crates/native_str" }
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"
Expand Down
4 changes: 2 additions & 2 deletions crates/fspy/src/unix/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use std::{io, path::Path};
use fspy_seccomp_unotify::supervisor::supervise;
use fspy_shared::ipc::PathAccess;
#[cfg(not(target_env = "musl"))]
use fspy_shared::ipc::{NativeStr, channel::channel};
use fspy_shared::ipc::{IpcStr, channel::channel};
#[cfg(target_os = "macos")]
use fspy_shared_unix::payload::Artifacts;
use fspy_shared_unix::{
Expand All @@ -34,7 +34,7 @@ pub struct SpyImpl {
artifacts: Artifacts,

#[cfg(not(target_env = "musl"))]
preload_path: Box<NativeStr>,
preload_path: Box<IpcStr>,
}

impl SpyImpl {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[package]
name = "native_str"
name = "fspy_ipc_str"
version = "0.0.0"
edition.workspace = true
license.workspace = true
Expand Down
14 changes: 7 additions & 7 deletions crates/native_str/README.md → crates/fspy_ipc_str/README.md
Original file line number Diff line number Diff line change
@@ -1,34 +1,34 @@
# native_str
# fspy_ipc_str

A platform-native string type for lossless, zero-copy IPC.

`NativeStr` is a `#[repr(transparent)]` newtype over `[u8]` that represents OS strings in their native encoding:
`IpcStr` is a `#[repr(transparent)]` newtype over `[u8]` that represents OS strings in their native encoding:

- **Unix**: raw bytes (same as `OsStr`)
- **Windows**: raw wide character bytes (from `&[u16]`, stored as `&[u8]` for uniform handling)

## Why not `OsStr`?

`OsStr` requires valid UTF-8 for serialization. `NativeStr` can be serialized/deserialized losslessly regardless of encoding, with zero-copy support via wincode's `SchemaRead`.
`OsStr` requires valid UTF-8 for serialization. `IpcStr` can be serialized/deserialized losslessly regardless of encoding, with zero-copy support via wincode's `SchemaRead`.

## Limitations

**Not portable across platforms.** The binary representation of a `NativeStr` is platform-specific — Unix uses raw bytes while Windows uses wide character pairs. Deserializing a `NativeStr` that was serialized on a different platform leads to unspecified behavior (garbage data), but is not unsafe.
**Not portable across platforms.** The binary representation of an `IpcStr` is platform-specific — Unix uses raw bytes while Windows uses wide character pairs. Deserializing an `IpcStr` that was serialized on a different platform leads to unspecified behavior (garbage data), but is not unsafe.

This type is designed for same-platform IPC (e.g., shared memory between a parent process and its children), not for cross-platform data exchange or persistent storage. For portable paths, use UTF-8 strings instead.

## Usage

```rust
use native_str::NativeStr;
use fspy_ipc_str::IpcStr;

// Unix: construct from bytes
#[cfg(unix)]
let s: &NativeStr = NativeStr::from_bytes(b"/tmp/foo");
let s: &IpcStr = IpcStr::from_bytes(b"/tmp/foo");

// Windows: construct from wide chars
#[cfg(windows)]
let s: &NativeStr = NativeStr::from_wide(&[0x0048, 0x0069]); // "Hi"
let s: &IpcStr = IpcStr::from_wide(&[0x0048, 0x0069]); // "Hi"

// Convert back to OsStr/OsString
let os = s.to_cow_os_str();
Expand Down
58 changes: 29 additions & 29 deletions crates/native_str/src/lib.rs → crates/fspy_ipc_str/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,18 +35,18 @@ use wincode::{
/// # Limitations
///
/// **Not portable across platforms.** The binary representation is platform-specific.
/// Deserializing a `NativeStr` serialized on a different platform leads to unspecified
/// Deserializing an `IpcStr` serialized on a different platform leads to unspecified
/// behavior (garbage data), but is not unsafe. Designed for same-platform IPC only.
#[derive(TransparentWrapper, PartialEq, Eq, Hash)]
#[repr(transparent)]
pub struct NativeStr {
pub struct IpcStr {
// On unix, this is the raw bytes of the OsStr.
// On windows, this is safely transmuted from `&[u16]` in `NativeStr::from_wide`. We don't declare it as `&[u16]` to allow zero-copy read.
// On windows, this is safely transmuted from `&[u16]` in `IpcStr::from_wide`. We don't declare it as `&[u16]` to allow zero-copy read.
// Transmuting back to `&[u16]` would be unsafe because of different alignments between `u8` and `u16` (See `to_os_string`).
data: [u8],
}

impl NativeStr {
impl IpcStr {
#[cfg(unix)]
#[must_use]
pub fn from_bytes(bytes: &[u8]) -> &Self {
Expand Down Expand Up @@ -92,15 +92,15 @@ impl NativeStr {
}
}

impl Debug for NativeStr {
impl Debug for IpcStr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
<OsStr as Debug>::fmt(self.to_cow_os_str().as_ref(), f)
}
}

// Manual impl: wincode derive requires Sized, but NativeStr wraps unsized [u8].
// Manual impl: wincode derive requires Sized, but IpcStr wraps unsized [u8].
// SAFETY: Delegates to `[u8]`'s SchemaWrite impl, preserving its size/write invariants.
unsafe impl<C: Config> SchemaWrite<C> for NativeStr {
unsafe impl<C: Config> SchemaWrite<C> for IpcStr {
type Src = Self;

fn size_of(src: &Self::Src) -> WriteResult<usize> {
Expand All @@ -112,67 +112,67 @@ unsafe impl<C: Config> SchemaWrite<C> for NativeStr {
}
}

// SchemaRead for &NativeStr: zero-copy borrow from input bytes
// SchemaRead for &IpcStr: zero-copy borrow from input bytes
// SAFETY: Delegates to `&[u8]`'s SchemaRead impl; dst is initialized on Ok.
unsafe impl<'de, C: Config> SchemaRead<'de, C> for &'de NativeStr {
type Dst = &'de NativeStr;
unsafe impl<'de, C: Config> SchemaRead<'de, C> for &'de IpcStr {
type Dst = &'de IpcStr;

fn read(mut reader: impl Reader<'de>, dst: &mut MaybeUninit<Self::Dst>) -> ReadResult<()> {
let data: &'de [u8] = <&[u8] as SchemaRead<'de, C>>::get(&mut reader)?;
dst.write(NativeStr::wrap_ref(data));
dst.write(IpcStr::wrap_ref(data));
Ok(())
}
}

// SAFETY: Delegates to `NativeStr`'s SchemaWrite impl, preserving its invariants.
unsafe impl<C: Config> SchemaWrite<C> for Box<NativeStr> {
// SAFETY: Delegates to `IpcStr`'s SchemaWrite impl, preserving its invariants.
unsafe impl<C: Config> SchemaWrite<C> for Box<IpcStr> {
type Src = Self;

fn size_of(src: &Self::Src) -> WriteResult<usize> {
<NativeStr as SchemaWrite<C>>::size_of(src)
<IpcStr as SchemaWrite<C>>::size_of(src)
}

fn write(writer: impl Writer, src: &Self::Src) -> WriteResult<()> {
<NativeStr as SchemaWrite<C>>::write(writer, src)
<IpcStr as SchemaWrite<C>>::write(writer, src)
}
}

// SchemaRead for Box<NativeStr>: owned decode
// SchemaRead for Box<IpcStr>: owned decode
// SAFETY: Delegates to `&[u8]`'s SchemaRead impl; dst is initialized on Ok.
unsafe impl<'de, C: Config> SchemaRead<'de, C> for Box<NativeStr> {
unsafe impl<'de, C: Config> SchemaRead<'de, C> for Box<IpcStr> {
type Dst = Self;

fn read(mut reader: impl Reader<'de>, dst: &mut MaybeUninit<Self::Dst>) -> ReadResult<()> {
let data: &[u8] = <&[u8] as SchemaRead<'de, C>>::get(&mut reader)?;
dst.write(NativeStr::wrap_box(data.into()));
dst.write(IpcStr::wrap_box(data.into()));
Ok(())
}
}

#[cfg(unix)]
impl<'a, S: AsRef<OsStr> + ?Sized> From<&'a S> for &'a NativeStr {
impl<'a, S: AsRef<OsStr> + ?Sized> From<&'a S> for &'a IpcStr {
fn from(value: &'a S) -> Self {
NativeStr::from_bytes(value.as_ref().as_bytes())
IpcStr::from_bytes(value.as_ref().as_bytes())
}
}

impl Clone for Box<NativeStr> {
impl Clone for Box<IpcStr> {
fn clone(&self) -> Self {
NativeStr::wrap_box(self.data.into())
IpcStr::wrap_box(self.data.into())
}
}

impl<S: AsRef<OsStr>> From<S> for Box<NativeStr> {
impl<S: AsRef<OsStr>> From<S> for Box<IpcStr> {
#[cfg(unix)]
fn from(value: S) -> Self {
NativeStr::wrap_box(value.as_ref().as_bytes().into())
IpcStr::wrap_box(value.as_ref().as_bytes().into())
}

#[cfg(windows)]
fn from(value: S) -> Self {
let wide: Vec<u16> = value.as_ref().encode_wide().collect();
let data: &[u8] = must_cast_slice(&wide);
NativeStr::wrap_box(data.into())
IpcStr::wrap_box(data.into())
}
}

Expand All @@ -187,19 +187,19 @@ mod tests {
use std::os::windows::ffi::OsStrExt;

let wide_str: &[u16] = &[528, 491];
let native_str = NativeStr::from_wide(wide_str);
let ipc_str = IpcStr::from_wide(wide_str);

let mut encoded = wincode::serialize(native_str).unwrap();
let mut encoded = wincode::serialize(ipc_str).unwrap();

let decoded: &NativeStr = wincode::deserialize(&encoded).unwrap();
let decoded: &IpcStr = wincode::deserialize(&encoded).unwrap();
let decoded_wide = decoded.to_os_string().encode_wide().collect::<Vec<u16>>();
assert_eq!(decoded_wide, wide_str);

let encoded_len = encoded.len();
encoded.push(0);
encoded.copy_within(..encoded_len, 1);

let decoded: &NativeStr = wincode::deserialize(&encoded[1..]).unwrap();
let decoded: &IpcStr = wincode::deserialize(&encoded[1..]).unwrap();
let decoded_wide = decoded.to_os_string().encode_wide().collect::<Vec<u16>>();
assert_eq!(decoded_wide, wide_str);
}
Expand Down
10 changes: 5 additions & 5 deletions crates/fspy_preload_windows/src/windows/detours/nt.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::mem::{offset_of, size_of};

use fspy_shared::ipc::{AccessMode, NativePath, PathAccess};
use fspy_shared::ipc::{AccessMode, IpcPath, PathAccess};
use ntapi::{
ntioapi::{
FILE_INFORMATION_CLASS, NtQueryDirectoryFile, NtQueryFullAttributesFile,
Expand Down Expand Up @@ -96,11 +96,11 @@ static DETOUR_NT_CREATE_USER_PROCESS: Detour<
unsafe fn handle_process_image(attribute_list: PPS_ATTRIBUTE_LIST) {
// SAFETY: NtCreateUserProcess requires its attribute list to remain valid for this call.
if let Some(image_path) = unsafe { read_process_image_attribute(attribute_list) } {
// Sender serialization completes before this call returns, so NativePath does not retain
// Sender serialization completes before this call returns, so IpcPath does not retain
// the borrowed PS_ATTRIBUTE_IMAGE_NAME buffer past the NtCreateUserProcess call.
// SAFETY: accessing the global client which was initialized during DLL_PROCESS_ATTACH
unsafe { global_client() }
.send(PathAccess { mode: AccessMode::READ, path: NativePath::from_wide(image_path) });
.send(PathAccess { mode: AccessMode::READ, path: IpcPath::from_wide(image_path) });
}
}

Expand Down Expand Up @@ -296,7 +296,7 @@ unsafe fn handle_open(access_mode: impl ToAccessMode, path: impl ToAbsolutePath)
// SAFETY: converting access mask to AccessMode via FFI-aware trait
PathAccess {
mode: access_mode.to_access_mode(),
path: NativePath::from_wide(path),
path: IpcPath::from_wide(path),
}
},
|wildcard_pos| {
Expand All @@ -307,7 +307,7 @@ unsafe fn handle_open(access_mode: impl ToAccessMode, path: impl ToAbsolutePath)
.unwrap_or(0);
PathAccess {
mode: AccessMode::READ_DIR,
path: NativePath::from_wide(&path[..slash_pos]),
path: IpcPath::from_wide(&path[..slash_pos]),
}
},
);
Expand Down
2 changes: 1 addition & 1 deletion crates/fspy_shared/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ bumpalo = { workspace = true }
bstr = { workspace = true, features = ["alloc", "std"] }
bytemuck = { workspace = true, features = ["must_cast", "derive"] }
fspy_shm = { workspace = true }
native_str = { workspace = true }
fspy_ipc_str = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
uuid = { workspace = true, features = ["v4"] }
Expand Down
8 changes: 4 additions & 4 deletions crates/fspy_shared/src/ipc/channel/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ use tracing::debug;
use uuid::Uuid;
use wincode::{SchemaRead, SchemaWrite};

use super::NativeStr;
use super::IpcStr;

/// Serializable configuration to create channel senders.
#[derive(SchemaWrite, SchemaRead, Clone, Debug)]
pub struct ChannelConf {
lock_file_path: Box<NativeStr>,
shm_id: Box<NativeStr>,
lock_file_path: Box<IpcStr>,
shm_id: Box<IpcStr>,
}

/// Creates a mpsc IPC channel with one receiver and a `ChannelConf` that can be passed around processes and used to create multiple senders
Expand Down Expand Up @@ -61,7 +61,7 @@ impl ChannelConf {

pub struct Sender {
writer: ShmWriter<Mapping>,
lock_file_path: Box<NativeStr>,
lock_file_path: Box<IpcStr>,
lock_file: File,
}

Expand Down
Loading
Loading