Skip to main content

candid_core/
limits.rs

1use serde::{Deserialize, Serialize};
2use std::sync::{
3    atomic::{AtomicBool, Ordering},
4    Arc,
5};
6
7/// The version of the portable limits configuration schema this build reads
8/// and writes. See [`LimitsConfig`].
9pub const LIMITS_CONFIG_VERSION: u64 = 1;
10
11/// A named, versioned set of default operational limit values.
12///
13/// A profile freezes every default number behind a stable wire name, so a
14/// serialized configuration can say *which* defaults it started from instead
15/// of copying platform-dependent values. Existing profile values are never
16/// changed once released; new tunings become new profile variants. The enum is
17/// `#[non_exhaustive]` so adding a profile is not a breaking change.
18#[non_exhaustive]
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20pub enum LimitsProfile {
21    /// The interactive-tooling defaults this crate has always shipped:
22    /// safe for parsing and validating untrusted documents in an editor,
23    /// CLI, or agent context on an ordinary desktop host. The exact numbers
24    /// are frozen; see each [`Limits`] getter for the value and rationale.
25    InteractiveV1,
26}
27
28impl LimitsProfile {
29    /// The profile's frozen default limit values.
30    pub fn limits(self) -> Limits {
31        match self {
32            Self::InteractiveV1 => interactive_v1(),
33        }
34    }
35
36    /// The stable wire name used by [`LimitsConfig`].
37    pub fn wire_name(self) -> &'static str {
38        match self {
39            Self::InteractiveV1 => "interactive_v1",
40        }
41    }
42
43    fn from_wire_name(name: &str) -> Option<Self> {
44        match name {
45            "interactive_v1" => Some(Self::InteractiveV1),
46            _ => None,
47        }
48    }
49}
50
51/// Declares every numeric limit field exactly once, together with its
52/// builder name and `InteractiveV1` default, and derives the four surfaces
53/// that must never drift apart: the private struct fields, the public
54/// getters, the public `with_*` builders, and the portable override schema
55/// (its keys, its diff against the profile baseline, and its checked
56/// application). `deadline_unix_ms` is declared separately because it is the
57/// one field that is optional and already `u64` on the wire.
58macro_rules! limit_fields {
59    ($( $(#[$doc:meta])* $field:ident / $with:ident = $default:expr; )+) => {
60        /// Operational limits for work performed on untrusted Contracts,
61        /// sources, and values.
62        ///
63        /// Limits never participate in Contract identity. Hosts may raise
64        /// them explicitly for trusted workloads.
65        ///
66        /// # Construction
67        ///
68        /// Fields are private so adding a limit is never a breaking change.
69        /// Start from a profile and override individual fields with the
70        /// `with_*` builders:
71        ///
72        /// ```
73        /// use candid_core::{Limits, LimitsProfile};
74        ///
75        /// let limits = LimitsProfile::InteractiveV1
76        ///     .limits()
77        ///     .with_max_input_bytes(64 * 1024)
78        ///     .with_deadline_unix_ms(Some(2_000_000_000_000));
79        /// assert_eq!(limits.max_input_bytes(), 64 * 1024);
80        /// ```
81        ///
82        /// [`Limits::default`] is [`LimitsProfile::InteractiveV1`].
83        ///
84        /// # Zero values
85        ///
86        /// Every limit accepts `0`; a zero limit is a defined, fail-closed
87        /// policy rather than a rejected configuration. A zero byte/count/work
88        /// limit rejects any input that consumes the resource at all, and
89        /// `with_max_diagnostics(0)` retains exactly one out-of-band
90        /// `resource_limit_exceeded` sentinel violation so an invalid input
91        /// never yields an empty error collection. `deadline_unix_ms` values
92        /// at or before the current time (including `Some(0)`) make every
93        /// bounded operation fail closed with `operation_deadline_exceeded`.
94        ///
95        /// # Serialization
96        ///
97        /// `Limits` serializes as the versioned portable configuration
98        /// described by [`LimitsConfig`], never as a bare field map.
99        #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
100        #[serde(into = "LimitsConfig", try_from = "LimitsConfig")]
101        pub struct Limits {
102            pub(crate) profile: LimitsProfile,
103            $( pub(crate) $field: usize, )+
104            /// See [`Limits::deadline_unix_ms`].
105            pub(crate) deadline_unix_ms: Option<u64>,
106        }
107
108        impl Limits {
109            $(
110                $(#[$doc])*
111                #[must_use]
112                pub fn $field(&self) -> usize {
113                    self.$field
114                }
115            )+
116
117            $(
118                #[doc = concat!("Returns `self` with `", stringify!($field), "` replaced. See [`Limits::", stringify!($field), "`].")]
119                #[must_use]
120                pub fn $with(mut self, value: usize) -> Self {
121                    self.$field = value;
122                    self
123                }
124            )+
125        }
126
127        /// The explicit override values of a [`LimitsConfig`].
128        ///
129        /// Only overrides that differ from the named profile's frozen
130        /// defaults are serialized, and every value is a fixed-width `u64`
131        /// so the document means the same thing on every platform. An
132        /// explicit JSON `null` is rejected rather than read as "no
133        /// override": absence is the only spelling of "use the profile
134        /// value".
135        #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
136        #[serde(deny_unknown_fields)]
137        struct LimitsOverrides {
138            $(
139                #[serde(
140                    default,
141                    deserialize_with = "deserialize_override_forbidding_null",
142                    skip_serializing_if = "Option::is_none"
143                )]
144                $field: Option<u64>,
145            )+
146            #[serde(
147                default,
148                deserialize_with = "deserialize_override_forbidding_null",
149                skip_serializing_if = "Option::is_none"
150            )]
151            deadline_unix_ms: Option<u64>,
152        }
153
154        impl LimitsOverrides {
155            /// The overrides that reproduce `limits` from its profile's
156            /// frozen baseline. An override equal to the baseline value is
157            /// normalized away: the profile numbers are frozen, so omission
158            /// and an equal explicit value are the same policy forever.
159            fn diff(limits: &Limits) -> Self {
160                let baseline = limits.profile.limits();
161                Self {
162                    $( $field: (limits.$field != baseline.$field)
163                        .then(|| portable_count(limits.$field)), )+
164                    // No profile defines a default deadline, so "deadline
165                    // present and different" is the only representable
166                    // override; `apply` can only ever set one.
167                    deadline_unix_ms: match (limits.deadline_unix_ms, baseline.deadline_unix_ms) {
168                        (Some(deadline), baseline) if baseline != Some(deadline) => Some(deadline),
169                        _ => None,
170                    },
171                }
172            }
173
174            /// Applies the overrides to the profile baseline, converting each
175            /// `u64` into the platform word with an exact checked conversion.
176            fn apply(self, profile: LimitsProfile) -> Result<Limits, LimitsConfigError> {
177                let mut limits = profile.limits();
178                $(
179                    if let Some(value) = self.$field {
180                        limits.$field = override_to_usize(stringify!($field), value)?;
181                    }
182                )+
183                if let Some(value) = self.deadline_unix_ms {
184                    limits.deadline_unix_ms = Some(value);
185                }
186                Ok(limits)
187            }
188        }
189
190        /// The frozen `InteractiveV1` default values.
191        ///
192        /// Generated from the single per-field declaration above so the
193        /// profile numbers are defined in exactly one place and cannot drift
194        /// from the field list, getters, builders, or override schema.
195        fn interactive_v1() -> Limits {
196            Limits {
197                profile: LimitsProfile::InteractiveV1,
198                $( $field: $default, )+
199                deadline_unix_ms: None,
200            }
201        }
202    };
203}
204
205limit_fields! {
206    /// Maximum bytes accepted by a bounded parse entry point before the
207    /// document is decoded.
208    max_input_bytes / with_max_input_bytes = 4 * 1024 * 1024;
209    /// Maximum bytes of a single resolved DID source.
210    max_source_bytes / with_max_source_bytes = 1024 * 1024;
211    /// Maximum aggregate bytes across every source in a resolved bundle.
212    max_bundle_bytes / with_max_bundle_bytes = 8 * 1024 * 1024;
213    /// Maximum number of sources in a resolved bundle.
214    max_sources / with_max_sources = 256;
215    /// Maximum bytes in a single logical source ID (name/path).
216    ///
217    /// Source IDs are otherwise bounded only cumulatively by
218    /// [`Limits::max_string_bytes`], so one entry could carry a megabyte-long
219    /// path. This bounds each ID individually on both the resolver and the
220    /// embedded-sidecar paths.
221    max_source_id_bytes / with_max_source_id_bytes = 1024;
222    /// Maximum import chain depth during source resolution.
223    max_import_depth / with_max_import_depth = 64;
224    /// Maximum import edges across a resolved bundle.
225    max_import_edges / with_max_import_edges = 1024;
226    /// Maximum lexical nesting accepted before invoking the upstream parser.
227    max_source_nesting / with_max_source_nesting = 256;
228    /// Maximum semantic type nesting lowered from a checked Candid program.
229    max_type_depth / with_max_type_depth = 256;
230    /// Maximum lexical JSON container nesting accepted before invoking the
231    /// recursive `serde_json` decoder on a HostValue document.
232    ///
233    /// This is the HostValue analogue of [`Limits::max_source_nesting`]: it
234    /// bounds *lexical* nesting (`{` and `[` in the JSON text) before a
235    /// recursive decoder runs, whereas [`Limits::max_value_depth`] bounds
236    /// *semantic* HostValue nesting after decoding. The two units differ — one
237    /// `vec` level costs two JSON containers and one `record` level costs
238    /// three — so a document rejected here reports `value_nesting`, never
239    /// `value_depth`.
240    ///
241    /// Raising this above 128 has no effect: `serde_json` applies a fixed
242    /// 128-frame recursion ceiling that this crate deliberately does not
243    /// disable, so documents nested deeper than 128 containers are rejected by
244    /// the decoder as malformed rather than by this limit.
245    ///
246    /// # Choosing a value for a small stack
247    ///
248    /// Rejecting a document costs constant stack, so no input can drive an
249    /// abort by being *deeper* than this limit. Accepting one still recurses,
250    /// at a cost that is build-profile dependent, so this limit is the knob for
251    /// matching decode to the stack the host actually runs on. Measured on a
252    /// 64 KiB stack with a nested-`opt` document:
253    ///
254    /// | Profile | Cost per container | Deepest safe |
255    /// |---|---|---|
256    /// | release | ~640 B | ~103 |
257    /// | debug | ~8 KiB | ~7 |
258    ///
259    /// The default of 64 is chosen to keep a release build inside a 64 KiB
260    /// stack with roughly a third of it to spare, which is the bar
261    /// `tests/deep_nesting.rs` sets. A debug build on a stack that small needs
262    /// this lowered to single digits; a host on an ordinary 8 MiB stack can
263    /// raise it to 128 without approaching either bound.
264    //
265    // Deliberately below `serde_json`'s fixed 128-frame ceiling so this
266    // crate's own check is always the one that fires, and low enough that
267    // decoding a document at exactly this bound stays well inside the 64 KiB
268    // stack `tests/deep_nesting.rs` asserts elsewhere.
269    max_value_nesting / with_max_value_nesting = 64;
270    /// Maximum type nodes in a Contract arena.
271    max_type_nodes / with_max_type_nodes = 100_000;
272    /// Maximum edges in a Contract type graph.
273    max_graph_edges / with_max_graph_edges = 1_000_000;
274    /// Maximum named declarations in a Contract.
275    max_declarations / with_max_declarations = 100_000;
276    /// Maximum aggregate record/variant fields across a Contract.
277    ///
278    /// This bounds a *Contract*. It is not the ceiling on how wide a record
279    /// `validate_host_value` will accept: that is governed by
280    /// [`Limits::max_canonicalization_work`], which at its default binds a
281    /// single record at 2 581 fields — far below the 500 000 permitted here.
282    max_fields / with_max_fields = 500_000;
283    /// Maximum aggregate service methods across a Contract.
284    max_methods / with_max_methods = 100_000;
285    /// Maximum aggregate function arguments and results across a Contract.
286    max_function_values / with_max_function_values = 500_000;
287    /// Maximum aggregate string bytes across declaration and method names.
288    max_string_bytes / with_max_string_bytes = 1024 * 1024;
289    /// Maximum aggregate bytes across the four producer metadata strings.
290    ///
291    /// Producer metadata is untrusted, caller-supplied provenance that is
292    /// deliberately kept out of the semantic Contract identities (see
293    /// [`crate::ProducerInfo`]); this bounds the bytes it may contribute to a
294    /// validated Contract without ever affecting a semantic identity hash.
295    max_producer_bytes / with_max_producer_bytes = 4096;
296    /// Maximum retained diagnostics per failure.
297    ///
298    /// When more violations are observed than fit under this cap, the final
299    /// retained item is replaced by a `resource_limit_exceeded` sentinel
300    /// carrying the true observed count. A cap of `0` retains exactly that
301    /// one sentinel, so an invalid input never yields an empty error
302    /// collection.
303    max_diagnostics / with_max_diagnostics = 100;
304    /// Maximum canonicalization work units per operation.
305    ///
306    /// This counter, not [`Limits::max_fields`] or
307    /// [`Limits::max_value_elements`], is what bounds how wide a record
308    /// `validate_host_value` can accept. Record validation is
309    /// deliberately allocation-free: instead of building a field-ID index it
310    /// scans pairwise and charges one unit per comparison, checking the
311    /// deadline before each charge. Three such scans run per record — duplicate
312    /// detection, field-set agreement, and per-field lookup — so the cost is
313    /// roughly `1.5n²` units for an `n`-field record.
314    ///
315    /// At this default that puts the ceiling at **2 581 fields**: a record with
316    /// 2 582 fields fails closed with `resource_limit_exceeded` naming
317    /// `canonicalization_work`. That is three orders of magnitude below the
318    /// 500 000 fields [`Limits::max_fields`] permits and the 1 000 000 elements
319    /// [`Limits::max_value_elements`] permits, so those two are not the binding
320    /// limit for wide records. Raise this counter to validate wider ones, and
321    /// note the cost grows quadratically. The failure is structured and
322    /// interruptible, never a hang.
323    ///
324    /// The counter is per operation and shared with Contract canonicalization,
325    /// so several moderately wide records in one value tree accumulate against
326    /// the same budget.
327    max_canonicalization_work / with_max_canonicalization_work = 10_000_000;
328    /// Maximum work units charged while resolving provenance targets.
329    ///
330    /// Kept separate from [`Limits::max_canonicalization_work`] so that
331    /// rederiving a large graph and then indexing its provenance sidecar cannot
332    /// jointly exhaust one counter. Bounds building each referenced container's
333    /// field-ID / method-name index and every membership test, so adversarial
334    /// fan-out and duplicate provenance entries cannot drive an unbounded scan.
335    max_provenance_work / with_max_provenance_work = 10_000_000;
336    /// Maximum work units charged while serializing and hashing source-bundle
337    /// identity (`candid-core:source-bundle:v1`).
338    ///
339    /// Each identity computation charges one unit per serialized payload byte
340    /// during an allocation-free counting pass, then reserves two more units
341    /// per byte (materializing and hashing the canonical bytes) plus the
342    /// domain-tag overhead before any allocation occurs. A presented sidecar
343    /// validation performs two passes on one budget — verifying the presented
344    /// `source_bundle_id` and emitting the rederived bundle's ID — while a
345    /// plain compilation performs one.
346    ///
347    /// Kept separate from [`Limits::max_canonicalization_work`] because the
348    /// serialized bundle scales with `max_bundle_bytes`: metering it on the
349    /// canonicalization counter would either starve graph work or force that
350    /// default far above what graph canonicalization needs. The default
351    /// accepts every bundle valid under the default byte/count limits: JSON
352    /// string escaping expands a byte to at most six, so one pass costs at
353    /// most `3 * 6 * (max_bundle_bytes + identity strings) + entry overhead`,
354    /// about 213M units for a compile pass and 341M for the two validation
355    /// passes together; 400M covers both with headroom.
356    max_source_identity_work / with_max_source_identity_work = 400_000_000;
357    /// Maximum work units charged while computing a detached artifact identity
358    /// (`candid-core:artifact:*`; see [`crate::artifact_id_with_limits`]).
359    ///
360    /// One unit per artifact byte plus the fixed domain framing cost — the
361    /// domain tag and its one separator byte. There is no length field and no
362    /// second kind label in the preimage, so that constant is the whole
363    /// overhead, and the cost is exactly
364    /// `bytes.len() + domain.len() + 1`.
365    ///
366    /// The default is proven against the default byte gate rather than guessed.
367    /// [`Limits::max_input_bytes`] is enforced first, so the largest artifact
368    /// this ever hashes by default is 4 MiB (4 194 304 bytes); the longest of
369    /// the frozen domains,
370    /// `candid-core:artifact:contract-envelope-json:v1`, is 46 bytes, so the
371    /// worst case is 4 194 351 units. 10 000 000 covers that with more than
372    /// twice the headroom, matches
373    /// [`Limits::max_canonicalization_work`]'s default, and cannot overflow:
374    /// every charge saturates rather than wrapping.
375    ///
376    /// Kept separate from every other work counter on purpose. Artifact
377    /// identity is an explicit, detached call, so metering it on
378    /// `canonicalization_work` would let content-addressing a document starve
379    /// the Contract canonicalization that follows it on the same budget, and
380    /// metering it on `source_identity_work` would do the same to provenance.
381    /// Raising [`Limits::max_input_bytes`] above this value without raising
382    /// this one makes over-sized artifacts fail on `artifact_identity_work`
383    /// instead of on `input_bytes`; raise both together.
384    ///
385    /// # Wire compatibility
386    ///
387    /// This override key is additive. [`Limits::default`] and every
388    /// configuration that leaves this limit at its profile value still
389    /// serialize with no `max_artifact_identity_work` key at all, so existing
390    /// documents are unchanged in both directions. The implication is
391    /// one-directional and deliberate: because [`LimitsConfig`] rejects unknown
392    /// override keys, a document that *does* carry an explicit
393    /// `max_artifact_identity_work` override is readable by this build and
394    /// newer ones but rejected by a build that predates the key. That is the
395    /// intended failure — silently ignoring an unknown limit override would
396    /// apply a policy the document did not ask for. [`LIMITS_CONFIG_VERSION`]
397    /// is unchanged, because adding a limit is not a schema break: `Limits`
398    /// fields are private precisely so that adding one is never a breaking
399    /// change, and no existing key, value, or default moved.
400    max_artifact_identity_work / with_max_artifact_identity_work = 10_000_000;
401    /// Maximum work units charged while enforcing [`Limits::max_type_depth`].
402    ///
403    /// Two iterative traversals guard type depth before anything recursive
404    /// expands a parsed bundle: the parse-side preflight that follows
405    /// declaration references before the upstream checker runs, and the
406    /// checked-type walk that re-verifies the checker's environment before
407    /// lowering. Both walks — and the recursion map each builds first (the
408    /// declaration reference graph and its cycle members) — charge this
409    /// counter: one unit per visited syntax node or expansion state, plus one
410    /// unit per recursive name tracked on the state's path, which is the real
411    /// cost of cloning and comparing that path set.
412    ///
413    /// Shared subtrees are deduplicated: each node is expanded once per
414    /// distinct `(node, depth, active recursive names)` state rather than
415    /// once per referencing path, and only names that participate in a
416    /// reference cycle are ever tracked per path, so ordinary recursive
417    /// types add a small constant per state. The record DAG that motivated
418    /// this counter (issue #125: shared aliases re-expanded once per
419    /// incoming edge, O(2^n) visits from a 932-byte source) costs 2 796
420    /// units at n = 24 under deduplication — measured, and pinned together
421    /// with a minimal program's exact cost by `tests/deep_nesting.rs` and
422    /// the unit tests beside the walks. Shapes that still multiply states —
423    /// deep stacks of distinct-name diamonds, enormous mutual-recursion
424    /// groups, long structural alias chains re-walked from every
425    /// declaration root at shifted depths — exhaust this counter and fail
426    /// closed with `resource_limit_exceeded` naming `type_preflight_work`,
427    /// instead of hanging.
428    ///
429    /// Kept separate from [`Limits::max_canonicalization_work`] because both
430    /// counters accrue on one budget in a single compilation: metering the
431    /// depth guard on the graph counter would let a type-heavy bundle starve
432    /// the canonicalization that follows it on the same budget. The default
433    /// matches that counter's and covers realistic contracts by several
434    /// orders of magnitude: the largest corpus fixture (`ledger.did`)
435    /// consumes 806 units end to end, pinned by `tests/deep_nesting.rs`.
436    ///
437    /// # Choosing a value for a small heap
438    ///
439    /// Deduplication is what makes these walks cheap, and it is also what
440    /// makes them *retain*: each distinct state stays in a memo for the
441    /// duration of the walk, where the previous tree walks held only a stack.
442    /// So this counter bounds memory as well as time, and the exchange rate
443    /// is measured rather than assumed: **about 19 bytes of peak live heap
444    /// per unit**, near-constant across bundle sizes and pinned by
445    /// `tests/type_preflight_memory.rs`.
446    ///
447    /// At this default that authorizes roughly 190 MB of peak heap before
448    /// the counter refuses — ample headroom on an ordinary host, and more
449    /// than a small 32-bit `wasm32` heap can serve. A host whose allocator
450    /// fails before the counter does gets an allocation abort in place of
451    /// the clean, structured `resource_limit_exceeded` this is designed to
452    /// return, so a browser or other constrained host should lower this to
453    /// what its heap can actually hold: 1 000 000 keeps the walks under
454    /// ~19 MB and still clears every realistic contract by three orders of
455    /// magnitude. Rejecting costs no memory, so no input can drive an abort
456    /// by being *larger* than the configured bound — only by being accepted
457    /// under a bound the host cannot honor.
458    ///
459    /// The rate holds because a state's cost does not depend on anything an
460    /// attacker picks freely: recursive names are interned to dense indices
461    /// rather than compared or stored as text, and each path's set is shared
462    /// with the children that inherit it unchanged rather than copied into
463    /// every one of a wide record's fields.
464    ///
465    /// # Wire compatibility
466    ///
467    /// This override key is additive, exactly like
468    /// [`Limits::max_artifact_identity_work`]'s: [`Limits::default`] and
469    /// every configuration that leaves this limit at its profile value still
470    /// serialize with no `max_type_preflight_work` key at all, so existing
471    /// documents are unchanged in both directions, while a document that does
472    /// carry the key is readable by this build and newer ones but rejected by
473    /// a build that predates it — the intended failure, since silently
474    /// ignoring an unknown limit override would apply a policy the document
475    /// did not ask for. [`LIMITS_CONFIG_VERSION`] is unchanged: `Limits`
476    /// fields are private precisely so that adding one is never a breaking
477    /// change, and no existing key, value, or default moved.
478    max_type_preflight_work / with_max_type_preflight_work = 10_000_000;
479    /// Maximum semantic HostValue nesting depth.
480    max_value_depth / with_max_value_depth = 256;
481    /// Maximum aggregate HostValue elements per document.
482    ///
483    /// Reaching this ceiling with a single wide *record* is not possible at
484    /// default limits: [`Limits::max_canonicalization_work`] binds one record
485    /// at 2 581 fields, well before the million elements permitted here. This
486    /// limit is the binding one for wide vectors and for element counts
487    /// accumulated across a value tree.
488    max_value_elements / with_max_value_elements = 1_000_000;
489    /// Maximum aggregate HostValue text/blob bytes per document.
490    max_value_bytes / with_max_value_bytes = 16 * 1024 * 1024;
491}
492
493impl Default for Limits {
494    /// [`LimitsProfile::InteractiveV1`].
495    fn default() -> Self {
496        LimitsProfile::InteractiveV1.limits()
497    }
498}
499
500impl Limits {
501    /// The profile these limits started from. Overridden fields do not
502    /// change the profile; it names the baseline, not the final values.
503    pub fn profile(&self) -> LimitsProfile {
504        self.profile
505    }
506
507    /// Optional Unix timestamp in milliseconds after which work must abort.
508    ///
509    /// `None` means no deadline. A value at or before the current time makes
510    /// every bounded operation fail closed with
511    /// `operation_deadline_exceeded` before performing work.
512    ///
513    /// Bare `wasm32-unknown-unknown` has no clock, so a deadline cannot be
514    /// measured there: *any* explicit deadline fails closed on that target,
515    /// while `None` stays unbounded. Cancellation and every quantitative limit
516    /// are unaffected. See [`Limits::deadline_exceeded`].
517    pub fn deadline_unix_ms(&self) -> Option<u64> {
518        self.deadline_unix_ms
519    }
520
521    /// Returns `self` with `deadline_unix_ms` replaced. See
522    /// [`Limits::deadline_unix_ms`].
523    #[must_use]
524    pub fn with_deadline_unix_ms(mut self, deadline_unix_ms: Option<u64>) -> Self {
525        self.deadline_unix_ms = deadline_unix_ms;
526        self
527    }
528
529    /// Whether the configured deadline has already elapsed.
530    ///
531    /// No deadline is never exceeded. On bare `wasm32-unknown-unknown` there
532    /// is no clock to compare against — `SystemTime::now` panics rather than
533    /// failing — so an explicit deadline is reported as elapsed instead of
534    /// aborting the process. That is the same fail-closed direction the
535    /// internal budget takes, and it keeps an explicit deadline from silently
536    /// becoming unbounded on the one target that cannot honour it.
537    pub fn deadline_exceeded(&self) -> bool {
538        let Some(deadline) = self.deadline_unix_ms else {
539            return false;
540        };
541        #[cfg(target_os = "unknown")]
542        {
543            let _ = deadline;
544            true
545        }
546        #[cfg(not(target_os = "unknown"))]
547        {
548            let now = std::time::SystemTime::now()
549                .duration_since(std::time::UNIX_EPOCH)
550                .map_or(u64::MAX, |duration| {
551                    u64::try_from(duration.as_millis()).unwrap_or(u64::MAX)
552                });
553            now >= deadline
554        }
555    }
556}
557
558/// The versioned, portable wire form of [`Limits`].
559///
560/// ```json
561/// {"version":1,"profile":"interactive_v1","overrides":{}}
562/// ```
563///
564/// That document is exactly how [`Limits::default`] serializes. `version`
565/// pins the schema ([`LIMITS_CONFIG_VERSION`]), `profile` names the frozen
566/// baseline defaults ([`LimitsProfile::wire_name`]), and `overrides` carries
567/// only the explicitly overridden fields as fixed-width `u64` values, so the
568/// same document configures identical policy on every supported (32- and
569/// 64-bit) host.
570/// Unknown top-level fields, unknown override fields, unsupported versions,
571/// and unknown profiles are all rejected; an override that does not fit the
572/// host platform's `usize` is rejected with a structured
573/// [`LimitsConfigError`] rather than truncated or wrapped. A missing
574/// `overrides` object means no overrides.
575///
576/// This type is the serde representation behind `Limits`'
577/// [`Serialize`]/[`Deserialize`] impls; convert explicitly with
578/// [`From<&Limits>`] and [`TryFrom<LimitsConfig>`] when the structured
579/// [`LimitsConfigError`] must be inspected programmatically instead of
580/// wrapped in a serde error string.
581#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
582#[serde(deny_unknown_fields)]
583pub struct LimitsConfig {
584    version: u64,
585    profile: String,
586    #[serde(default)]
587    overrides: LimitsOverrides,
588}
589
590impl From<&Limits> for LimitsConfig {
591    fn from(limits: &Limits) -> Self {
592        Self {
593            version: LIMITS_CONFIG_VERSION,
594            profile: limits.profile.wire_name().to_string(),
595            overrides: LimitsOverrides::diff(limits),
596        }
597    }
598}
599
600impl From<Limits> for LimitsConfig {
601    fn from(limits: Limits) -> Self {
602        Self::from(&limits)
603    }
604}
605
606impl TryFrom<LimitsConfig> for Limits {
607    type Error = LimitsConfigError;
608
609    fn try_from(config: LimitsConfig) -> Result<Self, LimitsConfigError> {
610        if config.version != LIMITS_CONFIG_VERSION {
611            return Err(LimitsConfigError {
612                code: "unsupported_limits_version",
613                path: "$.version".to_string(),
614                message: format!(
615                    "unsupported limits config version {}; this build supports version {LIMITS_CONFIG_VERSION}",
616                    config.version
617                ),
618            });
619        }
620        let profile =
621            LimitsProfile::from_wire_name(&config.profile).ok_or_else(|| LimitsConfigError {
622                code: "unsupported_limits_profile",
623                path: "$.profile".to_string(),
624                message: format!(
625                    "unknown limits profile {:?}; known profiles: \"interactive_v1\"",
626                    config.profile
627                ),
628            })?;
629        config.overrides.apply(profile)
630    }
631}
632
633/// A structured, stable rejection of a portable limits configuration.
634///
635/// Carried by [`TryFrom<LimitsConfig>`] and wrapped (via [`std::fmt::Display`])
636/// by serde when a `Limits` or `RuntimeContext` document is rejected during
637/// deserialization. The `code`, `path`, and rendered message are pinned
638/// public API:
639///
640/// | code | path | condition |
641/// |---|---|---|
642/// | `unsupported_limits_version` | `$.version` | version is not `1` |
643/// | `unsupported_limits_profile` | `$.profile` | profile name is unknown |
644/// | `limit_override_unrepresentable` | `$.overrides.<field>` | override exceeds this platform's `usize::MAX` |
645///
646/// Displays as `{code} at {path}: {message}`.
647#[derive(Debug, Clone, PartialEq, Eq)]
648pub struct LimitsConfigError {
649    code: &'static str,
650    path: String,
651    message: String,
652}
653
654impl LimitsConfigError {
655    /// The stable machine-readable code.
656    pub fn code(&self) -> &str {
657        self.code
658    }
659
660    /// The JSON path of the rejected value within the configuration document.
661    pub fn path(&self) -> &str {
662        &self.path
663    }
664
665    /// The human-readable description.
666    pub fn message(&self) -> &str {
667        &self.message
668    }
669}
670
671impl std::fmt::Display for LimitsConfigError {
672    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
673        write!(
674            formatter,
675            "{} at {}: {}",
676            self.code, self.path, self.message
677        )
678    }
679}
680
681impl std::error::Error for LimitsConfigError {}
682
683/// Invoked only when an override key is present; an absent key takes the
684/// `None` default. Delegating to `u64` directly makes an explicit JSON `null`
685/// a decode error instead of a second spelling of "no override", mirroring
686/// how [`crate::RawContract`] rejects `"actor": null`.
687fn deserialize_override_forbidding_null<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error>
688where
689    D: serde::Deserializer<'de>,
690{
691    u64::deserialize(deserializer).map(Some)
692}
693
694/// Exact `usize` → `u64` widening for portable wire values.
695///
696/// Lossless by construction: the crate refuses to compile on targets whose
697/// `usize` exceeds 64 bits (see the `target_pointer_width` guard in
698/// `lib.rs`), so this cast is exact on every supported (32- and 64-bit)
699/// target.
700pub(crate) fn portable_count(value: usize) -> u64 {
701    value as u64
702}
703
704/// Whether a portable `u64` value fits a platform word whose maximum is
705/// `platform_max`. Factored out of [`override_to_usize`] so the 32-bit
706/// boundary is testable on any host by passing `u32::MAX as u64`.
707fn representable(value: u64, platform_max: u64) -> bool {
708    value <= platform_max
709}
710
711/// Checked `u64` → `usize` narrowing at the configuration boundary. Never
712/// truncates, wraps, or panics: a value the platform cannot represent is a
713/// structured [`LimitsConfigError`] at `$.overrides.<field>`.
714fn override_to_usize(field: &'static str, value: u64) -> Result<usize, LimitsConfigError> {
715    if !representable(value, portable_count(usize::MAX)) {
716        return Err(LimitsConfigError {
717            code: "limit_override_unrepresentable",
718            path: format!("$.overrides.{field}"),
719            message: format!(
720                "{field} override {value} exceeds this platform's usize::MAX ({})",
721                usize::MAX
722            ),
723        });
724    }
725    // Exact: `value <= usize::MAX` was just checked.
726    Ok(value as usize)
727}
728
729/// A cheap, cloneable signal for cooperatively cancelling runtime work.
730#[derive(Clone, Default)]
731pub struct CancellationToken {
732    cancelled: Arc<AtomicBool>,
733}
734
735impl CancellationToken {
736    pub fn new() -> Self {
737        Self::default()
738    }
739
740    pub fn cancel(&self) {
741        self.cancelled.store(true, Ordering::Release);
742    }
743
744    pub fn is_cancelled(&self) -> bool {
745        self.cancelled.load(Ordering::Acquire)
746    }
747}
748
749impl std::fmt::Debug for CancellationToken {
750    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
751        formatter
752            .debug_struct("CancellationToken")
753            .field("cancelled", &self.is_cancelled())
754            .finish()
755    }
756}
757
758impl PartialEq for CancellationToken {
759    fn eq(&self, other: &Self) -> bool {
760        self.is_cancelled() == other.is_cancelled()
761    }
762}
763
764impl Eq for CancellationToken {}
765
766/// Runtime policy and cooperative controls for one public operation.
767///
768/// Construct contexts with [`RuntimeContext::new`]; runtime controls are
769/// intentionally private so adding one does not reopen exhaustive literals.
770///
771/// ```compile_fail
772/// use candid_core::{Limits, RuntimeContext};
773///
774/// let _ = RuntimeContext { limits: Limits::default() };
775/// ```
776///
777/// Serializes as `{"limits": <portable limits config>}` (see
778/// [`LimitsConfig`]); the cancellation token is host-local bookkeeping and is
779/// never serialized.
780#[derive(Debug, Clone, Serialize, Deserialize)]
781#[serde(deny_unknown_fields)]
782pub struct RuntimeContext {
783    pub limits: Limits,
784    #[serde(skip, default)]
785    cancellation: CancellationToken,
786}
787
788impl PartialEq for RuntimeContext {
789    fn eq(&self, other: &Self) -> bool {
790        self.limits == other.limits
791    }
792}
793
794impl Eq for RuntimeContext {}
795
796impl RuntimeContext {
797    pub fn new(limits: Limits) -> Self {
798        Self {
799            limits,
800            cancellation: CancellationToken::new(),
801        }
802    }
803
804    pub fn with_cancellation(mut self, cancellation: CancellationToken) -> Self {
805        self.cancellation = cancellation;
806        self
807    }
808
809    pub fn cancellation_token(&self) -> CancellationToken {
810        self.cancellation.clone()
811    }
812
813    pub(crate) fn budget(&self) -> crate::budget::Budget<'_> {
814        crate::budget::Budget::new(&self.limits, self.cancellation.clone())
815    }
816}
817
818impl Default for RuntimeContext {
819    fn default() -> Self {
820        Self::new(Limits::default())
821    }
822}
823
824#[cfg(test)]
825mod tests {
826    use super::*;
827
828    #[test]
829    fn representable_simulates_the_32_bit_boundary_exactly() {
830        let simulated_32_bit_max = portable_count(u32::MAX as usize);
831        assert!(representable(u32::MAX as u64, simulated_32_bit_max));
832        assert!(!representable(u32::MAX as u64 + 1, simulated_32_bit_max));
833        assert!(!representable(u64::MAX, simulated_32_bit_max));
834        assert!(representable(0, simulated_32_bit_max));
835    }
836
837    #[test]
838    fn override_conversion_is_exact_or_a_structured_error() {
839        assert_eq!(override_to_usize("max_input_bytes", 0), Ok(0));
840        assert_eq!(
841            override_to_usize("max_input_bytes", portable_count(usize::MAX)),
842            Ok(usize::MAX)
843        );
844        #[cfg(target_pointer_width = "64")]
845        {
846            // On a 64-bit host every u64 is representable, so the error path
847            // is exercised through `representable` above and the pinned error
848            // shape is constructed directly here.
849            let error = LimitsConfigError {
850                code: "limit_override_unrepresentable",
851                path: "$.overrides.max_input_bytes".to_string(),
852                message: "max_input_bytes override 5000000000 exceeds this platform's usize::MAX (4294967295)".to_string(),
853            };
854            assert_eq!(error.code(), "limit_override_unrepresentable");
855            assert_eq!(error.path(), "$.overrides.max_input_bytes");
856        }
857        #[cfg(not(target_pointer_width = "64"))]
858        {
859            let error = override_to_usize("max_input_bytes", u64::MAX).unwrap_err();
860            assert_eq!(error.code(), "limit_override_unrepresentable");
861            assert_eq!(error.path(), "$.overrides.max_input_bytes");
862        }
863    }
864
865    #[test]
866    fn overrides_diff_and_apply_round_trip() {
867        let limits = Limits::default()
868            .with_max_input_bytes(1)
869            .with_max_diagnostics(0)
870            .with_deadline_unix_ms(Some(7));
871        let config = LimitsConfig::from(&limits);
872        assert_eq!(Limits::try_from(config), Ok(limits));
873
874        let untouched = Limits::default();
875        assert_eq!(
876            LimitsOverrides::diff(&untouched),
877            LimitsOverrides::default()
878        );
879    }
880
881    #[test]
882    fn an_override_equal_to_the_profile_value_is_normalized_away() {
883        let baseline = Limits::default();
884        let explicit = Limits::default().with_max_input_bytes(baseline.max_input_bytes());
885        assert_eq!(LimitsOverrides::diff(&explicit), LimitsOverrides::default());
886        assert_eq!(explicit, baseline);
887    }
888
889    #[test]
890    fn profile_wire_names_round_trip() {
891        assert_eq!(LimitsProfile::InteractiveV1.wire_name(), "interactive_v1");
892        assert_eq!(
893            LimitsProfile::from_wire_name("interactive_v1"),
894            Some(LimitsProfile::InteractiveV1)
895        );
896        assert_eq!(LimitsProfile::from_wire_name("interactive-v1"), None);
897        assert_eq!(LimitsProfile::from_wire_name("server_v1"), None);
898    }
899}