From 25f8e5df7ab1cd6cbbcea66077e9e4cd4dfee233 Mon Sep 17 00:00:00 2001 From: branchseer Date: Thu, 16 Apr 2026 21:23:02 +0800 Subject: [PATCH] refactor: extract NativeStr into standalone crate Move `NativeStr` from `fspy_shared::ipc::native_str` into a new `native_str` crate with its own README. The type is re-exported from `fspy_shared::ipc` so downstream crates are unaffected. Co-Authored-By: Claude Opus 4.6 (1M context) --- Cargo.lock | 10 ++++ Cargo.toml | 1 + crates/fspy_shared/Cargo.toml | 1 + crates/fspy_shared/src/ipc/mod.rs | 2 - crates/fspy_shared/src/ipc/native_path.rs | 3 +- crates/native_str/Cargo.toml | 18 +++++++ crates/native_str/README.md | 35 ++++++++++++ .../native_str.rs => native_str/src/lib.rs} | 53 +++++++++++-------- 8 files changed, 98 insertions(+), 25 deletions(-) create mode 100644 crates/native_str/Cargo.toml create mode 100644 crates/native_str/README.md rename crates/{fspy_shared/src/ipc/native_str.rs => native_str/src/lib.rs} (91%) diff --git a/Cargo.lock b/Cargo.lock index fc3de25e..344e310e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1287,6 +1287,7 @@ dependencies = [ "bstr", "bytemuck", "ctor", + "native_str", "os_str_bytes", "rustc-hash", "shared_memory", @@ -1959,6 +1960,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "native_str" +version = "0.0.0" +dependencies = [ + "allocator-api2", + "bytemuck", + "wincode", +] + [[package]] name = "nix" version = "0.23.2" diff --git a/Cargo.toml b/Cargo.toml index e235e198..5e43a621 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/crates/fspy_shared/Cargo.toml b/crates/fspy_shared/Cargo.toml index 8e074fc4..07f43b6f 100644 --- a/crates/fspy_shared/Cargo.toml +++ b/crates/fspy_shared/Cargo.toml @@ -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 } diff --git a/crates/fspy_shared/src/ipc/mod.rs b/crates/fspy_shared/src/ipc/mod.rs index ef63793a..c7236e5d 100644 --- a/crates/fspy_shared/src/ipc/mod.rs +++ b/crates/fspy_shared/src/ipc/mod.rs @@ -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; diff --git a/crates/fspy_shared/src/ipc/native_path.rs b/crates/fspy_shared/src/ipc/native_path.rs index 6e431c28..26903e9e 100644 --- a/crates/fspy_shared/src/ipc/native_path.rs +++ b/crates/fspy_shared/src/ipc/native_path.rs @@ -9,6 +9,7 @@ use std::{ use allocator_api2::alloc::Allocator; use bytemuck::TransparentWrapper; +use native_str::NativeStr; use wincode::{ SchemaRead, SchemaWrite, config::Config, @@ -16,8 +17,6 @@ use wincode::{ 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), diff --git a/crates/native_str/Cargo.toml b/crates/native_str/Cargo.toml new file mode 100644 index 00000000..2baa2d18 --- /dev/null +++ b/crates/native_str/Cargo.toml @@ -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 diff --git a/crates/native_str/README.md b/crates/native_str/README.md new file mode 100644 index 00000000..426f2888 --- /dev/null +++ b/crates/native_str/README.md @@ -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(); +``` diff --git a/crates/fspy_shared/src/ipc/native_str.rs b/crates/native_str/src/lib.rs similarity index 91% rename from crates/fspy_shared/src/ipc/native_str.rs rename to crates/native_str/src/lib.rs index c4800912..84a841a0 100644 --- a/crates/fspy_shared/src/ipc/native_str.rs +++ b/crates/native_str/src/lib.rs @@ -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 { @@ -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::::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 { + ::fmt(self.to_cow_os_str().as_ref(), f) + } } // Manual impl: wincode derive requires Sized, but NativeStr wraps unsized [u8]. @@ -89,12 +119,6 @@ unsafe impl SchemaWrite for NativeStr { } } -impl Debug for NativeStr { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - ::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 { @@ -159,19 +183,6 @@ impl> From for Box { } } -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::::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)]