Skip to main content

deep_time/
lite_str.rs

1use core::fmt::{self};
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`], [`Debug`], or
12/// serialization.
13///
14/// Both [`new`] and [`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()`][Self::as_str]`.unwrap().len()`
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    /// Returns the content as a `&str`, validating that it is well-formed UTF-8.
49    ///
50    /// Finds the first nul byte and uses that as the end of the str, or if
51    /// there isn't a nul byte then uses the whole len `N`.
52    #[inline(always)]
53    pub fn as_str(&self) -> Result<&str, LiteStrErr> {
54        let end = find_first_nul(&self.bytes);
55        str::from_utf8(&self.bytes[..end]).map_err(|_| LiteStrErr::CorruptedData)
56    }
57
58    /// Creates a `LiteStr<N>` from a byte slice.
59    ///
60    /// Copies up to `N` bytes from the input and zero-fills the remainder.
61    /// If `bytes.len() > N`, the input is silently truncated.
62    ///
63    /// No UTF-8 validation is performed.
64    #[inline(always)]
65    pub fn from_bytes(bytes: &[u8]) -> Self {
66        let mut arr = [0u8; N];
67        let len = bytes.len().min(N);
68        arr[..len].copy_from_slice(&bytes[..len]);
69        Self { bytes: arr }
70    }
71
72    /// Returns the content as a byte slice (up to the first nul byte).
73    #[inline(always)]
74    pub fn as_bytes(&self) -> &[u8] {
75        &self.bytes[..find_first_nul(&self.bytes)]
76    }
77}
78
79impl<const N: usize> fmt::Write for LiteStr<N> {
80    #[inline(never)]
81    fn write_str(&mut self, s: &str) -> fmt::Result {
82        let current = self.as_bytes().len();
83        let remaining = N.saturating_sub(current);
84        if remaining == 0 {
85            return Ok(());
86        }
87
88        copy_valid_utf8_prefix(&mut self.bytes[current..], s.as_bytes(), remaining);
89        Ok(())
90    }
91}
92
93impl<const N: usize> fmt::Debug for LiteStr<N> {
94    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95        match self.as_str() {
96            Ok(s) => write!(f, "{:?}", s),
97            Err(_) => f.write_str("LiteStr(<invalid utf-8>)"),
98        }
99    }
100}
101
102#[cfg(feature = "serde")]
103impl<const N: usize> serde::Serialize for LiteStr<N> {
104    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
105    where
106        S: serde::Serializer,
107    {
108        self.as_str()
109            .map_err(serde::ser::Error::custom)?
110            .serialize(serializer)
111    }
112}
113
114#[cfg(feature = "serde")]
115impl<'de, const N: usize> serde::Deserialize<'de> for LiteStr<N> {
116    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
117    where
118        D: serde::Deserializer<'de>,
119    {
120        let s: &str = serde::Deserialize::deserialize(deserializer)?;
121        Ok(LiteStr::new(s))
122    }
123}
124
125#[inline(never)]
126fn find_first_nul(bytes: &[u8]) -> usize {
127    bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len())
128}
129
130#[inline(never)]
131fn copy_valid_utf8_prefix(dst: &mut [u8], src: &[u8], max_len: usize) -> usize {
132    let len = src.len().min(max_len);
133    match str::from_utf8(&src[..len]) {
134        Ok(_) => {
135            dst[..len].copy_from_slice(&src[..len]);
136            len
137        }
138        Err(e) => {
139            let valid = e.valid_up_to();
140            dst[..valid].copy_from_slice(&src[..valid]);
141            valid
142        }
143    }
144}
145
146/// Errors that can occur when using a [`LiteStr`].
147#[derive(Debug, Clone, Copy, PartialEq, Eq)]
148pub enum LiteStrErr {
149    /// The content is not valid UTF-8.
150    CorruptedData,
151}
152
153impl fmt::Display for LiteStrErr {
154    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155        match self {
156            LiteStrErr::CorruptedData => f.write_str("content is not valid UTF-8"),
157        }
158    }
159}
160
161impl core::error::Error for LiteStrErr {}