Skip to main content

candid_core/model/
contract.rs

1use super::type_graph::{Actor, Declaration, TypeNode, TypeRef};
2use super::validation_error::{ContractJsonError, ContractValidationError};
3use crate::limits::Limits;
4use serde::{Deserialize, Serialize, Serializer};
5
6pub const CONTRACT_FORMAT: &str = "candid-core";
7pub const FORMAT_VERSION: u32 = 1;
8pub const SEMANTICS_PROFILE: &str = "candid-1";
9pub const CANONICALIZATION_PROFILE: &str = "candid-core-canon-1";
10const PACKAGE_MANIFEST: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/Cargo.toml"));
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(deny_unknown_fields)]
14pub struct ContractIdentities {
15    pub contract: String,
16    #[serde(default, skip_serializing_if = "Option::is_none")]
17    pub interface: Option<String>,
18}
19
20/// Untrusted, caller-supplied provenance about the tool that produced a
21/// Contract.
22///
23/// Producer metadata is deliberately **outside the semantic Contract
24/// identities**. The `candid-core:contract:v1` and `candid-core:interface:v1`
25/// hashes (see `docs/canonicalization-v1.md`) are computed over the type
26/// graph, declarations, actor, and format/profile markers only — never over
27/// `producer`. Two Contracts that differ only in their producer therefore
28/// share the same `contract_id` and `interface_id`, even though they are
29/// byte-different on the wire (producer *is* part of the canonical serialized
30/// JSON — and identical in every feature configuration). No unkeyed content ID
31/// authenticates itself, so neither identity authenticates these claims; a
32/// signature over one commits to the semantic Contract and callers must treat
33/// producer metadata as unverified.
34///
35/// A caller that must commit to the producer bytes it actually received commits
36/// to a detached
37/// [`artifact_id_with_limits`](crate::artifact_id_with_limits) instead, and what
38/// that binds depends on the [`ArtifactKind`](crate::ArtifactKind) named:
39/// `ContractJsonV1` covers the Contract document's octets, `producer` included
40/// and nothing more, while the envelope and compilation kinds cover those octets
41/// plus extensions or the provenance sidecar respectively. The semantic
42/// identities exclude producer metadata under every kind.
43///
44/// This boundary is load-bearing for compatibility: binding `producer` into the
45/// identity payload would change every existing `contract_id`. The bytes are
46/// still bounded — see [`crate::Limits::max_producer_bytes`] — so untrusted
47/// producer strings cannot grow without limit; they simply never influence an
48/// identity.
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
50#[serde(deny_unknown_fields)]
51pub struct ProducerInfo {
52    pub name: String,
53    pub version: String,
54    pub candid_version: String,
55    pub candid_parser_version: String,
56}
57
58impl ProducerInfo {
59    /// The producer metadata describing this build of `candid-core` itself:
60    /// the crate name and version plus the exact pinned `candid` and
61    /// `candid_parser` dependency versions. This is the producer a
62    /// [`ContractDraft`] builds with when none is supplied explicitly.
63    ///
64    /// The four fields are present and identical in every feature
65    /// configuration. `candid` and `candid_parser` are optional dependencies
66    /// enabled by the `compiler` feature, but the versions reported here are
67    /// read out of this package's own manifest text at compile time, not from
68    /// a linked crate, so a `default-features = false` build of a given
69    /// `candid-core` version reports exactly the same producer bytes as a full
70    /// one. That is deliberate: producer metadata is untrusted provenance
71    /// about *this package*, and making it vary by feature would fork the
72    /// serialized shape of otherwise identical Contracts.
73    pub fn current() -> Self {
74        Self {
75            name: env!("CARGO_PKG_NAME").to_string(),
76            version: env!("CARGO_PKG_VERSION").to_string(),
77            candid_version: exact_dependency_version(PACKAGE_MANIFEST, "candid"),
78            candid_parser_version: exact_dependency_version(PACKAGE_MANIFEST, "candid_parser"),
79        }
80    }
81}
82
83fn exact_dependency_version(manifest: &str, dependency: &str) -> String {
84    manifest_dependency_version(manifest, dependency).unwrap_or_else(|| {
85        panic!("{dependency} must be declared as an exact dependency in Cargo.toml")
86    })
87}
88
89/// Find `dependency`'s pinned version in a Cargo manifest, in every spelling
90/// this crate's manifest can legitimately take.
91///
92/// Three forms have to be accepted, and the reason is not stylistic:
93///
94/// * `dep = "=X.Y.Z"` — the plain form;
95/// * `dep = { version = "=X.Y.Z", optional = true }` — the form an *optional*
96///   dependency is required to use, which is what `candid`/`candid_parser`
97///   became when they moved behind the `compiler` feature;
98/// * a `[dependencies.dep]` section with a `version = "=X.Y.Z"` line — which
99///   is what **Cargo itself writes** when it normalizes a manifest for
100///   publishing. A build from crates.io reads that normalized file through
101///   `CARGO_MANIFEST_DIR`, not the manifest in this repository, so a reader
102///   that only understood the first two forms would panic inside
103///   [`ProducerInfo::current`] in exactly the builds that matter most.
104///
105/// The scan is section-aware so `[dev-dependencies.candid_parser]` can never be
106/// mistaken for the real dependency, and a non-exact requirement still yields
107/// `None`: the pin is what makes the reported version meaningful.
108fn manifest_dependency_version(manifest: &str, dependency: &str) -> Option<String> {
109    let inline_prefix = format!("{dependency} = ");
110    let section_header = format!("[dependencies.{dependency}]");
111    let mut in_dependencies = false;
112    let mut in_dependency_section = false;
113
114    for line in manifest.lines().map(str::trim) {
115        if line.starts_with('[') {
116            in_dependencies = line == "[dependencies]";
117            in_dependency_section = line == section_header;
118        } else if in_dependencies {
119            if let Some(version) = line
120                .strip_prefix(&inline_prefix)
121                .and_then(exact_version_literal)
122            {
123                return Some(version.to_string());
124            }
125        } else if in_dependency_section {
126            if let Some(value) = line.strip_prefix("version = ") {
127                return exact_version_literal(value).map(str::to_string);
128            }
129        }
130    }
131    None
132}
133
134/// Read the `=X.Y.Z` text out of a value that is either a bare string literal
135/// or an inline table containing a `version` key.
136fn exact_version_literal(declaration: &str) -> Option<&str> {
137    let literal = match declaration.strip_prefix('{') {
138        Some(table) => table.split_once("version = ")?.1,
139        None => declaration,
140    };
141    literal
142        .strip_prefix('"')
143        .and_then(|value| value.strip_prefix('='))
144        .and_then(|value| value.split_once('"').map(|(version, _)| version))
145}
146
147#[cfg(test)]
148mod manifest_tests {
149    use super::*;
150
151    /// The manifest this build actually compiled against must answer, whatever
152    /// spelling it uses — that is the invariant `ProducerInfo::current` rests
153    /// on.
154    #[test]
155    fn the_packages_own_manifest_reports_both_engine_versions() {
156        assert!(!exact_dependency_version(PACKAGE_MANIFEST, "candid").is_empty());
157        assert!(!exact_dependency_version(PACKAGE_MANIFEST, "candid_parser").is_empty());
158    }
159
160    #[test]
161    fn every_spelling_cargo_can_produce_is_read_identically() {
162        let plain = "[dependencies]\ncandid = \"=0.10.30\"\n";
163        let inline_table = "[dependencies]\ncandid = { version = \"=0.10.30\", optional = true }\n";
164        // Exactly what `cargo package` writes into the published manifest.
165        let normalized = "[dependencies.candid]\nversion = \"=0.10.30\"\noptional = true\n";
166        for manifest in [plain, inline_table, normalized] {
167            assert_eq!(exact_dependency_version(manifest, "candid"), "0.10.30");
168        }
169    }
170
171    #[test]
172    fn a_dev_dependency_is_never_mistaken_for_the_real_one() {
173        // Both sections exist in this package's normalized manifest, and the
174        // dev one may legitimately carry a different requirement.
175        let manifest = "[dependencies.candid_parser]\nversion = \"=0.4.0\"\noptional = true\n\
176                        \n[dev-dependencies.candid_parser]\nversion = \"=0.9.9\"\n";
177        assert_eq!(exact_dependency_version(manifest, "candid_parser"), "0.4.0");
178        // A dev-only declaration is not an answer at all.
179        assert_eq!(
180            manifest_dependency_version(
181                "[dev-dependencies.candid_parser]\nversion = \"=0.4.0\"\n",
182                "candid_parser"
183            ),
184            None
185        );
186    }
187
188    #[test]
189    fn a_floating_requirement_is_not_an_exact_pin() {
190        for manifest in [
191            "[dependencies]\ncandid = \"0.10.30\"\n",
192            "[dependencies]\ncandid = { version = \"^0.10\" }\n",
193            "[dependencies.candid]\nversion = \"0.10.30\"\n",
194        ] {
195            assert_eq!(manifest_dependency_version(manifest, "candid"), None);
196        }
197    }
198}
199
200/// The wire-semantics Contract consumed by host runtimes.
201///
202/// `declarations` supplies named roots for the arena. Comments, source spelling,
203/// and raw source are kept in the `SourceInfo` sidecar (`compiler` feature),
204/// not here.
205#[derive(Debug, Clone, PartialEq, Eq)]
206pub struct Contract {
207    pub(crate) format: String,
208    pub(crate) format_version: u32,
209    pub(crate) semantics_profile: String,
210    pub(crate) canonicalization_profile: String,
211    pub(crate) identities: ContractIdentities,
212    pub(crate) producer: ProducerInfo,
213    pub(crate) types: Vec<TypeNode>,
214    pub(crate) declarations: Vec<Declaration>,
215    pub(crate) actor: Option<Actor>,
216}
217
218impl Contract {
219    pub fn format(&self) -> &str {
220        &self.format
221    }
222
223    pub fn format_version(&self) -> u32 {
224        self.format_version
225    }
226
227    pub fn semantics_profile(&self) -> &str {
228        &self.semantics_profile
229    }
230
231    pub fn canonicalization_profile(&self) -> &str {
232        &self.canonicalization_profile
233    }
234
235    pub fn identities(&self) -> &ContractIdentities {
236        &self.identities
237    }
238
239    pub fn contract_id(&self) -> &str {
240        &self.identities.contract
241    }
242
243    pub fn interface_id(&self) -> Option<&str> {
244        self.identities.interface.as_deref()
245    }
246
247    pub fn producer(&self) -> &ProducerInfo {
248        &self.producer
249    }
250
251    pub fn types(&self) -> &[TypeNode] {
252        &self.types
253    }
254
255    pub fn declarations(&self) -> &[Declaration] {
256        &self.declarations
257    }
258
259    pub fn actor(&self) -> Option<&Actor> {
260        self.actor.as_ref()
261    }
262
263    /// Validate graph structure and verify its content identities.
264    pub fn validate(&self) -> Result<(), ContractValidationError> {
265        self.validate_with_limits(&Limits::default())
266    }
267
268    pub fn validate_with_limits(&self, limits: &Limits) -> Result<(), ContractValidationError> {
269        self.validate_with_context(&crate::RuntimeContext::new(limits.clone()))
270    }
271
272    pub fn validate_with_context(
273        &self,
274        context: &crate::RuntimeContext,
275    ) -> Result<(), ContractValidationError> {
276        let mut budget = context.budget();
277        crate::validate::validate_contract_with_budget(self, &mut budget)
278    }
279
280    /// Return a deterministically re-indexed copy with freshly calculated
281    /// identities. This is useful at JSON trust boundaries.
282    pub fn canonicalize(&self) -> Result<Self, ContractValidationError> {
283        self.canonicalize_with_limits(&Limits::default())
284    }
285
286    pub fn canonicalize_with_limits(
287        &self,
288        limits: &Limits,
289    ) -> Result<Self, ContractValidationError> {
290        self.canonicalize_with_context(&crate::RuntimeContext::new(limits.clone()))
291    }
292
293    pub fn canonicalize_with_context(
294        &self,
295        context: &crate::RuntimeContext,
296    ) -> Result<Self, ContractValidationError> {
297        let mut budget = context.budget();
298        crate::canonical::canonicalize_contract_with_budget(self, &mut budget)
299    }
300
301    /// Serialize validated canonical JSON under [`Limits::default`].
302    ///
303    /// A Contract built with raised limits can fail here. Use
304    /// [`Self::to_json_pretty_with_limits`] or
305    /// [`Self::to_json_pretty_with_context`] to serialize under the caller's
306    /// own policy.
307    pub fn to_json_pretty(&self) -> Result<String, ContractValidationError> {
308        self.to_json_pretty_with_context(&crate::RuntimeContext::default())
309    }
310
311    /// Serialize validated canonical JSON under caller-supplied limits.
312    ///
313    /// This revalidates and recanonicalizes, so it consumes the structural
314    /// limits construction consumed *and* charges the rendered length against
315    /// `max_canonicalization_work`. Raising only the limit that gated
316    /// construction is therefore not always sufficient; see
317    /// [`Self::to_json_pretty_with_context`].
318    pub fn to_json_pretty_with_limits(
319        &self,
320        limits: &Limits,
321    ) -> Result<String, ContractValidationError> {
322        self.to_json_pretty_with_context(&crate::RuntimeContext::new(limits.clone()))
323    }
324
325    /// Serialize validated canonical JSON under the caller's context.
326    ///
327    /// Consumes two distinct budgets: the structural limits that gated
328    /// construction, and `max_canonicalization_work`, against which the
329    /// rendered byte length is charged. A caller who raised only a structural
330    /// limit (for example `max_string_bytes`) to build the Contract may still
331    /// need to raise `max_canonicalization_work` to render it.
332    ///
333    /// For a completely unbounded render, `serde` [`Serialize`] is implemented
334    /// on [`Contract`] directly; it consults no limits and performs no
335    /// revalidation. That path is for trusted, already-validated values.
336    pub fn to_json_pretty_with_context(
337        &self,
338        context: &crate::RuntimeContext,
339    ) -> Result<String, ContractValidationError> {
340        let mut budget = context.budget();
341        let canonical =
342            crate::validate::validate_and_canonicalize_with_budget(self, &mut budget)?.contract;
343        budget
344            .checkpoint()
345            .map_err(crate::budget::BudgetError::into_contract_error)?;
346        let json = serde_json::to_string_pretty(&canonical).map_err(|error| {
347            ContractValidationError::single(
348                "contract_json_serialization_failed",
349                "$",
350                error.to_string(),
351            )
352        })?;
353        let max_work = budget.limits().max_canonicalization_work;
354        budget
355            .charge("canonicalization_work", max_work, json.len())
356            .map_err(crate::budget::BudgetError::into_contract_error)?;
357        budget
358            .checkpoint()
359            .map_err(crate::budget::BudgetError::into_contract_error)?;
360        Ok(json)
361    }
362
363    /// Parse, validate, and canonicalize a Contract JSON document under
364    /// [`Limits::default`].
365    pub fn from_json(input: &str) -> Result<Self, ContractJsonError> {
366        Self::from_json_with_limits(input, &Limits::default())
367    }
368
369    pub fn from_json_with_limits(input: &str, limits: &Limits) -> Result<Self, ContractJsonError> {
370        Self::from_json_with_context(input, &crate::RuntimeContext::new(limits.clone()))
371    }
372
373    /// Bounded parse: `max_input_bytes` is enforced before the document is
374    /// decoded, and decode and validation share one budget.
375    pub fn from_json_with_context(
376        input: &str,
377        context: &crate::RuntimeContext,
378    ) -> Result<Self, ContractJsonError> {
379        let mut budget = context.budget();
380        let raw: RawContract = crate::budget::decode_bounded(&mut budget, input.len(), || {
381            serde_json::from_str(input)
382        })?;
383        Self::from_raw_with_mapping_and_budget(raw, &mut budget)
384            .map(|(contract, _)| contract)
385            .map_err(ContractJsonError::InvalidContract)
386    }
387
388    /// Parse, validate, and canonicalize Contract JSON bytes under
389    /// caller-supplied limits.
390    pub fn from_slice_with_limits(
391        input: &[u8],
392        limits: &Limits,
393    ) -> Result<Self, ContractJsonError> {
394        Self::from_slice_with_context(input, &crate::RuntimeContext::new(limits.clone()))
395    }
396
397    /// Bounded parse from bytes. Equivalent to [`Self::from_json_with_context`]
398    /// without requiring the caller to validate UTF-8 first.
399    pub fn from_slice_with_context(
400        input: &[u8],
401        context: &crate::RuntimeContext,
402    ) -> Result<Self, ContractJsonError> {
403        let mut budget = context.budget();
404        let raw: RawContract = crate::budget::decode_bounded(&mut budget, input.len(), || {
405            serde_json::from_slice(input)
406        })?;
407        Self::from_raw_with_mapping_and_budget(raw, &mut budget)
408            .map(|(contract, _)| contract)
409            .map_err(ContractJsonError::InvalidContract)
410    }
411
412    pub fn try_from_raw(raw: RawContract) -> Result<Self, ContractValidationError> {
413        Self::from_raw_with_limits(raw, &Limits::default())
414    }
415
416    pub fn try_from_raw_with_limits(
417        raw: RawContract,
418        limits: &Limits,
419    ) -> Result<Self, ContractValidationError> {
420        Self::from_raw_with_limits(raw, limits)
421    }
422
423    pub fn try_from_raw_with_context(
424        raw: RawContract,
425        context: &crate::RuntimeContext,
426    ) -> Result<Self, ContractValidationError> {
427        let mut budget = context.budget();
428        Ok(Self::from_raw_with_mapping_and_budget(raw, &mut budget)?.0)
429    }
430
431    fn from_raw_with_limits(
432        raw: RawContract,
433        limits: &Limits,
434    ) -> Result<Self, ContractValidationError> {
435        Ok(Self::from_raw_with_mapping(raw, limits)?.0)
436    }
437
438    pub(crate) fn from_raw_with_mapping(
439        raw: RawContract,
440        limits: &Limits,
441    ) -> Result<(Self, Vec<TypeRef>), ContractValidationError> {
442        let mut budget = crate::budget::Budget::from_limits(limits);
443        Self::from_raw_with_mapping_and_budget(raw, &mut budget)
444    }
445
446    pub(crate) fn from_raw_with_mapping_and_budget(
447        raw: RawContract,
448        budget: &mut crate::budget::Budget<'_>,
449    ) -> Result<(Self, Vec<TypeRef>), ContractValidationError> {
450        let contract = Self {
451            format: raw.format,
452            format_version: raw.format_version,
453            semantics_profile: raw.semantics_profile,
454            canonicalization_profile: raw.canonicalization_profile,
455            identities: raw.identities,
456            producer: raw.producer,
457            types: raw.types,
458            declarations: raw.declarations,
459            actor: raw.actor,
460        };
461        let canonicalized =
462            crate::validate::validate_and_canonicalize_with_budget(&contract, budget)?;
463        Ok((canonicalized.contract, canonicalized.old_to_new))
464    }
465
466    pub(crate) fn new_unchecked(
467        types: Vec<TypeNode>,
468        declarations: Vec<Declaration>,
469        actor: Option<Actor>,
470    ) -> Self {
471        Self {
472            format: CONTRACT_FORMAT.to_string(),
473            format_version: FORMAT_VERSION,
474            semantics_profile: SEMANTICS_PROFILE.to_string(),
475            canonicalization_profile: CANONICALIZATION_PROFILE.to_string(),
476            identities: ContractIdentities {
477                contract: format!("candid-core:contract:v1:sha256:{}", "0".repeat(64)),
478                interface: actor
479                    .as_ref()
480                    .map(|_| format!("candid-core:interface:v1:sha256:{}", "0".repeat(64))),
481            },
482            producer: ProducerInfo::current(),
483            types,
484            declarations,
485            actor,
486        }
487    }
488}
489
490/// A producer-side Contract draft: the parts an authoring tool supplies, and
491/// nothing it must not.
492///
493/// A draft carries only the type graph, named declarations, an optional
494/// actor, and optional producer metadata. It deliberately has **no**
495/// format/version/profile markers and **no** identity fields: building stamps
496/// the current [`CONTRACT_FORMAT`]/[`FORMAT_VERSION`]/[`SEMANTICS_PROFILE`]/
497/// [`CANONICALIZATION_PROFILE`] constants and calculates fresh identities
498/// under the same validation and canonicalization budgets as every other
499/// entry point, so a draft can never carry a fake, stale, or placeholder
500/// identity. [`RawContract`] is the opposite boundary — the serde DTO for
501/// *decoded external* artifacts, whose supplied identities
502/// [`Contract::try_from_raw`] verifies instead of recalculating.
503///
504/// # Serialized shape
505///
506/// A serialized draft contains exactly the four fields above. Unknown keys
507/// are rejected; `declarations` defaults to empty when absent; `actor` is
508/// omitted when absent, and an explicit `"actor": null` is rejected just as
509/// [`RawContract`] rejects it; `producer` is omitted when absent and defaults
510/// at build time to [`ProducerInfo::current`], while a present `producer`
511/// overrides that default.
512///
513/// ```
514/// use candid_core::{ContractDraft, PrimitiveType, TypeNode};
515///
516/// let contract = ContractDraft::new(
517///     vec![TypeNode::Primitive { primitive: PrimitiveType::Nat }],
518///     vec![candid_core::Declaration { name: "Amount".to_string(), ty: 0 }],
519///     None,
520/// )
521/// .build()?;
522/// assert!(contract.contract_id().starts_with("candid-core:contract:v1:sha256:"));
523/// assert_eq!(contract.producer(), &candid_core::ProducerInfo::current());
524/// # Ok::<(), candid_core::ContractValidationError>(())
525/// ```
526#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
527#[serde(deny_unknown_fields)]
528pub struct ContractDraft {
529    pub types: Vec<TypeNode>,
530    #[serde(default)]
531    pub declarations: Vec<Declaration>,
532    /// An actorless draft omits this property entirely; `"actor": null` is
533    /// rejected on decode, exactly as [`RawContract`] rejects it.
534    #[serde(
535        default,
536        deserialize_with = "deserialize_actor_forbidding_null",
537        skip_serializing_if = "Option::is_none"
538    )]
539    pub actor: Option<Actor>,
540    /// Untrusted provenance about the authoring tool. `None` builds with
541    /// [`ProducerInfo::current`]. Never part of a semantic identity; see
542    /// [`ProducerInfo`]. An explicit `"producer": null` is rejected on
543    /// decode: absence is the only spelling of "default producer", mirroring
544    /// the `actor` rule.
545    #[serde(
546        default,
547        deserialize_with = "deserialize_producer_forbidding_null",
548        skip_serializing_if = "Option::is_none"
549    )]
550    pub producer: Option<ProducerInfo>,
551}
552
553/// Invoked only when the `producer` key is present; an absent key takes the
554/// `None` default. Delegating to [`ProducerInfo`] directly makes an explicit
555/// JSON `null` a decode error instead of a second spelling of "default
556/// producer".
557fn deserialize_producer_forbidding_null<'de, D>(
558    deserializer: D,
559) -> Result<Option<ProducerInfo>, D::Error>
560where
561    D: serde::Deserializer<'de>,
562{
563    ProducerInfo::deserialize(deserializer).map(Some)
564}
565
566impl ContractDraft {
567    pub fn new(types: Vec<TypeNode>, declarations: Vec<Declaration>, actor: Option<Actor>) -> Self {
568        Self {
569            types,
570            declarations,
571            actor,
572            producer: None,
573        }
574    }
575
576    /// Returns `self` with an explicit producer, overriding the
577    /// [`ProducerInfo::current`] default applied at build time.
578    #[must_use]
579    pub fn with_producer(mut self, producer: ProducerInfo) -> Self {
580        self.producer = Some(producer);
581        self
582    }
583
584    /// Validate the draft graph, canonicalize it, and calculate its
585    /// identities under [`Limits::default`].
586    pub fn build(self) -> Result<Contract, ContractValidationError> {
587        self.build_with_limits(&Limits::default())
588    }
589
590    /// Build under caller-supplied limits.
591    pub fn build_with_limits(self, limits: &Limits) -> Result<Contract, ContractValidationError> {
592        let mut budget = crate::budget::Budget::from_limits(limits);
593        self.build_with_budget(&mut budget)
594    }
595
596    /// Build under the caller's context, sharing its budget, deadline, and
597    /// cancellation token.
598    pub fn build_with_context(
599        self,
600        context: &crate::RuntimeContext,
601    ) -> Result<Contract, ContractValidationError> {
602        let mut budget = context.budget();
603        self.build_with_budget(&mut budget)
604    }
605
606    fn build_with_budget(
607        self,
608        budget: &mut crate::budget::Budget<'_>,
609    ) -> Result<Contract, ContractValidationError> {
610        let mut contract = Contract::new_unchecked(self.types, self.declarations, self.actor);
611        if let Some(producer) = self.producer {
612            contract.producer = producer;
613        }
614        crate::validate::validate_structure_with_budget(&contract, budget)?;
615        Ok(
616            crate::canonical::canonicalize_with_mapping_unchecked_with_budget(&contract, budget)?
617                .contract,
618        )
619    }
620}
621
622/// Unvalidated Contract data decoded from an external artifact.
623///
624/// This is the serde entry point for Contract JSON. [`Contract`] itself
625/// deliberately does not implement [`Deserialize`]: a trait impl has no
626/// argument position for a resource policy, so it could only ever decode under
627/// limits the library chose. Callers decode this DTO and convert through a
628/// policy-taking constructor such as [`Contract::try_from_raw_with_context`],
629/// or use a bounded parse entry point such as
630/// [`Contract::from_json_with_context`], which enforces `max_input_bytes`
631/// before decoding.
632///
633/// Decoding this DTO is *not* a trust boundary and carries no allocation
634/// bound; gate the byte length yourself, or use the bounded parse APIs.
635///
636/// This type is reserved for artifacts that already carry format markers and
637/// identities: [`Contract::try_from_raw`] verifies the supplied identities
638/// against recomputation, and [`From<&Contract>`] projects a validated
639/// Contract back onto the wire shape. To *author* a Contract — where no
640/// trustworthy identity exists yet — use [`ContractDraft`], which carries no
641/// identity fields at all and calculates them on build.
642///
643/// ```compile_fail
644/// // A validated Contract cannot be produced by serde alone.
645/// let _: candid_core::Contract = serde_json::from_str("{}").unwrap();
646/// ```
647#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
648#[serde(deny_unknown_fields)]
649pub struct RawContract {
650    pub format: String,
651    pub format_version: u32,
652    pub semantics_profile: String,
653    pub canonicalization_profile: String,
654    pub identities: ContractIdentities,
655    pub producer: ProducerInfo,
656    pub types: Vec<TypeNode>,
657    #[serde(default)]
658    pub declarations: Vec<Declaration>,
659    /// An actorless Contract omits this property entirely. `"actor": null` is
660    /// not part of the v1 wire format: serialization never emits it, the
661    /// identity payload never hashes it, and decoding rejects it.
662    #[serde(
663        default,
664        deserialize_with = "deserialize_actor_forbidding_null",
665        skip_serializing_if = "Option::is_none"
666    )]
667    pub actor: Option<Actor>,
668}
669
670/// Invoked only when the `actor` key is present; an absent key takes the
671/// `None` default. Delegating to [`Actor`] directly makes an explicit JSON
672/// `null` a decode error instead of a second spelling of "no actor".
673fn deserialize_actor_forbidding_null<'de, D>(deserializer: D) -> Result<Option<Actor>, D::Error>
674where
675    D: serde::Deserializer<'de>,
676{
677    Actor::deserialize(deserializer).map(Some)
678}
679
680impl From<&Contract> for RawContract {
681    fn from(contract: &Contract) -> Self {
682        Self {
683            format: contract.format.clone(),
684            format_version: contract.format_version,
685            semantics_profile: contract.semantics_profile.clone(),
686            canonicalization_profile: contract.canonicalization_profile.clone(),
687            identities: contract.identities.clone(),
688            producer: contract.producer.clone(),
689            types: contract.types.clone(),
690            declarations: contract.declarations.clone(),
691            actor: contract.actor.clone(),
692        }
693    }
694}
695
696impl Serialize for Contract {
697    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
698    where
699        S: Serializer,
700    {
701        RawContract::from(self).serialize(serializer)
702    }
703}