arcium-primitives 0.8.5

Arcium primitives
Documentation
//! [`InPlaceCodec`] for primitive scalar types used as `HeapArray` elements.
//!
//! All encodings are fixed-width little-endian, hence architecture-independent. `usize` is encoded
//! as a fixed 8 bytes (not the platform word size) so the format is portable across 32/64-bit
//! targets. Decoding MUST validate that the value fits into the local `usize`.

use std::mem::MaybeUninit;

use super::InPlaceCodec;
use crate::errors::PrimitiveError;

macro_rules! impl_inplace_codec_le {
    ($($ty:ty),* $(,)?) => {$(
        // SAFETY: `write_le_bytes` MUST write all `size_of::<$ty>()` little-endian bytes,
        // initializing every one. Every bit pattern of that width is a valid `$ty`, so
        // `read_le_bytes` is a total, unbiased inverse. Architecture-independent by construction
        // (fixed LE).
        unsafe impl InPlaceCodec for $ty {
            const ENCODED_SIZE: usize = std::mem::size_of::<$ty>();

            fn write_le_bytes(&self, out: &mut [MaybeUninit<u8>]) {
                for (slot, &b) in out.iter_mut().zip(self.to_le_bytes().iter()) {
                    slot.write(b);
                }
            }

            fn read_le_bytes(bytes: &[u8]) -> Result<Self, PrimitiveError> {
                let bytes = bytes.try_into().map_err(|_| {
                    PrimitiveError::InvalidSize(Self::ENCODED_SIZE, bytes.len())
                })?;
                Ok(<$ty>::from_le_bytes(bytes))
            }
        }
    )*};
}

impl_inplace_codec_le!(u8, u16, u32, u64);

// SAFETY: `bool` encodes as a single canonical byte (`0`/`1`). `write_le_bytes` MUST initialize it.
// `read_le_bytes` MUST reject any other value, keeping the round-trip unbiased.
unsafe impl InPlaceCodec for bool {
    const ENCODED_SIZE: usize = 1;

    fn write_le_bytes(&self, out: &mut [MaybeUninit<u8>]) {
        out[0].write(*self as u8);
    }

    fn read_le_bytes(bytes: &[u8]) -> Result<Self, PrimitiveError> {
        match bytes[0] {
            0 => Ok(false),
            1 => Ok(true),
            other => Err(PrimitiveError::DeserializationFailed(format!(
                "non-canonical bool byte: {other}"
            ))),
        }
    }
}

// SAFETY: `usize` is encoded as a fixed 8-byte little-endian value (architecture-independent).
// `write_le_bytes` MUST initialize all 8 bytes. `read_le_bytes` MUST reject values that don't fit
// the local `usize` (relevant on 32-bit targets), keeping the round-trip unbiased where it
// succeeds.
unsafe impl InPlaceCodec for usize {
    const ENCODED_SIZE: usize = 8;

    fn write_le_bytes(&self, out: &mut [MaybeUninit<u8>]) {
        for (slot, &b) in out.iter_mut().zip((*self as u64).to_le_bytes().iter()) {
            slot.write(b);
        }
    }

    fn read_le_bytes(bytes: &[u8]) -> Result<Self, PrimitiveError> {
        let bytes = bytes
            .try_into()
            .map_err(|_| PrimitiveError::InvalidSize(Self::ENCODED_SIZE, bytes.len()))?;
        let value = u64::from_le_bytes(bytes);
        usize::try_from(value).map_err(|_| {
            PrimitiveError::DeserializationFailed(format!("usize out of range: {value}"))
        })
    }
}

#[cfg(test)]
mod tests {
    use super::InPlaceCodec;

    fn roundtrip<T: InPlaceCodec + PartialEq + std::fmt::Debug>(v: T) {
        let bytes = v.to_inplace_bytes();
        assert_eq!(bytes.len(), T::ENCODED_SIZE);
        assert_eq!(T::from_inplace_bytes(&bytes).unwrap(), v);
    }

    #[test]
    fn prim_inplace_roundtrips() {
        roundtrip(true);
        roundtrip(false);
        roundtrip(0xABu8);
        roundtrip(0x1234u16);
        roundtrip(0xDEAD_BEEFu32);
        roundtrip(0x0123_4567_89AB_CDEFu64);
        roundtrip(1234567usize);
    }

    #[test]
    fn bool_rejects_non_canonical() {
        assert!(bool::from_inplace_bytes(&[2]).is_err());
    }
}