Skip to main content

deep_time/
lite_str.rs

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