Skip to main content

ic_wire/packed/
mod.rs

1//! Fixed-width bulk fields within the single Serde/Postcard codec.
2use alloc::vec::Vec;
3use core::{
4    fmt::{self, Debug},
5    marker::PhantomData,
6};
7use serde::Serialize;
8
9/// Supported fixed-width elements of [`Packed`]. Sealed to the qualified types.
10/// Custom types, floats and platform-sized integers are deliberately excluded.
11pub trait PackedElement: sealed::Word {}
12mod sealed {
13    use super::*;
14    pub trait Word: bytemuck::Pod + Debug + Eq {
15        #[cfg(feature = "typescript")]
16        const SCHEMA: &'static str;
17        fn read_le(bytes: &[u8]) -> Self;
18        #[cfg(target_endian = "big")]
19        fn append_le(self, bytes: &mut Vec<u8>);
20    }
21    // These concrete readers cross the crate boundary when a consumer instantiates
22    // Packed<T>. Expose their bodies for specialization of the bulk copy loop.
23    macro_rules! words {
24    ($($ty:ty),*) => {$(
25        impl super::PackedElement for $ty {}
26        impl Word for $ty {
27            #[cfg(feature = "typescript")]
28            const SCHEMA: &'static str = stringify!($ty);
29            #[inline]
30            fn read_le(bytes: &[u8]) -> Self {
31                Self::from_le_bytes(bytes.try_into().unwrap())
32            }
33            #[cfg(target_endian = "big")]
34            #[inline]
35            fn append_le(self, bytes: &mut Vec<u8>) {
36                bytes.extend_from_slice(&self.to_le_bytes());
37            }
38        }
39    )*};
40}
41    words!(i16, u16, i32, u32, i64, u64);
42    impl super::PackedElement for [u8; 32] {}
43    impl Word for [u8; 32] {
44        #[cfg(feature = "typescript")]
45        const SCHEMA: &'static str = "hash32";
46        #[inline]
47        fn read_le(bytes: &[u8]) -> Self {
48            bytes.try_into().unwrap()
49        }
50        #[cfg(target_endian = "big")]
51        #[inline]
52        fn append_le(self, bytes: &mut Vec<u8>) {
53            bytes.extend_from_slice(&self);
54        }
55    }
56}
57
58/// Owned fixed-width words serialized as one Postcard byte buffer.
59///
60/// Supports i16/u16/i32/u32/i64/u64 and 32-byte hashes. Runtime [`crate::Limits`]
61/// bound admission; this type has no per-field element limit. Small integers
62/// may use more bytes than ordinary lists. Switching from Vec requires coordinated
63/// consumers; see the packed-field guide.
64///
65/// ```compile_fail
66/// let unsupported = ic_wire::Packed::new(vec![1.0_f32]);
67/// fn serializable(_: impl serde::Serialize) {}
68/// serializable(unsupported);
69/// ```
70#[derive(Clone, Debug, Eq, PartialEq)]
71pub struct Packed<T>(Vec<T>);
72impl<T> Packed<T> {
73    /// Wrap owned words without copying. Admission happens at the codec boundary.
74    pub fn new(values: Vec<T>) -> Self {
75        Self(values)
76    }
77    /// Borrow the words.
78    pub fn as_slice(&self) -> &[T] {
79        &self.0
80    }
81    /// Recover the owned words without copying.
82    pub fn into_vec(self) -> Vec<T> {
83        self.0
84    }
85}
86impl<T: PackedElement> Serialize for Packed<T> {
87    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
88        // Opt-in field layout: element count + per-element encoding becomes
89        // byte count + fixed LE words. [32767_i16]: 01 fe ff 03 -> 02 ff 7f.
90        // One 32-byte hash changes its prefix 01 -> 20; hash bytes are unchanged.
91        // Adoption requires coordinated schemas/clients and retained retry bytes;
92        // per-element traversal becomes byte-buffer accounting. Packed<T>
93        // has no field-specific N; consumers requiring one need an explicit
94        // field constraint. Ordinary Vec encoding is unchanged.
95        #[cfg(target_endian = "little")]
96        {
97            serializer.serialize_bytes(bytemuck::must_cast_slice(&self.0))
98        }
99        #[cfg(target_endian = "big")]
100        {
101            let mut bytes = Vec::new();
102            let len = self
103                .0
104                .len()
105                .checked_mul(size_of::<T>())
106                .ok_or_else(|| serde::ser::Error::custom("length overflow"))?;
107            bytes
108                .try_reserve_exact(len)
109                .map_err(|_| serde::ser::Error::custom("allocation"))?;
110            for value in &self.0 {
111                value.append_le(&mut bytes);
112            }
113            serializer.serialize_bytes(&bytes)
114        }
115    }
116}
117impl<'de, T: PackedElement> serde::Deserialize<'de> for Packed<T> {
118    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
119        deserializer
120            .deserialize_bytes(PackedVisitor {
121                marker: PhantomData,
122            })
123            .map(Self)
124    }
125}
126// Only the word type is part of the field type. No per-field N.
127// Native admission checks the byte buffer before this visitor allocates;
128// unlike the prior prototype, the field relies on runtime Limits like Vec<T>.
129struct PackedVisitor<T> {
130    marker: PhantomData<T>,
131}
132impl<T: PackedElement> serde::de::Visitor<'_> for PackedVisitor<T> {
133    type Value = Vec<T>;
134    fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135        f.write_str("packed little-endian words")
136    }
137    #[inline(never)]
138    fn visit_bytes<E: serde::de::Error>(self, bytes: &[u8]) -> Result<Self::Value, E> {
139        let width = size_of::<T>();
140        if !bytes.len().is_multiple_of(width) {
141            return Err(E::custom("invalid packed length"));
142        }
143        let mut values = Vec::new();
144        values
145            .try_reserve_exact(bytes.len() / width)
146            .map_err(|_| E::custom("allocation"))?;
147        values.extend(bytes.chunks_exact(width).map(T::read_le));
148        Ok(values)
149    }
150}