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}
115
116/// Encode a payload into caller-owned storage without allocating codec output.
117///
118/// Returns only the completed encoding. A short buffer returns [`Error::Limit`]
119/// before writing; on any other error storage contents are unspecified and must
120/// not be published. The returned slice borrows storage, not the value. Admission,
121/// two-pass sizing and trusted custom Serde rules match [`encode_into`]; storage
122/// capacity does not replace or reduce the configured allocation allowance.
123pub fn encode_to_slice<'a, T: Serialize + ?Sized>(
124    value: &T,
125    limits: Limits,
126    storage: &'a mut [u8],
127) -> Result<&'a mut [u8], Error> {
128    encode_prefixed_to_slice(value, limits, &[], storage)
129}
130
131/// Encode a payload using scratch storage or owned fallback, then consume its bytes.
132///
133/// Preflight selects a destination once: scratch when the admitted payload fits,
134/// otherwise an owned buffer. Both paths retain [`encode`]'s two-pass admission
135/// and allocation allowance; custom serializers may still allocate independently.
136/// The owned `value` is dropped before the synchronous callback. Passing a
137/// reference instead keeps ownership with the caller.
138///
139/// Encoding failure never calls `consume`; scratch contents on error are
140/// unspecified. Only successful bytes may be published. Copy bytes that must
141/// survive the callback, including saved retries. No framing is added.
142///
143/// ```
144/// let limits = ic_wire::Limits { bytes: 128, allocation: 2048, nodes: 64, depth: 8 };
145/// let mut scratch = [0; 128];
146/// let length = ic_wire::native::with_encoded(7_u64, limits, &mut scratch, |bytes| {
147///     assert_eq!(bytes, [7]);
148///     bytes.len()
149/// })?;
150/// assert_eq!(length, 1);
151/// # Ok::<(), ic_wire::Error>(())
152/// ```
153/// Callback bytes cannot escape as a borrow or inside a future:
154/// ```compile_fail
155/// let limits = ic_wire::Limits { bytes: 128, allocation: 2048, nodes: 64, depth: 8 };
156/// let mut scratch = [0; 128];
157/// ic_wire::native::with_encoded(7_u64, limits, &mut scratch, |bytes| bytes);
158/// ```
159/// ```compile_fail
160/// let limits = ic_wire::Limits { bytes: 128, allocation: 2048, nodes: 64, depth: 8 };
161/// let mut scratch = [0; 128];
162/// ic_wire::native::with_encoded(7_u64, limits, &mut scratch, |bytes| async move {
163///     bytes.len()
164/// });
165/// ```
166pub fn with_encoded<T: Serialize, R>(
167    value: T,
168    limits: Limits,
169    scratch: &mut [u8],
170    consume: impl FnOnce(&[u8]) -> R,
171) -> Result<R, Error> {
172    let size = encoded_len(&value, limits)?;
173    let budget = Budget::new(limits);
174    let bounded = encode::Bounded {
175        value: &value,
176        budget: &budget,
177        depth: 1,
178    };
179    // New destination selection only: both writers are capped at the preflight
180    // size. Postcard bytes and admission match encode; e.g. u64 7 remains 07.
181    // Existing endpoints/retries need no migration or alternate codec.
182    if let Some(storage) = scratch.get_mut(..size) {
183        let bytes = postcard::to_slice(&bounded, storage).map_err(|_| budget.error())?;
184        drop(value);
185        Ok(consume(bytes))
186    } else {
187        let mut bytes = Vec::new();
188        bytes
189            .try_reserve_exact(size)
190            .map_err(|_| Error::Allocation)?;
191        let bytes =
192            postcard::serialize_with_flavor(&bounded, contract::Output { bytes, limit: size })
193                .map_err(|_| budget.error())?;
194        drop(value);
195        Ok(consume(&bytes))
196    }
197}
198
199/// Encode a complete request into caller storage; follows [`encode_to_slice`].
200pub fn encode_request_to_slice<'a, T: Serialize + ?Sized>(
201    id: u64,
202    value: &T,
203    limits: Limits,
204    storage: &'a mut [u8],
205) -> Result<&'a mut [u8], Error> {
206    encode_prefixed_to_slice(value, limits, &contract::header(id, 0), storage)
207}
208
209/// Encode a complete response into caller storage; follows [`encode_to_slice`].
210pub fn encode_response_to_slice<'a, T: Serialize + ?Sized>(
211    id: u64,
212    value: &T,
213    limits: Limits,
214    storage: &'a mut [u8],
215) -> Result<&'a mut [u8], Error> {
216    encode_prefixed_to_slice(value, limits, &contract::header(id, 1), storage)
217}
218
219fn encode_prefixed_to_slice<'a, T: Serialize + ?Sized>(
220    value: &T,
221    limits: Limits,
222    prefix: &[u8],
223    storage: &'a mut [u8],
224) -> Result<&'a mut [u8], Error> {
225    let size = encoded_len(value, payload_limits(limits, prefix.len())?)?;
226    let output = storage.get_mut(..size + prefix.len()).ok_or(Error::Limit)?;
227    output[..prefix.len()].copy_from_slice(prefix);
228    let budget = Budget::new(limits);
229    // New storage API, same Postcard bytes and admission as Vec output. The
230    // slice flavor bounds every write, including inconsistent custom Serialize
231    // passes. Only Ok publishes a completed prefix; no inline/spill policy or
232    // framing change is introduced, and existing consumers need no migration.
233    let payload = postcard::to_slice(
234        &encode::Bounded {
235            value,
236            budget: &budget,
237            depth: 1,
238        },
239        &mut output[prefix.len()..],
240    )
241    .map_err(|_| budget.error())?;
242    let written = prefix.len() + payload.len();
243    Ok(&mut output[..written])
244}
245
246fn encode_prefixed_into<T: Serialize + ?Sized>(
247    value: &T,
248    limits: Limits,
249    prefix: &[u8],
250    bytes: &mut Vec<u8>,
251) -> Result<(), Error> {
252    bytes.clear();
253    let size = encoded_len(value, payload_limits(limits, prefix.len())?)?;
254    bytes
255        .try_reserve_exact(size + prefix.len())
256        .map_err(|_| Error::Allocation)?;
257    bytes.extend_from_slice(prefix);
258    let budget = Budget::new(limits);
259    *bytes = postcard::serialize_with_flavor(
260        &encode::Bounded {
261            value,
262            budget: &budget,
263            depth: 1,
264        },
265        contract::Output {
266            bytes: core::mem::take(bytes),
267            limit: size + prefix.len(),
268        },
269    )
270    .map_err(|_| budget.error())?;
271    Ok(())
272}
273
274/// Encode a request into reusable storage; follows [`encode_into`] failure semantics.
275pub fn encode_request_into<T: Serialize + ?Sized>(
276    id: u64,
277    value: &T,
278    limits: Limits,
279    bytes: &mut Vec<u8>,
280) -> Result<(), Error> {
281    encode_prefixed_into(value, limits, &contract::header(id, 0), bytes)
282}
283
284/// Encode a response into reusable storage; follows [`encode_into`] failure semantics.
285pub fn encode_response_into<T: Serialize + ?Sized>(
286    id: u64,
287    value: &T,
288    limits: Limits,
289    bytes: &mut Vec<u8>,
290) -> Result<(), Error> {
291    encode_prefixed_into(value, limits, &contract::header(id, 1), bytes)
292}
293
294/// Decode a complete Postcard payload; allocation admission precedes visitor calls.
295/// Borrowed bytes and strings may refer directly to the input for its lifetime.
296/// Primitive representations are checked while reading; Serde normalization is consumer-owned.
297pub fn decode<'de, T: Deserialize<'de>>(bytes: &'de [u8], limits: Limits) -> Result<T, Error> {
298    if bytes.len() > limits.bytes {
299        return Err(Error::Limit);
300    }
301    let budget = Budget::new(limits);
302    let mut input = parser::Deserializer::from_bytes(bytes);
303    let value = T::deserialize(decode::Bounded {
304        inner: &mut input,
305        budget: &budget,
306        depth: 1,
307    })
308    .map_err(|_| budget.error())?;
309    if !input.finalize().map_err(|_| Error::Malformed)?.is_empty() {
310        return Err(Error::Trailing);
311    }
312    // Hard cut: decoding no longer reserializes or charges a serialization pass.
313    // Primitive parsing stays strict, but maps/sets and custom Serde may normalize:
314    // e.g. map bytes 02 02 14 01 0a now decode and re-encode as 02 01 0a 02 14.
315    // Consumers own normalization and must retain original bytes for exact retries.
316    Ok(value)
317}
318
319/// Encode a complete request; `limits.bytes` includes the canonical frame header.
320pub fn encode_request<T: Serialize + ?Sized>(
321    id: u64,
322    value: &T,
323    limits: Limits,
324) -> Result<Vec<u8>, Error> {
325    encode_prefixed(value, limits, &contract::header(id, 0))
326}
327/// Encode a complete response, including borrowed response projections.
328pub fn encode_response<T: Serialize + ?Sized>(
329    id: u64,
330    value: &T,
331    limits: Limits,
332) -> Result<Vec<u8>, Error> {
333    encode_prefixed(value, limits, &contract::header(id, 1))
334}
335/// Measure a request or response frame; both directions have equal header length.
336pub fn encoded_frame_len<T: Serialize + ?Sized>(value: &T, limits: Limits) -> Result<usize, Error> {
337    encoded_len(value, payload_limits(limits, contract::HEADER)?)
338        .map(|size| size + contract::HEADER)
339}
340/// Check request identity and direction before decoding its bounded native value.
341pub fn decode_request<'de, T: Deserialize<'de>>(
342    id: u64,
343    bytes: &'de [u8],
344    limits: Limits,
345) -> Result<T, Error> {
346    decode_frame(id, 0, bytes, limits)
347}
348/// Check response identity and direction before decoding its bounded native value.
349pub fn decode_response<'de, T: Deserialize<'de>>(
350    id: u64,
351    bytes: &'de [u8],
352    limits: Limits,
353) -> Result<T, Error> {
354    decode_frame(id, 1, bytes, limits)
355}
356fn decode_frame<'de, T: Deserialize<'de>>(
357    id: u64,
358    direction: u8,
359    bytes: &'de [u8],
360    limits: Limits,
361) -> Result<T, Error> {
362    if bytes.len() > limits.bytes {
363        return Err(Error::Limit);
364    }
365    let payload = contract::payload(id, direction, bytes)?;
366    decode(payload, payload_limits(limits, contract::HEADER)?)
367}
368
369fn payload_limits(limits: Limits, prefix: usize) -> Result<Limits, Error> {
370    Ok(Limits {
371        bytes: limits.bytes.checked_sub(prefix).ok_or(Error::Limit)?,
372        allocation: limits.allocation.checked_sub(prefix).ok_or(Error::Limit)?,
373        ..limits
374    })
375}