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