Skip to main content

ic_wire/native/
mod.rs

1//! Explicit resource admission for native Serde payloads in the same Postcard format.
2//!
3//! Types, IDs, deterministic serialization and domain rules are consumer-owned.
4//! Resource admission follows Serde visits; no static schema is inferred.
5//! Custom Serde implementations are trusted: allocations/work outside visited
6//! values cannot be constrained here. See the runtime guide for budget semantics.
7#[cfg(test)]
8mod tests;
9
10mod decode;
11mod encode;
12mod parser;
13
14use crate::{Error, Limits, contract};
15use alloc::vec::Vec;
16use core::cell::Cell;
17use serde::{Deserialize, Serialize};
18
19struct Budget {
20    nodes: Cell<usize>,
21    heap_units: Cell<usize>,
22    depth: usize,
23    refused: Cell<bool>,
24}
25impl Budget {
26    fn new(limits: Limits) -> Self {
27        Self {
28            nodes: Cell::new(limits.nodes),
29            // All value charges are multiples of eight bytes. Store whole charge
30            // units instead of bytes: the unused remainder cannot fund a charge.
31            // This removes per-visit multiplication without changing admission,
32            // overflow rejection, wire bytes or consumer limits.
33            heap_units: Cell::new(limits.allocation.saturating_sub(limits.bytes) / 8),
34            depth: limits.depth,
35            refused: Cell::new(limits.allocation < limits.bytes),
36        }
37    }
38    #[cfg_attr(target_arch = "wasm32", inline(always))]
39    fn enter(&self, depth: usize, size: usize) -> bool {
40        self.enter_many(depth, size, 1)
41    }
42    #[cfg_attr(target_arch = "wasm32", inline(always))]
43    fn enter_many(&self, depth: usize, size: usize, count: usize) -> bool {
44        if self.refused.get() {
45            return false;
46        }
47        let Some(nodes) = self.nodes.get().checked_sub(count) else {
48            self.refused.set(true);
49            return false;
50        };
51        if depth > self.depth || !self.bytes(size) {
52            self.refused.set(true);
53            return false;
54        }
55        self.nodes.set(nodes);
56        true
57    }
58    #[cfg_attr(target_arch = "wasm32", inline(always))]
59    fn bytes(&self, bytes: usize) -> bool {
60        let Some(heap) = self.heap_units.get().checked_sub(bytes) else {
61            self.refused.set(true);
62            return false;
63        };
64        self.heap_units.set(heap);
65        true
66    }
67    fn error(&self) -> Error {
68        if self.refused.get() {
69            Error::Limit
70        } else {
71            Error::Malformed
72        }
73    }
74}
75
76/// Measure one native payload with no output allocation; `bytes` excludes a frame.
77pub fn encoded_len<T: Serialize + ?Sized>(value: &T, limits: Limits) -> Result<usize, Error> {
78    let budget = Budget::new(limits);
79    let size = postcard::experimental::serialized_size(&encode::Bounded {
80        value,
81        budget: &budget,
82        depth: 1,
83    })
84    .map_err(|_| budget.error())?;
85    if size > limits.bytes {
86        return Err(Error::Limit);
87    }
88    Ok(size)
89}
90
91/// Encode an owned or borrowed native payload under explicit traversal allowances.
92pub fn encode<T: Serialize + ?Sized>(value: &T, limits: Limits) -> Result<Vec<u8>, Error> {
93    encode_prefixed(value, limits, &[])
94}
95fn encode_prefixed<T: Serialize + ?Sized>(
96    value: &T,
97    limits: Limits,
98    prefix: &[u8],
99) -> Result<Vec<u8>, Error> {
100    let mut bytes = Vec::new();
101    encode_prefixed_into(value, limits, prefix, &mut bytes)?;
102    Ok(bytes)
103}
104
105/// Encode into reusable caller storage. Previous contents are cleared, including on error.
106/// Capacity may be lost on serialization failure; successful calls retain it.
107/// Retain a separate immutable frame when bytes must survive subsequent calls for retries.
108pub fn encode_into<T: Serialize + ?Sized>(
109    value: &T,
110    limits: Limits,
111    bytes: &mut Vec<u8>,
112) -> Result<(), Error> {
113    encode_prefixed_into(value, limits, &[], bytes)
114}
115fn encode_prefixed_into<T: Serialize + ?Sized>(
116    value: &T,
117    limits: Limits,
118    prefix: &[u8],
119    bytes: &mut Vec<u8>,
120) -> Result<(), Error> {
121    bytes.clear();
122    let size = encoded_len(value, payload_limits(limits, prefix.len())?)?;
123    bytes
124        .try_reserve_exact(size + prefix.len())
125        .map_err(|_| Error::Allocation)?;
126    bytes.extend_from_slice(prefix);
127    let budget = Budget::new(limits);
128    *bytes = postcard::serialize_with_flavor(
129        &encode::Bounded {
130            value,
131            budget: &budget,
132            depth: 1,
133        },
134        contract::Output {
135            bytes: core::mem::take(bytes),
136            limit: size + prefix.len(),
137        },
138    )
139    .map_err(|_| budget.error())?;
140    Ok(())
141}
142
143/// Encode a request into reusable storage; follows [`encode_into`] failure semantics.
144pub fn encode_request_into<T: Serialize + ?Sized>(
145    id: u64,
146    value: &T,
147    limits: Limits,
148    bytes: &mut Vec<u8>,
149) -> Result<(), Error> {
150    encode_prefixed_into(value, limits, &contract::header(id, 0), bytes)
151}
152
153/// Encode a response into reusable storage; follows [`encode_into`] failure semantics.
154pub fn encode_response_into<T: Serialize + ?Sized>(
155    id: u64,
156    value: &T,
157    limits: Limits,
158    bytes: &mut Vec<u8>,
159) -> Result<(), Error> {
160    encode_prefixed_into(value, limits, &contract::header(id, 1), bytes)
161}
162
163/// Decode a complete Postcard payload; allocation admission precedes visitor calls.
164/// Borrowed bytes and strings may refer directly to the input for its lifetime.
165/// Primitive representations are checked while reading; Serde normalization is consumer-owned.
166pub fn decode<'de, T: Deserialize<'de>>(bytes: &'de [u8], limits: Limits) -> Result<T, Error> {
167    if bytes.len() > limits.bytes {
168        return Err(Error::Limit);
169    }
170    let budget = Budget::new(limits);
171    let mut input = parser::Deserializer::from_bytes(bytes);
172    let value = T::deserialize(decode::Bounded {
173        inner: &mut input,
174        budget: &budget,
175        depth: 1,
176    })
177    .map_err(|_| budget.error())?;
178    if !input.finalize().map_err(|_| Error::Malformed)?.is_empty() {
179        return Err(Error::Trailing);
180    }
181    // Hard cut: decoding no longer reserializes or charges a serialization pass.
182    // Primitive parsing stays strict, but maps/sets and custom Serde may normalize:
183    // e.g. map bytes 02 02 14 01 0a now decode and re-encode as 02 01 0a 02 14.
184    // Consumers own normalization and must retain original bytes for exact retries.
185    Ok(value)
186}
187
188/// Encode a complete request; `limits.bytes` includes the canonical frame header.
189pub fn encode_request<T: Serialize + ?Sized>(
190    id: u64,
191    value: &T,
192    limits: Limits,
193) -> Result<Vec<u8>, Error> {
194    encode_prefixed(value, limits, &contract::header(id, 0))
195}
196/// Encode a complete response, including borrowed response projections.
197pub fn encode_response<T: Serialize + ?Sized>(
198    id: u64,
199    value: &T,
200    limits: Limits,
201) -> Result<Vec<u8>, Error> {
202    encode_prefixed(value, limits, &contract::header(id, 1))
203}
204/// Measure a request or response frame; both directions have equal header length.
205pub fn encoded_frame_len<T: Serialize + ?Sized>(value: &T, limits: Limits) -> Result<usize, Error> {
206    encoded_len(value, payload_limits(limits, contract::HEADER)?)
207        .map(|size| size + contract::HEADER)
208}
209/// Check request identity and direction before decoding its bounded native value.
210pub fn decode_request<'de, T: Deserialize<'de>>(
211    id: u64,
212    bytes: &'de [u8],
213    limits: Limits,
214) -> Result<T, Error> {
215    decode_frame(id, 0, bytes, limits)
216}
217/// Check response identity and direction before decoding its bounded native value.
218pub fn decode_response<'de, T: Deserialize<'de>>(
219    id: u64,
220    bytes: &'de [u8],
221    limits: Limits,
222) -> Result<T, Error> {
223    decode_frame(id, 1, bytes, limits)
224}
225fn decode_frame<'de, T: Deserialize<'de>>(
226    id: u64,
227    direction: u8,
228    bytes: &'de [u8],
229    limits: Limits,
230) -> Result<T, Error> {
231    if bytes.len() > limits.bytes {
232        return Err(Error::Limit);
233    }
234    let payload = contract::payload(id, direction, bytes)?;
235    decode(payload, payload_limits(limits, contract::HEADER)?)
236}
237
238fn payload_limits(limits: Limits, prefix: usize) -> Result<Limits, Error> {
239    Ok(Limits {
240        bytes: limits.bytes.checked_sub(prefix).ok_or(Error::Limit)?,
241        allocation: limits.allocation.checked_sub(prefix).ok_or(Error::Limit)?,
242        ..limits
243    })
244}