Skip to main content

ic_wire/contract/
mod.rs

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