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 /// - [`AsciiStrError::InvalidAscii`] if the buffer contains non-ASCII bytes.
166 /// - [`AsciiStrError::CorruptedData`] if bytes after the first nul are
167 /// not all zero (violates the representation invariant).
168 pub fn try_from_filled_buffer(buffer: [u8; N]) -> Result<Self, AsciiStrError> {
169 if !buffer.is_ascii() {
170 return Err(AsciiStrError::InvalidAscii);
171 }
172
173 if let Some(first_nul) = buffer.iter().position(|&b| b == 0)
174 && buffer[first_nul..].iter().any(|&b| b != 0)
175 {
176 return Err(AsciiStrError::CorruptedData);
177 }
178
179 Ok(Self { bytes: buffer })
180 }
181
182 /// Attempts to create an `AsciiStr<N>` from a string slice.
183 ///
184 /// # Errors
185 /// - [`AsciiStrError::InvalidAscii`] if the input is not ASCII.
186 /// - [`AsciiStrError::TooLong`] if the input exceeds capacity `N`.
187 pub fn try_from_str(s: &str) -> Result<Self, AsciiStrError> {
188 if !s.is_ascii() {
189 return Err(AsciiStrError::InvalidAscii);
190 }
191 if s.len() > N {
192 return Err(AsciiStrError::TooLong {
193 capacity: N,
194 length: s.len(),
195 });
196 }
197 let mut bytes = [0u8; N];
198 bytes[..s.len()].copy_from_slice(s.as_bytes());
199 Ok(Self { bytes })
200 }
201
202 /// Attempts to create an `AsciiStr<N>` from a string slice, **uppercasing** the input.
203 ///
204 /// This is a convenience wrapper around [`try_from_str`](Self::try_from_str)
205 /// that converts the input to ASCII uppercase before storing it.
206 ///
207 /// # Errors
208 /// - [`AsciiStrError::InvalidAscii`] if the input is not ASCII.
209 /// - [`AsciiStrError::TooLong`] if the input exceeds capacity `N`.
210 pub fn try_from_str_upper(s: &str) -> Result<Self, AsciiStrError> {
211 if !s.is_ascii() {
212 return Err(AsciiStrError::InvalidAscii);
213 }
214 if s.len() > N {
215 return Err(AsciiStrError::TooLong {
216 capacity: N,
217 length: s.len(),
218 });
219 }
220 let mut bytes = [0u8; N];
221 let src = s.as_bytes();
222 bytes[..src.len()].copy_from_slice(src);
223 bytes[..src.len()].make_ascii_uppercase();
224 Ok(Self { bytes })
225 }
226
227 /// Returns the stored string as `&str`.
228 ///
229 /// The length is computed by locating the first nul byte.
230 ///
231 /// # Errors
232 /// Returns [`AsciiStrError::CorruptedData`] only if the internal data
233 /// has become invalid UTF-8 (unreachable via safe constructors).
234 pub fn as_str(&self) -> Result<&str, AsciiStrError> {
235 let len = self
236 .bytes
237 .iter()
238 .position(|&b| b == 0)
239 .unwrap_or(self.bytes.len());
240 str::from_utf8(&self.bytes[..len]).map_err(|_| AsciiStrError::CorruptedData)
241 }
242
243 /// Returns the raw bytes of the stored string (excluding the trailing nul).
244 pub fn as_bytes(&self) -> &[u8] {
245 let len = self
246 .bytes
247 .iter()
248 .position(|&b| b == 0)
249 .unwrap_or(self.bytes.len());
250 &self.bytes[..len]
251 }
252
253 /// Returns the current logical length of the string.
254 pub fn len(&self) -> usize {
255 self.bytes
256 .iter()
257 .position(|&b| b == 0)
258 .unwrap_or(self.bytes.len())
259 }
260
261 /// Returns `true` if the string is empty.
262 pub const fn is_empty(&self) -> bool {
263 self.bytes[0] == 0
264 }
265
266 /// Returns the fixed maximum capacity of this type (always `N`).
267 pub const fn capacity(&self) -> usize {
268 N
269 }
270
271 /// Creates an `AsciiStr` from a `&str`, **truncating** if it exceeds capacity `N`.
272 ///
273 /// Non-ASCII characters are allowed.
274 pub fn from_str_truncate(s: &str) -> Self {
275 let mut bytes = [0u8; N];
276 let len = s.len().min(N);
277 bytes[..len].copy_from_slice(&s.as_bytes()[..len]);
278 Self { bytes }
279 }
280
281 /// Creates an `AsciiStr` from any type that implements `Display`.
282 /// The output is truncated if it exceeds capacity `N`.
283 ///
284 /// Very useful for embedding numbers, paths, etc. into errors.
285 pub fn from_display<T: core::fmt::Display>(value: T) -> Self {
286 let mut s = Self::new();
287 let _ = write!(&mut s, "{}", value);
288 s
289 }
290
291 /// Convenience: create from a format string (most ergonomic for errors)
292 pub fn from_fmt(args: core::fmt::Arguments<'_>) -> Self {
293 let mut s = Self::new();
294 let _ = write!(&mut s, "{}", args);
295 s
296 }
297}
298
299impl<const N: usize> TryFrom<&str> for AsciiStr<N> {
300 type Error = AsciiStrError;
301
302 fn try_from(s: &str) -> Result<Self, Self::Error> {
303 AsciiStr::try_from_str(s)
304 }
305}
306
307impl<const N: usize> TryFrom<[u8; N]> for AsciiStr<N> {
308 type Error = AsciiStrError;
309
310 /// Attempts to create an `AsciiStr<N>` from a filled buffer.
311 ///
312 /// This is the idiomatic, **completely panic-free** way to construct
313 /// from a byte array using the `?` operator or `.unwrap_or_else()`.
314 fn try_from(buffer: [u8; N]) -> Result<Self, Self::Error> {
315 AsciiStr::try_from_filled_buffer(buffer)
316 }
317}
318
319impl<const N: usize> core::fmt::Write for AsciiStr<N> {
320 fn write_str(&mut self, s: &str) -> core::fmt::Result {
321 if !s.is_ascii() {
322 return Err(core::fmt::Error);
323 }
324
325 let current_len = self.len();
326 let remaining = N.saturating_sub(current_len);
327
328 // Nothing space to write
329 if remaining == 0 {
330 return Ok(());
331 }
332
333 // Copy as much as possible (truncate if necessary)
334 let to_copy = s.len().min(remaining);
335
336 self.bytes[current_len..current_len + to_copy].copy_from_slice(&s.as_bytes()[..to_copy]);
337
338 Ok(())
339 }
340}