Skip to main content

deep_time/
buf_str.rs

1use core::{fmt, str};
2
3/// A fixed-capacity, stack-allocated byte buffer that can hold a UTF-8
4/// string.
5///
6/// `BufStr<N>` stores its content in a `[u8; N]` array. The logical length is
7/// tracked in [`len`](#structfield.len) (0..=`N`, stored as `u16`). Unused bytes
8/// past `len` are zero-filled by the constructors. [`from_bytes`](#method.from_bytes)
9/// treats the first `b'\0'` in the input as an end marker (C-style), so
10/// zero-padded wire buffers deserialize with the correct length.
11///
12/// This type performs **no validation during construction**. UTF-8 validity is
13/// only checked when the content is accessed via [`as_str`](#method.as_str),
14/// [`Debug`], or serialization.
15///
16/// Both [`new`](#method.new) and [`from_bytes`](#method.from_bytes)
17/// silently truncate input that exceeds the capacity `N`.
18///
19/// This type is intentionally minimal because each `BufStr<N>` is
20/// monomorphized independently.
21///
22/// ## To get the length of the str
23///
24/// - **Byte length**: [`BufStr::len`](#method.len) or [`as_bytes`](#method.as_bytes)`.len()`
25/// - **Unicode character count**: Use `as_str().chars().count()`
26#[derive(Clone, PartialEq, Eq)]
27pub struct BufStr<const N: usize> {
28    /// Raw fixed-capacity byte storage (zero-filled past [`len`](#structfield.len)).
29    pub bytes: [u8; N],
30    /// Number of valid content bytes in [`bytes`](#structfield.bytes) (0..=`N`).
31    pub len: u16,
32}
33
34impl<const N: usize> Default for BufStr<N> {
35    #[inline(always)]
36    fn default() -> Self {
37        Self {
38            bytes: [0; N],
39            len: 0,
40        }
41    }
42}
43
44impl<const N: usize> BufStr<N> {
45    /// Capacity of the buffer in bytes (same as the const parameter `N`).
46    pub const SIZE: usize = N;
47
48    /// Creates a new `BufStr` from a `&str`.
49    ///
50    /// If the input is longer than `N` bytes, it is truncated at the nearest
51    /// valid UTF-8 boundary.
52    #[inline]
53    pub fn new(s: &str) -> Self {
54        let mut bytes = [0u8; N];
55        let len = copy_str_prefix(&mut bytes, s, N) as u16;
56        Self { bytes, len }
57    }
58
59    /// Creates a `BufStr<N>` from a byte slice.
60    ///
61    /// Copies up to `N` bytes from the input and zero-fills the remainder.
62    /// If `bytes.len() > N`, the input is silently truncated.
63    ///
64    /// Logical length stops at the first `b'\0'` in the copied prefix (or the
65    /// full copied length if there is no nul). This matches C-style /
66    /// zero-padded wire buffers.
67    ///
68    /// No UTF-8 validation is performed.
69    #[inline]
70    pub fn from_bytes(bytes: &[u8]) -> Self {
71        let mut arr = [0u8; N];
72        let copy_len = bytes.len().min(N);
73        arr[..copy_len].copy_from_slice(&bytes[..copy_len]);
74        let len = arr[..copy_len]
75            .iter()
76            .position(|&b| b == 0)
77            .unwrap_or(copy_len);
78        Self {
79            bytes: arr,
80            len: len as u16,
81        }
82    }
83
84    /// Returns the number of valid content bytes.
85    #[inline(always)]
86    pub const fn len(&self) -> usize {
87        let len = self.len as usize;
88        if len < N { len } else { N }
89    }
90
91    /// Returns `true` if the buffer has no content.
92    #[inline(always)]
93    pub const fn is_empty(&self) -> bool {
94        self.len == 0
95    }
96
97    /// Returns the longest valid UTF-8 prefix of the content as a `&str`.
98    ///
99    /// - If the data is valid UTF-8, returns it directly.
100    /// - If the data starts with invalid bytes, returns a single replacement
101    ///   character (`�`).
102    /// - Otherwise returns only the valid prefix up to the first invalid
103    ///   sequence (everything after the first error is discarded).
104    /// - This method is infallible.
105    #[inline]
106    pub fn as_str(&self) -> &str {
107        match str::from_utf8(self.as_bytes()) {
108            Ok(s) => s,
109            Err(e) => handle_invalid_utf8(self.as_bytes(), e),
110        }
111    }
112
113    /// Returns the content as a byte slice of length [`len`](#method.len).
114    #[inline(always)]
115    pub fn as_bytes(&self) -> &[u8] {
116        &self.bytes[..self.len()]
117    }
118}
119
120impl<const N: usize> fmt::Write for BufStr<N> {
121    #[inline(never)]
122    fn write_str(&mut self, s: &str) -> fmt::Result {
123        let current = self.len();
124        let remaining = N.saturating_sub(current);
125        if remaining == 0 {
126            return Ok(());
127        }
128
129        let written = copy_str_prefix(&mut self.bytes[current..], s, remaining);
130        self.len = (current + written) as u16;
131        Ok(())
132    }
133}
134
135impl<const N: usize> fmt::Display for BufStr<N> {
136    #[inline(always)]
137    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138        f.write_str(self.as_str())
139    }
140}
141
142impl<const N: usize> fmt::Debug for BufStr<N> {
143    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144        write!(f, "{:?}", self.as_str())
145    }
146}
147
148#[cfg(feature = "serde")]
149impl<const N: usize> serde::Serialize for BufStr<N> {
150    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
151    where
152        S: serde::Serializer,
153    {
154        self.as_str().serialize(serializer)
155    }
156}
157
158#[cfg(feature = "serde")]
159impl<'de, const N: usize> serde::Deserialize<'de> for BufStr<N> {
160    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
161    where
162        D: serde::Deserializer<'de>,
163    {
164        let s: &str = serde::Deserialize::deserialize(deserializer)?;
165        Ok(BufStr::new(s))
166    }
167}
168
169#[cfg(feature = "defmt")]
170impl<const N: usize> defmt::Format for BufStr<N> {
171    fn format(&self, f: defmt::Formatter) {
172        defmt::write!(f, "{}", self.as_str());
173    }
174}
175
176/// Copy up to `max_len` bytes of already-valid UTF-8 into `dst`.
177///
178/// Both call sites pass a `&str`, so a full `from_utf8` scan is unnecessary.
179/// When capacity is smaller than `src`, walk back at most 3 bytes to a char
180/// boundary, then one `copy_from_slice`.
181#[inline(never)]
182fn copy_str_prefix(dst: &mut [u8], src: &str, max_len: usize) -> usize {
183    let bytes = src.as_bytes();
184    let mut len = bytes.len().min(max_len).min(dst.len());
185    while !src.is_char_boundary(len) {
186        len -= 1;
187    }
188    if len != 0 {
189        dst[..len].copy_from_slice(&bytes[..len]);
190    }
191    len
192}
193
194#[cold]
195#[inline(never)]
196fn handle_invalid_utf8(slice: &[u8], e: core::str::Utf8Error) -> &str {
197    let valid = e.valid_up_to();
198    if valid == 0 {
199        "\u{FFFD}"
200    } else {
201        str::from_utf8(&slice[..valid]).unwrap_or("\u{FFFD}")
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208
209    #[test]
210    fn as_str_valid() {
211        let s = BufStr::<16>::new("hello");
212        assert_eq!(s.as_str(), "hello");
213        assert_eq!(s.len(), 5);
214        assert_eq!(BufStr::<8>::default().as_str(), "");
215        assert!(BufStr::<8>::default().is_empty());
216    }
217
218    #[test]
219    fn as_str_invalid_leading_byte() {
220        let s = BufStr::<8>::from_bytes(&[0xFF, b'a']);
221        assert_eq!(s.as_str(), "\u{FFFD}");
222        assert_eq!(s.len(), 2);
223    }
224
225    #[test]
226    fn as_str_valid_prefix_then_garbage() {
227        let s = BufStr::<8>::from_bytes(&[b'h', b'i', 0xFF, b'!']);
228        assert_eq!(s.as_str(), "hi");
229        assert_eq!(s.len(), 4);
230    }
231
232    #[test]
233    fn as_str_truncated_multibyte_at_start() {
234        // incomplete U+20AC (euro sign)
235        let s = BufStr::<8>::from_bytes(&[0xE2, 0x82]);
236        assert_eq!(s.as_str(), "\u{FFFD}");
237    }
238
239    #[test]
240    fn as_str_truncated_multibyte_after_valid_prefix() {
241        let s = BufStr::<8>::from_bytes(&[b'h', b'i', 0xE2, 0x82]);
242        assert_eq!(s.as_str(), "hi");
243    }
244
245    #[test]
246    fn as_str_stops_at_nul() {
247        let s = BufStr::<8>::from_bytes(b"ab\0cd");
248        assert_eq!(s.as_str(), "ab");
249        assert_eq!(s.as_bytes(), b"ab");
250        assert_eq!(s.len(), 2);
251    }
252
253    #[test]
254    fn from_bytes_zero_padded() {
255        // Wire-style: full capacity of zeros after content.
256        let mut raw = [0u8; 8];
257        raw[..5].copy_from_slice(b"hello");
258        let s = BufStr::<8>::from_bytes(&raw);
259        assert_eq!(s.as_str(), "hello");
260        assert_eq!(s.len(), 5);
261    }
262
263    #[test]
264    fn new_truncates_on_char_boundary() {
265        // "€" is 3 bytes (E2 82 AC). Capacity 2 must drop the whole char.
266        assert_eq!(BufStr::<2>::new("€").as_str(), "");
267        assert_eq!(BufStr::<2>::new("€").len(), 0);
268        assert_eq!(BufStr::<3>::new("€").as_str(), "€");
269        assert_eq!(BufStr::<3>::new("€").len(), 3);
270        assert_eq!(BufStr::<4>::new("a€b").as_str(), "a€");
271        assert_eq!(BufStr::<4>::new("a€b").len(), 4);
272        assert_eq!(BufStr::<5>::new("a€b").as_str(), "a€b");
273        assert_eq!(BufStr::<5>::new("a€b").len(), 5);
274    }
275
276    #[test]
277    fn write_str_appends_and_truncates() {
278        use core::fmt::Write;
279        let mut s = BufStr::<5>::default();
280        write!(&mut s, "hi").unwrap();
281        assert_eq!(s.len(), 2);
282        write!(&mut s, "€").unwrap(); // 3 bytes → fills exactly
283        assert_eq!(s.as_str(), "hi€");
284        assert_eq!(s.len(), 5);
285        write!(&mut s, "x").unwrap(); // full → no-op
286        assert_eq!(s.as_str(), "hi€");
287        assert_eq!(s.len(), 5);
288    }
289}