Skip to main content

candid_core/compile/
artifact.rs

1use super::*;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
4#[serde(deny_unknown_fields)]
5pub struct CompileOptions {
6    /// Preserve optional names, comments, raw source, and label spelling in a
7    /// sidecar. This never changes the Contract or its identities.
8    pub include_source_info: bool,
9}
10
11impl Default for CompileOptions {
12    fn default() -> Self {
13        Self {
14            include_source_info: true,
15        }
16    }
17}
18
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct Compilation {
21    pub(super) contract: Contract,
22    pub(super) source_info: Option<SourceInfo>,
23}
24
25impl Compilation {
26    pub fn contract(&self) -> &Contract {
27        &self.contract
28    }
29
30    pub fn source_info(&self) -> Option<&SourceInfo> {
31        self.source_info.as_ref()
32    }
33
34    pub fn into_parts(self) -> (Contract, Option<SourceInfo>) {
35        (self.contract, self.source_info)
36    }
37
38    pub fn try_from_raw(
39        raw_contract: RawContract,
40        raw_source_info: Option<SerializedSourceInfo>,
41        limits: &crate::Limits,
42    ) -> Result<Self, crate::ContractValidationError> {
43        let context = crate::RuntimeContext::new(limits.clone());
44        Self::try_from_raw_with_context(raw_contract, raw_source_info, &context)
45    }
46
47    pub fn try_from_raw_with_context(
48        raw_contract: RawContract,
49        raw_source_info: Option<SerializedSourceInfo>,
50        context: &crate::RuntimeContext,
51    ) -> Result<Self, crate::ContractValidationError> {
52        let mut budget = context.budget();
53        Self::try_from_raw_with_budget(raw_contract, raw_source_info, &mut budget)
54    }
55
56    pub(crate) fn try_from_raw_with_budget(
57        raw_contract: RawContract,
58        raw_source_info: Option<SerializedSourceInfo>,
59        budget: &mut crate::budget::Budget<'_>,
60    ) -> Result<Self, crate::ContractValidationError> {
61        let (contract, mapping) = Contract::from_raw_with_mapping_and_budget(raw_contract, budget)?;
62        let source_info = raw_source_info
63            .map(SourceInfo::from_raw_unchecked)
64            .map(|mut source_info| {
65                remap_source_info(&mut source_info, &mapping, budget)?;
66                source_info.validate_with_budget(&contract, budget)?;
67                Ok::<SourceInfo, crate::ContractValidationError>(source_info)
68            })
69            .transpose()?;
70        Ok(Self {
71            contract,
72            source_info,
73        })
74    }
75
76    /// Parse, validate, and canonicalize a Compilation JSON document under
77    /// caller-supplied limits.
78    ///
79    /// `max_input_bytes` is enforced before the document is decoded, so an
80    /// oversized sidecar is rejected without being materialized.
81    pub fn from_json_with_limits(
82        input: &str,
83        limits: &crate::Limits,
84    ) -> Result<Self, crate::ContractJsonError> {
85        Self::from_json_with_context(input, &crate::RuntimeContext::new(limits.clone()))
86    }
87
88    /// Bounded parse: the byte gate, decode, and validation share one budget.
89    pub fn from_json_with_context(
90        input: &str,
91        context: &crate::RuntimeContext,
92    ) -> Result<Self, crate::ContractJsonError> {
93        let mut budget = context.budget();
94        let raw: RawCompilation = crate::budget::decode_bounded(&mut budget, input.len(), || {
95            serde_json::from_str(input)
96        })?;
97        Self::try_from_raw_with_budget(raw.contract, raw.source_info, &mut budget)
98            .map_err(crate::ContractJsonError::InvalidContract)
99    }
100
101    /// Bounded parse from bytes.
102    pub fn from_slice_with_limits(
103        input: &[u8],
104        limits: &crate::Limits,
105    ) -> Result<Self, crate::ContractJsonError> {
106        Self::from_slice_with_context(input, &crate::RuntimeContext::new(limits.clone()))
107    }
108
109    pub fn from_slice_with_context(
110        input: &[u8],
111        context: &crate::RuntimeContext,
112    ) -> Result<Self, crate::ContractJsonError> {
113        let mut budget = context.budget();
114        let raw: RawCompilation = crate::budget::decode_bounded(&mut budget, input.len(), || {
115            serde_json::from_slice(input)
116        })?;
117        Self::try_from_raw_with_budget(raw.contract, raw.source_info, &mut budget)
118            .map_err(crate::ContractJsonError::InvalidContract)
119    }
120
121    /// Serialize validated canonical JSON under caller-supplied limits.
122    ///
123    /// Like [`Contract::to_json_pretty_with_limits`], this revalidates and
124    /// charges the rendered length against `max_canonicalization_work`, so it
125    /// may require raising that limit in addition to whichever structural
126    /// limit gated construction.
127    pub fn to_json_pretty_with_limits(
128        &self,
129        limits: &crate::Limits,
130    ) -> Result<String, crate::ContractValidationError> {
131        self.to_json_pretty_with_context(&crate::RuntimeContext::new(limits.clone()))
132    }
133
134    pub fn to_json_pretty_with_context(
135        &self,
136        context: &crate::RuntimeContext,
137    ) -> Result<String, crate::ContractValidationError> {
138        let mut budget = context.budget();
139        // Validate without recanonicalizing, and render the stored artifact.
140        // A Compilation's Contract is already canonical, and its sidecar's
141        // type references are indexed against exactly that arena — rendering a
142        // recanonicalized copy could desynchronize the two. The sidecar is not
143        // revalidated here either: provenance authentication rederives the
144        // whole bundle from source, which is construction-time work, not
145        // serialization work.
146        crate::validate::validate_contract_with_budget(&self.contract, &mut budget)?;
147        budget
148            .checkpoint()
149            .map_err(crate::budget::BudgetError::into_contract_error)?;
150        let json = serde_json::to_string_pretty(&CompilationRef {
151            contract: &self.contract,
152            source_info: &self.source_info,
153        })
154        .map_err(|error| {
155            crate::ContractValidationError::single(
156                "contract_json_serialization_failed",
157                "$",
158                error.to_string(),
159            )
160        })?;
161        let max_work = budget.limits().max_canonicalization_work;
162        budget
163            .charge("canonicalization_work", max_work, json.len())
164            .map_err(crate::budget::BudgetError::into_contract_error)?;
165        budget
166            .checkpoint()
167            .map_err(crate::budget::BudgetError::into_contract_error)?;
168        Ok(json)
169    }
170}
171
172#[derive(Serialize)]
173#[serde(deny_unknown_fields)]
174struct CompilationRef<'a> {
175    contract: &'a Contract,
176    #[serde(default, skip_serializing_if = "Option::is_none")]
177    source_info: &'a Option<SourceInfo>,
178}
179
180impl Serialize for Compilation {
181    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
182    where
183        S: Serializer,
184    {
185        CompilationRef {
186            contract: &self.contract,
187            source_info: &self.source_info,
188        }
189        .serialize(serializer)
190    }
191}
192
193/// Unvalidated Compilation data.
194///
195/// Kept private deliberately: [`Compilation::from_json_with_context`] is the
196/// decode path, so this shape is not frozen as public surface. [`Compilation`]
197/// itself does not implement [`Deserialize`] because a trait impl cannot
198/// accept a resource policy:
199///
200/// ```compile_fail
201/// let _: candid_core::Compilation = serde_json::from_str("{}").unwrap();
202/// ```
203#[derive(Deserialize)]
204#[serde(deny_unknown_fields)]
205struct RawCompilation {
206    contract: RawContract,
207    #[serde(default)]
208    source_info: Option<SerializedSourceInfo>,
209}
210
211fn remap_source_info(
212    source_info: &mut SourceInfo,
213    mapping: &[TypeRef],
214    budget: &mut crate::budget::Budget<'_>,
215) -> Result<(), crate::ContractValidationError> {
216    // Remapping walks attacker-sized collections before any validation stage
217    // runs, so it must bound them itself rather than inherit later checks.
218    crate::source::observe_remapped_collections(source_info, budget)?;
219    let map = |reference: TypeRef| {
220        mapping.get(reference as usize).copied().ok_or_else(|| {
221            crate::ContractValidationError::single(
222                "source_type_ref_out_of_bounds",
223                "$",
224                format!("source sidecar type reference {reference} is outside the input arena"),
225            )
226        })
227    };
228    for declaration in &mut source_info.declarations {
229        budget
230            .checkpoint()
231            .map_err(crate::budget::BudgetError::into_contract_error)?;
232        declaration.ty = map(declaration.ty)?;
233    }
234    for field in &mut source_info.field_labels {
235        budget
236            .checkpoint()
237            .map_err(crate::budget::BudgetError::into_contract_error)?;
238        field.container = map(field.container)?;
239    }
240    for method in &mut source_info.methods {
241        budget
242            .checkpoint()
243            .map_err(crate::budget::BudgetError::into_contract_error)?;
244        method.service = map(method.service)?;
245    }
246    for argument in &mut source_info.function_arguments {
247        budget
248            .checkpoint()
249            .map_err(crate::budget::BudgetError::into_contract_error)?;
250        argument.function = map(argument.function)?;
251    }
252    Ok(())
253}