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