zstring/
zstr.rs

1use super::*;
2use core::{cmp::Ordering, fmt::Write, marker::PhantomData, ptr::NonNull};
3
4/// Borrowed and non-null pointer to zero-terminated text data.
5///
6/// Because this is a thin pointer it's suitable for direct FFI usage.
7///
8/// The bytes pointed to *should* be utf-8 encoded, but the [`CharDecoder`] used
9/// to convert the bytes to `char` values is safe to use even when the bytes are
10/// not proper utf-8.
11///
12/// ## Safety
13/// * This is `repr(transparent)` over a [`NonNull<u8>`].
14/// * The wrapped pointer points at a sequence of valid-to-read non-zero byte
15///   values followed by at least one zero byte.
16/// * When you create a `ZStr<'a>` value the pointer must be valid for at least
17///   as long as the lifetime `'a`.
18#[derive(Clone, Copy)]
19#[repr(transparent)]
20pub struct ZStr<'a> {
21  pub(crate) nn: NonNull<u8>,
22  pub(crate) life: PhantomData<&'a [u8]>,
23}
24impl<'a> ZStr<'a> {
25  /// Makes a `ZStr<'static>` from a `&'static str`
26  ///
27  /// This is *intended* for use with string litearls, but if you leak a runtime
28  /// string into a static string I guess that works too.
29  ///
30  /// ```rust
31  /// # use zstring::*;
32  /// const FOO: ZStr<'static> = ZStr::from_lit("foo\0");
33  /// ```
34  ///
35  /// ## Panics
36  /// * If `try_from` would return an error, this will panic instead. Because
37  ///   this is intended for compile time constants, the panic will "just"
38  ///   trigger a build error.
39  #[inline]
40  #[track_caller]
41  pub const fn from_lit(s: &'static str) -> ZStr<'static> {
42    let bytes = s.as_bytes();
43    let mut tail_index = bytes.len() - 1;
44    while bytes[tail_index] == 0 {
45      tail_index -= 1;
46    }
47    assert!(tail_index < bytes.len() - 1, "No trailing nulls.");
48    let mut i = 0;
49    while i < tail_index {
50      if bytes[i] == 0 {
51        panic!("Input contains interior null.");
52      }
53      i += 1;
54    }
55    ZStr {
56      // Safety: References can't ever be null.
57      nn: unsafe { NonNull::new_unchecked(s.as_ptr() as *mut u8) },
58      life: PhantomData,
59    }
60  }
61
62  /// An iterator over the bytes of this `ZStr`.
63  ///
64  /// * This iterator **excludes** the terminating 0 byte.
65  #[inline]
66  pub fn bytes(self) -> impl Iterator<Item = u8> + 'a {
67    // Safety: per the type safety docs, whoever made this `ZStr` promised that
68    // we can read the pointer's bytes until we find a 0 byte.
69    unsafe { ConstPtrIter::read_until_default(self.nn.as_ptr()) }
70  }
71
72  /// An iterator over the decoded `char` values of this `ZStr`.
73  #[inline]
74  pub fn chars(self) -> impl Iterator<Item = char> + 'a {
75    CharDecoder::from(self.bytes())
76  }
77
78  /// Gets the raw pointer to this data.
79  #[inline]
80  #[must_use]
81  pub const fn as_ptr(self) -> *const u8 {
82    self.nn.as_ptr()
83  }
84}
85impl<'a> TryFrom<&'a str> for ZStr<'a> {
86  type Error = ZStringError;
87  /// Converts the value in place.
88  ///
89  /// The trailing nulls of the source `&str` will not "be in" the output
90  /// sequence of the returned `ZStr`.
91  ///
92  /// ```rust
93  /// # use zstring::*;
94  /// let z1 = ZStr::try_from("abcd\0").unwrap();
95  /// assert!(z1.chars().eq("abcd".chars()));
96  ///
97  /// let z2 = ZStr::try_from("abcd\0\0\0").unwrap();
98  /// assert!(z2.chars().eq("abcd".chars()));
99  /// ```
100  ///
101  /// ## Failure
102  /// * There must be at least one trailing null in the input `&str`.
103  /// * There must be no nulls followed by a non-null ("interior nulls"). This
104  ///   second condition is not a strict requirement of the type, more of a
105  ///   correctness lint. If interior nulls were allowed then `"ab\0cd\0"`
106  ///   converted to a `ZStr` would only be read as `"ab"`, and the second half
107  ///   of the string would effectively be erased.
108  #[inline]
109  fn try_from(value: &'a str) -> Result<Self, Self::Error> {
110    let trimmed = value.trim_end_matches('\0');
111    if value.len() == trimmed.len() {
112      Err(ZStringError::NoTrailingNulls)
113    } else if trimmed.contains('\0') {
114      Err(ZStringError::InteriorNulls)
115    } else {
116      // Note: We have verified that the starting `str` value contains at
117      // least one 0 byte.
118      Ok(Self {
119        nn: NonNull::new(value.as_ptr() as *mut u8).unwrap(),
120        life: PhantomData,
121      })
122    }
123  }
124}
125impl core::fmt::Display for ZStr<'_> {
126  /// Display formats the string (without outer `"`).
127  ///
128  /// ```rust
129  /// # use zstring::*;
130  /// const FOO: ZStr<'static> = ZStr::from_lit("foo\0");
131  /// let s = format!("{FOO}");
132  /// assert_eq!(s, "foo");
133  /// ```
134  #[inline]
135  fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
136    for ch in self.chars() {
137      write!(f, "{ch}")?;
138    }
139    Ok(())
140  }
141}
142impl core::fmt::Debug for ZStr<'_> {
143  /// Debug formats with outer `"` around the string.
144  ///
145  /// ```rust
146  /// # use zstring::*;
147  /// const FOO: ZStr<'static> = ZStr::from_lit("foo\0");
148  /// let s = format!("{FOO:?}");
149  /// assert_eq!(s, "\"foo\"");
150  /// ```
151  #[inline]
152  fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
153    f.write_char('"')?;
154    core::fmt::Display::fmt(self, f)?;
155    f.write_char('"')?;
156    Ok(())
157  }
158}
159impl core::fmt::Pointer for ZStr<'_> {
160  /// Formats the wrapped pointer value.
161  #[inline]
162  fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
163    core::fmt::Pointer::fmt(&self.nn, f)
164  }
165}
166
167impl PartialEq<ZStr<'_>> for ZStr<'_> {
168  /// Two `ZStr` are considered equal if they point at the exact same *byte
169  /// sequence*.
170  ///
171  /// This is much faster to compute when the bytes are valid UTF-8, though it
172  /// is stricter if the bytes are not valid UTF-8 (the character replacement
173  /// process during decoding *could* make two different byte sequences have the
174  /// same character sequence).
175  ///
176  /// ```rust
177  /// # use zstring::*;
178  /// const FOO1: ZStr<'static> = ZStr::from_lit("foo\0");
179  /// const FOO2: ZStr<'static> = ZStr::from_lit("foo\0");
180  /// assert_eq!(FOO1, FOO2);
181  /// ```
182  #[inline]
183  #[must_use]
184  fn eq(&self, other: &ZStr<'_>) -> bool {
185    if self.nn == other.nn {
186      true
187    } else {
188      self.bytes().eq(other.bytes())
189    }
190  }
191}
192impl PartialOrd<ZStr<'_>> for ZStr<'_> {
193  /// Compares based on the *byte sequence* pointed to.
194  ///
195  /// ```rust
196  /// # use zstring::*;
197  /// # use core::cmp::{PartialOrd, Ordering};
198  /// const ABC: ZStr<'static> = ZStr::from_lit("abc\0");
199  /// const DEF: ZStr<'static> = ZStr::from_lit("def\0");
200  /// const GHI: ZStr<'static> = ZStr::from_lit("ghi\0");
201  /// assert_eq!(ABC.partial_cmp(&DEF), Some(Ordering::Less));
202  /// assert_eq!(DEF.partial_cmp(&GHI), Some(Ordering::Less));
203  /// assert_eq!(GHI.partial_cmp(&ABC), Some(Ordering::Greater));
204  /// ```
205  #[inline]
206  #[must_use]
207  fn partial_cmp(&self, other: &ZStr<'_>) -> Option<core::cmp::Ordering> {
208    if self.nn == other.nn {
209      Some(Ordering::Equal)
210    } else {
211      Some(self.bytes().cmp(other.bytes()))
212    }
213  }
214}
215
216impl PartialEq<&str> for ZStr<'_> {
217  /// A `ZStr` equals a `&str` if the bytes match.
218  #[inline]
219  #[must_use]
220  fn eq(&self, other: &&str) -> bool {
221    self.bytes().eq(other.as_bytes().iter().copied())
222  }
223}
224impl PartialOrd<&str> for ZStr<'_> {
225  /// Compares based on the *byte sequence* pointed to.
226  #[inline]
227  #[must_use]
228  fn partial_cmp(&self, other: &&str) -> Option<core::cmp::Ordering> {
229    Some(self.bytes().cmp(other.as_bytes().iter().copied()))
230  }
231}
232
233#[cfg(feature = "alloc")]
234impl PartialEq<ZString> for ZStr<'_> {
235  /// A `ZStr` equals a `ZString` by calling `ZString::as_zstr`
236  #[inline]
237  #[must_use]
238  fn eq(&self, other: &ZString) -> bool {
239    self.eq(&other.as_zstr())
240  }
241}
242#[cfg(feature = "alloc")]
243impl PartialOrd<ZString> for ZStr<'_> {
244  /// Compares based on the *byte sequence* pointed to.
245  #[inline]
246  #[must_use]
247  fn partial_cmp(&self, other: &ZString) -> Option<core::cmp::Ordering> {
248    self.partial_cmp(&other.as_zstr())
249  }
250}
251
252impl core::hash::Hash for ZStr<'_> {
253  #[inline]
254  fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
255    for b in self.bytes() {
256      state.write_u8(b)
257    }
258  }
259}
260
261/// An error occurred while trying to make a [`ZStr`] or [`ZString`].
262#[derive(Debug, Clone, Copy, PartialEq, Eq)]
263pub enum ZStringError {
264  /// The provided data didn't have any trailing nulls (`'\0'`).
265  NoTrailingNulls,
266  /// The provided data had interior nulls (non-null data *after* a null).
267  InteriorNulls,
268}