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, units: usize) -> bool {
69        self.enter_many(depth, units, 1)
70    }
71    #[cfg_attr(target_arch = "wasm32", inline(always))]
72    // Previously nested checked subtraction committed the heap through bytes().
73    // Check both original allowances, then commit the same heap/node debits.
74    // Sticky refusal, depth/overflow rejection, bytes and acceptance are unchanged;
75    // consumers need no migration and retained retry/hash inputs remain valid.
76    fn enter_many(&self, depth: usize, units: usize, count: usize) -> bool {
77        if self.refused.get() {
78            return false;
79        }
80        let nodes = self.nodes.get();
81        let heap = self.heap_units.get();
82        if nodes < count || depth > self.depth || heap < units {
83            self.refused.set(true);
84            return false;
85        }
86        self.heap_units.set(heap - units);
87        self.nodes.set(nodes - count);
88        true
89    }
90    #[cfg_attr(target_arch = "wasm32", inline(always))]
91    // One unit is eight allowance bytes. Buffer length supplies the conservative
92    // charge in units, not an exact heap-byte measurement. Preserve that policy.
93    fn charge_buffer(&self, units: usize) -> bool {
94        let Some(heap) = self.heap_units.get().checked_sub(units) else {
95            self.refused.set(true);
96            return false;
97        };
98        self.heap_units.set(heap);
99        true
100    }
101    fn error(&self) -> Error {
102        if self.refused.get() {
103            Error::Limit
104        } else {
105            Error::Malformed
106        }
107    }
108}
109
110/// Measure one native payload with no output allocation; `bytes` excludes a frame.
111pub fn encoded_len<T: Serialize + ?Sized>(value: &T, limits: Limits) -> Result<usize, Error> {
112    let budget = Budget::new(limits);
113    let size = postcard::experimental::serialized_size(&encode::Bounded {
114        value,
115        budget: &budget,
116        depth: 1,
117    })
118    .map_err(|_| budget.error())?;
119    if size > limits.bytes {
120        return Err(Error::Limit);
121    }
122    Ok(size)
123}
124
125/// Encode an owned or borrowed native payload under explicit traversal allowances.
126pub fn encode<T: Serialize + ?Sized>(value: &T, limits: Limits) -> Result<Vec<u8>, Error> {
127    encode_prefixed(value, limits, &[])
128}
129fn encode_prefixed<T: Serialize + ?Sized>(
130    value: &T,
131    limits: Limits,
132    prefix: &[u8],
133) -> Result<Vec<u8>, Error> {
134    let mut bytes = Vec::new();
135    encode_prefixed_into(value, limits, prefix, &mut bytes)?;
136    Ok(bytes)
137}
138
139/// Encode into reusable caller storage. Previous contents are cleared, including on error.
140/// Capacity may be lost on serialization failure; successful calls retain it.
141/// Retain a separate immutable frame when bytes must survive subsequent calls for retries.
142pub fn encode_into<T: Serialize + ?Sized>(
143    value: &T,
144    limits: Limits,
145    bytes: &mut Vec<u8>,
146) -> Result<(), Error> {
147    encode_prefixed_into(value, limits, &[], bytes)
148}
149
150/// Encode a payload into caller-owned storage without allocating codec output.
151///
152/// Returns only the completed encoding. A short buffer returns [`Error::Limit`]
153/// before writing; on any other error storage contents are unspecified and must
154/// not be published. The returned slice borrows storage, not the value. Admission,
155/// two-pass sizing and trusted custom Serde rules match [`encode_into`]; storage
156/// capacity does not replace or reduce the configured allocation allowance.
157pub fn encode_to_slice<'a, T: Serialize + ?Sized>(
158    value: &T,
159    limits: Limits,
160    storage: &'a mut [u8],
161) -> Result<&'a mut [u8], Error> {
162    encode_prefixed_to_slice(value, limits, &[], storage)
163}
164
165/// Encode a payload using scratch storage or owned fallback, then consume its bytes.
166///
167/// Preflight selects a destination once: scratch when the admitted payload fits,
168/// otherwise an owned buffer. Both paths retain [`encode`]'s two-pass admission
169/// and allocation allowance; custom serializers may still allocate independently.
170/// The owned `value` is dropped before the synchronous callback. Passing a
171/// reference instead keeps ownership with the caller.
172///
173/// Encoding failure never calls `consume`; scratch contents on error are
174/// unspecified. Only successful bytes may be published. Copy bytes that must
175/// survive the callback, including saved retries. No framing is added.
176///
177/// ```
178/// let limits = ic_wire::Limits { bytes: 128, allocation: 2048, nodes: 64, depth: 8 };
179/// let mut scratch = [0; 128];
180/// let length = ic_wire::native::with_encoded(7_u64, limits, &mut scratch, |bytes| {
181///     assert_eq!(bytes, [7]);
182///     bytes.len()
183/// })?;
184/// assert_eq!(length, 1);
185/// # Ok::<(), ic_wire::Error>(())
186/// ```
187/// Callback bytes cannot escape as a borrow or inside a future:
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| bytes);
192/// ```
193/// ```compile_fail
194/// let limits = ic_wire::Limits { bytes: 128, allocation: 2048, nodes: 64, depth: 8 };
195/// let mut scratch = [0; 128];
196/// ic_wire::native::with_encoded(7_u64, limits, &mut scratch, |bytes| async move {
197///     bytes.len()
198/// });
199/// ```
200pub fn with_encoded<T: Serialize, R>(
201    value: T,
202    limits: Limits,
203    scratch: &mut [u8],
204    consume: impl FnOnce(&[u8]) -> R,
205) -> Result<R, Error> {
206    let size = encoded_len(&value, limits)?;
207    let budget = Budget::new(limits);
208    let bounded = encode::Bounded {
209        value: &value,
210        budget: &budget,
211        depth: 1,
212    };
213    // New destination selection only: both writers are capped at the preflight
214    // size. Postcard bytes and admission match encode; e.g. u64 7 remains 07.
215    // Existing endpoints/retries need no migration or alternate codec.
216    if let Some(storage) = scratch.get_mut(..size) {
217        let bytes = postcard::to_slice(&bounded, storage).map_err(|_| budget.error())?;
218        drop(value);
219        Ok(consume(bytes))
220    } else {
221        let mut bytes = Vec::new();
222        bytes
223            .try_reserve_exact(size)
224            .map_err(|_| Error::Allocation)?;
225        let bytes =
226            postcard::serialize_with_flavor(&bounded, contract::Output { bytes, limit: size })
227                .map_err(|_| budget.error())?;
228        drop(value);
229        Ok(consume(&bytes))
230    }
231}
232
233/// Encode a complete request into caller storage; follows [`encode_to_slice`].
234pub fn encode_request_to_slice<'a, T: Serialize + ?Sized>(
235    id: u64,
236    value: &T,
237    limits: Limits,
238    storage: &'a mut [u8],
239) -> Result<&'a mut [u8], Error> {
240    encode_prefixed_to_slice(value, limits, &contract::header(id, 0), storage)
241}
242
243/// Encode a complete response into caller storage; follows [`encode_to_slice`].
244pub fn encode_response_to_slice<'a, T: Serialize + ?Sized>(
245    id: u64,
246    value: &T,
247    limits: Limits,
248    storage: &'a mut [u8],
249) -> Result<&'a mut [u8], Error> {
250    encode_prefixed_to_slice(value, limits, &contract::header(id, 1), storage)
251}
252
253fn encode_prefixed_to_slice<'a, T: Serialize + ?Sized>(
254    value: &T,
255    limits: Limits,
256    prefix: &[u8],
257    storage: &'a mut [u8],
258) -> Result<&'a mut [u8], Error> {
259    let size = encoded_len(value, payload_limits(limits, prefix.len())?)?;
260    let output = storage.get_mut(..size + prefix.len()).ok_or(Error::Limit)?;
261    output[..prefix.len()].copy_from_slice(prefix);
262    let budget = Budget::new(limits);
263    // New storage API, same Postcard bytes and admission as Vec output. The
264    // slice flavor bounds every write, including inconsistent custom Serialize
265    // passes. Only Ok publishes a completed prefix; no inline/spill policy or
266    // framing change is introduced, and existing consumers need no migration.
267    let payload = postcard::to_slice(
268        &encode::Bounded {
269            value,
270            budget: &budget,
271            depth: 1,
272        },
273        &mut output[prefix.len()..],
274    )
275    .map_err(|_| budget.error())?;
276    let written = prefix.len() + payload.len();
277    Ok(&mut output[..written])
278}
279
280fn encode_prefixed_into<T: Serialize + ?Sized>(
281    value: &T,
282    limits: Limits,
283    prefix: &[u8],
284    bytes: &mut Vec<u8>,
285) -> Result<(), Error> {
286    bytes.clear();
287    let size = encoded_len(value, payload_limits(limits, prefix.len())?)?;
288    bytes
289        .try_reserve_exact(size + prefix.len())
290        .map_err(|_| Error::Allocation)?;
291    bytes.extend_from_slice(prefix);
292    let budget = Budget::new(limits);
293    *bytes = postcard::serialize_with_flavor(
294        &encode::Bounded {
295            value,
296            budget: &budget,
297            depth: 1,
298        },
299        contract::Output {
300            bytes: core::mem::take(bytes),
301            limit: size + prefix.len(),
302        },
303    )
304    .map_err(|_| budget.error())?;
305    Ok(())
306}
307
308/// Encode a request into reusable storage; follows [`encode_into`] failure semantics.
309pub fn encode_request_into<T: Serialize + ?Sized>(
310    id: u64,
311    value: &T,
312    limits: Limits,
313    bytes: &mut Vec<u8>,
314) -> Result<(), Error> {
315    encode_prefixed_into(value, limits, &contract::header(id, 0), bytes)
316}
317
318/// Encode a response into reusable storage; follows [`encode_into`] failure semantics.
319pub fn encode_response_into<T: Serialize + ?Sized>(
320    id: u64,
321    value: &T,
322    limits: Limits,
323    bytes: &mut Vec<u8>,
324) -> Result<(), Error> {
325    encode_prefixed_into(value, limits, &contract::header(id, 1), bytes)
326}
327
328/// Decode a complete Postcard payload; allocation admission precedes visitor calls.
329/// Borrowed bytes and strings may refer directly to the input for its lifetime.
330/// Primitive representations are checked while reading; Serde normalization is consumer-owned.
331pub fn decode<'de, T: Deserialize<'de>>(bytes: &'de [u8], limits: Limits) -> Result<T, Error> {
332    if bytes.len() > limits.bytes {
333        return Err(Error::Limit);
334    }
335    let budget = Budget::new(limits);
336    let mut input = parser::Deserializer::from_bytes(bytes);
337    let value = T::deserialize(decode::Bounded {
338        inner: &mut input,
339        budget: &budget,
340        depth: 1,
341    })
342    .map_err(|_| budget.error())?;
343    if !input.finalize().map_err(|_| Error::Malformed)?.is_empty() {
344        return Err(Error::Trailing);
345    }
346    // Hard cut: decoding no longer reserializes or charges a serialization pass.
347    // Primitive parsing stays strict, but maps/sets and custom Serde may normalize:
348    // e.g. map bytes 02 02 14 01 0a now decode and re-encode as 02 01 0a 02 14.
349    // Consumers own normalization and must retain original bytes for exact retries.
350    Ok(value)
351}
352
353/// Encode a complete request; `limits.bytes` includes the canonical frame header.
354pub fn encode_request<T: Serialize + ?Sized>(
355    id: u64,
356    value: &T,
357    limits: Limits,
358) -> Result<Vec<u8>, Error> {
359    encode_prefixed(value, limits, &contract::header(id, 0))
360}
361/// Encode a complete response, including borrowed response projections.
362pub fn encode_response<T: Serialize + ?Sized>(
363    id: u64,
364    value: &T,
365    limits: Limits,
366) -> Result<Vec<u8>, Error> {
367    encode_prefixed(value, limits, &contract::header(id, 1))
368}
369/// Measure a request or response frame; both directions have equal header length.
370pub fn encoded_frame_len<T: Serialize + ?Sized>(value: &T, limits: Limits) -> Result<usize, Error> {
371    encoded_len(value, payload_limits(limits, contract::HEADER)?)
372        .map(|size| size + contract::HEADER)
373}
374/// Check request identity and direction before decoding its bounded native value.
375pub fn decode_request<'de, T: Deserialize<'de>>(
376    id: u64,
377    bytes: &'de [u8],
378    limits: Limits,
379) -> Result<T, Error> {
380    decode_frame(id, 0, bytes, limits)
381}
382/// Check response identity and direction before decoding its bounded native value.
383pub fn decode_response<'de, T: Deserialize<'de>>(
384    id: u64,
385    bytes: &'de [u8],
386    limits: Limits,
387) -> Result<T, Error> {
388    decode_frame(id, 1, bytes, limits)
389}
390fn decode_frame<'de, T: Deserialize<'de>>(
391    id: u64,
392    direction: u8,
393    bytes: &'de [u8],
394    limits: Limits,
395) -> Result<T, Error> {
396    if bytes.len() > limits.bytes {
397        return Err(Error::Limit);
398    }
399    let payload = contract::payload(id, direction, bytes)?;
400    decode(payload, payload_limits(limits, contract::HEADER)?)
401}
402
403fn payload_limits(limits: Limits, prefix: usize) -> Result<Limits, Error> {
404    Ok(Limits {
405        bytes: limits.bytes.checked_sub(prefix).ok_or(Error::Limit)?,
406        allocation: limits.allocation.checked_sub(prefix).ok_or(Error::Limit)?,
407        ..limits
408    })
409}
410
411/// Native request frame header size, including magic, identity and direction.
412pub const REQUEST_HEADER_BYTES: usize = crate::contract::HEADER;
413
414/// Validate only the fixed request header before a transport copies the payload.
415/// The complete request still requires normal decoding and resource admission.
416/// This delegates to the same validator previously used inside Wire's endpoint
417/// module: accepted bytes and error ordering are unchanged after its package move.
418pub fn validate_request_header(
419    id: u64,
420    header: &[u8; REQUEST_HEADER_BYTES],
421) -> Result<(), crate::Error> {
422    crate::contract::payload(id, 0, header).map(|_| ())
423}
424
425#[cfg(test)]
426mod header_admission_tests {
427    #[test]
428    fn pre_copy_header_agrees_with_complete_frame_admission() {
429        let id = 7;
430        let frame = super::encode_request(
431            id,
432            &(),
433            crate::Limits {
434                bytes: 64,
435                allocation: 1024,
436                nodes: 8,
437                depth: 8,
438            },
439        )
440        .unwrap();
441        let header: [u8; super::REQUEST_HEADER_BYTES] =
442            frame[..super::REQUEST_HEADER_BYTES].try_into().unwrap();
443        assert_eq!(super::validate_request_header(id, &header), Ok(()));
444        for index in [0, 2, 10] {
445            let mut damaged = header;
446            damaged[index] ^= 1;
447            assert_eq!(
448                super::validate_request_header(id, &damaged),
449                Err(crate::Error::Identity)
450            );
451        }
452    }
453}