1use crate::{Contract, ContractJsonError, ContractValidationError, Limits, RawContract};
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4use std::collections::BTreeMap;
5
6#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
36#[serde(deny_unknown_fields)]
37pub struct ContractEnvelope {
38 contract: Contract,
39 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
40 extensions: BTreeMap<String, Value>,
41}
42
43impl ContractEnvelope {
44 pub fn new(contract: Contract) -> Self {
45 Self {
46 contract,
47 extensions: BTreeMap::new(),
48 }
49 }
50
51 pub fn contract(&self) -> &Contract {
52 &self.contract
53 }
54
55 pub fn extensions(&self) -> &BTreeMap<String, Value> {
56 &self.extensions
57 }
58
59 pub fn insert_extension(
60 &mut self,
61 name: impl Into<String>,
62 value: Value,
63 limits: &Limits,
64 ) -> Result<(), ContractValidationError> {
65 self.insert_extension_with_context(name, value, &crate::RuntimeContext::new(limits.clone()))
66 }
67
68 pub fn insert_extension_with_context(
69 &mut self,
70 name: impl Into<String>,
71 value: Value,
72 context: &crate::RuntimeContext,
73 ) -> Result<(), ContractValidationError> {
74 let name = name.into();
75 let previous = self.extensions.insert(name.clone(), value);
76 if let Err(error) = self.validate_with_context(context) {
77 match previous {
78 Some(previous) => {
79 self.extensions.insert(name, previous);
80 }
81 None => {
82 self.extensions.remove(&name);
83 }
84 }
85 return Err(error);
86 }
87 Ok(())
88 }
89
90 pub fn validate(&self, limits: &Limits) -> Result<(), ContractValidationError> {
91 self.validate_with_context(&crate::RuntimeContext::new(limits.clone()))
92 }
93
94 pub fn validate_with_context(
95 &self,
96 context: &crate::RuntimeContext,
97 ) -> Result<(), ContractValidationError> {
98 let mut budget = context.budget();
99 crate::validate::validate_contract_with_budget(&self.contract, &mut budget)?;
100 self.validate_extensions_with_budget(&mut budget)
101 }
102
103 pub(crate) fn validate_extensions_with_budget(
108 &self,
109 budget: &mut crate::budget::Budget<'_>,
110 ) -> Result<(), ContractValidationError> {
111 let max_value_bytes = budget.limits().max_value_bytes;
112 let mut bytes = 0usize;
113 for (name, value) in &self.extensions {
114 budget
115 .checkpoint()
116 .map_err(crate::budget::BudgetError::into_contract_error)?;
117 if !valid_extension_name(name) {
118 return Err(ContractValidationError::single(
119 "invalid_extension_name",
120 "$.extensions",
121 format!(
122 "extension {name:?} must be a reverse-domain name followed by /v<integer>"
123 ),
124 ));
125 }
126 bytes = bytes
127 .saturating_add(name.len())
128 .saturating_add(serde_json::to_vec(value).map_or(usize::MAX, |value| value.len()));
129 }
130 budget
131 .observe("extension_bytes", max_value_bytes, bytes)
132 .map_err(crate::budget::BudgetError::into_contract_error)?;
133 Ok(())
134 }
135
136 pub fn from_json_with_limits(input: &str, limits: &Limits) -> Result<Self, ContractJsonError> {
139 Self::from_json_with_context(input, &crate::RuntimeContext::new(limits.clone()))
140 }
141
142 pub fn from_json_with_context(
145 input: &str,
146 context: &crate::RuntimeContext,
147 ) -> Result<Self, ContractJsonError> {
148 let mut budget = context.budget();
149 let raw: RawContractEnvelope =
150 crate::budget::decode_bounded(&mut budget, input.len(), || {
151 serde_json::from_str(input)
152 })?;
153 Self::from_raw_with_budget(raw, &mut budget)
154 }
155
156 pub fn from_slice_with_limits(
158 input: &[u8],
159 limits: &Limits,
160 ) -> Result<Self, ContractJsonError> {
161 Self::from_slice_with_context(input, &crate::RuntimeContext::new(limits.clone()))
162 }
163
164 pub fn from_slice_with_context(
165 input: &[u8],
166 context: &crate::RuntimeContext,
167 ) -> Result<Self, ContractJsonError> {
168 let mut budget = context.budget();
169 let raw: RawContractEnvelope =
170 crate::budget::decode_bounded(&mut budget, input.len(), || {
171 serde_json::from_slice(input)
172 })?;
173 Self::from_raw_with_budget(raw, &mut budget)
174 }
175
176 fn from_raw_with_budget(
177 raw: RawContractEnvelope,
178 budget: &mut crate::budget::Budget<'_>,
179 ) -> Result<Self, ContractJsonError> {
180 let (contract, _) = Contract::from_raw_with_mapping_and_budget(raw.contract, budget)
181 .map_err(ContractJsonError::InvalidContract)?;
182 let envelope = Self {
183 contract,
184 extensions: raw.extensions,
185 };
186 envelope
187 .validate_extensions_with_budget(budget)
188 .map_err(ContractJsonError::InvalidContract)?;
189 Ok(envelope)
190 }
191
192 pub fn to_json_pretty_with_limits(
194 &self,
195 limits: &Limits,
196 ) -> Result<String, ContractValidationError> {
197 self.to_json_pretty_with_context(&crate::RuntimeContext::new(limits.clone()))
198 }
199
200 pub fn to_json_pretty_with_context(
203 &self,
204 context: &crate::RuntimeContext,
205 ) -> Result<String, ContractValidationError> {
206 let mut budget = context.budget();
207 crate::validate::validate_contract_with_budget(&self.contract, &mut budget)?;
208 self.validate_extensions_with_budget(&mut budget)?;
209 budget
210 .checkpoint()
211 .map_err(crate::budget::BudgetError::into_contract_error)?;
212 let json = serde_json::to_string_pretty(self).map_err(|error| {
213 ContractValidationError::single(
214 "contract_json_serialization_failed",
215 "$",
216 error.to_string(),
217 )
218 })?;
219 let max_work = budget.limits().max_canonicalization_work;
220 budget
221 .charge("canonicalization_work", max_work, json.len())
222 .map_err(crate::budget::BudgetError::into_contract_error)?;
223 budget
224 .checkpoint()
225 .map_err(crate::budget::BudgetError::into_contract_error)?;
226 Ok(json)
227 }
228}
229
230#[derive(Deserialize)]
241#[serde(deny_unknown_fields)]
242struct RawContractEnvelope {
243 contract: RawContract,
244 #[serde(default)]
245 extensions: BTreeMap<String, Value>,
246}
247
248fn valid_extension_name(name: &str) -> bool {
249 let Some((namespace, version)) = name.rsplit_once("/v") else {
250 return false;
251 };
252 namespace.contains('.')
253 && namespace
254 .split('.')
255 .all(|segment| !segment.is_empty() && segment.bytes().all(is_name_byte))
256 && !version.is_empty()
257 && version.bytes().all(|byte| byte.is_ascii_digit())
258 && !version.starts_with('0')
259}
260
261fn is_name_byte(byte: u8) -> bool {
262 byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-'
263}