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
19/// Stable automatic endpoint identity from the exact exported IC method name.
20///
21/// FNV-1a-64 over UTF-8 `ic-wire:method:` followed by the name, using offset basis
22/// 14695981039346656037, prime 1099511628211 and wrapping u64 arithmetic. This is
23/// a routing identity, not authentication or a cryptographic schema fingerprint.
24/// Consumers retain duplicate-ID admission; 64-bit hashes can collide.
25///
26/// The endpoint macro evaluates this at compile time. Rust module paths, type
27/// spelling and execution mode do not participate. Renaming the exported method
28/// changes its ID; `name = "stable-name"` decouples it from a Rust function rename.
29/// Explicit IDs remain valid. Replacing one with this default requires coordinated
30/// clients unless the values match. The frame format itself remains unchanged.
31pub const fn method_id(name: &str) -> u64 {
32    let mut hash = 14_695_981_039_346_656_037u64;
33    let domain = b"ic-wire:method:";
34    let mut index = 0;
35    while index < domain.len() {
36        hash = (hash ^ domain[index] as u64).wrapping_mul(1_099_511_628_211);
37        index += 1;
38    }
39    let bytes = name.as_bytes();
40    index = 0;
41    while index < bytes.len() {
42        hash = (hash ^ bytes[index] as u64).wrapping_mul(1_099_511_628_211);
43        index += 1;
44    }
45    hash
46}
47
48struct Budget {
49    nodes: Cell<usize>,
50    heap_units: Cell<usize>,
51    depth: usize,
52    refused: Cell<bool>,
53}
54impl Budget {
55    fn new(limits: Limits) -> Self {
56        Self {
57            nodes: Cell::new(limits.nodes),
58            // All value charges are multiples of eight bytes. Store whole charge
59            // units instead of bytes: the unused remainder cannot fund a charge.
60            // This removes per-visit multiplication without changing admission,
61            // overflow rejection, wire bytes or consumer limits.
62            heap_units: Cell::new(limits.allocation.saturating_sub(limits.bytes) / 8),
63            depth: limits.depth,
64            refused: Cell::new(limits.allocation < limits.bytes),
65        }
66    }
67    #[cfg_attr(target_arch = "wasm32", inline(always))]
68    fn enter(&self, depth: usize, size: usize) -> bool {
69        self.enter_many(depth, size, 1)
70    }
71    #[cfg_attr(target_arch = "wasm32", inline(always))]
72    fn enter_many(&self, depth: usize, size: usize, count: usize) -> bool {
73        if self.refused.get() {
74            return false;
75        }
76        let Some(nodes) = self.nodes.get().checked_sub(count) else {
77            self.refused.set(true);
78            return false;
79        };
80        if depth > self.depth || !self.bytes(size) {
81            self.refused.set(true);
82            return false;
83        }
84        self.nodes.set(nodes);
85        true
86    }
87    #[cfg_attr(target_arch = "wasm32", inline(always))]
88    fn bytes(&self, bytes: usize) -> bool {
89        let Some(heap) = self.heap_units.get().checked_sub(bytes) else {
90            self.refused.set(true);
91            return false;
92        };
93        self.heap_units.set(heap);
94        true
95    }
96    fn error(&self) -> Error {
97        if self.refused.get() {
98            Error::Limit
99        } else {
100            Error::Malformed
101        }
102    }
103}
104
105/// Measure one native payload with no output allocation; `bytes` excludes a frame.
106pub fn encoded_len<T: Serialize + ?Sized>(value: &T, limits: Limits) -> Result<usize, Error> {
107    let budget = Budget::new(limits);
108    let size = postcard::experimental::serialized_size(&encode::Bounded {
109        value,
110        budget: &budget,
111        depth: 1,
112    })
113    .map_err(|_| budget.error())?;
114    if size > limits.bytes {
115        return Err(Error::Limit);
116    }
117    Ok(size)
118}
119
120/// Encode an owned or borrowed native payload under explicit traversal allowances.
121pub fn encode<T: Serialize + ?Sized>(value: &T, limits: Limits) -> Result<Vec<u8>, Error> {
122    encode_prefixed(value, limits, &[])
123}
124fn encode_prefixed<T: Serialize + ?Sized>(
125    value: &T,
126    limits: Limits,
127    prefix: &[u8],
128) -> Result<Vec<u8>, Error> {
129    let mut bytes = Vec::new();
130    encode_prefixed_into(value, limits, prefix, &mut bytes)?;
131    Ok(bytes)
132}
133
134/// Encode into reusable caller storage. Previous contents are cleared, including on error.
135/// Capacity may be lost on serialization failure; successful calls retain it.
136/// Retain a separate immutable frame when bytes must survive subsequent calls for retries.
137pub fn encode_into<T: Serialize + ?Sized>(
138    value: &T,
139    limits: Limits,
140    bytes: &mut Vec<u8>,
141) -> Result<(), Error> {
142    encode_prefixed_into(value, limits, &[], bytes)
143}
144
145/// Encode a payload into caller-owned storage without allocating codec output.
146///
147/// Returns only the completed encoding. A short buffer returns [`Error::Limit`]
148/// before writing; on any other error storage contents are unspecified and must
149/// not be published. The returned slice borrows storage, not the value. Admission,
150/// two-pass sizing and trusted custom Serde rules match [`encode_into`]; storage
151/// capacity does not replace or reduce the configured allocation allowance.
152pub fn encode_to_slice<'a, T: Serialize + ?Sized>(
153    value: &T,
154    limits: Limits,
155    storage: &'a mut [u8],
156) -> Result<&'a mut [u8], Error> {
157    encode_prefixed_to_slice(value, limits, &[], storage)
158}
159
160/// Encode a payload using scratch storage or owned fallback, then consume its bytes.
161///
162/// Preflight selects a destination once: scratch when the admitted payload fits,
163/// otherwise an owned buffer. Both paths retain [`encode`]'s two-pass admission
164/// and allocation allowance; custom serializers may still allocate independently.
165/// The owned `value` is dropped before the synchronous callback. Passing a
166/// reference instead keeps ownership with the caller.
167///
168/// Encoding failure never calls `consume`; scratch contents on error are
169/// unspecified. Only successful bytes may be published. Copy bytes that must
170/// survive the callback, including saved retries. No framing is added.
171///
172/// ```
173/// let limits = ic_wire::Limits { bytes: 128, allocation: 2048, nodes: 64, depth: 8 };
174/// let mut scratch = [0; 128];
175/// let length = ic_wire::native::with_encoded(7_u64, limits, &mut scratch, |bytes| {
176///     assert_eq!(bytes, [7]);
177///     bytes.len()
178/// })?;
179/// assert_eq!(length, 1);
180/// # Ok::<(), ic_wire::Error>(())
181/// ```
182/// Callback bytes cannot escape as a borrow or inside a future:
183/// ```compile_fail
184/// let limits = ic_wire::Limits { bytes: 128, allocation: 2048, nodes: 64, depth: 8 };
185/// let mut scratch = [0; 128];
186/// ic_wire::native::with_encoded(7_u64, limits, &mut scratch, |bytes| bytes);
187/// ```
188/// ```compile_fail
189/// let limits = ic_wire::Limits { bytes: 128, allocation: 2048, nodes: 64, depth: 8 };
190/// let mut scratch = [0; 128];
191/// ic_wire::native::with_encoded(7_u64, limits, &mut scratch, |bytes| async move {
192///     bytes.len()
193/// });
194/// ```
195pub fn with_encoded<T: Serialize, R>(
196    value: T,
197    limits: Limits,
198    scratch: &mut [u8],
199    consume: impl FnOnce(&[u8]) -> R,
200) -> Result<R, Error> {
201    let size = encoded_len(&value, limits)?;
202    let budget = Budget::new(limits);
203    let bounded = encode::Bounded {
204        value: &value,
205        budget: &budget,
206        depth: 1,
207    };
208    // New destination selection only: both writers are capped at the preflight
209    // size. Postcard bytes and admission match encode; e.g. u64 7 remains 07.
210    // Existing endpoints/retries need no migration or alternate codec.
211    if let Some(storage) = scratch.get_mut(..size) {
212        let bytes = postcard::to_slice(&bounded, storage).map_err(|_| budget.error())?;
213        drop(value);
214        Ok(consume(bytes))
215    } else {
216        let mut bytes = Vec::new();
217        bytes
218            .try_reserve_exact(size)
219            .map_err(|_| Error::Allocation)?;
220        let bytes =
221            postcard::serialize_with_flavor(&bounded, contract::Output { bytes, limit: size })
222                .map_err(|_| budget.error())?;
223        drop(value);
224        Ok(consume(&bytes))
225    }
226}
227
228/// Encode a complete request into caller storage; follows [`encode_to_slice`].
229pub fn encode_request_to_slice<'a, T: Serialize + ?Sized>(
230    id: u64,
231    value: &T,
232    limits: Limits,
233    storage: &'a mut [u8],
234) -> Result<&'a mut [u8], Error> {
235    encode_prefixed_to_slice(value, limits, &contract::header(id, 0), storage)
236}
237
238/// Encode a complete response into caller storage; follows [`encode_to_slice`].
239pub fn encode_response_to_slice<'a, T: Serialize + ?Sized>(
240    id: u64,
241    value: &T,
242    limits: Limits,
243    storage: &'a mut [u8],
244) -> Result<&'a mut [u8], Error> {
245    encode_prefixed_to_slice(value, limits, &contract::header(id, 1), storage)
246}
247
248fn encode_prefixed_to_slice<'a, T: Serialize + ?Sized>(
249    value: &T,
250    limits: Limits,
251    prefix: &[u8],
252    storage: &'a mut [u8],
253) -> Result<&'a mut [u8], Error> {
254    let size = encoded_len(value, payload_limits(limits, prefix.len())?)?;
255    let output = storage.get_mut(..size + prefix.len()).ok_or(Error::Limit)?;
256    output[..prefix.len()].copy_from_slice(prefix);
257    let budget = Budget::new(limits);
258    // New storage API, same Postcard bytes and admission as Vec output. The
259    // slice flavor bounds every write, including inconsistent custom Serialize
260    // passes. Only Ok publishes a completed prefix; no inline/spill policy or
261    // framing change is introduced, and existing consumers need no migration.
262    let payload = postcard::to_slice(
263        &encode::Bounded {
264            value,
265            budget: &budget,
266            depth: 1,
267        },
268        &mut output[prefix.len()..],
269    )
270    .map_err(|_| budget.error())?;
271    let written = prefix.len() + payload.len();
272    Ok(&mut output[..written])
273}
274
275fn encode_prefixed_into<T: Serialize + ?Sized>(
276    value: &T,
277    limits: Limits,
278    prefix: &[u8],
279    bytes: &mut Vec<u8>,
280) -> Result<(), Error> {
281    bytes.clear();
282    let size = encoded_len(value, payload_limits(limits, prefix.len())?)?;
283    bytes
284        .try_reserve_exact(size + prefix.len())
285        .map_err(|_| Error::Allocation)?;
286    bytes.extend_from_slice(prefix);
287    let budget = Budget::new(limits);
288    *bytes = postcard::serialize_with_flavor(
289        &encode::Bounded {
290            value,
291            budget: &budget,
292            depth: 1,
293        },
294        contract::Output {
295            bytes: core::mem::take(bytes),
296            limit: size + prefix.len(),
297        },
298    )
299    .map_err(|_| budget.error())?;
300    Ok(())
301}
302
303/// Encode a request into reusable storage; follows [`encode_into`] failure semantics.
304pub fn encode_request_into<T: Serialize + ?Sized>(
305    id: u64,
306    value: &T,
307    limits: Limits,
308    bytes: &mut Vec<u8>,
309) -> Result<(), Error> {
310    encode_prefixed_into(value, limits, &contract::header(id, 0), bytes)
311}
312
313/// Encode a response into reusable storage; follows [`encode_into`] failure semantics.
314pub fn encode_response_into<T: Serialize + ?Sized>(
315    id: u64,
316    value: &T,
317    limits: Limits,
318    bytes: &mut Vec<u8>,
319) -> Result<(), Error> {
320    encode_prefixed_into(value, limits, &contract::header(id, 1), bytes)
321}
322
323/// Decode a complete Postcard payload; allocation admission precedes visitor calls.
324/// Borrowed bytes and strings may refer directly to the input for its lifetime.
325/// Primitive representations are checked while reading; Serde normalization is consumer-owned.
326pub fn decode<'de, T: Deserialize<'de>>(bytes: &'de [u8], limits: Limits) -> Result<T, Error> {
327    if bytes.len() > limits.bytes {
328        return Err(Error::Limit);
329    }
330    let budget = Budget::new(limits);
331    let mut input = parser::Deserializer::from_bytes(bytes);
332    let value = T::deserialize(decode::Bounded {
333        inner: &mut input,
334        budget: &budget,
335        depth: 1,
336    })
337    .map_err(|_| budget.error())?;
338    if !input.finalize().map_err(|_| Error::Malformed)?.is_empty() {
339        return Err(Error::Trailing);
340    }
341    // Hard cut: decoding no longer reserializes or charges a serialization pass.
342    // Primitive parsing stays strict, but maps/sets and custom Serde may normalize:
343    // e.g. map bytes 02 02 14 01 0a now decode and re-encode as 02 01 0a 02 14.
344    // Consumers own normalization and must retain original bytes for exact retries.
345    Ok(value)
346}
347
348/// Encode a complete request; `limits.bytes` includes the canonical frame header.
349pub fn encode_request<T: Serialize + ?Sized>(
350    id: u64,
351    value: &T,
352    limits: Limits,
353) -> Result<Vec<u8>, Error> {
354    encode_prefixed(value, limits, &contract::header(id, 0))
355}
356/// Encode a complete response, including borrowed response projections.
357pub fn encode_response<T: Serialize + ?Sized>(
358    id: u64,
359    value: &T,
360    limits: Limits,
361) -> Result<Vec<u8>, Error> {
362    encode_prefixed(value, limits, &contract::header(id, 1))
363}
364/// Measure a request or response frame; both directions have equal header length.
365pub fn encoded_frame_len<T: Serialize + ?Sized>(value: &T, limits: Limits) -> Result<usize, Error> {
366    encoded_len(value, payload_limits(limits, contract::HEADER)?)
367        .map(|size| size + contract::HEADER)
368}
369/// Check request identity and direction before decoding its bounded native value.
370pub fn decode_request<'de, T: Deserialize<'de>>(
371    id: u64,
372    bytes: &'de [u8],
373    limits: Limits,
374) -> Result<T, Error> {
375    decode_frame(id, 0, bytes, limits)
376}
377/// Check response identity and direction before decoding its bounded native value.
378pub fn decode_response<'de, T: Deserialize<'de>>(
379    id: u64,
380    bytes: &'de [u8],
381    limits: Limits,
382) -> Result<T, Error> {
383    decode_frame(id, 1, bytes, limits)
384}
385fn decode_frame<'de, T: Deserialize<'de>>(
386    id: u64,
387    direction: u8,
388    bytes: &'de [u8],
389    limits: Limits,
390) -> Result<T, Error> {
391    if bytes.len() > limits.bytes {
392        return Err(Error::Limit);
393    }
394    let payload = contract::payload(id, direction, bytes)?;
395    decode(payload, payload_limits(limits, contract::HEADER)?)
396}
397
398fn payload_limits(limits: Limits, prefix: usize) -> Result<Limits, Error> {
399    Ok(Limits {
400        bytes: limits.bytes.checked_sub(prefix).ok_or(Error::Limit)?,
401        allocation: limits.allocation.checked_sub(prefix).ok_or(Error::Limit)?,
402        ..limits
403    })
404}