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