Skip to main content

ic_wire/contract/
mod.rs

1#[cfg(test)]
2mod tests;
3
4use crate::{Error, Limits, Wire};
5use alloc::vec::Vec;
6use core::marker::PhantomData;
7use postcard::ser_flavors::Flavor;
8
9pub(crate) const HEADER: usize = 11;
10
11/// One consumer-owned endpoint contract. IDs must be unique across incompatible
12/// layouts and methods. A source rename alone must not change its identity.
13pub trait Method {
14    /// Typed request, including any consumer-owned proof/envelope.
15    type Request: Wire;
16    /// Typed response, typically `Result<Success, ApplicationError>`.
17    type Response: Wire;
18    /// Stable contract identity assigned by the consumer.
19    const ID: u64;
20    /// Exact exported IC method name; used by typed transport adapters.
21    const NAME: &'static str;
22    /// Request admission budget.
23    const REQUEST_LIMITS: Limits;
24    /// Reply admission budget.
25    const RESPONSE_LIMITS: Limits;
26}
27
28macro_rules! retained {
29    ($name:ident) => {
30        /// Validated, immutable bytes bound to a method and direction. Retain
31        /// these for retries; the codec does not decide when retry is safe.
32        pub struct $name<M: Method> {
33            bytes: Vec<u8>,
34            method: PhantomData<fn() -> M>,
35        }
36        impl<M: Method> $name<M> {
37            /// Borrow the exact encoding for transport or hashing.
38            pub fn as_bytes(&self) -> &[u8] {
39                &self.bytes
40            }
41            /// Transfer ownership to a transport or persistence owner.
42            pub fn into_bytes(self) -> Vec<u8> {
43                self.bytes
44            }
45        }
46    };
47}
48retained!(RequestBytes);
49retained!(ResponseBytes);
50
51pub(crate) fn admit<T: Wire>(limits: Limits) -> Result<usize, Error> {
52    let shape = T::SHAPE;
53    if [shape.bytes, shape.heap, shape.nodes, shape.depth].contains(&usize::MAX) {
54        return Err(Error::SchemaLimit);
55    }
56    let size = shape.bytes.checked_add(HEADER).ok_or(Error::SchemaLimit)?;
57    let allocation = shape.heap.checked_add(size).ok_or(Error::SchemaLimit)?;
58    if size > limits.bytes
59        || allocation > limits.allocation
60        || shape.nodes > limits.nodes
61        || shape.depth > limits.depth
62    {
63        return Err(Error::SchemaLimit);
64    }
65    Ok(size)
66}
67
68// Reserve fallibly once, then initialize only emitted bytes. Capacity is not
69// the schema limit: allocators may return more capacity than requested.
70pub(crate) struct Output {
71    pub(crate) bytes: Vec<u8>,
72    pub(crate) limit: usize,
73}
74impl Flavor for Output {
75    type Output = Vec<u8>;
76    fn try_push(&mut self, byte: u8) -> postcard::Result<()> {
77        if self.bytes.len() == self.limit {
78            return Err(postcard::Error::SerializeBufferFull);
79        }
80        self.bytes.push(byte);
81        Ok(())
82    }
83    fn try_extend(&mut self, bytes: &[u8]) -> postcard::Result<()> {
84        if bytes.len() > self.limit - self.bytes.len() {
85            return Err(postcard::Error::SerializeBufferFull);
86        }
87        self.bytes.extend_from_slice(bytes);
88        Ok(())
89    }
90    fn finalize(self) -> postcard::Result<Self::Output> {
91        Ok(self.bytes)
92    }
93}
94
95// Count with the same admitted byte ceiling as encoding, without output storage.
96struct Size {
97    bytes: usize,
98    limit: usize,
99}
100impl Flavor for Size {
101    type Output = usize;
102    fn try_push(&mut self, _: u8) -> postcard::Result<()> {
103        self.try_extend(&[0])
104    }
105    fn try_extend(&mut self, bytes: &[u8]) -> postcard::Result<()> {
106        if bytes.len() > self.limit - self.bytes {
107            return Err(postcard::Error::SerializeBufferFull);
108        }
109        self.bytes += bytes.len();
110        Ok(())
111    }
112    fn finalize(self) -> postcard::Result<usize> {
113        Ok(self.bytes)
114    }
115}
116
117fn encoded_len<T: Wire>(limits: Limits, value: &T) -> Result<usize, Error> {
118    let limit = admit::<T>(limits)?;
119    postcard::serialize_with_flavor(
120        value,
121        Size {
122            bytes: HEADER,
123            limit,
124        },
125    )
126    .map_err(|_| Error::Malformed)
127}
128
129// Compare canonical serialization directly with the admitted payload. Continue
130// after a mismatch so serializer errors still retain their Malformed mapping.
131pub(crate) struct Compare<'a> {
132    pub(crate) remaining: &'a [u8],
133    pub(crate) allowance: usize,
134    pub(crate) equal: bool,
135}
136impl Flavor for Compare<'_> {
137    type Output = bool;
138    #[inline(always)]
139    fn try_push(&mut self, byte: u8) -> postcard::Result<()> {
140        if self.allowance == 0 {
141            return Err(postcard::Error::SerializeBufferFull);
142        }
143        self.allowance -= 1;
144        if let Some((first, rest)) = self.remaining.split_first() {
145            self.equal &= *first == byte;
146            self.remaining = rest;
147        } else {
148            self.equal = false;
149        }
150        Ok(())
151    }
152    fn try_extend(&mut self, bytes: &[u8]) -> postcard::Result<()> {
153        if let [byte] = bytes {
154            return self.try_push(*byte);
155        }
156        if bytes.len() > self.allowance {
157            return Err(postcard::Error::SerializeBufferFull);
158        }
159        self.allowance -= bytes.len();
160        if let Some((prefix, rest)) = self.remaining.split_at_checked(bytes.len()) {
161            // Wasm's bytewise memcmp is expensive for bulk byte/text payloads.
162            // Exact unaligned word loads preserve canonical equality; no hash or
163            // scratch allocation is involved. Small scalar writes stay simple.
164            self.equal &= if cfg!(any(target_arch = "wasm32", test))
165                && core::ptr::eq(prefix.as_ptr(), bytes.as_ptr())
166            {
167                // Borrowed decoding can serialize the exact input region.
168                // Its length and allowance were checked above; equality needs no scan.
169                true
170            } else if cfg!(any(target_arch = "wasm32", test)) && bytes.len() >= 8 {
171                if bytes.len() >= 64 {
172                    let (left, left_tail) = prefix.as_chunks::<16>();
173                    let (right, right_tail) = bytes.as_chunks::<16>();
174                    left.iter().zip(right).fold(0_u128, |difference, (a, b)| {
175                        difference | (u128::from_ne_bytes(*a) ^ u128::from_ne_bytes(*b))
176                    }) == 0
177                        && left_tail == right_tail
178                } else {
179                    let (left, left_tail) = prefix.as_chunks::<8>();
180                    let (right, right_tail) = bytes.as_chunks::<8>();
181                    left.iter().zip(right).fold(0_u64, |difference, (a, b)| {
182                        difference | (u64::from_ne_bytes(*a) ^ u64::from_ne_bytes(*b))
183                    }) == 0
184                        && left_tail == right_tail
185                }
186            } else {
187                prefix == bytes
188            };
189            self.remaining = rest;
190        } else {
191            self.equal = false;
192            self.remaining = &[];
193        }
194        Ok(())
195    }
196    fn finalize(self) -> postcard::Result<Self::Output> {
197        Ok(self.equal && self.remaining.is_empty())
198    }
199}
200
201fn encode<T: Wire>(id: u64, direction: u8, limits: Limits, value: &T) -> Result<Vec<u8>, Error> {
202    let size = admit::<T>(limits)?;
203    let mut bytes = Vec::new();
204    bytes
205        .try_reserve_exact(size)
206        .map_err(|_| Error::Allocation)?;
207    bytes.extend_from_slice(&header(id, direction));
208    postcard::serialize_with_flavor(value, Output { bytes, limit: size })
209        .map_err(|_| Error::Malformed)
210}
211
212fn decode<T: Wire>(id: u64, direction: u8, limits: Limits, bytes: &[u8]) -> Result<T, Error> {
213    let size = admit::<T>(limits)?;
214    if bytes.len() > size || bytes.len() > limits.bytes {
215        return Err(Error::Limit);
216    }
217    let payload = payload(id, direction, bytes)?;
218    let (value, rest) = postcard::take_from_bytes::<T>(payload).map_err(|_| Error::Malformed)?;
219    if !rest.is_empty() {
220        return Err(Error::Trailing);
221    }
222    let equal = postcard::serialize_with_flavor(
223        &value,
224        Compare {
225            remaining: payload,
226            allowance: size - HEADER,
227            equal: true,
228        },
229    )
230    .map_err(|_| Error::Malformed)?;
231    if !equal {
232        return Err(Error::Noncanonical);
233    }
234    Ok(value)
235}
236
237/// Encode once, retaining exact request bytes bound to `M`.
238pub fn encode_request<M: Method>(value: &M::Request) -> Result<RequestBytes<M>, Error> {
239    Ok(RequestBytes {
240        bytes: encode(M::ID, 0, M::REQUEST_LIMITS, value)?,
241        method: PhantomData,
242    })
243}
244/// Measure the complete request envelope without allocating an output buffer.
245/// Uses the same schema admission as encoding. Custom serializers must be
246/// deterministic; sizing does not retain bytes for retries.
247pub fn encoded_request_len<M: Method>(value: &M::Request) -> Result<usize, Error> {
248    encoded_len(M::REQUEST_LIMITS, value)
249}
250/// Measure the complete response envelope without allocating an output buffer.
251/// Consumers can compare this with a delivery allowance before encoding. Schema
252/// admission still applies to the entire type, not just this value.
253pub fn encoded_response_len<M: Method>(value: &M::Response) -> Result<usize, Error> {
254    encoded_len(M::RESPONSE_LIMITS, value)
255}
256/// Decode a request only after method, direction, canonical and resource checks.
257pub fn decode_request<M: Method>(bytes: &[u8]) -> Result<M::Request, Error> {
258    decode(M::ID, 0, M::REQUEST_LIMITS, bytes)
259}
260/// Encode a reply. Application errors belong inside `M::Response`.
261pub fn encode_response<M: Method>(value: &M::Response) -> Result<ResponseBytes<M>, Error> {
262    Ok(ResponseBytes {
263        bytes: encode(M::ID, 1, M::RESPONSE_LIMITS, value)?,
264        method: PhantomData,
265    })
266}
267/// Decode a reply under the selected method's response contract.
268pub fn decode_response<M: Method>(bytes: &[u8]) -> Result<M::Response, Error> {
269    decode(M::ID, 1, M::RESPONSE_LIMITS, bytes)
270}
271
272pub(crate) fn header(id: u64, direction: u8) -> [u8; HEADER] {
273    let mut header = [0; HEADER];
274    header[..2].copy_from_slice(b"IW");
275    header[2..10].copy_from_slice(&id.to_le_bytes());
276    header[10] = direction;
277    header
278}
279pub(crate) fn payload(id: u64, direction: u8, bytes: &[u8]) -> Result<&[u8], Error> {
280    if bytes.len() < HEADER {
281        return Err(Error::Malformed);
282    }
283    if &bytes[..2] != b"IW" || bytes[2..10] != id.to_le_bytes() || bytes[10] != direction {
284        return Err(Error::Identity);
285    }
286    Ok(&bytes[HEADER..])
287}