Skip to main content

deep_time/
buf_str.rs

1use core::fmt;
2use core::str;
3
4/// A fixed-capacity, stack-allocated byte buffer that can hold a UTF-8
5/// string.
6///
7/// `BufStr<N>` stores its content in a `[u8; N]` array using C-style nul
8/// termination. The logical length is determined by the position of the first
9/// `b'\0'` byte (or `N` if the buffer is completely filled without a nul).
10///
11/// This type performs **no validation during construction**. UTF-8 validity is
12/// only checked when the content is accessed via [`as_str`](#method.as_str),
13/// [`Debug`], or serialization.
14///
15/// Both [`new`](#method.new) and [`from_bytes`](#method.from_bytes)
16/// silently truncate input that exceeds the capacity `N`.
17///
18/// This type is intentionally minimal because each `BufStr<N>`is
19/// monomorphized independently.
20///
21/// ## To get the length of the str
22///
23/// - **Byte length**: [`BufStr::as_bytes`](#method.as_bytes) (then `.len()`)
24/// - **Unicode character count**: Use `as_str().chars().count()`
25#[derive(Clone, PartialEq, Eq)]
26pub struct BufStr<const N: usize> {
27    pub bytes: [u8; N],
28}
29
30impl<const N: usize> Default for BufStr<N> {
31    #[inline(always)]
32    fn default() -> Self {
33        Self { bytes: [0; N] }
34    }
35}
36
37impl<const N: usize> BufStr<N> {
38    pub const SIZE: usize = N;
39
40    /// Creates a new `BufStr` from a `&str`.
41    ///
42    /// If the input is longer than `N` bytes, it is truncated at the nearest
43    /// valid UTF-8 boundary.
44    #[inline]
45    pub fn new(s: &str) -> Self {
46        let mut bytes = [0u8; N];
47        copy_valid_utf8_prefix(&mut bytes, s.as_bytes(), N);
48        Self { bytes }
49    }
50
51    /// Creates a `BufStr<N>` from a byte slice.
52    ///
53    /// Copies up to `N` bytes from the input and zero-fills the remainder.
54    /// If `bytes.len() > N`, the input is silently truncated.
55    ///
56    /// No UTF-8 validation is performed.
57    #[inline]
58    pub fn from_bytes(bytes: &[u8]) -> Self {
59        let mut arr = [0u8; N];
60        let len = bytes.len().min(N);
61        arr[..len].copy_from_slice(&bytes[..len]);
62        Self { bytes: arr }
63    }
64
65    /// Returns the longest valid UTF-8 prefix of the content as a `&str`.
66    ///
67    /// - If the data is valid UTF-8, returns it directly.
68    /// - If the data starts with invalid bytes, returns a single replacement
69    ///   character (`�`).
70    /// - Otherwise returns only the valid prefix up to the first invalid
71    ///   sequence (everything after the first error is discarded).
72    /// - This method is infallible.
73    pub fn as_str(&self) -> &str {
74        let slice = &self.bytes[..self
75            .bytes
76            .iter()
77            .position(|&b| b == 0)
78            .unwrap_or(Self::SIZE)];
79        match str::from_utf8(slice) {
80            Ok(s) => s,
81            Err(e) => handle_invalid_utf8(slice, e),
82        }
83    }
84
85    /// Returns the content as a byte slice (up to the first nul byte).
86    pub fn as_bytes(&self) -> &[u8] {
87        &self.bytes[..self
88            .bytes
89            .iter()
90            .position(|&b| b == 0)
91            .unwrap_or(Self::SIZE)]
92    }
93}
94
95impl<const N: usize> fmt::Write for BufStr<N> {
96    #[inline(never)]
97    fn write_str(&mut self, s: &str) -> fmt::Result {
98        let current = self.as_bytes().len();
99        let remaining = N.saturating_sub(current);
100        if remaining == 0 {
101            return Ok(());
102        }
103
104        copy_valid_utf8_prefix(&mut self.bytes[current..], s.as_bytes(), remaining);
105        Ok(())
106    }
107}
108
109impl<const N: usize> fmt::Display for BufStr<N> {
110    #[inline(always)]
111    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112        f.write_str(self.as_str())
113    }
114}
115
116impl<const N: usize> fmt::Debug for BufStr<N> {
117    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118        write!(f, "{:?}", self.as_str())
119    }
120}
121
122#[cfg(feature = "serde")]
123impl<const N: usize> serde::Serialize for BufStr<N> {
124    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
125    where
126        S: serde::Serializer,
127    {
128        self.as_str().serialize(serializer)
129    }
130}
131
132#[cfg(feature = "serde")]
133impl<'de, const N: usize> serde::Deserialize<'de> for BufStr<N> {
134    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
135    where
136        D: serde::Deserializer<'de>,
137    {
138        let s: &str = serde::Deserialize::deserialize(deserializer)?;
139        Ok(BufStr::new(s))
140    }
141}
142
143#[cfg(feature = "defmt")]
144impl<const N: usize> defmt::Format for BufStr<N> {
145    fn format(&self, f: defmt::Formatter) {
146        defmt::write!(f, "{}", self.as_str());
147    }
148}
149
150#[inline(never)]
151fn copy_valid_utf8_prefix(dst: &mut [u8], src: &[u8], max_len: usize) -> usize {
152    let len = src.len().min(max_len);
153    match str::from_utf8(&src[..len]) {
154        Ok(_) => {
155            dst[..len].copy_from_slice(&src[..len]);
156            len
157        }
158        Err(e) => {
159            let valid = e.valid_up_to();
160            dst[..valid].copy_from_slice(&src[..valid]);
161            valid
162        }
163    }
164}
165
166#[cold]
167#[inline(never)]
168fn handle_invalid_utf8(slice: &[u8], e: core::str::Utf8Error) -> &str {
169    let valid = e.valid_up_to();
170    if valid == 0 {
171        "\u{FFFD}"
172    } else {
173        str::from_utf8(&slice[..valid]).unwrap_or("\u{FFFD}")
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    #[test]
182    fn as_str_valid() {
183        assert_eq!(BufStr::<16>::new("hello").as_str(), "hello");
184        assert_eq!(BufStr::<8>::default().as_str(), "");
185    }
186
187    #[test]
188    fn as_str_invalid_leading_byte() {
189        let s = BufStr::<8>::from_bytes(&[0xFF, b'a']);
190        assert_eq!(s.as_str(), "\u{FFFD}");
191    }
192
193    #[test]
194    fn as_str_valid_prefix_then_garbage() {
195        let s = BufStr::<8>::from_bytes(&[b'h', b'i', 0xFF, b'!']);
196        assert_eq!(s.as_str(), "hi");
197    }
198
199    #[test]
200    fn as_str_truncated_multibyte_at_start() {
201        // incomplete U+20AC (euro sign)
202        let s = BufStr::<8>::from_bytes(&[0xE2, 0x82]);
203        assert_eq!(s.as_str(), "\u{FFFD}");
204    }
205
206    #[test]
207    fn as_str_truncated_multibyte_after_valid_prefix() {
208        let s = BufStr::<8>::from_bytes(&[b'h', b'i', 0xE2, 0x82]);
209        assert_eq!(s.as_str(), "hi");
210    }
211
212    #[test]
213    fn as_str_stops_at_nul() {
214        let s = BufStr::<8>::from_bytes(b"ab\0cd");
215        assert_eq!(s.as_str(), "ab");
216        assert_eq!(s.as_bytes(), b"ab");
217    }
218}