Skip to main content

ic_host_tools/candid/decode/
mod.rs

1//! Bounded single-value Candid decoding with caller-owned resource policy.
2//!
3//! This module does not authenticate replies or interpret application errors.
4
5use ic_platform::candid::{self as codec, contract::CandidType};
6use serde::de::DeserializeOwned;
7use thiserror::Error;
8
9#[cfg(test)]
10mod tests;
11
12/// Explicit per-reply byte and decoder work limits. Zero remains a real limit.
13#[derive(Clone, Copy, Debug, Eq, PartialEq)]
14pub struct DecodeLimits {
15    /// Maximum encoded reply bytes.
16    pub reply_bytes: usize,
17    /// Maximum Candid decoding work.
18    pub decoding_quota: usize,
19    /// Maximum Candid skipped-field work.
20    pub skipping_quota: usize,
21    /// Maximum number of entries in the type table.
22    pub type_entries: usize,
23    /// Maximum encoded header bytes.
24    pub header_bytes: usize,
25}
26
27/// Resource admission failures are distinct from malformed or incompatible Candid.
28#[derive(Debug, Error)]
29pub enum DecodeError {
30    /// The encoded reply exceeds the per-reply allowance.
31    #[error("Candid reply exceeds its byte limit")]
32    ReplyBytes,
33    /// The reply cannot be admitted within the aggregate allowance.
34    #[error("Candid reply exceeds the remaining aggregate byte budget")]
35    AggregateBytes,
36    /// Malformed, incompatible or decoder-quota-exhausting Candid.
37    #[error("Candid reply could not be decoded: {0}")]
38    Candid(#[source] codec::contract::Error),
39}
40
41/// Decode one result using Candid's compatibility rules, charging admitted bytes.
42///
43/// Oversized replies do not consume aggregate allowance. Once admitted, a reply
44/// consumes its bytes before decoding, so malformed replies cannot evade accounting.
45/// Caller policy owns quotas; this helper never authenticates the decoded value.
46/// Extra wire arguments are skipped under the selected work quotas, like Candid's
47/// decoder; trailing undecodable bytes reject. These are byte and decoder-work
48/// limits, not a heap, stack or elapsed-time guarantee.
49/// Platform selects compact Candid diagnostics, which can still include small
50/// inputs. Callers retain error-disclosure policy and the original typed cause.
51///
52/// # Errors
53/// Returns byte admission failure before decoding, or the original Candid error.
54pub fn decode_one<T: CandidType + DeserializeOwned>(
55    bytes: &[u8],
56    limits: DecodeLimits,
57    remaining: &mut usize,
58) -> Result<T, DecodeError> {
59    if bytes.len() > limits.reply_bytes {
60        return Err(DecodeError::ReplyBytes);
61    }
62    *remaining = remaining
63        .checked_sub(bytes.len())
64        .ok_or(DecodeError::AggregateBytes)?;
65    codec::decode_one(
66        bytes,
67        codec::DecodeLimits {
68            reply_bytes: limits.reply_bytes,
69            decoding_quota: limits.decoding_quota,
70            skipping_quota: limits.skipping_quota,
71            type_entries: limits.type_entries,
72            header_bytes: limits.header_bytes,
73        },
74    )
75    .map_err(|error| match error {
76        codec::DecodeError::ReplyTooLarge { .. } => DecodeError::ReplyBytes,
77        codec::DecodeError::Candid(source) => DecodeError::Candid(source),
78    })
79}