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(all(feature = "typescript", not(target_family = "wasm")))]
16        const SCHEMA: &'static str;
17        fn read_le(bytes: &[u8]) -> Self;
18        #[cfg(any(target_endian = "big", test))]
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(all(feature = "typescript", not(target_family = "wasm")))]
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(any(target_endian = "big", test))]
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!(i8, i16, u16, i32, u32, i64, u64);
42    impl super::PackedElement for [u8; 32] {}
43    impl Word for [u8; 32] {
44        #[cfg(all(feature = "typescript", not(target_family = "wasm")))]
45        const SCHEMA: &'static str = "hash32";
46        #[inline]
47        fn read_le(bytes: &[u8]) -> Self {
48            bytes.try_into().unwrap()
49        }
50        #[cfg(any(target_endian = "big", test))]
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/// Recommended for large homogeneous integer/hash fields. Use ordinary lists
61/// for general collections or when small-value varint compactness takes priority.
62/// Supports i8/i16/u16/i32/u32/i64/u64 and 32-byte hashes. Runtime [`crate::Limits`]
63/// bound admission; this type has no per-field element limit. Small integers
64/// may use more bytes than ordinary lists. Switching from Vec requires coordinated
65/// consumers; see the packed-field guide.
66///
67/// ```compile_fail
68/// let unsupported = ic_wire::Packed::new(vec![1.0_f32]);
69/// fn serializable(_: impl serde::Serialize) {}
70/// serializable(unsupported);
71/// ```
72#[derive(Clone, Debug, Eq, PartialEq)]
73pub struct Packed<T>(Vec<T>);
74impl<T> Packed<T> {
75    /// Wrap owned words without copying. Admission happens at the codec boundary.
76    pub fn new(values: Vec<T>) -> Self {
77        Self(values)
78    }
79    /// Borrow the words.
80    pub fn as_slice(&self) -> &[T] {
81        &self.0
82    }
83    /// Recover the owned words without copying.
84    pub fn into_vec(self) -> Vec<T> {
85        self.0
86    }
87}
88impl<T: PackedElement> Serialize for Packed<T> {
89    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
90        // Packed<i8> adds signed-byte bulk admission: [-128, 127] keeps the
91        // ordinary Vec bytes 02 80 7f, but metadata becomes Int8Array and
92        // traversal is charged per buffer. Regenerate clients and review limits.
93        // Other existing packed encodings are unchanged.
94        // Opt-in field layout: element count + per-element encoding becomes
95        // byte count + fixed LE words. [32767_i16]: 01 fe ff 03 -> 02 ff 7f.
96        // One 32-byte hash changes its prefix 01 -> 20; hash bytes are unchanged.
97        // Adoption requires coordinated schemas/clients and retained retry bytes;
98        // per-element traversal becomes byte-buffer accounting. Packed<T>
99        // has no field-specific N; consumers requiring one need an explicit
100        // field constraint. Ordinary Vec encoding is unchanged.
101        #[cfg(target_endian = "little")]
102        {
103            serializer.serialize_bytes(bytemuck::must_cast_slice(&self.0))
104        }
105        #[cfg(target_endian = "big")]
106        {
107            let mut bytes = Vec::new();
108            let len = self
109                .0
110                .len()
111                .checked_mul(size_of::<T>())
112                .ok_or_else(|| serde::ser::Error::custom("length overflow"))?;
113            bytes
114                .try_reserve_exact(len)
115                .map_err(|_| serde::ser::Error::custom("allocation"))?;
116            for value in &self.0 {
117                value.append_le(&mut bytes);
118            }
119            serializer.serialize_bytes(&bytes)
120        }
121    }
122}
123impl<'de, T: PackedElement> serde::Deserialize<'de> for Packed<T> {
124    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
125        deserializer
126            .deserialize_bytes(PackedVisitor {
127                marker: PhantomData,
128            })
129            .map(Self)
130    }
131}
132// Only the word type is part of the field type. No per-field N.
133// Native admission checks the byte buffer before this visitor allocates;
134// unlike the prior prototype, the field relies on runtime Limits like Vec<T>.
135struct PackedVisitor<T> {
136    marker: PhantomData<T>,
137}
138impl<T: PackedElement> serde::de::Visitor<'_> for PackedVisitor<T> {
139    type Value = Vec<T>;
140    fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
141        f.write_str("packed little-endian words")
142    }
143    #[inline(never)]
144    fn visit_bytes<E: serde::de::Error>(self, bytes: &[u8]) -> Result<Self::Value, E> {
145        let width = size_of::<T>();
146        if !bytes.len().is_multiple_of(width) {
147            return Err(E::custom("invalid packed length"));
148        }
149        let mut values = Vec::new();
150        values
151            .try_reserve_exact(bytes.len() / width)
152            .map_err(|_| E::custom("allocation"))?;
153        values.extend(bytes.chunks_exact(width).map(T::read_le));
154        Ok(values)
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    #[test]
161    fn endian_append_matches_wire_bytes() {
162        use super::sealed::Word;
163        use alloc::vec::Vec;
164        macro_rules! check {
165            ($($ty:ty),*) => {$(
166                let words = [<$ty>::MIN, 1, <$ty>::MAX];
167                let mut bytes = Vec::new();
168                for word in words { word.append_le(&mut bytes); }
169                let expected: Vec<_> = words.iter().flat_map(|word| word.to_le_bytes()).collect();
170                assert_eq!(bytes, expected);
171                for (chunk, word) in bytes.chunks_exact(size_of::<$ty>()).zip(words) {
172                    assert_eq!(<$ty>::read_le(chunk), word);
173                }
174            )*};
175        }
176        check!(i8, i16, u16, i32, u32, i64, u64);
177        let hash = core::array::from_fn::<_, 32, _>(|i| i as u8);
178        let mut bytes = Vec::new();
179        hash.append_le(&mut bytes);
180        assert_eq!(bytes, hash);
181        assert_eq!(<[u8; 32]>::read_le(&bytes), hash);
182    }
183}