#![allow(clippy::indexing_slicing)]
use prikk_crypto::Ed25519KeyPair;
use prikk_error::Result;
use super::{
compact_received_index, compact_ref_pointer_index, compact_trust_policy,
plan_compact_ref_pointer_index,
};
#[cfg(any(target_os = "linux", target_os = "macos"))]
use crate::fsutil::{TestFailPoint, fail_after_for_test};
use crate::generation::resolve_live_slot;
use crate::layout::{ContainerSlot, LockableContainer};
use crate::lock::acquire_container_locks;
use crate::test_support::{
signed_empty_block_envelope, signed_ref_state_envelope, unique_temp_dir,
};
use crate::{
FileObjectStore, ObjectWriter, RefPublication, RefStore, RepositoryLayout,
add_trusted_maintainer, load_maintainer_trust_policy, remove_trusted_maintainer,
};
fn public_key_hex(seed: &[u8; 32]) -> String {
Ed25519KeyPair::from_seed(seed)
.public_key_bytes()
.iter()
.map(|byte| format!("{byte:02x}"))
.collect()
}
fn publish_update(
store: &RefStore,
objects: &mut FileObjectStore,
ref_name: &str,
expected_previous: Option<prikk_object::ObjectId>,
seq: u64,
) -> Result<prikk_object::ObjectId> {
let target = objects.write_object(&signed_empty_block_envelope())?;
let ref_state = signed_ref_state_envelope(ref_name, expected_previous, target, seq);
let ref_state_id = ref_state.object_id();
store.publish(&RefPublication {
ref_name: ref_name.to_string(),
expected_previous_ref_state_id: expected_previous,
ref_update: crate::test_support::signed_ref_update_envelope(
ref_name,
expected_previous,
ref_state_id,
target,
seq,
),
ref_state,
})
}
#[test]
fn compacting_the_ref_pointer_index_reclaims_stale_entries_and_preserves_current_pointers()
-> Result<()> {
let root = unique_temp_dir("compact-pointer-index");
let layout = RepositoryLayout::init(root.clone())?;
let mut objects = FileObjectStore::new(layout.clone());
let store = RefStore::new(layout.clone());
let first = publish_update(&store, &mut objects, "heads/main", None, 1)?;
let second = publish_update(&store, &mut objects, "heads/main", Some(first), 2)?;
let third = publish_update(&store, &mut objects, "heads/main", Some(second), 3)?;
let other = publish_update(&store, &mut objects, "heads/topic", None, 1)?;
let generation_log_path = layout.ref_pointer_index_generation_log_path();
let live_before = resolve_live_slot(&layout, &generation_log_path)?;
assert_eq!(live_before, ContainerSlot::A);
let report = compact_ref_pointer_index(&layout)?;
assert_eq!(report.entries_before, 4);
assert_eq!(report.entries_after, 2);
let live_after = resolve_live_slot(&layout, &generation_log_path)?;
assert_eq!(live_after, ContainerSlot::B);
assert_eq!(store.read_current_ref_state_id("heads/main")?, Some(third));
assert_eq!(store.read_current_ref_state_id("heads/topic")?, Some(other));
let retired_bytes = std::fs::read(layout.ref_pointer_index_slot_path(ContainerSlot::A))?;
assert!(!retired_bytes.is_empty());
let fourth = publish_update(&store, &mut objects, "heads/main", Some(third), 4)?;
assert_eq!(store.read_current_ref_state_id("heads/main")?, Some(fourth));
let _ = std::fs::remove_dir_all(root);
Ok(())
}
#[test]
fn plan_compact_reports_the_same_counts_as_a_real_run_and_touches_nothing() -> Result<()> {
let root = unique_temp_dir("compact-pointer-index-plan-only");
let layout = RepositoryLayout::init(root.clone())?;
let mut objects = FileObjectStore::new(layout.clone());
let store = RefStore::new(layout.clone());
let first = publish_update(&store, &mut objects, "heads/main", None, 1)?;
let second = publish_update(&store, &mut objects, "heads/main", Some(first), 2)?;
publish_update(&store, &mut objects, "heads/main", Some(second), 3)?;
publish_update(&store, &mut objects, "heads/topic", None, 1)?;
let generation_log_path = layout.ref_pointer_index_generation_log_path();
let slot_a_before = std::fs::read(layout.ref_pointer_index_slot_path(ContainerSlot::A))?;
let slot_b_before = std::fs::read(layout.ref_pointer_index_slot_path(ContainerSlot::B))?;
let generation_log_before = std::fs::read(&generation_log_path)?;
let report = plan_compact_ref_pointer_index(&layout)?;
assert_eq!(report.entries_before, 4);
assert_eq!(report.entries_after, 2);
assert_eq!(
std::fs::read(layout.ref_pointer_index_slot_path(ContainerSlot::A))?,
slot_a_before
);
assert_eq!(
std::fs::read(layout.ref_pointer_index_slot_path(ContainerSlot::B))?,
slot_b_before
);
assert_eq!(std::fs::read(&generation_log_path)?, generation_log_before);
assert_eq!(
resolve_live_slot(&layout, &generation_log_path)?,
ContainerSlot::A
);
let real_report = compact_ref_pointer_index(&layout)?;
assert_eq!(real_report.entries_before, report.entries_before);
assert_eq!(real_report.entries_after, report.entries_after);
let _ = std::fs::remove_dir_all(root);
Ok(())
}
#[test]
fn plan_compact_refuses_while_its_own_container_lock_is_externally_held() -> Result<()> {
let root = unique_temp_dir("compact-pointer-index-plan-only-lock-conflict");
let layout = RepositoryLayout::init(root.clone())?;
let mut objects = FileObjectStore::new(layout.clone());
let store = RefStore::new(layout.clone());
publish_update(&store, &mut objects, "heads/main", None, 1)?;
let held = acquire_container_locks(&layout, &[LockableContainer::RefPointerIndex])?;
assert!(plan_compact_ref_pointer_index(&layout).is_err());
drop(held);
assert!(plan_compact_ref_pointer_index(&layout).is_ok());
let _ = std::fs::remove_dir_all(root);
Ok(())
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[test]
fn a_crash_before_the_generation_record_lands_leaves_the_old_generation_authoritative() -> Result<()>
{
let root = unique_temp_dir("compact-pointer-index-crash-before-publish");
let layout = RepositoryLayout::init(root.clone())?;
let mut objects = FileObjectStore::new(layout.clone());
let store = RefStore::new(layout.clone());
let first = publish_update(&store, &mut objects, "heads/main", None, 1)?;
let second = publish_update(&store, &mut objects, "heads/main", Some(first), 2)?;
let generation_log_path = layout.ref_pointer_index_generation_log_path();
fail_after_for_test(TestFailPoint::AppendWrite, 1);
assert!(compact_ref_pointer_index(&layout).is_err());
assert_eq!(
resolve_live_slot(&layout, &generation_log_path)?,
ContainerSlot::A
);
assert_eq!(store.read_current_ref_state_id("heads/main")?, Some(second));
let report = compact_ref_pointer_index(&layout)?;
assert_eq!(report.entries_after, 1);
assert_eq!(
resolve_live_slot(&layout, &generation_log_path)?,
ContainerSlot::B
);
assert_eq!(store.read_current_ref_state_id("heads/main")?, Some(second));
let _ = std::fs::remove_dir_all(root);
Ok(())
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[test]
fn a_crash_while_writing_the_new_slots_own_bytes_leaves_the_old_generation_authoritative()
-> Result<()> {
let root = unique_temp_dir("compact-pointer-index-crash-during-slot-write");
let layout = RepositoryLayout::init(root.clone())?;
let mut objects = FileObjectStore::new(layout.clone());
let store = RefStore::new(layout.clone());
let first = publish_update(&store, &mut objects, "heads/main", None, 1)?;
let second = publish_update(&store, &mut objects, "heads/main", Some(first), 2)?;
let generation_log_path = layout.ref_pointer_index_generation_log_path();
fail_after_for_test(TestFailPoint::AppendWrite, 0);
assert!(compact_ref_pointer_index(&layout).is_err());
assert_eq!(
resolve_live_slot(&layout, &generation_log_path)?,
ContainerSlot::A
);
assert_eq!(store.read_current_ref_state_id("heads/main")?, Some(second));
let report = compact_ref_pointer_index(&layout)?;
assert_eq!(report.entries_after, 1);
assert_eq!(
resolve_live_slot(&layout, &generation_log_path)?,
ContainerSlot::B
);
assert_eq!(store.read_current_ref_state_id("heads/main")?, Some(second));
let _ = std::fs::remove_dir_all(root);
Ok(())
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[test]
fn a_crash_while_truncating_the_retired_slot_leaves_the_previous_generation_authoritative()
-> Result<()> {
let root = unique_temp_dir("compact-pointer-index-crash-during-truncate");
let layout = RepositoryLayout::init(root.clone())?;
let mut objects = FileObjectStore::new(layout.clone());
let store = RefStore::new(layout.clone());
let first = publish_update(&store, &mut objects, "heads/main", None, 1)?;
let second = publish_update(&store, &mut objects, "heads/main", Some(first), 2)?;
compact_ref_pointer_index(&layout)?;
let generation_log_path = layout.ref_pointer_index_generation_log_path();
assert_eq!(
resolve_live_slot(&layout, &generation_log_path)?,
ContainerSlot::B
);
let third = publish_update(&store, &mut objects, "heads/main", Some(second), 3)?;
fail_after_for_test(TestFailPoint::Truncate, 0);
assert!(compact_ref_pointer_index(&layout).is_err());
assert_eq!(
resolve_live_slot(&layout, &generation_log_path)?,
ContainerSlot::B
);
assert_eq!(store.read_current_ref_state_id("heads/main")?, Some(third));
let report = compact_ref_pointer_index(&layout)?;
assert_eq!(report.entries_after, 1);
assert_eq!(
resolve_live_slot(&layout, &generation_log_path)?,
ContainerSlot::A
);
assert_eq!(store.read_current_ref_state_id("heads/main")?, Some(third));
let _ = std::fs::remove_dir_all(root);
Ok(())
}
#[test]
fn writer_and_compactor_lock_contention_leaves_no_partial_write_from_either_side() -> Result<()> {
let root = unique_temp_dir("compact-pointer-index-lock-race-both-directions");
let layout = RepositoryLayout::init(root.clone())?;
let mut objects = FileObjectStore::new(layout.clone());
let store = RefStore::new(layout.clone());
let first = publish_update(&store, &mut objects, "heads/main", None, 1)?;
let entries_before_attempt =
std::fs::read(layout.ref_pointer_index_slot_path(ContainerSlot::A))?;
let compactor_lock = acquire_container_locks(&layout, &[LockableContainer::RefPointerIndex])?;
assert!(publish_update(&store, &mut objects, "heads/main", Some(first), 2).is_err());
assert_eq!(
std::fs::read(layout.ref_pointer_index_slot_path(ContainerSlot::A))?,
entries_before_attempt,
"a writer blocked by the compactor's lock must not have appended anything"
);
drop(compactor_lock);
let generation_log_path = layout.ref_pointer_index_generation_log_path();
let slot_a_before = std::fs::read(layout.ref_pointer_index_slot_path(ContainerSlot::A))?;
let slot_b_before = std::fs::read(layout.ref_pointer_index_slot_path(ContainerSlot::B))?;
let generation_log_before = std::fs::read(&generation_log_path)?;
let writer_lock = acquire_container_locks(&layout, &[LockableContainer::RefPointerIndex])?;
assert!(compact_ref_pointer_index(&layout).is_err());
assert_eq!(
std::fs::read(layout.ref_pointer_index_slot_path(ContainerSlot::A))?,
slot_a_before
);
assert_eq!(
std::fs::read(layout.ref_pointer_index_slot_path(ContainerSlot::B))?,
slot_b_before
);
assert_eq!(std::fs::read(&generation_log_path)?, generation_log_before);
drop(writer_lock);
let second = publish_update(&store, &mut objects, "heads/main", Some(first), 2)?;
assert_eq!(store.read_current_ref_state_id("heads/main")?, Some(second));
let report = compact_ref_pointer_index(&layout)?;
assert_eq!(report.entries_after, 1);
let _ = std::fs::remove_dir_all(root);
Ok(())
}
#[test]
fn compaction_refuses_on_a_corrupt_container_and_touches_nothing() -> Result<()> {
let root = unique_temp_dir("compact-pointer-index-corrupt");
let layout = RepositoryLayout::init(root.clone())?;
let mut objects = FileObjectStore::new(layout.clone());
let store = RefStore::new(layout.clone());
publish_update(&store, &mut objects, "heads/main", None, 1)?;
let live_path = layout.ref_pointer_index_slot_path(ContainerSlot::A);
let sound_bytes = std::fs::read(&live_path)?;
let mut damaged = sound_bytes.clone();
let last = damaged.len() - 1;
damaged[last] ^= 0x01;
std::fs::write(&live_path, &damaged)?;
assert!(compact_ref_pointer_index(&layout).is_err());
assert_eq!(std::fs::read(&live_path)?, damaged);
assert!(std::fs::read(layout.ref_pointer_index_slot_path(ContainerSlot::B))?.is_empty());
assert_eq!(
resolve_live_slot(&layout, &layout.ref_pointer_index_generation_log_path())?,
ContainerSlot::A
);
std::fs::write(&live_path, &sound_bytes)?;
let report = compact_ref_pointer_index(&layout)?;
assert_eq!(report.entries_after, 1);
let _ = std::fs::remove_dir_all(root);
Ok(())
}
#[test]
fn compaction_refuses_while_its_own_container_lock_is_externally_held() -> Result<()> {
let root = unique_temp_dir("compact-pointer-index-lock-conflict");
let layout = RepositoryLayout::init(root.clone())?;
let mut objects = FileObjectStore::new(layout.clone());
let store = RefStore::new(layout.clone());
publish_update(&store, &mut objects, "heads/main", None, 1)?;
let held = acquire_container_locks(&layout, &[LockableContainer::RefPointerIndex])?;
assert!(compact_ref_pointer_index(&layout).is_err());
drop(held);
assert!(compact_ref_pointer_index(&layout).is_ok());
let _ = std::fs::remove_dir_all(root);
Ok(())
}
#[test]
fn compacting_the_received_index_reclaims_a_superseded_import() -> Result<()> {
let root = unique_temp_dir("compact-received-index");
let layout = RepositoryLayout::init(root.clone())?;
let mut objects = FileObjectStore::new(layout.clone());
let first = objects
.write_object(&signed_empty_block_envelope())?
.to_owned();
let second_state = signed_ref_state_envelope("heads/main", None, first, 1);
let second = second_state.object_id();
let third_state = signed_ref_state_envelope("heads/main", None, first, 2);
let third = third_state.object_id();
crate::received::write_received_pointer(&layout, "remotes/heads/main", second)?;
crate::received::write_received_pointer(&layout, "remotes/heads/main", third)?;
let report = compact_received_index(&layout)?;
assert_eq!(report.entries_before, 2);
assert_eq!(report.entries_after, 1);
let pointer = crate::received::read_received_pointer(&layout, "remotes/heads/main")?;
assert!(pointer.is_some());
if let Some(pointer) = pointer {
assert_eq!(pointer.ref_state_id, third);
}
let _ = std::fs::remove_dir_all(root);
Ok(())
}
#[test]
fn compacting_the_trust_policy_container_keeps_only_the_last_snapshot() -> Result<()> {
let root = unique_temp_dir("compact-trust-policy");
let layout = RepositoryLayout::init(root.clone())?;
let first_key = public_key_hex(&[7_u8; 32]);
let second_key = public_key_hex(&[8_u8; 32]);
add_trusted_maintainer(&layout, "first", &first_key)?;
add_trusted_maintainer(&layout, "second", &second_key)?;
remove_trusted_maintainer(&layout, "first")?;
let report = compact_trust_policy(&layout)?;
assert_eq!(report.entries_before, 3);
assert_eq!(report.entries_after, 1);
let policy = load_maintainer_trust_policy(&layout)?;
assert_eq!(policy.keys.len(), 1);
assert_eq!(policy.keys[0].key_id, "second");
let _ = std::fs::remove_dir_all(root);
Ok(())
}