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
10 changes: 10 additions & 0 deletions 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 @@ -85,6 +85,7 @@ jsonc-parser = { version = "0.29.0", features = ["serde"] }
libc = "0.2.172"
memmap2 = "0.9.7"
monostate = "1.0.2"
native_str = { path = "crates/native_str" }
nix = { version = "0.30.1", features = ["dir", "signal"] }
ntapi = "0.4.1"
nucleo-matcher = "0.3.1"
Expand Down
1 change: 1 addition & 0 deletions crates/fspy_shared/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ wincode = { workspace = true, features = ["derive"] }
bitflags = { workspace = true }
bstr = { workspace = true }
bytemuck = { workspace = true, features = ["must_cast", "derive"] }
native_str = { workspace = true }
shared_memory = { workspace = true, features = ["logging"] }
thiserror = { workspace = true }
tracing = { workspace = true }
Expand Down
2 changes: 0 additions & 2 deletions crates/fspy_shared/src/ipc/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
#[cfg(not(target_env = "musl"))]
pub mod channel;
mod native_path;
pub(crate) mod native_str;

use std::fmt::Debug;

use bitflags::bitflags;
Expand Down
3 changes: 1 addition & 2 deletions crates/fspy_shared/src/ipc/native_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,14 @@ use std::{

use allocator_api2::alloc::Allocator;
use bytemuck::TransparentWrapper;
use native_str::NativeStr;
use wincode::{
SchemaRead, SchemaWrite,
config::Config,
error::{ReadResult, WriteResult},
io::{Reader, Writer},
};

use super::native_str::NativeStr;

/// An opaque path type used in [`super::PathAccess`].
///
/// On Windows, tracked paths are NT Object Manager paths (`\??` prefix),
Expand Down
18 changes: 18 additions & 0 deletions crates/native_str/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "native_str"
version = "0.0.0"
edition.workspace = true
license.workspace = true
publish = false
rust-version.workspace = true

[dependencies]
allocator-api2 = { workspace = true }
bytemuck = { workspace = true, features = ["must_cast", "derive"] }
wincode = { workspace = true }

[lints]
workspace = true

[lib]
doctest = false
35 changes: 35 additions & 0 deletions crates/native_str/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# native_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:

- **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`.

## 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.

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;

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

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

// Convert back to OsStr/OsString
let os = s.to_cow_os_str();
```
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,24 @@ use wincode::{
io::{Reader, Writer},
};

/// Similar to `OsStr`, but
/// A platform-native string type for lossless, zero-copy IPC.
///
/// Similar to [`OsStr`], but:
/// - Can be infallibly and losslessly encoded/decoded using wincode.
/// (`SchemaWrite`/`SchemaRead` implementations for `OsStr` requires it to be valid UTF-8. This does not.)
/// (`SchemaWrite`/`SchemaRead` implementations for `OsStr` require it to be valid UTF-8. This does not.)
/// - Can be constructed from wide characters on Windows with zero copy.
/// - Supports zero-copy `SchemaRead`.
///
/// # Platform representation
///
/// - **Unix**: raw bytes of the `OsStr`.
/// - **Windows**: raw bytes transmuted from `&[u16]` (wide chars). See `to_os_string` for decoding.
///
/// # Limitations
///
/// **Not portable across platforms.** The binary representation is platform-specific.
/// Deserializing a `NativeStr` 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)]
#[repr(transparent)]
pub struct NativeStr {
Expand Down Expand Up @@ -73,6 +86,23 @@ impl NativeStr {
#[cfg(unix)]
return Cow::Borrowed(self.as_os_str());
}

pub fn clone_in<'new_alloc, A>(&self, alloc: &'new_alloc A) -> &'new_alloc Self
where
&'new_alloc A: Allocator,
{
use allocator_api2::vec::Vec;
let mut data = Vec::<u8, _>::with_capacity_in(self.data.len(), alloc);
data.extend_from_slice(&self.data);
let data = data.leak::<'new_alloc>();
Self::wrap_ref(data)
}
}

impl Debug for NativeStr {
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].
Expand All @@ -89,12 +119,6 @@ unsafe impl<C: Config> SchemaWrite<C> for NativeStr {
}
}

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

// SchemaRead for &NativeStr: 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 {
Expand Down Expand Up @@ -159,19 +183,6 @@ impl<S: AsRef<OsStr>> From<S> for Box<NativeStr> {
}
}

impl NativeStr {
pub fn clone_in<'new_alloc, A>(&self, alloc: &'new_alloc A) -> &'new_alloc Self
where
&'new_alloc A: Allocator,
{
use allocator_api2::vec::Vec;
let mut data = Vec::<u8, _>::with_capacity_in(self.data.len(), alloc);
data.extend_from_slice(&self.data);
let data = data.leak::<'new_alloc>();
Self::wrap_ref(data)
}
}

#[cfg(test)]
mod tests {
#[cfg(windows)]
Expand Down
Loading