Skip to content

Commit b2e2e2c

Browse files
branchseerclaude
andauthored
refactor: extract NativeStr into standalone crate (#338)
## Summary - Extract `NativeStr` from `fspy_shared::ipc::native_str` into a new `native_str` crate - Re-exported from `fspy_shared::ipc` so all downstream crates are unaffected - New crate has minimal dependencies: `allocator-api2`, `bytemuck`, `wincode` - Includes README.md documenting the type's purpose and platform behavior ## Test plan - [x] `cargo clippy` passes - [x] `cargo test --workspace --exclude fspy` passes - [x] `cargo shear` reports no new unused deps 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent dd1c0ec commit b2e2e2c

File tree

8 files changed

+98
-25
lines changed

8 files changed

+98
-25
lines changed

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ jsonc-parser = { version = "0.29.0", features = ["serde"] }
8585
libc = "0.2.172"
8686
memmap2 = "0.9.7"
8787
monostate = "1.0.2"
88+
native_str = { path = "crates/native_str" }
8889
nix = { version = "0.30.1", features = ["dir", "signal"] }
8990
ntapi = "0.4.1"
9091
nucleo-matcher = "0.3.1"

crates/fspy_shared/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ wincode = { workspace = true, features = ["derive"] }
1010
bitflags = { workspace = true }
1111
bstr = { workspace = true }
1212
bytemuck = { workspace = true, features = ["must_cast", "derive"] }
13+
native_str = { workspace = true }
1314
shared_memory = { workspace = true, features = ["logging"] }
1415
thiserror = { workspace = true }
1516
tracing = { workspace = true }

crates/fspy_shared/src/ipc/mod.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
#[cfg(not(target_env = "musl"))]
22
pub mod channel;
33
mod native_path;
4-
pub(crate) mod native_str;
5-
64
use std::fmt::Debug;
75

86
use bitflags::bitflags;

crates/fspy_shared/src/ipc/native_path.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,14 @@ use std::{
99

1010
use allocator_api2::alloc::Allocator;
1111
use bytemuck::TransparentWrapper;
12+
use native_str::NativeStr;
1213
use wincode::{
1314
SchemaRead, SchemaWrite,
1415
config::Config,
1516
error::{ReadResult, WriteResult},
1617
io::{Reader, Writer},
1718
};
1819

19-
use super::native_str::NativeStr;
20-
2120
/// An opaque path type used in [`super::PathAccess`].
2221
///
2322
/// On Windows, tracked paths are NT Object Manager paths (`\??` prefix),

crates/native_str/Cargo.toml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[package]
2+
name = "native_str"
3+
version = "0.0.0"
4+
edition.workspace = true
5+
license.workspace = true
6+
publish = false
7+
rust-version.workspace = true
8+
9+
[dependencies]
10+
allocator-api2 = { workspace = true }
11+
bytemuck = { workspace = true, features = ["must_cast", "derive"] }
12+
wincode = { workspace = true }
13+
14+
[lints]
15+
workspace = true
16+
17+
[lib]
18+
doctest = false

crates/native_str/README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# native_str
2+
3+
A platform-native string type for lossless, zero-copy IPC.
4+
5+
`NativeStr` is a `#[repr(transparent)]` newtype over `[u8]` that represents OS strings in their native encoding:
6+
7+
- **Unix**: raw bytes (same as `OsStr`)
8+
- **Windows**: raw wide character bytes (from `&[u16]`, stored as `&[u8]` for uniform handling)
9+
10+
## Why not `OsStr`?
11+
12+
`OsStr` requires valid UTF-8 for serialization. `NativeStr` can be serialized/deserialized losslessly regardless of encoding, with zero-copy support via wincode's `SchemaRead`.
13+
14+
## Limitations
15+
16+
**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.
17+
18+
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.
19+
20+
## Usage
21+
22+
```rust
23+
use native_str::NativeStr;
24+
25+
// Unix: construct from bytes
26+
#[cfg(unix)]
27+
let s: &NativeStr = NativeStr::from_bytes(b"/tmp/foo");
28+
29+
// Windows: construct from wide chars
30+
#[cfg(windows)]
31+
let s: &NativeStr = NativeStr::from_wide(&[0x0048, 0x0069]); // "Hi"
32+
33+
// Convert back to OsStr/OsString
34+
let os = s.to_cow_os_str();
35+
```
Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,24 @@ use wincode::{
1919
io::{Reader, Writer},
2020
};
2121

22-
/// Similar to `OsStr`, but
22+
/// A platform-native string type for lossless, zero-copy IPC.
23+
///
24+
/// Similar to [`OsStr`], but:
2325
/// - Can be infallibly and losslessly encoded/decoded using wincode.
24-
/// (`SchemaWrite`/`SchemaRead` implementations for `OsStr` requires it to be valid UTF-8. This does not.)
26+
/// (`SchemaWrite`/`SchemaRead` implementations for `OsStr` require it to be valid UTF-8. This does not.)
2527
/// - Can be constructed from wide characters on Windows with zero copy.
2628
/// - Supports zero-copy `SchemaRead`.
29+
///
30+
/// # Platform representation
31+
///
32+
/// - **Unix**: raw bytes of the `OsStr`.
33+
/// - **Windows**: raw bytes transmuted from `&[u16]` (wide chars). See `to_os_string` for decoding.
34+
///
35+
/// # Limitations
36+
///
37+
/// **Not portable across platforms.** The binary representation is platform-specific.
38+
/// Deserializing a `NativeStr` serialized on a different platform leads to unspecified
39+
/// behavior (garbage data), but is not unsafe. Designed for same-platform IPC only.
2740
#[derive(TransparentWrapper, PartialEq, Eq)]
2841
#[repr(transparent)]
2942
pub struct NativeStr {
@@ -73,6 +86,23 @@ impl NativeStr {
7386
#[cfg(unix)]
7487
return Cow::Borrowed(self.as_os_str());
7588
}
89+
90+
pub fn clone_in<'new_alloc, A>(&self, alloc: &'new_alloc A) -> &'new_alloc Self
91+
where
92+
&'new_alloc A: Allocator,
93+
{
94+
use allocator_api2::vec::Vec;
95+
let mut data = Vec::<u8, _>::with_capacity_in(self.data.len(), alloc);
96+
data.extend_from_slice(&self.data);
97+
let data = data.leak::<'new_alloc>();
98+
Self::wrap_ref(data)
99+
}
100+
}
101+
102+
impl Debug for NativeStr {
103+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104+
<OsStr as Debug>::fmt(self.to_cow_os_str().as_ref(), f)
105+
}
76106
}
77107

78108
// Manual impl: wincode derive requires Sized, but NativeStr wraps unsized [u8].
@@ -89,12 +119,6 @@ unsafe impl<C: Config> SchemaWrite<C> for NativeStr {
89119
}
90120
}
91121

92-
impl Debug for NativeStr {
93-
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94-
<OsStr as Debug>::fmt(self.to_cow_os_str().as_ref(), f)
95-
}
96-
}
97-
98122
// SchemaRead for &NativeStr: zero-copy borrow from input bytes
99123
// SAFETY: Delegates to `&[u8]`'s SchemaRead impl; dst is initialized on Ok.
100124
unsafe impl<'de, C: Config> SchemaRead<'de, C> for &'de NativeStr {
@@ -159,19 +183,6 @@ impl<S: AsRef<OsStr>> From<S> for Box<NativeStr> {
159183
}
160184
}
161185

162-
impl NativeStr {
163-
pub fn clone_in<'new_alloc, A>(&self, alloc: &'new_alloc A) -> &'new_alloc Self
164-
where
165-
&'new_alloc A: Allocator,
166-
{
167-
use allocator_api2::vec::Vec;
168-
let mut data = Vec::<u8, _>::with_capacity_in(self.data.len(), alloc);
169-
data.extend_from_slice(&self.data);
170-
let data = data.leak::<'new_alloc>();
171-
Self::wrap_ref(data)
172-
}
173-
}
174-
175186
#[cfg(test)]
176187
mod tests {
177188
#[cfg(windows)]

0 commit comments

Comments
 (0)