/rust/registry/src/index.crates.io-1949cf8c6b5b557f/tinystr-0.8.3/src/ascii.rs
Line | Count | Source |
1 | | // This file is part of ICU4X. For terms of use, please see the file |
2 | | // called LICENSE at the top level of the ICU4X source tree |
3 | | // (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). |
4 | | |
5 | | use crate::asciibyte::AsciiByte; |
6 | | use crate::int_ops::{Aligned4, Aligned8}; |
7 | | use crate::ParseError; |
8 | | #[cfg(feature = "alloc")] |
9 | | use alloc::string::String; |
10 | | use core::borrow::Borrow; |
11 | | use core::fmt; |
12 | | use core::ops::Deref; |
13 | | use core::str::{self, FromStr}; |
14 | | |
15 | | #[repr(transparent)] |
16 | | #[derive(PartialEq, Eq, Ord, PartialOrd, Copy, Clone, Hash)] |
17 | | pub struct TinyAsciiStr<const N: usize> { |
18 | | bytes: [AsciiByte; N], |
19 | | } |
20 | | |
21 | | impl<const N: usize> TinyAsciiStr<N> { |
22 | | /// The empty string. |
23 | | pub const EMPTY: Self = Self { |
24 | | bytes: [AsciiByte::B0; N], |
25 | | }; |
26 | | |
27 | | #[inline] |
28 | 0 | pub const fn try_from_str(s: &str) -> Result<Self, ParseError> { |
29 | 0 | Self::try_from_utf8(s.as_bytes()) |
30 | 0 | } |
31 | | |
32 | | /// Creates a `TinyAsciiStr<N>` from the given UTF-8 slice. |
33 | | /// `code_units` may contain at most `N` non-null ASCII code points. |
34 | | #[inline] |
35 | 0 | pub const fn try_from_utf8(code_units: &[u8]) -> Result<Self, ParseError> { |
36 | 0 | Self::try_from_utf8_inner(code_units, false) |
37 | 0 | } Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<2>>::try_from_utf8 Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<3>>::try_from_utf8 Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<4>>::try_from_utf8 Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<8>>::try_from_utf8 Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<_>>::try_from_utf8 |
38 | | |
39 | | /// Creates a `TinyAsciiStr<N>` from the given UTF-16 slice. |
40 | | /// `code_units` may contain at most `N` non-null ASCII code points. |
41 | | #[inline] |
42 | 0 | pub const fn try_from_utf16(code_units: &[u16]) -> Result<Self, ParseError> { |
43 | 0 | Self::try_from_utf16_inner(code_units, 0, code_units.len(), false) |
44 | 0 | } |
45 | | |
46 | | /// Creates a `TinyAsciiStr<N>` from a UTF-8 slice, replacing invalid code units. |
47 | | /// |
48 | | /// Invalid code units, as well as null or non-ASCII code points |
49 | | /// (i.e. those outside the range `U+0001..=U+007F`) |
50 | | /// will be replaced with the replacement byte. |
51 | | /// |
52 | | /// The input slice will be truncated if its length exceeds `N`. |
53 | | /// |
54 | | /// # Panics |
55 | | /// |
56 | | /// Panics if `replacement` is not a non-null ASCII byte (i.e. not in `1..=127`). |
57 | 0 | pub const fn from_utf8_lossy(code_units: &[u8], replacement: u8) -> Self { |
58 | 0 | assert!( |
59 | 0 | replacement > 0 && replacement < 0x80, |
60 | 0 | "replacement must be a non-null ASCII byte (1..=127)" |
61 | | ); |
62 | 0 | let mut out = [0; N]; |
63 | 0 | let mut i = 0; |
64 | | // Ord is not available in const, so no `.min(N)` |
65 | 0 | let len = if code_units.len() > N { |
66 | 0 | N |
67 | | } else { |
68 | 0 | code_units.len() |
69 | | }; |
70 | | |
71 | | // Indexing is protected by the len check above |
72 | | #[expect(clippy::indexing_slicing)] |
73 | 0 | while i < len { |
74 | 0 | let b = code_units[i]; |
75 | 0 | if b > 0 && b < 0x80 { |
76 | 0 | out[i] = b; |
77 | 0 | } else { |
78 | 0 | out[i] = replacement; |
79 | 0 | } |
80 | 0 | i += 1; |
81 | | } |
82 | | |
83 | 0 | Self { |
84 | 0 | // SAFETY: `out` only contains ASCII bytes and has same size as `self.bytes` |
85 | 0 | bytes: unsafe { AsciiByte::to_ascii_byte_array(&out) }, |
86 | 0 | } |
87 | 0 | } |
88 | | |
89 | | /// Creates a `TinyAsciiStr<N>` from a UTF-16 slice, replacing invalid code units. |
90 | | /// |
91 | | /// Invalid code units, as well as null or non-ASCII code points |
92 | | /// (i.e. those outside the range `U+0001..=U+007F`) |
93 | | /// will be replaced with the replacement byte. |
94 | | /// |
95 | | /// The input slice will be truncated if its length exceeds `N`. |
96 | | /// |
97 | | /// # Panics |
98 | | /// |
99 | | /// Panics if `replacement` is not a non-null ASCII byte (i.e. not in `1..=127`). |
100 | 0 | pub const fn from_utf16_lossy(code_units: &[u16], replacement: u8) -> Self { |
101 | 0 | assert!( |
102 | 0 | replacement > 0 && replacement < 0x80, |
103 | 0 | "replacement must be a non-null ASCII byte (1..=127)" |
104 | | ); |
105 | 0 | let mut out = [0; N]; |
106 | 0 | let mut i = 0; |
107 | | // Ord is not available in const, so no `.min(N)` |
108 | 0 | let len = if code_units.len() > N { |
109 | 0 | N |
110 | | } else { |
111 | 0 | code_units.len() |
112 | | }; |
113 | | |
114 | | // Indexing is protected by the len check above |
115 | | #[expect(clippy::indexing_slicing)] |
116 | 0 | while i < len { |
117 | 0 | let b = code_units[i]; |
118 | 0 | if b > 0 && b < 0x80 { |
119 | 0 | out[i] = b as u8; |
120 | 0 | } else { |
121 | 0 | out[i] = replacement; |
122 | 0 | } |
123 | 0 | i += 1; |
124 | | } |
125 | | |
126 | 0 | Self { |
127 | 0 | // SAFETY: `out` only contains ASCII bytes and has same size as `self.bytes` |
128 | 0 | bytes: unsafe { AsciiByte::to_ascii_byte_array(&out) }, |
129 | 0 | } |
130 | 0 | } |
131 | | |
132 | | /// Attempts to parse a fixed-length byte array to a `TinyAsciiStr`. |
133 | | /// |
134 | | /// The byte array may contain trailing NUL bytes. |
135 | | /// |
136 | | /// # Example |
137 | | /// |
138 | | /// ``` |
139 | | /// use tinystr::tinystr; |
140 | | /// use tinystr::TinyAsciiStr; |
141 | | /// |
142 | | /// assert_eq!( |
143 | | /// TinyAsciiStr::<3>::try_from_raw(*b"GB\0"), |
144 | | /// Ok(tinystr!(3, "GB")) |
145 | | /// ); |
146 | | /// assert_eq!( |
147 | | /// TinyAsciiStr::<3>::try_from_raw(*b"USD"), |
148 | | /// Ok(tinystr!(3, "USD")) |
149 | | /// ); |
150 | | /// assert!(TinyAsciiStr::<3>::try_from_raw(*b"\0A\0").is_err()); |
151 | | /// ``` |
152 | 0 | pub const fn try_from_raw(raw: [u8; N]) -> Result<Self, ParseError> { |
153 | 0 | Self::try_from_utf8_inner(&raw, true) |
154 | 0 | } Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<2>>::try_from_raw Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<3>>::try_from_raw Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<4>>::try_from_raw Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<8>>::try_from_raw Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<_>>::try_from_raw |
155 | | |
156 | 0 | pub(crate) const fn try_from_utf8_inner( |
157 | 0 | code_units: &[u8], |
158 | 0 | allow_trailing_null: bool, |
159 | 0 | ) -> Result<Self, ParseError> { |
160 | 0 | if code_units.len() > N { |
161 | 0 | return Err(ParseError::TooLong { |
162 | 0 | max: N, |
163 | 0 | len: code_units.len(), |
164 | 0 | }); |
165 | 0 | } |
166 | | |
167 | 0 | let mut out = [0; N]; |
168 | 0 | let mut i = 0; |
169 | 0 | let mut found_null = false; |
170 | | // Indexing is protected by TinyStrError::TooLarge |
171 | | #[expect(clippy::indexing_slicing)] |
172 | 0 | while i < code_units.len() { |
173 | 0 | let b = code_units[i]; |
174 | | |
175 | 0 | if b == 0 { |
176 | 0 | found_null = true; |
177 | 0 | } else if b >= 0x80 { |
178 | 0 | return Err(ParseError::NonAscii); |
179 | 0 | } else if found_null { |
180 | | // Error if there are contentful bytes after null |
181 | 0 | return Err(ParseError::ContainsNull); |
182 | 0 | } |
183 | 0 | out[i] = b; |
184 | | |
185 | 0 | i += 1; |
186 | | } |
187 | | |
188 | 0 | if !allow_trailing_null && found_null { |
189 | | // We found some trailing nulls, error |
190 | 0 | return Err(ParseError::ContainsNull); |
191 | 0 | } |
192 | | |
193 | 0 | Ok(Self { |
194 | 0 | // SAFETY: `out` only contains ASCII bytes and has same size as `self.bytes` |
195 | 0 | bytes: unsafe { AsciiByte::to_ascii_byte_array(&out) }, |
196 | 0 | }) |
197 | 0 | } Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<2>>::try_from_utf8_inner Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<3>>::try_from_utf8_inner Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<4>>::try_from_utf8_inner Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<8>>::try_from_utf8_inner Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<_>>::try_from_utf8_inner |
198 | | |
199 | 0 | pub(crate) const fn try_from_utf16_inner( |
200 | 0 | code_units: &[u16], |
201 | 0 | start: usize, |
202 | 0 | end: usize, |
203 | 0 | allow_trailing_null: bool, |
204 | 0 | ) -> Result<Self, ParseError> { |
205 | 0 | let len = end - start; |
206 | 0 | if len > N { |
207 | 0 | return Err(ParseError::TooLong { max: N, len }); |
208 | 0 | } |
209 | | |
210 | 0 | let mut out = [0; N]; |
211 | 0 | let mut i = 0; |
212 | 0 | let mut found_null = false; |
213 | | // Indexing is protected by TinyStrError::TooLarge |
214 | | #[expect(clippy::indexing_slicing)] |
215 | 0 | while i < len { |
216 | 0 | let b = code_units[start + i]; |
217 | | |
218 | 0 | if b == 0 { |
219 | 0 | found_null = true; |
220 | 0 | } else if b >= 0x80 { |
221 | 0 | return Err(ParseError::NonAscii); |
222 | 0 | } else if found_null { |
223 | | // Error if there are contentful bytes after null |
224 | 0 | return Err(ParseError::ContainsNull); |
225 | 0 | } |
226 | 0 | out[i] = b as u8; |
227 | | |
228 | 0 | i += 1; |
229 | | } |
230 | | |
231 | 0 | if !allow_trailing_null && found_null { |
232 | | // We found some trailing nulls, error |
233 | 0 | return Err(ParseError::ContainsNull); |
234 | 0 | } |
235 | | |
236 | 0 | Ok(Self { |
237 | 0 | // SAFETY: `out` only contains ASCII bytes and has same size as `self.bytes` |
238 | 0 | bytes: unsafe { AsciiByte::to_ascii_byte_array(&out) }, |
239 | 0 | }) |
240 | 0 | } |
241 | | |
242 | | /// Creates a `TinyAsciiStr<N>` containing the decimal representation of |
243 | | /// the given unsigned integer. |
244 | | /// |
245 | | /// If the number of decimal digits exceeds `N`, the highest-magnitude |
246 | | /// digits are truncated, and the lowest-magnitude digits are returned |
247 | | /// as the error. |
248 | | /// |
249 | | /// Note: this function takes a u32. Larger integer types should probably |
250 | | /// not be stored in a `TinyAsciiStr`. |
251 | | /// |
252 | | /// # Examples |
253 | | /// |
254 | | /// ``` |
255 | | /// use tinystr::tinystr; |
256 | | /// use tinystr::TinyAsciiStr; |
257 | | /// |
258 | | /// let s0_4 = TinyAsciiStr::<4>::new_unsigned_decimal(0).unwrap(); |
259 | | /// let s456_4 = TinyAsciiStr::<4>::new_unsigned_decimal(456).unwrap(); |
260 | | /// let s456_3 = TinyAsciiStr::<3>::new_unsigned_decimal(456).unwrap(); |
261 | | /// let s456_2 = TinyAsciiStr::<2>::new_unsigned_decimal(456).unwrap_err(); |
262 | | /// |
263 | | /// assert_eq!(s0_4, tinystr!(4, "0")); |
264 | | /// assert_eq!(s456_4, tinystr!(4, "456")); |
265 | | /// assert_eq!(s456_3, tinystr!(3, "456")); |
266 | | /// assert_eq!(s456_2, tinystr!(2, "56")); |
267 | | /// ``` |
268 | | /// |
269 | | /// Example with saturating the value: |
270 | | /// |
271 | | /// ``` |
272 | | /// use tinystr::tinystr; |
273 | | /// use tinystr::TinyAsciiStr; |
274 | | /// |
275 | | /// let str_truncated = |
276 | | /// TinyAsciiStr::<2>::new_unsigned_decimal(456).unwrap_or_else(|s| s); |
277 | | /// let str_saturated = TinyAsciiStr::<2>::new_unsigned_decimal(456) |
278 | | /// .unwrap_or(tinystr!(2, "99")); |
279 | | /// |
280 | | /// assert_eq!(str_truncated, tinystr!(2, "56")); |
281 | | /// assert_eq!(str_saturated, tinystr!(2, "99")); |
282 | | /// ``` |
283 | 0 | pub fn new_unsigned_decimal(number: u32) -> Result<Self, Self> { |
284 | 0 | let mut bytes = [AsciiByte::B0; N]; |
285 | 0 | let mut x = number; |
286 | 0 | let mut i = 0usize; |
287 | | #[expect(clippy::indexing_slicing)] // in-range: i < N |
288 | 0 | while i < N && (x != 0 || i == 0) { |
289 | 0 | bytes[N - i - 1] = AsciiByte::from_decimal_digit((x % 10) as u8); |
290 | 0 | x /= 10; |
291 | 0 | i += 1; |
292 | 0 | } |
293 | 0 | if i < N { |
294 | 0 | bytes.copy_within((N - i)..N, 0); |
295 | 0 | bytes[i..N].fill(AsciiByte::B0); |
296 | 0 | } |
297 | 0 | let s = Self { bytes }; |
298 | 0 | if x != 0 { |
299 | 0 | Err(s) |
300 | | } else { |
301 | 0 | Ok(s) |
302 | | } |
303 | 0 | } |
304 | | |
305 | | #[inline] |
306 | 0 | pub const fn as_str(&self) -> &str { |
307 | | // as_utf8 is valid utf8 |
308 | 0 | unsafe { str::from_utf8_unchecked(self.as_utf8()) } |
309 | 0 | } Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<2>>::as_str Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<3>>::as_str Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<4>>::as_str Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<8>>::as_str Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<_>>::as_str |
310 | | |
311 | | #[inline] |
312 | | #[must_use] |
313 | 0 | pub const fn len(&self) -> usize { |
314 | 0 | if N <= 4 { |
315 | 0 | Aligned4::from_ascii_bytes(&self.bytes).len() |
316 | 0 | } else if N <= 8 { |
317 | 0 | Aligned8::from_ascii_bytes(&self.bytes).len() |
318 | | } else { |
319 | 0 | let mut i = 0; |
320 | | #[expect(clippy::indexing_slicing)] // < N is safe |
321 | 0 | while i < N && self.bytes[i] as u8 != AsciiByte::B0 as u8 { |
322 | 0 | i += 1 |
323 | | } |
324 | 0 | i |
325 | | } |
326 | 0 | } Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<2>>::len Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<3>>::len Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<4>>::len Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<8>>::len Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<_>>::len |
327 | | |
328 | | #[inline] |
329 | | #[must_use] |
330 | 0 | pub const fn is_empty(&self) -> bool { |
331 | 0 | self.bytes[0] as u8 == AsciiByte::B0 as u8 |
332 | 0 | } |
333 | | |
334 | | #[inline] |
335 | | #[must_use] |
336 | 0 | pub const fn as_utf8(&self) -> &[u8] { |
337 | | // Safe because `self.bytes.as_slice()` pointer-casts to `&[u8]`, |
338 | | // and changing the length of that slice to self.len() < N is safe. |
339 | | unsafe { |
340 | 0 | core::slice::from_raw_parts(self.bytes.as_slice().as_ptr() as *const u8, self.len()) |
341 | | } |
342 | 0 | } Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<2>>::as_utf8 Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<3>>::as_utf8 Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<4>>::as_utf8 Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<8>>::as_utf8 Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<_>>::as_utf8 |
343 | | |
344 | | #[inline] |
345 | | #[must_use] |
346 | 0 | pub const fn all_bytes(&self) -> &[u8; N] { |
347 | | // SAFETY: `self.bytes` has same size as [u8; N] |
348 | 0 | unsafe { &*(self.bytes.as_ptr() as *const [u8; N]) } |
349 | 0 | } Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<2>>::all_bytes Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<3>>::all_bytes Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<4>>::all_bytes Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<8>>::all_bytes Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<_>>::all_bytes |
350 | | |
351 | | #[inline] |
352 | | #[must_use] |
353 | | /// Resizes a `TinyAsciiStr<N>` to a `TinyAsciiStr<M>`. |
354 | | /// |
355 | | /// If `M < len()` the string gets truncated, otherwise only the |
356 | | /// memory representation changes. |
357 | 0 | pub const fn resize<const M: usize>(self) -> TinyAsciiStr<M> { |
358 | 0 | let mut bytes = [0; M]; |
359 | 0 | let mut i = 0; |
360 | | // Indexing is protected by the loop guard |
361 | | #[expect(clippy::indexing_slicing)] |
362 | 0 | while i < M && i < N { |
363 | 0 | bytes[i] = self.bytes[i] as u8; |
364 | 0 | i += 1; |
365 | 0 | } |
366 | | // `self.bytes` only contains ASCII bytes, with no null bytes between |
367 | | // ASCII characters, so this also holds for `bytes`. |
368 | 0 | unsafe { TinyAsciiStr::from_utf8_unchecked(bytes) } |
369 | 0 | } Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<3>>::resize::<8> Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<4>>::resize::<8> Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<8>>::resize::<3> Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<_>>::resize::<_> |
370 | | |
371 | | #[inline] |
372 | | #[must_use] |
373 | | /// Returns a `TinyAsciiStr<Q>` with the concatenation of this string, |
374 | | /// `TinyAsciiStr<N>`, and another string, `TinyAsciiStr<M>`. |
375 | | /// |
376 | | /// If `Q < N + M`, the string gets truncated. |
377 | | /// |
378 | | /// # Examples |
379 | | /// |
380 | | /// ``` |
381 | | /// use tinystr::tinystr; |
382 | | /// use tinystr::TinyAsciiStr; |
383 | | /// |
384 | | /// let abc = tinystr!(6, "abc"); |
385 | | /// let defg = tinystr!(6, "defg"); |
386 | | /// |
387 | | /// // The concatenation is successful if Q is large enough... |
388 | | /// assert_eq!(abc.concat(defg), tinystr!(16, "abcdefg")); |
389 | | /// assert_eq!(abc.concat(defg), tinystr!(12, "abcdefg")); |
390 | | /// assert_eq!(abc.concat(defg), tinystr!(8, "abcdefg")); |
391 | | /// assert_eq!(abc.concat(defg), tinystr!(7, "abcdefg")); |
392 | | /// |
393 | | /// /// ...but it truncates of Q is too small. |
394 | | /// assert_eq!(abc.concat(defg), tinystr!(6, "abcdef")); |
395 | | /// assert_eq!(abc.concat(defg), tinystr!(2, "ab")); |
396 | | /// ``` |
397 | 0 | pub const fn concat<const M: usize, const Q: usize>( |
398 | 0 | self, |
399 | 0 | other: TinyAsciiStr<M>, |
400 | 0 | ) -> TinyAsciiStr<Q> { |
401 | 0 | let mut result = self.resize::<Q>(); |
402 | 0 | let mut i = self.len(); |
403 | 0 | let mut j = 0; |
404 | | // Indexing is protected by the loop guard |
405 | | #[expect(clippy::indexing_slicing)] |
406 | 0 | while i < Q && j < M { |
407 | 0 | result.bytes[i] = other.bytes[j]; |
408 | 0 | i += 1; |
409 | 0 | j += 1; |
410 | 0 | } |
411 | 0 | result |
412 | 0 | } Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<3>>::concat::<4, 8> Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<_>>::concat::<_, _> |
413 | | |
414 | | /// # Safety |
415 | | /// Must be called with a bytes array made of valid ASCII bytes, with no null bytes |
416 | | /// between ASCII characters |
417 | | #[must_use] |
418 | 0 | pub const unsafe fn from_utf8_unchecked(code_units: [u8; N]) -> Self { |
419 | 0 | Self { |
420 | 0 | bytes: AsciiByte::to_ascii_byte_array(&code_units), |
421 | 0 | } |
422 | 0 | } Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<2>>::from_utf8_unchecked Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<3>>::from_utf8_unchecked Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<4>>::from_utf8_unchecked Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<8>>::from_utf8_unchecked Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<_>>::from_utf8_unchecked |
423 | | } |
424 | | |
425 | | macro_rules! check_is { |
426 | | ($self:ident, $check_int:ident, $check_u8:ident) => { |
427 | | if N <= 4 { |
428 | | Aligned4::from_ascii_bytes(&$self.bytes).$check_int() |
429 | | } else if N <= 8 { |
430 | | Aligned8::from_ascii_bytes(&$self.bytes).$check_int() |
431 | | } else { |
432 | | let mut i = 0; |
433 | | while i < N && $self.bytes[i] as u8 != AsciiByte::B0 as u8 { |
434 | | if !($self.bytes[i] as u8).$check_u8() { |
435 | | return false; |
436 | | } |
437 | | i += 1; |
438 | | } |
439 | | true |
440 | | } |
441 | | }; |
442 | | ($self:ident, $check_int:ident, !$check_u8_0_inv:ident, !$check_u8_1_inv:ident) => { |
443 | | if N <= 4 { |
444 | | Aligned4::from_ascii_bytes(&$self.bytes).$check_int() |
445 | | } else if N <= 8 { |
446 | | Aligned8::from_ascii_bytes(&$self.bytes).$check_int() |
447 | | } else { |
448 | | // Won't panic because N is > 8 |
449 | | if ($self.bytes[0] as u8).$check_u8_0_inv() { |
450 | | return false; |
451 | | } |
452 | | let mut i = 1; |
453 | | while i < N && $self.bytes[i] as u8 != AsciiByte::B0 as u8 { |
454 | | if ($self.bytes[i] as u8).$check_u8_1_inv() { |
455 | | return false; |
456 | | } |
457 | | i += 1; |
458 | | } |
459 | | true |
460 | | } |
461 | | }; |
462 | | ($self:ident, $check_int:ident, $check_u8_0_inv:ident, $check_u8_1_inv:ident) => { |
463 | | if N <= 4 { |
464 | | Aligned4::from_ascii_bytes(&$self.bytes).$check_int() |
465 | | } else if N <= 8 { |
466 | | Aligned8::from_ascii_bytes(&$self.bytes).$check_int() |
467 | | } else { |
468 | | // Won't panic because N is > 8 |
469 | | if !($self.bytes[0] as u8).$check_u8_0_inv() { |
470 | | return false; |
471 | | } |
472 | | let mut i = 1; |
473 | | while i < N && $self.bytes[i] as u8 != AsciiByte::B0 as u8 { |
474 | | if !($self.bytes[i] as u8).$check_u8_1_inv() { |
475 | | return false; |
476 | | } |
477 | | i += 1; |
478 | | } |
479 | | true |
480 | | } |
481 | | }; |
482 | | } |
483 | | |
484 | | impl<const N: usize> TinyAsciiStr<N> { |
485 | | /// Checks if the value is composed of ASCII alphabetic characters: |
486 | | /// |
487 | | /// * U+0041 'A' ..= U+005A 'Z', or |
488 | | /// * U+0061 'a' ..= U+007A 'z'. |
489 | | /// |
490 | | /// # Examples |
491 | | /// |
492 | | /// ``` |
493 | | /// use tinystr::TinyAsciiStr; |
494 | | /// |
495 | | /// let s1: TinyAsciiStr<4> = "Test".parse().expect("Failed to parse."); |
496 | | /// let s2: TinyAsciiStr<4> = "Te3t".parse().expect("Failed to parse."); |
497 | | /// |
498 | | /// assert!(s1.is_ascii_alphabetic()); |
499 | | /// assert!(!s2.is_ascii_alphabetic()); |
500 | | /// ``` |
501 | | #[inline] |
502 | | #[must_use] |
503 | 0 | pub const fn is_ascii_alphabetic(&self) -> bool { |
504 | 0 | check_is!(self, is_ascii_alphabetic, is_ascii_alphabetic) |
505 | 0 | } Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<3>>::is_ascii_alphabetic Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<4>>::is_ascii_alphabetic Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<8>>::is_ascii_alphabetic Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<_>>::is_ascii_alphabetic |
506 | | |
507 | | /// Checks if the value is composed of ASCII alphanumeric characters: |
508 | | /// |
509 | | /// * U+0041 'A' ..= U+005A 'Z', or |
510 | | /// * U+0061 'a' ..= U+007A 'z', or |
511 | | /// * U+0030 '0' ..= U+0039 '9'. |
512 | | /// |
513 | | /// # Examples |
514 | | /// |
515 | | /// ``` |
516 | | /// use tinystr::TinyAsciiStr; |
517 | | /// |
518 | | /// let s1: TinyAsciiStr<4> = "A15b".parse().expect("Failed to parse."); |
519 | | /// let s2: TinyAsciiStr<4> = "[3@w".parse().expect("Failed to parse."); |
520 | | /// |
521 | | /// assert!(s1.is_ascii_alphanumeric()); |
522 | | /// assert!(!s2.is_ascii_alphanumeric()); |
523 | | /// ``` |
524 | | #[inline] |
525 | | #[must_use] |
526 | 0 | pub const fn is_ascii_alphanumeric(&self) -> bool { |
527 | 0 | check_is!(self, is_ascii_alphanumeric, is_ascii_alphanumeric) |
528 | 0 | } Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<4>>::is_ascii_alphanumeric Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<8>>::is_ascii_alphanumeric Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<_>>::is_ascii_alphanumeric |
529 | | |
530 | | /// Checks if the value is composed of ASCII decimal digits: |
531 | | /// |
532 | | /// * U+0030 '0' ..= U+0039 '9'. |
533 | | /// |
534 | | /// # Examples |
535 | | /// |
536 | | /// ``` |
537 | | /// use tinystr::TinyAsciiStr; |
538 | | /// |
539 | | /// let s1: TinyAsciiStr<4> = "312".parse().expect("Failed to parse."); |
540 | | /// let s2: TinyAsciiStr<4> = "3d".parse().expect("Failed to parse."); |
541 | | /// |
542 | | /// assert!(s1.is_ascii_numeric()); |
543 | | /// assert!(!s2.is_ascii_numeric()); |
544 | | /// ``` |
545 | | #[inline] |
546 | | #[must_use] |
547 | 0 | pub const fn is_ascii_numeric(&self) -> bool { |
548 | 0 | check_is!(self, is_ascii_numeric, is_ascii_digit) |
549 | 0 | } Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<3>>::is_ascii_numeric Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<_>>::is_ascii_numeric |
550 | | |
551 | | /// Checks if the value is in ASCII lower case. |
552 | | /// |
553 | | /// All letter characters are checked for case. Non-letter characters are ignored. |
554 | | /// |
555 | | /// # Examples |
556 | | /// |
557 | | /// ``` |
558 | | /// use tinystr::TinyAsciiStr; |
559 | | /// |
560 | | /// let s1: TinyAsciiStr<4> = "teSt".parse().expect("Failed to parse."); |
561 | | /// let s2: TinyAsciiStr<4> = "test".parse().expect("Failed to parse."); |
562 | | /// let s3: TinyAsciiStr<4> = "001z".parse().expect("Failed to parse."); |
563 | | /// |
564 | | /// assert!(!s1.is_ascii_lowercase()); |
565 | | /// assert!(s2.is_ascii_lowercase()); |
566 | | /// assert!(s3.is_ascii_lowercase()); |
567 | | /// ``` |
568 | | #[inline] |
569 | | #[must_use] |
570 | 0 | pub const fn is_ascii_lowercase(&self) -> bool { |
571 | 0 | check_is!( |
572 | | self, |
573 | | is_ascii_lowercase, |
574 | | !is_ascii_uppercase, |
575 | | !is_ascii_uppercase |
576 | | ) |
577 | 0 | } Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<4>>::is_ascii_lowercase Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<8>>::is_ascii_lowercase Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<_>>::is_ascii_lowercase |
578 | | |
579 | | /// Checks if the value is in ASCII title case. |
580 | | /// |
581 | | /// This verifies that the first character is ASCII uppercase and all others ASCII lowercase. |
582 | | /// Non-letter characters are ignored. |
583 | | /// |
584 | | /// # Examples |
585 | | /// |
586 | | /// ``` |
587 | | /// use tinystr::TinyAsciiStr; |
588 | | /// |
589 | | /// let s1: TinyAsciiStr<4> = "teSt".parse().expect("Failed to parse."); |
590 | | /// let s2: TinyAsciiStr<4> = "Test".parse().expect("Failed to parse."); |
591 | | /// let s3: TinyAsciiStr<4> = "001z".parse().expect("Failed to parse."); |
592 | | /// |
593 | | /// assert!(!s1.is_ascii_titlecase()); |
594 | | /// assert!(s2.is_ascii_titlecase()); |
595 | | /// assert!(s3.is_ascii_titlecase()); |
596 | | /// ``` |
597 | | #[inline] |
598 | | #[must_use] |
599 | 0 | pub const fn is_ascii_titlecase(&self) -> bool { |
600 | 0 | check_is!( |
601 | | self, |
602 | | is_ascii_titlecase, |
603 | | !is_ascii_lowercase, |
604 | | !is_ascii_uppercase |
605 | | ) |
606 | 0 | } |
607 | | |
608 | | /// Checks if the value is in ASCII upper case. |
609 | | /// |
610 | | /// All letter characters are checked for case. Non-letter characters are ignored. |
611 | | /// |
612 | | /// # Examples |
613 | | /// |
614 | | /// ``` |
615 | | /// use tinystr::TinyAsciiStr; |
616 | | /// |
617 | | /// let s1: TinyAsciiStr<4> = "teSt".parse().expect("Failed to parse."); |
618 | | /// let s2: TinyAsciiStr<4> = "TEST".parse().expect("Failed to parse."); |
619 | | /// let s3: TinyAsciiStr<4> = "001z".parse().expect("Failed to parse."); |
620 | | /// |
621 | | /// assert!(!s1.is_ascii_uppercase()); |
622 | | /// assert!(s2.is_ascii_uppercase()); |
623 | | /// assert!(!s3.is_ascii_uppercase()); |
624 | | /// ``` |
625 | | #[inline] |
626 | | #[must_use] |
627 | 0 | pub const fn is_ascii_uppercase(&self) -> bool { |
628 | 0 | check_is!( |
629 | | self, |
630 | | is_ascii_uppercase, |
631 | | !is_ascii_lowercase, |
632 | | !is_ascii_lowercase |
633 | | ) |
634 | 0 | } |
635 | | |
636 | | /// Checks if the value is composed of ASCII alphabetic lower case characters: |
637 | | /// |
638 | | /// * U+0061 'a' ..= U+007A 'z', |
639 | | /// |
640 | | /// # Examples |
641 | | /// |
642 | | /// ``` |
643 | | /// use tinystr::TinyAsciiStr; |
644 | | /// |
645 | | /// let s1: TinyAsciiStr<4> = "Test".parse().expect("Failed to parse."); |
646 | | /// let s2: TinyAsciiStr<4> = "Te3t".parse().expect("Failed to parse."); |
647 | | /// let s3: TinyAsciiStr<4> = "teSt".parse().expect("Failed to parse."); |
648 | | /// let s4: TinyAsciiStr<4> = "test".parse().expect("Failed to parse."); |
649 | | /// let s5: TinyAsciiStr<4> = "001z".parse().expect("Failed to parse."); |
650 | | /// |
651 | | /// assert!(!s1.is_ascii_alphabetic_lowercase()); |
652 | | /// assert!(!s2.is_ascii_alphabetic_lowercase()); |
653 | | /// assert!(!s3.is_ascii_alphabetic_lowercase()); |
654 | | /// assert!(s4.is_ascii_alphabetic_lowercase()); |
655 | | /// assert!(!s5.is_ascii_alphabetic_lowercase()); |
656 | | /// ``` |
657 | | #[inline] |
658 | | #[must_use] |
659 | 0 | pub const fn is_ascii_alphabetic_lowercase(&self) -> bool { |
660 | 0 | check_is!( |
661 | | self, |
662 | | is_ascii_alphabetic_lowercase, |
663 | | is_ascii_lowercase, |
664 | | is_ascii_lowercase |
665 | | ) |
666 | 0 | } Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<3>>::is_ascii_alphabetic_lowercase Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<_>>::is_ascii_alphabetic_lowercase |
667 | | |
668 | | /// Checks if the value is composed of ASCII alphabetic, with the first character being ASCII uppercase, and all others ASCII lowercase. |
669 | | /// |
670 | | /// # Examples |
671 | | /// |
672 | | /// ``` |
673 | | /// use tinystr::TinyAsciiStr; |
674 | | /// |
675 | | /// let s1: TinyAsciiStr<4> = "Test".parse().expect("Failed to parse."); |
676 | | /// let s2: TinyAsciiStr<4> = "Te3t".parse().expect("Failed to parse."); |
677 | | /// let s3: TinyAsciiStr<4> = "teSt".parse().expect("Failed to parse."); |
678 | | /// let s4: TinyAsciiStr<4> = "test".parse().expect("Failed to parse."); |
679 | | /// let s5: TinyAsciiStr<4> = "001z".parse().expect("Failed to parse."); |
680 | | /// |
681 | | /// assert!(s1.is_ascii_alphabetic_titlecase()); |
682 | | /// assert!(!s2.is_ascii_alphabetic_titlecase()); |
683 | | /// assert!(!s3.is_ascii_alphabetic_titlecase()); |
684 | | /// assert!(!s4.is_ascii_alphabetic_titlecase()); |
685 | | /// assert!(!s5.is_ascii_alphabetic_titlecase()); |
686 | | /// ``` |
687 | | #[inline] |
688 | | #[must_use] |
689 | 0 | pub const fn is_ascii_alphabetic_titlecase(&self) -> bool { |
690 | 0 | check_is!( |
691 | | self, |
692 | | is_ascii_alphabetic_titlecase, |
693 | | is_ascii_uppercase, |
694 | | is_ascii_lowercase |
695 | | ) |
696 | 0 | } Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<4>>::is_ascii_alphabetic_titlecase Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<_>>::is_ascii_alphabetic_titlecase |
697 | | |
698 | | /// Checks if the value is composed of ASCII alphabetic upper case characters: |
699 | | /// |
700 | | /// * U+0041 'A' ..= U+005A 'Z', |
701 | | /// |
702 | | /// # Examples |
703 | | /// |
704 | | /// ``` |
705 | | /// use tinystr::TinyAsciiStr; |
706 | | /// |
707 | | /// let s1: TinyAsciiStr<4> = "Test".parse().expect("Failed to parse."); |
708 | | /// let s2: TinyAsciiStr<4> = "Te3t".parse().expect("Failed to parse."); |
709 | | /// let s3: TinyAsciiStr<4> = "teSt".parse().expect("Failed to parse."); |
710 | | /// let s4: TinyAsciiStr<4> = "TEST".parse().expect("Failed to parse."); |
711 | | /// let s5: TinyAsciiStr<4> = "001z".parse().expect("Failed to parse."); |
712 | | /// |
713 | | /// assert!(!s1.is_ascii_alphabetic_uppercase()); |
714 | | /// assert!(!s2.is_ascii_alphabetic_uppercase()); |
715 | | /// assert!(!s3.is_ascii_alphabetic_uppercase()); |
716 | | /// assert!(s4.is_ascii_alphabetic_uppercase()); |
717 | | /// assert!(!s5.is_ascii_alphabetic_uppercase()); |
718 | | /// ``` |
719 | | #[inline] |
720 | | #[must_use] |
721 | 0 | pub const fn is_ascii_alphabetic_uppercase(&self) -> bool { |
722 | 0 | check_is!( |
723 | | self, |
724 | | is_ascii_alphabetic_uppercase, |
725 | | is_ascii_uppercase, |
726 | | is_ascii_uppercase |
727 | | ) |
728 | 0 | } Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<3>>::is_ascii_alphabetic_uppercase Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<_>>::is_ascii_alphabetic_uppercase |
729 | | } |
730 | | |
731 | | macro_rules! to { |
732 | | ($self:ident, $to:ident, $later_char_to:ident $(,$first_char_to:ident)?) => {{ |
733 | | let mut i = 0; |
734 | | if N <= 4 { |
735 | | let aligned = Aligned4::from_ascii_bytes(&$self.bytes).$to(); |
736 | | // Won't panic because self.bytes has length N and aligned has length >= N |
737 | | #[expect(clippy::indexing_slicing)] |
738 | | while i < N { |
739 | | $self.bytes[i] = aligned[i]; |
740 | | i += 1; |
741 | | } |
742 | | } else if N <= 8 { |
743 | | let aligned = Aligned8::from_ascii_bytes(&$self.bytes).$to(); |
744 | | // Won't panic because self.bytes has length N and aligned has length >= N |
745 | | #[expect(clippy::indexing_slicing)] |
746 | | while i < N { |
747 | | $self.bytes[i] = aligned[i]; |
748 | | i += 1; |
749 | | } |
750 | | } else { |
751 | | while i < N && $self.bytes[i] as u8 != AsciiByte::B0 as u8 { |
752 | | $self.bytes[i] = $self.bytes[i].$later_char_to(); |
753 | | i += 1; |
754 | | } |
755 | | $( |
756 | | $self.bytes[0] = $self.bytes[0].$first_char_to(); |
757 | | )? |
758 | | } |
759 | | $self |
760 | | }}; |
761 | | } |
762 | | |
763 | | impl<const N: usize> TinyAsciiStr<N> { |
764 | | /// Converts this type to its ASCII lower case equivalent in-place. |
765 | | /// |
766 | | /// ASCII letters 'A' to 'Z' are mapped to 'a' to 'z', other characters are unchanged. |
767 | | /// |
768 | | /// # Examples |
769 | | /// |
770 | | /// ``` |
771 | | /// use tinystr::TinyAsciiStr; |
772 | | /// |
773 | | /// let s1: TinyAsciiStr<4> = "TeS3".parse().expect("Failed to parse."); |
774 | | /// |
775 | | /// assert_eq!(&*s1.to_ascii_lowercase(), "tes3"); |
776 | | /// ``` |
777 | | #[inline] |
778 | | #[must_use] |
779 | 0 | pub const fn to_ascii_lowercase(mut self) -> Self { |
780 | 0 | to!(self, to_ascii_lowercase, to_ascii_lowercase) |
781 | 0 | } Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<2>>::to_ascii_lowercase Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<3>>::to_ascii_lowercase Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<4>>::to_ascii_lowercase Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<8>>::to_ascii_lowercase Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<_>>::to_ascii_lowercase |
782 | | |
783 | | /// Converts this type to its ASCII title case equivalent in-place. |
784 | | /// |
785 | | /// The first character is converted to ASCII uppercase; the remaining characters |
786 | | /// are converted to ASCII lowercase. |
787 | | /// |
788 | | /// # Examples |
789 | | /// |
790 | | /// ``` |
791 | | /// use tinystr::TinyAsciiStr; |
792 | | /// |
793 | | /// let s1: TinyAsciiStr<4> = "teSt".parse().expect("Failed to parse."); |
794 | | /// |
795 | | /// assert_eq!(&*s1.to_ascii_titlecase(), "Test"); |
796 | | /// ``` |
797 | | #[inline] |
798 | | #[must_use] |
799 | 0 | pub const fn to_ascii_titlecase(mut self) -> Self { |
800 | 0 | to!( |
801 | | self, |
802 | | to_ascii_titlecase, |
803 | | to_ascii_lowercase, |
804 | | to_ascii_uppercase |
805 | | ) |
806 | 0 | } Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<4>>::to_ascii_titlecase Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<_>>::to_ascii_titlecase |
807 | | |
808 | | /// Converts this type to its ASCII upper case equivalent in-place. |
809 | | /// |
810 | | /// ASCII letters 'a' to 'z' are mapped to 'A' to 'Z', other characters are unchanged. |
811 | | /// |
812 | | /// # Examples |
813 | | /// |
814 | | /// ``` |
815 | | /// use tinystr::TinyAsciiStr; |
816 | | /// |
817 | | /// let s1: TinyAsciiStr<4> = "Tes3".parse().expect("Failed to parse."); |
818 | | /// |
819 | | /// assert_eq!(&*s1.to_ascii_uppercase(), "TES3"); |
820 | | /// ``` |
821 | | #[inline] |
822 | | #[must_use] |
823 | 0 | pub const fn to_ascii_uppercase(mut self) -> Self { |
824 | 0 | to!(self, to_ascii_uppercase, to_ascii_uppercase) |
825 | 0 | } Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<3>>::to_ascii_uppercase Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<_>>::to_ascii_uppercase |
826 | | } |
827 | | |
828 | | impl<const N: usize> fmt::Debug for TinyAsciiStr<N> { |
829 | 0 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
830 | 0 | fmt::Debug::fmt(self.as_str(), f) |
831 | 0 | } |
832 | | } |
833 | | |
834 | | impl<const N: usize> fmt::Display for TinyAsciiStr<N> { |
835 | 0 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
836 | 0 | fmt::Display::fmt(self.as_str(), f) |
837 | 0 | } |
838 | | } |
839 | | |
840 | | impl<const N: usize> Deref for TinyAsciiStr<N> { |
841 | | type Target = str; |
842 | | #[inline] |
843 | 0 | fn deref(&self) -> &str { |
844 | 0 | self.as_str() |
845 | 0 | } Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<8> as core::ops::deref::Deref>::deref Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<_> as core::ops::deref::Deref>::deref |
846 | | } |
847 | | |
848 | | impl<const N: usize> Borrow<str> for TinyAsciiStr<N> { |
849 | | #[inline] |
850 | 0 | fn borrow(&self) -> &str { |
851 | 0 | self.as_str() |
852 | 0 | } |
853 | | } |
854 | | |
855 | | impl<const N: usize> FromStr for TinyAsciiStr<N> { |
856 | | type Err = ParseError; |
857 | | #[inline] |
858 | 0 | fn from_str(s: &str) -> Result<Self, Self::Err> { |
859 | 0 | Self::try_from_str(s) |
860 | 0 | } |
861 | | } |
862 | | |
863 | | impl<const N: usize> PartialEq<str> for TinyAsciiStr<N> { |
864 | 0 | fn eq(&self, other: &str) -> bool { |
865 | 0 | self.deref() == other |
866 | 0 | } |
867 | | } |
868 | | |
869 | | impl<const N: usize> PartialEq<&str> for TinyAsciiStr<N> { |
870 | 0 | fn eq(&self, other: &&str) -> bool { |
871 | 0 | self.deref() == *other |
872 | 0 | } Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<8> as core::cmp::PartialEq<&str>>::eq Unexecuted instantiation: <tinystr::ascii::TinyAsciiStr<_> as core::cmp::PartialEq<&str>>::eq |
873 | | } |
874 | | |
875 | | #[cfg(feature = "alloc")] |
876 | | impl<const N: usize> PartialEq<String> for TinyAsciiStr<N> { |
877 | | fn eq(&self, other: &String) -> bool { |
878 | | self.deref() == other.deref() |
879 | | } |
880 | | } |
881 | | |
882 | | #[cfg(feature = "alloc")] |
883 | | impl<const N: usize> PartialEq<TinyAsciiStr<N>> for String { |
884 | | fn eq(&self, other: &TinyAsciiStr<N>) -> bool { |
885 | | self.deref() == other.deref() |
886 | | } |
887 | | } |
888 | | |
889 | | #[cfg(test)] |
890 | | mod test { |
891 | | use super::*; |
892 | | use rand::distr::Distribution; |
893 | | use rand::distr::StandardUniform; |
894 | | use rand::rngs::SmallRng; |
895 | | use rand::SeedableRng; |
896 | | |
897 | | const STRINGS: [&str; 26] = [ |
898 | | "Latn", |
899 | | "laTn", |
900 | | "windows", |
901 | | "AR", |
902 | | "Hans", |
903 | | "macos", |
904 | | "AT", |
905 | | "infiniband", |
906 | | "FR", |
907 | | "en", |
908 | | "Cyrl", |
909 | | "FromIntegral", |
910 | | "NO", |
911 | | "419", |
912 | | "MacintoshOSX2019", |
913 | | "a3z", |
914 | | "A3z", |
915 | | "A3Z", |
916 | | "a3Z", |
917 | | "3A", |
918 | | "3Z", |
919 | | "3a", |
920 | | "3z", |
921 | | "@@[`{", |
922 | | "UK", |
923 | | "E12", |
924 | | ]; |
925 | | |
926 | | fn gen_strings(num_strings: usize, allowed_lengths: &[usize]) -> Vec<String> { |
927 | | use rand::seq::IndexedRandom; |
928 | | let mut rng = SmallRng::seed_from_u64(2022); |
929 | | // Need to do this in 2 steps since the RNG is needed twice |
930 | | let string_lengths = core::iter::repeat_with(|| *allowed_lengths.choose(&mut rng).unwrap()) |
931 | | .take(num_strings) |
932 | | .collect::<Vec<usize>>(); |
933 | | string_lengths |
934 | | .iter() |
935 | | .map(|len| { |
936 | | StandardUniform |
937 | | .sample_iter(&mut rng) |
938 | | .filter(|b: &u8| *b > 0 && *b < 0x80) |
939 | | .take(*len) |
940 | | .collect::<Vec<u8>>() |
941 | | }) |
942 | | .map(|byte_vec| String::from_utf8(byte_vec).expect("All ASCII")) |
943 | | .collect() |
944 | | } |
945 | | |
946 | | fn check_operation<T, F1, F2, const N: usize>(reference_f: F1, tinystr_f: F2) |
947 | | where |
948 | | F1: Fn(&str) -> T, |
949 | | F2: Fn(TinyAsciiStr<N>) -> T, |
950 | | T: fmt::Debug + PartialEq, |
951 | | { |
952 | | for s in STRINGS |
953 | | .into_iter() |
954 | | .map(str::to_owned) |
955 | | .chain(gen_strings(100, &[3, 4, 5, 8, 12])) |
956 | | { |
957 | | let t = match TinyAsciiStr::<N>::from_str(&s) { |
958 | | Ok(t) => t, |
959 | | Err(ParseError::TooLong { .. }) => continue, |
960 | | Err(e) => panic!("{}", e), |
961 | | }; |
962 | | let expected = reference_f(&s); |
963 | | let actual = tinystr_f(t); |
964 | | assert_eq!(expected, actual, "TinyAsciiStr<{N}>: {s:?}"); |
965 | | |
966 | | let s_utf16: Vec<u16> = s.encode_utf16().collect(); |
967 | | let t = match TinyAsciiStr::<N>::try_from_utf16(&s_utf16) { |
968 | | Ok(t) => t, |
969 | | Err(ParseError::TooLong { .. }) => continue, |
970 | | Err(e) => panic!("{}", e), |
971 | | }; |
972 | | let expected = reference_f(&s); |
973 | | let actual = tinystr_f(t); |
974 | | assert_eq!(expected, actual, "TinyAsciiStr<{N}>: {s:?}"); |
975 | | } |
976 | | } |
977 | | |
978 | | #[test] |
979 | | fn test_is_ascii_alphabetic() { |
980 | | fn check<const N: usize>() { |
981 | | check_operation( |
982 | | |s| s.chars().all(|c| c.is_ascii_alphabetic()), |
983 | | |t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_alphabetic(&t), |
984 | | ) |
985 | | } |
986 | | check::<2>(); |
987 | | check::<3>(); |
988 | | check::<4>(); |
989 | | check::<5>(); |
990 | | check::<8>(); |
991 | | check::<16>(); |
992 | | } |
993 | | |
994 | | #[test] |
995 | | fn test_is_ascii_alphanumeric() { |
996 | | fn check<const N: usize>() { |
997 | | check_operation( |
998 | | |s| s.chars().all(|c| c.is_ascii_alphanumeric()), |
999 | | |t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_alphanumeric(&t), |
1000 | | ) |
1001 | | } |
1002 | | check::<2>(); |
1003 | | check::<3>(); |
1004 | | check::<4>(); |
1005 | | check::<5>(); |
1006 | | check::<8>(); |
1007 | | check::<16>(); |
1008 | | } |
1009 | | |
1010 | | #[test] |
1011 | | fn test_is_ascii_numeric() { |
1012 | | fn check<const N: usize>() { |
1013 | | check_operation( |
1014 | | |s| s.chars().all(|c| c.is_ascii_digit()), |
1015 | | |t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_numeric(&t), |
1016 | | ) |
1017 | | } |
1018 | | check::<2>(); |
1019 | | check::<3>(); |
1020 | | check::<4>(); |
1021 | | check::<5>(); |
1022 | | check::<8>(); |
1023 | | check::<16>(); |
1024 | | } |
1025 | | |
1026 | | #[test] |
1027 | | fn test_is_ascii_lowercase() { |
1028 | | fn check<const N: usize>() { |
1029 | | check_operation( |
1030 | | |s| { |
1031 | | s == TinyAsciiStr::<16>::try_from_str(s) |
1032 | | .unwrap() |
1033 | | .to_ascii_lowercase() |
1034 | | .as_str() |
1035 | | }, |
1036 | | |t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_lowercase(&t), |
1037 | | ) |
1038 | | } |
1039 | | check::<2>(); |
1040 | | check::<3>(); |
1041 | | check::<4>(); |
1042 | | check::<5>(); |
1043 | | check::<8>(); |
1044 | | check::<16>(); |
1045 | | } |
1046 | | |
1047 | | #[test] |
1048 | | fn test_is_ascii_titlecase() { |
1049 | | fn check<const N: usize>() { |
1050 | | check_operation( |
1051 | | |s| { |
1052 | | s == TinyAsciiStr::<16>::try_from_str(s) |
1053 | | .unwrap() |
1054 | | .to_ascii_titlecase() |
1055 | | .as_str() |
1056 | | }, |
1057 | | |t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_titlecase(&t), |
1058 | | ) |
1059 | | } |
1060 | | check::<2>(); |
1061 | | check::<3>(); |
1062 | | check::<4>(); |
1063 | | check::<5>(); |
1064 | | check::<8>(); |
1065 | | check::<16>(); |
1066 | | } |
1067 | | |
1068 | | #[test] |
1069 | | fn test_is_ascii_uppercase() { |
1070 | | fn check<const N: usize>() { |
1071 | | check_operation( |
1072 | | |s| { |
1073 | | s == TinyAsciiStr::<16>::try_from_str(s) |
1074 | | .unwrap() |
1075 | | .to_ascii_uppercase() |
1076 | | .as_str() |
1077 | | }, |
1078 | | |t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_uppercase(&t), |
1079 | | ) |
1080 | | } |
1081 | | check::<2>(); |
1082 | | check::<3>(); |
1083 | | check::<4>(); |
1084 | | check::<5>(); |
1085 | | check::<8>(); |
1086 | | check::<16>(); |
1087 | | } |
1088 | | |
1089 | | #[test] |
1090 | | fn test_is_ascii_alphabetic_lowercase() { |
1091 | | fn check<const N: usize>() { |
1092 | | check_operation( |
1093 | | |s| { |
1094 | | // Check alphabetic |
1095 | | s.chars().all(|c| c.is_ascii_alphabetic()) && |
1096 | | // Check lowercase |
1097 | | s == TinyAsciiStr::<16>::try_from_str(s) |
1098 | | .unwrap() |
1099 | | .to_ascii_lowercase() |
1100 | | .as_str() |
1101 | | }, |
1102 | | |t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_alphabetic_lowercase(&t), |
1103 | | ) |
1104 | | } |
1105 | | check::<2>(); |
1106 | | check::<3>(); |
1107 | | check::<4>(); |
1108 | | check::<5>(); |
1109 | | check::<8>(); |
1110 | | check::<16>(); |
1111 | | } |
1112 | | |
1113 | | #[test] |
1114 | | fn test_is_ascii_alphabetic_titlecase() { |
1115 | | fn check<const N: usize>() { |
1116 | | check_operation( |
1117 | | |s| { |
1118 | | // Check alphabetic |
1119 | | s.chars().all(|c| c.is_ascii_alphabetic()) && |
1120 | | // Check titlecase |
1121 | | s == TinyAsciiStr::<16>::try_from_str(s) |
1122 | | .unwrap() |
1123 | | .to_ascii_titlecase() |
1124 | | .as_str() |
1125 | | }, |
1126 | | |t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_alphabetic_titlecase(&t), |
1127 | | ) |
1128 | | } |
1129 | | check::<2>(); |
1130 | | check::<3>(); |
1131 | | check::<4>(); |
1132 | | check::<5>(); |
1133 | | check::<8>(); |
1134 | | check::<16>(); |
1135 | | } |
1136 | | |
1137 | | #[test] |
1138 | | fn test_is_ascii_alphabetic_uppercase() { |
1139 | | fn check<const N: usize>() { |
1140 | | check_operation( |
1141 | | |s| { |
1142 | | // Check alphabetic |
1143 | | s.chars().all(|c| c.is_ascii_alphabetic()) && |
1144 | | // Check uppercase |
1145 | | s == TinyAsciiStr::<16>::try_from_str(s) |
1146 | | .unwrap() |
1147 | | .to_ascii_uppercase() |
1148 | | .as_str() |
1149 | | }, |
1150 | | |t: TinyAsciiStr<N>| TinyAsciiStr::is_ascii_alphabetic_uppercase(&t), |
1151 | | ) |
1152 | | } |
1153 | | check::<2>(); |
1154 | | check::<3>(); |
1155 | | check::<4>(); |
1156 | | check::<5>(); |
1157 | | check::<8>(); |
1158 | | check::<16>(); |
1159 | | } |
1160 | | |
1161 | | #[test] |
1162 | | fn test_to_ascii_lowercase() { |
1163 | | fn check<const N: usize>() { |
1164 | | check_operation( |
1165 | | |s| { |
1166 | | s.chars() |
1167 | | .map(|c| c.to_ascii_lowercase()) |
1168 | | .collect::<String>() |
1169 | | }, |
1170 | | |t: TinyAsciiStr<N>| TinyAsciiStr::to_ascii_lowercase(t).as_str().to_owned(), |
1171 | | ) |
1172 | | } |
1173 | | check::<2>(); |
1174 | | check::<3>(); |
1175 | | check::<4>(); |
1176 | | check::<5>(); |
1177 | | check::<8>(); |
1178 | | check::<16>(); |
1179 | | } |
1180 | | |
1181 | | #[test] |
1182 | | fn test_to_ascii_titlecase() { |
1183 | | fn check<const N: usize>() { |
1184 | | check_operation( |
1185 | | |s| { |
1186 | | let mut r = s |
1187 | | .chars() |
1188 | | .map(|c| c.to_ascii_lowercase()) |
1189 | | .collect::<String>(); |
1190 | | // Safe because the string is nonempty and an ASCII string |
1191 | | unsafe { r.as_bytes_mut()[0].make_ascii_uppercase() }; |
1192 | | r |
1193 | | }, |
1194 | | |t: TinyAsciiStr<N>| TinyAsciiStr::to_ascii_titlecase(t).as_str().to_owned(), |
1195 | | ) |
1196 | | } |
1197 | | check::<2>(); |
1198 | | check::<3>(); |
1199 | | check::<4>(); |
1200 | | check::<5>(); |
1201 | | check::<8>(); |
1202 | | check::<16>(); |
1203 | | } |
1204 | | |
1205 | | #[test] |
1206 | | fn test_to_ascii_uppercase() { |
1207 | | fn check<const N: usize>() { |
1208 | | check_operation( |
1209 | | |s| { |
1210 | | s.chars() |
1211 | | .map(|c| c.to_ascii_uppercase()) |
1212 | | .collect::<String>() |
1213 | | }, |
1214 | | |t: TinyAsciiStr<N>| TinyAsciiStr::to_ascii_uppercase(t).as_str().to_owned(), |
1215 | | ) |
1216 | | } |
1217 | | check::<2>(); |
1218 | | check::<3>(); |
1219 | | check::<4>(); |
1220 | | check::<5>(); |
1221 | | check::<8>(); |
1222 | | check::<16>(); |
1223 | | } |
1224 | | |
1225 | | #[test] |
1226 | | fn lossy_constructor() { |
1227 | | assert_eq!(TinyAsciiStr::<4>::from_utf8_lossy(b"", b'?').as_str(), ""); |
1228 | | assert_eq!( |
1229 | | TinyAsciiStr::<4>::from_utf8_lossy(b"oh\0o", b'?').as_str(), |
1230 | | "oh?o" |
1231 | | ); |
1232 | | assert_eq!( |
1233 | | TinyAsciiStr::<4>::from_utf8_lossy(b"\0", b'?').as_str(), |
1234 | | "?" |
1235 | | ); |
1236 | | assert_eq!( |
1237 | | TinyAsciiStr::<4>::from_utf8_lossy(b"toolong", b'?').as_str(), |
1238 | | "tool" |
1239 | | ); |
1240 | | assert_eq!( |
1241 | | TinyAsciiStr::<4>::from_utf8_lossy(&[b'a', 0x80, 0xFF, b'1'], b'?').as_str(), |
1242 | | "a??1" |
1243 | | ); |
1244 | | } |
1245 | | |
1246 | | #[test] |
1247 | | #[should_panic(expected = "replacement must be a non-null ASCII byte")] |
1248 | | fn from_utf8_lossy_rejects_non_ascii_replacement() { |
1249 | | // A non-ASCII replacement byte (>= 0x80) would create an invalid AsciiByte |
1250 | | // discriminant and then from_utf8_unchecked would be called on non-UTF-8 |
1251 | | // data (UB). The fix validates the replacement byte on entry. |
1252 | | let _ = TinyAsciiStr::<4>::from_utf8_lossy(b"\xFF", 0xFF); |
1253 | | } |
1254 | | |
1255 | | #[test] |
1256 | | #[should_panic(expected = "replacement must be a non-null ASCII byte")] |
1257 | | fn from_utf16_lossy_rejects_non_ascii_replacement() { |
1258 | | let _ = TinyAsciiStr::<4>::from_utf16_lossy(&[0xFFFF], 0xFF); |
1259 | | } |
1260 | | |
1261 | | #[test] |
1262 | | #[should_panic(expected = "replacement must be a non-null ASCII byte")] |
1263 | | fn from_utf8_lossy_rejects_null_replacement() { |
1264 | | // Null (0x00) is also invalid — it's the TinyAsciiStr terminator. |
1265 | | let _ = TinyAsciiStr::<4>::from_utf8_lossy(b"\xFF", 0x00); |
1266 | | } |
1267 | | } |