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    fn from(value: &[u8]) -> Self {
181        Self::try_from_slice(value).expect("FixedBytes capacity exceeded")
182    }
183}
184
185impl<const N: usize> From<&[u8; N]> for FixedBytes<N> {
186    fn from(value: &[u8; N]) -> Self {
187        Self {
188            data: *value,
189            len: N,
190        }
191    }
192}
193
194impl<const N: usize> fmt::Debug for FixedBytes<N> {
195    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
196        f.debug_tuple("FixedBytes").field(&self.as_bytes()).finish()
197    }
198}
199
200impl<const N: usize> fmt::Display for FixedBytes<N> {
201    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202        match self.try_as_str() {
203            Ok(value) => f.write_str(value),
204            Err(_) => write!(f, "{:?}", self.as_bytes()),
205        }
206    }
207}
208
209/// Stack-allocated, valid UTF-8 text with a fixed byte capacity.
210#[derive(Clone, Copy, PartialEq, Eq)]
211pub struct FixedStr<const N: usize>(FixedBytes<N>);
212
213impl<const N: usize> FixedStr<N> {
214    /// Creates an empty string.
215    pub const fn new() -> Self {
216        Self(FixedBytes::new())
217    }
218
219    /// Returns the UTF-8 byte length.
220    pub const fn len(&self) -> usize {
221        self.0.len()
222    }
223
224    /// Returns whether the string is empty.
225    pub const fn is_empty(&self) -> bool {
226        self.0.is_empty()
227    }
228
229    /// Returns the maximum UTF-8 byte capacity.
230    pub const fn capacity(&self) -> usize {
231        N
232    }
233
234    /// Returns the remaining UTF-8 byte capacity.
235    pub const fn remaining_capacity(&self) -> usize {
236        self.0.remaining_capacity()
237    }
238
239    /// Returns the contents as a string slice.
240    pub fn as_str(&self) -> &str {
241        // Construction and mutation accept only valid UTF-8.
242        unsafe { core::str::from_utf8_unchecked(self.0.as_bytes()) }
243    }
244
245    /// Returns the UTF-8 bytes.
246    pub fn as_bytes(&self) -> &[u8] {
247        self.0.as_bytes()
248    }
249
250    /// Removes all text.
251    pub fn clear(&mut self) {
252        self.0.clear();
253    }
254
255    /// Appends text and returns its byte range.
256    pub fn try_push_str(&mut self, value: &str) -> Result<Range<usize>, CapacityError> {
257        self.0.try_push_slice(value.as_bytes())
258    }
259
260    /// Appends text and returns its byte range.
261    ///
262    /// # Panics
263    /// Panics if the text exceeds the remaining capacity.
264    pub fn push_str(&mut self, value: &str) -> Range<usize> {
265        self.try_push_str(value)
266            .expect("FixedStr capacity exceeded")
267    }
268
269    /// Appends one Unicode scalar and returns its UTF-8 byte range.
270    pub fn try_push(&mut self, value: char) -> Result<Range<usize>, CapacityError> {
271        let mut bytes = [0; 4];
272        self.try_push_str(value.encode_utf8(&mut bytes))
273    }
274
275    /// Shortens the string to `new_len` UTF-8 bytes.
276    ///
277    /// # Panics
278    /// Panics unless `new_len` is a character boundary within the string.
279    pub fn truncate(&mut self, new_len: usize) {
280        assert!(self.as_str().is_char_boundary(new_len));
281        self.0.truncate(new_len);
282    }
283
284    /// Converts the string into its fixed byte buffer.
285    pub const fn into_bytes(self) -> FixedBytes<N> {
286        self.0
287    }
288}
289
290impl<const N: usize> Default for FixedStr<N> {
291    fn default() -> Self {
292        Self::new()
293    }
294}
295
296impl<const N: usize> TryFrom<&str> for FixedStr<N> {
297    type Error = CapacityError;
298
299    fn try_from(value: &str) -> Result<Self, Self::Error> {
300        Ok(Self(FixedBytes::try_from_slice(value.as_bytes())?))
301    }
302}
303
304impl<const N: usize> TryFrom<FixedBytes<N>> for FixedStr<N> {
305    type Error = core::str::Utf8Error;
306
307    fn try_from(value: FixedBytes<N>) -> Result<Self, Self::Error> {
308        value.try_as_str()?;
309        Ok(Self(value))
310    }
311}
312
313impl<const N: usize> fmt::Debug for FixedStr<N> {
314    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
315        f.debug_tuple("FixedStr").field(&self.as_str()).finish()
316    }
317}
318
319impl<const N: usize> fmt::Display for FixedStr<N> {
320    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
321        f.write_str(self.as_str())
322    }
323}
324
325/// UTF-16 byte order used by [`FixedUtf16`].
326pub trait Utf16ByteOrder: Copy {
327    /// Decodes one code unit from its on-disk byte order.
328    fn read(bytes: [u8; 2]) -> u16;
329    /// Encodes one code unit into its on-disk byte order.
330    fn write(value: u16) -> [u8; 2];
331}
332
333/// Little-endian UTF-16 code-unit encoding.
334#[derive(Debug, Clone, Copy, PartialEq, Eq)]
335pub struct LittleEndian;
336
337/// Big-endian UTF-16 code-unit encoding.
338#[derive(Debug, Clone, Copy, PartialEq, Eq)]
339pub struct BigEndian;
340
341impl Utf16ByteOrder for LittleEndian {
342    fn read(bytes: [u8; 2]) -> u16 {
343        u16::from_le_bytes(bytes)
344    }
345    fn write(value: u16) -> [u8; 2] {
346        value.to_le_bytes()
347    }
348}
349
350impl Utf16ByteOrder for BigEndian {
351    fn read(bytes: [u8; 2]) -> u16 {
352        u16::from_be_bytes(bytes)
353    }
354    fn write(value: u16) -> [u8; 2] {
355        value.to_be_bytes()
356    }
357}
358
359/// Fixed-width, NUL-padded UTF-16 code units in a specified byte order.
360#[repr(C)]
361#[derive(Debug, Clone, Copy, PartialEq, Eq)]
362pub struct FixedUtf16<const N: usize, E: Utf16ByteOrder> {
363    data: [[u8; 2]; N],
364    byte_order: PhantomData<E>,
365}
366
367/// Fixed-width little-endian UTF-16 text.
368pub type FixedUtf16Le<const N: usize> = FixedUtf16<N, LittleEndian>;
369/// Fixed-width big-endian UTF-16 text.
370pub type FixedUtf16Be<const N: usize> = FixedUtf16<N, BigEndian>;
371
372impl<const N: usize, E: Utf16ByteOrder> FixedUtf16<N, E> {
373    /// Creates a NUL-filled value.
374    pub const fn new() -> Self {
375        Self {
376            data: [[0; 2]; N],
377            byte_order: PhantomData,
378        }
379    }
380
381    /// Encodes a string into fixed-width UTF-16.
382    pub fn try_from_str(value: &str) -> Result<Self, CapacityError> {
383        let mut result = Self::new();
384        for (index, unit) in value.encode_utf16().enumerate() {
385            if index == N {
386                return Err(CapacityError);
387            }
388            result.data[index] = E::write(unit);
389        }
390        Ok(result)
391    }
392
393    /// Returns all encoded code-unit bytes, including NUL padding.
394    pub fn as_bytes(&self) -> &[[u8; 2]; N] {
395        &self.data
396    }
397
398    /// Decodes code units up to the first NUL.
399    pub fn decode(&self) -> impl Iterator<Item = Result<char, core::char::DecodeUtf16Error>> + '_ {
400        char::decode_utf16(
401            self.data
402                .iter()
403                .map(|bytes| E::read(*bytes))
404                .take_while(|unit| *unit != 0),
405        )
406    }
407
408    #[cfg(feature = "alloc")]
409    /// Decodes the value into an allocated UTF-8 string.
410    pub fn to_string(&self) -> Result<alloc::string::String, core::char::DecodeUtf16Error> {
411        self.decode().collect()
412    }
413}
414
415impl<const N: usize, E: Utf16ByteOrder> Default for FixedUtf16<N, E> {
416    fn default() -> Self {
417        Self::new()
418    }
419}
420
421#[cfg(feature = "bytemuck")]
422unsafe impl<const N: usize, E: Utf16ByteOrder + 'static> bytemuck::Zeroable for FixedUtf16<N, E> {}
423#[cfg(feature = "bytemuck")]
424unsafe impl<const N: usize, E: Utf16ByteOrder + 'static> bytemuck::Pod for FixedUtf16<N, E> {}
425
426#[cfg(test)]
427mod tests {
428    use super::*;
429
430    #[test]
431    fn bytes_allow_non_utf8() {
432        let bytes = FixedBytes::<2>::try_from_slice([0xff, 0].as_slice()).unwrap();
433        assert!(bytes.try_as_str().is_err());
434    }
435
436    #[test]
437    fn fixed_str_preserves_utf8() {
438        let mut text = FixedStr::<8>::try_from("é").unwrap();
439        text.try_push('!').unwrap();
440        assert_eq!(text.as_str(), "é!");
441    }
442
443    #[test]
444    fn utf16_round_trips_both_orders() {
445        let le = FixedUtf16Le::<8>::try_from_str("A😀").unwrap();
446        let be = FixedUtf16Be::<8>::try_from_str("A😀").unwrap();
447        assert_eq!(le.to_string().unwrap(), "A😀");
448        assert_eq!(be.to_string().unwrap(), "A😀");
449    }
450}