Skip to main content

deep_time/
ascii_str.rs

1use core::fmt::{self, Write};
2use core::str;
3
4/// Fixed-capacity, stack-only ASCII string stored in a single `[u8; N]` array.
5///
6/// The string is stored as raw bytes. Its logical length is determined at
7/// runtime by the position of the first nul byte (`b'\0'`). All bytes after
8/// the string content are guaranteed to be zero.
9#[derive(Clone, Copy, PartialEq, Eq)]
10pub struct AsciiStr<const N: usize> {
11    bytes: [u8; N],
12}
13
14impl<const N: usize> fmt::Debug for AsciiStr<N> {
15    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
16        match self.as_str() {
17            Ok(s) => write!(f, "{:?}", s),
18            Err(_) => write!(f, "AsciiStr(<invalid ascii>)"),
19        }
20    }
21}
22
23#[cfg(feature = "serde")]
24impl<const N: usize> serde::Serialize for AsciiStr<N> {
25    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
26    where
27        S: serde::Serializer,
28    {
29        self.as_str()
30            .map_err(serde::ser::Error::custom)?
31            .serialize(serializer)
32    }
33}
34
35#[cfg(feature = "serde")]
36impl<'de, const N: usize> serde::Deserialize<'de> for AsciiStr<N> {
37    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
38    where
39        D: serde::Deserializer<'de>,
40    {
41        let s: &str = serde::Deserialize::deserialize(deserializer)?;
42        AsciiStr::try_from_str(s).map_err(serde::de::Error::custom)
43    }
44}
45
46/// Errors returned by [`AsciiStr`] operations.
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum AsciiStrError {
49    /// Input contained non-ASCII characters.
50    InvalidAscii,
51    /// Input exceeded the fixed capacity `N`.
52    TooLong {
53        /// Maximum capacity of this `AsciiStr`.
54        capacity: usize,
55        /// Length of the rejected input.
56        length: usize,
57    },
58    /// Internal data is corrupted or violates the type invariant.
59    ///
60    /// This can occur when:
61    /// - The bytes are not valid UTF-8 (should never happen for ASCII data).
62    /// - Non-zero bytes appear after the first nul byte (violates the
63    ///   "nul-terminated + trailing zeros" representation invariant).
64    ///
65    /// This variant exists only to keep the public API 100% panic-free.
66    /// It is unreachable when the type is constructed through the safe API.
67    CorruptedData,
68}
69
70// ─────────────────────────────────────────────────────────────────────────────
71// Display implementation (required by serde::ser::Error::custom / de::Error::custom)
72// ─────────────────────────────────────────────────────────────────────────────
73
74impl fmt::Display for AsciiStrError {
75    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76        match self {
77            AsciiStrError::InvalidAscii => f.write_str("input contained non-ASCII characters"),
78            AsciiStrError::TooLong { capacity, length } => {
79                write!(
80                    f,
81                    "input is too long: length {} exceeds capacity {}",
82                    length, capacity
83                )
84            }
85            AsciiStrError::CorruptedData => {
86                f.write_str("internal data is corrupted or violates the representation invariant")
87            }
88        }
89    }
90}
91
92impl<const N: usize> Default for AsciiStr<N> {
93    fn default() -> Self {
94        Self::new()
95    }
96}
97
98impl<const N: usize> AsciiStr<N> {
99    /// Creates a new empty `AsciiStr` (all bytes zero).
100    pub const fn new() -> Self {
101        Self { bytes: [0; N] }
102    }
103
104    /// Size of the wire representation in bytes (always equal to the capacity `N`).
105    pub const WIRE_SIZE: usize = N;
106
107    pub const DEFAULT: Self = Self::new();
108
109    /// Serializes this `AsciiStr` into a fixed-size byte array.
110    ///
111    /// The entire internal buffer is written (including trailing zeros after
112    /// the logical string content). This preserves the exact representation.
113    #[cfg(feature = "wire")]
114    #[inline]
115    pub fn to_wire_bytes(&self) -> [u8; N] {
116        self.bytes
117    }
118
119    /// Deserializes an `AsciiStr<N>` from exactly `N` bytes.
120    ///
121    /// The input must be valid ASCII. Any bytes after the first nul byte
122    /// must be zero (as required by the type invariant).
123    ///
124    /// Returns `None` if the input is not valid ASCII or violates the
125    /// internal representation rules.
126    #[cfg(feature = "wire")]
127    pub fn from_wire_bytes(bytes: &[u8]) -> Option<Self> {
128        if bytes.len() != N {
129            return None;
130        }
131        let mut arr = [0u8; N];
132        arr.copy_from_slice(bytes);
133        Self::try_from_filled_buffer(arr).ok()
134    }
135
136    /// Internal constructor used by the `strftime` formatter (and other
137    /// trusted code paths).
138    ///
139    /// The caller **must** guarantee that:
140    /// - The first `pos` bytes contain the formatted ASCII string.
141    /// - All remaining bytes are zero (nul-terminated).
142    ///
143    /// For untrusted input use the safe [`try_from_filled_buffer`](Self::try_from_filled_buffer) instead.
144    pub(crate) const fn from_filled_buffer(buffer: [u8; N]) -> Self {
145        Self { bytes: buffer }
146    }
147
148    /// Attempts to create an `AsciiStr<N>` from a raw byte buffer **safely**.
149    ///
150    /// This is the public, validated counterpart to the internal
151    /// [`from_filled_buffer`](Self::from_filled_buffer).
152    ///
153    /// It performs full validation:
154    /// - All bytes must be valid ASCII.
155    /// - Every byte after the first `b'\0'` must be zero (preserves the
156    ///   nul-terminated + trailing-zeros invariant).
157    ///
158    /// Use this when you have untrusted or externally-supplied bytes
159    /// (network packets, C `strftime` output, user input, etc.).
160    ///
161    /// **This method (and the entire public API) is completely panic-free.**
162    /// All fallible operations return `Result` or `Option`.
163    ///
164    /// ## Errors
165    ///
166    /// - [`AsciiStrError::InvalidAscii`] if the buffer contains non-ASCII bytes.
167    /// - [`AsciiStrError::CorruptedData`] if bytes after the first nul are
168    ///   not all zero (violates the representation invariant).
169    pub fn try_from_filled_buffer(buffer: [u8; N]) -> Result<Self, AsciiStrError> {
170        if !buffer.is_ascii() {
171            return Err(AsciiStrError::InvalidAscii);
172        }
173
174        if let Some(first_nul) = buffer.iter().position(|&b| b == 0)
175            && buffer[first_nul..].iter().any(|&b| b != 0)
176        {
177            return Err(AsciiStrError::CorruptedData);
178        }
179
180        Ok(Self { bytes: buffer })
181    }
182
183    /// Attempts to create an `AsciiStr<N>` from a string slice.
184    ///
185    /// ## Errors
186    ///
187    /// - [`AsciiStrError::InvalidAscii`] if the input is not ASCII.
188    /// - [`AsciiStrError::TooLong`] if the input exceeds capacity `N`.
189    pub fn try_from_str(s: &str) -> Result<Self, AsciiStrError> {
190        if !s.is_ascii() {
191            return Err(AsciiStrError::InvalidAscii);
192        }
193        if s.len() > N {
194            return Err(AsciiStrError::TooLong {
195                capacity: N,
196                length: s.len(),
197            });
198        }
199        let mut bytes = [0u8; N];
200        bytes[..s.len()].copy_from_slice(s.as_bytes());
201        Ok(Self { bytes })
202    }
203
204    /// Attempts to create an `AsciiStr<N>` from a string slice, **uppercasing** the input.
205    ///
206    /// This is a convenience wrapper around [`try_from_str`](Self::try_from_str)
207    /// that converts the input to ASCII uppercase before storing it.
208    ///
209    /// # Errors
210    /// - [`AsciiStrError::InvalidAscii`] if the input is not ASCII.
211    /// - [`AsciiStrError::TooLong`] if the input exceeds capacity `N`.
212    pub fn try_from_str_upper(s: &str) -> Result<Self, AsciiStrError> {
213        if !s.is_ascii() {
214            return Err(AsciiStrError::InvalidAscii);
215        }
216        if s.len() > N {
217            return Err(AsciiStrError::TooLong {
218                capacity: N,
219                length: s.len(),
220            });
221        }
222        let mut bytes = [0u8; N];
223        let src = s.as_bytes();
224        bytes[..src.len()].copy_from_slice(src);
225        bytes[..src.len()].make_ascii_uppercase();
226        Ok(Self { bytes })
227    }
228
229    /// Returns the stored string as `&str`.
230    ///
231    /// The length is computed by locating the first nul byte.
232    ///
233    /// ## Errors
234    ///
235    /// - Returns [`AsciiStrError::CorruptedData`] only if the internal data
236    ///   has become invalid UTF-8 (unreachable via safe constructors).
237    pub fn as_str(&self) -> Result<&str, AsciiStrError> {
238        let len = self
239            .bytes
240            .iter()
241            .position(|&b| b == 0)
242            .unwrap_or(self.bytes.len());
243        str::from_utf8(&self.bytes[..len]).map_err(|_| AsciiStrError::CorruptedData)
244    }
245
246    /// Returns the raw bytes of the stored string (excluding the trailing nul).
247    pub fn as_bytes(&self) -> &[u8] {
248        let len = self
249            .bytes
250            .iter()
251            .position(|&b| b == 0)
252            .unwrap_or(self.bytes.len());
253        &self.bytes[..len]
254    }
255
256    /// Returns the current logical length of the string.
257    pub fn len(&self) -> usize {
258        self.bytes
259            .iter()
260            .position(|&b| b == 0)
261            .unwrap_or(self.bytes.len())
262    }
263
264    /// Returns `true` if the string is empty.
265    pub const fn is_empty(&self) -> bool {
266        self.bytes[0] == 0
267    }
268
269    /// Returns the fixed maximum capacity of this type (always `N`).
270    pub const fn capacity(&self) -> usize {
271        N
272    }
273
274    /// Creates an `AsciiStr` from a `&str`, **truncating** if it exceeds capacity `N`.
275    ///
276    /// Non-ASCII characters are allowed.
277    pub fn from_str_truncate(s: &str) -> Self {
278        let mut bytes = [0u8; N];
279        let len = s.len().min(N);
280        bytes[..len].copy_from_slice(&s.as_bytes()[..len]);
281        Self { bytes }
282    }
283
284    /// Creates an `AsciiStr` from any type that implements `Display`.
285    /// The output is truncated if it exceeds capacity `N`.
286    ///
287    /// Very useful for embedding numbers, paths, etc. into errors.
288    pub fn from_display<T: core::fmt::Display>(value: T) -> Self {
289        let mut s = Self::new();
290        let _ = write!(&mut s, "{}", value);
291        s
292    }
293
294    /// Convenience: create from a format string (most ergonomic for errors)
295    pub fn from_fmt(args: core::fmt::Arguments<'_>) -> Self {
296        let mut s = Self::new();
297        let _ = write!(&mut s, "{}", args);
298        s
299    }
300}
301
302impl<const N: usize> TryFrom<&str> for AsciiStr<N> {
303    type Error = AsciiStrError;
304
305    fn try_from(s: &str) -> Result<Self, Self::Error> {
306        AsciiStr::try_from_str(s)
307    }
308}
309
310impl<const N: usize> TryFrom<[u8; N]> for AsciiStr<N> {
311    type Error = AsciiStrError;
312
313    /// Attempts to create an `AsciiStr<N>` from a filled buffer.
314    ///
315    /// This is the idiomatic, **completely panic-free** way to construct
316    /// from a byte array using the `?` operator or `.unwrap_or_else()`.
317    fn try_from(buffer: [u8; N]) -> Result<Self, Self::Error> {
318        AsciiStr::try_from_filled_buffer(buffer)
319    }
320}
321
322impl<const N: usize> core::fmt::Write for AsciiStr<N> {
323    fn write_str(&mut self, s: &str) -> core::fmt::Result {
324        if !s.is_ascii() {
325            return Err(core::fmt::Error);
326        }
327
328        let current_len = self.len();
329        let remaining = N.saturating_sub(current_len);
330
331        // Nothing space to write
332        if remaining == 0 {
333            return Ok(());
334        }
335
336        // Copy as much as possible (truncate if necessary)
337        let to_copy = s.len().min(remaining);
338
339        self.bytes[current_len..current_len + to_copy].copy_from_slice(&s.as_bytes()[..to_copy]);
340
341        Ok(())
342    }
343}