use std::collections::BTreeMap;
use runx_contracts::{Receipt, Reference, ReferenceType};
use super::{ReceiptTreeConfig, ResolvedReceipt};
use crate::{ReceiptFinding, ReceiptFindingCode, validate_receipt};
pub(super) fn duplicate_child_findings(children: &[ResolvedReceipt<'_>]) -> Vec<ReceiptFinding> {
let mut seen = BTreeMap::new();
children
.iter()
.filter_map(|child| {
if seen
.insert(child.receipt.id.as_str(), child.path.as_str())
.is_some()
{
Some(ReceiptFinding {
code: ReceiptFindingCode::DuplicateChildReceipt,
path: format!("{}.id", child.path),
message: "child receipt ids must be unique".to_owned(),
})
} else {
None
}
})
.collect()
}
pub(super) fn child_receipt_findings(children: &[ResolvedReceipt<'_>]) -> Vec<ReceiptFinding> {
children
.iter()
.flat_map(|child| {
validate_receipt(child.receipt)
.err()
.map_or_else(Vec::new, |verification| {
verification
.findings
.into_iter()
.map(|finding| child_finding(&child.path, finding))
.collect()
})
})
.collect()
}
pub(super) fn orphan_child_findings(
children: &[ResolvedReceipt<'_>],
reached: &std::collections::BTreeSet<String>,
) -> Vec<ReceiptFinding> {
children
.iter()
.filter(|child| !reached.contains(child.receipt.id.as_str()))
.map(|child| ReceiptFinding {
code: ReceiptFindingCode::OrphanChildReceipt,
path: format!("{}.id", child.path),
message: "supplied child receipts must be reachable from the root receipt".to_owned(),
})
.collect()
}
pub(super) fn missing_child(path: &str) -> ReceiptFinding {
ReceiptFinding {
code: ReceiptFindingCode::ChildReceiptMissing,
path: path.to_owned(),
message: "child receipt ref must resolve to a supplied child receipt".to_owned(),
}
}
pub(super) fn malformed_child_ref(path: &str) -> ReceiptFinding {
ReceiptFinding {
code: ReceiptFindingCode::ChildReceiptRefMalformed,
path: path.to_owned(),
message: "child receipt ref must be a typed runx receipt URI".to_owned(),
}
}
pub(super) fn ambiguous_child(path: &str) -> ReceiptFinding {
ReceiptFinding {
code: ReceiptFindingCode::ChildReceiptAmbiguous,
path: path.to_owned(),
message: "child receipt ref resolved to multiple supplied receipts".to_owned(),
}
}
pub(super) fn resolver_error(path: &str) -> ReceiptFinding {
ReceiptFinding {
code: ReceiptFindingCode::ChildReceiptResolverError,
path: path.to_owned(),
message: "child receipt ref resolver failed before proof verification".to_owned(),
}
}
pub(super) fn parent_link_findings(
path: &str,
parent: &Receipt,
child: &Receipt,
config: ReceiptTreeConfig,
) -> Vec<ReceiptFinding> {
let parent_uri = format!("runx:receipt:{}", parent.id);
let child_parent = child
.lineage
.as_ref()
.and_then(|lineage| lineage.parent.as_ref());
match child_parent {
Some(parent_ref) if parent_ref.uri == parent_uri => Vec::new(),
Some(_) => vec![ReceiptFinding {
code: ReceiptFindingCode::ChildReceiptParentMismatch,
path: format!("{path}.lineage.parent"),
message: "child lineage parent ref must match the parent receipt".to_owned(),
}],
None if config.require_parent_links => vec![ReceiptFinding {
code: ReceiptFindingCode::ChildReceiptParentMismatch,
path: format!("{path}.lineage.parent"),
message: "strict tree verification requires child lineage parent refs".to_owned(),
}],
None => Vec::new(),
}
}
pub(super) fn child_digest_link_findings(
path: &str,
reference: &Reference,
child: &Receipt,
) -> Vec<ReceiptFinding> {
if reference.locator.as_deref() == Some(child.digest.as_str()) {
return Vec::new();
}
vec![ReceiptFinding {
code: ReceiptFindingCode::ChildReceiptDigestMismatch,
path: format!("{path}.locator"),
message:
"strict tree proof requires child receipt refs to carry the exact child receipt digest"
.to_owned(),
}]
}
pub(super) fn child_finding(path: &str, finding: ReceiptFinding) -> ReceiptFinding {
ReceiptFinding {
path: format!("{path}.{}", finding.path),
..finding
}
}
pub(super) fn join(path: &str, segment: &str) -> String {
if path.is_empty() {
segment.to_owned()
} else {
format!("{path}.{segment}")
}
}
pub(super) fn referenced_receipt_id(reference: &Reference) -> Option<&str> {
if reference.reference_type != ReferenceType::Receipt {
return None;
}
reference
.uri
.strip_prefix("runx:receipt:")
.filter(|id| !id.is_empty())
}