wtf_string/lib.rs
1// Copyright (c) 2026 Mike Grier
2//! `OsString`-shaped strings with native, conversion-free `u16` storage.
3//!
4//! Rust's [`OsString`](std::ffi::OsString) stores **WTF-8** on Windows, so every
5//! call into a wide (`*W`) Win32 API pays a WTF-8 -> UTF-16 re-encode and an
6//! allocation on the way out, and the reverse on the way in. For code that lives
7//! in `u16` and calls Windows APIs repeatedly, that conversion is pure overhead.
8//!
9//! This crate provides an `OsString`-shaped type whose canonical storage is
10//! `[u16]` (**WTF-16**: arbitrary, ill-formed-surrogate-tolerant UTF-16, exactly
11//! what NTFS and the Win32 APIs traffic in), so the conversion happens **once at
12//! the boundary from `str`/`OsStr`** and never again. The analog of
13//! `OsStrExt::encode_wide` becomes a zero-copy borrow of our own slice.
14//!
15//! # Shape
16//!
17//! The core is generic over a [code-unit encoding][WtfEncoding]: `WtfString<E>`
18//! owns the units and `WtfStr<E>` is the borrowed slice. Two encodings ship: the
19//! `Wtf16` width (aliases `Wtf16String` / `Wtf16Str`) and the `Wtf8` width
20//! (aliases `Wtf8String` / `Wtf8Str`), a `u8`/WTF-8 storage variant whose
21//! encode/decode, comparison, and formatting semantics this crate defines, backed
22//! by a crate-owned `Vec<u8>` (its WTF-8 storage matches `OsString`'s, but the arm
23//! is not built on `OsString`).
24//!
25//! The storage and `str`/`String` conversions are portable; the `OsStr` /
26//! `OsString` interop is Windows-only.
27//!
28//! ```
29//! use wtf_string::Wtf16String;
30//!
31//! // Encode once, at the boundary.
32//! let s = Wtf16String::from("C:\\Windows");
33//!
34//! // From here on the units are the storage: no re-encode, no allocation.
35//! assert_eq!(s.len(), 10);
36//! assert_eq!(s.as_units()[0], u16::from(b'C'));
37//!
38//! // And back again when a `String` is genuinely wanted.
39//! assert_eq!(s.to_string_lossy(), "C:\\Windows");
40//! ```
41//!
42//! # Conversion costs
43//!
44//! The point of the crate is that the middle rows are free. Everything that
45//! costs anything is a boundary crossing you asked for:
46//!
47//! | Operation | Cost |
48//! |---|---|
49//! | `Wtf16String::from(&str)` / `from(String)` | encode + allocate, once |
50//! | `Wtf16String::from_os_str` (Windows) | encode + allocate, once |
51//! | [`as_ptr`](Wtf16Str::as_ptr) + [`len`](WtfStr::len), for a counted `*W` call | **free** |
52//! | [`as_terminated_ptr`](Wtf16String::as_terminated_ptr), for an `LPCWSTR` call | **free** |
53//! | `encode_wide` (Windows), the `OsStrExt` analog | **free** (borrows our slice) |
54//! | [`as_units`](WtfStr::as_units) / [`len`](WtfStr::len) / comparison / hashing | **free** |
55//! | [`to_string_checked`](WtfStr::to_string_checked) / [`to_string_lossy`](WtfStr::to_string_lossy) | decode + allocate |
56//! | `to_os_string` (Windows) | re-encode + allocate |
57//!
58//! Compare `OsString`, where the *first* two rows are free and the wide-call
59//! rows are the ones that re-encode. The two types are mirror images; which one
60//! is right depends on whether your code spends its time in `str` or in Win32.
61//!
62//! # The FFI surface
63//!
64//! Both directions of a Win32 call are covered without leaving `u16`.
65//!
66//! **Input** -- pick the convention the signature wants:
67//!
68//! - counted (`ptr` + `cch`): [`as_ptr`](Wtf16Str::as_ptr) with
69//! [`len`](WtfStr::len);
70//! - NUL-terminated (`LPCWSTR` / `PCWSTR`):
71//! [`as_terminated_ptr`](Wtf16String::as_terminated_ptr). The terminator is
72//! always present in the owned buffer, so this never allocates.
73//!
74//! **Output** -- pick the shape the API uses:
75//!
76//! - caller-allocated buffer-fill: [`with_capacity`](Wtf16String::with_capacity),
77//! [`as_mut_ptr`](Wtf16String::as_mut_ptr), then
78//! [`set_len_from_ffi`](Wtf16String::set_len_from_ffi) to publish the content
79//! length the API reported;
80//! - callee-allocated buffer: [`from_wide_ptr`](Wtf16String::from_wide_ptr),
81//! which copies, leaving the caller to free the original.
82//!
83//! See `examples/win32_round_trip.rs` for both halves against a real Win32 call.
84//!
85//! # Interior NULs
86//!
87//! Content may contain NUL, matching `OsString` and the underlying WTF model.
88//! The trailing terminator is *storage*, not content: it is excluded from
89//! [`len`](WtfStr::len), [`as_units`](WtfStr::as_units), comparison and hashing.
90//!
91//! One consequence is worth knowing before using the terminated pointer: a C
92//! string ends at the first NUL, so a callee reading
93//! [`as_terminated_ptr`](Wtf16String::as_terminated_ptr) sees a value with an
94//! interior NUL *truncated*. Counted access is always exact.
95//! [`has_interior_nul`](WtfStr::has_interior_nul) reports the condition when it
96//! matters.
97//!
98//! A checked, no-interior-NUL companion type -- one that makes "this really is a
99//! valid C string" a type-level guarantee rather than a precondition to check --
100//! is a **reserved** seam for a future release, deliberately outside the v1
101//! surface. Until then, pair `has_interior_nul` with the terminated pointer, or
102//! use the counted pair, which is never ambiguous.
103//!
104//! # Features
105//!
106//! - **`std`** (on by default) -- adds the Windows `OsStr` / `OsString` interop.
107//! `OsStr` lives in `std` and has no `alloc`-only equivalent, so it is the one
108//! part of the crate that needs it. With this feature off the crate is
109//! `no_std` + `alloc`: storage, `str` / `String` conversions, the mutation
110//! surface and the whole FFI pointer surface all still work.
111//! - **`windows-core`** (off by default) -- implements the high-level `windows`
112//! crate's `Param<PCWSTR>` for `&Wtf16String`, so a `windows` API taking
113//! `impl Param<PCWSTR>` accepts our type directly, handing over the
114//! already-terminated pointer with no conversion, allocation or copy. Raw
115//! `windows-sys` signatures need no feature: they take `*const u16`, which
116//! [`Wtf16String::as_terminated_ptr`] already provides. The impl is written
117//! against one `windows-core` version, so a caller must resolve to that same
118//! semver-compatible version for it to apply.
119//!
120//! The crate is `#![no_std]` at its root and pulls in `alloc`. Unless the
121//! `windows-core` feature is on, it has **zero dependencies**.
122//!
123//! See the crate's design records for the full set of decisions.
124
125#![no_std]
126#![warn(missing_docs)]
127
128extern crate alloc;
129
130// `std` is linked when the `std` feature is on (the `OsStr` interop needs it),
131// under `cfg(test)` (the harness and tests use `HashMap`, `DefaultHasher`, ...),
132// and under `cfg(doc)` so intra-doc links to `std` items resolve even in an
133// `alloc`-only documentation build. The portable core itself never uses it.
134#[cfg(any(feature = "std", test, doc))]
135extern crate std;
136
137mod encoding;
138mod string;
139
140pub use encoding::{Wtf8, Wtf16, WtfEncoding};
141pub use string::{Wtf8Str, Wtf8String, Wtf16Str, Wtf16String, WtfStr, WtfString};