1#![allow(missing_docs)]
3
4use serde::{Deserialize, Serialize};
5use std::fmt;
6
7#[derive(Clone, Copy, Debug)]
13pub struct PortableV2Limits {
14 pub max_components: u64,
16 pub max_entries: u64,
18 pub max_entry_bytes: u64,
20 pub max_total_bytes: u64,
22 pub max_manifest_bytes: u64,
27 pub max_semantic_manifest_bytes: u64,
31 pub max_tag_manifest_bytes: u64,
36 pub max_path_bytes: usize,
38 pub copy_buffer_bytes: usize,
40}
41
42pub const PORTABLE_V2_MANIFEST_BYTES_PER_ENTRY: u64 = 512;
49const PRODUCER_MAX_GRAPH_FILES: u64 = 100_000;
52
53impl PortableV2Limits {
54 #[must_use]
60 pub fn semantic_manifest_bound(&self, entries: u64) -> u64 {
61 self.max_manifest_bytes
62 .saturating_add(entries.saturating_mul(PORTABLE_V2_MANIFEST_BYTES_PER_ENTRY))
63 .min(self.max_semantic_manifest_bytes)
64 }
65}
66
67impl Default for PortableV2Limits {
68 fn default() -> Self {
69 Self {
70 max_components: 10_000,
71 max_entries: 1_000_000,
72 max_entry_bytes: 16 * 1024_u64.pow(4),
73 max_total_bytes: 1024 * 1024_u64.pow(4),
74 max_manifest_bytes: 16 * 1024 * 1024,
75 max_semantic_manifest_bytes: (PRODUCER_MAX_GRAPH_FILES
78 * PORTABLE_V2_MANIFEST_BYTES_PER_ENTRY)
79 .next_power_of_two(),
80 max_tag_manifest_bytes: 4 * 1024 * 1024,
81 max_path_bytes: 4096,
82 copy_buffer_bytes: 1024 * 1024,
83 }
84 }
85}
86
87#[derive(Clone, Copy, Debug, Eq, PartialEq)]
88pub enum PortableV2Mode {
89 StructureOnly,
90 Full,
91}
92
93#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
94#[serde(rename_all = "snake_case")]
95pub enum PortableV2Representation {
96 Expanded,
97 Bundle,
98}
99
100#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
101#[serde(rename_all = "kebab-case")]
102pub enum PortableV2PackageClass {
103 Complete,
104 OntologyOnly,
105 ComponentSelective,
106 GraphDataSubset,
107}
108
109#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
110#[serde(rename_all = "snake_case")]
111pub enum PortableV2Integrity {
112 NotChecked,
113 Verified,
114 Failed,
115}
116
117#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
118#[serde(rename_all = "snake_case")]
119pub enum PortableV2Compatibility {
120 Supported,
121 UnsupportedFuture,
122 Failed,
123}
124
125#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
126#[serde(rename_all = "snake_case")]
127pub enum PortableV2Authenticity {
128 NotEvaluated,
129 Unsigned,
130 Verified,
131 Failed,
132}
133
134#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
135pub struct PortableV2Report {
136 pub research_interchange: bool,
138 pub research_entries: Vec<PortableV2ResearchEntry>,
140 pub contract: &'static str,
141 pub representation: PortableV2Representation,
142 pub package_digest: String,
143 pub package_class: PortableV2PackageClass,
144 pub component_count: u64,
145 pub entry_count: u64,
146 pub payload_bytes: u64,
147 pub integrity: PortableV2Integrity,
148 pub compatibility: PortableV2Compatibility,
149 pub authenticity: PortableV2Authenticity,
150 pub transport_digest: Option<String>,
151 pub ontology_composition: Option<PortableV2OntologyComposition>,
153 pub ontology_composition_entries: Vec<PortableV2CompositionEntry>,
155}
156
157#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
159pub struct PortableV2ResearchEntry {
160 pub component_id: String,
161 pub path: String,
162 pub length: u64,
163 pub sha256: String,
164}
165
166#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
167pub struct PortableV2CompositionEntry {
168 pub kind: String,
169 pub identity: PortableV2ExactIdentity,
170 pub path: String,
171 pub media_type: String,
172 pub length: u64,
173 pub sha256: String,
174 pub required_dependencies: Vec<String>,
175}
176
177#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
178#[serde(deny_unknown_fields)]
179pub struct PortableV2ExactIdentity {
180 pub id: String,
181 pub version: String,
182 pub content_digest: String,
183}
184
185#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
186#[serde(deny_unknown_fields)]
187pub struct PortableV2ActivationOverride {
188 pub scope: String,
189 pub subject: PortableV2ExactIdentity,
190 pub mode: String,
191}
192
193#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
194#[serde(deny_unknown_fields)]
195pub struct PortableV2ActivationProfile {
196 pub profile_default: String,
197 pub overrides: Vec<PortableV2ActivationOverride>,
198}
199
200#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
201#[serde(deny_unknown_fields)]
202pub struct PortableV2OntologyModule {
203 pub ontology_id: String,
204 pub version: String,
205 pub content_digest: String,
206 pub dialect: String,
207 pub profile: String,
208}
209
210#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
211#[serde(deny_unknown_fields)]
212pub struct PortableV2BridgeSet {
213 pub bridge_id: String,
214 pub version: String,
215 pub content_digest: String,
216 pub source_modules: Vec<PortableV2ExactIdentity>,
217 pub target_modules: Vec<PortableV2ExactIdentity>,
218}
219
220#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
221#[serde(deny_unknown_fields)]
222pub struct PortableV2OntologyComposition {
223 pub contract: String,
224 pub activation_profile: PortableV2ActivationProfile,
225 pub modules: Vec<PortableV2OntologyModule>,
226 pub bridge_sets: Vec<PortableV2BridgeSet>,
227 pub required_features: Vec<String>,
228 pub optional_features: Vec<String>,
229 pub composition_digest: String,
230}
231
232#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
233#[serde(rename_all = "snake_case")]
234pub enum PortableV2ErrorCode {
235 Cancelled,
236 LimitExceeded,
237 Io,
238 InvalidStructure,
239 InvalidPath,
240 DuplicateEntry,
241 UnsupportedFuture,
242 Incompatible,
243 DigestMismatch,
244 ConcurrentMutation,
245}
246
247#[derive(Debug)]
248pub struct PortableV2Error {
249 pub committed_import: Option<Box<PortableV2CommittedImport>>,
251 pub code: PortableV2ErrorCode,
252 pub entry: Option<String>,
253 detail: &'static str,
254 #[doc(hidden)]
258 pub cause: Option<String>,
259 #[doc(hidden)]
262 pub allocation_identity_allocated_bytes: std::collections::BTreeMap<String, u64>,
263 #[doc(hidden)]
265 pub recovery_reauthentication_read_bytes: u64,
266 #[doc(hidden)]
268 pub recovery_reauthentication_read_calls: u64,
269}
270
271impl PortableV2Error {
272 #[must_use]
274 pub fn new(code: PortableV2ErrorCode, detail: &'static str) -> Self {
275 Self {
276 committed_import: None,
277 code,
278 entry: None,
279 detail,
280 cause: None,
281 allocation_identity_allocated_bytes: std::collections::BTreeMap::new(),
282 recovery_reauthentication_read_bytes: 0,
283 recovery_reauthentication_read_calls: 0,
284 }
285 }
286 #[must_use]
287 pub fn at(code: PortableV2ErrorCode, entry: &str, detail: &'static str) -> Self {
288 Self {
289 committed_import: None,
290 code,
291 entry: Some(entry.chars().take(4096).collect()),
292 detail,
293 cause: None,
294 allocation_identity_allocated_bytes: std::collections::BTreeMap::new(),
295 recovery_reauthentication_read_bytes: 0,
296 recovery_reauthentication_read_calls: 0,
297 }
298 }
299
300 #[must_use]
302 pub fn with_cause(mut self, cause: impl Into<String>) -> Self {
303 self.cause = Some(cause.into());
304 self
305 }
306
307 #[must_use]
308 pub fn with_allocation_identities(
309 mut self,
310 identities: std::collections::BTreeMap<String, u64>,
311 ) -> Self {
312 self.allocation_identity_allocated_bytes = identities;
313 self
314 }
315
316 #[must_use]
317 pub fn with_recovery_reauthentication(mut self, read_bytes: u64, read_calls: u64) -> Self {
318 self.recovery_reauthentication_read_bytes = read_bytes;
319 self.recovery_reauthentication_read_calls = read_calls;
320 self
321 }
322
323 #[must_use]
325 pub fn with_committed_import(mut self, receipt: PortableV2CommittedImport) -> Self {
326 self.committed_import = Some(Box::new(receipt));
327 self
328 }
329}
330
331#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
333pub struct PortableV2CommittedImport {
334 pub operation_uuid: uuid::Uuid,
335 pub generation_uuid: uuid::Uuid,
336 pub generation_manifest_sha256: [u8; 32],
337 pub package_digest: String,
338}
339impl fmt::Display for PortableV2Error {
340 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
341 write!(f, "portable-v2 {:?}: {}", self.code, self.detail)?;
342 if let Some(receipt) = &self.committed_import {
343 write!(
344 f,
345 " committed=true operation_uuid={} generation_uuid={}",
346 receipt.operation_uuid, receipt.generation_uuid
347 )?;
348 }
349 Ok(())
350 }
351}
352impl std::error::Error for PortableV2Error {}
353impl From<crate::GfError> for PortableV2Error {
354 fn from(_: crate::GfError) -> Self {
355 Self::new(
356 PortableV2ErrorCode::Incompatible,
357 "pinned project generation is not exportable",
358 )
359 }
360}
361
362use std::path::PathBuf;
363#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
365pub struct PortableV2OciReference {
366 pub registry: String,
368 pub repository: String,
370 pub oci_manifest_digest: String,
372 pub package_digest: String,
374 pub package_class: PortableV2PackageClass,
376 pub tag: Option<String>,
378 pub bytes_transferred: u64,
380 pub blob_count: u64,
382}
383
384#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
386pub struct PortableV2OciPullReceipt {
387 pub reference: PortableV2OciReference,
389 pub destination: PathBuf,
391 pub report: PortableV2Report,
393 pub signature_state: PortableV2OciSignatureState,
395}
396
397#[derive(Clone, Default, Eq, PartialEq)]
399pub struct PortableV2OciAuthenticityPolicy {
400 pub require_named_signer: Option<String>,
402 pub verification_key: Option<Vec<u8>>,
404}
405
406#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
408#[serde(rename_all = "snake_case")]
409pub enum PortableV2OciSignatureState {
410 Valid,
412 Invalid,
414 Absent,
416 PolicyMismatched,
418}
419
420#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
422#[serde(rename_all = "snake_case")]
423pub enum PortableV2OciPhase {
424 VerifyLocal,
425 UploadBlob,
426 UploadManifest,
427 AttachSignature,
428 Observe,
429 DownloadManifest,
430 DownloadBlob,
431 VerifyPulled,
432 EvaluateAuthenticity,
433}
434
435#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
437pub struct PortableV2OciProgress {
438 pub phase: PortableV2OciPhase,
440 pub bytes_transferred: u64,
442 pub digest: Option<String>,
444}
445
446#[derive(Clone, Eq, PartialEq)]
448pub struct PortableV2OciSignatureMaterial {
449 pub signer: String,
451 pub key_id: String,
453 pub secret: Vec<u8>,
455}
456
457impl std::fmt::Debug for PortableV2OciAuthenticityPolicy {
458 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
459 f.debug_struct("PortableV2OciAuthenticityPolicy")
460 .field("require_named_signer", &self.require_named_signer)
461 .field("verification_key", &"[REDACTED]")
462 .finish()
463 }
464}
465
466impl std::fmt::Debug for PortableV2OciSignatureMaterial {
467 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
468 f.debug_struct("PortableV2OciSignatureMaterial")
469 .field("signer", &self.signer)
470 .field("key_id", &self.key_id)
471 .field("secret", &"[REDACTED]")
472 .finish()
473 }
474}
475
476#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
478pub struct PortableV2ParticipantId {
479 pub capability_id: String,
481 pub record_family_id: String,
483}
484
485#[derive(Clone, Debug, Eq, PartialEq)]
487pub enum PortableV2SelectionProfile {
488 Complete,
490 OntologyOnly,
492 DataComponents,
494 Artifacts,
496 Settings,
498 Custom(Vec<PortableV2ParticipantId>),
500 OntologyComposition(Vec<PortableV2ExactIdentity>),
503}
504
505#[derive(Clone, Debug, Eq, PartialEq)]
507pub struct PortableV2SelectionRequest {
508 pub profile: PortableV2SelectionProfile,
510 pub strict: bool,
512}
513
514#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
516#[serde(rename_all = "kebab-case")]
517pub enum PortableV2SelectionReason {
518 Requested,
520 RequiredSchemaAuthority,
522 RequiredOntologyComposition,
524 ProfileExcluded,
526}
527
528#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
530pub struct PortableV2SelectionEntry {
531 pub identity: PortableV2ParticipantId,
533 pub kind: String,
535 pub reason: PortableV2SelectionReason,
537 pub estimated_bytes: u64,
539 pub row_count: u64,
541}
542
543#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
545pub struct PortableV2ProjectedSelectionEntry {
546 pub kind: String,
548 pub identity: PortableV2ExactIdentity,
550 pub participant_id: String,
552 pub reason: PortableV2SelectionReason,
554 pub estimated_bytes: u64,
556}
557
558#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
560pub struct PortableV2SelectionPlan {
561 pub source_generation_uuid: String,
563 pub source_manifest_sha256: String,
565 pub package_class: String,
567 pub included: Vec<PortableV2SelectionEntry>,
569 pub excluded: Vec<PortableV2SelectionEntry>,
571 pub projected: Vec<PortableV2ProjectedSelectionEntry>,
573 pub redactions: Vec<String>,
575 pub required_capabilities: Vec<String>,
577 pub estimated_payload_bytes: u64,
579 pub selection_fingerprint: String,
581 include_graph_tree: bool,
582}
583
584impl PortableV2SelectionPlan {
585 #[must_use]
587 #[allow(clippy::too_many_arguments)]
588 pub fn new(
589 source_generation_uuid: String,
590 source_manifest_sha256: String,
591 package_class: String,
592 included: Vec<PortableV2SelectionEntry>,
593 excluded: Vec<PortableV2SelectionEntry>,
594 projected: Vec<PortableV2ProjectedSelectionEntry>,
595 redactions: Vec<String>,
596 required_capabilities: Vec<String>,
597 estimated_payload_bytes: u64,
598 selection_fingerprint: String,
599 include_graph_tree: bool,
600 ) -> Self {
601 Self {
602 source_generation_uuid,
603 source_manifest_sha256,
604 package_class,
605 included,
606 excluded,
607 projected,
608 redactions,
609 required_capabilities,
610 estimated_payload_bytes,
611 selection_fingerprint,
612 include_graph_tree,
613 }
614 }
615 #[must_use]
617 pub fn includes_graph_tree(&self) -> bool {
618 self.include_graph_tree
619 }
620 pub fn set_include_graph_tree(&mut self, include: bool) {
622 self.include_graph_tree = include;
623 }
624 #[must_use]
626 pub fn includes(&self, capability: &str, family: &str) -> bool {
627 self.included.iter().any(|entry| {
628 entry.identity.capability_id == capability && entry.identity.record_family_id == family
629 })
630 }
631}
632
633#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)]
635pub struct PortableV2GraphSelector {
636 pub node_uuids: Vec<String>,
638 pub edge_uuids: Vec<String>,
640}
641
642#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
644#[serde(rename_all = "kebab-case")]
645pub enum PortableV2SubsetClosure {
646 InducedEdges,
648 Referential,
650}
651
652#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)]
654pub struct PortableV2PropertyProjection {
655 pub exclude: Vec<String>,
657}
658
659#[derive(Clone, Debug, Eq, PartialEq)]
661pub struct PortableV2SubsetRequest {
662 pub selector: PortableV2GraphSelector,
664 pub closure: PortableV2SubsetClosure,
666 pub projection: PortableV2PropertyProjection,
668}
669
670#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
672pub struct PortableV2GraphSubsetMeta {
673 pub selector: String,
675 pub closure: String,
677}
678
679#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
681pub struct PortableV2SubsetPreview {
682 pub selection: PortableV2SelectionPlan,
684 pub graph_subset: PortableV2GraphSubsetMeta,
686 pub selected_node_count: u64,
688 pub selected_edge_count: u64,
690 pub endpoint_node_count: u64,
692 pub result_fingerprint: String,
694 pub subset_fingerprint: String,
696}
697
698#[derive(Debug, Clone, Copy, PartialEq, Eq)]
699pub enum PortableV2Output {
701 Expanded,
703 Bundle,
705}
706#[derive(Debug, Clone, Copy, PartialEq, Eq)]
707pub struct PortableV2ExportProgress {
709 pub entries_completed: usize,
711 pub bytes_completed: u64,
713 pub entries_total: usize,
715 pub bytes_total: u64,
717}
718
719#[cfg(test)]
720mod preview_wire_tests {
721 use super::*;
722
723 #[test]
724 fn moved_preview_contract_keeps_private_serialized_graph_decision() {
725 let plan = PortableV2SelectionPlan::new(
726 "generation".into(),
727 "manifest".into(),
728 "graph-data-subset".into(),
729 vec![PortableV2SelectionEntry {
730 identity: PortableV2ParticipantId {
731 capability_id: "graph@1".into(),
732 record_family_id: "files@1".into(),
733 },
734 kind: "graph-data".into(),
735 reason: PortableV2SelectionReason::Requested,
736 estimated_bytes: 7,
737 row_count: 2,
738 }],
739 Vec::new(),
740 Vec::new(),
741 vec!["redacted".into()],
742 vec!["graph@1".into()],
743 7,
744 "selection".into(),
745 false,
746 );
747 let expected = serde_json::json!({
748 "source_generation_uuid":"generation", "source_manifest_sha256":"manifest",
749 "package_class":"graph-data-subset", "included":[{"identity":{"capability_id":"graph@1","record_family_id":"files@1"},
750 "kind":"graph-data","reason":"requested","estimated_bytes":7,"row_count":2}],
751 "excluded":[],"projected":[],"redactions":["redacted"],"required_capabilities":["graph@1"],
752 "estimated_payload_bytes":7,"selection_fingerprint":"selection","include_graph_tree":false
753 });
754 assert_eq!(serde_json::to_value(&plan).unwrap(), expected);
755 let subset = PortableV2SubsetPreview {
756 selection: plan,
757 graph_subset: PortableV2GraphSubsetMeta {
758 selector: "selector".into(),
759 closure: "referential".into(),
760 },
761 selected_node_count: 2,
762 selected_edge_count: 1,
763 endpoint_node_count: 1,
764 result_fingerprint: "result".into(),
765 subset_fingerprint: "subset".into(),
766 };
767 assert_eq!(
768 serde_json::to_value(subset).unwrap(),
769 serde_json::json!({
770 "selection":expected,"graph_subset":{"selector":"selector","closure":"referential"},
771 "selected_node_count":2,"selected_edge_count":1,"endpoint_node_count":1,
772 "result_fingerprint":"result","subset_fingerprint":"subset"
773 })
774 );
775 }
776}
777
778#[cfg(test)]
779mod limit_tests {
780 use super::*;
781
782 fn longest_record_without_path() -> u64 {
784 let record = serde_json::json!({
785 "length": u64::MAX,
786 "media_type": "application/vnd.graphforge.ontology-composition+json",
787 "path": "",
788 "sha256": "0".repeat(64),
789 });
790 (serde_json::to_vec(&record).unwrap().len() + ",".len()) as u64
791 }
792
793 #[test]
794 fn default_limits_admit_every_package_a_project_can_produce() {
795 let limits = PortableV2Limits::default();
796 assert_eq!(longest_record_without_path(), 186);
798 let path_budget = PORTABLE_V2_MANIFEST_BYTES_PER_ENTRY - longest_record_without_path();
799 assert_eq!(path_budget, 326);
800 assert_eq!(limits.max_manifest_bytes, 16 * 1024 * 1024);
803 assert_eq!(limits.max_semantic_manifest_bytes, 64 * 1024 * 1024);
804 assert!(
805 PRODUCER_MAX_GRAPH_FILES * PORTABLE_V2_MANIFEST_BYTES_PER_ENTRY
806 <= limits.max_semantic_manifest_bytes
807 );
808 assert!(6_874_538 <= limits.semantic_manifest_bound(21_535));
811 let s28_entries = 4 * 21_535;
812 let s28_manifest = s28_entries * (longest_record_without_path() + 169);
813 assert!(s28_manifest <= limits.semantic_manifest_bound(s28_entries));
814 assert!(169 < path_budget);
815 assert_eq!(limits.semantic_manifest_bound(0), limits.max_manifest_bytes);
817 assert_eq!(
818 limits.semantic_manifest_bound(1),
819 limits.max_manifest_bytes + PORTABLE_V2_MANIFEST_BYTES_PER_ENTRY
820 );
821 assert_eq!(
822 limits.semantic_manifest_bound(limits.max_entries),
823 limits.max_semantic_manifest_bytes
824 );
825 assert_eq!(
826 limits.semantic_manifest_bound(u64::MAX),
827 limits.max_semantic_manifest_bytes
828 );
829 let tag_manifest = ["bag-info.txt", "bagit.txt", "manifest-sha256.txt"]
832 .iter()
833 .map(|path| 64 + 2 + path.len() as u64 + 1)
834 .sum::<u64>();
835 assert!(tag_manifest <= limits.max_tag_manifest_bytes);
836 }
837}