#![allow(missing_docs)]
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Clone, Copy, Debug)]
pub struct PortableV2Limits {
pub max_components: u64,
pub max_entries: u64,
pub max_entry_bytes: u64,
pub max_total_bytes: u64,
pub max_manifest_bytes: u64,
pub max_semantic_manifest_bytes: u64,
pub max_tag_manifest_bytes: u64,
pub max_path_bytes: usize,
pub copy_buffer_bytes: usize,
}
pub const PORTABLE_V2_MANIFEST_BYTES_PER_ENTRY: u64 = 512;
const PRODUCER_MAX_GRAPH_FILES: u64 = 100_000;
impl PortableV2Limits {
#[must_use]
pub fn semantic_manifest_bound(&self, entries: u64) -> u64 {
self.max_manifest_bytes
.saturating_add(entries.saturating_mul(PORTABLE_V2_MANIFEST_BYTES_PER_ENTRY))
.min(self.max_semantic_manifest_bytes)
}
}
impl Default for PortableV2Limits {
fn default() -> Self {
Self {
max_components: 10_000,
max_entries: 1_000_000,
max_entry_bytes: 16 * 1024_u64.pow(4),
max_total_bytes: 1024 * 1024_u64.pow(4),
max_manifest_bytes: 16 * 1024 * 1024,
max_semantic_manifest_bytes: (PRODUCER_MAX_GRAPH_FILES
* PORTABLE_V2_MANIFEST_BYTES_PER_ENTRY)
.next_power_of_two(),
max_tag_manifest_bytes: 4 * 1024 * 1024,
max_path_bytes: 4096,
copy_buffer_bytes: 1024 * 1024,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PortableV2Mode {
StructureOnly,
Full,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PortableV2Representation {
Expanded,
Bundle,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum PortableV2PackageClass {
Complete,
OntologyOnly,
ComponentSelective,
GraphDataSubset,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PortableV2Integrity {
NotChecked,
Verified,
Failed,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PortableV2Compatibility {
Supported,
UnsupportedFuture,
Failed,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PortableV2Authenticity {
NotEvaluated,
Unsigned,
Verified,
Failed,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct PortableV2Report {
pub research_interchange: bool,
pub research_entries: Vec<PortableV2ResearchEntry>,
pub contract: &'static str,
pub representation: PortableV2Representation,
pub package_digest: String,
pub package_class: PortableV2PackageClass,
pub component_count: u64,
pub entry_count: u64,
pub payload_bytes: u64,
pub integrity: PortableV2Integrity,
pub compatibility: PortableV2Compatibility,
pub authenticity: PortableV2Authenticity,
pub transport_digest: Option<String>,
pub ontology_composition: Option<PortableV2OntologyComposition>,
pub ontology_composition_entries: Vec<PortableV2CompositionEntry>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct PortableV2ResearchEntry {
pub component_id: String,
pub path: String,
pub length: u64,
pub sha256: String,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct PortableV2CompositionEntry {
pub kind: String,
pub identity: PortableV2ExactIdentity,
pub path: String,
pub media_type: String,
pub length: u64,
pub sha256: String,
pub required_dependencies: Vec<String>,
}
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PortableV2ExactIdentity {
pub id: String,
pub version: String,
pub content_digest: String,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PortableV2ActivationOverride {
pub scope: String,
pub subject: PortableV2ExactIdentity,
pub mode: String,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PortableV2ActivationProfile {
pub profile_default: String,
pub overrides: Vec<PortableV2ActivationOverride>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PortableV2OntologyModule {
pub ontology_id: String,
pub version: String,
pub content_digest: String,
pub dialect: String,
pub profile: String,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PortableV2BridgeSet {
pub bridge_id: String,
pub version: String,
pub content_digest: String,
pub source_modules: Vec<PortableV2ExactIdentity>,
pub target_modules: Vec<PortableV2ExactIdentity>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PortableV2OntologyComposition {
pub contract: String,
pub activation_profile: PortableV2ActivationProfile,
pub modules: Vec<PortableV2OntologyModule>,
pub bridge_sets: Vec<PortableV2BridgeSet>,
pub required_features: Vec<String>,
pub optional_features: Vec<String>,
pub composition_digest: String,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PortableV2ErrorCode {
Cancelled,
LimitExceeded,
Io,
InvalidStructure,
InvalidPath,
DuplicateEntry,
UnsupportedFuture,
Incompatible,
DigestMismatch,
ConcurrentMutation,
}
#[derive(Debug)]
pub struct PortableV2Error {
pub committed_import: Option<Box<PortableV2CommittedImport>>,
pub code: PortableV2ErrorCode,
pub entry: Option<String>,
detail: &'static str,
#[doc(hidden)]
pub cause: Option<String>,
#[doc(hidden)]
pub allocation_identity_allocated_bytes: std::collections::BTreeMap<String, u64>,
#[doc(hidden)]
pub recovery_reauthentication_read_bytes: u64,
#[doc(hidden)]
pub recovery_reauthentication_read_calls: u64,
}
impl PortableV2Error {
#[must_use]
pub fn new(code: PortableV2ErrorCode, detail: &'static str) -> Self {
Self {
committed_import: None,
code,
entry: None,
detail,
cause: None,
allocation_identity_allocated_bytes: std::collections::BTreeMap::new(),
recovery_reauthentication_read_bytes: 0,
recovery_reauthentication_read_calls: 0,
}
}
#[must_use]
pub fn at(code: PortableV2ErrorCode, entry: &str, detail: &'static str) -> Self {
Self {
committed_import: None,
code,
entry: Some(entry.chars().take(4096).collect()),
detail,
cause: None,
allocation_identity_allocated_bytes: std::collections::BTreeMap::new(),
recovery_reauthentication_read_bytes: 0,
recovery_reauthentication_read_calls: 0,
}
}
#[must_use]
pub fn with_cause(mut self, cause: impl Into<String>) -> Self {
self.cause = Some(cause.into());
self
}
#[must_use]
pub fn with_allocation_identities(
mut self,
identities: std::collections::BTreeMap<String, u64>,
) -> Self {
self.allocation_identity_allocated_bytes = identities;
self
}
#[must_use]
pub fn with_recovery_reauthentication(mut self, read_bytes: u64, read_calls: u64) -> Self {
self.recovery_reauthentication_read_bytes = read_bytes;
self.recovery_reauthentication_read_calls = read_calls;
self
}
#[must_use]
pub fn with_committed_import(mut self, receipt: PortableV2CommittedImport) -> Self {
self.committed_import = Some(Box::new(receipt));
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct PortableV2CommittedImport {
pub operation_uuid: uuid::Uuid,
pub generation_uuid: uuid::Uuid,
pub generation_manifest_sha256: [u8; 32],
pub package_digest: String,
}
impl fmt::Display for PortableV2Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "portable-v2 {:?}: {}", self.code, self.detail)?;
if let Some(receipt) = &self.committed_import {
write!(
f,
" committed=true operation_uuid={} generation_uuid={}",
receipt.operation_uuid, receipt.generation_uuid
)?;
}
Ok(())
}
}
impl std::error::Error for PortableV2Error {}
impl From<crate::GfError> for PortableV2Error {
fn from(_: crate::GfError) -> Self {
Self::new(
PortableV2ErrorCode::Incompatible,
"pinned project generation is not exportable",
)
}
}
use std::path::PathBuf;
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct PortableV2OciReference {
pub registry: String,
pub repository: String,
pub oci_manifest_digest: String,
pub package_digest: String,
pub package_class: PortableV2PackageClass,
pub tag: Option<String>,
pub bytes_transferred: u64,
pub blob_count: u64,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct PortableV2OciPullReceipt {
pub reference: PortableV2OciReference,
pub destination: PathBuf,
pub report: PortableV2Report,
pub signature_state: PortableV2OciSignatureState,
}
#[derive(Clone, Default, Eq, PartialEq)]
pub struct PortableV2OciAuthenticityPolicy {
pub require_named_signer: Option<String>,
pub verification_key: Option<Vec<u8>>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PortableV2OciSignatureState {
Valid,
Invalid,
Absent,
PolicyMismatched,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PortableV2OciPhase {
VerifyLocal,
UploadBlob,
UploadManifest,
AttachSignature,
Observe,
DownloadManifest,
DownloadBlob,
VerifyPulled,
EvaluateAuthenticity,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct PortableV2OciProgress {
pub phase: PortableV2OciPhase,
pub bytes_transferred: u64,
pub digest: Option<String>,
}
#[derive(Clone, Eq, PartialEq)]
pub struct PortableV2OciSignatureMaterial {
pub signer: String,
pub key_id: String,
pub secret: Vec<u8>,
}
impl std::fmt::Debug for PortableV2OciAuthenticityPolicy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PortableV2OciAuthenticityPolicy")
.field("require_named_signer", &self.require_named_signer)
.field("verification_key", &"[REDACTED]")
.finish()
}
}
impl std::fmt::Debug for PortableV2OciSignatureMaterial {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PortableV2OciSignatureMaterial")
.field("signer", &self.signer)
.field("key_id", &self.key_id)
.field("secret", &"[REDACTED]")
.finish()
}
}
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
pub struct PortableV2ParticipantId {
pub capability_id: String,
pub record_family_id: String,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PortableV2SelectionProfile {
Complete,
OntologyOnly,
DataComponents,
Artifacts,
Settings,
Custom(Vec<PortableV2ParticipantId>),
OntologyComposition(Vec<PortableV2ExactIdentity>),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PortableV2SelectionRequest {
pub profile: PortableV2SelectionProfile,
pub strict: bool,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum PortableV2SelectionReason {
Requested,
RequiredSchemaAuthority,
RequiredOntologyComposition,
ProfileExcluded,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct PortableV2SelectionEntry {
pub identity: PortableV2ParticipantId,
pub kind: String,
pub reason: PortableV2SelectionReason,
pub estimated_bytes: u64,
pub row_count: u64,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct PortableV2ProjectedSelectionEntry {
pub kind: String,
pub identity: PortableV2ExactIdentity,
pub participant_id: String,
pub reason: PortableV2SelectionReason,
pub estimated_bytes: u64,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct PortableV2SelectionPlan {
pub source_generation_uuid: String,
pub source_manifest_sha256: String,
pub package_class: String,
pub included: Vec<PortableV2SelectionEntry>,
pub excluded: Vec<PortableV2SelectionEntry>,
pub projected: Vec<PortableV2ProjectedSelectionEntry>,
pub redactions: Vec<String>,
pub required_capabilities: Vec<String>,
pub estimated_payload_bytes: u64,
pub selection_fingerprint: String,
include_graph_tree: bool,
}
impl PortableV2SelectionPlan {
#[must_use]
#[allow(clippy::too_many_arguments)]
pub fn new(
source_generation_uuid: String,
source_manifest_sha256: String,
package_class: String,
included: Vec<PortableV2SelectionEntry>,
excluded: Vec<PortableV2SelectionEntry>,
projected: Vec<PortableV2ProjectedSelectionEntry>,
redactions: Vec<String>,
required_capabilities: Vec<String>,
estimated_payload_bytes: u64,
selection_fingerprint: String,
include_graph_tree: bool,
) -> Self {
Self {
source_generation_uuid,
source_manifest_sha256,
package_class,
included,
excluded,
projected,
redactions,
required_capabilities,
estimated_payload_bytes,
selection_fingerprint,
include_graph_tree,
}
}
#[must_use]
pub fn includes_graph_tree(&self) -> bool {
self.include_graph_tree
}
pub fn set_include_graph_tree(&mut self, include: bool) {
self.include_graph_tree = include;
}
#[must_use]
pub fn includes(&self, capability: &str, family: &str) -> bool {
self.included.iter().any(|entry| {
entry.identity.capability_id == capability && entry.identity.record_family_id == family
})
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)]
pub struct PortableV2GraphSelector {
pub node_uuids: Vec<String>,
pub edge_uuids: Vec<String>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum PortableV2SubsetClosure {
InducedEdges,
Referential,
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)]
pub struct PortableV2PropertyProjection {
pub exclude: Vec<String>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PortableV2SubsetRequest {
pub selector: PortableV2GraphSelector,
pub closure: PortableV2SubsetClosure,
pub projection: PortableV2PropertyProjection,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct PortableV2GraphSubsetMeta {
pub selector: String,
pub closure: String,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct PortableV2SubsetPreview {
pub selection: PortableV2SelectionPlan,
pub graph_subset: PortableV2GraphSubsetMeta,
pub selected_node_count: u64,
pub selected_edge_count: u64,
pub endpoint_node_count: u64,
pub result_fingerprint: String,
pub subset_fingerprint: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PortableV2Output {
Expanded,
Bundle,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PortableV2ExportProgress {
pub entries_completed: usize,
pub bytes_completed: u64,
pub entries_total: usize,
pub bytes_total: u64,
}
#[cfg(test)]
mod preview_wire_tests {
use super::*;
#[test]
fn moved_preview_contract_keeps_private_serialized_graph_decision() {
let plan = PortableV2SelectionPlan::new(
"generation".into(),
"manifest".into(),
"graph-data-subset".into(),
vec![PortableV2SelectionEntry {
identity: PortableV2ParticipantId {
capability_id: "graph@1".into(),
record_family_id: "files@1".into(),
},
kind: "graph-data".into(),
reason: PortableV2SelectionReason::Requested,
estimated_bytes: 7,
row_count: 2,
}],
Vec::new(),
Vec::new(),
vec!["redacted".into()],
vec!["graph@1".into()],
7,
"selection".into(),
false,
);
let expected = serde_json::json!({
"source_generation_uuid":"generation", "source_manifest_sha256":"manifest",
"package_class":"graph-data-subset", "included":[{"identity":{"capability_id":"graph@1","record_family_id":"files@1"},
"kind":"graph-data","reason":"requested","estimated_bytes":7,"row_count":2}],
"excluded":[],"projected":[],"redactions":["redacted"],"required_capabilities":["graph@1"],
"estimated_payload_bytes":7,"selection_fingerprint":"selection","include_graph_tree":false
});
assert_eq!(serde_json::to_value(&plan).unwrap(), expected);
let subset = PortableV2SubsetPreview {
selection: plan,
graph_subset: PortableV2GraphSubsetMeta {
selector: "selector".into(),
closure: "referential".into(),
},
selected_node_count: 2,
selected_edge_count: 1,
endpoint_node_count: 1,
result_fingerprint: "result".into(),
subset_fingerprint: "subset".into(),
};
assert_eq!(
serde_json::to_value(subset).unwrap(),
serde_json::json!({
"selection":expected,"graph_subset":{"selector":"selector","closure":"referential"},
"selected_node_count":2,"selected_edge_count":1,"endpoint_node_count":1,
"result_fingerprint":"result","subset_fingerprint":"subset"
})
);
}
}
#[cfg(test)]
mod limit_tests {
use super::*;
fn longest_record_without_path() -> u64 {
let record = serde_json::json!({
"length": u64::MAX,
"media_type": "application/vnd.graphforge.ontology-composition+json",
"path": "",
"sha256": "0".repeat(64),
});
(serde_json::to_vec(&record).unwrap().len() + ",".len()) as u64
}
#[test]
fn default_limits_admit_every_package_a_project_can_produce() {
let limits = PortableV2Limits::default();
assert_eq!(longest_record_without_path(), 186);
let path_budget = PORTABLE_V2_MANIFEST_BYTES_PER_ENTRY - longest_record_without_path();
assert_eq!(path_budget, 326);
assert_eq!(limits.max_manifest_bytes, 16 * 1024 * 1024);
assert_eq!(limits.max_semantic_manifest_bytes, 64 * 1024 * 1024);
assert!(
PRODUCER_MAX_GRAPH_FILES * PORTABLE_V2_MANIFEST_BYTES_PER_ENTRY
<= limits.max_semantic_manifest_bytes
);
assert!(6_874_538 <= limits.semantic_manifest_bound(21_535));
let s28_entries = 4 * 21_535;
let s28_manifest = s28_entries * (longest_record_without_path() + 169);
assert!(s28_manifest <= limits.semantic_manifest_bound(s28_entries));
assert!(169 < path_budget);
assert_eq!(limits.semantic_manifest_bound(0), limits.max_manifest_bytes);
assert_eq!(
limits.semantic_manifest_bound(1),
limits.max_manifest_bytes + PORTABLE_V2_MANIFEST_BYTES_PER_ENTRY
);
assert_eq!(
limits.semantic_manifest_bound(limits.max_entries),
limits.max_semantic_manifest_bytes
);
assert_eq!(
limits.semantic_manifest_bound(u64::MAX),
limits.max_semantic_manifest_bytes
);
let tag_manifest = ["bag-info.txt", "bagit.txt", "manifest-sha256.txt"]
.iter()
.map(|path| 64 + 2 + path.len() as u64 + 1)
.sum::<u64>();
assert!(tag_manifest <= limits.max_tag_manifest_bytes);
}
}