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 candid_parser::candid::{CandidType, de::DecoderConfig, utils::decode_one_with_config};
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] candid_parser::candid::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///
50/// # Errors
51/// Returns byte admission failure before decoding, or the original Candid error.
52pub fn decode_one<T: CandidType + DeserializeOwned>(
53 bytes: &[u8],
54 limits: DecodeLimits,
55 remaining: &mut usize,
56) -> Result<T, DecodeError> {
57 if bytes.len() > limits.reply_bytes {
58 return Err(DecodeError::ReplyBytes);
59 }
60 *remaining = remaining
61 .checked_sub(bytes.len())
62 .ok_or(DecodeError::AggregateBytes)?;
63 let mut config = DecoderConfig::new();
64 config
65 .set_decoding_quota(limits.decoding_quota)
66 .set_skipping_quota(limits.skipping_quota)
67 .set_max_type_len(limits.type_entries)
68 .set_max_header_len(limits.header_bytes);
69 decode_one_with_config(bytes, &config).map_err(DecodeError::Candid)
70}