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}