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