mod proptest_decode_bundle;
use prikk_object::{
BlockKind, CanonicalEncode, CreateFile, NodeId, ObjectEnvelope, ObjectId, ObjectType,
Operation, OperationKind, PatchPayload, PatchPurpose, RefKind, RefStatePayload,
RefUpdatePayload, TagPayload,
};
use crate::author_key_index::{
AuthorKeyEntry, force_conflicting_author_key_entry_for_test, lookup_author_key_entries,
record_author_key_material, verify_author_signature,
};
use crate::author_signing::{AuthorSigner, author_signature};
use crate::bundle::{
BundleImportOptions, DEFAULT_BUNDLE_MAX_OBJECT_COUNT, decode_bundle, encode_bundle,
encode_bundle_v1_for_test, export_bundle, import_bundle,
};
use crate::layout::{ContainerSlot, LockableContainer};
use crate::lock::{ActiveLock, acquire_container_locks};
use crate::received::read_received_pointer;
use crate::test_support::{
rollback_patch_blob_envelope, signed_block, signed_patch_blob_envelope, signed_patch_envelope,
signed_ref_state_envelope, signed_ref_update_envelope, unique_temp_dir,
};
use crate::{
Ed25519AuthorSigner, Ed25519MaintainerSigner, FileObjectStore, MaintainerSigner, ObjectReader,
ObjectWriter, RefPublication, RefStore, RepositoryLayout,
};
fn seal_two_block_history(
layout: &RepositoryLayout,
) -> prikk_error::Result<prikk_object::ObjectId> {
let mut object_store = FileObjectStore::new(layout.clone());
object_store.write_object(&signed_patch_blob_envelope())?;
let patch = signed_patch_envelope();
let patch_id = object_store.write_object(&patch)?;
let root_block = signed_block(BlockKind::Root, Vec::new(), Vec::new(), None);
let root_block_id = object_store.write_object(&root_block)?;
let child_block = signed_block(BlockKind::Normal, vec![root_block_id], vec![patch_id], None);
let child_block_id = object_store.write_object(&child_block)?;
let ref_store = RefStore::new(layout.clone());
let ref_state = signed_ref_state_envelope("heads/main", None, child_block_id, 1);
let ref_state_id = ref_state.object_id();
let ref_update =
signed_ref_update_envelope("heads/main", None, ref_state_id, child_block_id, 1);
ref_store.publish(&RefPublication {
ref_name: "heads/main".to_string(),
expected_previous_ref_state_id: None,
ref_state,
ref_update,
})?;
Ok(child_block_id)
}
fn seal_two_block_history_with_snapshot_blob(
layout: &RepositoryLayout,
) -> prikk_error::Result<(ObjectId, ObjectId)> {
let mut object_store = FileObjectStore::new(layout.clone());
object_store.write_object(&signed_patch_blob_envelope())?;
let patch = signed_patch_envelope();
let patch_id = object_store.write_object(&patch)?;
let snapshot_blob = rollback_patch_blob_envelope();
let snapshot_blob_id = object_store.write_object(&snapshot_blob)?;
let root_block = signed_block(
BlockKind::Root,
Vec::new(),
Vec::new(),
Some(snapshot_blob_id),
);
let root_block_id = object_store.write_object(&root_block)?;
let child_block = signed_block(BlockKind::Normal, vec![root_block_id], vec![patch_id], None);
let child_block_id = object_store.write_object(&child_block)?;
let ref_store = RefStore::new(layout.clone());
let ref_state = signed_ref_state_envelope("heads/main", None, child_block_id, 1);
let ref_state_id = ref_state.object_id();
let ref_update =
signed_ref_update_envelope("heads/main", None, ref_state_id, child_block_id, 1);
ref_store.publish(&RefPublication {
ref_name: "heads/main".to_string(),
expected_previous_ref_state_id: None,
ref_state,
ref_update,
})?;
Ok((child_block_id, snapshot_blob_id))
}
fn transport_test_signer(discriminant: u8) -> prikk_error::Result<Ed25519AuthorSigner> {
Ed25519AuthorSigner::from_seed(
format!("dc53-stage2-transport-{discriminant}"),
&[discriminant; 32],
)
}
fn seal_two_block_history_with_author(
layout: &RepositoryLayout,
signer: &Ed25519AuthorSigner,
record_locally: bool,
) -> prikk_error::Result<ObjectId> {
let mut object_store = FileObjectStore::new(layout.clone());
let blob = signed_patch_blob_envelope();
object_store.write_object(&blob)?;
let payload = PatchPayload {
operations: vec![Operation {
op_seq: 1,
op_id: None,
preconditions: Vec::new(),
kind: OperationKind::CreateFile(CreateFile {
path: "transport.txt".to_string(),
node_id: NodeId::from_bytes([0x54; 32]),
blob_id: blob.object_id(),
mode: 0o100_644,
}),
}],
intent: None,
preconditions: Vec::new(),
purpose: PatchPurpose::Normal,
};
let mut patch = ObjectEnvelope::unsigned(ObjectType::Patch, 1, payload.to_canonical_bytes()?);
let patch_object_id = patch.object_id();
let signature = author_signature(signer, patch_object_id)?;
patch.add_signature(signature)?;
let patch_id = object_store.write_object(&patch)?;
if record_locally {
let active_lock = ActiveLock::acquire(layout)?;
record_author_key_material(
layout,
signer.key_id(),
signer.public_key_bytes(),
&active_lock,
)?;
}
let root_block = signed_block(BlockKind::Root, Vec::new(), Vec::new(), None);
let root_block_id = object_store.write_object(&root_block)?;
let child_block = signed_block(BlockKind::Normal, vec![root_block_id], vec![patch_id], None);
let child_block_id = object_store.write_object(&child_block)?;
let ref_store = RefStore::new(layout.clone());
let ref_state = signed_ref_state_envelope("heads/main", None, child_block_id, 1);
let ref_state_id = ref_state.object_id();
let ref_update =
signed_ref_update_envelope("heads/main", None, ref_state_id, child_block_id, 1);
ref_store.publish(&RefPublication {
ref_name: "heads/main".to_string(),
expected_previous_ref_state_id: None,
ref_state,
ref_update,
})?;
Ok(child_block_id)
}
fn find_imported_transport_patch(target: &RepositoryLayout) -> prikk_error::Result<ObjectEnvelope> {
let target_store = FileObjectStore::new(target.clone());
let ref_state_id = read_received_pointer(target, "remotes/heads/main")?
.ok_or_else(|| {
prikk_error::PrikkError::Integrity("received ref state missing".to_string())
})?
.ref_state_id;
let ref_state_envelope = target_store
.read_typed(ref_state_id, ObjectType::RefState)?
.ok_or_else(|| {
prikk_error::PrikkError::Integrity("received RefState missing".to_string())
})?;
let ref_state_payload = prikk_object::RefStatePayload::decode_canonical(
&ref_state_envelope.canonical_payload,
ref_state_envelope.schema_version,
)?;
let block_envelope = target_store
.read_typed(ref_state_payload.target_object_id, ObjectType::Block)?
.ok_or_else(|| prikk_error::PrikkError::Integrity("received Block missing".to_string()))?;
let block_payload =
prikk_object::BlockPayload::decode_canonical(&block_envelope.canonical_payload)?;
for patch_id in &block_payload.patch_ids {
if let Some(envelope) = target_store.read_typed(*patch_id, ObjectType::Patch)? {
return Ok(envelope);
}
}
Err(prikk_error::PrikkError::Integrity(
"no Patch found in received Block".to_string(),
))
}
#[test]
fn export_of_missing_ref_fails() {
let root = unique_temp_dir("bundle-export-missing-ref");
let layout = RepositoryLayout::init(root.clone());
assert!(layout.is_ok());
if let Ok(layout) = layout {
assert!(export_bundle(&layout, "heads/main").is_err());
}
let _ = std::fs::remove_dir_all(root);
}
#[test]
fn import_of_malformed_bytes_fails_closed() {
let root = unique_temp_dir("bundle-import-malformed");
let layout = RepositoryLayout::init(root.clone());
assert!(layout.is_ok());
if let Ok(layout) = layout {
let options = BundleImportOptions::default_limits();
assert!(import_bundle(&layout, b"not a bundle", &options).is_err());
assert!(import_bundle(&layout, b"PBNDL001", &options).is_err());
assert!(import_bundle(&layout, &[], &options).is_err());
}
let _ = std::fs::remove_dir_all(root);
}
#[test]
fn export_then_import_carries_the_full_genesis_complete_closure() -> prikk_error::Result<()> {
let source_root = unique_temp_dir("bundle-export-source");
let source = RepositoryLayout::init(source_root.clone())?;
let child_block_id = seal_two_block_history(&source)?;
let (report, bytes) = export_bundle(&source, "heads/main")?;
assert_eq!(report.ref_name, "heads/main");
assert_eq!(report.tip_block_id, child_block_id);
assert_eq!(report.object_count, 5);
let target_root = unique_temp_dir("bundle-import-target");
let target = RepositoryLayout::init(target_root.clone())?;
let import_report = import_bundle(&target, &bytes, &BundleImportOptions::default_limits())?;
assert_eq!(import_report.ref_name, "remotes/heads/main");
assert_eq!(import_report.object_count, 5);
assert_eq!(import_report.written_object_count, 5);
let pointer = read_received_pointer(&target, "remotes/heads/main")?;
assert!(pointer.is_some());
if let Some(pointer) = pointer {
assert_eq!(pointer.ref_state_id, import_report.ref_state_id);
}
let target_objects = FileObjectStore::new(target.clone());
assert!(
target_objects
.read_typed(child_block_id, ObjectType::Block)?
.is_some()
);
let _ = std::fs::remove_dir_all(source_root);
let _ = std::fs::remove_dir_all(target_root);
Ok(())
}
#[test]
fn import_refuses_while_received_index_lock_is_externally_held() -> prikk_error::Result<()> {
let source_root = unique_temp_dir("bundle-lock-conflict-source");
let source = RepositoryLayout::init(source_root.clone())?;
seal_two_block_history(&source)?;
let (_, bytes) = export_bundle(&source, "heads/main")?;
let target_root = unique_temp_dir("bundle-lock-conflict-target");
let target = RepositoryLayout::init(target_root.clone())?;
let held = acquire_container_locks(&target, &[LockableContainer::ReceivedIndex])?;
assert!(import_bundle(&target, &bytes, &BundleImportOptions::default_limits()).is_err());
assert!(read_received_pointer(&target, "remotes/heads/main")?.is_none());
drop(held);
let report = import_bundle(&target, &bytes, &BundleImportOptions::default_limits())?;
assert_eq!(report.ref_name, "remotes/heads/main");
assert!(read_received_pointer(&target, "remotes/heads/main")?.is_some());
let _ = std::fs::remove_dir_all(source_root);
let _ = std::fs::remove_dir_all(target_root);
Ok(())
}
#[test]
fn import_never_writes_a_local_ref_pointer() -> prikk_error::Result<()> {
let source_root = unique_temp_dir("bundle-negctrl-source");
let source = RepositoryLayout::init(source_root.clone())?;
seal_two_block_history(&source)?;
let (_, bytes) = export_bundle(&source, "heads/main")?;
let target_root = unique_temp_dir("bundle-negctrl-target");
let target = RepositoryLayout::init(target_root.clone())?;
let ref_store = RefStore::new(target.clone());
let before = ref_store.list_ref_pointers()?;
assert!(before.is_empty());
import_bundle(&target, &bytes, &BundleImportOptions::default_limits())?;
let after = RefStore::new(target.clone()).list_ref_pointers()?;
assert_eq!(
before, after,
"bundle import must never advance or create a local heads/*-or-tags/* ref pointer"
);
let _ = std::fs::remove_dir_all(source_root);
let _ = std::fs::remove_dir_all(target_root);
Ok(())
}
#[test]
fn reimporting_the_same_bundle_is_idempotent() -> prikk_error::Result<()> {
let source_root = unique_temp_dir("bundle-reimport-source");
let source = RepositoryLayout::init(source_root.clone())?;
seal_two_block_history(&source)?;
let (_, bytes) = export_bundle(&source, "heads/main")?;
let target_root = unique_temp_dir("bundle-reimport-target");
let target = RepositoryLayout::init(target_root.clone())?;
let options = BundleImportOptions::default_limits();
let first = import_bundle(&target, &bytes, &options)?;
assert_eq!(first.written_object_count, 5);
let second = import_bundle(&target, &bytes, &options)?;
assert_eq!(second.written_object_count, 0);
assert_eq!(second.ref_state_id, first.ref_state_id);
let _ = std::fs::remove_dir_all(source_root);
let _ = std::fs::remove_dir_all(target_root);
Ok(())
}
#[test]
fn import_object_count_limit_fires_exactly_at_the_boundary() -> prikk_error::Result<()> {
let source_root = unique_temp_dir("bundle-limit-count-boundary-source");
let source = RepositoryLayout::init(source_root.clone())?;
seal_two_block_history(&source)?;
let (report, bytes) = export_bundle(&source, "heads/main")?;
assert_eq!(report.object_count, 5);
let under_root = unique_temp_dir("bundle-limit-count-boundary-under");
let under_target = RepositoryLayout::init(under_root.clone())?;
let refused = import_bundle(
&under_target,
&bytes,
&BundleImportOptions::default_limits().with_max_object_count(4),
);
assert!(
refused.is_err(),
"a limit one below the actual count (5) must refuse"
);
let at_root = unique_temp_dir("bundle-limit-count-boundary-at");
let at_target = RepositoryLayout::init(at_root.clone())?;
let accepted = import_bundle(
&at_target,
&bytes,
&BundleImportOptions::default_limits().with_max_object_count(5),
);
assert!(
accepted.is_ok(),
"a limit exactly at the actual count (5) must accept"
);
let _ = std::fs::remove_dir_all(source_root);
let _ = std::fs::remove_dir_all(under_root);
let _ = std::fs::remove_dir_all(at_root);
Ok(())
}
#[test]
fn import_total_bytes_limit_fires_exactly_at_the_boundary() -> prikk_error::Result<()> {
let source_root = unique_temp_dir("bundle-limit-bytes-boundary-source");
let source = RepositoryLayout::init(source_root.clone())?;
seal_two_block_history(&source)?;
let (_, bytes) = export_bundle(&source, "heads/main")?;
let under_root = unique_temp_dir("bundle-limit-bytes-boundary-under");
let under_target = RepositoryLayout::init(under_root.clone())?;
let refused = import_bundle(
&under_target,
&bytes,
&BundleImportOptions::default_limits().with_max_total_bytes(bytes.len() - 1),
);
assert!(
refused.is_err(),
"a byte limit one below the bundle's own length must refuse"
);
let at_root = unique_temp_dir("bundle-limit-bytes-boundary-at");
let at_target = RepositoryLayout::init(at_root.clone())?;
let accepted = import_bundle(
&at_target,
&bytes,
&BundleImportOptions::default_limits().with_max_total_bytes(bytes.len()),
);
assert!(
accepted.is_ok(),
"a byte limit exactly at the bundle's own length must accept"
);
let _ = std::fs::remove_dir_all(source_root);
let _ = std::fs::remove_dir_all(under_root);
let _ = std::fs::remove_dir_all(at_root);
Ok(())
}
#[test]
fn import_refused_over_the_object_count_limit_writes_nothing() -> prikk_error::Result<()> {
let source_root = unique_temp_dir("bundle-limit-writes-nothing-source");
let source = RepositoryLayout::init(source_root.clone())?;
seal_two_block_history(&source)?;
let (_, bytes) = export_bundle(&source, "heads/main")?;
let target_root = unique_temp_dir("bundle-limit-writes-nothing-target");
let target = RepositoryLayout::init(target_root.clone())?;
let before = crate::verify_repository(&target)?.checked_objects;
let refused = import_bundle(
&target,
&bytes,
&BundleImportOptions::default_limits().with_max_object_count(4),
);
assert!(refused.is_err());
let after = crate::verify_repository(&target)?.checked_objects;
assert_eq!(
before, after,
"a refused import must leave the object store's checked-object count unchanged"
);
assert_eq!(before, Some(0));
let _ = std::fs::remove_dir_all(source_root);
let _ = std::fs::remove_dir_all(target_root);
Ok(())
}
#[test]
fn dc53_stage2_vector7_omitted_material_imports_as_unverifiable() -> prikk_error::Result<()> {
let source_root = unique_temp_dir("dc53-vector7-source");
let source = RepositoryLayout::init(source_root.clone())?;
let signer = transport_test_signer(0xa1)?;
seal_two_block_history_with_author(&source, &signer, false)?;
let (report, bytes) = export_bundle(&source, "heads/main")?;
assert_eq!(
report.author_key_count, 0,
"no local material exists for this key_id, so the section must omit it"
);
let target_root = unique_temp_dir("dc53-vector7-target");
let target = RepositoryLayout::init(target_root.clone())?;
let import_report = import_bundle(&target, &bytes, &BundleImportOptions::default_limits())?;
assert_eq!(import_report.recorded_author_key_count, 0);
let imported_patch = find_imported_transport_patch(&target)?;
let outcome = verify_author_signature(&target, &imported_patch)?;
assert_eq!(
outcome,
Some((signer.key_id().to_string(), false)),
"expected Unverifiable, got {outcome:?}"
);
let _ = std::fs::remove_dir_all(source_root);
let _ = std::fs::remove_dir_all(target_root);
Ok(())
}
#[test]
fn dc53_stage2_transported_material_imports_as_sound() -> prikk_error::Result<()> {
let source_root = unique_temp_dir("dc53-vector-sound-source");
let source = RepositoryLayout::init(source_root.clone())?;
let signer = transport_test_signer(0xa2)?;
seal_two_block_history_with_author(&source, &signer, true)?;
let (report, bytes) = export_bundle(&source, "heads/main")?;
assert_eq!(report.author_key_count, 1);
let target_root = unique_temp_dir("dc53-vector-sound-target");
let target = RepositoryLayout::init(target_root.clone())?;
let import_report = import_bundle(&target, &bytes, &BundleImportOptions::default_limits())?;
assert_eq!(import_report.recorded_author_key_count, 1);
let imported_patch = find_imported_transport_patch(&target)?;
let outcome = verify_author_signature(&target, &imported_patch)?;
assert_eq!(outcome, Some((signer.key_id().to_string(), true)));
let _ = std::fs::remove_dir_all(source_root);
let _ = std::fs::remove_dir_all(target_root);
Ok(())
}
#[test]
fn dc53_stage2_vector8_a_transported_key_that_does_not_verify_reads_failed()
-> prikk_error::Result<()> {
let source_root = unique_temp_dir("dc53-vector8-source");
let source = RepositoryLayout::init(source_root.clone())?;
let signer = transport_test_signer(0xa3)?;
seal_two_block_history_with_author(&source, &signer, true)?;
let (_, bytes) = export_bundle(&source, "heads/main")?;
let (ref_name, objects, mut author_keys) =
decode_bundle(&bytes, DEFAULT_BUNDLE_MAX_OBJECT_COUNT)?;
assert_eq!(author_keys.len(), 1);
if let Some(entry) = author_keys.first_mut() {
entry.public_key = [0xbb; 32];
}
let tampered = encode_bundle(&ref_name, &objects, &author_keys)?;
let target_root = unique_temp_dir("dc53-vector8-target");
let target = RepositoryLayout::init(target_root.clone())?;
let import_report = import_bundle(&target, &tampered, &BundleImportOptions::default_limits())?;
assert_eq!(
import_report.recorded_author_key_count, 1,
"import records material without checking it, D7"
);
let imported_patch = find_imported_transport_patch(&target)?;
let outcome = verify_author_signature(&target, &imported_patch);
assert!(
outcome.is_err(),
"the transported key must not verify the Patch's real signature -- got {outcome:?}"
);
let _ = std::fs::remove_dir_all(source_root);
let _ = std::fs::remove_dir_all(target_root);
Ok(())
}
#[test]
fn import_rejects_a_bundle_whose_author_key_section_disagrees_with_itself()
-> prikk_error::Result<()> {
let source_root = unique_temp_dir("dc53-bundle-internal-conflict-source");
let source = RepositoryLayout::init(source_root.clone())?;
let signer = transport_test_signer(0xa4)?;
seal_two_block_history_with_author(&source, &signer, true)?;
let (_, bytes) = export_bundle(&source, "heads/main")?;
let (ref_name, objects, mut author_keys) =
decode_bundle(&bytes, DEFAULT_BUNDLE_MAX_OBJECT_COUNT)?;
assert_eq!(author_keys.len(), 1);
let key_id = author_keys
.first()
.map(|entry| entry.key_id.clone())
.ok_or_else(|| {
prikk_error::PrikkError::Integrity("expected one decoded author-key entry".to_string())
})?;
author_keys.push(AuthorKeyEntry {
key_id: key_id.clone(),
public_key: [0xcc; 32],
});
let hostile = encode_bundle(&ref_name, &objects, &author_keys)?;
let target_root = unique_temp_dir("dc53-bundle-internal-conflict-target");
let target = RepositoryLayout::init(target_root.clone())?;
let before = crate::verify_repository(&target)?.checked_objects;
let result = import_bundle(&target, &hostile, &BundleImportOptions::default_limits());
assert!(
result.is_err(),
"a bundle whose own author-key section disagrees with itself must be refused"
);
let after = crate::verify_repository(&target)?.checked_objects;
assert_eq!(
before, after,
"refused before any write -- the object store must be untouched"
);
assert_eq!(before, Some(0));
assert_eq!(
lookup_author_key_entries(&target, &key_id)?,
Vec::new(),
"a refused hostile bundle must leave no author-key entry behind, not even the attacker's \
first-listed one"
);
let _ = std::fs::remove_dir_all(source_root);
let _ = std::fs::remove_dir_all(target_root);
Ok(())
}
#[test]
fn import_rejects_a_transported_key_conflicting_with_local_material() -> prikk_error::Result<()> {
let source_root = unique_temp_dir("dc53-import-local-conflict-source");
let source = RepositoryLayout::init(source_root.clone())?;
let signer = transport_test_signer(0xa5)?;
seal_two_block_history_with_author(&source, &signer, true)?;
let (_, bytes) = export_bundle(&source, "heads/main")?;
let target_root = unique_temp_dir("dc53-import-local-conflict-target");
let target = RepositoryLayout::init(target_root.clone())?;
let active_lock = ActiveLock::acquire(&target)?;
record_author_key_material(&target, signer.key_id(), [0xdd; 32], &active_lock)?;
drop(active_lock);
let result = import_bundle(&target, &bytes, &BundleImportOptions::default_limits());
assert!(
result.is_err(),
"a transported key conflicting with existing local material must be refused"
);
assert!(
read_received_pointer(&target, "remotes/heads/main")?.is_none(),
"a refused import must not create the received pointer"
);
let entries = lookup_author_key_entries(&target, signer.key_id())?;
assert_eq!(
entries,
vec![AuthorKeyEntry {
key_id: signer.key_id().to_string(),
public_key: [0xdd; 32],
}],
"the transported conflicting key must never be recorded -- {entries:?}"
);
let _ = std::fs::remove_dir_all(source_root);
let _ = std::fs::remove_dir_all(target_root);
Ok(())
}
#[test]
fn import_rejects_a_later_conflicting_key_without_recording_an_earlier_one()
-> prikk_error::Result<()> {
let source_root = unique_temp_dir("dc53-multi-key-import-source");
let source = RepositoryLayout::init(source_root.clone())?;
let signer_a = transport_test_signer(0xb1)?;
seal_two_block_history_with_author(&source, &signer_a, true)?;
let (_, bytes) = export_bundle(&source, "heads/main")?;
let (ref_name, objects, mut author_keys) =
decode_bundle(&bytes, DEFAULT_BUNDLE_MAX_OBJECT_COUNT)?;
assert_eq!(
author_keys.len(),
1,
"expected exactly one transported key from a single-author export"
);
let signer_b = transport_test_signer(0xb2)?;
author_keys.push(AuthorKeyEntry {
key_id: signer_b.key_id().to_string(),
public_key: signer_b.public_key_bytes(),
});
let hostile = encode_bundle(&ref_name, &objects, &author_keys)?;
let target_root = unique_temp_dir("dc53-multi-key-import-target");
let target = RepositoryLayout::init(target_root.clone())?;
let active_lock = ActiveLock::acquire(&target)?;
record_author_key_material(&target, signer_b.key_id(), [0xdd; 32], &active_lock)?;
drop(active_lock);
let result = import_bundle(&target, &hostile, &BundleImportOptions::default_limits());
assert!(
result.is_err(),
"a later transported key conflicting with local material must refuse the whole import"
);
assert!(
read_received_pointer(&target, "remotes/heads/main")?.is_none(),
"a refused import must not create the received pointer"
);
assert_eq!(
lookup_author_key_entries(&target, signer_a.key_id())?,
Vec::new(),
"the earlier entry, which conflicted with nothing, must not have been recorded either -- \
the whole import is refused before any entry is written"
);
assert_eq!(
lookup_author_key_entries(&target, signer_b.key_id())?,
vec![AuthorKeyEntry {
key_id: signer_b.key_id().to_string(),
public_key: [0xdd; 32],
}],
"the target's own pre-existing material for the conflicting key_id must survive untouched"
);
let _ = std::fs::remove_dir_all(source_root);
let _ = std::fs::remove_dir_all(target_root);
Ok(())
}
#[test]
fn reimporting_the_same_bundle_records_no_new_author_key_entries() -> prikk_error::Result<()> {
let source_root = unique_temp_dir("dc53-multi-key-reimport-source");
let source = RepositoryLayout::init(source_root.clone())?;
let signer = transport_test_signer(0xb3)?;
seal_two_block_history_with_author(&source, &signer, true)?;
let (_, bytes) = export_bundle(&source, "heads/main")?;
let target_root = unique_temp_dir("dc53-multi-key-reimport-target");
let target = RepositoryLayout::init(target_root.clone())?;
let options = BundleImportOptions::default_limits();
import_bundle(&target, &bytes, &options)?;
let after_first = lookup_author_key_entries(&target, signer.key_id())?;
assert_eq!(after_first.len(), 1);
import_bundle(&target, &bytes, &options)?;
let after_second = lookup_author_key_entries(&target, signer.key_id())?;
assert_eq!(
after_second, after_first,
"re-importing an unchanged bundle must not grow the author-key container"
);
let _ = std::fs::remove_dir_all(source_root);
let _ = std::fs::remove_dir_all(target_root);
Ok(())
}
#[test]
fn export_fails_when_local_material_already_conflicts_for_an_exported_key_id()
-> prikk_error::Result<()> {
let root = unique_temp_dir("dc53-export-local-conflict");
let layout = RepositoryLayout::init(root.clone())?;
let signer = transport_test_signer(0xa6)?;
seal_two_block_history_with_author(&layout, &signer, true)?;
force_conflicting_author_key_entry_for_test(&layout, signer.key_id(), [0xee; 32])?;
let result = export_bundle(&layout, "heads/main");
assert!(
result.is_err(),
"export must refuse rather than silently pick one of two conflicting local keys"
);
let _ = std::fs::remove_dir_all(root);
Ok(())
}
#[test]
fn author_key_count_limit_fires_exactly_at_the_boundary() -> prikk_error::Result<()> {
let ref_name = "heads/main".to_string();
let objects: Vec<ObjectEnvelope> = Vec::new();
let author_keys = vec![
AuthorKeyEntry {
key_id: "a".to_string(),
public_key: [1; 32],
},
AuthorKeyEntry {
key_id: "b".to_string(),
public_key: [2; 32],
},
];
let bytes = encode_bundle(&ref_name, &objects, &author_keys)?;
let refused = decode_bundle(&bytes, 1);
assert!(
refused.is_err(),
"a limit one below the actual author-key count (2) must refuse"
);
let accepted = decode_bundle(&bytes, 2);
assert!(
accepted.is_ok(),
"a limit exactly at the actual author-key count (2) must accept"
);
Ok(())
}
#[test]
fn a_pbndl001_bundle_imports_and_its_patch_reads_unverifiable() -> prikk_error::Result<()> {
let source_root = unique_temp_dir("dc53-pbndl001-import-source");
let source = RepositoryLayout::init(source_root.clone())?;
let author = transport_test_signer(0xa8)?;
let maintainer = Ed25519MaintainerSigner::from_seed("dc53-pbndl001-maintainer", &[0xa9; 32])?;
crate::trust::add_trusted_maintainer(
&source,
maintainer.key_id(),
&prikk_hash::to_hex(&maintainer.public_key_bytes()),
)?;
std::fs::write(source.root().join("v1-import.txt"), b"v1 import\n")?;
crate::worktree_patch::commit_worktree_changes_signed(
&source,
"heads/main",
"dc53 pbndl001 fixture",
crate::worktree_patch::WorktreePatchCommitOptions::default(),
&author,
)?;
crate::rfc111_seal_simulation::simulate_one_seal(&source, "heads/main", &maintainer)?;
let (_, v2_bytes) = export_bundle(&source, "heads/main")?;
let (ref_name, objects, author_keys) =
decode_bundle(&v2_bytes, DEFAULT_BUNDLE_MAX_OBJECT_COUNT)?;
assert_eq!(
author_keys.len(),
1,
"sanity: the sender really did have material to carry"
);
let v1_bytes = encode_bundle_v1_for_test(&ref_name, &objects)?;
let target_root = unique_temp_dir("dc53-pbndl001-import-target");
let target = RepositoryLayout::init(target_root.clone())?;
let import_report = import_bundle(&target, &v1_bytes, &BundleImportOptions::default_limits())?;
assert_eq!(
import_report.recorded_author_key_count, 0,
"a PBNDL001 bundle carries no author-key section to record"
);
let imported_patch = find_imported_transport_patch(&target)?;
let outcome = verify_author_signature(&target, &imported_patch)?;
assert_eq!(
outcome,
Some((author.key_id().to_string(), false)),
"expected Unverifiable, got {outcome:?}"
);
let report = crate::verify_repository(&target)?;
assert!(
!report.has_item_failure(),
"verify must pass against a v1-imported repository: {report:?}"
);
let _ = std::fs::remove_dir_all(source_root);
let _ = std::fs::remove_dir_all(target_root);
Ok(())
}
fn author_key_container_bytes(layout: &RepositoryLayout) -> prikk_error::Result<Vec<u8>> {
let relative = layout.repository_relative(&layout.author_key_container_path())?;
Ok(
crate::fsutil::read_file_if_exists(layout.repository_mutation_root(), &relative)?
.unwrap_or_default(),
)
}
fn received_index_bytes(
layout: &RepositoryLayout,
) -> prikk_error::Result<(Vec<u8>, Vec<u8>, Vec<u8>)> {
let read = |path: std::path::PathBuf| -> prikk_error::Result<Vec<u8>> {
let relative = layout.repository_relative(&path)?;
Ok(
crate::fsutil::read_file_if_exists(layout.repository_mutation_root(), &relative)?
.unwrap_or_default(),
)
};
Ok((
read(layout.received_index_slot_path(ContainerSlot::A))?,
read(layout.received_index_slot_path(ContainerSlot::B))?,
read(layout.received_index_generation_log_path())?,
))
}
#[test]
fn row1_a_bundle_whose_ref_target_is_absent_is_refused() -> prikk_error::Result<()> {
let source_root = unique_temp_dir("dc78-closure-row1-source");
let source = RepositoryLayout::init(source_root.clone())?;
let tip_block_id = seal_two_block_history(&source)?;
let maintainer =
Ed25519MaintainerSigner::from_seed("dc78-closure-row1-maintainer", &[0xc1; 32])?;
let mut object_store = FileObjectStore::new(source.clone());
publish_tag(
&source,
&mut object_store,
"tags/v1",
tip_block_id,
&maintainer,
)?;
let (_, bytes) = export_bundle(&source, "tags/v1")?;
let (ref_name, objects, author_keys) = decode_bundle(&bytes, DEFAULT_BUNDLE_MAX_OBJECT_COUNT)?;
assert!(
objects
.iter()
.any(|envelope| envelope.object_type == ObjectType::Tag),
"fixture sanity: a genuine tag bundle must carry the Tag envelope"
);
let pre_fix_shaped_objects: Vec<ObjectEnvelope> = objects
.into_iter()
.filter(|envelope| envelope.object_type != ObjectType::Tag)
.collect();
let pre_fix_shaped_bytes = encode_bundle(&ref_name, &pre_fix_shaped_objects, &author_keys)?;
let target_root = unique_temp_dir("dc78-closure-row1-target");
let target = RepositoryLayout::init(target_root.clone())?;
let result = import_bundle(
&target,
&pre_fix_shaped_bytes,
&BundleImportOptions::default_limits(),
);
let err = match result {
Ok(report) => panic!(
"a bundle whose RefState targets a Tag object it never carried must be refused: \
{report:?}"
),
Err(err) => err,
};
assert!(
err.to_string().contains("targets missing tag"),
"unexpected error: {err}"
);
assert!(read_received_pointer(&target, "remotes/tags/v1")?.is_none());
let _ = std::fs::remove_dir_all(source_root);
let _ = std::fs::remove_dir_all(target_root);
Ok(())
}
#[test]
fn row2_a_bundle_missing_a_referenced_blob_is_refused() -> prikk_error::Result<()> {
let source_root = unique_temp_dir("dc78-closure-row2-source");
let source = RepositoryLayout::init(source_root.clone())?;
seal_two_block_history(&source)?;
let (_, bytes) = export_bundle(&source, "heads/main")?;
let (ref_name, objects, author_keys) = decode_bundle(&bytes, DEFAULT_BUNDLE_MAX_OBJECT_COUNT)?;
assert!(
objects
.iter()
.any(|envelope| envelope.object_type == ObjectType::Blob),
"fixture sanity"
);
let broken_objects: Vec<ObjectEnvelope> = objects
.into_iter()
.filter(|envelope| envelope.object_type != ObjectType::Blob)
.collect();
let broken_bytes = encode_bundle(&ref_name, &broken_objects, &author_keys)?;
let target_root = unique_temp_dir("dc78-closure-row2-target");
let target = RepositoryLayout::init(target_root.clone())?;
let result = import_bundle(
&target,
&broken_bytes,
&BundleImportOptions::default_limits(),
);
let err = match result {
Ok(report) => {
panic!("a bundle missing a patch-referenced blob must be refused: {report:?}")
}
Err(err) => err,
};
assert!(
err.to_string().contains("references blob"),
"unexpected error: {err}"
);
assert!(read_received_pointer(&target, "remotes/heads/main")?.is_none());
let _ = std::fs::remove_dir_all(source_root);
let _ = std::fs::remove_dir_all(target_root);
Ok(())
}
#[test]
fn row2b_a_bundle_missing_a_blocks_snapshot_blob_is_refused() -> prikk_error::Result<()> {
let source_root = unique_temp_dir("dc78-closure-row2b-source");
let source = RepositoryLayout::init(source_root.clone())?;
let (_, snapshot_blob_id) = seal_two_block_history_with_snapshot_blob(&source)?;
let (_, bytes) = export_bundle(&source, "heads/main")?;
let (ref_name, objects, author_keys) = decode_bundle(&bytes, DEFAULT_BUNDLE_MAX_OBJECT_COUNT)?;
assert!(
objects
.iter()
.any(|envelope| envelope.object_id() == snapshot_blob_id),
"fixture sanity: the exported bundle must carry the block's own snapshot blob"
);
let good_target_root = unique_temp_dir("dc78-closure-row2b-good-target");
let good_target = RepositoryLayout::init(good_target_root.clone())?;
import_bundle(&good_target, &bytes, &BundleImportOptions::default_limits())?;
let good_target_objects = FileObjectStore::new(good_target.clone());
assert!(
good_target_objects
.read_typed(snapshot_blob_id, ObjectType::Blob)?
.is_some(),
"the snapshot blob must actually land in the receiving repository's store"
);
let broken_objects: Vec<ObjectEnvelope> = objects
.into_iter()
.filter(|envelope| envelope.object_id() != snapshot_blob_id)
.collect();
let broken_bytes = encode_bundle(&ref_name, &broken_objects, &author_keys)?;
let target_root = unique_temp_dir("dc78-closure-row2b-target");
let target = RepositoryLayout::init(target_root.clone())?;
let result = import_bundle(
&target,
&broken_bytes,
&BundleImportOptions::default_limits(),
);
let err = match result {
Ok(report) => {
panic!("a bundle missing a block's own snapshot blob must be refused: {report:?}")
}
Err(err) => err,
};
assert!(
err.to_string().contains("names snapshot blob"),
"unexpected error: {err}"
);
assert!(read_received_pointer(&target, "remotes/heads/main")?.is_none());
let _ = std::fs::remove_dir_all(source_root);
let _ = std::fs::remove_dir_all(good_target_root);
let _ = std::fs::remove_dir_all(target_root);
Ok(())
}
#[test]
fn row3_a_bundle_missing_a_blocks_patch_is_refused() -> prikk_error::Result<()> {
let source_root = unique_temp_dir("dc78-closure-row3-source");
let source = RepositoryLayout::init(source_root.clone())?;
seal_two_block_history(&source)?;
let (_, bytes) = export_bundle(&source, "heads/main")?;
let (ref_name, objects, author_keys) = decode_bundle(&bytes, DEFAULT_BUNDLE_MAX_OBJECT_COUNT)?;
assert!(
objects
.iter()
.any(|envelope| envelope.object_type == ObjectType::Patch),
"fixture sanity"
);
let broken_objects: Vec<ObjectEnvelope> = objects
.into_iter()
.filter(|envelope| envelope.object_type != ObjectType::Patch)
.collect();
let broken_bytes = encode_bundle(&ref_name, &broken_objects, &author_keys)?;
let target_root = unique_temp_dir("dc78-closure-row3-target");
let target = RepositoryLayout::init(target_root.clone())?;
let result = import_bundle(
&target,
&broken_bytes,
&BundleImportOptions::default_limits(),
);
let err = match result {
Ok(report) => {
panic!("a bundle missing a block's own named patch must be refused: {report:?}")
}
Err(err) => err,
};
assert!(
err.to_string().contains("names patch"),
"unexpected error: {err}"
);
assert!(read_received_pointer(&target, "remotes/heads/main")?.is_none());
let _ = std::fs::remove_dir_all(source_root);
let _ = std::fs::remove_dir_all(target_root);
Ok(())
}
#[test]
fn row4_a_bundle_missing_a_blocks_parent_is_refused() -> prikk_error::Result<()> {
let source_root = unique_temp_dir("dc78-closure-row4-source");
let source = RepositoryLayout::init(source_root.clone())?;
let child_block_id = seal_two_block_history(&source)?;
let (_, bytes) = export_bundle(&source, "heads/main")?;
let (ref_name, objects, author_keys) = decode_bundle(&bytes, DEFAULT_BUNDLE_MAX_OBJECT_COUNT)?;
let is_root_block = |envelope: &ObjectEnvelope| {
envelope.object_type == ObjectType::Block && envelope.object_id() != child_block_id
};
assert!(
objects.iter().any(is_root_block),
"fixture sanity: the root block must be present before removal"
);
let broken_objects: Vec<ObjectEnvelope> = objects
.into_iter()
.filter(|envelope| !is_root_block(envelope))
.collect();
let broken_bytes = encode_bundle(&ref_name, &broken_objects, &author_keys)?;
let target_root = unique_temp_dir("dc78-closure-row4-target");
let target = RepositoryLayout::init(target_root.clone())?;
let result = import_bundle(
&target,
&broken_bytes,
&BundleImportOptions::default_limits(),
);
let err = match result {
Ok(report) => {
panic!("a bundle missing a block's own named parent must be refused: {report:?}")
}
Err(err) => err,
};
assert!(
err.to_string().contains("names parent"),
"unexpected error: {err}"
);
assert!(read_received_pointer(&target, "remotes/heads/main")?.is_none());
let _ = std::fs::remove_dir_all(source_root);
let _ = std::fs::remove_dir_all(target_root);
Ok(())
}
#[test]
fn row5_objects_already_held_locally_satisfy_present() -> prikk_error::Result<()> {
let source_root = unique_temp_dir("dc78-closure-row5-source");
let source = RepositoryLayout::init(source_root.clone())?;
let child_block_id = seal_two_block_history(&source)?;
let (_, bytes) = export_bundle(&source, "heads/main")?;
let (ref_name, objects, author_keys) = decode_bundle(&bytes, DEFAULT_BUNDLE_MAX_OBJECT_COUNT)?;
let target_root = unique_temp_dir("dc78-closure-row5-target");
let target = RepositoryLayout::init(target_root.clone())?;
let mut target_objects = FileObjectStore::new(target.clone());
let mut carried_objects: Vec<ObjectEnvelope> = Vec::new();
for envelope in objects {
let already_local = match envelope.object_type {
ObjectType::Block => envelope.object_id() != child_block_id,
ObjectType::Patch | ObjectType::Blob => true,
_ => false,
};
if already_local {
target_objects.write_object(&envelope)?;
} else {
carried_objects.push(envelope);
}
}
assert_eq!(
carried_objects.len(),
2,
"fixture sanity: only the RefState and tip Block should remain in the partial bundle"
);
let partial_bytes = encode_bundle(&ref_name, &carried_objects, &author_keys)?;
let report = import_bundle(
&target,
&partial_bytes,
&BundleImportOptions::default_limits(),
)?;
assert_eq!(report.object_count, 2);
assert_eq!(
report.written_object_count, 2,
"the pre-seeded objects must not be double-counted as newly written"
);
assert!(read_received_pointer(&target, "remotes/heads/main")?.is_some());
assert!(
target_objects
.read_typed(child_block_id, ObjectType::Block)?
.is_some()
);
let (ref_name_b, objects_b, author_keys_b) =
decode_bundle(&bytes, DEFAULT_BUNDLE_MAX_OBJECT_COUNT)?;
let target_b_root = unique_temp_dir("dc78-closure-row5-target-b");
let target_b = RepositoryLayout::init(target_b_root.clone())?;
let mut target_b_objects = FileObjectStore::new(target_b.clone());
let mut carried_objects_b: Vec<ObjectEnvelope> = Vec::new();
for envelope in objects_b {
if envelope.object_type == ObjectType::Blob {
target_b_objects.write_object(&envelope)?;
} else {
carried_objects_b.push(envelope);
}
}
assert_eq!(
carried_objects_b.len(),
4,
"fixture sanity: everything except the Blob should remain in this partial bundle"
);
assert!(
carried_objects_b
.iter()
.any(|envelope| envelope.object_type == ObjectType::Patch),
"fixture sanity: the Patch that references the omitted Blob must itself be carried, or \
the blob check's own loop never runs"
);
let partial_bytes_b = encode_bundle(&ref_name_b, &carried_objects_b, &author_keys_b)?;
let report_b = import_bundle(
&target_b,
&partial_bytes_b,
&BundleImportOptions::default_limits(),
)?;
assert_eq!(report_b.object_count, 4);
assert_eq!(
report_b.written_object_count, 4,
"the pre-seeded Blob must not be double-counted as newly written"
);
assert!(read_received_pointer(&target_b, "remotes/heads/main")?.is_some());
let _ = std::fs::remove_dir_all(source_root);
let _ = std::fs::remove_dir_all(target_root);
let _ = std::fs::remove_dir_all(target_b_root);
Ok(())
}
#[test]
fn row6_a_refused_import_writes_no_pointer_and_records_no_key_material() -> prikk_error::Result<()>
{
let good_source_root = unique_temp_dir("dc78-closure-row6-good-source");
let good_source = RepositoryLayout::init(good_source_root.clone())?;
seal_two_block_history(&good_source)?;
let (_, good_bytes) = export_bundle(&good_source, "heads/main")?;
let target_root = unique_temp_dir("dc78-closure-row6-target");
let target = RepositoryLayout::init(target_root.clone())?;
let good_report = import_bundle(&target, &good_bytes, &BundleImportOptions::default_limits())?;
let unrelated_signer = transport_test_signer(0xc6)?;
let unrelated_lock = ActiveLock::acquire(&target)?;
record_author_key_material(
&target,
unrelated_signer.key_id(),
unrelated_signer.public_key_bytes(),
&unrelated_lock,
)?;
drop(unrelated_lock);
let received_before = received_index_bytes(&target)?;
let author_keys_before = author_key_container_bytes(&target)?;
assert!(
!received_before.0.is_empty() || !received_before.1.is_empty(),
"fixture sanity"
);
assert!(!author_keys_before.is_empty(), "fixture sanity");
let attack_signer = transport_test_signer(0xc7)?;
let attack_source_root = unique_temp_dir("dc78-closure-row6-attack-source");
let attack_source = RepositoryLayout::init(attack_source_root.clone())?;
seal_two_block_history_with_author(&attack_source, &attack_signer, true)?;
let (_, attack_bytes) = export_bundle(&attack_source, "heads/main")?;
let (ref_name, objects, author_keys) =
decode_bundle(&attack_bytes, DEFAULT_BUNDLE_MAX_OBJECT_COUNT)?;
assert_eq!(
author_keys.len(),
1,
"fixture sanity: the attack bundle must carry material"
);
let broken_objects: Vec<ObjectEnvelope> = objects
.into_iter()
.filter(|envelope| envelope.object_type != ObjectType::Patch)
.collect();
let broken_bytes = encode_bundle(&ref_name, &broken_objects, &author_keys)?;
let result = import_bundle(
&target,
&broken_bytes,
&BundleImportOptions::default_limits(),
);
assert!(result.is_err(), "the hostile re-import must be refused");
let received_after = received_index_bytes(&target)?;
let author_keys_after = author_key_container_bytes(&target)?;
assert_eq!(
received_before, received_after,
"byte-for-byte: the received-ref index must be untouched by a refused import"
);
assert_eq!(
author_keys_before, author_keys_after,
"byte-for-byte: the author-key container must be untouched by a refused import"
);
let pointer = read_received_pointer(&target, "remotes/heads/main")?;
assert_eq!(
pointer.map(|pointer| pointer.ref_state_id),
Some(good_report.ref_state_id),
"the genuine earlier import's pointer must survive a refused re-import unchanged"
);
assert!(
lookup_author_key_entries(&target, attack_signer.key_id())?.is_empty(),
"the attack bundle's own key material must not be recorded"
);
let _ = std::fs::remove_dir_all(good_source_root);
let _ = std::fs::remove_dir_all(attack_source_root);
let _ = std::fs::remove_dir_all(target_root);
Ok(())
}
#[test]
fn row7_a_well_formed_bundle_still_imports_both_formats() -> prikk_error::Result<()> {
let source_root = unique_temp_dir("dc78-closure-row7-source");
let source = RepositoryLayout::init(source_root.clone())?;
seal_two_block_history(&source)?;
let (_, bytes) = export_bundle(&source, "heads/main")?;
let (ref_name, objects, _author_keys) = decode_bundle(&bytes, DEFAULT_BUNDLE_MAX_OBJECT_COUNT)?;
let v1_bytes = encode_bundle_v1_for_test(&ref_name, &objects)?;
let v2_target_root = unique_temp_dir("dc78-closure-row7-v2-target");
let v2_target = RepositoryLayout::init(v2_target_root.clone())?;
import_bundle(&v2_target, &bytes, &BundleImportOptions::default_limits())?;
assert!(read_received_pointer(&v2_target, "remotes/heads/main")?.is_some());
let v1_target_root = unique_temp_dir("dc78-closure-row7-v1-target");
let v1_target = RepositoryLayout::init(v1_target_root.clone())?;
import_bundle(
&v1_target,
&v1_bytes,
&BundleImportOptions::default_limits(),
)?;
assert!(read_received_pointer(&v1_target, "remotes/heads/main")?.is_some());
let _ = std::fs::remove_dir_all(source_root);
let _ = std::fs::remove_dir_all(v2_target_root);
let _ = std::fs::remove_dir_all(v1_target_root);
Ok(())
}
fn publish_tag(
layout: &RepositoryLayout,
object_store: &mut FileObjectStore,
tag_name: &str,
target_block_id: ObjectId,
maintainer: &Ed25519MaintainerSigner,
) -> prikk_error::Result<ObjectId> {
let (patch_set_digest, patch_count) =
crate::compute_patch_set_digest_and_count_from_block(object_store, target_block_id)?;
let tag_payload = TagPayload {
name: tag_name.to_string(),
target_block_id,
message: None,
created_at: 0,
author_key_id: maintainer.key_id().to_string(),
patch_set_digest,
patch_count,
};
let mut tag_envelope =
ObjectEnvelope::unsigned(ObjectType::Tag, 1, tag_payload.to_canonical_bytes()?);
let tag_object_id = tag_envelope.object_id();
tag_envelope.add_signature(crate::maintainer_signature(
maintainer,
ObjectType::Tag,
tag_object_id,
)?)?;
let tag_id = object_store.write_object(&tag_envelope)?;
let ref_state_payload = RefStatePayload {
ref_name: tag_name.to_string(),
kind: RefKind::Tag,
target_object_id: tag_id,
update_seq: 1,
previous_ref_state_id: None,
required_attestation_ids: Vec::new(),
closed: false,
};
let mut ref_state_envelope = ObjectEnvelope::unsigned(
ObjectType::RefState,
1,
ref_state_payload.to_canonical_bytes()?,
);
let ref_state_id = ref_state_envelope.object_id();
ref_state_envelope.add_signature(crate::maintainer_signature(
maintainer,
ObjectType::RefState,
ref_state_id,
)?)?;
let ref_update_payload = RefUpdatePayload {
ref_name: tag_name.to_string(),
old_ref_state_id: None,
new_ref_state_id: ref_state_id,
new_target_object_id: tag_id,
update_seq: 1,
created_at: 0,
author_key_id: maintainer.key_id().to_string(),
};
let mut ref_update_envelope = ObjectEnvelope::unsigned(
ObjectType::RefUpdate,
1,
ref_update_payload.to_canonical_bytes()?,
);
let ref_update_id = ref_update_envelope.object_id();
ref_update_envelope.add_signature(crate::maintainer_signature(
maintainer,
ObjectType::RefUpdate,
ref_update_id,
)?)?;
let ref_store = RefStore::new(layout.clone());
ref_store.publish_with_object_store(
object_store,
&RefPublication {
ref_name: tag_name.to_string(),
expected_previous_ref_state_id: None,
ref_state: ref_state_envelope,
ref_update: ref_update_envelope,
},
)?;
Ok(tag_id)
}
#[test]
fn export_of_a_tag_ref_succeeds_and_the_imported_bundle_verifies() -> prikk_error::Result<()> {
let source_root = unique_temp_dir("dc78-tag-export-source");
let source = RepositoryLayout::init(source_root.clone())?;
let author = transport_test_signer(0xb4)?;
let maintainer = Ed25519MaintainerSigner::from_seed("dc78-tag-maintainer", &[0xb5; 32])?;
crate::trust::add_trusted_maintainer(
&source,
maintainer.key_id(),
&prikk_hash::to_hex(&maintainer.public_key_bytes()),
)?;
std::fs::write(source.root().join("dc78-tag.txt"), b"dc78 tag fixture\n")?;
crate::worktree_patch::commit_worktree_changes_signed(
&source,
"heads/main",
"dc78 tag fixture",
crate::worktree_patch::WorktreePatchCommitOptions::default(),
&author,
)?;
let sealed_ref_state_id =
crate::rfc111_seal_simulation::simulate_one_seal(&source, "heads/main", &maintainer)?;
let source_object_store = FileObjectStore::new(source.clone());
let sealed_ref_state_envelope = source_object_store
.read_typed(sealed_ref_state_id, ObjectType::RefState)?
.ok_or_else(|| prikk_error::PrikkError::Integrity("missing sealed RefState".to_string()))?;
let sealed_ref_state_payload = RefStatePayload::decode_canonical(
&sealed_ref_state_envelope.canonical_payload,
sealed_ref_state_envelope.schema_version,
)?;
let tip_block_id = sealed_ref_state_payload.target_object_id;
let mut object_store = FileObjectStore::new(source.clone());
let tag_id = publish_tag(
&source,
&mut object_store,
"tags/v1",
tip_block_id,
&maintainer,
)?;
let (_, bytes) = export_bundle(&source, "tags/v1")?;
let target_root = unique_temp_dir("dc78-tag-export-target");
let target = RepositoryLayout::init(target_root.clone())?;
import_bundle(&target, &bytes, &BundleImportOptions::default_limits())?;
let report = crate::verify_repository(&target)?;
assert!(
!report.has_item_failure(),
"verify must pass against a repository that imported a tag bundle: {report:?}"
);
let target_object_store = FileObjectStore::new(target.clone());
assert!(
target_object_store
.read_typed(tag_id, ObjectType::Tag)?
.is_some(),
"the Tag object must be present in the receiving repository's store after import"
);
let _ = std::fs::remove_dir_all(source_root);
let _ = std::fs::remove_dir_all(target_root);
Ok(())
}
#[test]
fn tag_ref_and_heads_ref_at_the_same_block_export_the_same_object_closure()
-> prikk_error::Result<()> {
let root = unique_temp_dir("dc78-tag-vs-heads-closure");
let layout = RepositoryLayout::init(root.clone())?;
let tip_block_id = seal_two_block_history(&layout)?;
let maintainer =
Ed25519MaintainerSigner::from_seed("dc78-tag-vs-heads-maintainer", &[0xb6; 32])?;
let mut object_store = FileObjectStore::new(layout.clone());
publish_tag(
&layout,
&mut object_store,
"tags/v1",
tip_block_id,
&maintainer,
)?;
let (_, heads_bytes) = export_bundle(&layout, "heads/main")?;
let (_, tag_bytes) = export_bundle(&layout, "tags/v1")?;
let (_, heads_objects, _) = decode_bundle(&heads_bytes, DEFAULT_BUNDLE_MAX_OBJECT_COUNT)?;
let (_, tag_objects, _) = decode_bundle(&tag_bytes, DEFAULT_BUNDLE_MAX_OBJECT_COUNT)?;
let closure_only = |objects: &[ObjectEnvelope]| {
objects
.iter()
.filter(|envelope| {
matches!(
envelope.object_type,
ObjectType::Block | ObjectType::Patch | ObjectType::Blob
)
})
.map(|envelope| (envelope.object_type, envelope.object_id()))
.collect::<std::collections::BTreeSet<_>>()
};
assert_eq!(
closure_only(&heads_objects),
closure_only(&tag_objects),
"a tag ref and a heads ref at the same block must export the identical Block/Patch/Blob \
closure"
);
assert_eq!(tag_objects.len(), heads_objects.len() + 1);
let _ = std::fs::remove_dir_all(root);
Ok(())
}