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}