1use std::error::Error;
4use std::fmt;
5use std::num::NonZeroU64;
6
7use crate::{
8 AdministrationSequence, ApplicationExportOperationId, CapabilityApplicationExportScopeV1,
9 CapabilityId, CommitSequence, ContractBundleHash, ContractLineage, ContractVersion, DatabaseId,
10 QueryModuleHash, ReactiveModuleHash,
11};
12
13pub const MAX_APPLICATION_EXPORT_MODULES: usize = 256;
15
16pub const MAX_APPLICATION_EXPORT_PAGE_ROWS: usize = 500;
18pub const MAX_APPLICATION_EXPORT_PAGE_BYTES: usize = 4 * 1024 * 1024;
20pub const MAX_APPLICATION_EXPORT_PAGES: usize = 4_096;
22
23#[derive(Clone, Copy, Debug, Eq, PartialEq)]
25pub enum ApplicationExportValueError {
26 InvalidShape,
28 LimitExceeded,
30 NonCanonical,
32}
33
34impl fmt::Display for ApplicationExportValueError {
35 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
36 formatter.write_str(match self {
37 Self::InvalidShape => "application export has an invalid closed shape",
38 Self::LimitExceeded => "application export exceeds a hard limit",
39 Self::NonCanonical => "application export identities are not canonical",
40 })
41 }
42}
43
44impl Error for ApplicationExportValueError {}
45
46#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
48#[repr(u8)]
49pub enum ApplicationExportClassV1 {
50 Entity = 1,
52 Event = 2,
54 Provenance = 3,
56 PublicAudit = 4,
58}
59
60impl ApplicationExportClassV1 {
61 #[must_use]
63 pub const fn tag(self) -> u8 {
64 self as u8
65 }
66
67 #[must_use]
69 pub const fn from_tag(tag: u8) -> Option<Self> {
70 match tag {
71 1 => Some(Self::Entity),
72 2 => Some(Self::Event),
73 3 => Some(Self::Provenance),
74 4 => Some(Self::PublicAudit),
75 _ => None,
76 }
77 }
78}
79
80pub fn canonical_application_export_page_preimage(
86 operation_id: ApplicationExportOperationId,
87 page_number: NonZeroU64,
88 class: ApplicationExportClassV1,
89 lines: &[&[u8]],
90 class_complete: bool,
91 operation_complete: bool,
92) -> Result<Vec<u8>, ApplicationExportValueError> {
93 let line_count =
94 u32::try_from(lines.len()).map_err(|_| ApplicationExportValueError::LimitExceeded)?;
95 let mut output = Vec::new();
96 output.extend_from_slice(operation_id.as_bytes());
97 output.extend_from_slice(&page_number.get().to_be_bytes());
98 output.push(class.tag());
99 output.push(u8::from(class_complete));
100 output.push(u8::from(operation_complete));
101 output.extend_from_slice(&line_count.to_be_bytes());
102 for line in lines {
103 let line_length =
104 u32::try_from(line.len()).map_err(|_| ApplicationExportValueError::LimitExceeded)?;
105 output.extend_from_slice(&line_length.to_be_bytes());
106 output.extend_from_slice(line);
107 }
108 Ok(output)
109}
110
111#[derive(Clone, Eq, PartialEq)]
113pub struct ApplicationExportSelectionV1 {
114 lineage: ContractLineage,
115 scope: CapabilityApplicationExportScopeV1,
116 entities: bool,
117 events: bool,
118 provenance: bool,
119 public_audit: bool,
120}
121
122impl ApplicationExportSelectionV1 {
123 #[allow(clippy::fn_params_excessive_bools)]
125 pub fn new(
126 lineage: ContractLineage,
127 scope: CapabilityApplicationExportScopeV1,
128 entities: bool,
129 events: bool,
130 provenance: bool,
131 public_audit: bool,
132 ) -> Result<Self, ApplicationExportValueError> {
133 if !entities && !events {
134 return Err(ApplicationExportValueError::InvalidShape);
135 }
136 Ok(Self {
137 lineage,
138 scope,
139 entities,
140 events,
141 provenance,
142 public_audit,
143 })
144 }
145
146 #[must_use]
148 pub const fn lineage(&self) -> &ContractLineage {
149 &self.lineage
150 }
151
152 #[must_use]
154 pub const fn scope(&self) -> CapabilityApplicationExportScopeV1 {
155 self.scope
156 }
157
158 #[must_use]
160 pub const fn entities(&self) -> bool {
161 self.entities
162 }
163
164 #[must_use]
166 pub const fn events(&self) -> bool {
167 self.events
168 }
169
170 #[must_use]
172 pub const fn provenance(&self) -> bool {
173 self.provenance
174 }
175
176 #[must_use]
178 pub const fn public_audit(&self) -> bool {
179 self.public_audit
180 }
181
182 #[must_use]
184 pub const fn includes(&self, class: ApplicationExportClassV1) -> bool {
185 match class {
186 ApplicationExportClassV1::Entity => self.entities,
187 ApplicationExportClassV1::Event => self.events,
188 ApplicationExportClassV1::Provenance => self.provenance,
189 ApplicationExportClassV1::PublicAudit => self.public_audit,
190 }
191 }
192}
193
194impl fmt::Debug for ApplicationExportSelectionV1 {
195 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
196 formatter
197 .debug_struct("ApplicationExportSelectionV1")
198 .field("lineage", &self.lineage)
199 .field("scope", &self.scope)
200 .field("entities", &self.entities)
201 .field("events", &self.events)
202 .field("provenance", &self.provenance)
203 .field("public_audit", &self.public_audit)
204 .finish()
205 }
206}
207
208#[derive(Clone, Copy, Debug, Eq, PartialEq)]
210pub struct ApplicationExportAuthorityV1 {
211 capability_id: CapabilityId,
212 capability_revision: NonZeroU64,
213}
214
215impl ApplicationExportAuthorityV1 {
216 #[must_use]
218 pub const fn new(capability_id: CapabilityId, capability_revision: NonZeroU64) -> Self {
219 Self {
220 capability_id,
221 capability_revision,
222 }
223 }
224
225 #[must_use]
227 pub const fn capability_id(self) -> CapabilityId {
228 self.capability_id
229 }
230
231 #[must_use]
233 pub const fn capability_revision(self) -> NonZeroU64 {
234 self.capability_revision
235 }
236}
237
238#[derive(Clone, Eq, PartialEq)]
240pub struct ApplicationExportSnapshotBindingV1 {
241 database_id: DatabaseId,
242 history_incarnation: NonZeroU64,
243 application_frontier: Option<CommitSequence>,
244 administration_frontier: Option<AdministrationSequence>,
245 contract_version: ContractVersion,
246 contract_bundle_hash: ContractBundleHash,
247 query_modules: Vec<QueryModuleHash>,
248 reactive_modules: Vec<ReactiveModuleHash>,
249}
250
251impl ApplicationExportSnapshotBindingV1 {
252 #[allow(clippy::too_many_arguments)]
254 pub fn new(
255 database_id: DatabaseId,
256 history_incarnation: NonZeroU64,
257 application_frontier: Option<CommitSequence>,
258 administration_frontier: Option<AdministrationSequence>,
259 contract_version: ContractVersion,
260 contract_bundle_hash: ContractBundleHash,
261 query_modules: Vec<QueryModuleHash>,
262 reactive_modules: Vec<ReactiveModuleHash>,
263 ) -> Result<Self, ApplicationExportValueError> {
264 if query_modules.len() > MAX_APPLICATION_EXPORT_MODULES
265 || reactive_modules.len() > MAX_APPLICATION_EXPORT_MODULES
266 {
267 return Err(ApplicationExportValueError::LimitExceeded);
268 }
269 if query_modules.windows(2).any(|pair| pair[0] >= pair[1])
270 || reactive_modules.windows(2).any(|pair| pair[0] >= pair[1])
271 {
272 return Err(ApplicationExportValueError::NonCanonical);
273 }
274 Ok(Self {
275 database_id,
276 history_incarnation,
277 application_frontier,
278 administration_frontier,
279 contract_version,
280 contract_bundle_hash,
281 query_modules,
282 reactive_modules,
283 })
284 }
285
286 #[must_use]
288 pub const fn database_id(&self) -> DatabaseId {
289 self.database_id
290 }
291
292 #[must_use]
294 pub const fn history_incarnation(&self) -> NonZeroU64 {
295 self.history_incarnation
296 }
297
298 #[must_use]
300 pub const fn application_frontier(&self) -> Option<CommitSequence> {
301 self.application_frontier
302 }
303
304 #[must_use]
306 pub const fn administration_frontier(&self) -> Option<AdministrationSequence> {
307 self.administration_frontier
308 }
309
310 #[must_use]
312 pub const fn contract_version(&self) -> ContractVersion {
313 self.contract_version
314 }
315
316 #[must_use]
318 pub const fn contract_bundle_hash(&self) -> ContractBundleHash {
319 self.contract_bundle_hash
320 }
321
322 #[must_use]
324 pub fn query_modules(&self) -> &[QueryModuleHash] {
325 &self.query_modules
326 }
327
328 #[must_use]
330 pub fn reactive_modules(&self) -> &[ReactiveModuleHash] {
331 &self.reactive_modules
332 }
333}
334
335impl fmt::Debug for ApplicationExportSnapshotBindingV1 {
336 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
337 formatter
338 .debug_struct("ApplicationExportSnapshotBindingV1")
339 .field("database_id", &self.database_id)
340 .field("history_incarnation", &self.history_incarnation)
341 .field("application_frontier", &self.application_frontier)
342 .field("administration_frontier", &self.administration_frontier)
343 .field("contract_version", &self.contract_version)
344 .field("contract_bundle_hash", &self.contract_bundle_hash)
345 .field("query_module_count", &self.query_modules.len())
346 .field("reactive_module_count", &self.reactive_modules.len())
347 .finish()
348 }
349}
350
351#[cfg(test)]
352mod tests {
353 use super::*;
354
355 #[test]
356 fn selection_has_no_supporting_only_or_implicit_class_shape() {
357 let lineage = ContractLineage::new("TicketDesk").expect("lineage");
358 assert_eq!(
359 ApplicationExportSelectionV1::new(
360 lineage.clone(),
361 CapabilityApplicationExportScopeV1::WholeApplication,
362 false,
363 false,
364 true,
365 true,
366 ),
367 Err(ApplicationExportValueError::InvalidShape)
368 );
369 let selection = ApplicationExportSelectionV1::new(
370 lineage,
371 CapabilityApplicationExportScopeV1::PrincipalFiltered,
372 true,
373 false,
374 false,
375 false,
376 )
377 .expect("selection");
378 assert!(selection.includes(ApplicationExportClassV1::Entity));
379 assert!(!selection.includes(ApplicationExportClassV1::Event));
380 assert!(!selection.includes(ApplicationExportClassV1::Provenance));
381 assert!(!selection.includes(ApplicationExportClassV1::PublicAudit));
382 }
383}