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; 16] {}
43    impl Word for [u8; 16] {
44        #[cfg(all(feature = "typescript", not(target_family = "wasm")))]
45        const SCHEMA: &'static str = "hash16";
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    impl super::PackedElement for [u8; 32] {}
57    impl Word for [u8; 32] {
58        #[cfg(all(feature = "typescript", not(target_family = "wasm")))]
59        const SCHEMA: &'static str = "hash32";
60        #[inline]
61        fn read_le(bytes: &[u8]) -> Self {
62            bytes.try_into().unwrap()
63        }
64        #[cfg(any(target_endian = "big", test))]
65        #[inline]
66        fn append_le(self, bytes: &mut Vec<u8>) {
67            bytes.extend_from_slice(&self);
68        }
69    }
70}
71
72/// Owned fixed-width words serialized as one Postcard byte buffer.
73///
74/// Recommended for large homogeneous integer/hash fields. Use ordinary lists
75/// for general collections or when small-value varint compactness takes priority.
76/// Supports i8/i16/u16/i32/u32/i64/u64 and 16-/32-byte arrays. Runtime [`crate::Limits`]
77/// bound admission; this type has no per-field element limit. Small integers
78/// may use more bytes than ordinary lists. Switching from Vec requires coordinated
79/// consumers; see the packed-field guide.
80///
81/// ```compile_fail
82/// let unsupported = ic_wire::Packed::new(vec![1.0_f32]);
83/// fn serializable(_: impl serde::Serialize) {}
84/// serializable(unsupported);
85/// ```
86#[derive(Clone, Debug, Eq, PartialEq)]
87pub struct Packed<T>(Vec<T>);
88impl<T> Packed<T> {
89    /// Wrap owned words without copying. Admission happens at the codec boundary.
90    pub fn new(values: Vec<T>) -> Self {
91        Self(values)
92    }
93    /// Borrow the words.
94    pub fn as_slice(&self) -> &[T] {
95        &self.0
96    }
97    /// Recover the owned words without copying.
98    pub fn into_vec(self) -> Vec<T> {
99        self.0
100    }
101}
102impl<T: PackedElement> Serialize for Packed<T> {
103    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
104        // Packed<i8> adds signed-byte bulk admission: [-128, 127] keeps the
105        // ordinary Vec bytes 02 80 7f, but metadata becomes Int8Array and
106        // traversal is charged per buffer. Regenerate clients and review limits.
107        // Other existing packed encodings are unchanged.
108        // Opt-in field layout: element count + per-element encoding becomes
109        // byte count + fixed LE words. [32767_i16]: 01 fe ff 03 -> 02 ff 7f.
110        // One 16-byte ID changes 01 -> 10; metadata is hash16 / Uint8Array.
111        // Coordinate identities/clients and requalify buffer budgets on adoption.
112        // Existing field bytes, identities, budgets and acceptance are unchanged.
113        // One 32-byte hash changes its prefix 01 -> 20; hash bytes are unchanged.
114        // Adoption requires coordinated schemas/clients and retained retry bytes;
115        // per-element traversal becomes byte-buffer accounting. Packed<T>
116        // has no field-specific N; consumers requiring one need an explicit
117        // field constraint. Ordinary Vec encoding is unchanged.
118        serialize_words(&self.0, serializer)
119    }
120}
121pub(crate) fn serialize_words<T: PackedElement, S: serde::Serializer>(
122    words: &[T],
123    serializer: S,
124) -> Result<S::Ok, S::Error> {
125    #[cfg(target_endian = "little")]
126    {
127        serializer.serialize_bytes(bytemuck::must_cast_slice(words))
128    }
129    #[cfg(target_endian = "big")]
130    {
131        let mut bytes = Vec::new();
132        let len = words
133            .len()
134            .checked_mul(size_of::<T>())
135            .ok_or_else(|| serde::ser::Error::custom("length overflow"))?;
136        bytes
137            .try_reserve_exact(len)
138            .map_err(|_| serde::ser::Error::custom("allocation"))?;
139        for value in words {
140            value.append_le(&mut bytes);
141        }
142        serializer.serialize_bytes(&bytes)
143    }
144}
145impl<'de, T: PackedElement> serde::Deserialize<'de> for Packed<T> {
146    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
147        deserializer
148            .deserialize_bytes(PackedVisitor {
149                marker: PhantomData,
150            })
151            .map(Self)
152    }
153}
154// Share validation/allocation mechanics while preserving each specialized iterator.
155// A runtime visitor merge lost the bounded i16 bulk-copy optimization at opt-z.
156// This expansion keeps the original LE bytes, bound-before-allocation, descriptions,
157// error text and budget charges, without changing either public field type.
158macro_rules! decode_words {
159    ($error:ty, $bytes:ident, $width:expr, $excess:expr, $words:expr) => {{
160        if !$bytes.len().is_multiple_of($width) || $excess {
161            return Err(<$error as serde::de::Error>::custom(
162                "invalid packed length",
163            ));
164        }
165        let mut values = alloc::vec::Vec::new();
166        values
167            .try_reserve_exact($bytes.len() / $width)
168            .map_err(|_| <$error as serde::de::Error>::custom("allocation"))?;
169        values.extend($words);
170        Ok(values)
171    }};
172}
173pub(crate) use decode_words;
174
175// Only the word type is part of the field type. No per-field N.
176// Native admission checks the byte buffer before this visitor allocates;
177// unlike the prior prototype, the field relies on runtime Limits like Vec<T>.
178struct PackedVisitor<T> {
179    marker: PhantomData<T>,
180}
181impl<T: PackedElement> serde::de::Visitor<'_> for PackedVisitor<T> {
182    type Value = Vec<T>;
183    fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
184        f.write_str("packed little-endian words")
185    }
186    #[inline(never)]
187    fn visit_bytes<E: serde::de::Error>(self, bytes: &[u8]) -> Result<Self::Value, E> {
188        let width = size_of::<T>();
189        // Byte-aligned words can use the slice copy without initialization or
190        // per-word iteration. Wider words keep their unaligned LE reader.
191        // Bytes, identities, limits and malformed-input acceptance are unchanged.
192        #[cfg(target_endian = "little")]
193        if align_of::<T>() == 1 {
194            if !bytes.len().is_multiple_of(width) {
195                return Err(E::custom("invalid packed length"));
196            }
197            let mut values = Vec::new();
198            values
199                .try_reserve_exact(bytes.len() / width)
200                .map_err(|_| E::custom("allocation"))?;
201            let words =
202                bytemuck::try_cast_slice(bytes).map_err(|_| E::custom("invalid packed length"))?;
203            values.extend_from_slice(words);
204            return Ok(values);
205        }
206        decode_words!(
207            E,
208            bytes,
209            width,
210            false,
211            bytes.chunks_exact(width).map(T::read_le)
212        )
213    }
214}
215
216#[cfg(test)]
217mod tests;