Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions crates/crab-metadata/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ Captured revision, remote-mount and current-state hydration use the shared paged
lookup. Control capture and selected closure queries are measured separately;
complete opening and history-amplification qualification remain required.

Ref deletion removes its live visibility closure even when the capsule has no
visibility payload. Retained dictionary objects cannot supply a new ref's base;
complete and paged reconciliation enforce the same deletion boundary.

External pointer catalogs use `CRBPCP01` authenticated pages. A bounded descriptor
commits file, shard, xorb and reverse-chunk directories; nested shard closures and
ordered xorb chunk lists are paged too. Keys preserve canonical lowercase Xet hex
Expand Down
18 changes: 17 additions & 1 deletion crates/crab-metadata/src/capsule_protocol/captured_visibility.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,16 @@ impl VisibilityReconciliation {
index,
)
}
Self::Complete(_) => Ok(()),
Self::Complete(index) => {
// A deletion-only capsule has no visibility payload. Its ref
// closure must still disappear before another control borrows it.
for edit in control.transaction().edits() {
if edit.new_oid().is_none() {
index.remove_ref(edit.ref_name());
}
}
Ok(())
}
Self::Paged(index) => index.apply(control).await,
}
}
Expand Down Expand Up @@ -335,6 +344,13 @@ impl CapturedVisibility {
/// Apply one authenticated control without widening to mutable ref state.
pub async fn apply(&mut self, control: &CapsuleControl) -> Result<()> {
let Some((reader, headers)) = self.headers(control).await? else {
// No payload skips OID changes, not deletion of an entire ref.
// Keep historical dictionary entries separate from live closures.
for edit in control.transaction().edits() {
if edit.new_oid().is_none() {
self.refs.remove(edit.ref_name());
}
}
return Ok(());
};
for (edit, header) in control.transaction().edits().iter().zip(headers) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,51 @@ async fn removed_invisible_objects_do_not_supply_new_ref_bases() {
);
}

#[tokio::test]
async fn deleted_ref_objects_remain_in_dictionary_but_cannot_supply_new_bases() {
let layout = StoreLayout::new(
Store::new(Arc::new(InMemory::new())),
"repositories/deleted".into(),
);
let mut lazy = CapturedVisibility::new(layout.store().clone(), initial(), std::iter::empty());
let create = control(
&layout,
"refs/heads/main",
None,
GitVisibilityEdit::from_replacement_objects(None, hex(1), vec![hex(1), hex(3)]),
)
.await;
lazy.apply(&create).await.unwrap();
let transaction = CapsuleTransaction::new(
&"1".repeat(64),
vec![CapsuleRefEdit::new(
"refs/heads/main",
Some(hex(1)),
None,
None,
)],
)
.unwrap();
let run =
CapsuleRun::leaf(Capsule::build(&transaction, Vec::new(), Vec::new()).unwrap()).unwrap();
let (_, mut controls) = run.verified_controls().unwrap();
lazy.apply(&controls.remove(0)).await.unwrap();
let branch = control(
&layout,
"refs/heads/feature",
None,
GitVisibilityEdit::from_delta_objects(Some(hex(3)), hex(4), vec![hex(4)], vec![]),
)
.await;
assert_eq!(
(
lazy.dictionary_contains(&[object(3)]).await.unwrap(),
lazy.ready(&branch, &BTreeMap::new()).await.unwrap()
),
(vec![true], false)
);
}

#[tokio::test]
async fn removal_outside_captured_prior_closure_is_rejected() {
let layout = StoreLayout::new(
Expand Down
3 changes: 3 additions & 0 deletions crates/crab-metadata/src/capsule_protocol/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ use crate::error::Result;
#[cfg(test)]
mod tests;

#[cfg(test)]
mod catalog_order_tests;

/// A verified stored root and the provider token protecting its next update.
#[derive(Debug, Clone)]
pub struct RootSnapshot {
Expand Down
Loading
Loading