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
6pub(crate) const 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.
67pub(crate) struct Output {
68    pub(crate) bytes: Vec<u8>,
69    pub(crate) 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.
128pub(crate) struct Compare<'a> {
129    pub(crate) remaining: &'a [u8],
130    pub(crate) allowance: usize,
131    pub(crate) 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(&header(id, direction));
164    postcard::serialize_with_flavor(value, Output { bytes, limit: size })
165        .map_err(|_| Error::Malformed)
166}
167
168fn decode<T: Wire>(id: u64, direction: u8, limits: Limits, bytes: &[u8]) -> Result<T, Error> {
169    let size = admit::<T>(limits)?;
170    if bytes.len() > size || bytes.len() > limits.bytes {
171        return Err(Error::Limit);
172    }
173    let payload = payload(id, direction, bytes)?;
174    let (value, rest) = postcard::take_from_bytes::<T>(payload).map_err(|_| Error::Malformed)?;
175    if !rest.is_empty() {
176        return Err(Error::Trailing);
177    }
178    let equal = postcard::serialize_with_flavor(
179        &value,
180        Compare {
181            remaining: payload,
182            allowance: size - HEADER,
183            equal: true,
184        },
185    )
186    .map_err(|_| Error::Malformed)?;
187    if !equal {
188        return Err(Error::Noncanonical);
189    }
190    Ok(value)
191}
192
193/// Encode once, retaining exact request bytes bound to `M`.
194pub fn encode_request<M: Method>(value: &M::Request) -> Result<RequestBytes<M>, Error> {
195    Ok(RequestBytes {
196        bytes: encode(M::ID, 0, M::REQUEST_LIMITS, value)?,
197        method: PhantomData,
198    })
199}
200/// Measure the complete request envelope without allocating an output buffer.
201/// Uses the same schema admission as encoding. Custom serializers must be
202/// deterministic; sizing does not retain bytes for retries.
203pub fn encoded_request_len<M: Method>(value: &M::Request) -> Result<usize, Error> {
204    encoded_len(M::REQUEST_LIMITS, value)
205}
206/// Measure the complete response envelope without allocating an output buffer.
207/// Consumers can compare this with a delivery allowance before encoding. Schema
208/// admission still applies to the entire type, not just this value.
209pub fn encoded_response_len<M: Method>(value: &M::Response) -> Result<usize, Error> {
210    encoded_len(M::RESPONSE_LIMITS, value)
211}
212/// Decode a request only after method, direction, canonical and resource checks.
213pub fn decode_request<M: Method>(bytes: &[u8]) -> Result<M::Request, Error> {
214    decode(M::ID, 0, M::REQUEST_LIMITS, bytes)
215}
216/// Encode a reply. Application errors belong inside `M::Response`.
217pub fn encode_response<M: Method>(value: &M::Response) -> Result<ResponseBytes<M>, Error> {
218    Ok(ResponseBytes {
219        bytes: encode(M::ID, 1, M::RESPONSE_LIMITS, value)?,
220        method: PhantomData,
221    })
222}
223/// Decode a reply under the selected method's response contract.
224pub fn decode_response<M: Method>(bytes: &[u8]) -> Result<M::Response, Error> {
225    decode(M::ID, 1, M::RESPONSE_LIMITS, bytes)
226}
227
228pub(crate) fn header(id: u64, direction: u8) -> [u8; HEADER] {
229    let mut header = [0; HEADER];
230    header[..2].copy_from_slice(b"IW");
231    header[2..10].copy_from_slice(&id.to_le_bytes());
232    header[10] = direction;
233    header
234}
235pub(crate) fn payload(id: u64, direction: u8, bytes: &[u8]) -> Result<&[u8], Error> {
236    if bytes.len() < HEADER {
237        return Err(Error::Malformed);
238    }
239    if &bytes[..2] != b"IW" || bytes[2..10] != id.to_le_bytes() || bytes[10] != direction {
240        return Err(Error::Identity);
241    }
242    Ok(&bytes[HEADER..])
243}