Skip to main content

smart_string/pascal_string/
mod.rs

1use std::borrow::Borrow;
2use std::borrow::BorrowMut;
3use std::borrow::Cow;
4use std::cmp;
5use std::fmt;
6use std::hash::Hash;
7use std::hash::Hasher;
8use std::ops;
9use std::rc::Rc;
10use std::str::from_utf8_unchecked;
11use std::str::from_utf8_unchecked_mut;
12use std::sync::Arc;
13
14use crate::DisplayExt;
15
16mod error;
17#[cfg(feature = "serde")]
18mod with_serde;
19
20pub use error::InsertError;
21pub use error::RemoveError;
22pub use error::ReplaceRangeError;
23pub use error::SplitOffError;
24pub use error::TryFromBytesError;
25pub use error::TryFromStrError;
26
27#[derive(Clone, Copy)]
28#[repr(C)]
29pub struct PascalString<const CAPACITY: usize> {
30    len: u8,
31    data: [u8; CAPACITY],
32}
33
34/// Snapshot `$ps.data[..len]` into a stack buffer and bind `$name: &str` to it.
35///
36/// Used by `retain` and `remove_matches` to iterate over the original content
37/// while mutating `self.data` in place. Centralizes the one `unsafe` call
38/// (`from_utf8_unchecked`) that relies on the PascalString UTF-8 invariant.
39macro_rules! snapshot_as_str {
40    ($ps:expr, $buf:ident, $name:ident) => {
41        let len = $ps.len();
42        let mut $buf = [0_u8; CAPACITY];
43        $buf[..len].copy_from_slice(&$ps.data[..len]);
44        // SAFETY: `$buf[..len]` is a byte-for-byte copy of a PascalString which maintains UTF-8.
45        let $name = unsafe { from_utf8_unchecked(&$buf[..len]) };
46    };
47}
48
49impl<const CAPACITY: usize> PascalString<CAPACITY> {
50    #[inline]
51    fn validate_range_bounds(&self, start: usize, end: usize) -> Result<(), ReplaceRangeError> {
52        let len = self.len();
53        if start > end || end > len {
54            return Err(ReplaceRangeError::OutOfBounds { start, end, len });
55        }
56        if !self.is_char_boundary(start) {
57            return Err(ReplaceRangeError::NotCharBoundary { idx: start });
58        }
59        if !self.is_char_boundary(end) {
60            return Err(ReplaceRangeError::NotCharBoundary { idx: end });
61        }
62        Ok(())
63    }
64
65    #[inline]
66    fn try_replace_range_bounds_impl(
67        &mut self,
68        start: usize,
69        end: usize,
70        replace_with: &str,
71    ) -> Result<(), ReplaceRangeError> {
72        self.validate_range_bounds(start, end)?;
73
74        let len = self.len();
75        let range_len = end - start;
76        let repl_len = replace_with.len();
77        let new_len = len - range_len + repl_len;
78        if new_len > CAPACITY {
79            return Err(ReplaceRangeError::TooLong);
80        }
81
82        if repl_len > range_len {
83            let delta = repl_len - range_len;
84            self.data.copy_within(end..len, end + delta);
85        } else if repl_len < range_len {
86            let new_tail_start = start + repl_len;
87            self.data.copy_within(end..len, new_tail_start);
88            // deterministic tail bytes
89            self.data[new_len..len].fill(0);
90        }
91
92        self.data[start..start + repl_len].copy_from_slice(replace_with.as_bytes());
93        self.len = new_len as u8;
94        Ok(())
95    }
96
97    /// Replaces the specified byte range with `replace_with`.
98    ///
99    /// This is a true `try_` API: it **never panics**. All failure modes are returned as `ReplaceRangeError`.
100    #[inline]
101    pub fn try_replace_range_bounds(
102        &mut self,
103        start: usize,
104        end: usize,
105        replace_with: &str,
106    ) -> Result<(), ReplaceRangeError> {
107        self.try_replace_range_bounds_impl(start, end, replace_with)
108    }
109
110    /// Replaces the specified byte range with a prefix of `replace_with` that fits in capacity.
111    ///
112    /// Returns the remainder of `replace_with` that did not fit.
113    ///
114    /// This is a true `try_` API: it **never panics**. Range/boundary errors are returned as `ReplaceRangeError`.
115    #[inline]
116    pub fn try_replace_range_bounds_truncated<'s>(
117        &mut self,
118        start: usize,
119        end: usize,
120        replace_with: &'s str,
121    ) -> Result<&'s str, ReplaceRangeError> {
122        self.validate_range_bounds(start, end)?;
123
124        let len = self.len();
125        let range_len = end - start;
126        let max_repl_len = CAPACITY - (len - range_len);
127        if replace_with.len() <= max_repl_len {
128            self.try_replace_range_bounds_impl(start, end, replace_with)?;
129            return Ok("");
130        }
131
132        let mut prefix_len = 0;
133        for c in replace_with.chars() {
134            let l = c.len_utf8();
135            if prefix_len + l > max_repl_len {
136                break;
137            }
138            prefix_len += l;
139        }
140
141        let (prefix, remainder) = replace_with.split_at(prefix_len);
142        self.try_replace_range_bounds_impl(start, end, prefix)?;
143        Ok(remainder)
144    }
145
146    /// Panicking variant of `try_replace_range_bounds`.
147    #[inline]
148    pub fn replace_range_bounds_expect_capacity(
149        &mut self,
150        start: usize,
151        end: usize,
152        replace_with: &str,
153    ) {
154        self.try_replace_range_bounds(start, end, replace_with)
155            .expect("PascalString replace_range failed");
156    }
157
158    /// Panicking + truncating variant of `try_replace_range_bounds_truncated`.
159    #[inline]
160    pub fn replace_range_bounds_truncated<'s>(
161        &mut self,
162        start: usize,
163        end: usize,
164        replace_with: &'s str,
165    ) -> &'s str {
166        self.try_replace_range_bounds_truncated(start, end, replace_with)
167            .expect("invalid range or char boundary")
168    }
169    pub const CAPACITY: usize = {
170        assert!(
171            CAPACITY <= u8::MAX as usize,
172            "PascalString max capacity is 255"
173        );
174        CAPACITY
175    };
176
177    #[inline(always)]
178    pub const fn new() -> Self {
179        // This line triggers a compile time error, if CAPACITY > 255.
180        // TODO look for a better way to assert CAPACITY.
181        let _ = Self::CAPACITY;
182
183        Self {
184            len: 0,
185            data: [0; CAPACITY],
186        }
187    }
188
189    /// Creates a new `PascalString<CAPACITY>` instance from a `&str` within a const context.
190    /// This implementation prioritizes const context compatibility over performance.
191    /// If a const context is not required, use `try_from` for better performance.
192    /// In the future, once const in trait methods is stabilized, this method will be deprecated
193    /// in favor of `try_from`.
194    pub const fn try_from_str_const(string: &str) -> Option<Self> {
195        let _ = Self::CAPACITY;
196
197        if string.len() > CAPACITY {
198            return None;
199        }
200        let mut this = PascalString {
201            len: string.len() as u8,
202            data: [0; CAPACITY],
203        };
204        let bytes = string.as_bytes();
205        let mut i = 0;
206        while i < string.len() {
207            this.data[i] = bytes[i];
208            i += 1;
209        }
210        Some(this)
211    }
212
213    /// Creates a new `PascalString<CAPACITY>` instance from a `&str`.
214    /// If the length of the string exceeds `CAPACITY`,
215    /// the string is truncated at the nearest valid UTF-8 boundary
216    /// to ensure its length does not exceed `CAPACITY`.
217    #[inline]
218    pub fn from_str_truncated(string: &str) -> Self {
219        let _ = Self::CAPACITY;
220
221        if let Ok(ps) = Self::try_from(string) {
222            return ps;
223        }
224
225        let mut ps = Self::new();
226        ps.push_str_truncated(string);
227        ps
228    }
229
230    #[inline(always)]
231    pub const fn into_inner(self) -> (u8, [u8; CAPACITY]) {
232        (self.len, self.data)
233    }
234
235    #[inline(always)]
236    pub const fn capacity(&self) -> usize {
237        CAPACITY
238    }
239
240    #[inline(always)]
241    pub const fn len(&self) -> usize {
242        self.len as usize
243    }
244
245    #[inline(always)]
246    pub const fn is_empty(&self) -> bool {
247        self.len == 0
248    }
249
250    #[inline(always)]
251    pub fn as_str(&self) -> &str {
252        self
253    }
254
255    /// Returns the underlying UTF-8 bytes.
256    ///
257    /// This is equivalent to `self.as_str().as_bytes()`, but provided as an inherent method for
258    /// `std::string::String` API parity and rustdoc discoverability.
259    #[inline]
260    #[must_use]
261    pub fn as_bytes(&self) -> &[u8] {
262        self.as_str().as_bytes()
263    }
264
265    #[inline(always)]
266    pub fn as_mut_str(&mut self) -> &mut str {
267        self
268    }
269
270    #[inline(always)]
271    #[deprecated(
272        note = "Use `as_mut_str()` (this method name suggests `&mut str` but returns `&str`)."
273    )]
274    pub fn as_str_mut(&mut self) -> &str {
275        self
276    }
277
278    #[inline]
279    pub fn try_push_str(&mut self, string: &str) -> Result<(), TryFromStrError> {
280        let len = self.len();
281        let new_len = len + string.len();
282
283        if new_len > CAPACITY {
284            return Err(TryFromStrError::TooLong);
285        }
286
287        self.data[len..new_len].copy_from_slice(string.as_bytes());
288        self.len = new_len as u8;
289
290        Ok(())
291    }
292
293    #[inline]
294    pub fn try_push(&mut self, ch: char) -> Result<(), TryFromStrError> {
295        // TODO special case for ch.len_utf8() == 1
296        self.try_push_str(ch.encode_utf8(&mut [0; 4]))
297    }
298
299    /// Appends a string slice, panicking if the capacity would be exceeded.
300    ///
301    /// This mirrors `String::push_str`’s “cannot fail” ergonomics; use `try_push_str` if you want a recoverable error.
302    #[inline]
303    #[deprecated(
304        note = "PascalString is fixed-capacity; prefer `try_push_str`, `push_str_truncated`, or `push_str_expect_capacity`."
305    )]
306    pub fn push_str(&mut self, string: &str) {
307        self.push_str_expect_capacity(string);
308    }
309
310    /// Appends a character, panicking if the capacity would be exceeded.
311    ///
312    /// This mirrors `String::push`’s “cannot fail” ergonomics; use `try_push` if you want a recoverable error.
313    #[inline]
314    #[deprecated(
315        note = "PascalString is fixed-capacity; prefer `try_push`, `push_str_truncated`, or `push_expect_capacity`."
316    )]
317    pub fn push(&mut self, ch: char) {
318        self.push_expect_capacity(ch);
319    }
320
321    /// Appends a string slice, panicking if the capacity would be exceeded.
322    #[inline]
323    pub fn push_str_expect_capacity(&mut self, string: &str) {
324        self.try_push_str(string)
325            .expect("PascalString capacity exceeded");
326    }
327
328    /// Appends a character, panicking if the capacity would be exceeded.
329    #[inline]
330    pub fn push_expect_capacity(&mut self, ch: char) {
331        self.try_push(ch).expect("PascalString capacity exceeded");
332    }
333
334    /// Inserts a string slice at the given byte index.
335    ///
336    /// This is a true `try_` API: it **never panics**. All failure modes are returned as `InsertError`.
337    #[inline]
338    pub fn try_insert_str(&mut self, idx: usize, string: &str) -> Result<(), InsertError> {
339        let len = self.len();
340        if idx > len {
341            return Err(InsertError::OutOfBounds { idx, len });
342        }
343        if !self.is_char_boundary(idx) {
344            return Err(InsertError::NotCharBoundary { idx });
345        }
346
347        let insert_len = string.len();
348        let new_len = len + insert_len;
349        if new_len > CAPACITY {
350            return Err(InsertError::TooLong);
351        }
352
353        // Shift tail to make room.
354        self.data.copy_within(idx..len, idx + insert_len);
355        // Copy inserted bytes.
356        self.data[idx..idx + insert_len].copy_from_slice(string.as_bytes());
357        self.len = new_len as u8;
358        Ok(())
359    }
360
361    /// Inserts a string slice at the given byte index, truncating the inserted string to available capacity.
362    ///
363    /// Returns the remainder that did not fit.
364    ///
365    /// This is a true `try_` API: it **never panics**. Index/boundary errors are returned as `InsertError`.
366    #[inline]
367    pub fn try_insert_str_truncated<'s>(
368        &mut self,
369        idx: usize,
370        string: &'s str,
371    ) -> Result<&'s str, InsertError> {
372        let len = self.len();
373        if idx > len {
374            return Err(InsertError::OutOfBounds { idx, len });
375        }
376        if !self.is_char_boundary(idx) {
377            return Err(InsertError::NotCharBoundary { idx });
378        }
379
380        let available = CAPACITY.saturating_sub(len);
381        if available >= string.len() {
382            self.try_insert_str(idx, string)?;
383            return Ok("");
384        }
385
386        let mut prefix_len = 0;
387        for c in string.chars() {
388            let l = c.len_utf8();
389            if prefix_len + l > available {
390                break;
391            }
392            prefix_len += l;
393        }
394
395        let (prefix, remainder) = string.split_at(prefix_len);
396        // Prefix is constructed from `chars()` boundaries, so it is valid UTF-8 and fits by construction.
397        self.try_insert_str(idx, prefix)?;
398        Ok(remainder)
399    }
400
401    /// Inserts a string slice at the given byte index, truncating to capacity, panicking on invalid index/boundary.
402    ///
403    /// Returns the remainder that did not fit.
404    #[inline]
405    pub fn insert_str_truncated<'s>(&mut self, idx: usize, string: &'s str) -> &'s str {
406        self.try_insert_str_truncated(idx, string)
407            .expect("invalid index or char boundary")
408    }
409
410    /// Inserts a string slice at the given byte index, panicking if the capacity would be exceeded.
411    ///
412    /// This is an explicit opt-in panicking API for fixed-capacity strings.
413    #[inline]
414    pub fn insert_str_expect_capacity(&mut self, idx: usize, string: &str) {
415        self.try_insert_str(idx, string)
416            .expect("PascalString insert failed");
417    }
418
419    /// Inserts a string slice at the given byte index, panicking if the capacity would be exceeded.
420    #[inline]
421    #[deprecated(
422        note = "PascalString is fixed-capacity; prefer `try_insert_str`, `try_insert_str_truncated`, or `insert_str_expect_capacity`."
423    )]
424    pub fn insert_str(&mut self, idx: usize, string: &str) {
425        self.insert_str_expect_capacity(idx, string);
426    }
427
428    /// Inserts a character at the given byte index.
429    ///
430    /// This is a true `try_` API: it **never panics**. All failure modes are returned as `InsertError`.
431    #[inline]
432    pub fn try_insert(&mut self, idx: usize, ch: char) -> Result<(), InsertError> {
433        let mut buf = [0_u8; 4];
434        let s = ch.encode_utf8(&mut buf);
435        self.try_insert_str(idx, s)
436    }
437
438    /// Inserts a character at the given byte index, panicking if the capacity would be exceeded.
439    #[inline]
440    pub fn insert_expect_capacity(&mut self, idx: usize, ch: char) {
441        self.try_insert(idx, ch)
442            .expect("PascalString insert failed");
443    }
444
445    /// Inserts a character at the given byte index, panicking if the capacity would be exceeded.
446    #[inline]
447    #[deprecated(
448        note = "PascalString is fixed-capacity; prefer `try_insert`, `try_insert_str_truncated`, or `insert_expect_capacity`."
449    )]
450    pub fn insert(&mut self, idx: usize, ch: char) {
451        self.insert_expect_capacity(idx, ch);
452    }
453
454    /// Removes and returns the `char` at the given byte index.
455    ///
456    /// # Panics
457    ///
458    /// - If `idx >= self.len()`
459    /// - If `idx` is not on a UTF-8 character boundary
460    #[inline]
461    pub fn remove(&mut self, idx: usize) -> char {
462        let len = self.len();
463        assert!(idx < len, "index out of bounds");
464        assert!(self.is_char_boundary(idx), "index is not a char boundary");
465
466        let ch = self.as_str()[idx..].chars().next().expect("idx < len");
467        let ch_len = ch.len_utf8();
468
469        // Shift tail left to close the gap.
470        self.data.copy_within(idx + ch_len..len, idx);
471        let new_len = len - ch_len;
472        self.len = new_len as u8;
473
474        // Keep deterministic contents beyond len (not required for soundness, but helps debugging/tests).
475        self.data[new_len..len].fill(0);
476
477        ch
478    }
479
480    /// Removes and returns the `char` at the given byte index.
481    ///
482    /// This is a true `try_` API: it **never panics**. All failure modes are returned as `RemoveError`.
483    #[inline]
484    pub fn try_remove(&mut self, idx: usize) -> Result<char, RemoveError> {
485        let len = self.len();
486        if idx >= len {
487            return Err(RemoveError::OutOfBounds { idx, len });
488        }
489        if !self.is_char_boundary(idx) {
490            return Err(RemoveError::NotCharBoundary { idx });
491        }
492        Ok(self.remove(idx))
493    }
494
495    /// Returns the remainder of the string that was not pushed.
496    #[inline]
497    pub fn push_str_truncated<'s>(&mut self, string: &'s str) -> &'s str {
498        if self.try_push_str(string).is_ok() {
499            return "";
500        }
501
502        // TODO is there more efficient way to do this?
503        //   Maybe iter four bytes from the end of the slice and find the UTF-8 boundary?
504
505        let mut new_len = self.len();
506        for c in string.chars() {
507            let len = c.len_utf8();
508            if new_len + len > CAPACITY {
509                break;
510            };
511            new_len += len;
512        }
513
514        let pos = new_len - self.len();
515        let (substring, remainder) = string.split_at(pos);
516        self.try_push_str(substring).unwrap();
517
518        remainder
519    }
520
521    #[inline]
522    pub fn truncate(&mut self, new_len: usize) {
523        if new_len <= self.len() {
524            assert!(self.is_char_boundary(new_len));
525            self.len = new_len as u8;
526        }
527    }
528
529    #[inline]
530    pub fn pop(&mut self) -> Option<char> {
531        let ch = self.chars().next_back()?;
532        let newlen = self.len() - ch.len_utf8();
533        self.len = newlen as u8;
534        Some(ch)
535    }
536
537    #[inline]
538    pub fn clear(&mut self) {
539        self.len = 0;
540    }
541
542    /// Retains only the characters specified by the predicate.
543    ///
544    /// This never panics and cannot overflow capacity, because the resulting string is never longer
545    /// than the original.
546    #[inline]
547    pub fn retain<F>(&mut self, mut f: F)
548    where
549        F: FnMut(char) -> bool,
550    {
551        snapshot_as_str!(self, src, s);
552        let len = s.len();
553
554        let mut write = 0_usize;
555        let mut buf = [0_u8; 4];
556        for ch in s.chars() {
557            if f(ch) {
558                let part = ch.encode_utf8(&mut buf);
559                let bytes = part.as_bytes();
560                let end = write + bytes.len();
561                // `write` only advances by bytes from the original string, so `end <= len <= CAPACITY`.
562                self.data[write..end].copy_from_slice(bytes);
563                write = end;
564            }
565        }
566
567        // Deterministic tail bytes.
568        self.data[write..len].fill(0);
569        self.len = write as u8;
570    }
571
572    /// Splits the string into two at the given byte index.
573    ///
574    /// Returns a new `PascalString` containing the bytes `[at, len)`.
575    /// `self` is truncated to `[0, at)`.
576    ///
577    /// # Panics
578    ///
579    /// - If `at > self.len()`
580    /// - If `at` is not on a UTF-8 character boundary
581    #[inline]
582    #[must_use]
583    pub fn split_off(&mut self, at: usize) -> PascalString<CAPACITY> {
584        assert!(
585            self.as_str().is_char_boundary(at),
586            "split_off: index {at} is not a char boundary (len={})",
587            self.len()
588        );
589        // `is_char_boundary` returns false for at > len, so the assert above covers that too.
590        // Build the tail before modifying self.
591        let tail = &self.as_str()[at..];
592        // SAFETY: tail is a substring of a valid PascalString; its length <= CAPACITY.
593        let result = PascalString::try_from(tail).expect("tail fits in same capacity");
594        self.truncate(at);
595        result
596    }
597
598    /// Non-panicking version of `split_off`.
599    ///
600    /// Returns `Err(SplitOffError)` if `at > self.len()` or `at` is not on a char boundary.
601    #[inline]
602    pub fn try_split_off(&mut self, at: usize) -> Result<PascalString<CAPACITY>, SplitOffError> {
603        let len = self.len();
604        if at > len {
605            return Err(SplitOffError::OutOfBounds { at, len });
606        }
607        if !self.as_str().is_char_boundary(at) {
608            return Err(SplitOffError::NotCharBoundary { at });
609        }
610        // SAFETY: bounds and boundary already checked above.
611        let tail = &self.as_str()[at..];
612        let result = PascalString::try_from(tail).expect("tail fits in same capacity");
613        self.truncate(at);
614        Ok(result)
615    }
616
617    /// Removes all non-overlapping occurrences of `pat` from the string in place.
618    ///
619    /// If `pat` is empty, the string is unchanged.
620    ///
621    /// This never panics and cannot overflow capacity because the result is never longer than the
622    /// original.
623    #[inline]
624    pub fn remove_matches(&mut self, pat: &str) {
625        if pat.is_empty() || self.is_empty() {
626            return;
627        }
628
629        snapshot_as_str!(self, src, s);
630        let len = s.len();
631
632        let mut read = 0_usize;
633        let mut write = 0_usize;
634
635        while read < len {
636            if s[read..].starts_with(pat) {
637                read += pat.len();
638            } else {
639                // Advance by one char to avoid splitting multi-byte sequences.
640                let ch_len = s[read..]
641                    .chars()
642                    .next()
643                    .expect("read < len implies at least one char")
644                    .len_utf8();
645                self.data[write..write + ch_len].copy_from_slice(&src[read..read + ch_len]);
646                write += ch_len;
647                read += ch_len;
648            }
649        }
650
651        // Deterministic tail bytes.
652        self.data[write..len].fill(0);
653        self.len = write as u8;
654    }
655}
656
657// -- Common traits --------------------------------------------------------------------------------
658
659impl<const CAPACITY: usize> Default for PascalString<CAPACITY> {
660    #[inline(always)]
661    fn default() -> Self {
662        let _ = Self::CAPACITY;
663
664        Self::new()
665    }
666}
667
668impl<T: ops::Deref<Target = str> + ?Sized, const CAPACITY: usize> PartialEq<T>
669    for PascalString<CAPACITY>
670{
671    #[inline(always)]
672    fn eq(&self, other: &T) -> bool {
673        self.as_str().eq(other.deref())
674    }
675}
676
677macro_rules! impl_reverse_eq_for_str_types {
678    ($($t:ty),*) => {
679        $(
680            impl<const CAPACITY: usize> PartialEq<PascalString<CAPACITY>> for $t {
681                #[inline(always)]
682                fn eq(&self, other: &PascalString<CAPACITY>) -> bool {
683                    let a: &str = self.as_ref();
684                    let b = other.as_str();
685                    a.eq(b)
686                }
687            }
688
689            impl<const CAPACITY: usize> PartialEq<PascalString<CAPACITY>> for &$t {
690                #[inline(always)]
691                fn eq(&self, other: &PascalString<CAPACITY>) -> bool {
692                    let a: &str = self.as_ref();
693                    let b = other.as_str();
694                    a.eq(b)
695                }
696            }
697
698            impl<const CAPACITY: usize> PartialEq<PascalString<CAPACITY>> for &mut $t {
699                #[inline(always)]
700                fn eq(&self, other: &PascalString<CAPACITY>) -> bool {
701                    let a: &str = self.as_ref();
702                    let b = other.as_str();
703                    a.eq(b)
704                }
705            }
706        )*
707    };
708}
709
710impl_reverse_eq_for_str_types!(String, str, Cow<'_, str>, Box<str>, Rc<str>, Arc<str>);
711
712impl<const CAPACITY: usize> Eq for PascalString<CAPACITY> {}
713
714impl<T: ops::Deref<Target = str>, const CAPACITY: usize> PartialOrd<T> for PascalString<CAPACITY> {
715    #[inline(always)]
716    fn partial_cmp(&self, other: &T) -> Option<cmp::Ordering> {
717        self.as_str().partial_cmp(other.deref())
718    }
719}
720
721impl<const CAPACITY: usize> Ord for PascalString<CAPACITY> {
722    #[inline(always)]
723    fn cmp(&self, other: &Self) -> cmp::Ordering {
724        self.as_str().cmp(other.as_str())
725    }
726}
727
728impl<const CAPACITY: usize> Hash for PascalString<CAPACITY> {
729    #[inline(always)]
730    fn hash<H: Hasher>(&self, state: &mut H) {
731        self.as_str().hash(state)
732    }
733}
734
735// -- Formatting -----------------------------------------------------------------------------------
736
737impl<const CAPACITY: usize> fmt::Debug for PascalString<CAPACITY> {
738    #[inline(always)]
739    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
740        let name: PascalString<39> = format_args!("PascalString<{CAPACITY}>")
741            .try_to_fmt()
742            .unwrap_or_else(|_| "PascalString<?>".to_fmt());
743        f.debug_tuple(&name).field(&self.as_str()).finish()
744    }
745}
746
747impl<const CAPACITY: usize> fmt::Display for PascalString<CAPACITY> {
748    #[inline(always)]
749    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
750        self.as_str().fmt(f)
751    }
752}
753
754// -- Reference ------------------------------------------------------------------------------------
755
756impl<const CAPACITY: usize> ops::Deref for PascalString<CAPACITY> {
757    type Target = str;
758
759    #[inline(always)]
760    fn deref(&self) -> &Self::Target {
761        // SAFETY: PascalString maintains its length invariant.
762        let bytes = unsafe { self.data.get_unchecked(..self.len()) };
763        // SAFETY: PascalString maintains its utf8 invariant.
764        unsafe { from_utf8_unchecked(bytes) }
765    }
766}
767
768impl<const CAPACITY: usize> ops::DerefMut for PascalString<CAPACITY> {
769    #[inline(always)]
770    fn deref_mut(&mut self) -> &mut Self::Target {
771        let len = self.len();
772        // SAFETY: PascalString maintains its length invariant.
773        let bytes = unsafe { self.data.get_unchecked_mut(..len) };
774        // SAFETY: PascalString maintains its utf8 invariant.
775        unsafe { from_utf8_unchecked_mut(bytes) }
776    }
777}
778
779impl<const CAPACITY: usize> Borrow<str> for PascalString<CAPACITY> {
780    #[inline(always)]
781    fn borrow(&self) -> &str {
782        self
783    }
784}
785
786impl<const CAPACITY: usize> AsRef<str> for PascalString<CAPACITY> {
787    #[inline(always)]
788    fn as_ref(&self) -> &str {
789        self
790    }
791}
792
793impl<const CAPACITY: usize> AsRef<[u8]> for PascalString<CAPACITY> {
794    #[inline(always)]
795    fn as_ref(&self) -> &[u8] {
796        self.as_bytes()
797    }
798}
799
800impl<const CAPACITY: usize> AsMut<str> for PascalString<CAPACITY> {
801    #[inline(always)]
802    fn as_mut(&mut self) -> &mut str {
803        self
804    }
805}
806
807impl<const CAPACITY: usize> BorrowMut<str> for PascalString<CAPACITY> {
808    #[inline(always)]
809    fn borrow_mut(&mut self) -> &mut str {
810        self
811    }
812}
813
814// -- Conversion -----------------------------------------------------------------------------------
815
816impl<'a, const CAPACITY: usize> TryFrom<&'a [u8]> for PascalString<CAPACITY> {
817    type Error = TryFromBytesError;
818
819    #[inline]
820    fn try_from(bytes: &'a [u8]) -> Result<PascalString<CAPACITY>, Self::Error> {
821        let _ = Self::CAPACITY;
822
823        let string = core::str::from_utf8(bytes)?;
824        Ok(Self::try_from(string)?)
825    }
826}
827
828impl<'a, const CAPACITY: usize> TryFrom<&'a mut str> for PascalString<CAPACITY> {
829    type Error = TryFromStrError;
830
831    #[inline]
832    fn try_from(value: &'a mut str) -> Result<PascalString<CAPACITY>, Self::Error> {
833        Self::try_from(&*value)
834    }
835}
836
837impl<'a, const CAPACITY: usize> TryFrom<&'a str> for PascalString<CAPACITY> {
838    type Error = TryFromStrError;
839
840    #[inline]
841    fn try_from(value: &'a str) -> Result<PascalString<CAPACITY>, Self::Error> {
842        let _ = Self::CAPACITY;
843
844        let bytes = value.as_bytes();
845        let len = bytes.len();
846
847        if len > CAPACITY {
848            return Err(TryFromStrError::TooLong);
849        }
850
851        let data = match <[u8; CAPACITY]>::try_from(bytes).ok() {
852            Some(data) => data,
853            None => {
854                let mut data = [0; CAPACITY];
855                data[..len].copy_from_slice(bytes);
856                data
857            }
858        };
859
860        let len = len as u8;
861
862        Ok(PascalString { len, data })
863    }
864}
865
866impl<const CAPACITY: usize> TryFrom<char> for PascalString<CAPACITY> {
867    type Error = TryFromStrError;
868
869    #[inline]
870    fn try_from(value: char) -> Result<PascalString<CAPACITY>, Self::Error> {
871        let _ = Self::CAPACITY;
872
873        Self::try_from(value.encode_utf8(&mut [0; 4]))
874    }
875}
876
877impl<const CAPACITY: usize> std::str::FromStr for PascalString<CAPACITY> {
878    type Err = TryFromStrError;
879
880    #[inline]
881    fn from_str(s: &str) -> Result<Self, Self::Err> {
882        Self::try_from(s)
883    }
884}
885
886// -- IO -------------------------------------------------------------------------------------------
887
888impl<const CAPACITY: usize> fmt::Write for PascalString<CAPACITY> {
889    #[inline]
890    fn write_str(&mut self, s: &str) -> fmt::Result {
891        self.try_push_str(s).map_err(|_| fmt::Error)
892    }
893}
894
895// -- Tests ----------------------------------------------------------------------------------------
896
897#[cfg(test)]
898mod tests {
899    use std::mem;
900
901    use super::*;
902
903    #[test]
904    fn test_eq() {
905        use std::fmt::Write;
906
907        let s = String::from("abc");
908        let ps = PascalString::<4>::try_from("abc").unwrap();
909
910        assert_eq!(ps, s);
911        // assert_eq!(ps.as_view(), s);
912        // assert_eq!(ps.as_view(), ps);
913
914        let s = String::from("abcd");
915        let mut ps = PascalString::<4>::new();
916        write!(&mut ps, "abcd").unwrap();
917
918        assert_eq!(ps, s);
919    }
920
921    #[test]
922    fn test_ord() {
923        let ps1 = PascalString::<4>::try_from("abc").unwrap();
924        let ps2 = PascalString::<4>::try_from("abcd").unwrap();
925
926        assert!(ps1 < ps2);
927        assert!(ps1 <= ps2);
928        assert!(ps2 > ps1);
929        assert!(ps2 >= ps1);
930    }
931
932    #[test]
933    fn test_size() {
934        assert_eq!(mem::size_of::<PascalString<0>>(), 1);
935        assert_eq!(mem::size_of::<PascalString<1>>(), 2);
936        assert_eq!(mem::size_of::<PascalString<2>>(), 3);
937        assert_eq!(mem::size_of::<PascalString<3>>(), 4);
938        assert_eq!(mem::size_of::<PascalString<4>>(), 5);
939    }
940
941    // TODO use https://github.com/Manishearth/compiletest-rs to test compile errors.
942    // #[test]
943    fn _test_max_size() {
944        // Every of these lines should not compile with a compile error:
945        // "the evaluated program panicked at 'PascalString max capacity is 255'".
946        //
947        // Also, compiler should point to the line that triggered the error, e.g.:
948        //
949        // note: the above error was encountered while instantiating `fn <PascalString<256> as std::default::Default>::default`
950        //    --> src/lib.rs:254:37
951        //     |
952        // 254 |         let _x: PascalString<256> = PascalString::default();
953        //     |                                     ^^^^^^^^^^^^^^^^^^^^^^^
954        //
955        let _x: PascalString<256> = PascalString::default();
956        let _x: PascalString<256> = PascalString::new();
957        let _x: PascalString<256> = PascalString::try_from("").unwrap();
958        let _x: PascalString<256> = PascalString::from_str_truncated("");
959    }
960
961    #[test]
962    fn test_deref() {
963        let ps = PascalString::<3>::try_from("abc").unwrap();
964        let map: std::collections::HashSet<_> = ["abc"].into_iter().collect();
965        assert!(map.contains(&*ps));
966    }
967
968    #[test]
969    fn test_try_push_str_too_long_does_not_modify() {
970        let mut ps = PascalString::<4>::try_from("ab").unwrap();
971        assert_eq!(ps.as_str(), "ab");
972
973        let err = ps.try_push_str("cde").unwrap_err();
974        assert_eq!(err, TryFromStrError::TooLong);
975        assert_eq!(ps.as_str(), "ab");
976    }
977
978    #[test]
979    fn test_try_push_char_too_long_does_not_modify() {
980        let mut ps = PascalString::<3>::new();
981        ps.try_push('€').unwrap(); // 3 bytes
982        assert_eq!(ps.as_str(), "€");
983
984        let err = ps.try_push('a').unwrap_err(); // +1 would overflow
985        assert_eq!(err, TryFromStrError::TooLong);
986        assert_eq!(ps.as_str(), "€");
987    }
988
989    #[test]
990    fn test_push_str_truncated_respects_utf8_boundary() {
991        let mut ps = PascalString::<4>::new();
992
993        // "€" is 3 bytes. "€a" is 4 bytes. "€ab" is 5 bytes.
994        let remainder = ps.push_str_truncated("€ab");
995        assert_eq!(ps.as_str(), "€a");
996        assert_eq!(remainder, "b");
997    }
998
999    #[test]
1000    fn test_from_str_truncated_truncates_on_boundary() {
1001        let ps = PascalString::<4>::from_str_truncated("€ab");
1002        assert_eq!(ps.as_str(), "€a");
1003        assert_eq!(ps.len(), 4);
1004    }
1005
1006    #[test]
1007    fn test_truncate_requires_char_boundary() {
1008        let mut ps = PascalString::<4>::new();
1009        ps.try_push('€').unwrap(); // 3 bytes
1010        ps.try_push('a').unwrap(); // 1 byte => 4
1011        assert_eq!(ps.as_str(), "€a");
1012
1013        // 1 is in the middle of the 3-byte UTF-8 sequence for '€'.
1014        let result = std::panic::catch_unwind(move || {
1015            let mut ps = ps;
1016            ps.truncate(1);
1017        });
1018        assert!(result.is_err());
1019    }
1020
1021    #[test]
1022    fn test_try_from_bytes_invalid_utf8() {
1023        let err = PascalString::<8>::try_from(&[0xff_u8][..]).unwrap_err();
1024        match err {
1025            TryFromBytesError::Utf8Error(_) => {}
1026            _ => panic!("expected Utf8Error, got: {err:?}"),
1027        }
1028    }
1029
1030    #[test]
1031    fn test_try_from_bytes_too_long() {
1032        let err = PascalString::<2>::try_from(&b"abc"[..]).unwrap_err();
1033        assert_eq!(err, TryFromBytesError::TooLong);
1034    }
1035
1036    #[test]
1037    fn test_capacity_zero_behavior() {
1038        let mut ps = PascalString::<0>::new();
1039        assert_eq!(ps.len(), 0);
1040        assert!(ps.is_empty());
1041        assert_eq!(ps.as_str(), "");
1042
1043        assert_eq!(ps.try_push_str(""), Ok(()));
1044        assert_eq!(ps.try_push_str("a"), Err(TryFromStrError::TooLong));
1045
1046        let rem = ps.push_str_truncated("hello");
1047        assert_eq!(ps.as_str(), "");
1048        assert_eq!(rem, "hello");
1049
1050        assert_eq!(PascalString::<0>::from_str_truncated("hello").as_str(), "");
1051        assert!(PascalString::<0>::try_from("").is_ok());
1052        assert_eq!(
1053            PascalString::<0>::try_from("a").unwrap_err(),
1054            TryFromStrError::TooLong
1055        );
1056    }
1057
1058    #[test]
1059    fn test_into_inner_invariants() {
1060        let ps = PascalString::<4>::try_from("ab").unwrap();
1061        let (len, data) = ps.into_inner();
1062        assert_eq!(len, 2);
1063        assert_eq!(&data[..2], b"ab");
1064        assert_eq!(&data[2..], &[0, 0]);
1065    }
1066
1067    #[test]
1068    fn test_as_mut_str_allows_in_place_mutation() {
1069        let mut ps = PascalString::<4>::try_from("ab").unwrap();
1070        ps.as_mut_str().make_ascii_uppercase();
1071        assert_eq!(ps.as_str(), "AB");
1072    }
1073
1074    #[test]
1075    fn test_retain_filters_in_place() {
1076        let mut ps = PascalString::<8>::try_from("a1b2c3").unwrap();
1077        ps.retain(|ch| ch.is_ascii_alphabetic());
1078        assert_eq!(ps.as_str(), "abc");
1079    }
1080
1081    #[test]
1082    fn test_push_str_panics_on_overflow() {
1083        let result = std::panic::catch_unwind(|| {
1084            let mut ps = PascalString::<4>::new();
1085            ps.push_str_expect_capacity("abcde");
1086        });
1087        assert!(result.is_err());
1088    }
1089
1090    #[test]
1091    fn test_insert_str_and_remove_unicode_boundaries() {
1092        let mut ps = PascalString::<8>::try_from("ab").unwrap();
1093        ps.insert_str_expect_capacity(1, "€"); // 3 bytes
1094        assert_eq!(ps.as_str(), "a€b");
1095
1096        let removed = ps.remove(1);
1097        assert_eq!(removed, '€');
1098        assert_eq!(ps.as_str(), "ab");
1099    }
1100
1101    #[test]
1102    fn test_try_insert_str_too_long_does_not_modify() {
1103        let mut ps = PascalString::<4>::try_from("ab").unwrap();
1104        let err = ps.try_insert_str(1, "€").unwrap_err(); // would become 5 bytes
1105        assert_eq!(err, InsertError::TooLong);
1106        assert_eq!(ps.as_str(), "ab");
1107    }
1108
1109    #[test]
1110    fn test_try_replace_range_bounds_truncated() {
1111        let mut ps = PascalString::<4>::try_from("ab").unwrap();
1112        // Replace empty range at idx=1 with "cde" => only "cd" fits.
1113        let rem = ps.try_replace_range_bounds_truncated(1, 1, "cde").unwrap();
1114        assert_eq!(ps.as_str(), "acdb");
1115        assert_eq!(rem, "e");
1116    }
1117
1118    #[test]
1119    fn test_try_from_str_const() {
1120        const PS: Option<PascalString<4>> = PascalString::<4>::try_from_str_const("ab");
1121        let ps = PS.unwrap();
1122        assert_eq!(ps.as_str(), "ab");
1123
1124        const TOO_LONG: Option<PascalString<2>> = PascalString::<2>::try_from_str_const("abc");
1125        assert!(TOO_LONG.is_none());
1126    }
1127
1128    // -- split_off -------------------------------------------------------------------------------
1129
1130    #[test]
1131    fn test_split_off_at_middle() {
1132        let mut ps = PascalString::<8>::try_from("abcdef").unwrap();
1133        let tail = ps.split_off(3);
1134        assert_eq!(ps.as_str(), "abc");
1135        assert_eq!(tail.as_str(), "def");
1136    }
1137
1138    #[test]
1139    fn test_split_off_at_zero() {
1140        let mut ps = PascalString::<8>::try_from("hello").unwrap();
1141        let tail = ps.split_off(0);
1142        assert_eq!(ps.as_str(), "");
1143        assert_eq!(tail.as_str(), "hello");
1144    }
1145
1146    #[test]
1147    fn test_split_off_at_len() {
1148        let mut ps = PascalString::<8>::try_from("hello").unwrap();
1149        let len = ps.len();
1150        let tail = ps.split_off(len);
1151        assert_eq!(ps.as_str(), "hello");
1152        assert_eq!(tail.as_str(), "");
1153    }
1154
1155    #[test]
1156    fn test_split_off_multibyte_boundary() {
1157        // "€" is 3 bytes, "ab" is 2 bytes => total 5 bytes
1158        let mut ps = PascalString::<8>::try_from("€ab").unwrap();
1159        let tail = ps.split_off(3); // split after '€'
1160        assert_eq!(ps.as_str(), "€");
1161        assert_eq!(tail.as_str(), "ab");
1162    }
1163
1164    #[test]
1165    #[should_panic]
1166    fn test_split_off_panics_on_non_char_boundary() {
1167        let mut ps = PascalString::<8>::try_from("€ab").unwrap();
1168        let _ = ps.split_off(1); // mid of 3-byte '€'
1169    }
1170
1171    #[test]
1172    #[should_panic]
1173    fn test_split_off_panics_at_gt_len() {
1174        let mut ps = PascalString::<8>::try_from("abc").unwrap();
1175        let _ = ps.split_off(10);
1176    }
1177
1178    // -- try_split_off ---------------------------------------------------------------------------
1179
1180    #[test]
1181    fn test_try_split_off_at_middle() {
1182        let mut ps = PascalString::<8>::try_from("abcdef").unwrap();
1183        let tail = ps.try_split_off(3).unwrap();
1184        assert_eq!(ps.as_str(), "abc");
1185        assert_eq!(tail.as_str(), "def");
1186    }
1187
1188    #[test]
1189    fn test_try_split_off_at_zero() {
1190        let mut ps = PascalString::<8>::try_from("hello").unwrap();
1191        let tail = ps.try_split_off(0).unwrap();
1192        assert_eq!(ps.as_str(), "");
1193        assert_eq!(tail.as_str(), "hello");
1194    }
1195
1196    #[test]
1197    fn test_try_split_off_at_len() {
1198        let mut ps = PascalString::<8>::try_from("hello").unwrap();
1199        let len = ps.len();
1200        let tail = ps.try_split_off(len).unwrap();
1201        assert_eq!(ps.as_str(), "hello");
1202        assert_eq!(tail.as_str(), "");
1203    }
1204
1205    #[test]
1206    fn test_try_split_off_multibyte_boundary() {
1207        let mut ps = PascalString::<8>::try_from("€ab").unwrap();
1208        let tail = ps.try_split_off(3).unwrap();
1209        assert_eq!(ps.as_str(), "€");
1210        assert_eq!(tail.as_str(), "ab");
1211    }
1212
1213    #[test]
1214    fn test_try_split_off_err_not_char_boundary() {
1215        let mut ps = PascalString::<8>::try_from("€ab").unwrap();
1216        let err = ps.try_split_off(1).unwrap_err();
1217        assert_eq!(err, SplitOffError::NotCharBoundary { at: 1 });
1218        // self is unchanged
1219        assert_eq!(ps.as_str(), "€ab");
1220    }
1221
1222    #[test]
1223    fn test_try_split_off_err_out_of_bounds() {
1224        let mut ps = PascalString::<8>::try_from("abc").unwrap();
1225        let err = ps.try_split_off(10).unwrap_err();
1226        assert_eq!(err, SplitOffError::OutOfBounds { at: 10, len: 3 });
1227        assert_eq!(ps.as_str(), "abc");
1228    }
1229
1230    // -- remove_matches --------------------------------------------------------------------------
1231
1232    #[test]
1233    fn test_remove_matches_single() {
1234        let mut ps = PascalString::<16>::try_from("hello world").unwrap();
1235        ps.remove_matches("world");
1236        assert_eq!(ps.as_str(), "hello ");
1237    }
1238
1239    #[test]
1240    fn test_remove_matches_multiple() {
1241        let mut ps = PascalString::<16>::try_from("abcabcabc").unwrap();
1242        ps.remove_matches("abc");
1243        assert_eq!(ps.as_str(), "");
1244    }
1245
1246    #[test]
1247    fn test_remove_matches_no_match() {
1248        let mut ps = PascalString::<16>::try_from("hello").unwrap();
1249        ps.remove_matches("xyz");
1250        assert_eq!(ps.as_str(), "hello");
1251    }
1252
1253    #[test]
1254    fn test_remove_matches_empty_pat() {
1255        let mut ps = PascalString::<8>::try_from("hello").unwrap();
1256        ps.remove_matches("");
1257        assert_eq!(ps.as_str(), "hello");
1258    }
1259
1260    #[test]
1261    fn test_remove_matches_equals_whole_string() {
1262        let mut ps = PascalString::<8>::try_from("hello").unwrap();
1263        ps.remove_matches("hello");
1264        assert_eq!(ps.as_str(), "");
1265    }
1266
1267    #[test]
1268    fn test_remove_matches_multibyte() {
1269        let mut ps = PascalString::<16>::try_from("a€b€c").unwrap();
1270        ps.remove_matches("€");
1271        assert_eq!(ps.as_str(), "abc");
1272    }
1273
1274    #[test]
1275    fn test_remove_matches_non_overlapping() {
1276        // "aaa" with pat "aa": first match consumes [0,2), leaving "a"
1277        let mut ps = PascalString::<8>::try_from("aaa").unwrap();
1278        ps.remove_matches("aa");
1279        assert_eq!(ps.as_str(), "a");
1280    }
1281}