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;