use std::collections::BTreeSet;
use prikk_error::{PrikkError, Result};
use prikk_object::{ObjectEnvelope, ObjectId, ObjectType, Signature, SignerRole};
use crate::author_key_index::{AuthorKeyEntry, lookup_author_key_entries};
use crate::byte_cursor::ByteCursor;
use crate::file_codec::{decode_envelope_file, encode_envelope_file, push_bytes_u64, push_u64};
use crate::fsutil::len_to_u64;
use crate::layout::RepositoryLayout;
use crate::object_store::{ObjectReadSnapshot, ObjectReader};
use crate::patch_replay::decode::{
DecodedDeletePreimage, DecodedOperationKind, decode_patch_operations,
};
use crate::patch_set_digest::{PatchSetDigest, compute_patch_set_digest};
const EXCHANGE_ARTIFACT_MAGIC: &[u8; 8] = b"PEXCH002";
pub const DEFAULT_EXCHANGE_ARTIFACT_MAX_OBJECT_COUNT: usize = 100_000;
pub const DEFAULT_EXCHANGE_ARTIFACT_MAX_TOTAL_BYTES: usize = 256 * 1024 * 1024;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExchangeExportReport {
pub patch_count: usize,
pub blob_count: usize,
pub author_key_count: usize,
pub claim_count: usize,
pub tag_count: usize,
}
#[derive(Debug)]
pub(super) struct DecodedExchangeArtifact {
pub(super) declared_digest: PatchSetDigest,
pub(super) patches: Vec<ObjectEnvelope>,
pub(super) blobs: Vec<ObjectEnvelope>,
pub(super) author_keys: Vec<AuthorKeyEntry>,
pub(super) claims: Vec<ObjectEnvelope>,
pub(super) tags: Vec<ObjectEnvelope>,
}
pub fn export_exchange_artifact(
layout: &RepositoryLayout,
patch_ids: &[ObjectId],
claim_ids: &[ObjectId],
tag_ids: &[ObjectId],
) -> Result<(ExchangeExportReport, Vec<u8>)> {
let mut seen_patch_ids: BTreeSet<ObjectId> = BTreeSet::new();
for patch_id in patch_ids {
if !seen_patch_ids.insert(*patch_id) {
return Err(PrikkError::Integrity(format!(
"patch id {patch_id} is named more than once in the export list"
)));
}
}
let object_store = ObjectReadSnapshot::open(layout)?;
let mut patch_envelopes: Vec<ObjectEnvelope> = Vec::with_capacity(patch_ids.len());
let mut blob_ids: BTreeSet<ObjectId> = BTreeSet::new();
for patch_id in patch_ids {
let envelope = read_required(&object_store, *patch_id, ObjectType::Patch)?;
for operation in
decode_patch_operations(&envelope.canonical_payload, envelope.schema_version)?
{
match operation.kind {
DecodedOperationKind::CreateFile { blob_id, .. } => {
blob_ids.insert(blob_id);
}
DecodedOperationKind::ReplaceBinary {
old_blob_id,
new_blob_id,
..
} => {
blob_ids.insert(old_blob_id);
blob_ids.insert(new_blob_id);
}
DecodedOperationKind::DeleteNode {
preimage: DecodedDeletePreimage::File { old_blob_id, .. },
..
} => {
blob_ids.insert(old_blob_id);
}
_ => {}
}
}
patch_envelopes.push(envelope);
}
let mut blob_envelopes: Vec<ObjectEnvelope> = Vec::with_capacity(blob_ids.len());
for blob_id in &blob_ids {
blob_envelopes.push(read_required(&object_store, *blob_id, ObjectType::Blob)?);
}
let mut author_key_ids: BTreeSet<String> = BTreeSet::new();
for envelope in &patch_envelopes {
if let Some(signature) = envelope
.signatures
.iter()
.find(|signature| signature.signer_role == SignerRole::Author)
{
author_key_ids.insert(signature.key_id.clone());
}
}
let mut author_keys: Vec<AuthorKeyEntry> = Vec::with_capacity(author_key_ids.len());
for key_id in &author_key_ids {
let entries = lookup_author_key_entries(layout, key_id)?;
let mut distinct = entries.iter().map(|entry| entry.public_key);
let Some(first) = distinct.next() else {
continue;
};
if distinct.any(|public_key| public_key != first) {
return Err(PrikkError::Integrity(format!(
"author key_id {key_id} has more than one distinct recorded public key locally; \
refusing to export a provenance claim this repository's own material does not \
agree on"
)));
}
author_keys.push(AuthorKeyEntry {
key_id: key_id.clone(),
public_key: first,
});
}
let mut claim_envelopes: Vec<ObjectEnvelope> = Vec::with_capacity(claim_ids.len());
for claim_id in claim_ids {
claim_envelopes.push(read_required(
&object_store,
*claim_id,
ObjectType::RecognitionClaim,
)?);
}
let mut tag_envelopes: Vec<ObjectEnvelope> = Vec::with_capacity(tag_ids.len());
for tag_id in tag_ids {
tag_envelopes.push(read_required(&object_store, *tag_id, ObjectType::Tag)?);
}
let report = ExchangeExportReport {
patch_count: patch_envelopes.len(),
blob_count: blob_envelopes.len(),
author_key_count: author_keys.len(),
claim_count: claim_envelopes.len(),
tag_count: tag_envelopes.len(),
};
let bytes = encode_exchange_artifact(
patch_ids,
&patch_envelopes,
&blob_envelopes,
&author_keys,
&claim_envelopes,
&tag_envelopes,
)?;
Ok((report, bytes))
}
fn read_required(
object_store: &impl ObjectReader,
id: ObjectId,
object_type: ObjectType,
) -> Result<ObjectEnvelope> {
object_store
.read_typed(id, object_type)?
.ok_or_else(|| PrikkError::Integrity(format!("missing {object_type} object: {id}")))
}
fn encode_exchange_artifact(
patch_ids: &[ObjectId],
patch_envelopes: &[ObjectEnvelope],
blob_envelopes: &[ObjectEnvelope],
author_keys: &[AuthorKeyEntry],
claim_envelopes: &[ObjectEnvelope],
tag_envelopes: &[ObjectEnvelope],
) -> Result<Vec<u8>> {
let mut sorted_patch_ids: Vec<ObjectId> = patch_ids.to_vec();
sorted_patch_ids.sort_unstable();
sorted_patch_ids.dedup();
let digest = compute_patch_set_digest(&sorted_patch_ids)?;
let mut out = Vec::new();
out.extend_from_slice(EXCHANGE_ARTIFACT_MAGIC);
out.extend_from_slice(&digest.0);
push_u64(&mut out, len_to_u64(patch_envelopes.len())?);
for envelope in patch_envelopes {
push_bytes_u64(&mut out, &encode_envelope_file(envelope)?)?;
}
push_u64(&mut out, len_to_u64(blob_envelopes.len())?);
for envelope in blob_envelopes {
push_bytes_u64(&mut out, &encode_envelope_file(envelope)?)?;
}
push_u64(&mut out, len_to_u64(author_keys.len())?);
for entry in author_keys {
push_bytes_u64(&mut out, entry.key_id.as_bytes())?;
out.extend_from_slice(&entry.public_key);
}
push_u64(&mut out, len_to_u64(claim_envelopes.len())?);
for envelope in claim_envelopes {
push_bytes_u64(&mut out, &encode_envelope_file(envelope)?)?;
}
push_u64(&mut out, len_to_u64(tag_envelopes.len())?);
for envelope in tag_envelopes {
push_bytes_u64(&mut out, &encode_envelope_file(envelope)?)?;
}
Ok(out)
}
pub(super) fn decode_exchange_artifact(
bytes: &[u8],
max_object_count: usize,
) -> Result<DecodedExchangeArtifact> {
let mut cursor = ByteCursor::new(bytes);
let magic = cursor.read_array::<8>()?;
if &magic != EXCHANGE_ARTIFACT_MAGIC {
return Err(PrikkError::MalformedData(
"invalid patch-exchange artifact magic".to_string(),
));
}
let declared_digest = PatchSetDigest(cursor.read_array::<32>()?);
let patches = decode_envelope_section(&mut cursor, max_object_count, "patches")?;
let blobs = decode_envelope_section(&mut cursor, max_object_count, "blobs")?;
let author_key_count = cursor.read_u64()?;
if author_key_count > len_to_u64(max_object_count)? {
return Err(PrikkError::MalformedData(format!(
"patch-exchange artifact declares {author_key_count} author key entries, over the \
configured limit of {max_object_count}"
)));
}
let mut author_keys = Vec::new();
for _ in 0..author_key_count {
let key_id_bytes = cursor.read_bytes_u64()?;
let key_id = String::from_utf8(key_id_bytes).map_err(|err| {
PrikkError::MalformedData(format!(
"invalid patch-exchange artifact author key_id utf-8: {err}"
))
})?;
Signature::validate_key_id(&key_id)?;
let public_key = cursor.read_array::<32>()?;
author_keys.push(AuthorKeyEntry { key_id, public_key });
}
let claims = decode_envelope_section(&mut cursor, max_object_count, "recognition claims")?;
let tags = decode_envelope_section(&mut cursor, max_object_count, "tags")?;
if !cursor.is_finished() {
return Err(PrikkError::MalformedData(
"trailing bytes in patch-exchange artifact".to_string(),
));
}
Ok(DecodedExchangeArtifact {
declared_digest,
patches,
blobs,
author_keys,
claims,
tags,
})
}
fn decode_envelope_section(
cursor: &mut ByteCursor<'_>,
max_object_count: usize,
section_name: &str,
) -> Result<Vec<ObjectEnvelope>> {
let count = cursor.read_u64()?;
if count > len_to_u64(max_object_count)? {
return Err(PrikkError::MalformedData(format!(
"patch-exchange artifact declares {count} {section_name}, over the configured limit \
of {max_object_count}"
)));
}
let mut envelopes = Vec::new();
for _ in 0..count {
let encoded = cursor.read_bytes_u64()?;
envelopes.push(decode_envelope_file(&encoded)?);
}
Ok(envelopes)
}
#[cfg(test)]
mod tests;