Skip to main content

smart_format/
str_buffer.rs

1/// Fixed-size stack-allocated string buffer.
2///
3/// `StrBuffer<N>` stores up to `N` bytes of valid UTF-8 on the stack without heap allocation.
4/// Used internally by streaming formatters (e.g., HTML unescape) to buffer partial output
5/// across `format_with` chunk boundaries.
6///
7/// # Design note
8///
9/// This exists because streaming `Display` wrappers receive output in arbitrary-sized chunks
10/// via `fmt::Write::write_str`. When a multi-byte entity (like `&amp;`) is split across two
11/// chunks, the buffer accumulates bytes until the entity is complete. The const-generic size
12/// `N` is chosen per use site to match the longest possible entity.
13#[derive(Clone, PartialEq, Eq)]
14pub struct StrBuffer<const N: usize> {
15    buf: [u8; N],
16    len: usize,
17}
18
19impl<const N: usize> Default for StrBuffer<N> {
20    fn default() -> Self {
21        Self::new()
22    }
23}
24
25impl<const N: usize> StrBuffer<N> {
26    pub fn new() -> Self {
27        StrBuffer {
28            buf: [0; N],
29            len: 0,
30        }
31    }
32
33    pub fn push_str<'s>(&mut self, s: &'s str) -> Result<(), &'s str> {
34        let rem_len = self.buf.len() - self.len;
35        if rem_len < s.len() {
36            Err(s)
37        } else {
38            let start = self.len;
39            let end = start + s.len();
40            self.buf[start..end].copy_from_slice(s.as_bytes());
41            self.len += s.len();
42            Ok(())
43        }
44    }
45
46    pub fn as_str(&self) -> &str {
47        // SAFETY: `self.buf[..self.len]` is always valid UTF-8 because `push_str`
48        // only appends bytes copied from `&str` slices via `copy_from_slice`.
49        unsafe { core::str::from_utf8_unchecked(&self.buf[..self.len]) }
50    }
51
52    pub fn clear(&mut self) {
53        self.len = 0;
54    }
55
56    pub fn len(&self) -> usize {
57        self.len
58    }
59
60    pub fn is_empty(&self) -> bool {
61        self.len == 0
62    }
63
64    pub fn first(&self) -> Option<&str> {
65        if self.is_empty() {
66            None
67        } else {
68            self.as_str().split("").find(|c| !c.is_empty())
69        }
70    }
71
72    pub fn strip_first(&mut self) -> Option<()> {
73        let first_len = self.first()?.len();
74        self.buf.copy_within(first_len..self.len, 0);
75        self.len -= first_len;
76        Some(())
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    const EXHAUSTED: &str = "Buffer has been exhausted";
85
86    #[test]
87    fn test_push_str() {
88        let mut buf = StrBuffer::<5>::new();
89        buf.push_str("&").expect(EXHAUSTED);
90        buf.push_str("amp").expect(EXHAUSTED);
91        buf.push_str(";").expect(EXHAUSTED);
92        assert_eq!("&amp;", buf.as_str());
93    }
94
95    #[test]
96    fn test_exhaustion() {
97        let mut buf = StrBuffer::<5>::new();
98        buf.push_str("&amp;").expect(EXHAUSTED);
99        assert_eq!(Err(" "), buf.push_str(" "));
100    }
101
102    #[test]
103    fn test_clear() {
104        let mut buf = StrBuffer::<5>::new();
105        buf.push_str("&amp;").expect(EXHAUSTED);
106        assert_eq!("&amp;", buf.as_str());
107        buf.clear();
108        assert_eq!("", buf.as_str());
109    }
110
111    #[test]
112    fn test_length() {
113        let mut buf = StrBuffer::<5>::new();
114        assert_eq!(buf.len(), 0);
115        buf.push_str("&").expect(EXHAUSTED);
116        assert_eq!(buf.len(), 1);
117        buf.push_str("amp").expect(EXHAUSTED);
118        assert_eq!(buf.len(), 4);
119        buf.push_str(";").expect(EXHAUSTED);
120        assert_eq!(buf.len(), 5);
121        buf.clear();
122        assert_eq!(buf.len(), 0);
123    }
124
125    #[test]
126    fn test_is_empty() {
127        let mut buf = StrBuffer::<5>::new();
128        assert!(buf.is_empty());
129        buf.push_str("&").expect(EXHAUSTED);
130        assert!(!buf.is_empty());
131        buf.push_str("amp").expect(EXHAUSTED);
132        assert!(!buf.is_empty());
133        buf.push_str(";").expect(EXHAUSTED);
134        assert!(!buf.is_empty());
135        buf.clear();
136        assert!(buf.is_empty());
137    }
138
139    #[test]
140    fn test_first() {
141        let mut buf = StrBuffer::<5>::new();
142        assert_eq!(None, buf.first());
143        buf.push_str("&").expect(EXHAUSTED);
144        assert_eq!(Some("&"), buf.first());
145        buf.push_str("amp").expect(EXHAUSTED);
146        assert_eq!(Some("&"), buf.first());
147        buf.push_str(";").expect(EXHAUSTED);
148        assert_eq!(Some("&"), buf.first());
149        buf.clear();
150        assert_eq!(None, buf.first());
151    }
152
153    #[test]
154    fn test_strip_first() {
155        let mut buf = StrBuffer::<8>::new();
156        assert_eq!(None, buf.strip_first());
157        buf.push_str("&&amp;").expect(EXHAUSTED);
158        assert_eq!(Some("&"), buf.first());
159        assert_eq!(Some(()), buf.strip_first());
160        assert_eq!("&amp;", buf.as_str());
161    }
162
163    #[test]
164    fn test_strip_first_wide() {
165        let mut buf = StrBuffer::<8>::new();
166        assert_eq!(None, buf.strip_first());
167        buf.push_str("АБВ").expect(EXHAUSTED);
168        assert_eq!(Some("А"), buf.first());
169        assert_eq!(Some(()), buf.strip_first());
170        assert_eq!("БВ", buf.as_str());
171    }
172}