Skip to main content

autocore_std/
fixed_string.rs

1//! Fixed-length string type for shared memory variables.
2//!
3//! [`FixedString<N>`] is a `Copy`-able, `repr(transparent)` wrapper around
4//! `[u8; N]` that stores UTF-8 text zero-padded to a fixed capacity. It is
5//! designed for use in `#[repr(C)]` structs that are shared via memory-mapped
6//! regions between processes.
7//!
8//! # Example
9//!
10//! ```
11//! use autocore_std::FixedString;
12//!
13//! let mut s = FixedString::<64>::new();
14//! assert!(s.is_empty());
15//!
16//! s.set("Hello, world!");
17//! assert_eq!(s.as_str(), "Hello, world!");
18//! assert_eq!(s.len(), 13);
19//!
20//! // Strings longer than N are silently truncated at a UTF-8 boundary
21//! let mut tiny = FixedString::<4>::new();
22//! tiny.set("Hello");
23//! assert_eq!(tiny.as_str(), "Hell");
24//! ```
25
26use core::fmt;
27
28/// A fixed-capacity string stored as a zero-padded UTF-8 byte array.
29///
30/// `N` is the maximum number of bytes (not characters). Strings shorter than
31/// `N` are zero-padded. Strings longer than `N` are truncated at the last
32/// valid UTF-8 character boundary that fits.
33///
34/// This type is `Copy`, `#[repr(transparent)]`, and safe to embed in
35/// `#[repr(C)]` shared memory structs.
36#[derive(Clone, Copy, Eq, Hash)]
37#[repr(transparent)]
38pub struct FixedString<const N: usize>(pub [u8; N]);
39
40impl<const N: usize> FixedString<N> {
41    /// Create an empty (all-zero) fixed string.
42    pub const fn new() -> Self {
43        Self([0u8; N])
44    }
45
46    /// Set the string content, truncating at the last UTF-8 boundary that fits.
47    pub fn set(&mut self, s: &str) {
48        self.0 = [0u8; N];
49        let bytes = s.as_bytes();
50        let mut copy_len = bytes.len().min(N);
51        // Walk back to a valid UTF-8 boundary if we truncated mid-character
52        while copy_len > 0 && !s.is_char_boundary(copy_len) {
53            copy_len -= 1;
54        }
55        self.0[..copy_len].copy_from_slice(&bytes[..copy_len]);
56    }
57
58    /// Return the string content as a `&str`.
59    ///
60    /// Trailing zeros are excluded. If the buffer contains invalid UTF-8
61    /// (e.g. from external corruption), returns an empty string.
62    pub fn as_str(&self) -> &str {
63        let end = self.0.iter().position(|&b| b == 0).unwrap_or(N);
64        core::str::from_utf8(&self.0[..end]).unwrap_or("")
65    }
66
67    /// Number of bytes in the string (excluding trailing zeros).
68    pub fn len(&self) -> usize {
69        self.0.iter().position(|&b| b == 0).unwrap_or(N)
70    }
71
72    /// Returns `true` if the string is empty.
73    pub fn is_empty(&self) -> bool {
74        self.0[0] == 0
75    }
76
77    /// Clear the string (fill with zeros).
78    pub fn clear(&mut self) {
79        self.0 = [0u8; N];
80    }
81
82    /// The fixed capacity in bytes.
83    pub const fn capacity(&self) -> usize {
84        N
85    }
86}
87
88impl<const N: usize> Default for FixedString<N> {
89    fn default() -> Self {
90        Self::new()
91    }
92}
93
94impl<const N: usize> PartialEq for FixedString<N> {
95    fn eq(&self, other: &Self) -> bool {
96        self.0 == other.0
97    }
98}
99
100impl<const N: usize> fmt::Debug for FixedString<N> {
101    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102        write!(f, "FixedString<{}>({:?})", N, self.as_str())
103    }
104}
105
106impl<const N: usize> fmt::Display for FixedString<N> {
107    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108        f.write_str(self.as_str())
109    }
110}
111
112// ---------------------------------------------------------------------------
113// Serde: serialize as a JSON string, deserialize from a JSON string
114// ---------------------------------------------------------------------------
115
116impl<const N: usize> serde::Serialize for FixedString<N> {
117    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
118        serializer.serialize_str(self.as_str())
119    }
120}
121
122impl<'de, const N: usize> serde::Deserialize<'de> for FixedString<N> {
123    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
124        struct Visitor<const M: usize>;
125
126        impl<'de, const M: usize> serde::de::Visitor<'de> for Visitor<M> {
127            type Value = FixedString<M>;
128
129            fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130                write!(f, "a string of at most {} bytes", M)
131            }
132
133            fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
134                let mut fs = FixedString::<M>::new();
135                fs.set(v);
136                Ok(fs)
137            }
138        }
139
140        deserializer.deserialize_str(Visitor::<N>)
141    }
142}
143
144// ---------------------------------------------------------------------------
145// From / Into conversions
146// ---------------------------------------------------------------------------
147
148impl<const N: usize> From<&str> for FixedString<N> {
149    fn from(s: &str) -> Self {
150        let mut fs = Self::new();
151        fs.set(s);
152        fs
153    }
154}
155
156// ---------------------------------------------------------------------------
157// Tests
158// ---------------------------------------------------------------------------
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    #[test]
165    fn new_is_empty() {
166        let s = FixedString::<64>::new();
167        assert!(s.is_empty());
168        assert_eq!(s.len(), 0);
169        assert_eq!(s.as_str(), "");
170    }
171
172    #[test]
173    fn set_and_read() {
174        let mut s = FixedString::<64>::new();
175        s.set("Hello, world!");
176        assert_eq!(s.as_str(), "Hello, world!");
177        assert_eq!(s.len(), 13);
178        assert!(!s.is_empty());
179    }
180
181    #[test]
182    fn truncation_at_capacity() {
183        let mut s = FixedString::<4>::new();
184        s.set("Hello");
185        assert_eq!(s.as_str(), "Hell");
186        assert_eq!(s.len(), 4);
187    }
188
189    #[test]
190    fn truncation_at_utf8_boundary() {
191        // '€' is 3 bytes in UTF-8 (0xE2 0x82 0xAC)
192        let mut s = FixedString::<5>::new();
193        s.set("ab€x");
194        // "ab€" = 2 + 3 = 5 bytes, fits exactly
195        assert_eq!(s.as_str(), "ab€");
196
197        let mut s = FixedString::<4>::new();
198        s.set("ab€");
199        // "ab€" = 5 bytes, doesn't fit. Truncate to "ab" (2 bytes)
200        assert_eq!(s.as_str(), "ab");
201    }
202
203    #[test]
204    fn clear() {
205        let mut s = FixedString::<64>::new();
206        s.set("test");
207        assert!(!s.is_empty());
208        s.clear();
209        assert!(s.is_empty());
210        assert_eq!(s.as_str(), "");
211    }
212
213    #[test]
214    fn exact_capacity_fill() {
215        let mut s = FixedString::<5>::new();
216        s.set("abcde");
217        assert_eq!(s.as_str(), "abcde");
218        assert_eq!(s.len(), 5);
219    }
220
221    #[test]
222    fn overwrite_shorter() {
223        let mut s = FixedString::<64>::new();
224        s.set("Hello, world!");
225        s.set("Hi");
226        assert_eq!(s.as_str(), "Hi");
227        assert_eq!(s.len(), 2);
228    }
229
230    #[test]
231    fn equality() {
232        let a = FixedString::<64>::from("test");
233        let b = FixedString::<64>::from("test");
234        let c = FixedString::<64>::from("other");
235        assert_eq!(a, b);
236        assert_ne!(a, c);
237    }
238
239    #[test]
240    fn display() {
241        let s = FixedString::<64>::from("display me");
242        assert_eq!(format!("{}", s), "display me");
243    }
244
245    #[test]
246    fn debug() {
247        let s = FixedString::<32>::from("debug me");
248        let dbg = format!("{:?}", s);
249        assert!(dbg.contains("FixedString<32>"));
250        assert!(dbg.contains("debug me"));
251    }
252
253    #[test]
254    fn serde_roundtrip() {
255        let original = FixedString::<64>::from("serde test");
256        let json = serde_json::to_string(&original).unwrap();
257        assert_eq!(json, "\"serde test\"");
258
259        let restored: FixedString<64> = serde_json::from_str(&json).unwrap();
260        assert_eq!(original, restored);
261    }
262
263    #[test]
264    fn serde_truncation() {
265        let json = "\"this string is longer than eight bytes\"";
266        let s: FixedString<8> = serde_json::from_str(json).unwrap();
267        assert_eq!(s.as_str(), "this str");
268    }
269
270    #[test]
271    fn default_is_empty() {
272        let s = FixedString::<64>::default();
273        assert!(s.is_empty());
274    }
275
276    #[test]
277    fn copy_semantics() {
278        let a = FixedString::<64>::from("copy me");
279        let b = a; // Copy, not move
280        assert_eq!(a.as_str(), "copy me");
281        assert_eq!(b.as_str(), "copy me");
282    }
283
284    #[test]
285    fn capacity() {
286        let s = FixedString::<128>::new();
287        assert_eq!(s.capacity(), 128);
288    }
289}