devela/text/str/nonul.rs
1// devela::text::nonul
2//
3//! Non-nul `String` backed by an array.
4//
5// TOC
6// - definitions
7// - trait impls
8
9use crate::{
10 cfor, iif, text::char::*, unwrap, ConstDefault, Deref, InvalidText, IterChars, Mismatch,
11 MismatchedCapacity, NotEnoughElements, _core::fmt,
12};
13#[cfg(feature = "alloc")]
14use crate::{CString, ToString};
15crate::_use! {basic::from_utf8}
16
17/* definitions */
18
19/// The nul character.
20const NUL_CHAR: char = '\0';
21
22/// A UTF-8 string with up to [`u8::MAX`] bytes, excluding nul chars
23///
24/// Internally, the first 0 byte in the array indicates the end of the string.
25///
26/// ## Methods
27///
28/// - Construct:
29/// [`new`][Self::new],
30/// [`from_char`][Self::from_char]*(
31/// [`7`](Self::from_char7),
32/// [`8`](Self::from_char8),
33/// [`16`](Self::from_char16).
34/// )*.
35/// - Deconstruct:
36/// [`into_array`][Self::into_array],
37/// [`as_array`][Self::as_array],
38/// [`as_bytes`][Self::as_bytes]
39/// *([mut][Self::as_bytes_mut]<sup title="unsafe function">⚠</sup>)*,
40/// [`as_str`][Self::as_str]
41/// *([mut][Self::as_mut_str]<sup title="unsafe function">⚠</sup>)*,
42/// [`chars`][Self::chars],
43/// [`to_cstring`][Self::to_cstring](`alloc`).
44/// - Query:
45/// [`len`][Self::len],
46/// [`is_empty`][Self::is_empty],
47/// [`is_full`][Self::is_full],
48/// [`capacity`][Self::capacity],
49/// [`remaining_capacity`][Self::remaining_capacity].
50/// - Operations:
51/// [`clear`][Self::clear],
52/// [`pop`][Self::pop]*([try][Self::try_pop])*,
53/// [`push`][Self::push]*([try][Self::try_push])*.
54/// [`push_str`][Self::push]*([try][Self::try_push_str])*,
55/// [`try_push_str_complete`][Self::try_push_str_complete].
56#[must_use]
57#[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
58pub struct StringNonul<const CAP: usize> {
59 arr: [u8; CAP],
60}
61
62impl<const CAP: usize> StringNonul<CAP> {
63 /// Creates a new empty `StringNonul`.
64 ///
65 /// # Errors
66 /// Returns [`MismatchedCapacity`] if `CAP` > [`u8::MAX`].
67 pub const fn new() -> Result<Self, MismatchedCapacity> {
68 if CAP <= u8::MAX as usize {
69 Ok(Self { arr: [0; CAP] })
70 } else {
71 Err(MismatchedCapacity::closed(0, u8::MAX as usize, CAP))
72 }
73 }
74
75 /* query */
76
77 /// Returns the total capacity in bytes.
78 #[must_use] #[rustfmt::skip]
79 pub const fn capacity() -> usize { CAP }
80
81 /// Returns the remaining capacity.
82 #[must_use] #[rustfmt::skip]
83 pub const fn remaining_capacity(&self) -> usize { CAP - self.len() }
84
85 /// Returns the current length.
86 ///
87 /// # Examples
88 /// ```
89 /// # use devela::{StringNonul, MismatchedCapacity};
90 /// # fn main() -> Result<(), MismatchedCapacity> {
91 /// let mut s = StringNonul::<4>::new()?;
92 /// assert_eq![0, s.len()];
93 ///
94 /// assert_eq![1, s.push('a')];
95 /// assert_eq![1, s.len()];
96 ///
97 /// assert_eq![3, s.push('€')];
98 /// assert_eq![4, s.len()];
99 /// # Ok(()) }
100 /// ```
101 #[must_use]
102 pub const fn len(&self) -> usize {
103 let mut position = 0;
104 while position < CAP {
105 iif![self.arr[position] == 0; break];
106 position += 1;
107 }
108 position
109 }
110
111 /// Returns `true` if the current length is 0.
112 #[must_use] #[rustfmt::skip]
113 pub const fn is_empty(&self) -> bool { self.len() == 0 }
114
115 /// Returns `true` if the current remaining capacity is 0.
116 #[must_use] #[rustfmt::skip]
117 pub const fn is_full(&self) -> bool { self.len() == CAP }
118
119 /* deconstruct */
120
121 /// Returns the inner array with the full contents.
122 ///
123 /// The array contains all the bytes, including those outside the current length.
124 #[must_use] #[rustfmt::skip]
125 pub const fn into_array(self) -> [u8; CAP] { self.arr }
126
127 /// Returns a copy of the inner array with the full contents.
128 ///
129 /// The array contains all the bytes, including those outside the current length.
130 #[must_use] #[rustfmt::skip]
131 pub const fn as_array(&self) -> [u8; CAP] { self.arr }
132
133 /// Returns a byte slice of the inner string slice.
134 ///
135 /// # Features
136 /// Makes use of the `unsafe_slice` feature if enabled.
137 #[must_use] #[rustfmt::skip]
138 pub const fn as_bytes(&self) -> &[u8] { self.arr.split_at(self.len()).0 }
139
140 /// Returns a mutable byte slice of the inner string slice.
141 ///
142 /// # Safety
143 /// The caller must ensure that the content of the slice is valid UTF-8
144 /// before the borrow ends and the underlying `str` is used.
145 ///
146 /// Use of a `str` whose contents are not valid UTF-8 is undefined behavior.
147 #[must_use]
148 #[cfg(all(not(feature = "safe_text"), feature = "unsafe_slice"))]
149 #[cfg_attr(feature = "nightly_doc", doc(cfg(feature = "unsafe_slice")))]
150 pub unsafe fn as_bytes_mut(&mut self) -> &mut [u8] {
151 let len = self.len();
152 // SAFETY: caller must ensure safety
153 unsafe { self.arr.get_unchecked_mut(0..len) }
154 }
155
156 /// Returns the inner string slice.
157 /// # Features
158 /// Makes use of the `unsafe_slice` feature if enabled.
159 #[must_use] #[rustfmt::skip]
160 pub const fn as_str(&self) -> &str {
161 #[cfg(any(feature = "safe_text", not(feature = "unsafe_slice")))]
162 return unwrap![ok_expect from_utf8(self.as_bytes()), "Invalid UTF-8"];
163
164 #[cfg(all(not(feature = "safe_text"), feature = "unsafe_slice"))]
165 // SAFETY: we ensure to contain only valid UTF-8
166 unsafe { ::core::str::from_utf8_unchecked(self.as_bytes()) }
167 }
168
169 /// Returns the mutable inner string slice.
170 ///
171 /// # Safety
172 /// The caller must ensure that the content of the slice is valid UTF-8
173 /// and that it doesn't contain any `NUL` characters before the borrow
174 /// ends and the underlying `str` is used.
175 #[must_use]
176 #[cfg(all(not(feature = "safe_text"), feature = "unsafe_slice"))]
177 #[cfg_attr(feature = "nightly_doc", doc(cfg(feature = "unsafe_slice")))]
178 pub unsafe fn as_mut_str(&mut self) -> &mut str {
179 // SAFETY: caller must ensure safety
180 unsafe { &mut *(self.as_bytes_mut() as *mut [u8] as *mut str) }
181 }
182
183 /// Returns an iterator over the `chars` of this grapheme cluster.
184 pub fn chars(&self) -> IterChars {
185 self.as_str().chars()
186 }
187
188 /// Returns a new allocated C-compatible, nul-terminanted string.
189 #[must_use]
190 #[cfg(feature = "alloc")]
191 #[cfg_attr(feature = "nightly_doc", doc(cfg(feature = "alloc")))]
192 pub fn to_cstring(&self) -> CString {
193 CString::new(self.to_string()).unwrap()
194 }
195
196 /* operations */
197
198 /// Sets the length to 0, by resetting all bytes to 0.
199 #[rustfmt::skip]
200 pub fn clear(&mut self) { self.arr = [0; CAP]; }
201
202 /// Removes the last character and returns it, or `None` if
203 /// the string is empty.
204 #[must_use]
205 pub fn pop(&mut self) -> Option<char> {
206 if self.is_empty() {
207 None
208 } else {
209 Some(self.pop_unchecked())
210 }
211 }
212
213 /// Tries to remove the last character and return it.
214 ///
215 /// # Errors
216 /// Returns [`NotEnoughElements`] if the string is empty.
217 pub fn try_pop(&mut self) -> Result<char, NotEnoughElements> {
218 if self.is_empty() {
219 Err(NotEnoughElements(Some(1)))
220 } else {
221 Ok(self.pop_unchecked())
222 }
223 }
224
225 /// Removes the last character and returns it.
226 ///
227 /// # Panics
228 /// Panics if the string is empty.
229 #[must_use]
230 pub fn pop_unchecked(&mut self) -> char {
231 let len = self.len();
232 let mut idx_last_char = len - 1;
233 while idx_last_char > 0 && !self.as_str().is_char_boundary(idx_last_char) {
234 idx_last_char -= 1;
235 }
236 let last_char = self.as_str()[idx_last_char..len].chars().next().unwrap();
237 for i in idx_last_char..len {
238 self.arr[i] = 0;
239 }
240 last_char
241 }
242
243 /// Appends to the end of the string the given `character`.
244 ///
245 /// Returns the number of bytes written.
246 ///
247 /// It will return 0 bytes if the given `character` doesn't fit in
248 /// the remaining capacity, or if it is the nul character.
249 pub fn push(&mut self, character: char) -> usize {
250 let char_len = character.len_utf8();
251
252 if character != NUL_CHAR && self.remaining_capacity() >= char_len {
253 let len = self.len();
254 let new_len = len + char_len;
255
256 let _ = character.encode_utf8(&mut self.arr[len..new_len]);
257 char_len
258 } else {
259 0
260 }
261 }
262
263 /// Tries to append to the end of the string the given `character`.
264 ///
265 /// Returns the number of bytes written.
266 ///
267 /// Trying to push a nul character does nothing and returns 0 bytes.
268 ///
269 /// # Errors
270 /// Returns [`MismatchedCapacity`]
271 /// if the capacity is not enough to hold the given character.
272 pub fn try_push(&mut self, character: char) -> Result<usize, MismatchedCapacity> {
273 let char_len = character.len_utf8();
274
275 if character == NUL_CHAR {
276 Ok(0)
277 } else if self.remaining_capacity() >= char_len {
278 let len = self.len();
279 let new_len = len + char_len;
280
281 let _ = character.encode_utf8(&mut self.arr[len..new_len]);
282 Ok(char_len)
283 } else {
284 Err(MismatchedCapacity::closed(0, self.len() + character.len_utf8(), CAP))
285 }
286 }
287
288 /// Appends to the end the fitting characters from the given `string` slice.
289 ///
290 /// Nul characters will be stripped out.
291 ///
292 /// Returns the number of bytes written, which will be 0
293 /// if not even the first non-nul character can fit.
294 pub fn push_str(&mut self, string: &str) -> usize {
295 let mut rem_cap = self.remaining_capacity();
296 let mut bytes_written = 0;
297
298 for character in string.chars() {
299 if character != NUL_CHAR {
300 let char_len = character.len_utf8();
301
302 if char_len <= rem_cap {
303 self.push(character);
304 rem_cap -= char_len;
305 bytes_written += char_len;
306 } else {
307 break;
308 }
309 }
310 }
311 bytes_written
312 }
313
314 /// Tries to append to the end the fitting characters from the given `string`
315 /// slice.
316 ///
317 /// Nul characters will be stripped out.
318 ///
319 /// Returns the number of bytes written.
320 ///
321 /// # Errors
322 /// Returns [`MismatchedCapacity`] if the capacity is not enough
323 /// to hold even the first non-nul character.
324 pub fn try_push_str(&mut self, string: &str) -> Result<usize, MismatchedCapacity> {
325 let first_char_len = string.chars().find(|&c| c != NUL_CHAR).map_or(0, |c| c.len_utf8());
326 if self.remaining_capacity() < first_char_len {
327 Err(MismatchedCapacity::closed(0, self.len() + first_char_len, CAP))
328 } else {
329 Ok(self.push_str(string))
330 }
331 }
332
333 /// Tries to append the complete `string` slice to the end.
334 ///
335 /// Returns the number of bytes written in success.
336 ///
337 /// Nul characters will not be taken into account.
338 ///
339 /// # Errors
340 /// Returns [`MismatchedCapacity`] if the slice wont completely fit.
341 pub fn try_push_str_complete(&mut self, string: &str) -> Result<usize, MismatchedCapacity> {
342 let non_nul_len = string.as_bytes().iter().filter(|x| **x != 0).count();
343 if self.remaining_capacity() >= non_nul_len {
344 Ok(self.push_str(string))
345 } else {
346 Err(MismatchedCapacity::closed(0, self.len() + string.len(), CAP))
347 }
348 }
349
350 /* from char */
351
352 /// Creates a new `StringNonul` from a `char`.
353 ///
354 /// If `c` is NUL an empty string will be returned.
355 ///
356 /// # Errors
357 /// Returns [`MismatchedCapacity`] if `CAP` > [`u8::MAX`],
358 /// or if `!c.is_nul()` and `CAP` < `c.`[`len_utf8()`][Char::len_utf8].
359 ///
360 /// Will always succeed if `CAP` >= 4.
361 #[rustfmt::skip]
362 pub const fn from_char(c: char) -> Result<Self, MismatchedCapacity> {
363 let mut new = unwrap![ok? Self::new()];
364 if c != '\0' {
365 let bytes = Char::to_utf8_bytes(c);
366 let len = Char::utf8_len(bytes[0]) as usize;
367 iif![CAP < len; return Err(MismatchedCapacity::closed(0, len, CAP))];
368 new.arr[0] = bytes[0];
369 if len > 1 { new.arr[1] = bytes[1]; }
370 if len > 2 { new.arr[2] = bytes[2]; }
371 if len > 3 { new.arr[3] = bytes[3]; }
372 }
373 Ok(new)
374 }
375
376 /// Creates a new `StringNonul` from a `char7`.
377 ///
378 /// If `c`.[`is_nul()`][char7#method.is_nul] an empty string will be returned.
379 ///
380 /// # Errors
381 /// Returns [`MismatchedCapacity`] if `CAP` > [`u8::MAX`],
382 /// or if `!c.is_nul()` and `CAP` < 1.
383 ///
384 /// Will always succeed if `CAP` >= 1.
385 #[cfg(feature = "_char7")]
386 #[cfg_attr(feature = "nightly_doc", doc(cfg(feature = "_char7")))]
387 pub const fn from_char7(c: char7) -> Result<Self, MismatchedCapacity> {
388 let mut new = unwrap![ok? Self::new()];
389 if !c.is_nul() {
390 new.arr[0] = c.to_utf8_bytes()[0];
391 }
392 Ok(new)
393 }
394
395 /// Creates a new `StringNonul` from a `char8`.
396 ///
397 /// If `c`.[`is_nul()`][char8#method.is_nul] an empty string will be returned.
398 ///
399 /// # Errors
400 /// Returns [`MismatchedCapacity`] if `CAP` > [`u8::MAX`],
401 /// or if `!c.is_nul()` and `CAP` < `c.`[`len_utf8()`][char8#method.len_utf8].
402 ///
403 /// Will always succeed if `CAP` >= 2.
404 #[rustfmt::skip]
405 #[cfg(feature = "_char8")]
406 #[cfg_attr(feature = "nightly_doc", doc(cfg(feature = "_char8")))]
407 pub const fn from_char8(c: char8) -> Result<Self, MismatchedCapacity> {
408 let mut new = unwrap![ok? Self::new()];
409 if !c.is_nul() {
410 let bytes = c.to_utf8_bytes();
411 let len = Char::utf8_len(bytes[0]) as usize;
412 iif![CAP < len; return Err(MismatchedCapacity::closed(0, len, CAP))];
413 new.arr[0] = bytes[0];
414 if len > 1 { new.arr[1] = bytes[1]; }
415 }
416 Ok(new)
417 }
418
419 /// Creates a new `StringNonul` from a `char16`.
420 ///
421 /// If `c`.[`is_nul()`][char16#method.is_nul] an empty string will be returned.
422 ///
423 /// # Errors
424 /// Returns [`MismatchedCapacity`] if `CAP` > [`u8::MAX`],
425 /// or if `!c.is_nul()` and `CAP` < `c.`[`len_utf8()`][char16#method.len_utf8].
426 ///
427 /// Will always succeed if `CAP` >= 3.
428 #[rustfmt::skip]
429 #[cfg(feature = "_char16")]
430 #[cfg_attr(feature = "nightly_doc", doc(cfg(feature = "_char16")))]
431 pub const fn from_char16(c: char16) -> Result<Self, MismatchedCapacity> {
432 let mut new = unwrap![ok? Self::new()];
433 if !c.is_nul() {
434 let bytes = c.to_utf8_bytes();
435 let len = Char::utf8_len(bytes[0]) as usize;
436 iif![CAP < len; return Err(MismatchedCapacity::closed(0, len, CAP))];
437 new.arr[0] = bytes[0];
438 if len > 1 { new.arr[1] = bytes[1]; }
439 if len > 2 { new.arr[2] = bytes[2]; }
440 }
441 Ok(new)
442 }
443
444 /* from bytes */
445
446 /// Returns a string from an array of `bytes`.
447 ///
448 /// # Errors
449 /// Returns [`InvalidText::Utf8`] if the bytes are not valid UTF-8,
450 /// and [`InvalidText::Char`] if the bytes contains a NUL character.
451 pub const fn from_bytes(bytes: [u8; CAP]) -> Result<Self, InvalidText> {
452 // WAIT: [const_methods](https://github.com/rusticstuff/simdutf8/pull/111)
453 match ::core::str::from_utf8(&bytes) {
454 Ok(_) => {
455 cfor![index in 0..CAP => {
456 iif![bytes[index] == 0; return Err(InvalidText::Char('\0'))];
457 }];
458 Ok(Self { arr: bytes })
459 }
460 Err(e) => Err(InvalidText::from_utf8_error(e)),
461 }
462 }
463
464 /// Returns a string from an array of `bytes` that must be valid UTF-8.
465 ///
466 /// # Safety
467 /// The caller must ensure that the content of the slice is valid UTF-8,
468 /// and that it doesn't contain nul characters.
469 ///
470 /// Use of a `str` whose contents are not valid UTF-8 is undefined behavior.
471 #[cfg(all(not(feature = "safe_text"), feature = "unsafe_str"))]
472 #[cfg_attr(feature = "nightly_doc", doc(cfg(feature = "unsafe_str")))]
473 pub const unsafe fn from_bytes_unchecked(bytes: [u8; CAP]) -> Self {
474 Self { arr: bytes }
475 }
476}
477
478/* traits implementations */
479
480impl<const CAP: usize> Default for StringNonul<CAP> {
481 /// Returns an empty string.
482 ///
483 /// # Panics
484 /// Panics if `CAP > [`u8::MAX`]`.
485 fn default() -> Self {
486 Self::new().unwrap()
487 }
488}
489impl<const CAP: usize> ConstDefault for StringNonul<CAP> {
490 /// Returns an empty string.
491 ///
492 /// # Panics
493 /// Panics if `CAP > [`u8::MAX`]`.
494 const DEFAULT: Self = unwrap![ok Self::new()];
495}
496
497impl<const CAP: usize> fmt::Display for StringNonul<CAP> {
498 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
499 write!(f, "{}", self.as_str())
500 }
501}
502impl<const CAP: usize> fmt::Debug for StringNonul<CAP> {
503 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
504 write!(f, "{:?}", self.as_str())
505 }
506}
507
508impl<const CAP: usize> PartialEq<&str> for StringNonul<CAP> {
509 #[must_use] #[rustfmt::skip]
510 fn eq(&self, slice: &&str) -> bool { self.as_str() == *slice }
511}
512impl<const CAP: usize> PartialEq<StringNonul<CAP>> for &str {
513 #[must_use] #[rustfmt::skip]
514 fn eq(&self, string: &StringNonul<CAP>) -> bool { *self == string.as_str() }
515}
516
517impl<const CAP: usize> Deref for StringNonul<CAP> {
518 type Target = str;
519 #[must_use] #[rustfmt::skip]
520 fn deref(&self) -> &Self::Target { self.as_str() }
521}
522
523impl<const CAP: usize> AsRef<str> for StringNonul<CAP> {
524 #[must_use] #[rustfmt::skip]
525 fn as_ref(&self) -> &str { self.as_str() }
526}
527
528impl<const CAP: usize> AsRef<[u8]> for StringNonul<CAP> {
529 #[must_use] #[rustfmt::skip]
530 fn as_ref(&self) -> &[u8] { self.as_bytes() }
531}
532
533impl<const CAP: usize> TryFrom<&str> for StringNonul<CAP> {
534 type Error = MismatchedCapacity;
535
536 /// Tries to create a new `StringNonul` from the given string slice.
537 ///
538 /// # Errors
539 /// Returns [`MismatchedCapacity`] if `CAP > `[`u8::MAX`] or if `CAP < str.len()`.
540 fn try_from(string: &str) -> Result<Self, MismatchedCapacity> {
541 let non_nul_len = string.as_bytes().iter().filter(|x| **x != 0).count();
542 if CAP < non_nul_len {
543 Err(MismatchedCapacity::closed_open(0, non_nul_len, CAP))
544 } else {
545 let mut new_string = Self::new()?;
546 let copied_bytes = new_string.push_str(string);
547 debug_assert_eq![non_nul_len, copied_bytes];
548 Ok(new_string)
549 }
550 }
551}
552
553impl<const CAP: usize> TryFrom<&[u8]> for StringNonul<CAP> {
554 type Error = InvalidText;
555
556 /// Tries to create a new `StringNonul` from the given slice of `bytes`.
557 ///
558 /// The string will stop before the first nul character or the end of the slice.
559 ///
560 /// # Errors
561 /// Returns [`InvalidText::Capacity`] if `CAP > `[u8::MAX`] or if `CAP < bytes.len()`
562 /// or [`InvalidText::Utf8`] if the `bytes` are not valid UTF-8.
563 // WAIT: [const_methods](https://github.com/rusticstuff/simdutf8/pull/111)
564 fn try_from(bytes: &[u8]) -> Result<Self, InvalidText> {
565 if bytes.len() >= CAP {
566 #[rustfmt::skip]
567 return Err(InvalidText::Capacity(
568 Mismatch::in_closed_interval(0, bytes.len(), CAP, "")));
569 }
570 match ::core::str::from_utf8(bytes) {
571 Ok(_) => {
572 let mut arr = [0; CAP];
573 let mut idx = 0;
574
575 for &byte in bytes.iter() {
576 if byte != 0 {
577 arr[idx] = byte;
578 idx += 1;
579 }
580 }
581 Ok(Self { arr })
582 }
583 Err(e) => Err(InvalidText::from_utf8_error(e)),
584 }
585 }
586}