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