Skip to main content

hadris_fixed/
lib.rs

1//! Fixed-capacity byte and text types for `no_std` applications.
2
3#![no_std]
4#![deny(missing_docs)]
5
6#[cfg(any(feature = "alloc", test))]
7extern crate alloc;
8
9use core::fmt;
10use core::marker::PhantomData;
11use core::ops::Range;
12
13/// The fixed-capacity buffer cannot hold the requested data.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub struct CapacityError;
16
17impl fmt::Display for CapacityError {
18    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19        f.write_str("fixed-capacity buffer is full")
20    }
21}
22
23impl core::error::Error for CapacityError {}
24
25/// Stack-allocated arbitrary bytes with a fixed maximum capacity.
26#[derive(Clone, Copy, PartialEq, Eq)]
27pub struct FixedBytes<const N: usize> {
28    data: [u8; N],
29    len: usize,
30}
31
32impl<const N: usize> FixedBytes<N> {
33    /// Creates an empty buffer.
34    pub const fn new() -> Self {
35        Self {
36            data: [0; N],
37            len: 0,
38        }
39    }
40
41    /// Compatibility name for [`new`](Self::new).
42    pub const fn empty() -> Self {
43        Self::new()
44    }
45
46    /// Creates a zero-filled buffer with an initialized length.
47    ///
48    /// # Panics
49    /// Panics when `size` exceeds the capacity.
50    pub const fn with_size(size: usize) -> Self {
51        assert!(size <= N);
52        Self {
53            data: [0; N],
54            len: size,
55        }
56    }
57
58    /// Returns the number of initialized bytes.
59    pub const fn len(&self) -> usize {
60        self.len
61    }
62
63    /// Returns whether the buffer contains no initialized bytes.
64    pub const fn is_empty(&self) -> bool {
65        self.len == 0
66    }
67
68    /// Returns the maximum number of bytes the buffer can hold.
69    pub const fn capacity(&self) -> usize {
70        N
71    }
72
73    /// Returns the number of bytes that can still be appended.
74    pub const fn remaining_capacity(&self) -> usize {
75        N - self.len
76    }
77
78    /// Returns the initialized portion of the buffer.
79    pub fn as_bytes(&self) -> &[u8] {
80        &self.data[..self.len]
81    }
82
83    /// Returns the initialized portion of the buffer mutably.
84    pub fn as_bytes_mut(&mut self) -> &mut [u8] {
85        &mut self.data[..self.len]
86    }
87
88    /// Interprets the initialized bytes as UTF-8.
89    pub fn try_as_str(&self) -> Result<&str, core::str::Utf8Error> {
90        core::str::from_utf8(self.as_bytes())
91    }
92
93    /// Returns the initialized bytes as UTF-8.
94    ///
95    /// # Panics
96    /// Panics if the buffer does not contain valid UTF-8.
97    pub fn as_str(&self) -> &str {
98        self.try_as_str()
99            .expect("FixedBytes contains invalid UTF-8")
100    }
101
102    /// Removes all initialized bytes.
103    pub fn clear(&mut self) {
104        self.len = 0;
105    }
106
107    /// Shortens the initialized portion to `new_len`.
108    ///
109    /// # Panics
110    /// Panics if `new_len` exceeds the current length.
111    pub fn truncate(&mut self, new_len: usize) {
112        assert!(new_len <= self.len);
113        self.len = new_len;
114    }
115
116    /// Extends the initialized portion with zero-filled bytes.
117    ///
118    /// # Panics
119    /// Panics if the requested bytes exceed the remaining capacity.
120    pub fn allocate(&mut self, bytes: usize) {
121        assert!(bytes <= self.remaining_capacity());
122        self.len += bytes;
123    }
124
125    /// Copies a byte slice into a new buffer.
126    pub fn try_from_slice(value: &[u8]) -> Result<Self, CapacityError> {
127        let mut result = Self::new();
128        result.try_push_slice(value)?;
129        Ok(result)
130    }
131
132    /// Appends a slice and returns the occupied range.
133    pub fn try_push_slice(&mut self, value: &[u8]) -> Result<Range<usize>, CapacityError> {
134        if value.len() > self.remaining_capacity() {
135            return Err(CapacityError);
136        }
137        let start = self.len;
138        self.len += value.len();
139        self.data[start..self.len].copy_from_slice(value);
140        Ok(start..self.len)
141    }
142
143    /// Appends a slice and returns the occupied range.
144    ///
145    /// # Panics
146    /// Panics if the slice exceeds the remaining capacity.
147    pub fn push_slice(&mut self, value: &[u8]) -> Range<usize> {
148        self.try_push_slice(value)
149            .expect("FixedBytes capacity exceeded")
150    }
151
152    /// Appends one byte and returns its index.
153    pub fn try_push_byte(&mut self, value: u8) -> Result<usize, CapacityError> {
154        if self.len == N {
155            return Err(CapacityError);
156        }
157        let index = self.len;
158        self.data[index] = value;
159        self.len += 1;
160        Ok(index)
161    }
162
163    /// Appends one byte and returns its index.
164    ///
165    /// # Panics
166    /// Panics if the buffer is full.
167    pub fn push_byte(&mut self, value: u8) -> usize {
168        self.try_push_byte(value)
169            .expect("FixedBytes capacity exceeded")
170    }
171}
172
173impl<const N: usize> Default for FixedBytes<N> {
174    fn default() -> Self {
175        Self::new()
176    }
177}
178
179impl<const N: usize> From<&[u8]> for FixedBytes<N> {
180    /// # Panics
181    /// Panics if the slice is longer than `N`. Use
182    /// [`FixedBytes::try_from_slice`] for a fallible conversion.
183    fn from(value: &[u8]) -> Self {
184        Self::try_from_slice(value).expect("FixedBytes capacity exceeded")
185    }
186}
187
188impl<const N: usize> From<&[u8; N]> for FixedBytes<N> {
189    fn from(value: &[u8; N]) -> Self {
190        Self {
191            data: *value,
192            len: N,
193        }
194    }
195}
196
197impl<const N: usize> fmt::Debug for FixedBytes<N> {
198    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199        f.debug_tuple("FixedBytes").field(&self.as_bytes()).finish()
200    }
201}
202
203impl<const N: usize> fmt::Display for FixedBytes<N> {
204    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205        match self.try_as_str() {
206            Ok(value) => f.write_str(value),
207            Err(_) => write!(f, "{:?}", self.as_bytes()),
208        }
209    }
210}
211
212/// Stack-allocated, valid UTF-8 text with a fixed byte capacity.
213#[derive(Clone, Copy, PartialEq, Eq)]
214pub struct FixedStr<const N: usize>(FixedBytes<N>);
215
216impl<const N: usize> FixedStr<N> {
217    /// Creates an empty string.
218    pub const fn new() -> Self {
219        Self(FixedBytes::new())
220    }
221
222    /// Returns the UTF-8 byte length.
223    pub const fn len(&self) -> usize {
224        self.0.len()
225    }
226
227    /// Returns whether the string is empty.
228    pub const fn is_empty(&self) -> bool {
229        self.0.is_empty()
230    }
231
232    /// Returns the maximum UTF-8 byte capacity.
233    pub const fn capacity(&self) -> usize {
234        N
235    }
236
237    /// Returns the remaining UTF-8 byte capacity.
238    pub const fn remaining_capacity(&self) -> usize {
239        self.0.remaining_capacity()
240    }
241
242    /// Returns the contents as a string slice.
243    pub fn as_str(&self) -> &str {
244        // Construction and mutation accept only valid UTF-8.
245        unsafe { core::str::from_utf8_unchecked(self.0.as_bytes()) }
246    }
247
248    /// Returns the UTF-8 bytes.
249    pub fn as_bytes(&self) -> &[u8] {
250        self.0.as_bytes()
251    }
252
253    /// Removes all text.
254    pub fn clear(&mut self) {
255        self.0.clear();
256    }
257
258    /// Appends text and returns its byte range.
259    pub fn try_push_str(&mut self, value: &str) -> Result<Range<usize>, CapacityError> {
260        self.0.try_push_slice(value.as_bytes())
261    }
262
263    /// Appends text and returns its byte range.
264    ///
265    /// # Panics
266    /// Panics if the text exceeds the remaining capacity.
267    pub fn push_str(&mut self, value: &str) -> Range<usize> {
268        self.try_push_str(value)
269            .expect("FixedStr capacity exceeded")
270    }
271
272    /// Appends one Unicode scalar and returns its UTF-8 byte range.
273    pub fn try_push(&mut self, value: char) -> Result<Range<usize>, CapacityError> {
274        let mut bytes = [0; 4];
275        self.try_push_str(value.encode_utf8(&mut bytes))
276    }
277
278    /// Shortens the string to `new_len` UTF-8 bytes.
279    ///
280    /// # Panics
281    /// Panics unless `new_len` is a character boundary within the string.
282    pub fn truncate(&mut self, new_len: usize) {
283        assert!(self.as_str().is_char_boundary(new_len));
284        self.0.truncate(new_len);
285    }
286
287    /// Converts the string into its fixed byte buffer.
288    pub const fn into_bytes(self) -> FixedBytes<N> {
289        self.0
290    }
291}
292
293impl<const N: usize> Default for FixedStr<N> {
294    fn default() -> Self {
295        Self::new()
296    }
297}
298
299impl<const N: usize> TryFrom<&str> for FixedStr<N> {
300    type Error = CapacityError;
301
302    fn try_from(value: &str) -> Result<Self, Self::Error> {
303        Ok(Self(FixedBytes::try_from_slice(value.as_bytes())?))
304    }
305}
306
307impl<const N: usize> TryFrom<FixedBytes<N>> for FixedStr<N> {
308    type Error = core::str::Utf8Error;
309
310    fn try_from(value: FixedBytes<N>) -> Result<Self, Self::Error> {
311        value.try_as_str()?;
312        Ok(Self(value))
313    }
314}
315
316impl<const N: usize> fmt::Debug for FixedStr<N> {
317    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
318        f.debug_tuple("FixedStr").field(&self.as_str()).finish()
319    }
320}
321
322impl<const N: usize> fmt::Display for FixedStr<N> {
323    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
324        f.write_str(self.as_str())
325    }
326}
327
328/// UTF-16 byte order used by [`FixedUtf16`].
329pub trait Utf16ByteOrder: Copy {
330    /// Decodes one code unit from its on-disk byte order.
331    fn read(bytes: [u8; 2]) -> u16;
332    /// Encodes one code unit into its on-disk byte order.
333    fn write(value: u16) -> [u8; 2];
334}
335
336/// Little-endian UTF-16 code-unit encoding.
337#[derive(Debug, Clone, Copy, PartialEq, Eq)]
338pub struct LittleEndian;
339
340/// Big-endian UTF-16 code-unit encoding.
341#[derive(Debug, Clone, Copy, PartialEq, Eq)]
342pub struct BigEndian;
343
344impl Utf16ByteOrder for LittleEndian {
345    fn read(bytes: [u8; 2]) -> u16 {
346        u16::from_le_bytes(bytes)
347    }
348    fn write(value: u16) -> [u8; 2] {
349        value.to_le_bytes()
350    }
351}
352
353impl Utf16ByteOrder for BigEndian {
354    fn read(bytes: [u8; 2]) -> u16 {
355        u16::from_be_bytes(bytes)
356    }
357    fn write(value: u16) -> [u8; 2] {
358        value.to_be_bytes()
359    }
360}
361
362/// Fixed-width, NUL-padded UTF-16 code units in a specified byte order.
363#[repr(C)]
364#[derive(Debug, Clone, Copy, PartialEq, Eq)]
365pub struct FixedUtf16<const N: usize, E: Utf16ByteOrder> {
366    data: [[u8; 2]; N],
367    byte_order: PhantomData<E>,
368}
369
370/// Fixed-width little-endian UTF-16 text.
371pub type FixedUtf16Le<const N: usize> = FixedUtf16<N, LittleEndian>;
372/// Fixed-width big-endian UTF-16 text.
373pub type FixedUtf16Be<const N: usize> = FixedUtf16<N, BigEndian>;
374
375impl<const N: usize, E: Utf16ByteOrder> FixedUtf16<N, E> {
376    /// Creates a NUL-filled value.
377    pub const fn new() -> Self {
378        Self {
379            data: [[0; 2]; N],
380            byte_order: PhantomData,
381        }
382    }
383
384    /// Encodes a string into fixed-width UTF-16.
385    pub fn try_from_str(value: &str) -> Result<Self, CapacityError> {
386        let mut result = Self::new();
387        for (index, unit) in value.encode_utf16().enumerate() {
388            if index == N {
389                return Err(CapacityError);
390            }
391            result.data[index] = E::write(unit);
392        }
393        Ok(result)
394    }
395
396    /// Returns all encoded code-unit bytes, including NUL padding.
397    pub fn as_bytes(&self) -> &[[u8; 2]; N] {
398        &self.data
399    }
400
401    /// Decodes code units up to the first NUL.
402    pub fn decode(&self) -> impl Iterator<Item = Result<char, core::char::DecodeUtf16Error>> + '_ {
403        char::decode_utf16(
404            self.data
405                .iter()
406                .map(|bytes| E::read(*bytes))
407                .take_while(|unit| *unit != 0),
408        )
409    }
410
411    #[cfg(feature = "alloc")]
412    /// Decodes the value into an allocated UTF-8 string.
413    pub fn to_string(&self) -> Result<alloc::string::String, core::char::DecodeUtf16Error> {
414        self.decode().collect()
415    }
416}
417
418impl<const N: usize, E: Utf16ByteOrder> Default for FixedUtf16<N, E> {
419    fn default() -> Self {
420        Self::new()
421    }
422}
423
424#[cfg(feature = "bytemuck")]
425unsafe impl<const N: usize, E: Utf16ByteOrder + 'static> bytemuck::Zeroable for FixedUtf16<N, E> {}
426#[cfg(feature = "bytemuck")]
427unsafe impl<const N: usize, E: Utf16ByteOrder + 'static> bytemuck::Pod for FixedUtf16<N, E> {}
428
429#[cfg(test)]
430mod tests {
431    use super::*;
432
433    #[test]
434    fn bytes_allow_non_utf8() {
435        let bytes = FixedBytes::<2>::try_from_slice([0xff, 0].as_slice()).unwrap();
436        assert!(bytes.try_as_str().is_err());
437    }
438
439    #[test]
440    fn fixed_str_preserves_utf8() {
441        let mut text = FixedStr::<8>::try_from("é").unwrap();
442        text.try_push('!').unwrap();
443        assert_eq!(text.as_str(), "é!");
444    }
445
446    #[test]
447    fn utf16_round_trips_both_orders() {
448        let le = FixedUtf16Le::<8>::try_from_str("A😀").unwrap();
449        let be = FixedUtf16Be::<8>::try_from_str("A😀").unwrap();
450        assert_eq!(le.to_string().unwrap(), "A😀");
451        assert_eq!(be.to_string().unwrap(), "A😀");
452    }
453}