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}