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