Skip to main content

primitives/utils/codec/
prims.rs

1//! [`InPlaceCodec`] for primitive scalar types used as `HeapArray` elements.
2//!
3//! All encodings are fixed-width little-endian, hence architecture-independent. `usize` is encoded
4//! as a fixed 8 bytes (not the platform word size) so the format is portable across 32/64-bit
5//! targets. Decoding MUST validate that the value fits into the local `usize`.
6
7use std::mem::MaybeUninit;
8
9use super::InPlaceCodec;
10use crate::errors::PrimitiveError;
11
12macro_rules! impl_inplace_codec_le {
13    ($($ty:ty),* $(,)?) => {$(
14        // SAFETY: `write_le_bytes` MUST write all `size_of::<$ty>()` little-endian bytes,
15        // initializing every one. Every bit pattern of that width is a valid `$ty`, so
16        // `read_le_bytes` is a total, unbiased inverse. Architecture-independent by construction
17        // (fixed LE).
18        unsafe impl InPlaceCodec for $ty {
19            const ENCODED_SIZE: usize = std::mem::size_of::<$ty>();
20
21            fn write_le_bytes(&self, out: &mut [MaybeUninit<u8>]) {
22                for (slot, &b) in out.iter_mut().zip(self.to_le_bytes().iter()) {
23                    slot.write(b);
24                }
25            }
26
27            fn read_le_bytes(bytes: &[u8]) -> Result<Self, PrimitiveError> {
28                let bytes = bytes.try_into().map_err(|_| {
29                    PrimitiveError::InvalidSize(Self::ENCODED_SIZE, bytes.len())
30                })?;
31                Ok(<$ty>::from_le_bytes(bytes))
32            }
33        }
34    )*};
35}
36
37impl_inplace_codec_le!(u8, u16, u32, u64);
38
39// SAFETY: `bool` encodes as a single canonical byte (`0`/`1`). `write_le_bytes` MUST initialize it.
40// `read_le_bytes` MUST reject any other value, keeping the round-trip unbiased.
41unsafe impl InPlaceCodec for bool {
42    const ENCODED_SIZE: usize = 1;
43
44    fn write_le_bytes(&self, out: &mut [MaybeUninit<u8>]) {
45        out[0].write(*self as u8);
46    }
47
48    fn read_le_bytes(bytes: &[u8]) -> Result<Self, PrimitiveError> {
49        match bytes[0] {
50            0 => Ok(false),
51            1 => Ok(true),
52            other => Err(PrimitiveError::DeserializationFailed(format!(
53                "non-canonical bool byte: {other}"
54            ))),
55        }
56    }
57}
58
59// SAFETY: `usize` is encoded as a fixed 8-byte little-endian value (architecture-independent).
60// `write_le_bytes` MUST initialize all 8 bytes. `read_le_bytes` MUST reject values that don't fit
61// the local `usize` (relevant on 32-bit targets), keeping the round-trip unbiased where it
62// succeeds.
63unsafe impl InPlaceCodec for usize {
64    const ENCODED_SIZE: usize = 8;
65
66    fn write_le_bytes(&self, out: &mut [MaybeUninit<u8>]) {
67        for (slot, &b) in out.iter_mut().zip((*self as u64).to_le_bytes().iter()) {
68            slot.write(b);
69        }
70    }
71
72    fn read_le_bytes(bytes: &[u8]) -> Result<Self, PrimitiveError> {
73        let bytes = bytes
74            .try_into()
75            .map_err(|_| PrimitiveError::InvalidSize(Self::ENCODED_SIZE, bytes.len()))?;
76        let value = u64::from_le_bytes(bytes);
77        usize::try_from(value).map_err(|_| {
78            PrimitiveError::DeserializationFailed(format!("usize out of range: {value}"))
79        })
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::InPlaceCodec;
86
87    fn roundtrip<T: InPlaceCodec + PartialEq + std::fmt::Debug>(v: T) {
88        let bytes = v.to_inplace_bytes();
89        assert_eq!(bytes.len(), T::ENCODED_SIZE);
90        assert_eq!(T::from_inplace_bytes(&bytes).unwrap(), v);
91    }
92
93    #[test]
94    fn prim_inplace_roundtrips() {
95        roundtrip(true);
96        roundtrip(false);
97        roundtrip(0xABu8);
98        roundtrip(0x1234u16);
99        roundtrip(0xDEAD_BEEFu32);
100        roundtrip(0x0123_4567_89AB_CDEFu64);
101        roundtrip(1234567usize);
102    }
103
104    #[test]
105    fn bool_rejects_non_canonical() {
106        assert!(bool::from_inplace_bytes(&[2]).is_err());
107    }
108}