Skip to main content

wtf_string/
string.rs

1// Copyright (c) 2026 Mike Grier
2//! The owned [`WtfString`] and borrowed [`WtfStr`] string types.
3
4use alloc::borrow::ToOwned;
5use alloc::string::String;
6use alloc::vec;
7use alloc::vec::Vec;
8use core::borrow::Borrow;
9use core::cmp::Ordering;
10use core::fmt::{self, Debug, Display, Formatter};
11use core::hash::{Hash, Hasher};
12use core::ops::Deref;
13
14use crate::encoding::{Wtf8, Wtf16, WtfEncoding};
15
16/// A borrowed string slice of code units in encoding `E` (the analog of
17/// [`OsStr`](std::ffi::OsStr) / [`str`]).
18///
19/// This is `#[repr(transparent)]` over `[E::Unit]`, so a `&WtfStr<E>` can be
20/// created from a `&[E::Unit]` without copying. The units are the string's
21/// *content*: there is no terminator here, since the always-terminated invariant
22/// is a property of the owned [`WtfString`], not of an arbitrary borrowed slice.
23#[repr(transparent)]
24pub struct WtfStr<E: WtfEncoding> {
25    units: [E::Unit],
26}
27
28impl<E: WtfEncoding> WtfStr<E> {
29    /// Wrap a slice of code units as a `&WtfStr<E>` without copying.
30    #[must_use]
31    pub fn from_units(units: &[E::Unit]) -> &WtfStr<E> {
32        // SAFETY: `WtfStr<E>` is `#[repr(transparent)]` over `[E::Unit]`, so the
33        // two have identical layout and the slice's length metadata carries over.
34        unsafe { &*(units as *const [E::Unit] as *const WtfStr<E>) }
35    }
36
37    /// The content code units (there is no terminator on a borrowed slice).
38    #[must_use]
39    pub fn as_units(&self) -> &[E::Unit] {
40        &self.units
41    }
42
43    /// The number of content code units (not bytes, not code points).
44    #[must_use]
45    pub fn len(&self) -> usize {
46        self.units.len()
47    }
48
49    /// Whether the string has no content units.
50    #[must_use]
51    pub fn is_empty(&self) -> bool {
52        self.units.is_empty()
53    }
54
55    /// Whether the content contains a NUL (`E::NUL`) code unit.
56    ///
57    /// For the `Wtf16` arm, a terminated `LPCWSTR` view of an owned string is a
58    /// valid C string only when this is `false` (see
59    /// [`Wtf16String::as_terminated_ptr`]); counted access is always valid
60    /// regardless of this encoding's storage width.
61    #[must_use]
62    pub fn has_interior_nul(&self) -> bool {
63        self.units.contains(&E::NUL)
64    }
65
66    /// Decode to a `String` if the content is well-formed for this encoding.
67    ///
68    /// Returns `None` for content a strict `String` cannot hold (e.g. an unpaired
69    /// surrogate in WTF-16); use [`to_string_lossy`](Self::to_string_lossy) to
70    /// decode with replacement instead.
71    #[must_use]
72    pub fn to_string_checked(&self) -> Option<String> {
73        E::decode(self.as_units())
74    }
75
76    /// Decode to a `String`, replacing any ill-formed sequence with `U+FFFD`.
77    #[must_use]
78    pub fn to_string_lossy(&self) -> String {
79        E::decode_lossy(self.as_units())
80    }
81}
82
83/// An owned, growable string of code units in encoding `E` (the analog of
84/// [`OsString`](std::ffi::OsString) / [`String`]).
85///
86/// The backing buffer always carries a trailing `E::NUL` beyond the logical
87/// content, so content access (via [`Deref`] to [`WtfStr`]) excludes the
88/// terminator while a width-specific FFI surface can still reach it with no
89/// extra allocation -- for the `Wtf16` arm this is a terminated pointer for wide
90/// (`*W`) Win32 APIs (see [`Wtf16String::as_terminated_ptr`]). Content may itself
91/// contain interior NULs (parity with [`OsString`](std::ffi::OsString)); see
92/// [`WtfStr::has_interior_nul`].
93pub struct WtfString<E: WtfEncoding> {
94    // Invariant: non-empty; `units[..units.len() - 1]` is the content and the
95    // final element is the always-present `E::NUL` terminator.
96    units: Vec<E::Unit>,
97}
98
99impl<E: WtfEncoding> WtfString<E> {
100    /// Create an empty string (a buffer holding only the terminator).
101    #[must_use]
102    pub fn new() -> Self {
103        WtfString {
104            units: vec![E::NUL],
105        }
106    }
107
108    /// Create an owned string from content code units, appending the terminator.
109    #[must_use]
110    pub fn from_units(units: &[E::Unit]) -> Self {
111        let capacity = units.len().checked_add(1).expect("capacity overflow");
112        let mut buf = Vec::with_capacity(capacity);
113        buf.extend_from_slice(units);
114        buf.push(E::NUL);
115        WtfString { units: buf }
116    }
117
118    /// The content code units, excluding the terminator.
119    fn content(&self) -> &[E::Unit] {
120        // The invariant guarantees at least the terminator element is present.
121        &self.units[..self.units.len() - 1]
122    }
123
124    /// Build an owned string from already-encoded content units by appending the
125    /// terminator. The encoded vector becomes the backing buffer; the final `push`
126    /// may reallocate if it had no spare capacity.
127    fn from_encoded(mut units: Vec<E::Unit>) -> Self {
128        units.push(E::NUL);
129        WtfString { units }
130    }
131
132    /// Consume the string and decode it to a `String` if its content is
133    /// well-formed for this encoding, otherwise return the original unchanged.
134    ///
135    /// The native-`u16` analog of
136    /// [`OsString::into_string`](std::ffi::OsString::into_string).
137    pub fn into_string(self) -> Result<String, Self> {
138        match E::decode(self.content()) {
139            Some(s) => Ok(s),
140            None => Err(self),
141        }
142    }
143
144    /// Append `s`'s code units after the current content, re-establishing the
145    /// terminator (the [`OsString::push`](std::ffi::OsString::push) analog).
146    ///
147    /// There is deliberately no `truncate`/`pop`/indexed edit: like `OsString`,
148    /// the content is opaque code units that may be ill-formed (D-4/D-15), so an
149    /// arbitrary byte/unit-offset edit could split a multi-unit sequence into
150    /// content with no way to detect the damage afterward (D-16).
151    pub fn push<S: AsRef<WtfStr<E>>>(&mut self, s: S) {
152        let new_units = s.as_ref().as_units();
153        self.units.reserve(new_units.len());
154        // The invariant guarantees the terminator is present to pop.
155        self.units.pop();
156        self.units.extend_from_slice(new_units);
157        self.units.push(E::NUL);
158    }
159
160    /// Encode `s` and append it, re-establishing the terminator: the
161    /// `str`-ergonomic sibling of [`push`](Self::push), for callers without an
162    /// existing [`WtfStr<E>`] to hand.
163    pub fn push_str(&mut self, s: &str) {
164        self.push(WtfStr::from_units(&E::encode_str(s)));
165    }
166
167    /// Truncate to empty, re-establishing the terminator (the
168    /// [`OsString::clear`](std::ffi::OsString::clear) analog).
169    pub fn clear(&mut self) {
170        self.units.clear();
171        self.units.push(E::NUL);
172    }
173
174    /// The number of content code units this string can hold without
175    /// reallocating (excluding the always-reserved terminator slot); the
176    /// [`OsString::capacity`](std::ffi::OsString::capacity) analog.
177    #[must_use]
178    pub fn capacity(&self) -> usize {
179        // The terminator always occupies one slot of the buffer's capacity, so it
180        // is never counted as available content capacity (mirrors `len()`).
181        self.units.capacity() - 1
182    }
183
184    /// Reserve capacity for at least `additional` more content code units
185    /// without reallocating; the
186    /// [`OsString::reserve`](std::ffi::OsString::reserve) analog.
187    pub fn reserve(&mut self, additional: usize) {
188        self.units.reserve(additional);
189    }
190
191    /// Reserve capacity for exactly `additional` more content code units
192    /// (modulo allocator granularity); the
193    /// [`OsString::reserve_exact`](std::ffi::OsString::reserve_exact) analog.
194    pub fn reserve_exact(&mut self, additional: usize) {
195        self.units.reserve_exact(additional);
196    }
197
198    /// Shrink the backing buffer's capacity to fit its current content (plus the
199    /// terminator); the
200    /// [`OsString::shrink_to_fit`](std::ffi::OsString::shrink_to_fit) analog.
201    pub fn shrink_to_fit(&mut self) {
202        self.units.shrink_to_fit();
203    }
204
205    /// Shrink the backing buffer's capacity to hold at least `min_capacity`
206    /// content code units (plus the terminator), never growing it; the
207    /// [`OsString::shrink_to`](std::ffi::OsString::shrink_to) analog.
208    pub fn shrink_to(&mut self, min_capacity: usize) {
209        let min_capacity = min_capacity.checked_add(1).expect("capacity overflow");
210        self.units.shrink_to(min_capacity);
211    }
212}
213
214impl<E: WtfEncoding> Deref for WtfString<E> {
215    type Target = WtfStr<E>;
216
217    fn deref(&self) -> &WtfStr<E> {
218        WtfStr::from_units(self.content())
219    }
220}
221
222impl<E: WtfEncoding> AsRef<WtfStr<E>> for WtfString<E> {
223    fn as_ref(&self) -> &WtfStr<E> {
224        self
225    }
226}
227
228impl<E: WtfEncoding> AsRef<WtfStr<E>> for WtfStr<E> {
229    fn as_ref(&self) -> &WtfStr<E> {
230        self
231    }
232}
233
234impl<E: WtfEncoding> Borrow<WtfStr<E>> for WtfString<E> {
235    fn borrow(&self) -> &WtfStr<E> {
236        self
237    }
238}
239
240impl<E: WtfEncoding> ToOwned for WtfStr<E> {
241    type Owned = WtfString<E>;
242
243    fn to_owned(&self) -> WtfString<E> {
244        WtfString::from_units(self.as_units())
245    }
246}
247
248impl<E: WtfEncoding> Default for WtfString<E> {
249    fn default() -> Self {
250        Self::new()
251    }
252}
253
254impl<E: WtfEncoding> Clone for WtfString<E> {
255    fn clone(&self) -> Self {
256        WtfString {
257            units: self.units.clone(),
258        }
259    }
260}
261
262impl<E: WtfEncoding> From<&str> for WtfString<E> {
263    fn from(s: &str) -> Self {
264        Self::from_encoded(E::encode_str(s))
265    }
266}
267
268impl<E: WtfEncoding> From<String> for WtfString<E> {
269    fn from(s: String) -> Self {
270        Self::from_encoded(E::encode_str(&s))
271    }
272}
273
274impl<E: WtfEncoding> Display for WtfStr<E> {
275    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
276        Display::fmt(&self.to_string_lossy(), f)
277    }
278}
279
280impl<E: WtfEncoding> Display for WtfString<E> {
281    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
282        Display::fmt(&**self, f)
283    }
284}
285
286impl<E: WtfEncoding> Debug for WtfStr<E> {
287    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
288        E::debug_fmt(self.as_units(), f)
289    }
290}
291
292impl<E: WtfEncoding> Debug for WtfString<E> {
293    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
294        Debug::fmt(&**self, f)
295    }
296}
297
298// Ordering, equality, and hashing are a binary comparison of the content code
299// units, so they are the same across every encoding.
300
301impl<E: WtfEncoding> PartialEq for WtfStr<E> {
302    fn eq(&self, other: &Self) -> bool {
303        self.units == other.units
304    }
305}
306
307impl<E: WtfEncoding> Eq for WtfStr<E> {}
308
309impl<E: WtfEncoding> Ord for WtfStr<E> {
310    fn cmp(&self, other: &Self) -> Ordering {
311        self.units.cmp(&other.units)
312    }
313}
314
315impl<E: WtfEncoding> PartialOrd for WtfStr<E> {
316    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
317        Some(self.cmp(other))
318    }
319}
320
321impl<E: WtfEncoding> Hash for WtfStr<E> {
322    fn hash<H: Hasher>(&self, state: &mut H) {
323        self.units.hash(state);
324    }
325}
326
327impl<E: WtfEncoding> PartialEq for WtfString<E> {
328    fn eq(&self, other: &Self) -> bool {
329        **self == **other
330    }
331}
332
333impl<E: WtfEncoding> Eq for WtfString<E> {}
334
335impl<E: WtfEncoding> Ord for WtfString<E> {
336    fn cmp(&self, other: &Self) -> Ordering {
337        (**self).cmp(&**other)
338    }
339}
340
341impl<E: WtfEncoding> PartialOrd for WtfString<E> {
342    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
343        Some(self.cmp(other))
344    }
345}
346
347impl<E: WtfEncoding> Hash for WtfString<E> {
348    fn hash<H: Hasher>(&self, state: &mut H) {
349        (**self).hash(state);
350    }
351}
352
353// Cross-type comparison with `str`: a `str` is encoded to units and compared
354// exactly, so a `WtfStr` holding ill-formed units is never equal to any `str`.
355
356impl<E: WtfEncoding> PartialEq<str> for WtfStr<E> {
357    fn eq(&self, other: &str) -> bool {
358        E::eq_str(self.as_units(), other)
359    }
360}
361
362impl<E: WtfEncoding> PartialEq<&str> for WtfStr<E> {
363    fn eq(&self, other: &&str) -> bool {
364        *self == **other
365    }
366}
367
368impl<E: WtfEncoding> PartialEq<str> for WtfString<E> {
369    fn eq(&self, other: &str) -> bool {
370        **self == *other
371    }
372}
373
374impl<E: WtfEncoding> PartialEq<&str> for WtfString<E> {
375    fn eq(&self, other: &&str) -> bool {
376        **self == **other
377    }
378}
379
380/// A [`WtfString`] whose storage is WTF-16 (`u16` code units).
381pub type Wtf16String = WtfString<Wtf16>;
382
383/// A [`WtfStr`] whose storage is WTF-16 (`u16` code units).
384pub type Wtf16Str = WtfStr<Wtf16>;
385
386/// A [`WtfString`] whose storage is WTF-8 (`u8` code units).
387pub type Wtf8String = WtfString<Wtf8>;
388
389/// A [`WtfStr`] whose storage is WTF-8 (`u8` code units).
390pub type Wtf8Str = WtfStr<Wtf8>;
391
392// FFI surface specific to WTF-16 storage: `*const u16` is the `windows-sys`
393// `PCWSTR`/`LPCWSTR` shape (D-10). These live on the concrete instantiations
394// because a raw `u16` pointer only makes sense for the `Wtf16` width (D-2).
395
396impl Wtf16Str {
397    /// A pointer to the content code units, for counted FFI paired with
398    /// [`len`](WtfStr::len).
399    ///
400    /// The pointer is **not** guaranteed to be NUL-terminated (a borrowed slice
401    /// carries no terminator); use it only with the matching unit count. It is
402    /// valid while `self` is borrowed and unmodified. For a terminated
403    /// `LPCWSTR`, start from an owned [`Wtf16String`] and use
404    /// [`Wtf16String::as_terminated_ptr`].
405    #[must_use]
406    pub fn as_ptr(&self) -> *const u16 {
407        self.as_units().as_ptr()
408    }
409}
410
411impl Wtf16String {
412    /// A NUL-terminated `*const u16` (`LPCWSTR`/`PCWSTR`) over the whole buffer.
413    ///
414    /// The always-present terminator makes this allocation-free (D-7). It is a
415    /// valid C string only when [`has_interior_nul`](WtfStr::has_interior_nul) is
416    /// `false`; otherwise a reader stops at the first interior NUL. The pointer is
417    /// valid while `self` is borrowed and unmodified.
418    #[must_use]
419    pub fn as_terminated_ptr(&self) -> *const u16 {
420        // The buffer is `[content.., NUL]`, so its first element is the start of
421        // a terminated string.
422        self.units.as_ptr()
423    }
424
425    /// An empty string with room for `units` content code units to be filled in
426    /// place via [`as_mut_ptr`](Self::as_mut_ptr) plus
427    /// [`set_len_from_ffi`](Self::set_len_from_ffi).
428    ///
429    /// The reserved capacity also covers the always-present terminator, so a
430    /// later [`set_len_from_ffi`](Self::set_len_from_ffi) of up to `units` content
431    /// units re-establishes the invariant without reallocating (D-9).
432    #[must_use]
433    pub fn with_capacity(units: usize) -> Self {
434        // Reserve content + terminator up front, then seed the empty-string
435        // invariant `[NUL]`; the spare capacity is where a foreign buffer-fill
436        // writes. `checked_add` guards the `usize::MAX` edge that would otherwise
437        // wrap to a tiny allocation in release builds.
438        let capacity = units.checked_add(1).expect("capacity overflow");
439        let mut buf = Vec::with_capacity(capacity);
440        buf.push(Wtf16::NUL);
441        WtfString { units: buf }
442    }
443
444    /// A mutable pointer to the start of the buffer, for a foreign buffer-fill.
445    ///
446    /// [`with_capacity`](Self::with_capacity)`(n)` reserves `n + 1` units: room
447    /// for `n` content units plus the terminator slot. A foreign API may fill up
448    /// to `n` content units, and one more if it writes its own terminator into
449    /// the reserved slot (`n + 1` units total). Either way, pass only the
450    /// **content** length to [`set_len_from_ffi`](Self::set_len_from_ffi), which
451    /// publishes that length and re-establishes the terminator.
452    ///
453    /// Writing through this pointer overwrites the buffer -- including element 0,
454    /// which is the sole terminator of a fresh `with_capacity` -- so it **breaks
455    /// the always-terminated invariant** until
456    /// [`set_len_from_ffi`](Self::set_len_from_ffi) restores it. Between the write
457    /// and that call the value must **not** be observed through any other method
458    /// ([`as_terminated_ptr`](Self::as_terminated_ptr), [`Deref`] content access,
459    /// `Clone`, `Debug`, `PartialEq`, ...): they could read a non-terminated or
460    /// partially written buffer. This holds on **failure paths too** -- if the
461    /// foreign call fails, restore the invariant with `set_len_from_ffi(0)` (the
462    /// empty string) or drop the value before any other use. The pointer is valid
463    /// while `self` is borrowed and not reallocated.
464    #[must_use]
465    pub fn as_mut_ptr(&mut self) -> *mut u16 {
466        self.units.as_mut_ptr()
467    }
468
469    /// Publish `content_units` content code units written into the buffer from
470    /// [`as_mut_ptr`](Self::as_mut_ptr), then append the terminator.
471    ///
472    /// `content_units` counts **content only** and never includes a terminator.
473    /// The written units are taken verbatim -- they may themselves end in `NUL`,
474    /// since interior NULs are permitted (see
475    /// [`has_interior_nul`](WtfStr::has_interior_nul)) -- and exactly one
476    /// terminator is appended. A foreign API that reports a count *including* the
477    /// terminator it wrote must subtract one and pass the content length; this
478    /// method never inspects the buffer to guess the convention, so a genuine
479    /// trailing content `NUL` is never mistaken for the terminator.
480    ///
481    /// # Safety
482    ///
483    /// The caller must guarantee that:
484    /// - the first `content_units` code units at [`as_mut_ptr`](Self::as_mut_ptr)
485    ///   are initialized `u16` values, and
486    /// - `content_units` does not exceed the count requested via
487    ///   [`with_capacity`](Self::with_capacity), so the appended terminator fits
488    ///   without reallocating a buffer whose pointer the caller may still hold.
489    pub unsafe fn set_len_from_ffi(&mut self, content_units: usize) {
490        // `content_units < capacity` guards both `set_len` soundness and the room
491        // to append the terminator without reallocating (which would strand a
492        // pointer handed out via `as_mut_ptr`).
493        debug_assert!(
494            content_units < self.units.capacity(),
495            "set_len_from_ffi content length leaves no room for the terminator"
496        );
497        // SAFETY: the caller guarantees `content_units` initialized code units,
498        // within capacity, so this length names only initialized storage.
499        unsafe { self.units.set_len(content_units) };
500        self.units.push(Wtf16::NUL);
501    }
502
503    /// Copy `len` content code units from a foreign `*const u16` into a new owned
504    /// string, appending the terminator.
505    ///
506    /// For callee-allocated Win32 output: the bytes are **copied**, so the caller
507    /// keeps ownership of (and remains responsible for freeing) the source buffer.
508    /// The copy is lossless — arbitrary WTF-16, including unpaired surrogates, is
509    /// preserved (D-4/D-9).
510    ///
511    /// # Safety
512    ///
513    /// This copies the range through `core::slice::from_raw_parts` and shares its
514    /// preconditions. When `len > 0` the caller must guarantee that:
515    /// - `ptr` is non-null and properly aligned for `u16`;
516    /// - `ptr` is valid for reads of `len` consecutive, initialized `u16` values,
517    ///   all contained within a **single allocated object**;
518    /// - the total size `len * size_of::<u16>()` is no larger than `isize::MAX`,
519    ///   and adding it to `ptr` does not wrap the address space; and
520    /// - that region stays unmutated for the duration of the call.
521    ///
522    /// When `len == 0` the pointer is not dereferenced, so it may be null or
523    /// dangling. No reference to `ptr` is retained past the call. `len` is a
524    /// **count of code units**, not bytes, and excludes any terminator the callee
525    /// may have written (pass the content length).
526    #[must_use]
527    pub unsafe fn from_wide_ptr(ptr: *const u16, len: usize) -> Self {
528        if len == 0 {
529            // `slice::from_raw_parts` forbids a null (or dangling) pointer even at
530            // zero length, so an empty result must not touch `ptr` at all.
531            return Self::new();
532        }
533        // SAFETY: `len > 0`, and the caller guarantees `ptr` is non-null, valid,
534        // and aligned for `len` reads; the slice is used only to copy and is not
535        // retained.
536        let content = unsafe { core::slice::from_raw_parts(ptr, len) };
537        Self::from_units(content)
538    }
539}
540
541// Windows `OsStr` / `OsString` interop is the only platform-gated surface (D-5).
542// It also needs `std`: `OsStr`/`OsString` have no `alloc`-only equivalent (D-11).
543#[cfg(all(windows, feature = "std"))]
544mod os_str;
545
546// `windows`-crate `Param<PCWSTR>` interop, off unless the feature is on (D-10).
547// Gated on the feature alone, not on `cfg(windows)`: it builds on the portable
548// terminated-pointer surface, so it compiles wherever `windows-core` does.
549#[cfg(feature = "windows-core")]
550mod param;
551
552#[cfg(test)]
553mod tests;