Skip to main content

deep_time/
lite_str.rs

1use core::fmt::{self};
2use core::str;
3
4/// Fixed-capacity, stack-only UTF-8 string stored in a single `[u8; N]` array.
5///
6/// The string is stored as raw bytes with C-style nul termination.
7/// Its logical length is determined at runtime by the position of the first nul byte (`b'\0'`).
8/// All bytes after the string content are guaranteed to be zero.
9///
10/// The type guarantees that the prefix up to the first nul is always valid UTF-8
11/// (when constructed through the safe API).
12#[derive(Clone, Copy, PartialEq, Eq)]
13pub struct LiteStr<const N: usize> {
14    bytes: [u8; N],
15}
16
17impl<const N: usize> Default for LiteStr<N> {
18    #[inline(always)]
19    fn default() -> Self {
20        Self { bytes: [0; N] }
21    }
22}
23
24impl<const N: usize> LiteStr<N> {
25    pub const SIZE: usize = N;
26
27    /// Recommended ergonomic constructor – truncates at a UTF-8 boundary if necessary.
28    #[inline(never)]
29    pub fn from_str(s: &str) -> Self {
30        let mut bytes = [0u8; N];
31        copy_valid_utf8_prefix(&mut bytes, s.as_bytes(), N);
32        Self { bytes }
33    }
34
35    /// Returns the stored string as `&str`.
36    #[inline(always)]
37    pub fn as_str(&self) -> Result<&str, LiteStrErr> {
38        str::from_utf8(&self.bytes[..find_first_nul(&self.bytes)])
39            .map_err(|_| LiteStrErr::CorruptedData)
40    }
41
42    pub fn from_bytes(bytes: &[u8]) -> Result<Self, LiteStrErr> {
43        if bytes.len() != N {
44            return Err(LiteStrErr::WrongLen);
45        }
46        let mut arr = [0u8; N];
47        arr.copy_from_slice(bytes);
48        validate_filled_buffer(&arr)?;
49        Ok(Self { bytes: arr })
50    }
51
52    #[inline(always)]
53    pub fn to_bytes(&self) -> [u8; N] {
54        self.bytes
55    }
56
57    /// Returns the stored string as `&[u8]`.
58    #[inline(always)]
59    pub fn as_bytes(&self) -> &[u8] {
60        &self.bytes[..find_first_nul(&self.bytes)]
61    }
62
63    /// Returns the current len of the utf-8.
64    #[inline(always)]
65    pub fn len(&self) -> usize {
66        find_first_nul(&self.bytes)
67    }
68}
69
70impl<const N: usize> fmt::Write for LiteStr<N> {
71    #[inline(never)]
72    fn write_str(&mut self, s: &str) -> fmt::Result {
73        let current = self.len();
74        let remaining = N.saturating_sub(current);
75        if remaining == 0 {
76            return Ok(());
77        }
78
79        copy_valid_utf8_prefix(&mut self.bytes[current..], s.as_bytes(), remaining);
80        Ok(())
81    }
82}
83
84impl<const N: usize> fmt::Debug for LiteStr<N> {
85    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86        match self.as_str() {
87            Ok(s) => write!(f, "{:?}", s),
88            Err(_) => f.write_str("LiteStr(<invalid utf-8>)"),
89        }
90    }
91}
92
93#[cfg(feature = "serde")]
94impl<const N: usize> serde::Serialize for LiteStr<N> {
95    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
96    where
97        S: serde::Serializer,
98    {
99        self.as_str()
100            .map_err(serde::ser::Error::custom)?
101            .serialize(serializer)
102    }
103}
104
105#[cfg(feature = "serde")]
106impl<'de, const N: usize> serde::Deserialize<'de> for LiteStr<N> {
107    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
108    where
109        D: serde::Deserializer<'de>,
110    {
111        let s: &str = serde::Deserialize::deserialize(deserializer)?;
112        Ok(LiteStr::from_str(s))
113    }
114}
115
116#[inline(never)]
117fn find_first_nul(bytes: &[u8]) -> usize {
118    bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len())
119}
120
121#[inline(never)]
122fn validate_filled_buffer(bytes: &[u8]) -> Result<(), LiteStrErr> {
123    let len = find_first_nul(bytes);
124
125    if str::from_utf8(&bytes[..len]).is_err() {
126        return Err(LiteStrErr::InvalidUtf8);
127    }
128
129    if len < bytes.len() && bytes[len..].iter().any(|&b| b != 0) {
130        return Err(LiteStrErr::CorruptedData);
131    }
132
133    Ok(())
134}
135
136#[inline(never)]
137fn copy_valid_utf8_prefix(dst: &mut [u8], src: &[u8], max_len: usize) -> usize {
138    let len = src.len().min(max_len);
139    match str::from_utf8(&src[..len]) {
140        Ok(_) => {
141            dst[..len].copy_from_slice(&src[..len]);
142            len
143        }
144        Err(e) => {
145            let valid = e.valid_up_to();
146            dst[..valid].copy_from_slice(&src[..valid]);
147            valid
148        }
149    }
150}
151
152/// Errors returned by [`LiteStr`] operations.
153#[derive(Debug, Clone, Copy, PartialEq, Eq)]
154pub enum LiteStrErr {
155    /// Input was not valid UTF-8.
156    WrongLen,
157    /// Input was not valid UTF-8.
158    InvalidUtf8,
159    /// Internal data is corrupted or violates the type invariant.
160    CorruptedData,
161}
162
163impl fmt::Display for LiteStrErr {
164    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
165        match self {
166            LiteStrErr::WrongLen => f.write_str("input len does not match SIZE"),
167            LiteStrErr::InvalidUtf8 => f.write_str("input is not valid UTF-8"),
168            LiteStrErr::CorruptedData => {
169                f.write_str("internal data is corrupted or violates the representation invariant")
170            }
171        }
172    }
173}
174
175impl core::error::Error for LiteStrErr {}