Skip to main content

ic_testkit_candid/
lib.rs

1//! Caller-bounded Candid reply decoding, independent of PocketIC or transport.
2//!
3//! Callers select finite work limits, authenticate sources and retain original
4//! bytes. Ordinary single-result decoding permits Candid's bounded skipping of
5//! additional values; exact-single-result decoding rejects them.
6//! All decoders select Candid's compact diagnostics while retaining typed causes.
7//! Compact diagnostics can include small inputs; callers own error disclosure.
8
9use candid::{
10    CandidType,
11    de::{DecoderConfig, IDLDeserialize},
12};
13use serde::Deserialize;
14use std::{error::Error, fmt};
15
16#[cfg(test)]
17mod tests;
18
19/// Caller-selected wire, decoding, skipping, type-table and header ceilings.
20#[derive(Clone, Copy, Debug, Eq, PartialEq)]
21pub struct DecodeLimits {
22    /// Maximum original reply bytes admitted before decoder construction.
23    pub reply_bytes: usize,
24    /// Maximum Candid decoding work; zero permits no charged work.
25    pub decoding_quota: usize,
26    /// Maximum work spent skipping values not requested by the caller.
27    pub skipping_quota: usize,
28    /// Maximum number of type-table entries in the wire header.
29    pub type_entries: usize,
30    /// Maximum wire bytes occupied by the Candid header.
31    pub header_bytes: usize,
32}
33
34/// Wire admission or Candid decoding failure, retaining the original decoder cause.
35#[derive(Debug)]
36pub enum DecodeError {
37    /// Original wire reply exceeds the selected byte ceiling.
38    ReplyTooLarge {
39        /// Observed original reply length.
40        actual: usize,
41        /// Caller-selected maximum reply length.
42        maximum: usize,
43    },
44    /// Candid shape, work quota, truncation or exact-result admission failed.
45    Candid(candid::Error),
46}
47
48impl fmt::Display for DecodeError {
49    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50        match self {
51            Self::ReplyTooLarge { actual, maximum } => {
52                write!(f, "Candid reply is {actual} bytes, maximum is {maximum}")
53            }
54            Self::Candid(error) => write!(f, "Candid reply decoding failed: {error}"),
55        }
56    }
57}
58
59impl Error for DecodeError {
60    fn source(&self) -> Option<&(dyn Error + 'static)> {
61        match self {
62            Self::ReplyTooLarge { .. } => None,
63            Self::Candid(error) => Some(error),
64        }
65    }
66}
67
68fn config(bytes: &[u8], limits: DecodeLimits) -> Result<DecoderConfig, DecodeError> {
69    if bytes.len() > limits.reply_bytes {
70        return Err(DecodeError::ReplyTooLarge {
71            actual: bytes.len(),
72            maximum: limits.reply_bytes,
73        });
74    }
75    let mut config = DecoderConfig::new();
76    config
77        .set_decoding_quota(limits.decoding_quota)
78        .set_skipping_quota(limits.skipping_quota)
79        .set_max_type_len(limits.type_entries)
80        .set_max_header_len(limits.header_bytes)
81        .set_full_error_message(false);
82    Ok(config)
83}
84
85/// Decode one typed result with caller-bounded skipping of other Candid values.
86///
87/// # Errors
88/// Refuses oversized replies and preserves Candid shape/quota failures.
89pub fn decode_one<'a, T: Deserialize<'a> + CandidType>(
90    bytes: &'a [u8],
91    limits: DecodeLimits,
92) -> Result<T, DecodeError> {
93    candid::decode_one_with_config(bytes, &config(bytes, limits)?).map_err(DecodeError::Candid)
94}
95
96/// Decode top-level reply arguments with caller-bounded decoding and skipping.
97///
98/// `(T, U)` requests two arguments, while `(T,)` requests one. As with
99/// [`decode_one`], additional values may be skipped within the selected quota.
100///
101/// # Errors
102/// Refuses wire/work ceilings, malformed arguments and trailing bytes.
103pub fn decode_args<'a, T: candid::utils::ArgumentDecoder<'a>>(
104    bytes: &'a [u8],
105    limits: DecodeLimits,
106) -> Result<T, DecodeError> {
107    candid::decode_args_with_config(bytes, &config(bytes, limits)?).map_err(DecodeError::Candid)
108}
109
110/// Decode exactly one typed Candid result, rejecting missing or additional values or bytes.
111///
112/// # Errors
113/// Refuses wire/work ceilings, wrong result shape/arity and trailing bytes.
114pub fn decode_exactly_one<'a, T: Deserialize<'a> + CandidType>(
115    bytes: &'a [u8],
116    limits: DecodeLimits,
117) -> Result<T, DecodeError> {
118    let config = config(bytes, limits)?;
119    let mut decoder =
120        IDLDeserialize::new_with_config(bytes, &config).map_err(DecodeError::Candid)?;
121    if decoder.is_done() {
122        return Err(DecodeError::Candid(candid::Error::msg(
123            "expected exactly one Candid result",
124        )));
125    }
126    let value = decoder.get_value::<T>().map_err(DecodeError::Candid)?;
127    if !decoder.is_done() {
128        return Err(DecodeError::Candid(candid::Error::msg(
129            "expected exactly one Candid result",
130        )));
131    }
132    decoder.done().map_err(DecodeError::Candid)?;
133    Ok(value)
134}