Skip to main content

candid_core/
value.rs

1//! The lossless tagged HostValue ABI and graph-directed value validation.
2//!
3//! Gated on the `host-value` feature. Principal text is checked with
4//! `ic_principal`, taken as a direct dependency rather than reached through
5//! `candid_parser::Principal`: that re-export chain is
6//! `candid_parser` → `candid` → `ic_principal`, so borrowing it would put the
7//! whole Candid source engine in the graph of a host that only ever validates
8//! values. `candid::Principal` *is* `ic_principal::Principal` — a plain
9//! `pub use`, not a wrapper — so accepted and rejected principal text, the
10//! `PrincipalError` variants, and their rendered messages are the same type's,
11//! unchanged.
12
13use crate::limits::Limits;
14use crate::model::{Contract, PrimitiveType, TypeNode, TypeRef};
15use serde::{Deserialize, Serialize};
16use std::fmt;
17
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19#[serde(deny_unknown_fields)]
20pub struct ContractTypeRef {
21    pub contract_id: String,
22    pub type_ref: TypeRef,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(deny_unknown_fields)]
27pub struct ContractMethodRef {
28    pub contract_id: String,
29    pub method_name: String,
30}
31
32/// One field of a [`HostValue`] record, addressed by its authoritative Candid
33/// field ID.
34///
35/// The fields are private so a record entry cannot be assembled by struct
36/// literal, which would let a caller reach [`HostValue::record`] with a value
37/// whose extent was never measured.
38#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
39pub struct HostFieldValue {
40    id: u32,
41    value: HostValue,
42}
43
44impl HostFieldValue {
45    /// Pair a field ID with a value.
46    ///
47    /// This is deliberately infallible: a field carries no bound of its own,
48    /// and the enclosing [`HostValue::record`] call is what measures the
49    /// combined extent against the caller's [`Limits`].
50    pub fn new(id: u32, value: HostValue) -> Self {
51        Self { id, value }
52    }
53
54    pub fn id(&self) -> u32 {
55        self.id
56    }
57
58    pub fn value(&self) -> &HostValue {
59        &self.value
60    }
61}
62
63/// A locally canonical tagged HostValue.
64///
65/// This type serializes as the portable tagged JSON ABI, but deliberately does
66/// not implement `Deserialize`. JSON callers must use
67/// [`HostValue::from_json_with_limits`], which decodes a private raw DTO and
68/// checks locally canonical scalar encodings before exposing this value.
69///
70/// Every value carries its own measured extent, so the container constructors can
71/// reject an over-deep or over-large value in constant time per level. That
72/// bound is what makes the recursive operations on this type safe: `Drop`,
73/// `Clone`, `PartialEq`, `Debug`, and `Serialize` all recurse once per level,
74/// and all of them abort the process on a value deep enough to exhaust the
75/// stack. Bounding construction is the only chokepoint that covers all five,
76/// so there is deliberately no way to obtain a `HostValue` whose depth was
77/// never checked against a caller-supplied limit.
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct HostValue {
80    kind: HostValueKind,
81    extent: Extent,
82}
83
84/// The measured size of a value: how deeply it nests and how many nodes it
85/// contains.
86///
87/// Both counters use the same units as the JSON decode path in
88/// [`HostValueLocalValidationState::canonicalize_value`], so one value reports
89/// the same `observed` figure whether it was rejected while being decoded or
90/// while being constructed. `depth` counts container edges below a node, so a
91/// scalar and an empty container are both `0`; `elements` counts nodes
92/// inclusive of the node itself, so a scalar is `1`.
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94struct Extent {
95    depth: u32,
96    elements: u32,
97}
98
99impl Extent {
100    const LEAF: Self = Self {
101        depth: 0,
102        elements: 1,
103    };
104
105    fn of(kind: &HostValueKind) -> Self {
106        match kind {
107            HostValueKind::Opt { value: Some(value) } => Self::enclosing([value.extent]),
108            HostValueKind::Vec { values } => {
109                Self::enclosing(values.iter().map(|value| value.extent))
110            }
111            HostValueKind::Record { fields } => {
112                Self::enclosing(fields.iter().map(|field| field.value.extent))
113            }
114            HostValueKind::Variant { value, .. } => Self::enclosing([value.extent]),
115            _ => Self::LEAF,
116        }
117    }
118
119    /// An empty container yields depth `0`, not `1`: it encloses nothing, so it
120    /// adds no edge, exactly as the decode path charges it no child recursion.
121    fn enclosing(children: impl IntoIterator<Item = Self>) -> Self {
122        let mut deepest: Option<u32> = None;
123        let mut elements = 1u32;
124        for child in children {
125            deepest = Some(deepest.map_or(child.depth, |depth| depth.max(child.depth)));
126            elements = elements.saturating_add(child.elements);
127        }
128        Self {
129            depth: deepest.map_or(0, |depth| depth.saturating_add(1)),
130            elements,
131        }
132    }
133}
134
135// Hand-written so the wire shape stays exactly what `#[serde(transparent)]`
136// produced before `HostValue` gained its cached extent: the tagged ABI of
137// ADR 0006, with no envelope and no extent field.
138impl Serialize for HostValue {
139    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
140    where
141        S: serde::Serializer,
142    {
143        self.kind.serialize(serializer)
144    }
145}
146
147#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
148#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
149enum HostValueKind {
150    Null,
151    Bool { value: bool },
152    Nat { value: String },
153    Int { value: String },
154    Nat8 { value: u8 },
155    Nat16 { value: u16 },
156    Nat32 { value: u32 },
157    Nat64 { value: String },
158    Int8 { value: i8 },
159    Int16 { value: i16 },
160    Int32 { value: i32 },
161    Int64 { value: String },
162    Float32 { bits: String },
163    Float64 { bits: String },
164    Text { value: String },
165    Reserved,
166    Principal { value: String },
167    Opt { value: Option<Box<HostValue>> },
168    Vec { values: Vec<HostValue> },
169    Record { fields: Vec<HostFieldValue> },
170    Variant { id: u32, value: Box<HostValue> },
171    Service { principal: String },
172    Func { principal: String, method: String },
173}
174
175impl HostValue {
176    pub fn from_json_with_limits(input: &str, limits: &Limits) -> Result<Self, HostValueJsonError> {
177        Self::from_json_with_context(input, &crate::RuntimeContext::new(limits.clone()))
178    }
179
180    pub fn from_json_with_context(
181        input: &str,
182        context: &crate::RuntimeContext,
183    ) -> Result<Self, HostValueJsonError> {
184        let limits = &context.limits;
185        if input.len() > limits.max_value_bytes {
186            return Err(HostValueJsonError::Limit {
187                limit: limits.max_value_bytes,
188                observed: input.len(),
189            });
190        }
191        let mut budget = context.budget();
192        budget
193            .checkpoint()
194            .map_err(|error| host_json_budget_error(error, "$"))?;
195        check_value_nesting(input, &mut budget)?;
196        let raw: RawHostValue = serde_json::from_str(input)
197            .map_err(|error| HostValueJsonError::Malformed(error.to_string()))?;
198        budget
199            .checkpoint()
200            .map_err(|error| host_json_budget_error(error, "$"))?;
201        HostValueLocalValidationState::new(&mut budget).canonicalize(raw)
202    }
203
204    pub fn null() -> Self {
205        Self::scalar(HostValueKind::Null)
206    }
207
208    pub fn boolean(value: bool) -> Self {
209        Self::scalar(HostValueKind::Bool { value })
210    }
211
212    pub fn nat(value: impl Into<String>) -> Result<Self, HostValueJsonError> {
213        let value = value.into();
214        Self::require(canonical_nat(&value), "non-canonical nat")?;
215        Ok(Self::scalar(HostValueKind::Nat { value }))
216    }
217
218    pub fn int(value: impl Into<String>) -> Result<Self, HostValueJsonError> {
219        let value = value.into();
220        Self::require(canonical_int(&value), "non-canonical int")?;
221        Ok(Self::scalar(HostValueKind::Int { value }))
222    }
223
224    pub fn nat8(value: u8) -> Self {
225        Self::scalar(HostValueKind::Nat8 { value })
226    }
227
228    pub fn nat16(value: u16) -> Self {
229        Self::scalar(HostValueKind::Nat16 { value })
230    }
231
232    pub fn nat32(value: u32) -> Self {
233        Self::scalar(HostValueKind::Nat32 { value })
234    }
235
236    pub fn nat64(value: impl Into<String>) -> Result<Self, HostValueJsonError> {
237        let value = value.into();
238        Self::require(
239            canonical_nat(&value) && value.parse::<u64>().is_ok(),
240            "non-canonical nat64",
241        )?;
242        Ok(Self::scalar(HostValueKind::Nat64 { value }))
243    }
244
245    pub fn int8(value: i8) -> Self {
246        Self::scalar(HostValueKind::Int8 { value })
247    }
248
249    pub fn int16(value: i16) -> Self {
250        Self::scalar(HostValueKind::Int16 { value })
251    }
252
253    pub fn int32(value: i32) -> Self {
254        Self::scalar(HostValueKind::Int32 { value })
255    }
256
257    pub fn int64(value: impl Into<String>) -> Result<Self, HostValueJsonError> {
258        let value = value.into();
259        Self::require(
260            canonical_int(&value) && value.parse::<i64>().is_ok(),
261            "non-canonical int64",
262        )?;
263        Ok(Self::scalar(HostValueKind::Int64 { value }))
264    }
265
266    pub fn float32(bits: impl Into<String>) -> Result<Self, HostValueJsonError> {
267        let bits = bits.into();
268        Self::require(canonical_hex(&bits, 8), "non-canonical float32 bits")?;
269        Ok(Self::scalar(HostValueKind::Float32 { bits }))
270    }
271
272    pub fn float64(bits: impl Into<String>) -> Result<Self, HostValueJsonError> {
273        let bits = bits.into();
274        Self::require(canonical_hex(&bits, 16), "non-canonical float64 bits")?;
275        Ok(Self::scalar(HostValueKind::Float64 { bits }))
276    }
277
278    pub fn text(value: impl Into<String>) -> Self {
279        Self::scalar(HostValueKind::Text {
280            value: value.into(),
281        })
282    }
283
284    pub fn reserved() -> Self {
285        Self::scalar(HostValueKind::Reserved)
286    }
287
288    pub fn principal(value: impl Into<String>) -> Result<Self, HostValueJsonError> {
289        let value = value.into();
290        Self::require_canonical_principal(&value)?;
291        Ok(Self::scalar(HostValueKind::Principal { value }))
292    }
293
294    /// Wrap a value in an `opt`, failing closed past `limits.max_value_depth`
295    /// or `limits.max_value_elements`.
296    ///
297    /// Container construction is fallible because every recursive operation on
298    /// the result — including `Drop`, `Clone`, and `Debug`, none of which can
299    /// signal failure — walks one stack frame per level. Refusing to build the
300    /// value is the only point at which a policy can still be applied.
301    pub fn opt(value: Option<Self>, limits: &Limits) -> Result<Self, HostValueJsonError> {
302        Self::bounded(
303            HostValueKind::Opt {
304                value: value.map(Box::new),
305            },
306            limits,
307        )
308    }
309
310    /// Build a `vec`, failing closed past `limits.max_value_depth` or
311    /// `limits.max_value_elements`.
312    pub fn vector(values: Vec<Self>, limits: &Limits) -> Result<Self, HostValueJsonError> {
313        Self::bounded(HostValueKind::Vec { values }, limits)
314    }
315
316    /// Build a `record`, failing closed past `limits.max_value_depth` or
317    /// `limits.max_value_elements`.
318    ///
319    /// This does not check for duplicate field IDs or agreement with any
320    /// Contract type; that remains [`validate_host_value`]'s contract-directed
321    /// job.
322    pub fn record(
323        fields: Vec<HostFieldValue>,
324        limits: &Limits,
325    ) -> Result<Self, HostValueJsonError> {
326        Self::bounded(HostValueKind::Record { fields }, limits)
327    }
328
329    /// Build a `variant`, failing closed past `limits.max_value_depth` or
330    /// `limits.max_value_elements`.
331    pub fn variant(id: u32, value: Self, limits: &Limits) -> Result<Self, HostValueJsonError> {
332        Self::bounded(
333            HostValueKind::Variant {
334                id,
335                value: Box::new(value),
336            },
337            limits,
338        )
339    }
340
341    pub fn service(principal: impl Into<String>) -> Result<Self, HostValueJsonError> {
342        let principal = principal.into();
343        Self::require_canonical_principal(&principal)?;
344        Ok(Self::scalar(HostValueKind::Service { principal }))
345    }
346
347    pub fn func(
348        principal: impl Into<String>,
349        method: impl Into<String>,
350    ) -> Result<Self, HostValueJsonError> {
351        let principal = principal.into();
352        Self::require_canonical_principal(&principal)?;
353        Ok(Self::scalar(HostValueKind::Func {
354            principal,
355            method: method.into(),
356        }))
357    }
358
359    /// Wrap a kind that encloses nothing, so its extent needs no measuring.
360    fn scalar(kind: HostValueKind) -> Self {
361        debug_assert!(
362            Extent::of(&kind) == Extent::LEAF,
363            "scalar() used for a kind that encloses children",
364        );
365        Self {
366            kind,
367            extent: Extent::LEAF,
368        }
369    }
370
371    /// Wrap a kind whose children were already bounded by the caller.
372    ///
373    /// Used only by the JSON decode path, which enforces depth and elements
374    /// through the budget as it walks, so re-checking here would report the
375    /// same exhaustion twice under two different code paths.
376    fn measured(kind: HostValueKind) -> Self {
377        let extent = Extent::of(&kind);
378        Self { kind, extent }
379    }
380
381    /// Measure an enclosing kind and reject it if it breaches the caller's
382    /// policy.
383    ///
384    /// Constant time per level: each child already carries its own extent, so a
385    /// chain built one level at a time costs O(depth) overall rather than
386    /// O(depth^2).
387    fn bounded(kind: HostValueKind, limits: &Limits) -> Result<Self, HostValueJsonError> {
388        let extent = Extent::of(&kind);
389        if usize::try_from(extent.depth).unwrap_or(usize::MAX) > limits.max_value_depth {
390            return Err(HostValueJsonError::ValueLimit {
391                resource: "value_depth",
392                limit: limits.max_value_depth,
393                observed: usize::try_from(extent.depth).unwrap_or(usize::MAX),
394                path: "$".to_string(),
395            });
396        }
397        if usize::try_from(extent.elements).unwrap_or(usize::MAX) > limits.max_value_elements {
398            return Err(HostValueJsonError::ValueLimit {
399                resource: "value_elements",
400                limit: limits.max_value_elements,
401                observed: usize::try_from(extent.elements).unwrap_or(usize::MAX),
402                path: "$".to_string(),
403            });
404        }
405        Ok(Self { kind, extent })
406    }
407
408    fn require(condition: bool, message: &str) -> Result<(), HostValueJsonError> {
409        if condition {
410            Ok(())
411        } else {
412            Err(HostValueJsonError::Malformed(format!("$: {message}")))
413        }
414    }
415
416    fn require_canonical_principal(value: &str) -> Result<(), HostValueJsonError> {
417        let principal = ic_principal::Principal::from_text(value).map_err(|error| {
418            HostValueJsonError::Malformed(format!("$: invalid principal {value:?}: {error}"))
419        })?;
420        Self::require(principal.to_text() == value, "non-canonical principal")
421    }
422}
423
424#[derive(Debug, Clone, PartialEq, Eq)]
425pub enum HostValueJsonError {
426    Malformed(String),
427    Limit {
428        limit: usize,
429        observed: usize,
430    },
431    ValueLimit {
432        resource: &'static str,
433        limit: usize,
434        observed: usize,
435        path: String,
436    },
437    Deadline {
438        path: String,
439    },
440    Cancelled {
441        path: String,
442    },
443}
444
445impl fmt::Display for HostValueJsonError {
446    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
447        match self {
448            Self::Malformed(message) => write!(formatter, "malformed HostValue JSON: {message}"),
449            Self::Limit { limit, observed } => write!(
450                formatter,
451                "HostValue JSON uses {observed} bytes; limit is {limit}"
452            ),
453            Self::ValueLimit {
454                resource,
455                limit,
456                observed,
457                path,
458            } => write!(
459                formatter,
460                "HostValue JSON at {path} uses {observed} {resource}; limit is {limit}"
461            ),
462            Self::Deadline { path } => {
463                write!(
464                    formatter,
465                    "HostValue JSON validation deadline elapsed at {path}"
466                )
467            }
468            Self::Cancelled { path } => {
469                write!(formatter, "HostValue JSON validation cancelled at {path}")
470            }
471        }
472    }
473}
474
475impl std::error::Error for HostValueJsonError {}
476
477fn host_json_budget_error(error: crate::budget::BudgetError, path: &str) -> HostValueJsonError {
478    match error {
479        crate::budget::BudgetError::Cancelled => HostValueJsonError::Cancelled {
480            path: path.to_string(),
481        },
482        crate::budget::BudgetError::DeadlineExceeded => HostValueJsonError::Deadline {
483            path: path.to_string(),
484        },
485        crate::budget::BudgetError::ResourceLimit {
486            resource,
487            limit,
488            observed,
489        } => HostValueJsonError::ValueLimit {
490            resource,
491            limit,
492            observed,
493            path: path.to_string(),
494        },
495    }
496}
497
498/// Poll cancellation every this many input bytes rather than every byte, so a
499/// scan of a `max_value_bytes`-sized document stays responsive without paying
500/// an atomic load per byte.
501const NESTING_CHECKPOINT_INTERVAL: usize = 4096;
502
503/// Reject stack-hostile JSON nesting before `serde_json`'s recursive decoder
504/// sees it.
505///
506/// This is the HostValue analogue of `check_source_nesting`: an iterative,
507/// constant-stack scan that runs ahead of a recursive parser so hostile nesting
508/// is rejected by a crate-owned budget check instead of by exhausting the
509/// stack. `serde_json` counts recursion per JSON container too, so `observed`
510/// here is directly comparable to its fixed 128-frame ceiling; keeping
511/// `max_value_nesting` below that ceiling means this check always fires first
512/// while the ceiling remains in place as an unmodified second line of defence.
513///
514/// The scan deliberately does not validate JSON syntax. Unbalanced or malformed
515/// input keeps `serde_json`'s established diagnostic rather than gaining a
516/// second, competing one.
517///
518/// Scanning raw bytes is sound for UTF-8: a multi-byte sequence never contains
519/// an ASCII byte, so no `{`, `[`, `"`, or `\` can be mistaken for structure
520/// inside a multi-byte character.
521fn check_value_nesting(
522    input: &str,
523    budget: &mut crate::budget::Budget<'_>,
524) -> Result<(), HostValueJsonError> {
525    let limit = budget.limits().max_value_nesting;
526    let mut depth = 0usize;
527    let mut in_string = false;
528    let mut escaped = false;
529    // A comparison against a running target rather than `index % INTERVAL`:
530    // this loop runs once per input byte, and a division there is a measurable
531    // fraction of the whole scan.
532    let mut next_checkpoint = 0usize;
533
534    for (index, byte) in input.as_bytes().iter().enumerate() {
535        if index == next_checkpoint {
536            budget
537                .checkpoint()
538                .map_err(|error| host_json_budget_error(error, "$"))?;
539            next_checkpoint = index.saturating_add(NESTING_CHECKPOINT_INTERVAL);
540        }
541        if in_string {
542            if escaped {
543                escaped = false;
544            } else if *byte == b'\\' {
545                escaped = true;
546            } else if *byte == b'"' {
547                in_string = false;
548            }
549            continue;
550        }
551        match byte {
552            b'"' => in_string = true,
553            b'{' | b'[' => {
554                depth = depth.saturating_add(1);
555                if depth > limit {
556                    return Err(HostValueJsonError::ValueLimit {
557                        resource: "value_nesting",
558                        limit,
559                        observed: depth,
560                        path: "$".to_string(),
561                    });
562                }
563            }
564            b'}' | b']' => depth = depth.saturating_sub(1),
565            _ => {}
566        }
567    }
568    Ok(())
569}
570
571#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
572#[serde(deny_unknown_fields)]
573struct RawHostFieldValue {
574    id: u32,
575    value: RawHostValue,
576}
577
578#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
579#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
580enum RawHostValue {
581    Null,
582    Bool { value: bool },
583    Nat { value: String },
584    Int { value: String },
585    Nat8 { value: u8 },
586    Nat16 { value: u16 },
587    Nat32 { value: u32 },
588    Nat64 { value: String },
589    Int8 { value: i8 },
590    Int16 { value: i16 },
591    Int32 { value: i32 },
592    Int64 { value: String },
593    Float32 { bits: String },
594    Float64 { bits: String },
595    Text { value: String },
596    Reserved,
597    Principal { value: String },
598    Opt { value: Option<Box<RawHostValue>> },
599    Vec { values: Vec<RawHostValue> },
600    Record { fields: Vec<RawHostFieldValue> },
601    Variant { id: u32, value: Box<RawHostValue> },
602    Service { principal: String },
603    Func { principal: String, method: String },
604}
605
606struct HostValueLocalValidationState<'a, 'limits> {
607    budget: &'a mut crate::budget::Budget<'limits>,
608}
609
610impl<'a, 'limits> HostValueLocalValidationState<'a, 'limits> {
611    fn new(budget: &'a mut crate::budget::Budget<'limits>) -> Self {
612        Self { budget }
613    }
614
615    fn canonicalize(mut self, raw: RawHostValue) -> Result<HostValue, HostValueJsonError> {
616        self.canonicalize_value(raw, "$", 0)
617    }
618
619    fn canonicalize_value(
620        &mut self,
621        raw: RawHostValue,
622        path: &str,
623        depth: usize,
624    ) -> Result<HostValue, HostValueJsonError> {
625        self.budget
626            .checkpoint()
627            .map_err(|error| host_json_budget_error(error, path))?;
628        let limits = self.budget.limits().clone();
629        if depth > limits.max_value_depth {
630            return Err(HostValueJsonError::ValueLimit {
631                resource: "value_depth",
632                limit: limits.max_value_depth,
633                observed: depth,
634                path: path.to_string(),
635            });
636        }
637        self.budget
638            .charge("value_elements", limits.max_value_elements, 1)
639            .map_err(|error| host_json_budget_error(error, path))?;
640        self.budget
641            .charge("canonicalization_work", limits.max_canonicalization_work, 1)
642            .map_err(|error| host_json_budget_error(error, path))?;
643
644        let value = match raw {
645            RawHostValue::Null => HostValueKind::Null,
646            RawHostValue::Bool { value } => HostValueKind::Bool { value },
647            RawHostValue::Nat { value } => {
648                self.charge(&value, path)?;
649                self.require(canonical_nat(&value), path, "non-canonical nat")?;
650                HostValueKind::Nat { value }
651            }
652            RawHostValue::Int { value } => {
653                self.charge(&value, path)?;
654                self.require(canonical_int(&value), path, "non-canonical int")?;
655                HostValueKind::Int { value }
656            }
657            RawHostValue::Nat8 { value } => HostValueKind::Nat8 { value },
658            RawHostValue::Nat16 { value } => HostValueKind::Nat16 { value },
659            RawHostValue::Nat32 { value } => HostValueKind::Nat32 { value },
660            RawHostValue::Nat64 { value } => {
661                self.charge(&value, path)?;
662                self.require(
663                    canonical_nat(&value) && value.parse::<u64>().is_ok(),
664                    path,
665                    "non-canonical nat64",
666                )?;
667                HostValueKind::Nat64 { value }
668            }
669            RawHostValue::Int8 { value } => HostValueKind::Int8 { value },
670            RawHostValue::Int16 { value } => HostValueKind::Int16 { value },
671            RawHostValue::Int32 { value } => HostValueKind::Int32 { value },
672            RawHostValue::Int64 { value } => {
673                self.charge(&value, path)?;
674                self.require(
675                    canonical_int(&value) && value.parse::<i64>().is_ok(),
676                    path,
677                    "non-canonical int64",
678                )?;
679                HostValueKind::Int64 { value }
680            }
681            RawHostValue::Float32 { bits } => {
682                self.charge(&bits, path)?;
683                self.require(canonical_hex(&bits, 8), path, "non-canonical float32 bits")?;
684                HostValueKind::Float32 { bits }
685            }
686            RawHostValue::Float64 { bits } => {
687                self.charge(&bits, path)?;
688                self.require(canonical_hex(&bits, 16), path, "non-canonical float64 bits")?;
689                HostValueKind::Float64 { bits }
690            }
691            RawHostValue::Text { value } => {
692                self.charge(&value, path)?;
693                HostValueKind::Text { value }
694            }
695            RawHostValue::Reserved => HostValueKind::Reserved,
696            RawHostValue::Principal { value } => {
697                self.charge(&value, path)?;
698                self.require_canonical_principal(&value, path)?;
699                HostValueKind::Principal { value }
700            }
701            RawHostValue::Opt { value } => HostValueKind::Opt {
702                value: value
703                    .map(|value| {
704                        self.canonicalize_value(*value, &format!("{path}.value"), depth + 1)
705                    })
706                    .transpose()?
707                    .map(Box::new),
708            },
709            RawHostValue::Vec { values } => HostValueKind::Vec {
710                values: values
711                    .into_iter()
712                    .enumerate()
713                    .map(|(index, value)| {
714                        self.canonicalize_value(
715                            value,
716                            &format!("{path}.values[{index}]"),
717                            depth + 1,
718                        )
719                    })
720                    .collect::<Result<Vec<_>, _>>()?,
721            },
722            RawHostValue::Record { fields } => HostValueKind::Record {
723                fields: fields
724                    .into_iter()
725                    .map(|field| {
726                        Ok(HostFieldValue::new(
727                            field.id,
728                            self.canonicalize_value(
729                                field.value,
730                                &format!("{path}.fields[{}]", field.id),
731                                depth + 1,
732                            )?,
733                        ))
734                    })
735                    .collect::<Result<Vec<_>, HostValueJsonError>>()?,
736            },
737            RawHostValue::Variant { id, value } => HostValueKind::Variant {
738                id,
739                value: Box::new(self.canonicalize_value(
740                    *value,
741                    &format!("{path}.value"),
742                    depth + 1,
743                )?),
744            },
745            RawHostValue::Service { principal } => {
746                self.charge(&principal, path)?;
747                self.require_canonical_principal(&principal, path)?;
748                HostValueKind::Service { principal }
749            }
750            RawHostValue::Func { principal, method } => {
751                self.charge(&principal, path)?;
752                self.charge(&method, path)?;
753                self.require_canonical_principal(&principal, path)?;
754                HostValueKind::Func { principal, method }
755            }
756        };
757        Ok(HostValue::measured(value))
758    }
759
760    fn charge(&mut self, value: &str, path: &str) -> Result<(), HostValueJsonError> {
761        let limit = self.budget.limits().max_value_bytes;
762        self.budget
763            .charge("value_bytes", limit, value.len())
764            .map_err(|error| host_json_budget_error(error, path))?;
765        Ok(())
766    }
767
768    fn require(
769        &self,
770        condition: bool,
771        path: &str,
772        message: &str,
773    ) -> Result<(), HostValueJsonError> {
774        if condition {
775            Ok(())
776        } else {
777            Err(HostValueJsonError::Malformed(format!("{path}: {message}")))
778        }
779    }
780
781    fn require_canonical_principal(
782        &self,
783        value: &str,
784        path: &str,
785    ) -> Result<(), HostValueJsonError> {
786        let principal = ic_principal::Principal::from_text(value).map_err(|error| {
787            HostValueJsonError::Malformed(format!("{path}: invalid principal {value:?}: {error}"))
788        })?;
789        self.require(
790            principal.to_text() == value,
791            path,
792            "non-canonical principal",
793        )
794    }
795}
796
797/// Compatibility name for the shared diagnostic item in the HostValue
798/// validation domain.
799///
800/// HostValue violations are [`crate::Diagnostic`] values that always carry
801/// `path` and never carry `phase`/`severity`, so their serialized shape is
802/// unchanged: `{code, path, message, resource_limit?}`.
803pub type HostValueViolation = crate::Diagnostic;
804
805#[derive(Debug, Clone, PartialEq, Eq)]
806pub struct HostValueValidationError {
807    pub violations: Vec<HostValueViolation>,
808}
809
810impl fmt::Display for HostValueValidationError {
811    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
812        write!(
813            formatter,
814            "HostValue validation failed with {} violation(s)",
815            self.violations.len()
816        )
817    }
818}
819
820impl std::error::Error for HostValueValidationError {}
821
822pub fn validate_host_value(
823    contract: &Contract,
824    selector: &ContractTypeRef,
825    value: &HostValue,
826    limits: &Limits,
827) -> Result<(), HostValueValidationError> {
828    validate_host_value_with_context(
829        contract,
830        selector,
831        value,
832        &crate::RuntimeContext::new(limits.clone()),
833    )
834}
835
836pub fn validate_host_value_with_context(
837    contract: &Contract,
838    selector: &ContractTypeRef,
839    value: &HostValue,
840    context: &crate::RuntimeContext,
841) -> Result<(), HostValueValidationError> {
842    if selector.contract_id != contract.contract_id() {
843        return Err(single(
844            "value_contract_id_mismatch",
845            "$",
846            format!(
847                "expected Contract {}, found {}",
848                contract.contract_id(),
849                selector.contract_id
850            ),
851        ));
852    }
853    if selector.type_ref as usize >= contract.types().len() {
854        return Err(single(
855            "value_type_ref_out_of_bounds",
856            "$",
857            format!(
858                "type reference {} is outside the Contract",
859                selector.type_ref
860            ),
861        ));
862    }
863
864    let mut budget = context.budget();
865    let mut state = HostValueValidationState {
866        contract,
867        budget: &mut budget,
868    };
869    state.validate_node(selector.type_ref, value, "$", 0)
870}
871
872struct HostValueValidationState<'a, 'budget, 'limits> {
873    contract: &'a Contract,
874    budget: &'budget mut crate::budget::Budget<'limits>,
875}
876
877impl HostValueValidationState<'_, '_, '_> {
878    fn validate_node(
879        &mut self,
880        reference: TypeRef,
881        value: &HostValue,
882        path: &str,
883        depth: usize,
884    ) -> Result<(), HostValueValidationError> {
885        self.budget
886            .checkpoint()
887            .map_err(|error| host_validation_budget_error(error, path))?;
888        let limits = self.budget.limits().clone();
889        if depth > limits.max_value_depth {
890            return Err(resource_single(
891                "value_depth",
892                limits.max_value_depth,
893                depth,
894                path,
895                format!("value depth exceeds limit {}", limits.max_value_depth),
896            ));
897        }
898
899        self.charge_element(path)?;
900        self.charge_string_bytes(value, path)?;
901
902        match (&self.contract.types()[reference as usize], &value.kind) {
903            (TypeNode::Primitive { primitive }, value) => {
904                validate_primitive(*primitive, value, path)?;
905            }
906            (TypeNode::Opt { inner }, HostValueKind::Opt { value }) => {
907                if let Some(value) = value {
908                    self.preflight_children(1, path)?;
909                    let child_path = format!("{path}.value");
910                    self.validate_node(*inner, value, &child_path, depth + 1)?;
911                }
912            }
913            (TypeNode::Vec { inner }, HostValueKind::Vec { values }) => {
914                self.preflight_children(values.len(), path)?;
915                for (index, value) in values.iter().enumerate() {
916                    let child_path = format!("{path}.values[{index}]");
917                    self.validate_node(*inner, value, &child_path, depth + 1)?;
918                }
919            }
920            (TypeNode::Record { fields }, HostValueKind::Record { fields: values }) => {
921                self.preflight_children(values.len(), path)?;
922                for (index, field) in values.iter().enumerate() {
923                    for other in &values[index + 1..] {
924                        self.charge_work(path)?;
925                        if other.id == field.id {
926                            return Err(single(
927                                "duplicate_host_field",
928                                path,
929                                format!("record field ID {} occurs more than once", field.id),
930                            ));
931                        }
932                    }
933                }
934                let mut field_set_matches = fields.len() == values.len();
935                if field_set_matches {
936                    'expected_fields: for field in fields {
937                        for value in values {
938                            self.charge_work(path)?;
939                            if value.id == field.id {
940                                continue 'expected_fields;
941                            }
942                        }
943                        field_set_matches = false;
944                        break;
945                    }
946                }
947                if !field_set_matches {
948                    let expected_ids =
949                        self.sorted_field_ids(fields.len(), |index| fields[index].id, path)?;
950                    let actual_ids =
951                        self.sorted_field_ids(values.len(), |index| values[index].id, path)?;
952                    return Err(single(
953                        "record_field_set_mismatch",
954                        path,
955                        format!("expected field IDs {}, found {}", expected_ids, actual_ids),
956                    ));
957                }
958                for field in fields {
959                    let mut matching_value = None;
960                    for value in values {
961                        self.charge_work(path)?;
962                        if value.id == field.id {
963                            matching_value = Some(value);
964                            break;
965                        }
966                    }
967                    let value = matching_value.expect("record field set was checked above");
968                    let child_path = format!("{path}.fields[{}]", field.id);
969                    self.validate_node(field.ty, &value.value, &child_path, depth + 1)?;
970                }
971            }
972            (TypeNode::Variant { fields }, HostValueKind::Variant { id, value }) => {
973                // Charge each comparison, exactly like the record arm above.
974                // A variant type may hold up to `max_fields` entries, and a
975                // `vec variant` value can present one lookup per element, so an
976                // uncharged `fields.iter().find(...)` is `O(elements * fields)`
977                // of free, uninterruptible scan work. Charging bounds it to
978                // `max_canonicalization_work` and lets a deadline interrupt it.
979                let mut matched = None;
980                for field in fields {
981                    self.charge_work(path)?;
982                    if field.id == *id {
983                        matched = Some(field);
984                        break;
985                    }
986                }
987                let Some(field) = matched else {
988                    return Err(single(
989                        "unknown_variant_id",
990                        path,
991                        format!("variant ID {id} does not exist in the expected type"),
992                    ));
993                };
994                self.preflight_children(1, path)?;
995                let child_path = format!("{path}.value");
996                self.validate_node(field.ty, value, &child_path, depth + 1)?;
997            }
998            (TypeNode::Service { .. }, HostValueKind::Service { principal }) => {
999                validate_principal(principal, path)?;
1000            }
1001            (TypeNode::Func { .. }, HostValueKind::Func { principal, method }) => {
1002                validate_principal(principal, path)?;
1003                if method.is_empty() {
1004                    return Err(single(
1005                        "empty_function_method",
1006                        path,
1007                        "function method names must not be empty",
1008                    ));
1009                }
1010            }
1011            (TypeNode::Class { .. }, _) => {
1012                return Err(single(
1013                    "class_has_no_host_value",
1014                    path,
1015                    "service constructors are not first-class Candid values",
1016                ));
1017            }
1018            (expected, actual) => {
1019                return Err(single(
1020                    "host_value_kind_mismatch",
1021                    path,
1022                    format!(
1023                        "expected {}, found {}",
1024                        type_node_kind(expected),
1025                        host_value_kind(actual)
1026                    ),
1027                ));
1028            }
1029        }
1030        Ok(())
1031    }
1032
1033    fn charge_element(&mut self, path: &str) -> Result<(), HostValueValidationError> {
1034        let limit = self.budget.limits().max_value_elements;
1035        self.budget
1036            .charge("value_elements", limit, 1)
1037            .map_err(|error| host_validation_budget_error(error, path))?;
1038        Ok(())
1039    }
1040
1041    fn check_deadline(&self, path: &str) -> Result<(), HostValueValidationError> {
1042        self.budget
1043            .checkpoint()
1044            .map_err(|error| host_validation_budget_error(error, path))
1045    }
1046
1047    fn charge_work(&mut self, path: &str) -> Result<(), HostValueValidationError> {
1048        self.check_deadline(path)?;
1049        let limit = self.budget.limits().max_canonicalization_work;
1050        self.budget
1051            .charge("canonicalization_work", limit, 1)
1052            .map_err(|error| host_validation_budget_error(error, path))?;
1053        Ok(())
1054    }
1055
1056    fn preflight_children(
1057        &mut self,
1058        child_count: usize,
1059        path: &str,
1060    ) -> Result<(), HostValueValidationError> {
1061        let limit = self.budget.limits().max_value_elements;
1062        let observed = self
1063            .budget
1064            .consumed("value_elements")
1065            .saturating_add(child_count);
1066        if observed > limit {
1067            return Err(resource_single(
1068                "value_elements",
1069                limit,
1070                observed,
1071                path,
1072                format!("value elements exceed limit {limit}"),
1073            ));
1074        }
1075        Ok(())
1076    }
1077
1078    fn charge_string_bytes(
1079        &mut self,
1080        value: &HostValue,
1081        path: &str,
1082    ) -> Result<(), HostValueValidationError> {
1083        let limit = self.budget.limits().max_value_bytes;
1084        self.budget
1085            .charge("value_bytes", limit, value_string_bytes(value))
1086            .map_err(|error| host_validation_budget_error(error, path))?;
1087        Ok(())
1088    }
1089
1090    fn sorted_field_ids(
1091        &mut self,
1092        length: usize,
1093        id_at: impl Fn(usize) -> u32,
1094        path: &str,
1095    ) -> Result<String, HostValueValidationError> {
1096        let mut output = String::from("[");
1097        let mut previous = None;
1098        for position in 0..length {
1099            let mut next = None;
1100            for index in 0..length {
1101                self.charge_work(path)?;
1102                let id = id_at(index);
1103                let after_previous = previous.map_or(true, |previous| id > previous);
1104                let before_next = next.map_or(true, |next| id < next);
1105                if after_previous && before_next {
1106                    next = Some(id);
1107                }
1108            }
1109            let Some(id) = next else {
1110                break;
1111            };
1112            if position > 0 {
1113                output.push_str(", ");
1114            }
1115            output.push_str(&id.to_string());
1116            previous = Some(id);
1117        }
1118        output.push(']');
1119        Ok(output)
1120    }
1121}
1122
1123fn validate_primitive(
1124    primitive: PrimitiveType,
1125    value: &HostValueKind,
1126    path: &str,
1127) -> Result<(), HostValueValidationError> {
1128    let valid = match (primitive, value) {
1129        (PrimitiveType::Null, HostValueKind::Null)
1130        | (PrimitiveType::Bool, HostValueKind::Bool { .. })
1131        | (PrimitiveType::Nat8, HostValueKind::Nat8 { .. })
1132        | (PrimitiveType::Nat16, HostValueKind::Nat16 { .. })
1133        | (PrimitiveType::Nat32, HostValueKind::Nat32 { .. })
1134        | (PrimitiveType::Int8, HostValueKind::Int8 { .. })
1135        | (PrimitiveType::Int16, HostValueKind::Int16 { .. })
1136        | (PrimitiveType::Int32, HostValueKind::Int32 { .. })
1137        | (PrimitiveType::Reserved, HostValueKind::Reserved) => true,
1138        (PrimitiveType::Nat, HostValueKind::Nat { value }) => canonical_nat(value),
1139        (PrimitiveType::Int, HostValueKind::Int { value }) => canonical_int(value),
1140        (PrimitiveType::Nat64, HostValueKind::Nat64 { value }) => {
1141            canonical_nat(value) && value.parse::<u64>().is_ok()
1142        }
1143        (PrimitiveType::Int64, HostValueKind::Int64 { value }) => {
1144            canonical_int(value) && value.parse::<i64>().is_ok()
1145        }
1146        (PrimitiveType::Float32, HostValueKind::Float32 { bits }) => canonical_hex(bits, 8),
1147        (PrimitiveType::Float64, HostValueKind::Float64 { bits }) => canonical_hex(bits, 16),
1148        (PrimitiveType::Text, HostValueKind::Text { .. }) => true,
1149        (PrimitiveType::Principal, HostValueKind::Principal { value }) => {
1150            validate_principal(value, path)?;
1151            true
1152        }
1153        (PrimitiveType::Empty, _) => {
1154            return Err(single(
1155                "empty_has_no_value",
1156                path,
1157                "the Candid empty type has no constructible HostValue",
1158            ));
1159        }
1160        _ => false,
1161    };
1162    if valid {
1163        Ok(())
1164    } else {
1165        Err(single(
1166            "host_value_kind_mismatch",
1167            path,
1168            format!(
1169                "expected primitive {primitive:?}, found {} or a non-canonical representation",
1170                host_value_kind(value)
1171            ),
1172        ))
1173    }
1174}
1175
1176fn value_string_bytes(value: &HostValue) -> usize {
1177    match &value.kind {
1178        HostValueKind::Nat { value }
1179        | HostValueKind::Int { value }
1180        | HostValueKind::Nat64 { value }
1181        | HostValueKind::Int64 { value }
1182        | HostValueKind::Text { value }
1183        | HostValueKind::Principal { value } => value.len(),
1184        HostValueKind::Float32 { bits } | HostValueKind::Float64 { bits } => bits.len(),
1185        HostValueKind::Service { principal } => principal.len(),
1186        HostValueKind::Func { principal, method } => principal.len().saturating_add(method.len()),
1187        _ => 0,
1188    }
1189}
1190
1191fn canonical_nat(value: &str) -> bool {
1192    value == "0"
1193        || (!value.starts_with('0')
1194            && !value.is_empty()
1195            && value.bytes().all(|byte| byte.is_ascii_digit()))
1196}
1197
1198fn canonical_int(value: &str) -> bool {
1199    if let Some(magnitude) = value.strip_prefix('-') {
1200        magnitude != "0" && canonical_nat(magnitude)
1201    } else {
1202        canonical_nat(value)
1203    }
1204}
1205
1206fn canonical_hex(value: &str, length: usize) -> bool {
1207    value.len() == length
1208        && value
1209            .bytes()
1210            .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
1211}
1212
1213fn validate_principal(value: &str, path: &str) -> Result<(), HostValueValidationError> {
1214    let principal = ic_principal::Principal::from_text(value).map_err(|error| {
1215        single(
1216            "invalid_principal",
1217            path,
1218            format!("invalid principal {value:?}: {error}"),
1219        )
1220    })?;
1221    if principal.to_text() != value {
1222        return Err(single(
1223            "invalid_principal",
1224            path,
1225            format!("principal {value:?} is not in canonical textual form"),
1226        ));
1227    }
1228    Ok(())
1229}
1230
1231fn single(
1232    code: impl Into<String>,
1233    path: impl Into<String>,
1234    message: impl Into<String>,
1235) -> HostValueValidationError {
1236    HostValueValidationError {
1237        violations: vec![crate::Diagnostic::violation(code, path, message)],
1238    }
1239}
1240
1241fn resource_single(
1242    resource: &str,
1243    limit: usize,
1244    observed: usize,
1245    path: impl Into<String>,
1246    message: impl Into<String>,
1247) -> HostValueValidationError {
1248    HostValueValidationError {
1249        violations: vec![
1250            crate::Diagnostic::violation("resource_limit_exceeded", path, message)
1251                .with_resource_limit(crate::ResourceLimitInfo {
1252                    resource: resource.to_string(),
1253                    limit: crate::limits::portable_count(limit),
1254                    observed: crate::limits::portable_count(observed),
1255                }),
1256        ],
1257    }
1258}
1259
1260fn host_validation_budget_error(
1261    error: crate::budget::BudgetError,
1262    path: &str,
1263) -> HostValueValidationError {
1264    match error {
1265        crate::budget::BudgetError::Cancelled => single(
1266            "operation_cancelled",
1267            path,
1268            "HostValue validation was cancelled",
1269        ),
1270        crate::budget::BudgetError::DeadlineExceeded => single(
1271            "operation_deadline_exceeded",
1272            path,
1273            "HostValue validation deadline has elapsed",
1274        ),
1275        crate::budget::BudgetError::ResourceLimit {
1276            resource,
1277            limit,
1278            observed,
1279        } => resource_single(
1280            resource,
1281            limit,
1282            observed,
1283            path,
1284            format!("resource {resource} exceeded limit {limit}; observed {observed}"),
1285        ),
1286    }
1287}
1288
1289fn type_node_kind(node: &TypeNode) -> &'static str {
1290    match node {
1291        TypeNode::Primitive { .. } => "primitive",
1292        TypeNode::Opt { .. } => "opt",
1293        TypeNode::Vec { .. } => "vec",
1294        TypeNode::Record { .. } => "record",
1295        TypeNode::Variant { .. } => "variant",
1296        TypeNode::Func { .. } => "func",
1297        TypeNode::Service { .. } => "service",
1298        TypeNode::Class { .. } => "class",
1299    }
1300}
1301
1302fn host_value_kind(value: &HostValueKind) -> &'static str {
1303    match value {
1304        HostValueKind::Null => "null",
1305        HostValueKind::Bool { .. } => "bool",
1306        HostValueKind::Nat { .. } => "nat",
1307        HostValueKind::Int { .. } => "int",
1308        HostValueKind::Nat8 { .. } => "nat8",
1309        HostValueKind::Nat16 { .. } => "nat16",
1310        HostValueKind::Nat32 { .. } => "nat32",
1311        HostValueKind::Nat64 { .. } => "nat64",
1312        HostValueKind::Int8 { .. } => "int8",
1313        HostValueKind::Int16 { .. } => "int16",
1314        HostValueKind::Int32 { .. } => "int32",
1315        HostValueKind::Int64 { .. } => "int64",
1316        HostValueKind::Float32 { .. } => "float32",
1317        HostValueKind::Float64 { .. } => "float64",
1318        HostValueKind::Text { .. } => "text",
1319        HostValueKind::Reserved => "reserved",
1320        HostValueKind::Principal { .. } => "principal",
1321        HostValueKind::Opt { .. } => "opt",
1322        HostValueKind::Vec { .. } => "vec",
1323        HostValueKind::Record { .. } => "record",
1324        HostValueKind::Variant { .. } => "variant",
1325        HostValueKind::Service { .. } => "service",
1326        HostValueKind::Func { .. } => "func",
1327    }
1328}
1329
1330impl Contract {
1331    pub fn bind_type(
1332        &self,
1333        type_ref: TypeRef,
1334    ) -> Result<ContractTypeRef, HostValueValidationError> {
1335        if type_ref as usize >= self.types().len() {
1336            return Err(single(
1337                "value_type_ref_out_of_bounds",
1338                "$",
1339                format!("type reference {type_ref} is outside the Contract"),
1340            ));
1341        }
1342        Ok(ContractTypeRef {
1343            contract_id: self.contract_id().to_string(),
1344            type_ref,
1345        })
1346    }
1347
1348    pub fn bind_method(
1349        &self,
1350        method: impl Into<String>,
1351    ) -> Result<ContractMethodRef, HostValueValidationError> {
1352        let method = method.into();
1353        let service = match self.actor() {
1354            Some(crate::model::Actor::Service { service }) => *service,
1355            Some(crate::model::Actor::Class { class }) => match &self.types()[*class as usize] {
1356                TypeNode::Class { service, .. } => *service,
1357                _ => unreachable!("validated class actor targets a class"),
1358            },
1359            None => {
1360                return Err(single(
1361                    "actorless_contract",
1362                    "$",
1363                    "an actorless Contract has no methods",
1364                ));
1365            }
1366        };
1367        match &self.types()[service as usize] {
1368            TypeNode::Service { methods } if methods.iter().any(|entry| entry.name == method) => {
1369                Ok(ContractMethodRef {
1370                    contract_id: self.contract_id().to_string(),
1371                    method_name: method,
1372                })
1373            }
1374            _ => Err(single(
1375                "unknown_method",
1376                "$",
1377                format!("method {method:?} does not exist in the actor service"),
1378            )),
1379        }
1380    }
1381}