use anybytes::Bytes;
use ed25519_dalek::VerifyingKey;
use hex_literal::hex;
use memmap2::MmapOptions;
use memmap2::MmapRaw;
use std::collections::BTreeSet;
use std::convert::Infallible;
use std::error::Error;
use std::fs::File;
use std::fs::OpenOptions;
use std::io::IoSlice;
use std::io::Write;
use std::path::Path;
use std::ptr::slice_from_raw_parts;
use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::Arc;
use std::time::SystemTime;
use std::time::UNIX_EPOCH;
use zerocopy::Immutable;
use zerocopy::IntoBytes;
use zerocopy::KnownLayout;
use zerocopy::TryFromBytes;
use crate::blob::encodings::UnknownBlob;
use crate::blob::Blob;
use crate::blob::BlobEncoding;
use crate::blob::IntoBlob;
use crate::blob::TryFromBlob;
use crate::capability::{
CapabilityProof, CapabilityProofId, CAPABILITY_PROOF_EDGE_LEN, CAPABILITY_PROOF_HEADER_LEN,
CAPABILITY_PROOF_MAGIC, MAX_CAPABILITY_PROOF_STEPS,
};
use crate::collection::store::{selectors_match_record, CollectionRead};
pub use crate::collection::LegacyUnsignedCollectionEquation;
pub use crate::collection::RetiredCollectionEquation;
use crate::collection::{
collection_merge_bytes_len, CollectionCommit, CollectionData, CollectionDerive,
CollectionHandle, CollectionMap, CollectionMerge, CollectionRecord, CollectionRecordSelector,
CollectionStore, SourceLocator, COLLECTION_MERGE_FIXED_BYTES_LEN, MAX_MERGE_INPUTS,
MIN_MERGE_INPUTS,
};
use crate::id::Id;
use crate::id::RawId;
use crate::inline::encodings::ed25519::{ED25519PublicKey, ED25519RComponent, ED25519SComponent};
use crate::inline::encodings::hash::Blake3;
use crate::inline::encodings::hash::Hash;
use crate::inline::Inline;
use crate::inline::InlineEncoding;
use crate::inline::RawInline;
use crate::patch::Entry;
use crate::patch::{IdentitySchema, XorSip128, PATCH};
use crate::prelude::blobencodings::SimpleArchive;
use crate::prelude::inlineencodings::Handle;
use crate::repo::proof::{CapabilityProofRead, CapabilityProofStore};
use crate::repo::{
SnapshotSource, WantRequest, WantRequestDecodeError, WANT_REQUEST_BYTES_LEN,
WANT_REQUEST_KIND_BLOB_V1,
};
#[cfg(test)]
mod append_visibility_tests;
mod record_kind;
pub use record_kind::{described_kinds, description_blobs, RecordKind, KIND_PILE_RECORD};
pub(crate) fn collection_record_kind(record: CollectionRecord) -> RecordKind {
Inline::new(match record {
CollectionRecord::Commit(_) => record_kind::KIND_COLLECTION_COMMIT,
CollectionRecord::Merge(_) => record_kind::KIND_COLLECTION_MERGE,
CollectionRecord::Derive(_) => record_kind::KIND_COLLECTION_DERIVE,
CollectionRecord::Map(_) => record_kind::KIND_COLLECTION_MAP,
})
}
pub(crate) fn capability_proof_record_kind() -> RecordKind {
Inline::new(record_kind::KIND_AUTH_PROOF)
}
pub(crate) fn want_record_kind() -> RecordKind {
Inline::new(record_kind::KIND_WANT)
}
pub(crate) fn blob_record_kind() -> RecordKind {
Inline::new(record_kind::KIND_BLOB)
}
#[cfg(test)]
pub(crate) fn assert_record_kind_description_resident<R>(reader: &R, kind: RecordKind)
where
R: crate::repo::BlobStoreGet,
{
use crate::blob::encodings::utf8string::UTF8String;
use crate::prelude::{find, pattern};
use crate::trible::TribleSet;
let description = reader
.get::<TribleSet, SimpleArchive>(kind)
.unwrap_or_else(|_| {
panic!(
"record-kind archive {} is absent",
hex::encode_upper(kind.raw)
)
});
let text_handles = find!(
(
name: Inline<Handle<UTF8String>>,
layout: Inline<Handle<UTF8String>>
),
pattern!(&description, [{
crate::metadata::name: ?name,
crate::metadata::description: ?layout
}])
)
.collect::<Vec<_>>();
assert_eq!(text_handles.len(), 1);
for (name, layout) in text_handles {
for handle in [name, layout] {
assert!(
reader
.get::<anybytes::View<str>, UTF8String>(handle)
.is_ok(),
"record-kind description child {} is absent",
hex::encode_upper(handle.raw)
);
}
}
}
fn pin_head_record_kind() -> RecordKind {
Inline::new(record_kind::KIND_PIN_HEAD)
}
fn retain_record_kind_if_resident(
roots: &mut super::RetentionRoots,
reader: &PileFileSnapshot,
kind: RecordKind,
) {
if reader
.contains_blob(kind)
.expect("PileFileSnapshot residency lookup is infallible")
{
roots.retain_recursive(kind);
}
}
const MAGIC_MARKER_BLOB: RawId = hex!("1E08B022FF2F47B6EBACF1D68EB35D96");
const MAGIC_MARKER_BRANCH: RawId = hex!("2BC991A7F5D5D2A3A468C53B0AA03504");
const MAGIC_MARKER_BRANCH_TOMBSTONE: RawId = hex!("E888CC787202D2AE4C654BFE9699C430");
const MAGIC_MARKER_ENVELOPE: RawId = hex!("E5A95E5D8A0BBA8782E46B9C9E73B313");
const FRAME_MAGIC_LEN: usize = 28;
const FRAME_MAGIC: [u8; FRAME_MAGIC_LEN] =
hex!("0371B249F0626B2ABDDB80E23EA969059D9656A5EA5A497320351F3B");
const MAGIC_MARKER_BLOB_V3: RawId = hex!("9C33EEB525065A62EAEC4BE43DCC355A");
const MAGIC_MARKER_BRANCH_V3: RawId = hex!("AC363D04AFE1AF17B39581B1E23021D7");
const MAGIC_MARKER_BRANCH_TOMBSTONE_V3: RawId = hex!("D0CBA0C8EAAB4C0C73121C3205671E4F");
const MAGIC_MARKER_WEAK_PIN_V3: RawId = hex!("8F3EEFEDECD491F63F6EAAA5FD6F3D5E");
const MAGIC_MARKER_WEAK_UNPIN_V3: RawId = hex!("2D76662DFF0187EC36A8C90B12BB8B0D");
const MAGIC_MARKER_WANT_ASSERT_V2: RawId = hex!("9A06797600FA90B8A8259B0ED029EC21");
const MAGIC_MARKER_WANT_RETRACT_V2: RawId = hex!("2D957A780A52E474F58A06D44D6FE46C");
const MAGIC_MARKER_COLLECTION_DEFINITION_V3: RawId = hex!("3BE108504E4F5242FB24AA72D6D94CE1");
const MAGIC_MARKER_COLLECTION_COMMIT_V3: RawId = hex!("BB758AA6F79FBFC4D1958592A8956777");
const MAGIC_MARKER_COLLECTION_MERGE_V3: RawId = hex!("CC0108AC1DF4F335AFA856A529C42BE9");
const MAGIC_MARKER_COLLECTION_DERIVE_V3: RawId = hex!("07ECF056F6F015D94389FFF21F851480");
const MAGIC_MARKER_COLLECTION_COMMIT_V4: RawId = hex!("CBF2CF97D52A3486E16C12D70D397C66");
const MAGIC_MARKER_COLLECTION_MERGE_V4: RawId = hex!("9F5D028D4C423620D6957A5F726FA727");
const MAGIC_MARKER_COLLECTION_DERIVE_V4: RawId = hex!("ECFB2EE90ED8042244F7BAC704454BB9");
const MAGIC_MARKER_COLLECTION_DERIVE_V5: RawId = hex!("ED6B46F7286D4556B076C17B79FD8315");
const MAGIC_MARKER_LOCAL_CELL_V3: RawId = hex!("24264FA9EE46A1ACC0E024AE69774B09");
const MAGIC_MARKER_LOCAL_CELL_TOMBSTONE_V3: RawId = hex!("4FE372AE868D22A44DED7A60D579B651");
const BLOB_HEADER_LEN: usize = std::mem::size_of::<BlobHeader>();
const BLOB_ALIGNMENT: usize = BLOB_HEADER_LEN;
const GPU_DATA_ALIGNMENT: usize = 256;
const V3_HEADER_LEN: usize = 256;
const ENVELOPE_HEADER_LEN: usize = 256;
const FRAME_KIND_OFFSET: usize = FRAME_MAGIC_LEN + 4;
const FRAME_BODY_OFFSET: usize = FRAME_KIND_OFFSET + 32;
const ENVELOPE_BLOCK_LEN: usize = GPU_DATA_ALIGNMENT;
const ENVELOPE_HEADER_BLOCKS: u32 = 1;
fn block_post_pad(data_len: usize) -> usize {
(ENVELOPE_BLOCK_LEN - (data_len % ENVELOPE_BLOCK_LEN)) % ENVELOPE_BLOCK_LEN
}
const ATOMIC_WRITE_LIMIT: usize = 1 << 30;
#[cfg(any(feature = "parallel", test))]
const PARALLEL_BLAKE3_THRESHOLD: usize = 1 << 20;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ValidationState {
Validated,
Invalid,
}
#[cfg(feature = "parallel")]
fn should_parallelize_validation(len: usize) -> bool {
len >= PARALLEL_BLAKE3_THRESHOLD && rayon::current_num_threads() > 1
}
#[derive(Debug, Clone, Copy)]
enum ValidationStrategy {
Serial,
ParallelIfLarge,
}
fn classify_validation(
computed: Inline<Hash<Blake3>>,
expected: &Inline<Hash<Blake3>>,
) -> ValidationState {
if computed == *expected {
ValidationState::Validated
} else {
ValidationState::Invalid
}
}
fn compute_validation_state(
bytes: &Bytes,
expected: &Inline<Hash<Blake3>>,
strategy: ValidationStrategy,
) -> ValidationState {
#[cfg(not(feature = "parallel"))]
let _ = strategy;
#[cfg(feature = "parallel")]
if matches!(strategy, ValidationStrategy::ParallelIfLarge)
&& should_parallelize_validation(bytes.len())
{
let mut hasher = blake3::Hasher::new();
hasher.update_rayon(bytes);
let computed = Inline::new(*hasher.finalize().as_bytes());
return classify_validation(computed, expected);
}
classify_validation(Hash::<Blake3>::digest(bytes), expected)
}
#[derive(Debug, Default)]
struct CachedValidation(AtomicU8);
impl CachedValidation {
const UNKNOWN: u8 = 0;
const VALIDATED: u8 = 1;
const INVALID: u8 = 2;
fn decode(state: u8) -> Option<ValidationState> {
match state {
Self::UNKNOWN => None,
Self::VALIDATED => Some(ValidationState::Validated),
Self::INVALID => Some(ValidationState::Invalid),
_ => unreachable!("CachedValidation contains an invalid state"),
}
}
const fn encode(state: ValidationState) -> u8 {
match state {
ValidationState::Validated => Self::VALIDATED,
ValidationState::Invalid => Self::INVALID,
}
}
fn cached(&self) -> Option<ValidationState> {
Self::decode(self.0.load(Ordering::Acquire))
}
fn state(
&self,
bytes: &Bytes,
expected: &Inline<Hash<Blake3>>,
strategy: ValidationStrategy,
) -> ValidationState {
if let Some(cached) = self.cached() {
return cached;
}
let computed = compute_validation_state(bytes, expected, strategy);
let encoded = Self::encode(computed);
match self
.0
.compare_exchange(Self::UNKNOWN, encoded, Ordering::AcqRel, Ordering::Acquire)
{
Ok(_) => computed,
Err(published) => Self::decode(published)
.expect("a failed first-publication race must observe a cached result"),
}
}
}
#[derive(Debug, Clone, Copy)]
struct IndexEntry {
record_offset: usize,
}
impl IndexEntry {
fn new(record_offset: usize) -> Self {
Self { record_offset }
}
}
fn blob_occurrence_key(hash: &RawInline, entry: IndexEntry) -> [u8; 40] {
let mut key = [0; 40];
key[..32].copy_from_slice(hash);
key[32..].copy_from_slice(
&u64::try_from(entry.record_offset)
.expect("a pile offset must fit in the portable u64 key")
.to_be_bytes(),
);
key
}
fn blob_occurrence_entry(offset: [u8; 8]) -> IndexEntry {
IndexEntry::new(
usize::try_from(u64::from_be_bytes(offset))
.expect("an indexed pile offset must fit this platform's usize"),
)
}
mod blob_occurrence_key {
crate::key_segmentation!(Segments, 40, [32, 8]);
crate::key_schema!(Schema, Segments, 40, [0, 1]);
}
mod collection_record_key {
crate::key_segmentation!(Segments, 72, [32, 32, 8]);
crate::key_schema!(Schema, Segments, 72, [0, 1, 2]);
}
type PileBlobIndex = PATCH<40, blob_occurrence_key::Schema, CachedValidation, XorSip128>;
type CollectionRecordIndex = PATCH<72, collection_record_key::Schema, (), XorSip128>;
type CapabilityProofIndex = PATCH<32, IdentitySchema, CapabilityProof, XorSip128>;
type LegacyCollectionHeaderIndex = PATCH<V3_HEADER_LEN, IdentitySchema>;
fn collection_record_output(record: &CollectionRecord) -> CollectionData {
match record {
CollectionRecord::Commit(commit) => commit.data(),
CollectionRecord::Merge(merge) => merge.result(),
CollectionRecord::Derive(derive) => derive.output(),
CollectionRecord::Map(map) => map.attachment(),
}
}
fn collection_record_member_prefix(record: &CollectionRecord) -> [u8; 64] {
let mut prefix = [0; 64];
prefix[..32].copy_from_slice(&record.collection().raw);
prefix[32..].copy_from_slice(&collection_record_output(record).raw);
prefix
}
fn collection_record_key(record: &CollectionRecord, offset: usize) -> [u8; 72] {
let mut key = [0; 72];
key[..64].copy_from_slice(&collection_record_member_prefix(record));
key[64..].copy_from_slice(&(offset as u64).to_be_bytes());
key
}
fn collection_record_offset(key_offset: &[u8; 8]) -> usize {
usize::try_from(u64::from_be_bytes(*key_offset))
.expect("an indexed frame offset lies inside the applied prefix")
}
#[cfg(test)]
thread_local! {
static COLLECTION_RECORD_READS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
fn collection_record_at(mmap: &MmapRaw, applied_length: usize, offset: usize) -> CollectionRecord {
#[cfg(test)]
COLLECTION_RECORD_READS.with(|reads| reads.set(reads.get() + 1));
debug_assert!(offset < applied_length);
let bytes = unsafe {
slice_from_raw_parts(mmap.as_ptr().add(offset), applied_length - offset)
.as_ref()
.expect("PileFile mapping pointer is valid for its applied prefix")
};
match decode_record(bytes, offset) {
Ok(PileRecord {
content: PileRecordContent::Collection { record },
..
}) => record,
_ => unreachable!("an indexed frame offset names an applied collection record"),
}
}
struct MemberRun<T = CollectionRecord> {
member: Option<[u8; 64]>,
seen: BTreeSet<T>,
}
impl<T: Ord> MemberRun<T> {
fn new() -> Self {
Self {
member: None,
seen: BTreeSet::new(),
}
}
fn first(&mut self, key: &[u8; 72], record: T) -> bool {
let member: [u8; 64] = key[..64]
.try_into()
.expect("key head is collection and member");
if self.member != Some(member) {
self.member = Some(member);
self.seen.clear();
}
self.seen.insert(record)
}
}
fn collection_record_indexed(
index: &CollectionRecordIndex,
mmap: &MmapRaw,
applied_length: usize,
record: &CollectionRecord,
) -> bool {
let mut present = false;
index.infixes(
&collection_record_member_prefix(record),
|offset: &[u8; 8]| {
if !present {
present =
collection_record_at(mmap, applied_length, collection_record_offset(offset))
== *record;
}
},
);
present
}
fn retired_equation_member_prefix(equation: &RetiredCollectionEquation) -> [u8; 64] {
let mut prefix = [0; 64];
prefix[..32].copy_from_slice(&equation.collection().raw);
prefix[32..].copy_from_slice(&equation.produced().raw);
prefix
}
fn retired_equation_key(equation: &RetiredCollectionEquation, offset: usize) -> [u8; 72] {
let mut key = [0; 72];
key[..64].copy_from_slice(&retired_equation_member_prefix(equation));
key[64..].copy_from_slice(&(offset as u64).to_be_bytes());
key
}
fn retired_equation_at(
mmap: &MmapRaw,
applied_length: usize,
offset: usize,
) -> RetiredCollectionEquation {
debug_assert!(offset < applied_length);
let bytes = unsafe {
slice_from_raw_parts(mmap.as_ptr().add(offset), applied_length - offset)
.as_ref()
.expect("PileFile mapping pointer is valid for its applied prefix")
};
match decode_record(bytes, offset) {
Ok(PileRecord {
content: PileRecordContent::RetiredCollectionEquation { equation },
..
}) => equation,
_ => unreachable!("an indexed frame offset names an applied retired equation"),
}
}
fn retired_equation_indexed(
index: &CollectionRecordIndex,
mmap: &MmapRaw,
applied_length: usize,
equation: &RetiredCollectionEquation,
) -> bool {
let mut present = false;
index.infixes(
&retired_equation_member_prefix(equation),
|offset: &[u8; 8]| {
if !present {
present =
retired_equation_at(mmap, applied_length, collection_record_offset(offset))
== *equation;
}
},
);
present
}
fn first_blob_occurrence(occurrences: &PileBlobIndex, hash: &RawInline) -> Option<IndexEntry> {
occurrences
.first_infix_range(hash, &[u8::MIN; 8], &[u8::MAX; 8])
.map(blob_occurrence_entry)
}
fn next_blob_occurrence(
occurrences: &PileBlobIndex,
hash: &RawInline,
after: IndexEntry,
) -> Option<IndexEntry> {
let after = u64::try_from(after.record_offset)
.expect("a pile offset must fit in the portable u64 key")
.to_be_bytes();
occurrences
.next_infix_after(hash, &after, &[u8::MAX; 8])
.map(blob_occurrence_entry)
}
fn blob_occurrence_validation<'a>(
occurrences: &'a PileBlobIndex,
hash: &RawInline,
entry: IndexEntry,
) -> &'a CachedValidation {
occurrences
.get(&blob_occurrence_key(hash, entry))
.expect("an enumerated blob occurrence must retain its leaf value")
}
#[derive(TryFromBytes, IntoBytes, Immutable, KnownLayout, Copy, Clone)]
#[repr(C)]
struct BranchHeader {
magic_marker: RawId,
branch_id: RawId,
hash: RawInline,
}
#[derive(TryFromBytes, IntoBytes, Immutable, KnownLayout, Copy, Clone)]
#[repr(C)]
struct BranchTombstoneHeader {
magic_marker: RawId,
branch_id: RawId,
reserved: RawInline,
}
#[derive(TryFromBytes, IntoBytes, Immutable, KnownLayout, Copy, Clone)]
#[repr(C)]
struct BlobHeader {
magic_marker: RawId,
timestamp: u64,
length: u64,
hash: RawInline,
}
impl BlobHeader {
#[cfg(test)]
fn new(timestamp: u64, length: u64, hash: Inline<Hash<Blake3>>) -> Self {
Self {
magic_marker: MAGIC_MARKER_BLOB,
timestamp,
length,
hash: hash.raw,
}
}
}
#[derive(TryFromBytes, IntoBytes, Immutable, KnownLayout, Copy, Clone)]
#[repr(C)]
struct BlobHeaderV3 {
magic_marker: RawId,
timestamp: u64,
length: u64,
hash: RawInline,
reserved: [u8; 192],
}
impl BlobHeaderV3 {
#[cfg(test)]
fn new(timestamp: u64, length: u64, hash: Inline<Hash<Blake3>>) -> Self {
Self {
magic_marker: MAGIC_MARKER_BLOB_V3,
timestamp,
length,
hash: hash.raw,
reserved: [0u8; 192],
}
}
}
#[derive(TryFromBytes, IntoBytes, Immutable, KnownLayout, Copy, Clone)]
#[repr(C)]
struct BranchHeaderV3 {
magic_marker: RawId,
branch_id: RawId,
hash: RawInline,
reserved: [u8; 192],
}
#[derive(TryFromBytes, IntoBytes, Immutable, KnownLayout, Copy, Clone)]
#[repr(C)]
struct BranchTombstoneHeaderV3 {
magic_marker: RawId,
branch_id: RawId,
reserved: [u8; 224],
}
#[derive(TryFromBytes, IntoBytes, Immutable, KnownLayout, Copy, Clone)]
#[repr(C)]
struct WeakPinHeaderV3 {
magic_marker: RawId,
handle: RawInline,
reserved: [u8; 208],
}
#[derive(TryFromBytes, IntoBytes, Immutable, KnownLayout, Copy, Clone)]
#[repr(C)]
struct WeakUnpinHeaderV3 {
magic_marker: RawId,
handle: RawInline,
reserved: [u8; 208],
}
#[derive(TryFromBytes, IntoBytes, Immutable, KnownLayout, Copy, Clone)]
#[repr(C)]
struct CollectionDefinitionHeaderV3 {
magic_marker: RawId,
scope: RawId,
representation: RawId,
recipe: RawId,
reserved: [u8; 192],
}
#[derive(TryFromBytes, IntoBytes, Immutable, KnownLayout, Copy, Clone)]
#[repr(C)]
struct CollectionCommitHeaderV3 {
magic_marker: RawId,
collection: RawId,
data: RawInline,
metadata: RawInline,
public_key: RawInline,
signature_r: RawInline,
signature_s: RawInline,
reserved: [u8; 64],
}
#[derive(TryFromBytes, IntoBytes, Immutable, KnownLayout, Copy, Clone)]
#[repr(C)]
struct CollectionMergeHeaderV3 {
magic_marker: RawId,
collection: RawId,
low: RawInline,
high: RawInline,
result: RawInline,
reserved: [u8; 128],
}
#[derive(TryFromBytes, IntoBytes, Immutable, KnownLayout, Copy, Clone)]
#[repr(C)]
struct CollectionDeriveHeaderV3 {
magic_marker: RawId,
source: RawId,
target: RawId,
input: RawInline,
output: RawInline,
reserved: [u8; 144],
}
#[derive(TryFromBytes, IntoBytes, Immutable, KnownLayout, Copy, Clone)]
#[repr(C)]
struct CollectionCommitHeaderV4 {
magic_marker: RawId,
collection: RawInline,
data: RawInline,
metadata: RawInline,
public_key: RawInline,
signature_r: RawInline,
signature_s: RawInline,
reserved: [u8; 48],
}
#[derive(TryFromBytes, IntoBytes, Immutable, KnownLayout, Copy, Clone)]
#[repr(C)]
struct CollectionMergeHeaderV4 {
magic_marker: RawId,
collection: RawInline,
low: RawInline,
high: RawInline,
result: RawInline,
reserved: [u8; 112],
}
#[derive(TryFromBytes, IntoBytes, Immutable, KnownLayout, Copy, Clone)]
#[repr(C)]
struct CollectionDeriveHeaderV4 {
magic_marker: RawId,
source: RawInline,
target: RawInline,
input: RawInline,
output: RawInline,
reserved: [u8; 112],
}
#[derive(TryFromBytes, IntoBytes, Immutable, KnownLayout, Copy, Clone)]
#[repr(C)]
struct EnvelopePrefixV1 {
magic_marker: RawId,
record_kind: RawId,
span_blocks: [u8; 4],
}
#[derive(TryFromBytes, IntoBytes, Immutable, KnownLayout, Copy, Clone)]
#[repr(C)]
struct BlobHeaderEnvelopeV1 {
envelope_marker: RawId,
record_kind: RawId,
span_blocks: [u8; 4],
timestamp: [u8; 8],
length: [u8; 8],
hash: RawInline,
reserved: [u8; 172],
}
#[derive(TryFromBytes, IntoBytes, Immutable, KnownLayout, Copy, Clone)]
#[repr(C)]
struct BranchHeaderEnvelopeV1 {
envelope_marker: RawId,
record_kind: RawId,
span_blocks: [u8; 4],
branch_id: RawId,
hash: RawInline,
reserved: [u8; 172],
}
#[derive(TryFromBytes, IntoBytes, Immutable, KnownLayout, Copy, Clone)]
#[repr(C)]
struct BranchTombstoneHeaderEnvelopeV1 {
envelope_marker: RawId,
record_kind: RawId,
span_blocks: [u8; 4],
branch_id: RawId,
reserved: [u8; 204],
}
#[derive(TryFromBytes, IntoBytes, Immutable, KnownLayout, Copy, Clone)]
#[repr(C)]
struct WantHeaderEnvelopeV1 {
envelope_marker: RawId,
record_kind: RawId,
span_blocks: [u8; 4],
handle: RawInline,
reserved: [u8; 188],
}
#[derive(TryFromBytes, IntoBytes, Immutable, KnownLayout, Copy, Clone)]
#[repr(C)]
struct TypedWantHeaderEnvelopeV1 {
envelope_marker: RawId,
record_kind: RawId,
span_blocks: [u8; 4],
request_kind: u8,
field_a: RawInline,
field_b: RawInline,
field_c: RawInline,
reserved: [u8; 123],
}
impl TypedWantHeaderEnvelopeV1 {
fn retired_identity(&self) -> [u8; WANT_REQUEST_BYTES_LEN] {
retired_want_identity(
self.request_kind,
&self.field_a,
&self.field_b,
&self.field_c,
)
}
}
#[derive(TryFromBytes, IntoBytes, Immutable, KnownLayout, Copy, Clone)]
#[repr(C)]
struct CollectionCommitHeaderEnvelopeV1 {
envelope_marker: RawId,
record_kind: RawId,
span_blocks: [u8; 4],
collection: RawInline,
data: RawInline,
metadata: RawInline,
public_key: RawInline,
signature_r: RawInline,
signature_s: RawInline,
reserved: [u8; 28],
}
#[derive(TryFromBytes, IntoBytes, Immutable, KnownLayout, Copy, Clone)]
#[repr(C)]
struct CollectionMergeHeaderEnvelopeV1 {
envelope_marker: RawId,
record_kind: RawId,
span_blocks: [u8; 4],
collection: RawInline,
low: RawInline,
high: RawInline,
result: RawInline,
reserved: [u8; 92],
}
#[derive(TryFromBytes, IntoBytes, Immutable, KnownLayout, Copy, Clone)]
#[repr(C)]
struct CollectionDeriveHeaderEnvelopeV1 {
envelope_marker: RawId,
record_kind: RawId,
span_blocks: [u8; 4],
target: RawInline,
input: RawInline,
output: RawInline,
reserved: [u8; 124],
}
#[derive(TryFromBytes, IntoBytes, Immutable, KnownLayout, Copy, Clone)]
#[repr(C)]
struct CollectionDeriveHeaderEnvelopeV1V4 {
envelope_marker: RawId,
record_kind: RawId,
span_blocks: [u8; 4],
source: RawInline,
target: RawInline,
input: RawInline,
output: RawInline,
reserved: [u8; 92],
}
#[derive(TryFromBytes, IntoBytes, Immutable, KnownLayout, Copy, Clone)]
#[repr(C)]
struct RecordFrame {
magic: [u8; FRAME_MAGIC_LEN],
span_blocks: [u8; 4],
record_kind: RawInline,
}
#[derive(TryFromBytes, IntoBytes, Immutable, KnownLayout, Copy, Clone)]
#[repr(C)]
struct RetiredCapabilityProofRecordPrefix {
magic: [u8; FRAME_MAGIC_LEN],
span_blocks: [u8; 4],
record_kind: RawInline,
length: [u8; 8],
scalar_pad: [u8; 24],
}
#[cfg(test)]
impl RetiredCapabilityProofRecordPrefix {
fn new(span_blocks: u32, length: u64) -> Self {
Self {
magic: FRAME_MAGIC,
span_blocks: span_blocks.to_le_bytes(),
record_kind: record_kind::KIND_AUTH_PROOF_V1,
length: length.to_le_bytes(),
scalar_pad: [0u8; 24],
}
}
}
#[derive(TryFromBytes, IntoBytes, Immutable, KnownLayout, Copy, Clone)]
#[repr(C)]
struct BlobRecordHeader {
magic: [u8; FRAME_MAGIC_LEN],
span_blocks: [u8; 4],
record_kind: RawInline,
timestamp: [u8; 8],
length: [u8; 8],
scalar_pad: [u8; 16],
hash: RawInline,
reserved: [u8; 128],
}
impl BlobRecordHeader {
fn new(span_blocks: u32, timestamp: u64, length: u64, hash: Inline<Hash<Blake3>>) -> Self {
Self {
magic: FRAME_MAGIC,
span_blocks: span_blocks.to_le_bytes(),
record_kind: record_kind::KIND_BLOB,
timestamp: timestamp.to_le_bytes(),
length: length.to_le_bytes(),
scalar_pad: [0u8; 16],
hash: hash.raw,
reserved: [0u8; 128],
}
}
}
#[derive(TryFromBytes, IntoBytes, Immutable, KnownLayout, Copy, Clone)]
#[repr(C)]
struct PinHeadRecordHeader {
magic: [u8; FRAME_MAGIC_LEN],
span_blocks: [u8; 4],
record_kind: RawInline,
branch_id: RawId,
id_pad: [u8; 16],
hash: RawInline,
reserved: [u8; 128],
}
impl PinHeadRecordHeader {
fn new(branch_id: Id, hash: Inline<Handle<SimpleArchive>>) -> Self {
Self {
magic: FRAME_MAGIC,
span_blocks: ENVELOPE_HEADER_BLOCKS.to_le_bytes(),
record_kind: record_kind::KIND_PIN_HEAD,
branch_id: *branch_id,
id_pad: [0u8; 16],
hash: hash.raw,
reserved: [0u8; 128],
}
}
}
#[derive(TryFromBytes, IntoBytes, Immutable, KnownLayout, Copy, Clone)]
#[repr(C)]
struct PinTombstoneRecordHeader {
magic: [u8; FRAME_MAGIC_LEN],
span_blocks: [u8; 4],
record_kind: RawInline,
branch_id: RawId,
reserved: [u8; 176],
}
impl PinTombstoneRecordHeader {
fn new(branch_id: Id) -> Self {
Self {
magic: FRAME_MAGIC,
span_blocks: ENVELOPE_HEADER_BLOCKS.to_le_bytes(),
record_kind: record_kind::KIND_PIN_TOMBSTONE,
branch_id: *branch_id,
reserved: [0u8; 176],
}
}
}
#[derive(TryFromBytes, IntoBytes, Immutable, KnownLayout, Copy, Clone)]
#[repr(C)]
struct RetiredBlobWantRecordHeader {
magic: [u8; FRAME_MAGIC_LEN],
span_blocks: [u8; 4],
record_kind: RawInline,
handle: RawInline,
reserved: [u8; 160],
}
#[derive(TryFromBytes, IntoBytes, Immutable, KnownLayout, Copy, Clone)]
#[repr(C)]
struct WantRecordHeader {
magic: [u8; FRAME_MAGIC_LEN],
span_blocks: [u8; 4],
record_kind: RawInline,
request_kind: u8,
kind_pad: [u8; 31],
field_a: RawInline,
field_b: RawInline,
field_c: RawInline,
reserved: [u8; 64],
}
#[derive(TryFromBytes, IntoBytes, Immutable, KnownLayout, Copy, Clone)]
#[repr(C)]
struct RetiredPeerEvidenceRecordHeader {
magic: [u8; FRAME_MAGIC_LEN],
span_blocks: [u8; 4],
record_kind: RawInline,
team_public_key: RawInline,
peer_public_key: RawInline,
reserved: [u8; 128],
}
#[derive(TryFromBytes, IntoBytes, Immutable, KnownLayout, Copy, Clone)]
#[repr(C)]
struct RetiredStoreScopeRecordHeader {
magic: [u8; FRAME_MAGIC_LEN],
span_blocks: [u8; 4],
record_kind: RawInline,
team_public_key: RawInline,
reserved: [u8; 160],
}
impl WantRecordHeader {
fn new(request: WantRequest) -> Self {
let bytes = request.to_bytes();
let mut field_a = [0u8; 32];
let mut field_b = [0u8; 32];
let mut field_c = [0u8; 32];
field_a.copy_from_slice(&bytes[1..33]);
field_b.copy_from_slice(&bytes[33..65]);
field_c.copy_from_slice(&bytes[65..97]);
Self {
magic: FRAME_MAGIC,
span_blocks: ENVELOPE_HEADER_BLOCKS.to_le_bytes(),
record_kind: record_kind::KIND_WANT,
request_kind: bytes[0],
kind_pad: [0u8; 31],
field_a,
field_b,
field_c,
reserved: [0u8; 64],
}
}
fn retired_identity(&self) -> [u8; WANT_REQUEST_BYTES_LEN] {
retired_want_identity(
self.request_kind,
&self.field_a,
&self.field_b,
&self.field_c,
)
}
}
#[derive(TryFromBytes, IntoBytes, Immutable, KnownLayout, Copy, Clone)]
#[repr(C)]
struct CollectionCommitRecordHeader {
magic: [u8; FRAME_MAGIC_LEN],
span_blocks: [u8; 4],
record_kind: RawInline,
collection: RawInline,
data: RawInline,
metadata: RawInline,
public_key: RawInline,
signature_r: RawInline,
signature_s: RawInline,
}
impl CollectionCommitRecordHeader {
fn new(record: &CollectionCommit) -> Self {
let (signature_r, signature_s) = record.signature();
Self {
magic: FRAME_MAGIC,
span_blocks: ENVELOPE_HEADER_BLOCKS.to_le_bytes(),
record_kind: record_kind::KIND_COLLECTION_COMMIT,
collection: record.collection().raw,
data: record.data().raw,
metadata: record.metadata().raw,
public_key: record.public_key().raw,
signature_r: signature_r.raw,
signature_s: signature_s.raw,
}
}
}
#[derive(TryFromBytes, IntoBytes, Immutable, KnownLayout, Copy, Clone)]
#[repr(C)]
struct LegacyCollectionMergeRecordHeader {
magic: [u8; FRAME_MAGIC_LEN],
span_blocks: [u8; 4],
record_kind: RawInline,
collection: RawInline,
low: RawInline,
high: RawInline,
result: RawInline,
reserved: [u8; 64],
}
#[derive(TryFromBytes, IntoBytes, Immutable, KnownLayout, Copy, Clone)]
#[repr(C)]
struct RetiredCollectionMergeV8RecordHeader {
magic: [u8; FRAME_MAGIC_LEN],
span_blocks: [u8; 4],
record_kind: RawInline,
collection: RawInline,
low: RawInline,
high: RawInline,
result: RawInline,
public_key: RawInline,
signature_r: RawInline,
signature_s: RawInline,
reserved: [u8; 224],
}
fn collection_merge_frame(record: &CollectionMerge) -> Vec<u8> {
let dense = record.to_bytes();
let blocks = envelope_blocks_for_prefixed_payload(FRAME_BODY_OFFSET, dense.len())
.expect("a merge of at most sixteen inputs spans at most three blocks");
let frame_len = blocks as usize * ENVELOPE_BLOCK_LEN;
let mut frame = Vec::with_capacity(frame_len);
frame.extend_from_slice(
RecordFrame {
magic: FRAME_MAGIC,
span_blocks: blocks.to_le_bytes(),
record_kind: record_kind::KIND_COLLECTION_MERGE,
}
.as_bytes(),
);
frame.extend_from_slice(&dense);
frame.resize(frame_len, 0);
frame
}
fn retired_equation_frame(equation: &RetiredCollectionEquation) -> Vec<u8> {
match *equation {
RetiredCollectionEquation::MergeV8 {
collection,
low,
high,
result,
public_key,
signature_r,
signature_s,
} => RetiredCollectionMergeV8RecordHeader {
magic: FRAME_MAGIC,
span_blocks: 2u32.to_le_bytes(),
record_kind: record_kind::KIND_COLLECTION_MERGE_V8,
collection: collection.raw,
low: low.raw,
high: high.raw,
result: result.raw,
public_key: public_key.raw,
signature_r: signature_r.raw,
signature_s: signature_s.raw,
reserved: [0u8; 224],
}
.as_bytes()
.to_vec(),
RetiredCollectionEquation::DeriveV9 {
target,
input,
output,
public_key,
signature_r,
signature_s,
} => CollectionDeriveRecordHeader {
magic: FRAME_MAGIC,
span_blocks: 1u32.to_le_bytes(),
record_kind: record_kind::KIND_COLLECTION_DERIVE_V9,
target: target.raw,
input: input.raw,
output: output.raw,
public_key: public_key.raw,
signature_r: signature_r.raw,
signature_s: signature_s.raw,
}
.as_bytes()
.to_vec(),
}
}
#[derive(TryFromBytes, IntoBytes, Immutable, KnownLayout, Copy, Clone)]
#[repr(C)]
struct LegacyCollectionDeriveRecordHeader {
magic: [u8; FRAME_MAGIC_LEN],
span_blocks: [u8; 4],
record_kind: RawInline,
target: RawInline,
input: RawInline,
output: RawInline,
reserved: [u8; 96],
}
#[derive(TryFromBytes, IntoBytes, Immutable, KnownLayout, Copy, Clone)]
#[repr(C)]
struct CollectionDeriveRecordHeader {
magic: [u8; FRAME_MAGIC_LEN],
span_blocks: [u8; 4],
record_kind: RawInline,
target: RawInline,
input: RawInline,
output: RawInline,
public_key: RawInline,
signature_r: RawInline,
signature_s: RawInline,
}
impl CollectionDeriveRecordHeader {
fn new(record: &CollectionDerive) -> Self {
let (input, output) = (record.input(), record.output());
let (signature_r, signature_s) = record.signature();
Self {
magic: FRAME_MAGIC,
span_blocks: 1u32.to_le_bytes(),
record_kind: record_kind::KIND_COLLECTION_DERIVE,
target: record.collection().raw,
input: input.raw(),
output: output.raw,
public_key: record.public_key().raw,
signature_r: signature_r.raw,
signature_s: signature_s.raw,
}
}
}
#[derive(TryFromBytes, IntoBytes, Immutable, KnownLayout, Copy, Clone)]
#[repr(C)]
struct CollectionMapRecordHeader {
magic: [u8; FRAME_MAGIC_LEN],
span_blocks: [u8; 4],
record_kind: RawInline,
attached: RawInline,
node: RawInline,
attachment: RawInline,
public_key: RawInline,
signature_r: RawInline,
signature_s: RawInline,
}
impl CollectionMapRecordHeader {
fn new(record: &CollectionMap) -> Self {
let (signature_r, signature_s) = record.signature();
Self {
magic: FRAME_MAGIC,
span_blocks: 1u32.to_le_bytes(),
record_kind: record_kind::KIND_COLLECTION_MAP,
attached: record.attached().raw,
node: record.node().raw,
attachment: record.attachment().raw,
public_key: record.public_key().raw,
signature_r: signature_r.raw,
signature_s: signature_s.raw,
}
}
}
fn retired_want_identity(
request_kind: u8,
field_a: &RawInline,
field_b: &RawInline,
field_c: &RawInline,
) -> [u8; WANT_REQUEST_BYTES_LEN] {
let mut identity = [0u8; WANT_REQUEST_BYTES_LEN];
identity[0] = request_kind;
identity[1..33].copy_from_slice(field_a);
identity[33..65].copy_from_slice(field_b);
identity[65..97].copy_from_slice(field_c);
identity
}
fn envelope_blocks_for_payload(data_len: usize) -> Option<u32> {
let payload_blocks = data_len.checked_add(ENVELOPE_BLOCK_LEN - 1)? / ENVELOPE_BLOCK_LEN;
u32::try_from(payload_blocks)
.ok()?
.checked_add(ENVELOPE_HEADER_BLOCKS)
}
fn envelope_blocks_for_prefixed_payload(prefix_len: usize, data_len: usize) -> Option<u32> {
let record_len = prefix_len.checked_add(data_len)?;
let blocks = record_len.checked_add(ENVELOPE_BLOCK_LEN - 1)? / ENVELOPE_BLOCK_LEN;
u32::try_from(blocks).ok()
}
fn prefixed_payload_post_pad(prefix_len: usize, data_len: usize) -> Option<usize> {
prefix_len.checked_add(data_len).map(block_post_pad)
}
fn collection_record_header(record: &CollectionRecord) -> Vec<u8> {
match record {
CollectionRecord::Commit(record) => CollectionCommitRecordHeader::new(record)
.as_bytes()
.to_vec(),
CollectionRecord::Merge(record) => collection_merge_frame(record),
CollectionRecord::Derive(record) => CollectionDeriveRecordHeader::new(record)
.as_bytes()
.to_vec(),
CollectionRecord::Map(record) => CollectionMapRecordHeader::new(record).as_bytes().to_vec(),
}
}
const _: () = {
assert!(std::mem::size_of::<BlobHeaderV3>() == V3_HEADER_LEN);
assert!(std::mem::size_of::<BranchHeaderV3>() == V3_HEADER_LEN);
assert!(std::mem::size_of::<BranchTombstoneHeaderV3>() == V3_HEADER_LEN);
assert!(std::mem::size_of::<WeakPinHeaderV3>() == V3_HEADER_LEN);
assert!(std::mem::size_of::<WeakUnpinHeaderV3>() == V3_HEADER_LEN);
assert!(std::mem::size_of::<CollectionDefinitionHeaderV3>() == V3_HEADER_LEN);
assert!(std::mem::size_of::<CollectionCommitHeaderV3>() == V3_HEADER_LEN);
assert!(std::mem::size_of::<CollectionMergeHeaderV3>() == V3_HEADER_LEN);
assert!(std::mem::size_of::<CollectionDeriveHeaderV3>() == V3_HEADER_LEN);
assert!(std::mem::size_of::<CollectionCommitHeaderV4>() == V3_HEADER_LEN);
assert!(std::mem::size_of::<CollectionMergeHeaderV4>() == V3_HEADER_LEN);
assert!(std::mem::size_of::<CollectionDeriveHeaderV4>() == V3_HEADER_LEN);
assert!(std::mem::size_of::<EnvelopePrefixV1>() == 36);
assert!(std::mem::size_of::<BlobHeaderEnvelopeV1>() == ENVELOPE_HEADER_LEN);
assert!(std::mem::size_of::<BranchHeaderEnvelopeV1>() == ENVELOPE_HEADER_LEN);
assert!(std::mem::size_of::<BranchTombstoneHeaderEnvelopeV1>() == ENVELOPE_HEADER_LEN);
assert!(std::mem::size_of::<WantHeaderEnvelopeV1>() == ENVELOPE_HEADER_LEN);
assert!(std::mem::size_of::<TypedWantHeaderEnvelopeV1>() == ENVELOPE_HEADER_LEN);
assert!(std::mem::size_of::<CollectionCommitHeaderEnvelopeV1>() == ENVELOPE_HEADER_LEN);
assert!(std::mem::size_of::<CollectionMergeHeaderEnvelopeV1>() == ENVELOPE_HEADER_LEN);
assert!(std::mem::size_of::<CollectionDeriveHeaderEnvelopeV1>() == ENVELOPE_HEADER_LEN);
assert!(std::mem::size_of::<RecordFrame>() == FRAME_BODY_OFFSET);
assert!(std::mem::size_of::<RetiredCapabilityProofRecordPrefix>() == 96);
assert!(std::mem::size_of::<BlobRecordHeader>() == ENVELOPE_HEADER_LEN);
assert!(std::mem::size_of::<PinHeadRecordHeader>() == ENVELOPE_HEADER_LEN);
assert!(std::mem::size_of::<PinTombstoneRecordHeader>() == ENVELOPE_HEADER_LEN);
assert!(std::mem::size_of::<RetiredBlobWantRecordHeader>() == ENVELOPE_HEADER_LEN);
assert!(std::mem::size_of::<WantRecordHeader>() == ENVELOPE_HEADER_LEN);
assert!(std::mem::size_of::<RetiredPeerEvidenceRecordHeader>() == ENVELOPE_HEADER_LEN);
assert!(std::mem::size_of::<RetiredStoreScopeRecordHeader>() == ENVELOPE_HEADER_LEN);
assert!(std::mem::size_of::<CollectionCommitRecordHeader>() == ENVELOPE_HEADER_LEN);
assert!(std::mem::size_of::<LegacyCollectionMergeRecordHeader>() == ENVELOPE_HEADER_LEN);
assert!(std::mem::size_of::<LegacyCollectionDeriveRecordHeader>() == ENVELOPE_HEADER_LEN);
assert!(std::mem::size_of::<RetiredCollectionMergeV8RecordHeader>() == 2 * ENVELOPE_BLOCK_LEN);
assert!(std::mem::size_of::<CollectionDeriveRecordHeader>() == ENVELOPE_HEADER_LEN);
assert!(std::mem::size_of::<CollectionMapRecordHeader>() == ENVELOPE_HEADER_LEN);
};
#[derive(Debug, Clone, Copy)]
pub struct PileRecord {
pub offset: usize,
pub len: usize,
pub content: PileRecordContent,
}
#[derive(Debug, Clone, Copy, Eq, Ord, PartialEq, PartialOrd)]
pub enum LegacyCollectionRecordKindV3 {
Definition,
Commit,
Merge,
Derive,
}
#[derive(Debug, Clone, Copy, Eq, Ord, PartialEq, PartialOrd)]
pub enum OpaqueKind {
Legacy(RawId),
Described(RawInline),
}
impl AsRef<[u8]> for OpaqueKind {
fn as_ref(&self) -> &[u8] {
match self {
Self::Legacy(id) => &id[..],
Self::Described(handle) => &handle[..],
}
}
}
impl OpaqueKind {
pub fn as_bytes(&self) -> &[u8] {
self.as_ref()
}
pub fn description(&self) -> Option<RecordKind> {
match self {
Self::Legacy(_) => None,
Self::Described(handle) => Some(Inline::new(*handle)),
}
}
}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub enum PileRecordContent {
Blob {
timestamp: u64,
hash: Inline<Hash<Blake3>>,
data_offset: usize,
data_len: usize,
},
Branch {
branch_id: Id,
head: Inline<Handle<SimpleArchive>>,
},
BranchTombstone {
branch_id: Id,
},
Want {
request: WantRequest,
},
RetiredWantAssert {
identity: [u8; WANT_REQUEST_BYTES_LEN],
},
RetiredWantRetract {
identity: [u8; WANT_REQUEST_BYTES_LEN],
},
Collection {
record: CollectionRecord,
},
LegacyUnsignedCollectionEquation {
equation: LegacyUnsignedCollectionEquation,
},
RetiredCollectionEquation { equation: RetiredCollectionEquation },
CapabilityProof {
id: CapabilityProofId,
data_offset: usize,
data_len: usize,
},
RetiredCapabilityProof,
RetiredPeerEvidenceV1,
RetiredStoreScopeV1,
RetiredArtifactOfferV1,
LegacyCollectionV3 {
kind: LegacyCollectionRecordKindV3,
},
RetiredCollectionDeriveV4,
Opaque {
kind: OpaqueKind,
},
}
fn decode_enveloped_record(bytes: &[u8], offset: usize) -> Result<PileRecord, ReadError> {
let corrupt = || ReadError::CorruptPile {
valid_length: offset,
};
let (prefix, _) = RecordFrame::try_read_from_prefix(bytes).map_err(|_| corrupt())?;
let declared_blocks = u32::from_le_bytes(prefix.span_blocks);
if prefix.magic != FRAME_MAGIC || declared_blocks == 0 {
return Err(corrupt());
}
let span_blocks = usize::try_from(declared_blocks).map_err(|_| corrupt())?;
let len = span_blocks
.checked_mul(ENVELOPE_BLOCK_LEN)
.ok_or_else(corrupt)?;
if len < ENVELOPE_HEADER_LEN || bytes.len() < len {
return Err(corrupt());
}
let fixed_header = || {
if declared_blocks == ENVELOPE_HEADER_BLOCKS {
Ok(())
} else {
Err(corrupt())
}
};
match prefix.record_kind {
record_kind::KIND_BLOB => {
let (header, _) =
BlobRecordHeader::try_read_from_prefix(bytes).map_err(|_| corrupt())?;
if nonzero(&[&header.scalar_pad[..], &header.reserved[..]]) {
return Err(corrupt());
}
let data_len =
usize::try_from(u64::from_le_bytes(header.length)).map_err(|_| corrupt())?;
let expected_blocks = envelope_blocks_for_payload(data_len).ok_or_else(corrupt)?;
if declared_blocks != expected_blocks {
return Err(corrupt());
}
let data_offset = offset
.checked_add(ENVELOPE_HEADER_LEN)
.ok_or_else(corrupt)?;
Ok(PileRecord {
offset,
len,
content: PileRecordContent::Blob {
timestamp: u64::from_le_bytes(header.timestamp),
hash: Inline::new(header.hash),
data_offset,
data_len,
},
})
}
record_kind::KIND_PIN_HEAD => {
fixed_header()?;
let (header, _) =
PinHeadRecordHeader::try_read_from_prefix(bytes).map_err(|_| corrupt())?;
if nonzero(&[&header.id_pad[..], &header.reserved[..]]) {
return Err(corrupt());
}
let branch_id = Id::new(header.branch_id).ok_or_else(corrupt)?;
Ok(PileRecord {
offset,
len,
content: PileRecordContent::Branch {
branch_id,
head: Inline::<Hash<Blake3>>::new(header.hash).into(),
},
})
}
record_kind::KIND_PIN_TOMBSTONE => {
fixed_header()?;
let (header, _) =
PinTombstoneRecordHeader::try_read_from_prefix(bytes).map_err(|_| corrupt())?;
if nonzero(&[&header.reserved[..]]) {
return Err(corrupt());
}
let branch_id = Id::new(header.branch_id).ok_or_else(corrupt)?;
Ok(PileRecord {
offset,
len,
content: PileRecordContent::BranchTombstone { branch_id },
})
}
record_kind::KIND_WANT => {
fixed_header()?;
let (header, _) =
WantRecordHeader::try_read_from_prefix(bytes).map_err(|_| corrupt())?;
if nonzero(&[&header.kind_pad[..], &header.reserved[..]]) {
return Err(corrupt());
}
let identity = header.retired_identity();
let content = match WantRequest::from_bytes(identity) {
Ok(request) => PileRecordContent::Want { request },
Err(WantRequestDecodeError::UnknownKind(_)) => {
PileRecordContent::RetiredWantAssert { identity }
}
Err(WantRequestDecodeError::NonZeroUnusedFields { .. }) => return Err(corrupt()),
};
Ok(PileRecord {
offset,
len,
content,
})
}
record_kind::KIND_BLOB_WANT_ASSERT | record_kind::KIND_BLOB_WANT_RETRACT => {
fixed_header()?;
let (header, _) =
RetiredBlobWantRecordHeader::try_read_from_prefix(bytes).map_err(|_| corrupt())?;
if nonzero(&[&header.reserved[..]]) {
return Err(corrupt());
}
let identity =
WantRequest::blob(Inline::<Handle<UnknownBlob>>::new(header.handle)).to_bytes();
let content = if prefix.record_kind == record_kind::KIND_BLOB_WANT_ASSERT {
PileRecordContent::RetiredWantAssert { identity }
} else {
PileRecordContent::RetiredWantRetract { identity }
};
Ok(PileRecord {
offset,
len,
content,
})
}
record_kind::KIND_WANT_ASSERT | record_kind::KIND_WANT_RETRACT => {
fixed_header()?;
let (header, _) =
WantRecordHeader::try_read_from_prefix(bytes).map_err(|_| corrupt())?;
if nonzero(&[&header.kind_pad[..], &header.reserved[..]]) {
return Err(corrupt());
}
let identity = header.retired_identity();
if identity[0] == WANT_REQUEST_KIND_BLOB_V1 {
return Err(corrupt());
}
let content = if prefix.record_kind == record_kind::KIND_WANT_ASSERT {
PileRecordContent::RetiredWantAssert { identity }
} else {
PileRecordContent::RetiredWantRetract { identity }
};
Ok(PileRecord {
offset,
len,
content,
})
}
record_kind::KIND_AUTH_PROOF_V1 | record_kind::KIND_AUTH_PROOF_V2 => {
let (header, _) = RetiredCapabilityProofRecordPrefix::try_read_from_prefix(bytes)
.map_err(|_| corrupt())?;
if nonzero(&[&header.scalar_pad[..]]) {
return Err(corrupt());
}
let data_len =
usize::try_from(u64::from_le_bytes(header.length)).map_err(|_| corrupt())?;
let prefix_len = std::mem::size_of::<RetiredCapabilityProofRecordPrefix>();
let expected_blocks =
envelope_blocks_for_prefixed_payload(prefix_len, data_len).ok_or_else(corrupt)?;
if declared_blocks != expected_blocks {
return Err(corrupt());
}
let data_end = prefix_len
.checked_add(data_len)
.filter(|end| *end <= len)
.ok_or_else(corrupt)?;
if nonzero(&[&bytes[data_end..len]]) {
return Err(corrupt());
}
let body = &bytes[prefix_len..data_end];
let structural = if prefix.record_kind == record_kind::KIND_AUTH_PROOF_V1 {
retired_capability_proof_v1_is_structural(body)
} else {
crate::capability::legacy_action_proof_is_structural(body)
};
if !structural {
return Err(corrupt());
}
Ok(PileRecord {
offset,
len,
content: PileRecordContent::RetiredCapabilityProof,
})
}
record_kind::KIND_AUTH_PROOF => {
let prefix_len = FRAME_KIND_OFFSET;
let padded = &bytes[prefix_len..len];
let edges = padded
.get(CAPABILITY_PROOF_HEADER_LEN..)
.ok_or_else(corrupt)?;
let count = edges
.chunks_exact(CAPABILITY_PROOF_EDGE_LEN)
.take(MAX_CAPABILITY_PROOF_STEPS + 1)
.take_while(|edge| edge.iter().any(|byte| *byte != 0))
.count();
let data_len = CAPABILITY_PROOF_HEADER_LEN + count * CAPABILITY_PROOF_EDGE_LEN;
let expected_blocks =
envelope_blocks_for_prefixed_payload(prefix_len, data_len).ok_or_else(corrupt)?;
if declared_blocks != expected_blocks || nonzero(&[&padded[data_len..]]) {
return Err(corrupt());
}
let data_end = prefix_len + data_len;
let proof_bytes = &bytes[prefix_len..data_end];
CapabilityProof::validate_bytes(proof_bytes).map_err(|_| corrupt())?;
let data_offset = offset.checked_add(prefix_len).ok_or_else(corrupt)?;
Ok(PileRecord {
offset,
len,
content: PileRecordContent::CapabilityProof {
id: Inline::new(Blake3::digest(proof_bytes)),
data_offset,
data_len,
},
})
}
record_kind::KIND_PEER_EVIDENCE => {
fixed_header()?;
let (header, _) = RetiredPeerEvidenceRecordHeader::try_read_from_prefix(bytes)
.map_err(|_| corrupt())?;
if nonzero(&[&header.reserved[..]]) {
return Err(corrupt());
}
Ok(PileRecord {
offset,
len,
content: PileRecordContent::RetiredPeerEvidenceV1,
})
}
record_kind::KIND_STORE_SCOPE => {
fixed_header()?;
let (header, _) = RetiredStoreScopeRecordHeader::try_read_from_prefix(bytes)
.map_err(|_| corrupt())?;
if nonzero(&[&header.reserved[..]]) {
return Err(corrupt());
}
Ok(PileRecord {
offset,
len,
content: PileRecordContent::RetiredStoreScopeV1,
})
}
record_kind::KIND_ARTIFACT_OFFER_V1 => {
fixed_header()?;
Ok(PileRecord {
offset,
len,
content: PileRecordContent::RetiredArtifactOfferV1,
})
}
record_kind::KIND_COLLECTION_COMMIT => {
fixed_header()?;
let (header, _) =
CollectionCommitRecordHeader::try_read_from_prefix(bytes).map_err(|_| corrupt())?;
Ok(PileRecord {
offset,
len,
content: PileRecordContent::Collection {
record: CollectionRecord::Commit(CollectionCommit::from_parts(
Inline::new(header.collection),
Inline::new(header.data),
Inline::new(header.metadata),
Inline::<ED25519PublicKey>::new(header.public_key),
Inline::<ED25519RComponent>::new(header.signature_r),
Inline::<ED25519SComponent>::new(header.signature_s),
)),
},
})
}
record_kind::KIND_COLLECTION_MERGE => {
let count_at = FRAME_BODY_OFFSET + COLLECTION_MERGE_FIXED_BYTES_LEN - 4;
let count = u32::from_be_bytes(
bytes[count_at..count_at + 4]
.try_into()
.expect("the count lies inside the first block"),
) as usize;
if !(MIN_MERGE_INPUTS..=MAX_MERGE_INPUTS).contains(&count) {
return Err(corrupt());
}
let dense_len = collection_merge_bytes_len(count);
let expected_blocks =
envelope_blocks_for_prefixed_payload(FRAME_BODY_OFFSET, dense_len)
.ok_or_else(corrupt)?;
if declared_blocks != expected_blocks {
return Err(corrupt());
}
let dense_end = FRAME_BODY_OFFSET + dense_len;
if nonzero(&[&bytes[dense_end..len]]) {
return Err(corrupt());
}
let record = CollectionMerge::from_bytes_trusted(&bytes[FRAME_BODY_OFFSET..dense_end])
.map_err(|_| corrupt())?;
Ok(PileRecord {
offset,
len,
content: PileRecordContent::Collection {
record: CollectionRecord::Merge(record),
},
})
}
record_kind::KIND_COLLECTION_DERIVE => {
fixed_header()?;
let (header, _) =
CollectionDeriveRecordHeader::try_read_from_prefix(bytes).map_err(|_| corrupt())?;
Ok(PileRecord {
offset,
len,
content: PileRecordContent::Collection {
record: CollectionRecord::Derive(CollectionDerive::from_parts(
Inline::new(header.target),
SourceLocator::from_raw(header.input),
Inline::new(header.output),
Inline::new(header.public_key),
Inline::new(header.signature_r),
Inline::new(header.signature_s),
)),
},
})
}
record_kind::KIND_COLLECTION_MAP => {
fixed_header()?;
let (header, _) =
CollectionMapRecordHeader::try_read_from_prefix(bytes).map_err(|_| corrupt())?;
Ok(PileRecord {
offset,
len,
content: PileRecordContent::Collection {
record: CollectionRecord::Map(CollectionMap::from_parts(
Inline::new(header.attached),
Inline::new(header.node),
Inline::new(header.attachment),
Inline::new(header.public_key),
Inline::new(header.signature_r),
Inline::new(header.signature_s),
)),
},
})
}
record_kind::KIND_COLLECTION_MERGE_V8 => {
if declared_blocks != 2 {
return Err(corrupt());
}
let (header, _) = RetiredCollectionMergeV8RecordHeader::try_read_from_prefix(bytes)
.map_err(|_| corrupt())?;
if nonzero(&[&header.reserved[..]]) {
return Err(corrupt());
}
let equation = RetiredCollectionEquation::merge_v8(
Inline::new(header.collection),
Inline::new(header.low),
Inline::new(header.high),
Inline::new(header.result),
Inline::new(header.public_key),
Inline::new(header.signature_r),
Inline::new(header.signature_s),
)
.map_err(|_| corrupt())?;
Ok(PileRecord {
offset,
len,
content: PileRecordContent::RetiredCollectionEquation { equation },
})
}
record_kind::KIND_COLLECTION_DERIVE_V9 => {
fixed_header()?;
let (header, _) =
CollectionDeriveRecordHeader::try_read_from_prefix(bytes).map_err(|_| corrupt())?;
Ok(PileRecord {
offset,
len,
content: PileRecordContent::RetiredCollectionEquation {
equation: RetiredCollectionEquation::DeriveV9 {
target: Inline::new(header.target),
input: Inline::new(header.input),
output: Inline::new(header.output),
public_key: Inline::new(header.public_key),
signature_r: Inline::new(header.signature_r),
signature_s: Inline::new(header.signature_s),
},
},
})
}
record_kind::KIND_COLLECTION_MERGE_UNSIGNED => {
fixed_header()?;
let (header, _) = LegacyCollectionMergeRecordHeader::try_read_from_prefix(bytes)
.map_err(|_| corrupt())?;
if nonzero(&[&header.reserved[..]]) || header.high < header.low {
return Err(corrupt());
}
Ok(PileRecord {
offset,
len,
content: PileRecordContent::LegacyUnsignedCollectionEquation {
equation: LegacyUnsignedCollectionEquation::Merge {
collection: Inline::new(header.collection),
low: Inline::new(header.low),
high: Inline::new(header.high),
result: Inline::new(header.result),
},
},
})
}
record_kind::KIND_COLLECTION_DERIVE_UNSIGNED => {
fixed_header()?;
let (header, _) = LegacyCollectionDeriveRecordHeader::try_read_from_prefix(bytes)
.map_err(|_| corrupt())?;
if nonzero(&[&header.reserved[..]]) {
return Err(corrupt());
}
Ok(PileRecord {
offset,
len,
content: PileRecordContent::LegacyUnsignedCollectionEquation {
equation: LegacyUnsignedCollectionEquation::Derive {
collection: Inline::new(header.target),
input: Inline::new(header.input),
output: Inline::new(header.output),
},
},
})
}
kind => Ok(PileRecord {
offset,
len,
content: PileRecordContent::Opaque {
kind: OpaqueKind::Described(kind),
},
}),
}
}
fn retired_capability_proof_v1_is_structural(bytes: &[u8]) -> bool {
const ROOT_LEN: usize = 32;
const EDGE_LEN: usize = 128;
const DELEGATE_OFFSET: usize = 96;
const MAX_STEPS: usize = u8::MAX as usize;
if bytes.len() < ROOT_LEN + EDGE_LEN || (bytes.len() - ROOT_LEN) % EDGE_LEN != 0 {
return false;
}
let steps = (bytes.len() - ROOT_LEN) / EDGE_LEN;
if steps > MAX_STEPS {
return false;
}
let Ok(root) = <[u8; 32]>::try_from(&bytes[..ROOT_LEN]) else {
return false;
};
if VerifyingKey::from_bytes(&root).is_err() {
return false;
}
bytes[ROOT_LEN..].chunks_exact(EDGE_LEN).all(|edge| {
let delegate: [u8; 32] = edge[DELEGATE_OFFSET..].try_into().expect("fixed V1 edge");
VerifyingKey::from_bytes(&delegate).is_ok()
})
}
fn nonzero(regions: &[&[u8]]) -> bool {
regions
.iter()
.any(|region| region.iter().any(|byte| *byte != 0))
}
fn decode_enveloped_record_v1(bytes: &[u8], offset: usize) -> Result<PileRecord, ReadError> {
let corrupt = || ReadError::CorruptPile {
valid_length: offset,
};
let (prefix, _) = EnvelopePrefixV1::try_read_from_prefix(bytes).map_err(|_| corrupt())?;
let declared_blocks = u32::from_le_bytes(prefix.span_blocks);
if prefix.magic_marker != MAGIC_MARKER_ENVELOPE || declared_blocks == 0 {
return Err(corrupt());
}
let span_blocks = usize::try_from(declared_blocks).map_err(|_| corrupt())?;
let len = span_blocks
.checked_mul(ENVELOPE_BLOCK_LEN)
.ok_or_else(corrupt)?;
if len < ENVELOPE_HEADER_LEN || bytes.len() < len {
return Err(corrupt());
}
let fixed_header = || {
if declared_blocks == ENVELOPE_HEADER_BLOCKS {
Ok(())
} else {
Err(corrupt())
}
};
match prefix.record_kind {
MAGIC_MARKER_BLOB_V3 => {
let (header, _) =
BlobHeaderEnvelopeV1::try_read_from_prefix(bytes).map_err(|_| corrupt())?;
if header.reserved.iter().any(|byte| *byte != 0) {
return Err(corrupt());
}
let data_len =
usize::try_from(u64::from_le_bytes(header.length)).map_err(|_| corrupt())?;
let expected_blocks = envelope_blocks_for_payload(data_len).ok_or_else(corrupt)?;
if declared_blocks != expected_blocks {
return Err(corrupt());
}
let data_offset = offset
.checked_add(ENVELOPE_HEADER_LEN)
.ok_or_else(corrupt)?;
Ok(PileRecord {
offset,
len,
content: PileRecordContent::Blob {
timestamp: u64::from_le_bytes(header.timestamp),
hash: Inline::new(header.hash),
data_offset,
data_len,
},
})
}
MAGIC_MARKER_BRANCH_V3 => {
fixed_header()?;
let (header, _) =
BranchHeaderEnvelopeV1::try_read_from_prefix(bytes).map_err(|_| corrupt())?;
if header.reserved.iter().any(|byte| *byte != 0) {
return Err(corrupt());
}
let branch_id = Id::new(header.branch_id).ok_or_else(corrupt)?;
Ok(PileRecord {
offset,
len,
content: PileRecordContent::Branch {
branch_id,
head: Inline::<Hash<Blake3>>::new(header.hash).into(),
},
})
}
MAGIC_MARKER_BRANCH_TOMBSTONE_V3 => {
fixed_header()?;
let (header, _) = BranchTombstoneHeaderEnvelopeV1::try_read_from_prefix(bytes)
.map_err(|_| corrupt())?;
if header.reserved.iter().any(|byte| *byte != 0) {
return Err(corrupt());
}
let branch_id = Id::new(header.branch_id).ok_or_else(corrupt)?;
Ok(PileRecord {
offset,
len,
content: PileRecordContent::BranchTombstone { branch_id },
})
}
MAGIC_MARKER_WEAK_PIN_V3 | MAGIC_MARKER_WEAK_UNPIN_V3 => {
fixed_header()?;
let (header, _) =
WantHeaderEnvelopeV1::try_read_from_prefix(bytes).map_err(|_| corrupt())?;
if header.reserved.iter().any(|byte| *byte != 0) {
return Err(corrupt());
}
let identity =
WantRequest::blob(Inline::<Handle<UnknownBlob>>::new(header.handle)).to_bytes();
let content = if prefix.record_kind == MAGIC_MARKER_WEAK_PIN_V3 {
PileRecordContent::RetiredWantAssert { identity }
} else {
PileRecordContent::RetiredWantRetract { identity }
};
Ok(PileRecord {
offset,
len,
content,
})
}
MAGIC_MARKER_WANT_ASSERT_V2 | MAGIC_MARKER_WANT_RETRACT_V2 => {
fixed_header()?;
let (header, _) =
TypedWantHeaderEnvelopeV1::try_read_from_prefix(bytes).map_err(|_| corrupt())?;
if header.reserved.iter().any(|byte| *byte != 0) {
return Err(corrupt());
}
let identity = header.retired_identity();
if identity[0] == WANT_REQUEST_KIND_BLOB_V1 {
return Err(corrupt());
}
let content = if prefix.record_kind == MAGIC_MARKER_WANT_ASSERT_V2 {
PileRecordContent::RetiredWantAssert { identity }
} else {
PileRecordContent::RetiredWantRetract { identity }
};
Ok(PileRecord {
offset,
len,
content,
})
}
MAGIC_MARKER_COLLECTION_COMMIT_V4 => {
fixed_header()?;
let (header, _) = CollectionCommitHeaderEnvelopeV1::try_read_from_prefix(bytes)
.map_err(|_| corrupt())?;
if header.reserved.iter().any(|byte| *byte != 0) {
return Err(corrupt());
}
Ok(PileRecord {
offset,
len,
content: PileRecordContent::Collection {
record: CollectionRecord::Commit(CollectionCommit::from_parts(
Inline::new(header.collection),
Inline::new(header.data),
Inline::new(header.metadata),
Inline::<ED25519PublicKey>::new(header.public_key),
Inline::<ED25519RComponent>::new(header.signature_r),
Inline::<ED25519SComponent>::new(header.signature_s),
)),
},
})
}
MAGIC_MARKER_COLLECTION_MERGE_V4 => {
fixed_header()?;
let (header, _) = CollectionMergeHeaderEnvelopeV1::try_read_from_prefix(bytes)
.map_err(|_| corrupt())?;
if header.reserved.iter().any(|byte| *byte != 0) || header.high < header.low {
return Err(corrupt());
}
Ok(PileRecord {
offset,
len,
content: PileRecordContent::LegacyUnsignedCollectionEquation {
equation: LegacyUnsignedCollectionEquation::Merge {
collection: Inline::new(header.collection),
low: Inline::new(header.low),
high: Inline::new(header.high),
result: Inline::new(header.result),
},
},
})
}
MAGIC_MARKER_COLLECTION_DERIVE_V4 => {
fixed_header()?;
let (header, _) = CollectionDeriveHeaderEnvelopeV1V4::try_read_from_prefix(bytes)
.map_err(|_| corrupt())?;
if header.reserved.iter().any(|byte| *byte != 0) {
return Err(corrupt());
}
Ok(PileRecord {
offset,
len,
content: PileRecordContent::RetiredCollectionDeriveV4,
})
}
MAGIC_MARKER_COLLECTION_DERIVE_V5 => {
fixed_header()?;
let (header, _) = CollectionDeriveHeaderEnvelopeV1::try_read_from_prefix(bytes)
.map_err(|_| corrupt())?;
if header.reserved.iter().any(|byte| *byte != 0) {
return Err(corrupt());
}
Ok(PileRecord {
offset,
len,
content: PileRecordContent::LegacyUnsignedCollectionEquation {
equation: LegacyUnsignedCollectionEquation::Derive {
collection: Inline::new(header.target),
input: Inline::new(header.input),
output: Inline::new(header.output),
},
},
})
}
kind @ (MAGIC_MARKER_LOCAL_CELL_V3 | MAGIC_MARKER_LOCAL_CELL_TOMBSTONE_V3) => {
fixed_header()?;
Ok(PileRecord {
offset,
len,
content: PileRecordContent::Opaque {
kind: OpaqueKind::Legacy(kind),
},
})
}
kind => Ok(PileRecord {
offset,
len,
content: PileRecordContent::Opaque {
kind: OpaqueKind::Legacy(kind),
},
}),
}
}
fn decode_record(bytes: &[u8], offset: usize) -> Result<PileRecord, ReadError> {
let corrupt = || ReadError::CorruptPile {
valid_length: offset,
};
if bytes.len() < FRAME_MAGIC_LEN {
if bytes.len() < 16 || FRAME_MAGIC.starts_with(bytes) {
return Err(corrupt());
}
} else if bytes[0..FRAME_MAGIC_LEN] == FRAME_MAGIC {
return decode_enveloped_record(bytes, offset);
}
let magic: RawId = bytes[0..16].try_into().unwrap();
if magic == MAGIC_MARKER_ENVELOPE {
return decode_enveloped_record_v1(bytes, offset);
}
match magic {
MAGIC_MARKER_BLOB => {
let (header, _) = BlobHeader::try_read_from_prefix(bytes).map_err(|_| corrupt())?;
let data_len = header.length as usize;
let pad = padding_for_blob(data_len);
let len = BLOB_HEADER_LEN
.checked_add(data_len)
.and_then(|l| l.checked_add(pad))
.ok_or_else(corrupt)?;
if bytes.len() < len {
return Err(corrupt());
}
Ok(PileRecord {
offset,
len,
content: PileRecordContent::Blob {
timestamp: header.timestamp,
hash: Inline::new(header.hash),
data_offset: offset + BLOB_HEADER_LEN,
data_len,
},
})
}
MAGIC_MARKER_BRANCH => {
let (header, _) = BranchHeader::try_read_from_prefix(bytes).map_err(|_| corrupt())?;
let branch_id = Id::new(header.branch_id).ok_or_else(corrupt)?;
Ok(PileRecord {
offset,
len: std::mem::size_of::<BranchHeader>(),
content: PileRecordContent::Branch {
branch_id,
head: Inline::<Hash<Blake3>>::new(header.hash).into(),
},
})
}
MAGIC_MARKER_BRANCH_TOMBSTONE => {
let (header, _) =
BranchTombstoneHeader::try_read_from_prefix(bytes).map_err(|_| corrupt())?;
let branch_id = Id::new(header.branch_id).ok_or_else(corrupt)?;
Ok(PileRecord {
offset,
len: std::mem::size_of::<BranchTombstoneHeader>(),
content: PileRecordContent::BranchTombstone { branch_id },
})
}
MAGIC_MARKER_BLOB_V3 => {
let (header, _) = BlobHeaderV3::try_read_from_prefix(bytes).map_err(|_| corrupt())?;
let data_len = header.length as usize;
let post_pad = block_post_pad(data_len);
let len = V3_HEADER_LEN
.checked_add(data_len)
.and_then(|l| l.checked_add(post_pad))
.ok_or_else(corrupt)?;
if bytes.len() < len {
return Err(corrupt());
}
Ok(PileRecord {
offset,
len,
content: PileRecordContent::Blob {
timestamp: header.timestamp,
hash: Inline::new(header.hash),
data_offset: offset + V3_HEADER_LEN,
data_len,
},
})
}
MAGIC_MARKER_BRANCH_V3 => {
let (header, _) = BranchHeaderV3::try_read_from_prefix(bytes).map_err(|_| corrupt())?;
let branch_id = Id::new(header.branch_id).ok_or_else(corrupt)?;
Ok(PileRecord {
offset,
len: V3_HEADER_LEN,
content: PileRecordContent::Branch {
branch_id,
head: Inline::<Hash<Blake3>>::new(header.hash).into(),
},
})
}
MAGIC_MARKER_BRANCH_TOMBSTONE_V3 => {
let (header, _) =
BranchTombstoneHeaderV3::try_read_from_prefix(bytes).map_err(|_| corrupt())?;
let branch_id = Id::new(header.branch_id).ok_or_else(corrupt)?;
Ok(PileRecord {
offset,
len: V3_HEADER_LEN,
content: PileRecordContent::BranchTombstone { branch_id },
})
}
MAGIC_MARKER_LOCAL_CELL_V3 | MAGIC_MARKER_LOCAL_CELL_TOMBSTONE_V3 => {
if bytes.len() < V3_HEADER_LEN {
return Err(corrupt());
}
Ok(PileRecord {
offset,
len: V3_HEADER_LEN,
content: PileRecordContent::Opaque {
kind: OpaqueKind::Legacy(magic),
},
})
}
MAGIC_MARKER_WEAK_PIN_V3 => {
let (header, _) =
WeakPinHeaderV3::try_read_from_prefix(bytes).map_err(|_| corrupt())?;
let request = WantRequest::blob(Inline::<Handle<UnknownBlob>>::new(header.handle));
Ok(PileRecord {
offset,
len: V3_HEADER_LEN,
content: PileRecordContent::RetiredWantAssert {
identity: request.to_bytes(),
},
})
}
MAGIC_MARKER_WEAK_UNPIN_V3 => {
let (header, _) =
WeakUnpinHeaderV3::try_read_from_prefix(bytes).map_err(|_| corrupt())?;
let request = WantRequest::blob(Inline::<Handle<UnknownBlob>>::new(header.handle));
Ok(PileRecord {
offset,
len: V3_HEADER_LEN,
content: PileRecordContent::RetiredWantRetract {
identity: request.to_bytes(),
},
})
}
MAGIC_MARKER_COLLECTION_DEFINITION_V3 => {
let (header, _) =
CollectionDefinitionHeaderV3::try_read_from_prefix(bytes).map_err(|_| corrupt())?;
if header.reserved.iter().any(|byte| *byte != 0) {
return Err(corrupt());
}
Id::new(header.scope).ok_or_else(corrupt)?;
Id::new(header.representation).ok_or_else(corrupt)?;
Id::new(header.recipe).ok_or_else(corrupt)?;
Ok(PileRecord {
offset,
len: V3_HEADER_LEN,
content: PileRecordContent::LegacyCollectionV3 {
kind: LegacyCollectionRecordKindV3::Definition,
},
})
}
MAGIC_MARKER_COLLECTION_COMMIT_V3 => {
let (header, _) =
CollectionCommitHeaderV3::try_read_from_prefix(bytes).map_err(|_| corrupt())?;
if header.reserved.iter().any(|byte| *byte != 0) {
return Err(corrupt());
}
Id::new(header.collection).ok_or_else(corrupt)?;
Ok(PileRecord {
offset,
len: V3_HEADER_LEN,
content: PileRecordContent::LegacyCollectionV3 {
kind: LegacyCollectionRecordKindV3::Commit,
},
})
}
MAGIC_MARKER_COLLECTION_MERGE_V3 => {
let (header, _) =
CollectionMergeHeaderV3::try_read_from_prefix(bytes).map_err(|_| corrupt())?;
if header.reserved.iter().any(|byte| *byte != 0) || header.high < header.low {
return Err(corrupt());
}
Id::new(header.collection).ok_or_else(corrupt)?;
Ok(PileRecord {
offset,
len: V3_HEADER_LEN,
content: PileRecordContent::LegacyCollectionV3 {
kind: LegacyCollectionRecordKindV3::Merge,
},
})
}
MAGIC_MARKER_COLLECTION_DERIVE_V3 => {
let (header, _) =
CollectionDeriveHeaderV3::try_read_from_prefix(bytes).map_err(|_| corrupt())?;
if header.reserved.iter().any(|byte| *byte != 0) {
return Err(corrupt());
}
Id::new(header.source).ok_or_else(corrupt)?;
Id::new(header.target).ok_or_else(corrupt)?;
Ok(PileRecord {
offset,
len: V3_HEADER_LEN,
content: PileRecordContent::LegacyCollectionV3 {
kind: LegacyCollectionRecordKindV3::Derive,
},
})
}
MAGIC_MARKER_COLLECTION_COMMIT_V4 => {
let (header, _) =
CollectionCommitHeaderV4::try_read_from_prefix(bytes).map_err(|_| corrupt())?;
if header.reserved.iter().any(|byte| *byte != 0) {
return Err(corrupt());
}
Ok(PileRecord {
offset,
len: V3_HEADER_LEN,
content: PileRecordContent::Collection {
record: CollectionRecord::Commit(CollectionCommit::from_parts(
Inline::new(header.collection),
Inline::new(header.data),
Inline::new(header.metadata),
Inline::<ED25519PublicKey>::new(header.public_key),
Inline::<ED25519RComponent>::new(header.signature_r),
Inline::<ED25519SComponent>::new(header.signature_s),
)),
},
})
}
MAGIC_MARKER_COLLECTION_MERGE_V4 => {
let (header, _) =
CollectionMergeHeaderV4::try_read_from_prefix(bytes).map_err(|_| corrupt())?;
if header.reserved.iter().any(|byte| *byte != 0) || header.high < header.low {
return Err(corrupt());
}
Ok(PileRecord {
offset,
len: V3_HEADER_LEN,
content: PileRecordContent::LegacyUnsignedCollectionEquation {
equation: LegacyUnsignedCollectionEquation::Merge {
collection: Inline::new(header.collection),
low: Inline::new(header.low),
high: Inline::new(header.high),
result: Inline::new(header.result),
},
},
})
}
MAGIC_MARKER_COLLECTION_DERIVE_V4 => {
let (header, _) =
CollectionDeriveHeaderV4::try_read_from_prefix(bytes).map_err(|_| corrupt())?;
if header.reserved.iter().any(|byte| *byte != 0) {
return Err(corrupt());
}
Ok(PileRecord {
offset,
len: V3_HEADER_LEN,
content: PileRecordContent::RetiredCollectionDeriveV4,
})
}
_ => Err(ReadError::UnsupportedRecord {
offset,
marker: magic,
}),
}
}
struct IndexedBlobHeader {
timestamp: u64,
data_offset: usize,
data_len: usize,
}
fn indexed_blob_header(
mmap: &Arc<MmapRaw>,
covered_len: usize,
entry: IndexEntry,
expected: &Inline<Hash<Blake3>>,
) -> IndexedBlobHeader {
assert!(
entry.record_offset < covered_len,
"blob index offset lies outside its accepted pile prefix"
);
assert!(
covered_len <= mmap.len(),
"accepted pile prefix lies outside its mapping"
);
let record_bytes = unsafe {
slice_from_raw_parts(
mmap.as_ptr().add(entry.record_offset),
covered_len - entry.record_offset,
)
.as_ref()
.unwrap()
};
let record = decode_record(record_bytes, entry.record_offset)
.expect("indexed blob record changed below the accepted pile prefix");
let PileRecordContent::Blob {
timestamp,
hash,
data_offset,
data_len,
} = record.content
else {
panic!("blob index offset no longer names a blob record");
};
assert_eq!(
hash, *expected,
"blob index key no longer matches its record header"
);
IndexedBlobHeader {
timestamp,
data_offset,
data_len,
}
}
struct IndexedBlobRecord {
bytes: Bytes,
#[cfg(test)]
payload_offset: usize,
timestamp: u64,
}
fn indexed_blob_record(
mmap: &Arc<MmapRaw>,
covered_len: usize,
entry: IndexEntry,
expected: &Inline<Hash<Blake3>>,
) -> IndexedBlobRecord {
let header = indexed_blob_header(mmap, covered_len, entry, expected);
let bytes = unsafe {
let slice = slice_from_raw_parts(mmap.as_ptr().add(header.data_offset), header.data_len)
.as_ref()
.unwrap();
Bytes::from_raw_parts(slice, mmap.clone())
};
IndexedBlobRecord {
bytes,
#[cfg(test)]
payload_offset: header.data_offset,
timestamp: header.timestamp,
}
}
#[derive(Debug)]
pub struct PileRecords {
bytes: Bytes,
offset: usize,
failed: bool,
}
impl PileRecords {
pub fn open(path: &Path) -> Result<Self, ReadError> {
let file = File::open(path)?;
let length = file.metadata()?.len();
let bytes = if length == 0 {
Bytes::empty()
} else {
unsafe { Bytes::map_file(&file)? }
};
Ok(Self {
bytes,
offset: 0,
failed: false,
})
}
pub fn bytes(&self) -> &Bytes {
&self.bytes
}
}
impl Iterator for PileRecords {
type Item = Result<PileRecord, ReadError>;
fn next(&mut self) -> Option<Self::Item> {
if self.failed || self.offset >= self.bytes.len() {
return None;
}
match decode_record(&self.bytes[self.offset..], self.offset) {
Ok(record) => {
self.offset += record.len;
Some(Ok(record))
}
Err(e) => {
self.failed = true;
Some(Err(e))
}
}
}
}
#[derive(Debug)]
enum Applied {
Blob { hash: Inline<Hash<Blake3>> },
Branch { id: Id, hash: Inline<Hash<Blake3>> },
BranchTombstone { id: Id },
Want { request: WantRequest },
RetiredWantState,
Collection { record: CollectionRecord },
CapabilityProof { id: CapabilityProofId },
RetiredCapabilityProof,
RetiredTeamState,
RetiredArtifactOffer,
LegacyCollectionEvidence,
RetiredCollectionEquation,
RetiredCollectionDeriveV4,
Opaque,
}
pub type Pile = crate::collection::covered::Covered<PileFile>;
pub type PileSnapshot = crate::collection::covered::CoveredSnapshot<PileFileSnapshot>;
impl Pile {
pub fn open(path: &Path) -> Result<Self, ReadError> {
PileFile::open(path).map(Self::new)
}
pub fn open_as(path: &Path, host: VerifyingKey) -> Result<Self, ReadError> {
PileFile::open(path).map(|file| Self::with_host(file, Some(host)))
}
}
#[derive(Debug)]
pub struct PileFile {
file: File,
record_origin: Arc<()>,
mmap: Arc<MmapRaw>,
dirty: bool,
blobs: PileBlobIndex,
branches: PATCH<16, IdentitySchema, Inline<Handle<SimpleArchive>>>,
collection_records: CollectionRecordIndex,
capability_proofs: CapabilityProofIndex,
legacy_collection_headers: LegacyCollectionHeaderIndex,
retired_equations: CollectionRecordIndex,
opaque_records: usize,
opaque_frames: Vec<(usize, usize)>,
retired_proof_frames: Arc<Vec<(usize, usize)>>,
exact_frame_digests: PATCH<32, IdentitySchema>,
wants: PATCH<WANT_REQUEST_BYTES_LEN, IdentitySchema>,
applied_length: usize,
}
fn padding_for_blob(blob_size: usize) -> usize {
(BLOB_ALIGNMENT - ((BLOB_HEADER_LEN + blob_size) % BLOB_ALIGNMENT)) % BLOB_ALIGNMENT
}
#[derive(Debug, Clone)]
pub struct PileFileSnapshot {
record_origin: Arc<()>,
mmap: Arc<MmapRaw>,
covered_len: usize,
opaque_records: usize,
blobs: PileBlobIndex,
collection_records: CollectionRecordIndex,
legacy_collection_headers: LegacyCollectionHeaderIndex,
retired_equations: CollectionRecordIndex,
capability_proofs: CapabilityProofIndex,
wants: PATCH<WANT_REQUEST_BYTES_LEN, IdentitySchema>,
retired_proof_frames: Arc<Vec<(usize, usize)>>,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct WantCutoverStatus {
pub retired_records: usize,
pub resolved_active: usize,
pub already_current: usize,
pub missing_current: usize,
}
impl PileFileSnapshot {
fn new(
record_origin: Arc<()>,
mmap: Arc<MmapRaw>,
covered_len: usize,
opaque_records: usize,
blobs: PileBlobIndex,
collection_records: CollectionRecordIndex,
legacy_collection_headers: LegacyCollectionHeaderIndex,
retired_equations: CollectionRecordIndex,
capability_proofs: CapabilityProofIndex,
wants: PATCH<WANT_REQUEST_BYTES_LEN, IdentitySchema>,
retired_proof_frames: Arc<Vec<(usize, usize)>>,
) -> Self {
Self {
record_origin,
mmap,
covered_len,
opaque_records,
blobs,
collection_records,
legacy_collection_headers,
retired_equations,
capability_proofs,
wants,
retired_proof_frames,
}
}
pub(crate) fn retired_capability_proof_frames(&self) -> impl Iterator<Item = &[u8]> + '_ {
self.retired_proof_frames
.iter()
.map(|(offset, len)| mapped_frame(self, *offset, *len))
}
pub(crate) fn retired_capability_proof_references(
&self,
) -> impl Iterator<Item = Inline<Handle<UnknownBlob>>> + '_ {
self.retired_capability_proof_frames()
.flat_map(frame_windows)
}
pub fn iter(&self) -> PileBlobStoreIter {
PileBlobStoreIter {
snapshot: self.clone(),
inner: self.blobs.clone().into_prefixes(),
}
}
pub const fn prefix_len(&self) -> usize {
self.covered_len
}
pub fn raw_records(&self) -> PileRecords {
let bytes = if self.covered_len == 0 {
Bytes::empty()
} else {
assert!(
self.covered_len <= self.mmap.len(),
"accepted pile prefix lies outside its mapping"
);
unsafe {
let slice = slice_from_raw_parts(self.mmap.as_ptr(), self.covered_len)
.as_ref()
.unwrap();
Bytes::from_raw_parts(slice, self.mmap.clone())
}
};
PileRecords {
bytes,
offset: 0,
failed: false,
}
}
pub(crate) const fn opaque_record_count(&self) -> usize {
self.opaque_records
}
pub fn legacy_unsigned_collection_equations(
&self,
) -> impl Iterator<Item = LegacyUnsignedCollectionEquation> + '_ {
self.legacy_collection_headers
.iter_ordered()
.filter_map(|header| {
match decode_record(header, 0)
.expect("indexed legacy headers were structurally validated")
.content
{
PileRecordContent::LegacyUnsignedCollectionEquation { equation } => {
Some(equation)
}
_ => None,
}
})
}
pub(crate) fn legacy_unsigned_collection_references(
&self,
) -> impl Iterator<Item = Inline<Handle<UnknownBlob>>> + '_ {
self.legacy_unsigned_collection_equations()
.flat_map(|equation| {
let kind = match equation {
LegacyUnsignedCollectionEquation::Merge { .. } => {
record_kind::KIND_COLLECTION_MERGE_UNSIGNED
}
LegacyUnsignedCollectionEquation::Derive { .. } => {
record_kind::KIND_COLLECTION_DERIVE_UNSIGNED
}
};
equation
.blob_references()
.chain(std::iter::once(Inline::new(kind)))
})
}
pub fn retired_collection_equations(&self) -> PileRetiredEquationIter {
PileRetiredEquationIter {
keys: self.retired_equations.clone().into_iter_ordered(),
mmap: self.mmap.clone(),
covered_len: self.covered_len,
run: MemberRun::new(),
}
}
pub(crate) fn retired_collection_equation_references(
&self,
) -> impl Iterator<Item = Inline<Handle<UnknownBlob>>> + '_ {
self.retired_collection_equations().flat_map(|equation| {
let kind = match equation {
RetiredCollectionEquation::MergeV8 { .. } => record_kind::KIND_COLLECTION_MERGE_V8,
RetiredCollectionEquation::DeriveV9 { .. } => {
record_kind::KIND_COLLECTION_DERIVE_V9
}
};
equation
.blob_references()
.chain(std::iter::once(Inline::new(kind)))
})
}
pub(crate) fn unvalidated_blob_info(
&self,
handle: Inline<Handle<UnknownBlob>>,
) -> Option<super::BlobInfo> {
let hash: &Inline<Hash<Blake3>> = handle.as_transmute();
let entry = first_blob_occurrence(&self.blobs, &hash.raw)?;
let header = indexed_blob_header(&self.mmap, self.covered_len, entry, hash);
Some(super::BlobInfo {
handle,
length: header.data_len as u64,
})
}
fn expanded_setting_aside_corrupt(
&self,
roots: &super::RetentionRoots,
) -> (
BTreeSet<Inline<Handle<UnknownBlob>>>,
BTreeSet<Inline<Handle<UnknownBlob>>>,
) {
let mut keep = BTreeSet::new();
let mut corrupt = BTreeSet::new();
for handle in roots.direct() {
match self.get::<Bytes, UnknownBlob>(handle) {
Err(GetBlobError::ValidationError(_)) => corrupt.insert(handle),
_ => keep.insert(handle),
};
}
let mut seen: BTreeSet<[u8; 32]> = BTreeSet::new();
let mut queue: std::collections::VecDeque<_> = roots.recursive().collect();
while let Some(handle) = queue.pop_front() {
if !seen.insert(handle.raw) {
continue;
}
let read: Vec<Bytes> = match self.get::<Bytes, UnknownBlob>(handle) {
Ok(bytes) => {
keep.insert(handle);
vec![bytes]
}
Err(GetBlobError::ValidationError(_)) => {
corrupt.insert(handle);
let hash: &Inline<Hash<Blake3>> = handle.as_transmute();
let mut occurrences = Vec::new();
let mut entry = first_blob_occurrence(&self.blobs, &hash.raw);
while let Some(at) = entry {
occurrences.push(
indexed_blob_record(&self.mmap, self.covered_len, at, hash).bytes,
);
entry = next_blob_occurrence(&self.blobs, &hash.raw, at);
}
occurrences
}
Err(_) => {
keep.insert(handle);
Vec::new()
}
};
for bytes in read {
for word in bytes.chunks_exact(32) {
let child = Inline::<Handle<UnknownBlob>>::new(
word.try_into().expect("chunks are 32 bytes"),
);
if !seen.contains(&child.raw) && self.blobs.has_prefix(&child.raw) {
queue.push_back(child);
}
}
}
}
(keep, corrupt)
}
fn validated_blob_record<E: Error>(
&self,
hash: &Inline<Hash<Blake3>>,
strategy: ValidationStrategy,
) -> Result<IndexedBlobRecord, GetBlobError<E>> {
let Some(mut candidate) = first_blob_occurrence(&self.blobs, &hash.raw) else {
return Err(GetBlobError::BlobNotFound(super::MissingBlob {
handle: Inline::new(hash.raw),
}));
};
let mut first_invalid = None;
let mut validate = |entry: IndexEntry| {
let record = indexed_blob_record(&self.mmap, self.covered_len, entry, hash);
let validation = blob_occurrence_validation(&self.blobs, &hash.raw, entry);
match validation.state(&record.bytes, hash, strategy) {
ValidationState::Validated => Some(record),
ValidationState::Invalid => {
first_invalid.get_or_insert_with(|| record.bytes.clone());
None
}
}
};
loop {
if let Some(record) = validate(candidate) {
return Ok(record);
}
let Some(next) = next_blob_occurrence(&self.blobs, &hash.raw, candidate) else {
break;
};
candidate = next;
}
Err(GetBlobError::ValidationError(first_invalid.expect(
"a present primary candidate was validated and rejected",
)))
}
}
impl BlobStoreGet for PileFileSnapshot {
type GetError<E: Error + Send + Sync + 'static> = GetBlobError<E>;
fn get<T, S>(
&self,
handle: Inline<Handle<S>>,
) -> Result<T, Self::GetError<<T as TryFromBlob<S>>::Error>>
where
S: BlobEncoding + 'static,
T: TryFromBlob<S>,
Handle<S>: InlineEncoding,
{
let hash: &Inline<Hash<Blake3>> = handle.as_transmute();
let record = self.validated_blob_record(hash, ValidationStrategy::ParallelIfLarge)?;
let blob: Blob<S> = Blob::with_handle(record.bytes, handle);
match blob.try_from_blob() {
Ok(value) => Ok(value),
Err(e) => Err(GetBlobError::ConversionError(e)),
}
}
}
impl super::BlobChildren for PileFileSnapshot {}
impl super::StoreSnapshot for PileFileSnapshot {
fn changes_since(&self, previous: &Self) -> super::StoreChanges {
let mut changes = super::StoreChanges::NONE;
if !previous.blobs.shares_root(&self.blobs) {
changes = changes.union(super::StoreChanges::BLOBS);
}
if !Arc::ptr_eq(&self.record_origin, &previous.record_origin)
|| previous.collection_records != self.collection_records
{
changes = changes.union(super::StoreChanges::COLLECTION_RECORDS);
}
if previous.capability_proofs != self.capability_proofs {
changes = changes.union(super::StoreChanges::CAPABILITY_PROOFS);
}
if previous.wants != self.wants {
changes = changes.union(super::StoreChanges::WANTS);
}
changes
}
fn changes_for(
&self,
previous: &Self,
dependencies: &super::StoreDependencies,
) -> super::StoreChanges {
let mut changes = super::StoreChanges::NONE;
let records_changed = if dependencies.all_records {
!self
.collection_records
.shares_root(&previous.collection_records)
} else {
dependencies.records.iter().any(|selector| {
let collection = match selector {
CollectionRecordSelector::ProducedMember(collection, output)
| CollectionRecordSelector::CommitMember(collection, output) => {
let mut prefix = [0; 64];
prefix[..32].copy_from_slice(&collection.raw);
prefix[32..].copy_from_slice(&output.raw);
return !self
.collection_records
.shares_prefix(&previous.collection_records, &prefix);
}
CollectionRecordSelector::Collection(collection)
| CollectionRecordSelector::MergeCollection(collection)
| CollectionRecordSelector::DeriveTarget(collection)
| CollectionRecordSelector::Foundations(collection) => collection,
};
!self
.collection_records
.shares_prefix(&previous.collection_records, &collection.raw)
})
};
if records_changed {
changes = changes.union(super::StoreChanges::COLLECTION_RECORDS);
}
let blobs_changed = if dependencies.all_blobs {
!self.blobs.shares_root(&previous.blobs)
} else {
dependencies
.blobs
.iter()
.any(|hash| !self.blobs.shares_prefix(&previous.blobs, &hash.raw))
};
if blobs_changed {
changes = changes.union(super::StoreChanges::BLOBS);
}
if dependencies.capability_proofs
&& !self
.capability_proofs
.shares_root(&previous.capability_proofs)
{
changes = changes.union(super::StoreChanges::CAPABILITY_PROOFS);
}
changes
}
}
impl super::SnapshotSource for PileFile {
type Snapshot = PileFileSnapshot;
type SnapshotError = ReadError;
fn snapshot(&mut self) -> Result<Self::Snapshot, Self::SnapshotError> {
self.refresh()?;
Ok(PileFileSnapshot::new(
self.record_origin.clone(),
self.mmap.clone(),
self.applied_length,
self.opaque_records,
self.blobs.clone(),
self.collection_records.clone(),
self.legacy_collection_headers.clone(),
self.retired_equations.clone(),
self.capability_proofs.clone(),
self.wants.clone(),
self.retired_proof_frames.clone(),
))
}
}
#[derive(Debug)]
pub enum ReadError {
IoError(std::io::Error),
CorruptPile {
valid_length: usize,
},
UnsupportedRecord {
offset: usize,
marker: RawId,
},
FileTooLarge {
length: usize,
},
}
impl std::fmt::Display for ReadError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ReadError::IoError(err) => write!(f, "IO error: {err}"),
ReadError::CorruptPile { valid_length } => {
write!(f, "Corrupt pile at byte {valid_length}")
}
ReadError::UnsupportedRecord { offset, marker } => write!(
f,
"Unsupported pile record marker {} at byte {offset}; a newer reader may be required",
hex::encode_upper(marker)
),
ReadError::FileTooLarge { length } => {
write!(f, "PileFile of length {length} exceeds supported size")
}
}
}
}
impl std::error::Error for ReadError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::IoError(err) => Some(err),
Self::CorruptPile { .. }
| Self::UnsupportedRecord { .. }
| Self::FileTooLarge { .. } => None,
}
}
}
impl From<std::io::Error> for ReadError {
fn from(err: std::io::Error) -> Self {
Self::IoError(err)
}
}
impl From<ReadError> for std::io::Error {
fn from(err: ReadError) -> Self {
match err {
ReadError::IoError(e) => e,
ReadError::CorruptPile { valid_length } => {
std::io::Error::other(format!("corrupt pile at byte {valid_length}"))
}
ReadError::UnsupportedRecord { offset, marker } => std::io::Error::other(format!(
"unsupported pile record marker {} at byte {offset}; a newer reader may be required",
hex::encode_upper(marker)
)),
ReadError::FileTooLarge { length } => {
std::io::Error::other(format!("pile length {length} exceeds supported size"))
}
}
}
}
#[derive(Debug)]
pub enum InsertError {
IoError(std::io::Error),
TimeError(std::time::SystemTimeError),
}
impl std::fmt::Display for InsertError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
InsertError::IoError(err) => write!(f, "IO error: {err}"),
InsertError::TimeError(err) => write!(f, "system time error: {err}"),
}
}
}
impl std::error::Error for InsertError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::IoError(err) => Some(err),
Self::TimeError(err) => Some(err),
}
}
}
impl From<std::io::Error> for InsertError {
fn from(err: std::io::Error) -> Self {
Self::IoError(err)
}
}
impl From<std::time::SystemTimeError> for InsertError {
fn from(err: std::time::SystemTimeError) -> Self {
Self::TimeError(err)
}
}
impl From<ReadError> for InsertError {
fn from(err: ReadError) -> Self {
Self::IoError(err.into())
}
}
pub enum PileWriteError {
IoError(std::io::Error),
}
impl std::error::Error for PileWriteError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::IoError(err) => Some(err),
}
}
}
impl std::fmt::Debug for PileWriteError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PileWriteError::IoError(err) => write!(f, "IO error: {err}"),
}
}
}
impl std::fmt::Display for PileWriteError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PileWriteError::IoError(err) => write!(f, "IO error: {err}"),
}
}
}
impl From<std::io::Error> for PileWriteError {
fn from(err: std::io::Error) -> Self {
Self::IoError(err)
}
}
impl From<ReadError> for PileWriteError {
fn from(err: ReadError) -> Self {
Self::IoError(err.into())
}
}
#[derive(Debug)]
pub enum CollectionInsertError {
Read(ReadError),
Io(std::io::Error),
UnexpectedReadback,
}
impl std::fmt::Display for CollectionInsertError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Read(error) => write!(f, "failed to refresh collection records: {error}"),
Self::Io(error) => write!(f, "failed to append collection record: {error}"),
Self::UnexpectedReadback => {
f.write_str("collection append read back an unexpected pile record")
}
}
}
}
impl Error for CollectionInsertError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Read(error) => Some(error),
Self::Io(error) => Some(error),
Self::UnexpectedReadback => None,
}
}
}
impl From<ReadError> for CollectionInsertError {
fn from(error: ReadError) -> Self {
Self::Read(error)
}
}
impl From<std::io::Error> for CollectionInsertError {
fn from(error: std::io::Error) -> Self {
Self::Io(error)
}
}
#[derive(Debug)]
pub enum CapabilityProofInsertError {
Invalid(crate::capability::CapabilityProofError),
Read(ReadError),
Io(std::io::Error),
IdCollision { id: CapabilityProofId },
UnexpectedReadback,
}
impl std::fmt::Display for CapabilityProofInsertError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Invalid(error) => write!(f, "invalid capability proof: {error}"),
Self::Read(error) => write!(f, "failed to refresh capability proofs: {error}"),
Self::Io(error) => write!(f, "failed to append capability proof: {error}"),
Self::IdCollision { id } => write!(
f,
"capability proof id {} names different bytes",
hex::encode_upper(id.raw)
),
Self::UnexpectedReadback => {
f.write_str("capability proof append read back an unexpected pile record")
}
}
}
}
impl Error for CapabilityProofInsertError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Invalid(error) => Some(error),
Self::Read(error) => Some(error),
Self::Io(error) => Some(error),
Self::IdCollision { .. } | Self::UnexpectedReadback => None,
}
}
}
impl From<ReadError> for CapabilityProofInsertError {
fn from(error: ReadError) -> Self {
Self::Read(error)
}
}
impl From<std::io::Error> for CapabilityProofInsertError {
fn from(error: std::io::Error) -> Self {
Self::Io(error)
}
}
#[derive(Debug)]
pub enum GetBlobError<E: Error> {
BlobNotFound(super::MissingBlob),
ValidationError(Bytes),
ConversionError(E),
}
impl<E: Error> std::fmt::Display for GetBlobError<E> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
GetBlobError::BlobNotFound(error) => error.fmt(f),
GetBlobError::ConversionError(err) => write!(f, "Conversion error: {err}"),
GetBlobError::ValidationError(_) => write!(f, "Validation error"),
}
}
}
impl<E: Error + 'static> std::error::Error for GetBlobError<E> {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::BlobNotFound(error) => Some(error),
Self::ConversionError(error) => Some(error),
Self::ValidationError(_) => None,
}
}
}
#[derive(Debug)]
pub enum FlushError {
IoError(std::io::Error),
}
impl From<std::io::Error> for FlushError {
fn from(err: std::io::Error) -> Self {
Self::IoError(err)
}
}
impl std::fmt::Display for FlushError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FlushError::IoError(err) => write!(f, "IO error: {err}"),
}
}
}
impl std::error::Error for FlushError {}
impl PileFile {
pub fn backing_file_metadata(&self) -> std::io::Result<std::fs::Metadata> {
self.file.metadata()
}
pub fn open(path: &Path) -> Result<Self, ReadError> {
Self::from_file(OpenOptions::new().read(true).append(true).open(path)?)
}
pub fn open_read_only(path: &Path) -> Result<Self, ReadError> {
Self::from_file(OpenOptions::new().read(true).open(path)?)
}
fn from_file(file: File) -> Result<Self, ReadError> {
let length_u64 = file.metadata()?.len();
let length = usize::try_from(length_u64)
.map_err(|_| ReadError::FileTooLarge { length: usize::MAX })?;
let page_size = page_size::get();
let base_size = page_size * 1024;
let mapped_size = base_size.max(
length
.checked_next_power_of_two()
.ok_or(ReadError::FileTooLarge { length })?,
);
let mmap = MmapOptions::new()
.len(mapped_size)
.map_raw_read_only(&file)?;
let mmap = Arc::new(mmap);
Ok(Self {
file,
record_origin: Arc::new(()),
mmap,
dirty: false,
blobs: PileBlobIndex::new(),
branches: PATCH::<16, IdentitySchema, Inline<Handle<SimpleArchive>>>::new(),
collection_records: CollectionRecordIndex::new(),
capability_proofs: CapabilityProofIndex::new(),
legacy_collection_headers: LegacyCollectionHeaderIndex::new(),
retired_equations: CollectionRecordIndex::new(),
opaque_records: 0,
opaque_frames: Vec::new(),
retired_proof_frames: Arc::new(Vec::new()),
exact_frame_digests: PATCH::<32, IdentitySchema>::new(),
wants: PATCH::<WANT_REQUEST_BYTES_LEN, IdentitySchema>::new(),
applied_length: 0,
})
}
fn ensure_mapped(&mut self, file_len: usize) -> Result<(), ReadError> {
if file_len <= self.mmap.len() {
return Ok(());
}
let mapped_size = file_len
.checked_next_power_of_two()
.ok_or(ReadError::FileTooLarge { length: file_len })?;
self.mmap = Arc::new(
MmapOptions::new()
.len(mapped_size)
.map_raw_read_only(&self.file)?,
);
Ok(())
}
pub fn refresh(&mut self) -> Result<(), ReadError> {
self.file.lock_shared()?;
let res = self.refresh_shared_locked();
let unlock_res = self.file.unlock();
res?;
unlock_res?;
Ok(())
}
fn apply_next(&mut self) -> Result<Option<Applied>, ReadError> {
let file_len = self.observed_file_len()?;
self.ensure_mapped(file_len)?;
self.apply_next_bounded(file_len)
}
fn observed_file_len(&self) -> Result<usize, ReadError> {
usize::try_from(self.file.metadata()?.len())
.map_err(|_| ReadError::FileTooLarge { length: usize::MAX })
}
fn apply_next_bounded(&mut self, file_len: usize) -> Result<Option<Applied>, ReadError> {
if file_len < self.applied_length {
std::process::abort();
}
if file_len == self.applied_length {
return Ok(None);
}
debug_assert!(file_len <= self.mmap.len());
let start_offset = self.applied_length;
let slice = unsafe {
slice_from_raw_parts(
self.mmap.as_ptr().add(start_offset),
file_len - start_offset,
)
.as_ref()
.unwrap()
};
let record = decode_record(slice, start_offset)?;
let next_applied_length = start_offset + record.len;
let legacy_collection_header = matches!(
record.content,
PileRecordContent::LegacyCollectionV3 { .. }
| PileRecordContent::LegacyUnsignedCollectionEquation { .. }
)
.then(|| {
let mut header = [0u8; V3_HEADER_LEN];
header.copy_from_slice(&slice[..V3_HEADER_LEN]);
header
});
let applied = match record.content {
PileRecordContent::Blob { hash, .. } => {
let candidate = IndexEntry::new(start_offset);
let key = blob_occurrence_key(&hash.raw, candidate);
self.blobs
.insert(&Entry::with_value(&key, CachedValidation::default()));
Applied::Blob { hash }
}
PileRecordContent::Branch { branch_id, head } => {
let entry = Entry::with_value(&branch_id.into(), head);
self.branches.replace(&entry);
Applied::Branch {
id: branch_id,
hash: head.into(),
}
}
PileRecordContent::BranchTombstone { branch_id } => {
self.branches.remove(&branch_id.into());
Applied::BranchTombstone { id: branch_id }
}
PileRecordContent::Want { request } => {
self.wants.insert(&Entry::new(&request.to_bytes()));
Applied::Want { request }
}
PileRecordContent::RetiredWantAssert { .. }
| PileRecordContent::RetiredWantRetract { .. } => Applied::RetiredWantState,
PileRecordContent::Collection { record } => {
self.collection_records
.insert(&Entry::new(&collection_record_key(&record, start_offset)));
Applied::Collection { record }
}
PileRecordContent::CapabilityProof {
id,
data_offset,
data_len,
} => {
let bytes = unsafe {
let body = slice_from_raw_parts(self.mmap.as_ptr().add(data_offset), data_len)
.as_ref()
.unwrap();
Bytes::from_raw_parts(body, self.mmap.clone())
};
let candidate = CapabilityProof::from_validated_bytes(bytes);
if let Some(existing) = self.capability_proofs.get(&id.raw) {
if existing != &candidate {
return Err(ReadError::CorruptPile {
valid_length: start_offset,
});
}
} else {
self.capability_proofs
.insert(&Entry::with_value(&id.raw, candidate));
}
Applied::CapabilityProof { id }
}
PileRecordContent::RetiredCapabilityProof => {
let frame_len = next_applied_length - start_offset;
Arc::make_mut(&mut self.retired_proof_frames).push((start_offset, frame_len));
self.exact_frame_digests
.insert(&Entry::new(blake3::hash(&slice[..frame_len]).as_bytes()));
Applied::RetiredCapabilityProof
}
PileRecordContent::RetiredPeerEvidenceV1 | PileRecordContent::RetiredStoreScopeV1 => {
Applied::RetiredTeamState
}
PileRecordContent::RetiredArtifactOfferV1 => Applied::RetiredArtifactOffer,
PileRecordContent::LegacyCollectionV3 { .. }
| PileRecordContent::LegacyUnsignedCollectionEquation { .. } => {
let header = legacy_collection_header
.expect("legacy collection record must retain its physical header");
self.legacy_collection_headers.insert(&Entry::new(&header));
Applied::LegacyCollectionEvidence
}
PileRecordContent::RetiredCollectionEquation { equation } => {
self.retired_equations
.insert(&Entry::new(&retired_equation_key(&equation, start_offset)));
Applied::RetiredCollectionEquation
}
PileRecordContent::RetiredCollectionDeriveV4 => Applied::RetiredCollectionDeriveV4,
PileRecordContent::Opaque { .. } => {
self.opaque_records = self
.opaque_records
.checked_add(1)
.expect("opaque pile-record count overflow");
let frame_len = next_applied_length - start_offset;
self.opaque_frames.push((start_offset, frame_len));
self.exact_frame_digests
.insert(&Entry::new(blake3::hash(&slice[..frame_len]).as_bytes()));
Applied::Opaque
}
};
self.applied_length = next_applied_length;
Ok(Some(applied))
}
fn refresh_shared_locked(&mut self) -> Result<(), ReadError> {
match self.refresh_locked() {
Err(ReadError::CorruptPile { .. } | ReadError::UnsupportedRecord { .. }) => {
#[cfg(test)]
append_visibility_tests::before_exclusive_recheck();
self.file.unlock()?;
self.file.lock()?;
self.refresh_locked()
}
result => result,
}
}
fn refresh_locked(&mut self) -> Result<(), ReadError> {
let file_len = self.observed_file_len()?;
if file_len < self.applied_length {
std::process::abort();
}
self.ensure_mapped(file_len)?;
loop {
match self.apply_next_bounded(file_len) {
Ok(Some(_)) => {}
Ok(None) => return Ok(()),
Err(error) => return Err(error),
}
}
}
pub fn amputate(&mut self) -> Result<(), ReadError> {
match self.refresh() {
Ok(()) => Ok(()),
Err(ReadError::CorruptPile { .. }) => {
self.amputate_exclusive(None).map(|_truncated| ())
}
Err(e) => Err(e),
}
}
pub fn amputate_at(&mut self, expected_valid_length: usize) -> Result<bool, ReadError> {
self.amputate_exclusive(Some(expected_valid_length))
}
fn amputate_exclusive(
&mut self,
expected_valid_length: Option<usize>,
) -> Result<bool, ReadError> {
self.file.lock()?;
let result = (|| match self.refresh_locked() {
Ok(()) => Ok(false),
Err(ReadError::CorruptPile { valid_length })
if expected_valid_length.is_some_and(|expected| expected != valid_length) =>
{
Err(ReadError::CorruptPile { valid_length })
}
Err(ReadError::CorruptPile { valid_length }) => {
self.file.set_len(valid_length as u64)?;
self.dirty = true;
self.flush().map_err(|err| match err {
FlushError::IoError(err) => ReadError::IoError(err),
})?;
self.applied_length = valid_length;
Ok(true)
}
Err(error) => Err(error),
})();
let unlock = self.file.unlock().map_err(ReadError::from);
match (result, unlock) {
(Ok(truncated), Ok(())) => Ok(truncated),
(Err(error), _) => Err(error),
(Ok(_), Err(error)) => Err(error),
}
}
pub fn flush(&mut self) -> Result<(), FlushError> {
self.file.sync_all()?;
self.dirty = false;
Ok(())
}
fn flush_if_dirty(&mut self) -> Result<(), FlushError> {
if self.dirty {
self.flush()?;
}
Ok(())
}
pub fn close(mut self) -> Result<(), FlushError> {
let res = self.flush_if_dirty();
let mut this = std::mem::ManuallyDrop::new(self);
unsafe {
std::ptr::drop_in_place(&mut this.record_origin);
std::ptr::drop_in_place(&mut this.mmap);
std::ptr::drop_in_place(&mut this.file);
std::ptr::drop_in_place(&mut this.blobs);
std::ptr::drop_in_place(&mut this.branches);
std::ptr::drop_in_place(&mut this.collection_records);
std::ptr::drop_in_place(&mut this.capability_proofs);
std::ptr::drop_in_place(&mut this.legacy_collection_headers);
std::ptr::drop_in_place(&mut this.retired_equations);
std::ptr::drop_in_place(&mut this.opaque_frames);
std::ptr::drop_in_place(&mut this.retired_proof_frames);
std::ptr::drop_in_place(&mut this.exact_frame_digests);
std::ptr::drop_in_place(&mut this.wants);
}
res
}
}
impl Drop for PileFile {
fn drop(&mut self) {
eprintln!("warning: PileFile dropped without calling close(); data may not be persisted");
}
}
impl crate::repo::StorageClose for PileFile {
type Error = FlushError;
fn close(self) -> Result<(), Self::Error> {
PileFile::close(self)
}
}
impl crate::repo::StorageFlush for PileFile {
type Error = FlushError;
fn flush(&mut self) -> Result<(), Self::Error> {
PileFile::flush(self)
}
}
use super::BlobInfo;
use super::BlobStoreGet;
use super::BlobStoreList;
use super::BlobStorePut;
use super::WantStore;
pub struct PileBlobStoreIter {
snapshot: PileFileSnapshot,
inner: crate::patch::PATCHIntoPrefixSetIterator<
40,
32,
blob_occurrence_key::Schema,
CachedValidation,
XorSip128,
>,
}
impl Iterator for PileBlobStoreIter {
type Item = Result<(Inline<Handle<UnknownBlob>>, Blob<UnknownBlob>), GetBlobError<Infallible>>;
fn next(&mut self) -> Option<Self::Item> {
let key = self.inner.next()?;
let hash = Inline::<Hash<Blake3>>::new(key);
match self
.snapshot
.validated_blob_record(&hash, ValidationStrategy::ParallelIfLarge)
{
Ok(record) => {
let handle: Inline<Handle<UnknownBlob>> = hash.into();
let blob = Blob::with_handle(record.bytes, handle);
Some(Ok((handle, blob)))
}
Err(error) => Some(Err(error)),
}
}
}
pub struct PileBlobStoreListIter {
snapshot: PileFileSnapshot,
inner: crate::patch::PATCHIntoPrefixSetIterator<
40,
32,
blob_occurrence_key::Schema,
CachedValidation,
XorSip128,
>,
old: Option<PileBlobIndex>,
}
impl Iterator for PileBlobStoreListIter {
type Item = Result<BlobInfo, GetBlobError<Infallible>>;
fn next(&mut self) -> Option<Self::Item> {
loop {
let key = self.inner.next()?;
if self.old.as_ref().is_some_and(|old| old.has_prefix(&key)) {
continue;
}
let hash = Inline::<Hash<Blake3>>::new(key);
let handle = hash.into();
return Some(Ok(self.snapshot.unvalidated_blob_info(handle).expect(
"key from PATCH iterator must resolve in the same snapshot",
)));
}
}
}
impl BlobStoreList for PileFileSnapshot {
type Err = GetBlobError<Infallible>;
type Iter<'a> = PileBlobStoreListIter;
fn blobs(&self) -> Self::Iter<'_> {
PileBlobStoreListIter {
snapshot: self.clone(),
inner: self.blobs.clone().into_prefixes(),
old: None,
}
}
fn contains_blob<S>(&self, handle: Inline<Handle<S>>) -> Result<bool, Self::Err>
where
S: BlobEncoding + 'static,
Handle<S>: InlineEncoding,
{
Ok(self.blobs.has_prefix(&handle.raw))
}
fn blob_info<S>(&self, handle: Inline<Handle<S>>) -> Result<Option<super::BlobInfo>, Self::Err>
where
S: BlobEncoding + 'static,
Handle<S>: InlineEncoding,
{
Ok(self.unvalidated_blob_info(*handle.as_transmute()))
}
fn blobs_diff(&self, old: &Self) -> Self::Iter<'_> {
PileBlobStoreListIter {
snapshot: self.clone(),
inner: self.blobs.difference(&old.blobs).into_prefixes(),
old: Some(old.blobs.clone()),
}
}
}
pub struct PileCollectionRecordIter {
keys: crate::patch::PATCHIntoOrderedIterator<72, collection_record_key::Schema, (), XorSip128>,
mmap: Arc<MmapRaw>,
covered_len: usize,
run: MemberRun,
}
pub struct PileRetiredEquationIter {
keys: crate::patch::PATCHIntoOrderedIterator<72, collection_record_key::Schema, (), XorSip128>,
mmap: Arc<MmapRaw>,
covered_len: usize,
run: MemberRun<RetiredCollectionEquation>,
}
impl Iterator for PileRetiredEquationIter {
type Item = RetiredCollectionEquation;
fn next(&mut self) -> Option<Self::Item> {
loop {
let key = self.keys.next()?;
let offset =
collection_record_offset(key[64..].try_into().expect("key tail is the offset"));
let equation = retired_equation_at(&self.mmap, self.covered_len, offset);
if self.run.first(&key, equation) {
return Some(equation);
}
}
}
}
pub struct PileCapabilityProofIter {
keys: crate::patch::PATCHIntoOrderedIterator<32, IdentitySchema, CapabilityProof, XorSip128>,
lookup: CapabilityProofIndex,
}
impl Iterator for PileCapabilityProofIter {
type Item = Result<CapabilityProof, ReadError>;
fn next(&mut self) -> Option<Self::Item> {
let key = self.keys.next()?;
let proof = self
.lookup
.get(&key)
.expect("proof key from PATCH snapshot must retain its value");
Some(Ok(proof.clone()))
}
}
impl Iterator for PileCollectionRecordIter {
type Item = Result<CollectionRecord, ReadError>;
fn next(&mut self) -> Option<Self::Item> {
loop {
let key = self.keys.next()?;
let offset =
collection_record_offset(key[64..].try_into().expect("key tail is the offset"));
let record = collection_record_at(&self.mmap, self.covered_len, offset);
if self.run.first(&key, record) {
return Some(Ok(record));
}
}
}
}
impl PileFile {
pub fn publish_record_kind_descriptions(&mut self) -> Result<usize, InsertError> {
let mut stored = 0usize;
for blob in record_kind::description_blobs() {
self.put::<UnknownBlob, _>(blob)?;
stored += 1;
}
Ok(stored)
}
pub fn opaque_record_count(&mut self) -> Result<usize, ReadError> {
self.refresh()?;
Ok(self.opaque_records)
}
pub(crate) fn preserve_legacy_collection_headers_into(
&mut self,
destination: &mut PileFile,
) -> Result<(), CollectionInsertError> {
self.refresh()?;
let headers = self.legacy_collection_headers.clone();
for header in headers.into_iter_ordered() {
destination.preserve_legacy_collection_header(header)?;
}
Ok(())
}
fn preserve_legacy_collection_header(
&mut self,
header: [u8; V3_HEADER_LEN],
) -> Result<(), CollectionInsertError> {
debug_assert!(matches!(
decode_record(&header, 0),
Ok(PileRecord {
content: PileRecordContent::LegacyCollectionV3 { .. }
| PileRecordContent::LegacyUnsignedCollectionEquation { .. },
..
})
));
self.file.lock()?;
let result = (|| {
self.refresh_locked()?;
if self.legacy_collection_headers.get(&header).is_some() {
return Ok(());
}
self.dirty = true;
let written = self.file.write(&header)?;
if written != V3_HEADER_LEN {
return Err(CollectionInsertError::Io(std::io::Error::new(
std::io::ErrorKind::WriteZero,
"failed to write complete legacy collection record",
)));
}
match self.apply_next()? {
Some(Applied::LegacyCollectionEvidence) => Ok(()),
Some(_) | None => Err(CollectionInsertError::UnexpectedReadback),
}
})();
let unlock = self.file.unlock();
result?;
unlock?;
Ok(())
}
pub(crate) fn preserve_retired_capability_proofs_into(
&mut self,
destination: &mut PileFile,
) -> Result<(), CollectionInsertError> {
let reader = self.snapshot()?;
for frame in reader.retired_capability_proof_frames() {
destination.preserve_exact_frame(frame)?;
}
Ok(())
}
pub(crate) fn preserve_retired_collection_equations_into(
&mut self,
destination: &mut PileFile,
) -> Result<(), CollectionInsertError> {
let reader = self.snapshot()?;
for equation in reader.retired_collection_equations() {
destination.preserve_retired_equation(&equation)?;
}
Ok(())
}
fn preserve_retired_equation(
&mut self,
equation: &RetiredCollectionEquation,
) -> Result<(), CollectionInsertError> {
let frame = retired_equation_frame(equation);
self.file.lock()?;
let result = (|| {
self.refresh_locked()?;
if retired_equation_indexed(
&self.retired_equations,
&self.mmap,
self.applied_length,
equation,
) {
return Ok(());
}
self.dirty = true;
let written = self.file.write(&frame)?;
if written != frame.len() {
return Err(CollectionInsertError::Io(std::io::Error::new(
std::io::ErrorKind::WriteZero,
"failed to write complete retired collection equation",
)));
}
match self.apply_next()? {
Some(Applied::RetiredCollectionEquation) => Ok(()),
Some(_) | None => Err(CollectionInsertError::UnexpectedReadback),
}
})();
let unlock = self.file.unlock();
result?;
unlock?;
Ok(())
}
fn preserve_exact_frame(&mut self, frame: &[u8]) -> Result<(), CollectionInsertError> {
let retired_proof = matches!(
decode_record(frame, 0),
Ok(PileRecord {
content: PileRecordContent::RetiredCapabilityProof,
..
})
);
debug_assert!(
retired_proof
|| matches!(
decode_record(frame, 0),
Ok(PileRecord {
content: PileRecordContent::Opaque { .. },
..
})
)
);
self.file.lock()?;
let result = (|| {
self.refresh_locked()?;
if self
.exact_frame_digests
.get(blake3::hash(frame).as_bytes())
.is_some()
{
return Ok(());
}
self.dirty = true;
self.file.write_all(frame)?;
match (self.apply_next()?, retired_proof) {
(Some(Applied::Opaque), false) | (Some(Applied::RetiredCapabilityProof), true) => {
Ok(())
}
_ => Err(CollectionInsertError::UnexpectedReadback),
}
})();
let unlock = self.file.unlock();
result?;
unlock?;
Ok(())
}
}
impl crate::collection::covered::RecordDelta for PileFileSnapshot {
fn for_each_record_since(&self, since: Option<&Self>, each: &mut dyn FnMut(&CollectionRecord)) {
let fresh = match since {
Some(since) => self
.collection_records
.difference(&since.collection_records),
None => self.collection_records.clone(),
};
for key in fresh.iter_ordered() {
each(&self.record_at(&key[64..].try_into().expect("key tail is the offset")));
}
}
}
impl PileFileSnapshot {
fn select_record_index(
&self,
index: &CollectionRecordIndex,
selectors: &BTreeSet<CollectionRecordSelector>,
) -> Result<Vec<CollectionRecord>, ReadError> {
if selectors.is_empty() {
return Ok(Vec::new());
}
let mut keys = Vec::new();
let mut frames_under = |prefix: [u8; 64]| {
index.infixes(&prefix, |offset: &[u8; 8]| {
let mut key = [0; 72];
key[..64].copy_from_slice(&prefix);
key[64..].copy_from_slice(offset);
keys.push(key);
});
};
for selector in selectors {
let collection = match selector {
CollectionRecordSelector::ProducedMember(collection, output)
| CollectionRecordSelector::CommitMember(collection, output) => {
let mut prefix = [0; 64];
prefix[..32].copy_from_slice(&collection.raw);
prefix[32..].copy_from_slice(&output.raw);
frames_under(prefix);
continue;
}
CollectionRecordSelector::Collection(collection)
| CollectionRecordSelector::MergeCollection(collection)
| CollectionRecordSelector::DeriveTarget(collection)
| CollectionRecordSelector::Foundations(collection) => collection,
};
let mut members = Vec::new();
index.infixes(&collection.raw, |member: &[u8; 32]| members.push(*member));
for member in members {
let mut prefix = [0; 64];
prefix[..32].copy_from_slice(&collection.raw);
prefix[32..].copy_from_slice(&member);
frames_under(prefix);
}
}
keys.sort_unstable();
keys.dedup();
let mut run = MemberRun::new();
Ok(keys
.iter()
.filter_map(|key| {
let record = self.record_at(key[64..].try_into().expect("key tail is the offset"));
(selectors_match_record(selectors, record) && run.first(key, record))
.then_some(record)
})
.collect())
}
fn record_at(&self, offset: &[u8; 8]) -> CollectionRecord {
collection_record_at(
&self.mmap,
self.covered_len,
collection_record_offset(offset),
)
}
}
impl CollectionRead for PileFileSnapshot {
type RecordsError = ReadError;
type RecordIter<'a> = PileCollectionRecordIter;
fn records<'a>(&'a self) -> Result<Self::RecordIter<'a>, Self::RecordsError> {
Ok(PileCollectionRecordIter {
keys: self.collection_records.clone().into_iter_ordered(),
mmap: self.mmap.clone(),
covered_len: self.covered_len,
run: MemberRun::new(),
})
}
fn collections(&self) -> Result<Vec<CollectionHandle>, Self::RecordsError> {
let mut collections = Vec::new();
self.collection_records
.infixes(&[], |collection: &[u8; 32]| {
collections.push(Inline::new(*collection));
});
Ok(collections)
}
fn select_records(
&self,
selectors: &BTreeSet<CollectionRecordSelector>,
) -> Result<Vec<CollectionRecord>, Self::RecordsError> {
self.select_record_index(&self.collection_records, selectors)
}
fn select_record_changes(
&self,
previous: &Self,
selectors: &BTreeSet<CollectionRecordSelector>,
) -> Result<(Vec<CollectionRecord>, Vec<CollectionRecord>), Self::RecordsError> {
if selectors.is_empty() {
return Ok((Vec::new(), Vec::new()));
}
if !Arc::ptr_eq(&self.record_origin, &previous.record_origin) {
return crate::collection::store::selected_record_changes(self, previous, selectors);
}
let added = self
.collection_records
.difference(&previous.collection_records);
let removed = previous
.collection_records
.difference(&self.collection_records);
let added: BTreeSet<_> = self
.select_record_index(&added, selectors)?
.into_iter()
.filter(|record| {
!collection_record_indexed(
&previous.collection_records,
&previous.mmap,
previous.covered_len,
record,
)
})
.collect();
let removed: BTreeSet<_> = previous
.select_record_index(&removed, selectors)?
.into_iter()
.filter(|record| {
!collection_record_indexed(
&self.collection_records,
&self.mmap,
self.covered_len,
record,
)
})
.collect();
Ok((added.into_iter().collect(), removed.into_iter().collect()))
}
}
impl CollectionStore for PileFile {
type InsertError = CollectionInsertError;
fn insert(&mut self, record: CollectionRecord) -> Result<(), Self::InsertError> {
let header = collection_record_header(&record);
self.file.lock()?;
let result = (|| {
self.refresh_locked()?;
if collection_record_indexed(
&self.collection_records,
&self.mmap,
self.applied_length,
&record,
) {
return Ok(());
}
self.dirty = true;
let written = self.file.write(&header)?;
if written != header.len() {
return Err(CollectionInsertError::Io(std::io::Error::new(
std::io::ErrorKind::WriteZero,
"failed to write complete collection record",
)));
}
match self.apply_next()? {
Some(Applied::Collection { record: applied }) if applied == record => Ok(()),
Some(_) | None => Err(CollectionInsertError::UnexpectedReadback),
}
})();
let unlock = self.file.unlock();
result?;
unlock?;
Ok(())
}
}
impl CapabilityProofRead for PileFileSnapshot {
type ProofsError = ReadError;
type ProofIter<'a> = PileCapabilityProofIter;
fn proofs<'a>(&'a self) -> Result<Self::ProofIter<'a>, Self::ProofsError> {
let keys = self.capability_proofs.clone().into_iter_ordered();
Ok(PileCapabilityProofIter {
keys,
lookup: self.capability_proofs.clone(),
})
}
fn proof(&self, id: CapabilityProofId) -> Result<Option<CapabilityProof>, Self::ProofsError> {
Ok(self.capability_proofs.get(&id.raw).cloned())
}
}
impl CapabilityProofStore for PileFile {
type InsertError = CapabilityProofInsertError;
fn insert_proof(&mut self, proof: CapabilityProof) -> Result<(), Self::InsertError> {
proof
.verify_signatures()
.map_err(CapabilityProofInsertError::Invalid)?;
let bytes = proof.as_bytes();
let data_len = bytes.len();
let prefix_len = FRAME_KIND_OFFSET;
let span_blocks =
envelope_blocks_for_prefixed_payload(prefix_len, data_len).ok_or_else(|| {
CapabilityProofInsertError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"capability proof is too large for the u32 pile-record span",
))
})?;
let padding = prefixed_payload_post_pad(prefix_len, data_len).ok_or_else(|| {
CapabilityProofInsertError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"capability proof pile-record size overflows usize",
))
})?;
let id = proof.id();
self.file.lock()?;
let result = (|| {
self.refresh_locked()?;
if let Some(existing) = self.capability_proofs.get(&id.raw) {
return if existing.as_bytes() == bytes {
Ok(())
} else {
Err(CapabilityProofInsertError::IdCollision { id })
};
}
let header = RecordFrame {
magic: FRAME_MAGIC,
span_blocks: span_blocks.to_le_bytes(),
record_kind: CAPABILITY_PROOF_MAGIC,
};
let zero_buf = [0u8; ENVELOPE_BLOCK_LEN];
self.dirty = true;
self.file.write_all(header.as_bytes())?;
self.file
.write_all(&bytes[CAPABILITY_PROOF_MAGIC.len()..])?;
if padding > 0 {
self.file.write_all(&zero_buf[..padding])?;
}
match self.apply_next()? {
Some(Applied::CapabilityProof { id: applied }) if applied == id => Ok(()),
Some(_) | None => Err(CapabilityProofInsertError::UnexpectedReadback),
}
})();
let unlock = self.file.unlock();
result?;
unlock?;
Ok(())
}
}
impl PileFile {
pub(crate) fn physical_rewrite_guard(&mut self) -> Result<usize, ReadError> {
self.refresh().map(|()| self.opaque_records)
}
}
impl BlobStorePut for PileFile {
type PutError = InsertError;
fn put<S, T>(&mut self, item: T) -> Result<Inline<Handle<S>>, Self::PutError>
where
S: BlobEncoding + 'static,
T: IntoBlob<S>,
Handle<S>: InlineEncoding,
{
self.put_impl(item)
}
}
impl PileFile {
fn put_impl<S, T>(&mut self, item: T) -> Result<Inline<Handle<S>>, InsertError>
where
S: BlobEncoding + 'static,
T: IntoBlob<S>,
Handle<S>: InlineEncoding,
{
let blob = IntoBlob::to_blob(item);
let blob_size = blob.bytes.len();
let padding = block_post_pad(blob_size);
let span_blocks = envelope_blocks_for_payload(blob_size).ok_or_else(|| {
InsertError::IoError(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"blob is too large for the u32 pile-record span",
))
})?;
let record_size = ENVELOPE_HEADER_LEN
.checked_add(blob_size)
.and_then(|size| size.checked_add(padding))
.ok_or_else(|| {
InsertError::IoError(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"blob pile-record size overflows usize",
))
})?;
let use_atomic = record_size <= ATOMIC_WRITE_LIMIT;
if use_atomic {
self.file.lock_shared()?;
} else {
self.file.lock()?;
}
let res = (|| {
if use_atomic {
self.refresh_shared_locked().map_err(InsertError::from)?;
} else {
self.refresh_locked().map_err(InsertError::from)?;
}
let handle: Inline<Handle<S>> = blob.get_handle();
let hash: Inline<Hash<Blake3>> = handle.into();
if let Some(mut entry) = first_blob_occurrence(&self.blobs, &hash.raw) {
loop {
let record = indexed_blob_record(&self.mmap, self.applied_length, entry, &hash);
let validation = blob_occurrence_validation(&self.blobs, &hash.raw, entry);
if matches!(
validation.state(&record.bytes, &hash, ValidationStrategy::Serial),
ValidationState::Validated
) {
return Ok(handle.transmute());
}
let Some(next) = next_blob_occurrence(&self.blobs, &hash.raw, entry) else {
break;
};
entry = next;
}
}
let stamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() as u64;
let header = BlobRecordHeader::new(span_blocks, stamp, blob_size as u64, hash);
let actual_record_size = record_size;
let zero_buf = [0u8; ENVELOPE_BLOCK_LEN];
self.dirty = true;
if use_atomic {
let bufs = [
IoSlice::new(header.as_bytes()),
IoSlice::new(blob.bytes.as_ref()),
IoSlice::new(&zero_buf[..padding]),
];
let written = self.file.write_vectored(&bufs)?;
if written != actual_record_size {
return Err(InsertError::IoError(std::io::Error::new(
std::io::ErrorKind::WriteZero,
"failed to write blob record",
)));
}
} else {
self.file.write_all(header.as_bytes())?;
self.file.write_all(blob.bytes.as_ref())?;
if padding > 0 {
self.file.write_all(&zero_buf[..padding])?;
}
}
#[cfg(test)]
append_visibility_tests::before_blob_readback();
loop {
match self.apply_next().map_err(InsertError::from)? {
Some(Applied::Blob { hash: h }) => {
if h == hash {
break;
}
}
Some(Applied::Branch { .. }) => {}
Some(Applied::BranchTombstone { .. }) => {}
Some(Applied::Want { .. }) => {}
Some(Applied::RetiredWantState) => {}
Some(Applied::Collection { .. }) => {}
Some(Applied::CapabilityProof { .. }) => {}
Some(Applied::RetiredCapabilityProof) => {}
Some(Applied::RetiredTeamState) => {}
Some(Applied::RetiredArtifactOffer) => {}
Some(Applied::LegacyCollectionEvidence) => {}
Some(Applied::RetiredCollectionEquation) => {}
Some(Applied::RetiredCollectionDeriveV4) => {}
Some(Applied::Opaque) => {}
None => {
return Err(InsertError::IoError(std::io::Error::other(
"blob missing after write",
)));
}
}
}
Ok(handle.transmute())
})();
let unlock_res = self.file.unlock();
let handle = res?;
unlock_res?;
Ok(handle)
}
}
impl PileFile {
fn append_legacy_pin_record(
&mut self,
id: Id,
head: Option<Inline<Handle<SimpleArchive>>>,
) -> Result<(), PileWriteError> {
self.file.lock()?;
let result = (|| {
self.refresh_locked().map_err(PileWriteError::from)?;
self.dirty = true;
let written = match head {
Some(head) => self
.file
.write(PinHeadRecordHeader::new(id, head).as_bytes()),
None => self
.file
.write(PinTombstoneRecordHeader::new(id).as_bytes()),
}
.map_err(PileWriteError::IoError)?;
if written != ENVELOPE_HEADER_LEN {
return Err(PileWriteError::IoError(std::io::Error::new(
std::io::ErrorKind::WriteZero,
"failed to write legacy pin header",
)));
}
match self.apply_next().map_err(PileWriteError::from)? {
Some(Applied::Branch {
id: actual_id,
hash,
}) if matches!(head, Some(expected) if actual_id == id && hash == expected.into()) => {
Ok(())
}
Some(Applied::BranchTombstone { id: actual_id })
if head.is_none() && actual_id == id =>
{
Ok(())
}
Some(_) => Err(PileWriteError::IoError(std::io::Error::other(
"unexpected record after legacy pin restoration",
))),
None => Err(PileWriteError::IoError(std::io::Error::other(
"legacy pin record missing after restoration",
))),
}
})();
let unlock_result = self.file.unlock();
result?;
unlock_result?;
Ok(())
}
#[cfg(test)]
fn append_legacy_pin_for_test(
&mut self,
id: Id,
_previous: Option<Inline<Handle<SimpleArchive>>>,
head: Option<Inline<Handle<SimpleArchive>>>,
) -> Result<(), PileWriteError> {
self.append_legacy_pin_record(id, head)
}
#[cfg(test)]
fn legacy_pin_head_for_test(
&mut self,
id: Id,
) -> Result<Option<Inline<Handle<SimpleArchive>>>, ReadError> {
self.refresh()?;
Ok(self.branches.get(&id.into()).copied())
}
}
pub struct PileWantIter {
inner: crate::patch::PATCHIntoOrderedIterator<WANT_REQUEST_BYTES_LEN, IdentitySchema, ()>,
}
impl Iterator for PileWantIter {
type Item = Result<WantRequest, PileWriteError>;
fn next(&mut self) -> Option<Self::Item> {
let bytes = self.inner.next()?;
Some(Ok(WantRequest::from_bytes(bytes).expect(
"PileFile only indexes structurally decoded canonical want requests",
)))
}
}
impl PileFile {
fn retired_want_projection(
&self,
) -> Result<(usize, PATCH<WANT_REQUEST_BYTES_LEN, IdentitySchema>), ReadError> {
let bytes = unsafe {
slice_from_raw_parts(self.mmap.as_ptr(), self.applied_length)
.as_ref()
.expect("PileFile mapping pointer is valid for its applied prefix")
};
let mut offset = 0usize;
let mut retired_records = 0usize;
let mut historical = PATCH::<WANT_REQUEST_BYTES_LEN, IdentitySchema>::new();
while offset < bytes.len() {
let record = decode_record(&bytes[offset..], offset)?;
offset = offset
.checked_add(record.len)
.expect("validated pile record boundary fits usize");
match record.content {
PileRecordContent::RetiredWantAssert { identity } => {
retired_records += 1;
historical.insert(&Entry::new(&identity));
}
PileRecordContent::RetiredWantRetract { identity } => {
retired_records += 1;
historical.remove(&identity);
}
_ => {}
}
}
let mut active = PATCH::<WANT_REQUEST_BYTES_LEN, IdentitySchema>::new();
for identity in &historical {
if WantRequest::from_bytes(*identity).is_ok() {
active.insert(&Entry::new(identity));
}
}
Ok((retired_records, active))
}
fn want_cutover_status_from(
&self,
retired_records: usize,
active: &PATCH<WANT_REQUEST_BYTES_LEN, IdentitySchema>,
) -> WantCutoverStatus {
let resolved_active = active.iter().count();
let already_current = active
.iter()
.filter(|request| self.wants.get(request).is_some())
.count();
WantCutoverStatus {
retired_records,
resolved_active,
already_current,
missing_current: resolved_active - already_current,
}
}
pub fn want_cutover_status(&mut self) -> Result<WantCutoverStatus, ReadError> {
self.refresh()?;
let (retired_records, active) = self.retired_want_projection()?;
Ok(self.want_cutover_status_from(retired_records, &active))
}
fn append_want_locked(&mut self, request: WantRequest) -> Result<bool, PileWriteError> {
let key = request.to_bytes();
if self.wants.get(&key).is_some() {
return Ok(false);
}
self.dirty = true;
let written = self
.file
.write(WantRecordHeader::new(request).as_bytes())
.map_err(PileWriteError::IoError)?;
if written != ENVELOPE_HEADER_LEN {
return Err(PileWriteError::IoError(std::io::Error::new(
std::io::ErrorKind::WriteZero,
"failed to write typed want header",
)));
}
match self.apply_next().map_err(PileWriteError::from)? {
Some(Applied::Want { request: actual }) if actual == request => Ok(true),
Some(_) => Err(PileWriteError::IoError(std::io::Error::other(
"unexpected record after typed want write",
))),
None => Err(PileWriteError::IoError(std::io::Error::other(
"typed want marker missing after write",
))),
}
}
pub fn migrate_retired_wants(&mut self) -> Result<WantCutoverStatus, PileWriteError> {
self.file.lock()?;
let result: Result<WantCutoverStatus, PileWriteError> = (|| {
self.refresh_locked().map_err(PileWriteError::from)?;
let (retired_records, active) = self
.retired_want_projection()
.map_err(PileWriteError::from)?;
let status = self.want_cutover_status_from(retired_records, &active);
for bytes in active.into_iter_ordered() {
let request = WantRequest::from_bytes(bytes)
.expect("retired WANT projection contains canonical request bytes");
self.append_want_locked(request)?;
}
Ok(status)
})();
let unlock_result = self.file.unlock();
let status = result?;
unlock_result?;
Ok(status)
}
fn write_want_marker(&mut self, request: WantRequest) -> Result<(), PileWriteError> {
self.file.lock()?;
let res = (|| {
self.refresh_locked().map_err(PileWriteError::from)?;
self.append_want_locked(request).map(|_| ())
})();
let unlock_res = self.file.unlock();
res?;
unlock_res?;
Ok(())
}
}
impl super::PinSnapshotSource for PileFile {
type PinSnapshotError = ReadError;
fn snapshot_pin_heads(&mut self) -> Result<super::PinSnapshot, Self::PinSnapshotError> {
self.refresh()?;
Ok(self.branches.clone())
}
}
impl WantStore for PileFile {
type WantError = PileWriteError;
fn want(&mut self, request: WantRequest) -> Result<(), Self::WantError> {
self.write_want_marker(request)
}
}
impl super::WantRead for PileFileSnapshot {
type WantsError = PileWriteError;
type WantIter<'a> = PileWantIter;
fn wants<'a>(&'a self) -> Result<Self::WantIter<'a>, Self::WantsError> {
let cloned = self.wants.clone();
Ok(PileWantIter {
inner: cloned.into_iter_ordered(),
})
}
}
impl crate::repo::BlobStoreMeta for PileFileSnapshot {
type MetaError = Infallible;
fn metadata<S>(
&self,
handle: Inline<Handle<S>>,
) -> Result<Option<crate::repo::BlobMetadata>, Self::MetaError>
where
S: BlobEncoding + 'static,
Handle<S>: InlineEncoding,
{
let hash: &Inline<Hash<Blake3>> = handle.as_transmute();
match self.validated_blob_record::<Infallible>(hash, ValidationStrategy::ParallelIfLarge) {
Ok(record) => Ok(Some(crate::repo::BlobMetadata {
timestamp: record.timestamp,
length: record.bytes.len() as u64,
})),
Err(GetBlobError::BlobNotFound(_) | GetBlobError::ValidationError(_)) => Ok(None),
Err(GetBlobError::ConversionError(error)) => match error {},
}
}
fn resident(
&self,
handles: &PATCH<32, IdentitySchema, ()>,
) -> Result<PATCH<32, IdentitySchema, ()>, Self::MetaError> {
Ok(self.blobs.prefix_set::<32>().intersection(handles))
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum WantRewritePolicy {
Preserve,
Drop,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct PileReframeStats {
pub blobs: usize,
pub pin_updates: usize,
pub wants: usize,
pub retired_want_records: usize,
pub collection_records: usize,
pub legacy_unsigned_equations: usize,
pub retired_equations: usize,
pub legacy_v3_records: usize,
pub capability_proofs: usize,
pub dropped_inert: usize,
}
#[derive(Debug)]
#[non_exhaustive]
pub enum PileReframeError {
Source(ReadError),
SourcePayload {
offset: usize,
},
OpaqueRecord {
offset: usize,
kind: OpaqueKind,
},
Destination(InsertError),
Pin(PileWriteError),
Want(PileWriteError),
Collection(CollectionInsertError),
CapabilityProof(CapabilityProofInsertError),
DestinationNotEmpty {
length: usize,
},
Flush(FlushError),
}
impl std::fmt::Display for PileReframeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Source(error) => write!(f, "failed to read source pile: {error}"),
Self::SourcePayload { offset } => {
write!(f, "record at byte {offset} has no readable payload")
}
Self::OpaqueRecord { offset, kind } => write!(
f,
"unknown record kind {} at byte {offset}; refusing semantic reframe",
hex::encode_upper(kind.as_bytes())
),
Self::Destination(error) => write!(f, "failed to append to destination: {error}"),
Self::Pin(error) => write!(f, "failed to replay a pin update: {error}"),
Self::Want(error) => write!(f, "failed to replay a want: {error}"),
Self::Collection(error) => {
write!(f, "failed to re-encode a collection record: {error}")
}
Self::CapabilityProof(error) => {
write!(f, "failed to re-encode a capability proof: {error}")
}
Self::DestinationNotEmpty { length } => write!(
f,
"destination already holds {length} byte(s); reframe requires an empty pile"
),
Self::Flush(error) => write!(f, "failed to flush reframed pile: {error}"),
}
}
}
impl Error for PileReframeError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Source(error) => Some(error),
Self::Destination(error) => Some(error),
Self::Pin(error) | Self::Want(error) => Some(error),
Self::Collection(error) => Some(error),
Self::CapabilityProof(error) => Some(error),
Self::Flush(error) => Some(error),
Self::SourcePayload { .. }
| Self::OpaqueRecord { .. }
| Self::DestinationNotEmpty { .. } => None,
}
}
}
pub fn reframe_into(
source: &Path,
destination: &mut PileFile,
) -> Result<PileReframeStats, PileReframeError> {
let existing = destination
.refresh()
.map(|()| destination.applied_length)
.map_err(PileReframeError::Source)?;
if existing != 0 {
return Err(PileReframeError::DestinationNotEmpty { length: existing });
}
let mut records = PileRecords::open(source).map_err(PileReframeError::Source)?;
for record in records.by_ref() {
let record = record.map_err(PileReframeError::Source)?;
if let PileRecordContent::Opaque { kind } = record.content {
return Err(PileReframeError::OpaqueRecord {
offset: record.offset,
kind,
});
}
}
records.offset = 0;
let mut stats = PileReframeStats::default();
let mut retired_wants = PATCH::<WANT_REQUEST_BYTES_LEN, IdentitySchema>::new();
let mut output_wants = PATCH::<WANT_REQUEST_BYTES_LEN, IdentitySchema>::new();
loop {
let record = match records.next() {
None => break,
Some(record) => record.map_err(PileReframeError::Source)?,
};
match record.content {
PileRecordContent::Blob {
timestamp: _,
data_offset,
data_len,
..
} => {
let end = data_offset
.checked_add(data_len)
.filter(|end| *end <= records.bytes().len())
.ok_or(PileReframeError::SourcePayload {
offset: record.offset,
})?;
let bytes = records.bytes().slice(data_offset..end);
destination
.put::<UnknownBlob, _>(Blob::<UnknownBlob>::new(bytes))
.map_err(PileReframeError::Destination)?;
stats.blobs += 1;
}
PileRecordContent::Branch { branch_id, head } => {
destination
.append_legacy_pin_record(branch_id, Some(head))
.map_err(PileReframeError::Pin)?;
stats.pin_updates += 1;
}
PileRecordContent::BranchTombstone { branch_id } => {
destination
.append_legacy_pin_record(branch_id, None)
.map_err(PileReframeError::Pin)?;
stats.pin_updates += 1;
}
PileRecordContent::Want { request } => {
destination.want(request).map_err(PileReframeError::Want)?;
output_wants.insert(&Entry::new(&request.to_bytes()));
}
PileRecordContent::RetiredWantAssert { identity } => {
stats.retired_want_records += 1;
retired_wants.insert(&Entry::new(&identity));
}
PileRecordContent::RetiredWantRetract { identity } => {
stats.retired_want_records += 1;
retired_wants.remove(&identity);
}
PileRecordContent::Collection { record } => {
destination
.insert(record)
.map_err(PileReframeError::Collection)?;
stats.collection_records += 1;
}
PileRecordContent::RetiredCollectionEquation { equation } => {
destination
.preserve_retired_equation(&equation)
.map_err(PileReframeError::Collection)?;
stats.retired_equations += 1;
}
PileRecordContent::LegacyUnsignedCollectionEquation { .. }
| PileRecordContent::LegacyCollectionV3 { .. } => {
let header = records.bytes()[record.offset..record.offset + V3_HEADER_LEN]
.try_into()
.expect("validated historical collection frames occupy one block");
destination
.preserve_legacy_collection_header(header)
.map_err(PileReframeError::Collection)?;
if matches!(record.content, PileRecordContent::LegacyCollectionV3 { .. }) {
stats.legacy_v3_records += 1;
} else {
stats.legacy_unsigned_equations += 1;
}
}
PileRecordContent::CapabilityProof {
data_offset,
data_len,
..
} => {
let end = data_offset
.checked_add(data_len)
.filter(|end| *end <= records.bytes().len())
.ok_or(PileReframeError::SourcePayload {
offset: record.offset,
})?;
let proof =
CapabilityProof::from_validated_bytes(records.bytes().slice(data_offset..end));
destination
.insert_proof(proof)
.map_err(PileReframeError::CapabilityProof)?;
stats.capability_proofs += 1;
}
_ => stats.dropped_inert += 1,
}
}
for identity in retired_wants.iter_ordered() {
let Ok(request) = WantRequest::from_bytes(*identity) else {
continue;
};
destination.want(request).map_err(PileReframeError::Want)?;
output_wants.insert(&Entry::new(&request.to_bytes()));
}
stats.wants = output_wants.iter().count();
destination.flush().map_err(PileReframeError::Flush)?;
Ok(stats)
}
const ENDORSED_DERIVE_MARK: [u8; 32] = [0xFF; 32];
type EquationKey = ([u8; 32], [u8; 32], [u8; 32], [u8; 32]);
fn equation_key(
collection: [u8; 32],
first: [u8; 32],
second: [u8; 32],
result: [u8; 32],
) -> EquationKey {
let (low, high) = if first <= second {
(first, second)
} else {
(second, first)
};
(collection, low, high, result)
}
fn record_equation_key(record: &CollectionRecord) -> Option<EquationKey> {
match record {
CollectionRecord::Merge(record) => match record.inputs() {
[low, high] => Some(equation_key(
record.collection().raw,
low.raw,
high.raw,
record.result().raw,
)),
_ => None,
},
CollectionRecord::Derive(_) | CollectionRecord::Commit(_) | CollectionRecord::Map(_) => {
None
}
}
}
fn retired_equation_equation_key(equation: &RetiredCollectionEquation) -> EquationKey {
match *equation {
RetiredCollectionEquation::MergeV8 {
collection,
low,
high,
result,
..
} => equation_key(collection.raw, low.raw, high.raw, result.raw),
RetiredCollectionEquation::DeriveV9 {
target,
input,
output,
..
} => equation_key(target.raw, input.raw, ENDORSED_DERIVE_MARK, output.raw),
}
}
fn legacy_equation_key(equation: &LegacyUnsignedCollectionEquation) -> EquationKey {
match *equation {
LegacyUnsignedCollectionEquation::Merge {
collection,
low,
high,
result,
} => equation_key(collection.raw, low.raw, high.raw, result.raw),
LegacyUnsignedCollectionEquation::Derive {
collection,
input,
output,
} => equation_key(collection.raw, input.raw, ENDORSED_DERIVE_MARK, output.raw),
}
}
fn retired_signed_frame_equation_key(frame: &[u8]) -> Option<EquationKey> {
let kind = frame.get(FRAME_BODY_OFFSET - 32..FRAME_BODY_OFFSET)?;
let field = |index: usize| -> Option<[u8; 32]> {
frame
.get(FRAME_BODY_OFFSET + index * 32..FRAME_BODY_OFFSET + (index + 1) * 32)?
.try_into()
.ok()
};
if kind == record_kind::KIND_COLLECTION_MERGE_SIGNED_V2.as_slice()
|| kind == record_kind::KIND_COLLECTION_MERGE_WITNESSED_V6.as_slice()
{
Some(equation_key(field(0)?, field(1)?, field(2)?, field(3)?))
} else if kind == record_kind::KIND_COLLECTION_DERIVE_SIGNED_V2.as_slice()
|| kind == record_kind::KIND_COLLECTION_DERIVE_WITNESSED_V7.as_slice()
{
Some(equation_key(
field(0)?,
field(1)?,
ENDORSED_DERIVE_MARK,
field(2)?,
))
} else {
None
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PileRewriteStats {
pub retained_blobs: usize,
pub strong_pins: usize,
pub wants: usize,
pub capability_proofs: usize,
pub retired_capability_proofs: usize,
pub opaque_frames: usize,
pub superseded_equations: usize,
pub drained_frames: usize,
pub retired_equations: usize,
pub filtered_frames: usize,
pub repeated_frames: usize,
pub corrupt_blobs: BTreeSet<Inline<Handle<UnknownBlob>>>,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct RetainedRewritePlan {
pub retained: BTreeSet<Inline<Handle<UnknownBlob>>>,
pub retained_blobs: usize,
pub retained_bytes: u64,
pub capability_proofs: usize,
pub retired_capability_proofs: usize,
pub opaque_frames: usize,
pub superseded_equations: usize,
pub drained_frames: usize,
pub retired_equations: usize,
pub filtered_frames: usize,
pub repeated_frames: usize,
pub corrupt_blobs: BTreeSet<Inline<Handle<UnknownBlob>>>,
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum CollectionFrameGeneration {
Current,
RetiredV8V9,
RetiredOpaque,
LegacyUnsigned,
LegacyV3,
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum CollectionFrameRole {
Commit,
Merge,
Derive,
Definition,
Map,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct CollectionFrame {
pub generation: CollectionFrameGeneration,
pub role: CollectionFrameRole,
pub collection: Option<CollectionHandle>,
}
pub trait CollectionFrameFilter {
fn carries(&mut self, frame: &CollectionFrame) -> bool;
}
#[derive(Clone, Copy, Debug, Default)]
pub struct CarryEveryFrame;
impl CollectionFrameFilter for CarryEveryFrame {
fn carries(&mut self, _frame: &CollectionFrame) -> bool {
true
}
}
struct CollectionFrameSelection {
reader: PileFileSnapshot,
strong_pins: PATCH<16, IdentitySchema, Inline<Handle<SimpleArchive>>>,
legacy_headers: Vec<[u8; V3_HEADER_LEN]>,
opaque_frames: Vec<(usize, usize)>,
retired_proofs: Vec<(usize, usize)>,
retired_equations: Vec<RetiredCollectionEquation>,
current_records: Vec<[u8; 8]>,
superseded_equations: usize,
drained_frames: usize,
filtered_frames: usize,
repeated_frames: usize,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum FrameRetention {
EveryIndexedFrame,
CarriedFrames,
}
struct Retention {
keep: BTreeSet<Inline<Handle<UnknownBlob>>>,
corrupt: BTreeSet<Inline<Handle<UnknownBlob>>>,
capability_proofs: Vec<CapabilityProof>,
preserved_wants: Vec<WantRequest>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct DrainedGeneration {
pub retired: CollectionHandle,
pub current: CollectionHandle,
}
fn mapped_frame(reader: &PileFileSnapshot, offset: usize, len: usize) -> &[u8] {
assert!(
offset + len <= reader.covered_len,
"frame beyond the prefix"
);
unsafe {
slice_from_raw_parts(reader.mmap.as_ptr().add(offset), len)
.as_ref()
.expect("mapped frame")
}
}
fn frame_windows(frame: &[u8]) -> impl Iterator<Item = Inline<Handle<UnknownBlob>>> + '_ {
frame
.windows(32)
.map(|window| Inline::new(window.try_into().expect("windows are 32 bytes")))
}
fn legacy_v3_header_references(
header: &[u8; V3_HEADER_LEN],
) -> impl Iterator<Item = Inline<Handle<UnknownBlob>>> {
let magic: RawId = header[..16]
.try_into()
.expect("a V3 header starts with its marker");
let fields: Option<Vec<RawInline>> = match magic {
MAGIC_MARKER_COLLECTION_COMMIT_V3 => CollectionCommitHeaderV3::try_read_from_prefix(header)
.ok()
.map(|(header, _)| vec![header.data, header.metadata]),
MAGIC_MARKER_COLLECTION_MERGE_V3 => CollectionMergeHeaderV3::try_read_from_prefix(header)
.ok()
.map(|(header, _)| vec![header.low, header.high, header.result]),
MAGIC_MARKER_COLLECTION_DERIVE_V3 => CollectionDeriveHeaderV3::try_read_from_prefix(header)
.ok()
.map(|(header, _)| vec![header.input, header.output]),
_ => None,
};
fields.into_iter().flatten().map(Inline::new)
}
fn retired_signed_frame_collection(frame: &[u8]) -> Option<[u8; 32]> {
let kind = frame.get(FRAME_BODY_OFFSET - 32..FRAME_BODY_OFFSET)?;
let retired = [
record_kind::KIND_COLLECTION_MERGE_SIGNED_V2,
record_kind::KIND_COLLECTION_DERIVE_SIGNED_V2,
record_kind::KIND_COLLECTION_MERGE_WITNESSED_V6,
record_kind::KIND_COLLECTION_DERIVE_WITNESSED_V7,
];
if !retired.iter().any(|known| kind == known.as_slice()) {
return None;
}
frame
.get(FRAME_BODY_OFFSET..FRAME_BODY_OFFSET + 32)?
.try_into()
.ok()
}
fn retired_signed_collection_frame(frame: &[u8]) -> Option<CollectionFrame> {
let kind = frame.get(FRAME_BODY_OFFSET - 32..FRAME_BODY_OFFSET)?;
let role = if kind == record_kind::KIND_COLLECTION_MERGE_SIGNED_V2.as_slice()
|| kind == record_kind::KIND_COLLECTION_MERGE_WITNESSED_V6.as_slice()
{
CollectionFrameRole::Merge
} else if kind == record_kind::KIND_COLLECTION_DERIVE_SIGNED_V2.as_slice()
|| kind == record_kind::KIND_COLLECTION_DERIVE_WITNESSED_V7.as_slice()
{
CollectionFrameRole::Derive
} else {
return None;
};
Some(CollectionFrame {
generation: CollectionFrameGeneration::RetiredOpaque,
role,
collection: Some(CollectionHandle::new(retired_signed_frame_collection(
frame,
)?)),
})
}
fn close_drained_over_derivations(
reader: &PileFileSnapshot,
drained: &mut BTreeSet<[u8; 32]>,
candidates: &BTreeSet<[u8; 32]>,
) {
use crate::blob::TryFromBlob;
use crate::trible::TribleSet;
let mut sources: std::collections::BTreeMap<[u8; 32], Option<[u8; 32]>> =
std::collections::BTreeMap::new();
for candidate in candidates {
let descriptor: Result<Blob<SimpleArchive>, _> =
reader.get(CollectionHandle::new(*candidate));
let source = descriptor
.ok()
.and_then(|blob| <TribleSet as TryFromBlob<SimpleArchive>>::try_from_blob(blob).ok())
.and_then(|facts| crate::collection::descriptor::source(&facts).ok().flatten())
.map(|source| source.raw);
sources.insert(*candidate, source);
}
loop {
let before = drained.len();
for (candidate, source) in &sources {
if let Some(source) = source {
if drained.contains(source) {
drained.insert(*candidate);
}
}
}
if drained.len() == before {
break;
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CorruptBlobPolicy {
Refuse,
SetAside,
}
#[derive(Debug)]
#[non_exhaustive]
pub enum PileRewriteError {
Source(ReadError),
Transfer(super::TransferError<Infallible, GetBlobError<Infallible>, InsertError>),
StrongPin(PileWriteError),
StrongPinConflict {
id: Id,
current: Option<Inline<Handle<SimpleArchive>>>,
},
Want(PileWriteError),
Collection(CollectionInsertError),
CapabilityProof(CapabilityProofInsertError),
Flush(FlushError),
Undrained {
retired: CollectionHandle,
current: CollectionHandle,
unreached: usize,
},
Drain(String),
Corrupt {
blobs: BTreeSet<Inline<Handle<UnknownBlob>>>,
},
}
impl std::fmt::Display for PileRewriteError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Source(error) => write!(f, "failed to snapshot source pile: {error}"),
Self::Transfer(error) => write!(f, "failed to copy a retained blob: {error}"),
Self::StrongPin(error) => write!(f, "failed to recreate a strong pin: {error}"),
Self::StrongPinConflict { id, current } => write!(
f,
"destination has conflicting strong pin {id:X} at {current:?}"
),
Self::Want(error) => write!(f, "failed to recreate a want: {error}"),
Self::Collection(error) => {
write!(f, "failed to preserve a collection record: {error}")
}
Self::CapabilityProof(error) => {
write!(f, "failed to preserve a capability proof: {error}")
}
Self::Flush(error) => write!(f, "failed to flush rewritten pile: {error}"),
Self::Undrained {
retired,
current,
unreached,
} => write!(
f,
"generation {} is not drained into {}: {unreached} commit(s) have no counterpart",
hex::encode(retired.raw),
hex::encode(current.raw)
),
Self::Drain(error) => write!(f, "failed to check a drained generation: {error}"),
Self::Corrupt { blobs } => {
write!(
f,
"{} reached blob(s) have no occurrence matching their hash:",
blobs.len()
)?;
for blob in blobs {
write!(f, " blake3:{}", hex::encode_upper(blob.raw))?;
}
Ok(())
}
}
}
}
impl Error for PileRewriteError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Source(error) => Some(error),
Self::Transfer(error) => Some(error),
Self::StrongPin(error) | Self::Want(error) => Some(error),
Self::Collection(error) => Some(error),
Self::CapabilityProof(error) => Some(error),
Self::Flush(error) => Some(error),
Self::StrongPinConflict { .. }
| Self::Undrained { .. }
| Self::Drain(_)
| Self::Corrupt { .. } => None,
}
}
}
impl PileFile {
pub fn rewrite_retained_into(
&mut self,
destination: &mut PileFile,
explicit: &super::RetentionRoots,
wants: WantRewritePolicy,
) -> Result<PileRewriteStats, PileRewriteError> {
self.rewrite_retained_into_leaving(destination, explicit, wants, &[])
}
pub fn rewrite_retained_into_leaving(
&mut self,
destination: &mut PileFile,
explicit: &super::RetentionRoots,
wants: WantRewritePolicy,
drained_generations: &[DrainedGeneration],
) -> Result<PileRewriteStats, PileRewriteError> {
let selection = self.select_collection_frames(drained_generations, &mut CarryEveryFrame)?;
let retention = self.retention(
&selection,
explicit,
wants,
FrameRetention::EveryIndexedFrame,
);
Self::write_selection(destination, selection, retention)
}
pub fn plan_retained_rewrite(
&mut self,
explicit: &super::RetentionRoots,
wants: WantRewritePolicy,
drained_generations: &[DrainedGeneration],
filter: &mut dyn CollectionFrameFilter,
) -> Result<RetainedRewritePlan, PileRewriteError> {
let selection = self.select_collection_frames(drained_generations, filter)?;
let retention = self.retention(&selection, explicit, wants, FrameRetention::CarriedFrames);
let retained_bytes = retention
.keep
.iter()
.filter_map(|handle| selection.reader.get::<Bytes, UnknownBlob>(*handle).ok())
.map(|bytes| bytes.len() as u64)
.sum();
Ok(RetainedRewritePlan {
retained_blobs: retention.keep.len(),
retained: retention.keep,
retained_bytes,
capability_proofs: retention.capability_proofs.len(),
retired_capability_proofs: selection.retired_proofs.len(),
opaque_frames: selection.opaque_frames.len(),
superseded_equations: selection.superseded_equations,
drained_frames: selection.drained_frames,
retired_equations: selection.retired_equations.len(),
filtered_frames: selection.filtered_frames,
repeated_frames: selection.repeated_frames,
corrupt_blobs: retention.corrupt,
})
}
pub fn rewrite_retained_into_filtered(
&mut self,
destination: &mut PileFile,
explicit: &super::RetentionRoots,
wants: WantRewritePolicy,
drained_generations: &[DrainedGeneration],
filter: &mut dyn CollectionFrameFilter,
corrupt: CorruptBlobPolicy,
) -> Result<PileRewriteStats, PileRewriteError> {
let selection = self.select_collection_frames(drained_generations, filter)?;
let retention = self.retention(&selection, explicit, wants, FrameRetention::CarriedFrames);
if corrupt == CorruptBlobPolicy::Refuse && !retention.corrupt.is_empty() {
return Err(PileRewriteError::Corrupt {
blobs: retention.corrupt,
});
}
Self::write_selection(destination, selection, retention)
}
fn retention(
&self,
selection: &CollectionFrameSelection,
explicit: &super::RetentionRoots,
wants: WantRewritePolicy,
mode: FrameRetention,
) -> Retention {
let reader = &selection.reader;
let strong_pins = &selection.strong_pins;
let capability_proofs: Vec<CapabilityProof> = reader
.capability_proofs
.clone()
.into_iter_ordered()
.map(|id| {
reader
.capability_proofs
.get(&id)
.cloned()
.expect("proof key from snapshot must retain its value")
})
.collect();
let source_wants = self.wants.clone();
let preserved_wants: Vec<_> = if wants == WantRewritePolicy::Preserve {
source_wants
.into_iter_ordered()
.map(|bytes| {
WantRequest::from_bytes(bytes).expect(
"PileFile only indexes structurally decoded canonical want requests",
)
})
.collect()
} else {
Vec::new()
};
let resident = |handle: Inline<Handle<UnknownBlob>>| {
reader
.contains_blob(handle)
.expect("PileFileSnapshot residency lookup is infallible")
};
let mut roots = explicit.clone();
if !selection.opaque_frames.is_empty() {
match mode {
FrameRetention::EveryIndexedFrame => {
for info in reader.blobs() {
let info = info.expect("PileFileSnapshot blob listing is infallible");
roots.retain_direct(info.handle);
}
}
FrameRetention::CarriedFrames => {
for (offset, len) in &selection.opaque_frames {
for handle in frame_windows(mapped_frame(reader, *offset, *len)) {
if resident(handle) {
roots.retain_recursive(handle);
}
}
}
}
}
}
for (offset, len) in &selection.retired_proofs {
for handle in frame_windows(mapped_frame(reader, *offset, *len)) {
if resident(handle) {
roots.retain_recursive(handle);
}
}
}
if mode == FrameRetention::CarriedFrames {
for blob in description_blobs() {
let handle = blob.get_handle();
if resident(handle) {
roots.retain_recursive(handle);
}
}
}
if !strong_pins.is_empty() {
retain_record_kind_if_resident(&mut roots, reader, pin_head_record_kind());
}
if !capability_proofs.is_empty() {
retain_record_kind_if_resident(&mut roots, reader, capability_proof_record_kind());
}
for proof in &capability_proofs {
for handle in proof.blob_references() {
if resident(handle) {
roots.retain_recursive(handle);
}
}
}
if !preserved_wants.is_empty() {
retain_record_kind_if_resident(&mut roots, reader, want_record_kind());
}
for raw in strong_pins {
let head = *strong_pins
.get(raw)
.expect("pin key from snapshot must retain its value");
if reader
.contains_blob(head)
.expect("PileFileSnapshot residency lookup is infallible")
{
roots.retain_recursive(head);
}
}
let root_record = |roots: &mut super::RetentionRoots, record: CollectionRecord| {
retain_record_kind_if_resident(roots, reader, collection_record_kind(record));
for handle in record.blob_references() {
if resident(handle) {
roots.retain_recursive(handle);
}
}
};
match mode {
FrameRetention::EveryIndexedFrame => {
for key in reader.collection_records.iter() {
root_record(
&mut roots,
reader.record_at(key[64..].try_into().expect("key tail is the offset")),
);
}
for handle in reader.legacy_unsigned_collection_references() {
if resident(handle) {
roots.retain_recursive(handle);
}
}
for handle in reader.retired_collection_equation_references() {
if resident(handle) {
roots.retain_recursive(handle);
}
}
}
FrameRetention::CarriedFrames => {
for offset in &selection.current_records {
root_record(&mut roots, reader.record_at(offset));
}
for header in &selection.legacy_headers {
match decode_record(header, 0) {
Ok(PileRecord {
content:
PileRecordContent::LegacyUnsignedCollectionEquation { equation },
..
}) => {
let kind = match equation {
LegacyUnsignedCollectionEquation::Merge { .. } => {
record_kind::KIND_COLLECTION_MERGE_UNSIGNED
}
LegacyUnsignedCollectionEquation::Derive { .. } => {
record_kind::KIND_COLLECTION_DERIVE_UNSIGNED
}
};
for handle in equation
.blob_references()
.chain(std::iter::once(Inline::new(kind)))
{
if resident(handle) {
roots.retain_recursive(handle);
}
}
}
Ok(PileRecord {
content: PileRecordContent::LegacyCollectionV3 { .. },
..
}) => {
for handle in legacy_v3_header_references(header) {
if resident(handle) {
roots.retain_recursive(handle);
}
}
}
_ => {}
}
}
for equation in &selection.retired_equations {
let kind = match equation {
RetiredCollectionEquation::MergeV8 { .. } => {
record_kind::KIND_COLLECTION_MERGE_V8
}
RetiredCollectionEquation::DeriveV9 { .. } => {
record_kind::KIND_COLLECTION_DERIVE_V9
}
};
for handle in equation
.blob_references()
.chain(std::iter::once(Inline::new(kind)))
{
if resident(handle) {
roots.retain_recursive(handle);
}
}
}
}
}
for request in &preserved_wants {
for handle in request.blob_references() {
if resident(handle) {
roots.retain_recursive(handle);
}
}
}
let expand = |roots: &super::RetentionRoots| match mode {
FrameRetention::EveryIndexedFrame => (roots.expanded(reader), BTreeSet::new()),
FrameRetention::CarriedFrames => reader.expanded_setting_aside_corrupt(roots),
};
let (mut keep, mut corrupt) = expand(&roots);
let emits_blob = keep.iter().any(|handle| resident(*handle));
if emits_blob {
retain_record_kind_if_resident(&mut roots, reader, blob_record_kind());
(keep, corrupt) = expand(&roots);
}
Retention {
keep,
corrupt,
capability_proofs,
preserved_wants,
}
}
fn write_selection(
destination: &mut PileFile,
selection: CollectionFrameSelection,
retention: Retention,
) -> Result<PileRewriteStats, PileRewriteError> {
let CollectionFrameSelection {
reader,
strong_pins,
legacy_headers,
opaque_frames,
retired_proofs,
retired_equations,
current_records,
superseded_equations,
drained_frames,
filtered_frames,
repeated_frames,
} = selection;
let Retention {
keep,
corrupt,
capability_proofs,
preserved_wants,
} = retention;
let retained_blobs = keep.len();
for copied in super::transfer(&reader, destination, keep) {
copied.map_err(PileRewriteError::Transfer)?;
}
destination
.refresh()
.map_err(PileWriteError::from)
.map_err(PileRewriteError::StrongPin)?;
for raw in &strong_pins {
let id = Id::new(*raw).expect("PileFile never stores a nil strong-pin id");
let head = *strong_pins
.get(raw)
.expect("pin key from snapshot must retain its value");
match destination.branches.get(raw).copied() {
Some(current) if current == head => {}
Some(current) => {
return Err(PileRewriteError::StrongPinConflict {
id,
current: Some(current),
});
}
None => destination
.append_legacy_pin_record(id, Some(head))
.map_err(PileRewriteError::StrongPin)?,
}
}
for header in legacy_headers {
destination
.preserve_legacy_collection_header(header)
.map_err(PileRewriteError::Collection)?;
}
for (offset, len) in &opaque_frames {
destination
.preserve_exact_frame(mapped_frame(&reader, *offset, *len))
.map_err(PileRewriteError::Collection)?;
}
for equation in &retired_equations {
destination
.preserve_retired_equation(equation)
.map_err(PileRewriteError::Collection)?;
}
for offset in ¤t_records {
destination
.insert(reader.record_at(offset))
.map_err(PileRewriteError::Collection)?;
}
let capability_proof_count = capability_proofs.len();
for proof in capability_proofs {
destination
.insert_proof(proof)
.map_err(PileRewriteError::CapabilityProof)?;
}
for (offset, len) in &retired_proofs {
destination
.preserve_exact_frame(mapped_frame(&reader, *offset, *len))
.map_err(PileRewriteError::Collection)?;
}
for request in &preserved_wants {
destination.want(*request).map_err(PileRewriteError::Want)?;
}
destination.flush().map_err(PileRewriteError::Flush)?;
Ok(PileRewriteStats {
retained_blobs,
strong_pins: strong_pins.len() as usize,
wants: preserved_wants.len(),
capability_proofs: capability_proof_count,
retired_capability_proofs: retired_proofs.len(),
opaque_frames: opaque_frames.len(),
superseded_equations,
drained_frames,
retired_equations: retired_equations.len(),
filtered_frames,
repeated_frames,
corrupt_blobs: corrupt,
})
}
fn select_collection_frames(
&mut self,
drained_generations: &[DrainedGeneration],
filter: &mut dyn CollectionFrameFilter,
) -> Result<CollectionFrameSelection, PileRewriteError> {
let reader = self.snapshot().map_err(PileRewriteError::Source)?;
for pair in drained_generations {
let unreached = crate::collection::generation::unreached_records(
&reader,
pair.retired,
pair.current,
)
.map_err(|error| PileRewriteError::Drain(error.to_string()))?;
if unreached > 0 {
return Err(PileRewriteError::Undrained {
retired: pair.retired,
current: pair.current,
unreached,
});
}
}
let strong_pins = self.branches.clone();
self.physical_rewrite_guard()
.map_err(PileRewriteError::Source)?;
let covered = reader.covered_len;
let collection_records = reader.collection_records.clone();
let mut signed_equations = BTreeSet::new();
for key in collection_records.iter_ordered() {
let record = reader.record_at(key[64..].try_into().expect("key tail is the offset"));
if let Some(equation) = record_equation_key(&record) {
signed_equations.insert(equation);
}
}
for equation in reader.retired_collection_equations() {
signed_equations.insert(retired_equation_equation_key(&equation));
}
let mut superseded_equations = 0usize;
let mut drained_frames = 0usize;
let mut filtered_frames = 0usize;
let mut repeated_frames = 0usize;
let legacy_collection_headers = self.legacy_collection_headers.clone();
let mut drained: BTreeSet<[u8; 32]> = drained_generations
.iter()
.map(|pair| pair.retired.raw)
.collect();
if !drained.is_empty() {
let mut candidates: BTreeSet<[u8; 32]> = BTreeSet::new();
for key in collection_records.iter_ordered() {
candidates.insert(key[..32].try_into().expect("key head is the collection"));
}
for equation in reader.retired_collection_equations() {
candidates.insert(equation.collection().raw);
}
for header in legacy_collection_headers.iter_ordered() {
if let Ok(PileRecord {
content: PileRecordContent::LegacyUnsignedCollectionEquation { equation },
..
}) = decode_record(header, 0)
{
candidates.insert(equation.collection().raw);
}
}
for (offset, len) in self.opaque_frames.iter().copied() {
if offset + len > covered {
continue;
}
let frame = unsafe {
slice_from_raw_parts(reader.mmap.as_ptr().add(offset), len)
.as_ref()
.expect("mapped opaque frame")
};
if let Some(collection) = retired_signed_frame_collection(frame) {
candidates.insert(collection);
}
}
close_drained_over_derivations(&reader, &mut drained, &candidates);
}
let mut opaque_frames = Vec::new();
let mut opaque_seen: BTreeSet<[u8; 32]> = BTreeSet::new();
for (offset, len) in self.opaque_frames.iter().copied() {
if offset + len > covered {
continue;
}
let frame = unsafe {
slice_from_raw_parts(reader.mmap.as_ptr().add(offset), len)
.as_ref()
.expect("mapped opaque frame")
};
if !opaque_seen.insert(*blake3::hash(frame).as_bytes()) {
repeated_frames += 1;
continue;
}
if let Some(stated) = retired_signed_collection_frame(frame) {
if !filter.carries(&stated) {
filtered_frames += 1;
continue;
}
}
if retired_signed_frame_collection(frame)
.is_some_and(|collection| drained.contains(&collection))
{
drained_frames += 1;
continue;
}
if retired_signed_frame_equation_key(frame)
.is_some_and(|equation| signed_equations.contains(&equation))
{
superseded_equations += 1;
continue;
}
opaque_frames.push((offset, len));
}
let mut retired_proofs = Vec::new();
let mut proofs_seen: BTreeSet<[u8; 32]> = BTreeSet::new();
for (offset, len) in reader.retired_proof_frames.iter().copied() {
let frame = mapped_frame(&reader, offset, len);
if !proofs_seen.insert(*blake3::hash(frame).as_bytes()) {
repeated_frames += 1;
continue;
}
retired_proofs.push((offset, len));
}
let mut legacy_headers = Vec::new();
for header in legacy_collection_headers.into_iter_ordered() {
match decode_record(&header, 0) {
Ok(PileRecord {
content: PileRecordContent::LegacyUnsignedCollectionEquation { equation },
..
}) => {
let role = match equation {
LegacyUnsignedCollectionEquation::Merge { .. } => {
CollectionFrameRole::Merge
}
LegacyUnsignedCollectionEquation::Derive { .. } => {
CollectionFrameRole::Derive
}
};
let stated = CollectionFrame {
generation: CollectionFrameGeneration::LegacyUnsigned,
role,
collection: Some(equation.collection()),
};
if !filter.carries(&stated) {
filtered_frames += 1;
continue;
}
if drained.contains(&equation.collection().raw) {
drained_frames += 1;
continue;
}
if signed_equations.contains(&legacy_equation_key(&equation)) {
superseded_equations += 1;
continue;
}
}
Ok(PileRecord {
content: PileRecordContent::LegacyCollectionV3 { kind },
..
}) => {
let role = match kind {
LegacyCollectionRecordKindV3::Definition => CollectionFrameRole::Definition,
LegacyCollectionRecordKindV3::Commit => CollectionFrameRole::Commit,
LegacyCollectionRecordKindV3::Merge => CollectionFrameRole::Merge,
LegacyCollectionRecordKindV3::Derive => CollectionFrameRole::Derive,
};
let stated = CollectionFrame {
generation: CollectionFrameGeneration::LegacyV3,
role,
collection: None,
};
if !filter.carries(&stated) {
filtered_frames += 1;
continue;
}
}
_ => {}
}
legacy_headers.push(header);
}
let mut retired_equations = Vec::new();
for equation in reader.retired_collection_equations() {
let role = match equation {
RetiredCollectionEquation::MergeV8 { .. } => CollectionFrameRole::Merge,
RetiredCollectionEquation::DeriveV9 { .. } => CollectionFrameRole::Derive,
};
let stated = CollectionFrame {
generation: CollectionFrameGeneration::RetiredV8V9,
role,
collection: Some(equation.collection()),
};
if !filter.carries(&stated) {
filtered_frames += 1;
continue;
}
if drained.contains(&equation.collection().raw) {
drained_frames += 1;
continue;
}
retired_equations.push(equation);
}
let mut current_records = Vec::new();
let mut run = MemberRun::new();
for key in collection_records.iter_ordered() {
let offset: [u8; 8] = key[64..].try_into().expect("key tail is the offset");
let record = reader.record_at(&offset);
if !run.first(key, record) {
repeated_frames += 1;
continue;
}
let role = match record {
CollectionRecord::Commit(_) => CollectionFrameRole::Commit,
CollectionRecord::Merge(_) => CollectionFrameRole::Merge,
CollectionRecord::Derive(_) => CollectionFrameRole::Derive,
CollectionRecord::Map(_) => CollectionFrameRole::Map,
};
let stated = CollectionFrame {
generation: CollectionFrameGeneration::Current,
role,
collection: Some(record.collection()),
};
if !filter.carries(&stated) {
filtered_frames += 1;
continue;
}
if drained.contains(&key[..32]) {
drained_frames += 1;
continue;
}
current_records.push(offset);
}
Ok(CollectionFrameSelection {
reader,
strong_pins,
legacy_headers,
opaque_frames,
retired_proofs,
retired_equations,
current_records,
superseded_equations,
drained_frames,
filtered_frames,
repeated_frames,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::repo::WANT_REQUEST_KIND_DERIVE_V1;
use ed25519_dalek::SigningKey;
use rand::RngCore;
use std::collections::{BTreeSet, HashMap, HashSet};
use std::io::Write;
use std::path::PathBuf;
use std::time::SystemTime;
use std::time::UNIX_EPOCH;
use tempfile;
use crate::capability::{
CapabilityResource, CAPABILITY_PROOF_HEADER_LEN, MAX_CAPABILITY_PROOF_STEPS,
};
use crate::collection::descriptor::named_for_tests;
use crate::collection::{
empty_metadata_handle, AdmissionPolicy, Collection, CollectionCommit, CollectionData,
CollectionHandle, CollectionPolicy, CollectionSnapshotExt, CollectionStoreExt, Cover,
};
use crate::macros::entity;
use crate::repo::yard::{Yard, YardCollectError, YardConfig, YardReclaimError};
use crate::repo::{
BlobStoreMeta, RetentionRoots, SnapshotSource, StorageClose, StoreChanges,
StoreDependencies, StoreSnapshot, WantRead,
};
use crate::trible::TribleSet;
fn fresh_empty_pile_path(dir: &tempfile::TempDir, name: &str) -> PathBuf {
let path = dir.path().join(name);
std::fs::File::create(&path).unwrap();
path
}
fn team_key(seed: u8) -> RawInline {
SigningKey::from_bytes(&[seed; 32])
.verifying_key()
.to_bytes()
}
fn retired_peer_record(team_seed: u8, peer_seed: u8) -> Vec<u8> {
RetiredPeerEvidenceRecordHeader {
magic: FRAME_MAGIC,
span_blocks: ENVELOPE_HEADER_BLOCKS.to_le_bytes(),
record_kind: record_kind::KIND_PEER_EVIDENCE,
team_public_key: team_key(team_seed),
peer_public_key: team_key(peer_seed),
reserved: [0; 128],
}
.as_bytes()
.to_vec()
}
fn retired_store_scope_record(team_seed: u8) -> Vec<u8> {
RetiredStoreScopeRecordHeader {
magic: FRAME_MAGIC,
span_blocks: ENVELOPE_HEADER_BLOCKS.to_le_bytes(),
record_kind: record_kind::KIND_STORE_SCOPE,
team_public_key: team_key(team_seed),
reserved: [0; 160],
}
.as_bytes()
.to_vec()
}
#[test]
fn retired_artifact_offer_envelope_is_a_known_inert_kind() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "retired-offer.pile");
let mut retired = [0u8; ENVELOPE_HEADER_LEN];
retired[..FRAME_MAGIC_LEN].copy_from_slice(&FRAME_MAGIC);
retired[FRAME_MAGIC_LEN..FRAME_BODY_OFFSET - 32]
.copy_from_slice(&ENVELOPE_HEADER_BLOCKS.to_le_bytes());
retired[FRAME_BODY_OFFSET - 32..FRAME_BODY_OFFSET]
.copy_from_slice(&record_kind::KIND_ARTIFACT_OFFER_V1);
retired[FRAME_BODY_OFFSET..FRAME_BODY_OFFSET + 32].fill(4);
append_test_bytes(&path, &retired);
let mut pile = Pile::open(&path).unwrap();
assert_eq!(pile.opaque_record_count().unwrap(), 0);
pile.close().unwrap();
let mut records = PileRecords::open(&path).unwrap();
assert!(matches!(
records.next().unwrap().unwrap().content,
PileRecordContent::RetiredArtifactOfferV1
));
assert!(records.next().is_none());
}
fn capability_fixture(seed: u8, resource: [u8; 32]) -> CapabilityProof {
let root = SigningKey::from_bytes(&[seed; 32]);
let leaf = SigningKey::from_bytes(&[seed.wrapping_add(1); 32]);
let capability = Inline::new([seed.wrapping_add(2); 32]);
CapabilityProof::new(
CapabilityResource::new(resource),
&root,
capability,
leaf.verifying_key(),
)
}
fn retired_capability_v1_record(body: &[u8]) -> Vec<u8> {
let prefix_len = std::mem::size_of::<RetiredCapabilityProofRecordPrefix>();
let blocks = envelope_blocks_for_prefixed_payload(prefix_len, body.len()).unwrap();
let mut bytes = RetiredCapabilityProofRecordPrefix::new(blocks, body.len() as u64)
.as_bytes()
.to_vec();
bytes[FRAME_BODY_OFFSET - 32..FRAME_BODY_OFFSET]
.copy_from_slice(&record_kind::KIND_AUTH_PROOF_V1);
bytes.extend_from_slice(body);
bytes.resize(blocks as usize * ENVELOPE_BLOCK_LEN, 0);
bytes
}
fn framed_capability_proof(proof: &[u8]) -> Vec<u8> {
let blocks = envelope_blocks_for_prefixed_payload(FRAME_KIND_OFFSET, proof.len()).unwrap();
let mut frame = FRAME_MAGIC.to_vec();
frame.extend_from_slice(&blocks.to_le_bytes());
frame.extend_from_slice(proof);
frame.resize(blocks as usize * ENVELOPE_BLOCK_LEN, 0);
frame
}
#[test]
fn native_proof_padding_is_unambiguous_for_every_edge_count() {
let first = capability_fixture(101, [102; 32]);
let mut body = first.as_bytes()[..CAPABILITY_PROOF_HEADER_LEN].to_vec();
let mut edge = first.as_bytes()[CAPABILITY_PROOF_HEADER_LEN..].to_vec();
edge[CAPABILITY_PROOF_EDGE_LEN - 64..].fill(0);
for count in 1..=MAX_CAPABILITY_PROOF_STEPS {
body.extend_from_slice(&edge);
let frame = framed_capability_proof(&body);
let padding = frame.len() - FRAME_KIND_OFFSET - body.len();
assert_eq!(padding, if count % 2 == 0 { 128 } else { 0 });
let record = decode_enveloped_record(&frame, 0).unwrap();
let PileRecordContent::CapabilityProof {
id,
data_offset,
data_len,
} = record.content
else {
panic!("proof decoded as another record kind");
};
assert_eq!(data_offset, 32);
assert_eq!(data_len, body.len());
assert_eq!(&frame[data_offset..data_offset + data_len], &body);
assert_eq!(id.raw, Blake3::digest(&body));
}
body.extend_from_slice(&edge);
assert!(decode_enveloped_record(&framed_capability_proof(&body), 0).is_err());
}
#[test]
fn native_proof_rejects_nonzero_padding_gaps_and_nonminimal_frames() {
let first = capability_fixture(103, [104; 32]);
let frame = framed_capability_proof(first.as_bytes());
let padding_start = FRAME_KIND_OFFSET + first.as_bytes().len();
for index in padding_start..frame.len() {
let mut bad = frame.clone();
bad[index] = 1;
assert!(
decode_enveloped_record(&bad, 0).is_err(),
"padding at {index}"
);
}
let mut oversized = frame.clone();
oversized.extend_from_slice(&[0; ENVELOPE_BLOCK_LEN]);
oversized[FRAME_MAGIC_LEN..FRAME_KIND_OFFSET].copy_from_slice(&3u32.to_le_bytes());
assert!(decode_enveloped_record(&oversized, 0).is_err());
let mut gap = first.as_bytes().to_vec();
gap.extend_from_slice(&[0; CAPABILITY_PROOF_EDGE_LEN]);
gap.extend_from_slice(&first.as_bytes()[CAPABILITY_PROOF_HEADER_LEN..]);
assert!(decode_enveloped_record(&framed_capability_proof(&gap), 0).is_err());
}
#[test]
fn retired_action_proofs_are_readable_but_never_current_authority() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "retired-action-proof.pile");
let body = crate::capability::LEGACY_ACTION_PROOF_FIXTURE;
let mut frame = retired_capability_v1_record(&body);
frame[FRAME_BODY_OFFSET - 32..FRAME_BODY_OFFSET]
.copy_from_slice(&record_kind::KIND_AUTH_PROOF_V2);
append_test_bytes(&path, &frame);
let mut pile = Pile::open(&path).unwrap();
assert!(matches!(
PileRecords::open(&path)
.unwrap()
.next()
.unwrap()
.unwrap()
.content,
PileRecordContent::RetiredCapabilityProof
));
assert!(pile.snapshot().unwrap().proofs().unwrap().next().is_none());
assert!(CapabilityProof::from_bytes(&body).is_err());
let current = capability_fixture(91, [92; 32]);
pile.insert_proof(current.clone()).unwrap();
assert_eq!(
pile.snapshot()
.unwrap()
.proofs()
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap(),
vec![current]
);
pile.close().unwrap();
}
const TEST_UNKNOWN_KIND_A: RawInline = [0xA5; 32];
const TEST_UNKNOWN_KIND_B: RawInline = [0x5A; 32];
fn test_envelope_bytes(kind: RawInline, span_blocks: u32, physical_len: usize) -> Vec<u8> {
assert!(physical_len >= FRAME_BODY_OFFSET);
let mut bytes = vec![0xC3; physical_len];
bytes[..FRAME_MAGIC_LEN].copy_from_slice(&FRAME_MAGIC);
bytes[FRAME_MAGIC_LEN..FRAME_MAGIC_LEN + 4].copy_from_slice(&span_blocks.to_le_bytes());
bytes[FRAME_BODY_OFFSET - 32..FRAME_BODY_OFFSET].copy_from_slice(&kind);
bytes
}
fn test_envelope_v1_bytes(kind: RawId, span_blocks: u32, physical_len: usize) -> Vec<u8> {
assert!(physical_len >= 36);
let mut bytes = vec![0xC3; physical_len];
bytes[..16].copy_from_slice(&MAGIC_MARKER_ENVELOPE);
bytes[16..32].copy_from_slice(&kind);
bytes[32..36].copy_from_slice(&span_blocks.to_le_bytes());
bytes
}
fn append_test_bytes(path: &Path, bytes: &[u8]) {
let mut file = OpenOptions::new().append(true).open(path).unwrap();
file.write_all(bytes).unwrap();
file.sync_all().unwrap();
}
fn retired_blob_want_record(
request: WantRequest,
asserted: bool,
) -> RetiredBlobWantRecordHeader {
let WantRequest::Blob { handle } = request;
RetiredBlobWantRecordHeader {
magic: FRAME_MAGIC,
span_blocks: ENVELOPE_HEADER_BLOCKS.to_le_bytes(),
record_kind: if asserted {
record_kind::KIND_BLOB_WANT_ASSERT
} else {
record_kind::KIND_BLOB_WANT_RETRACT
},
handle: handle.raw,
reserved: [0; 160],
}
}
fn retired_typed_derive_v1_record(
source: CollectionHandle,
target: CollectionHandle,
input: Inline<Hash<Blake3>>,
asserted: bool,
) -> WantRecordHeader {
WantRecordHeader {
magic: FRAME_MAGIC,
span_blocks: ENVELOPE_HEADER_BLOCKS.to_le_bytes(),
record_kind: if asserted {
record_kind::KIND_WANT_ASSERT
} else {
record_kind::KIND_WANT_RETRACT
},
request_kind: WANT_REQUEST_KIND_DERIVE_V1,
kind_pad: [0; 31],
field_a: source.raw,
field_b: target.raw,
field_c: input.raw,
reserved: [0; 64],
}
}
fn collection_test_id(byte: u8) -> Id {
Id::new([byte; 16]).unwrap()
}
fn collection_test_hash(byte: u8) -> Inline<Hash<Blake3>> {
Inline::new([byte; 32])
}
fn collection_test_collection(byte: u8) -> CollectionHandle {
Inline::new([byte; 32])
}
fn register_simplearchive_collection(pile: &mut Pile, name: &str) -> Collection<SimpleArchive> {
let authority = SigningKey::from_bytes(&[0xAA; 32]).verifying_key();
pile.collection(
name,
CollectionPolicy::new(
AdmissionPolicy::direct(authority),
AdmissionPolicy::direct(authority),
),
)
.unwrap()
}
#[test]
fn replay_folds_admitted_records_into_downward_coverage() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "coverage-fold.pile");
let authority = SigningKey::from_bytes(&[0xAA; 32]);
let mut pile = Pile::open_as(&path, authority.verifying_key()).unwrap();
let collection = register_simplearchive_collection(&mut pile, "coverage-fold");
let low = collection_test_hash(6);
let high = collection_test_hash(7);
let result = collection_test_hash(8);
for payload in [low, high] {
pile.insert(CollectionRecord::Commit(CollectionCommit::sign(
&authority,
collection.handle(),
payload,
empty_metadata_handle(),
)))
.unwrap();
}
pile.insert(CollectionRecord::Merge(
CollectionMerge::sign(&authority, collection.handle(), [low, high], result).unwrap(),
))
.unwrap();
pile.refresh().unwrap();
let lineage = collection.handle();
assert!(pile
.snapshot()
.unwrap()
.coverage_index()
.covers(lineage, result, low));
assert!(pile
.snapshot()
.unwrap()
.coverage_index()
.covers(lineage, result, high));
assert_eq!(
pile.snapshot()
.unwrap()
.coverage_index()
.coverage(lineage, result)
.map(|row| row.len()),
Some(2)
);
assert_eq!(
pile.snapshot()
.unwrap()
.coverage_index()
.coverage(lineage, low)
.map(|row| row.len()),
Some(1)
);
assert!(!pile.snapshot().unwrap().coverage_index().has_parked());
pile.close().unwrap();
}
#[test]
fn coverage_is_rebuilt_from_the_file_on_every_open() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "coverage-rebuild.pile");
let authority = SigningKey::from_bytes(&[0xAA; 32]);
let low = collection_test_hash(6);
let high = collection_test_hash(7);
let result = collection_test_hash(8);
let handle = {
let mut pile = Pile::open_as(&path, authority.verifying_key()).unwrap();
let collection = register_simplearchive_collection(&mut pile, "coverage-rebuild");
for payload in [low, high] {
pile.insert(CollectionRecord::Commit(CollectionCommit::sign(
&authority,
collection.handle(),
payload,
empty_metadata_handle(),
)))
.unwrap();
}
pile.insert(CollectionRecord::Merge(
CollectionMerge::sign(&authority, collection.handle(), [low, high], result)
.unwrap(),
))
.unwrap();
pile.close().unwrap();
collection.handle()
};
let mut reopened = Pile::open_as(&path, authority.verifying_key()).unwrap();
reopened.refresh().unwrap();
assert_eq!(
reopened
.snapshot()
.unwrap()
.coverage_index()
.coverage(handle, result)
.map(|row| row.len()),
Some(2)
);
assert!(reopened
.snapshot()
.unwrap()
.coverage_index()
.covers(handle, result, low));
reopened.close().unwrap();
}
#[test]
fn an_unadmitted_signer_contributes_no_coverage_and_stays_parked() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "coverage-unadmitted.pile");
let mut pile = Pile::open(&path).unwrap();
let collection = register_simplearchive_collection(&mut pile, "coverage-unadmitted");
let stranger = SigningKey::from_bytes(&[7; 32]);
let payload = collection_test_hash(6);
pile.insert(CollectionRecord::Commit(CollectionCommit::sign(
&stranger,
collection.handle(),
payload,
empty_metadata_handle(),
)))
.unwrap();
pile.refresh().unwrap();
assert!(pile
.snapshot()
.unwrap()
.coverage_index()
.coverage(collection.handle(), payload)
.is_none());
assert_eq!(
pile.snapshot()
.unwrap()
.coverage_index()
.parked_on_signers(),
1
);
pile.close().unwrap();
}
#[test]
fn a_record_naming_an_unresolvable_collection_parks() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "coverage-no-descriptor.pile");
let mut pile = Pile::open(&path).unwrap();
for record in collection_test_records() {
pile.insert(record).unwrap();
}
pile.refresh().unwrap();
let index = pile.snapshot().unwrap().coverage_index();
assert!(index.is_empty());
assert_eq!(index.parked(), 2);
assert_eq!(index.parked_on_signers(), 0);
assert_eq!(index.parked_on_lineages(), 2);
assert_eq!(index.stored_joins(), 0);
pile.close().unwrap();
let signer = ed25519_dalek::SigningKey::from_bytes(&[7; 32]);
let mut hosted = Pile::open_as(&path, signer.verifying_key()).unwrap();
let index = hosted.snapshot().unwrap().coverage_index();
assert!(index.is_empty());
assert_eq!(index.parked(), 2);
assert_eq!(index.parked_on_lineages(), 2);
assert_eq!(index.stored_joins(), 1);
assert!(index.published().has_blocked(collection_test_collection(1)));
hosted.close().unwrap();
}
#[test]
fn coverage_matches_a_fold_over_the_same_records() {
use crate::collection::coverage::coverage_of;
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "coverage-agreement.pile");
let authority = SigningKey::from_bytes(&[0xAA; 32]);
let stranger = SigningKey::from_bytes(&[7; 32]);
let mut pile = Pile::open_as(&path, authority.verifying_key()).unwrap();
let collection = register_simplearchive_collection(&mut pile, "coverage-agreement");
let low = collection_test_hash(6);
let high = collection_test_hash(7);
let result = collection_test_hash(8);
for payload in [low, high] {
pile.insert(CollectionRecord::Commit(CollectionCommit::sign(
&authority,
collection.handle(),
payload,
empty_metadata_handle(),
)))
.unwrap();
}
pile.insert(CollectionRecord::Merge(
CollectionMerge::sign(&authority, collection.handle(), [low, high], result).unwrap(),
))
.unwrap();
pile.insert(CollectionRecord::Commit(CollectionCommit::sign(
&stranger,
collection.handle(),
collection_test_hash(9),
empty_metadata_handle(),
)))
.unwrap();
pile.insert(CollectionRecord::Merge(
CollectionMerge::sign(
&stranger,
collection.handle(),
[low, high],
collection_test_hash(10),
)
.unwrap(),
))
.unwrap();
for record in collection_test_records() {
pile.insert(record).unwrap();
}
let snapshot = pile.snapshot().unwrap();
let maintained = snapshot.coverage_index().published().clone();
let host = Some(Inline::new(authority.verifying_key().to_bytes()));
let folded = coverage_of(&snapshot, host).unwrap().published().clone();
assert_eq!(maintained, folded);
assert!(!maintained.is_empty());
assert_eq!(
maintained.frontier(collection.handle()).collect::<Vec<_>>(),
vec![result],
"control: the host's merge folded, the stranger's did not"
);
drop(snapshot);
pile.close().unwrap();
}
fn collection_test_records() -> Vec<CollectionRecord> {
let source = collection_test_collection(1);
let target = collection_test_collection(2);
let key = SigningKey::from_bytes(&[7; 32]);
vec![
CollectionRecord::Commit(CollectionCommit::sign(
&key,
source,
collection_test_hash(6),
empty_metadata_handle(),
)),
CollectionRecord::Merge(
CollectionMerge::sign(
&ed25519_dalek::SigningKey::from_bytes(&[7; 32]),
source,
[collection_test_hash(6), collection_test_hash(7)],
collection_test_hash(8),
)
.unwrap(),
),
CollectionRecord::Derive(CollectionDerive::sign(
&ed25519_dalek::SigningKey::from_bytes(&[7; 32]),
target,
crate::collection::SourceLocator::of(collection_test_hash(8).raw),
collection_test_hash(9),
)),
]
}
#[test]
fn native_capability_proof_record_round_trips_and_deduplicates_exact_bytes() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "proof.pile");
let proof = capability_fixture(11, [12; 32]);
assert_eq!(proof.as_bytes().len(), 224);
let mut pile = Pile::open(&path).unwrap();
pile.insert_proof(proof.clone()).unwrap();
assert_eq!(std::fs::metadata(&path).unwrap().len(), 256);
pile.insert_proof(proof.clone()).unwrap();
assert_eq!(std::fs::metadata(&path).unwrap().len(), 256);
let snapshot = pile.snapshot().unwrap();
assert_eq!(snapshot.proof(proof.id()).unwrap(), Some(proof.clone()));
assert_eq!(snapshot.proof(Inline::new([0; 32])).unwrap(), None);
assert_eq!(
snapshot
.proofs()
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap(),
vec![proof.clone()]
);
pile.close().unwrap();
let mut records = PileRecords::open(&path).unwrap();
let record = records.next().unwrap().unwrap();
let PileRecordContent::CapabilityProof {
id,
data_offset,
data_len,
} = record.content
else {
panic!("native proof decoded as another record kind");
};
assert_eq!(id, proof.id());
assert_eq!(data_offset, 32);
assert_eq!(data_len, proof.as_bytes().len());
assert_eq!(
&records.bytes()[data_offset..data_offset + data_len],
proof.as_bytes()
);
assert!(records.next().is_none());
let mut reopened = Pile::open(&path).unwrap();
let snapshot = reopened.snapshot().unwrap();
assert_eq!(
snapshot
.proofs()
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap(),
vec![proof]
);
reopened.close().unwrap();
}
#[test]
fn native_proof_views_survive_refresh_mapping_replacement_and_store_close() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "owned-proofs.pile");
let expected = capability_fixture(81, [82; 32]);
let later = capability_fixture(83, [84; 32]);
let mut pile = Pile::open(&path).unwrap();
pile.insert_proof(expected.clone()).unwrap();
let snapshot = pile.snapshot().unwrap();
let proof = snapshot.proof(expected.id()).unwrap().unwrap();
let repeated = snapshot.proof(expected.id()).unwrap().unwrap();
let mut proofs = snapshot.proofs().unwrap();
let mut retained_iterator = snapshot.proofs().unwrap();
let iterated = proofs.next().unwrap().unwrap();
assert!(proofs.next().is_none());
let body_offset = FRAME_KIND_OFFSET;
assert_eq!(
proof.as_bytes().as_ptr(),
pile.mmap.as_ptr().wrapping_add(body_offset),
"the indexed proof must own the decoded mmap slice, not a copied vector"
);
assert_eq!(proof.as_bytes().as_ptr(), repeated.as_bytes().as_ptr());
assert_eq!(proof.as_bytes().as_ptr(), iterated.as_bytes().as_ptr());
let original_mapping = Arc::downgrade(&pile.mmap);
let mapped_len = pile.mmap.len();
pile.put::<UnknownBlob, _>(Bytes::from_source(vec![7_u8; mapped_len]))
.unwrap();
assert!(!Arc::ptr_eq(
&original_mapping.upgrade().unwrap(),
&pile.mmap
));
pile.insert_proof(later.clone()).unwrap();
let refreshed = pile.snapshot().unwrap();
assert!(snapshot.proof(later.id()).unwrap().is_none());
assert_eq!(refreshed.proof(later.id()).unwrap(), Some(later));
assert_eq!(
refreshed
.proof(expected.id())
.unwrap()
.unwrap()
.as_bytes()
.as_ptr(),
proof.as_bytes().as_ptr()
);
let before_duplicate = std::fs::metadata(&path).unwrap().len();
pile.insert_proof(proof.clone()).unwrap();
assert_eq!(std::fs::metadata(&path).unwrap().len(), before_duplicate);
drop((snapshot, refreshed, proofs, repeated, iterated));
pile.close().unwrap();
assert!(original_mapping.upgrade().is_some());
assert_eq!(proof, expected);
proof.verify_signatures().unwrap();
let after_close = retained_iterator.next().unwrap().unwrap();
assert_eq!(after_close.as_bytes().as_ptr(), proof.as_bytes().as_ptr());
assert!(retained_iterator.next().is_none());
drop((proof, after_close, retained_iterator));
assert!(original_mapping.upgrade().is_none());
}
#[test]
fn native_capability_proofs_cat_as_an_order_independent_set_union() {
let dir = tempfile::tempdir().unwrap();
let path_a = fresh_empty_pile_path(&dir, "proof-a.pile");
let path_b = fresh_empty_pile_path(&dir, "proof-b.pile");
let path_ab = dir.path().join("proof-ab.pile");
let path_ba = dir.path().join("proof-ba.pile");
let first = capability_fixture(71, [72; 32]);
let second = capability_fixture(73, [74; 32]);
let third = capability_fixture(75, [76; 32]);
let mut a = Pile::open(&path_a).unwrap();
a.insert_proof(first.clone()).unwrap();
a.insert_proof(second.clone()).unwrap();
a.close().unwrap();
let mut b = Pile::open(&path_b).unwrap();
b.insert_proof(first.clone()).unwrap();
b.insert_proof(third.clone()).unwrap();
b.close().unwrap();
let bytes_a = std::fs::read(&path_a).unwrap();
let bytes_b = std::fs::read(&path_b).unwrap();
let mut ab = bytes_a.clone();
ab.extend_from_slice(&bytes_b);
std::fs::write(&path_ab, ab).unwrap();
let mut ba = bytes_b;
ba.extend_from_slice(&bytes_a);
std::fs::write(&path_ba, ba).unwrap();
let mut expected = vec![first, second, third];
expected.sort_unstable_by_key(|proof| proof.id().raw);
for path in [&path_ab, &path_ba] {
let mut pile = Pile::open(path).unwrap();
let snapshot = pile.snapshot().unwrap();
let actual = snapshot
.proofs()
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap();
assert_eq!(actual, expected);
pile.close().unwrap();
}
}
#[test]
fn retired_capability_proof_v1_is_known_inert() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "retired-proof-v1.pile");
let bytes = retired_capability_v1_record(&[0; 160]);
append_test_bytes(&path, &bytes);
let mut records = PileRecords::open(&path).unwrap();
assert!(matches!(
records.next().unwrap().unwrap().content,
PileRecordContent::RetiredCapabilityProof
));
assert!(records.next().is_none());
let mut pile = Pile::open(&path).unwrap();
assert!(pile.snapshot().unwrap().proofs().unwrap().next().is_none());
pile.close().unwrap();
}
#[test]
fn retired_capability_proof_v1_retains_its_structural_bounds() {
let dir = tempfile::tempdir().unwrap();
let invalid_key_path = fresh_empty_pile_path(&dir, "retired-invalid-key.pile");
let mut invalid_key = [0; 160];
let mut invalid_signer = [0; 32];
invalid_signer[0] = 2;
invalid_key[..32].copy_from_slice(&invalid_signer);
assert!(VerifyingKey::from_bytes(&invalid_signer).is_err());
append_test_bytes(
&invalid_key_path,
&retired_capability_v1_record(&invalid_key),
);
let mut pile = Pile::open(&invalid_key_path).unwrap();
assert!(matches!(
pile.refresh(),
Err(ReadError::CorruptPile { valid_length: 0 })
));
pile.close().unwrap();
let overlong_path = fresh_empty_pile_path(&dir, "retired-overlong.pile");
let overlong = vec![0; 32 + 128 * (MAX_CAPABILITY_PROOF_STEPS + 1)];
append_test_bytes(&overlong_path, &retired_capability_v1_record(&overlong));
let mut pile = Pile::open(&overlong_path).unwrap();
assert!(matches!(
pile.refresh(),
Err(ReadError::CorruptPile { valid_length: 0 })
));
pile.close().unwrap();
}
#[test]
fn known_capability_proof_record_rejects_root_only_and_nonzero_padding() {
let dir = tempfile::tempdir().unwrap();
let one_edge = capability_fixture(21, [22; 32]);
let root_only_path = fresh_empty_pile_path(&dir, "root-only.pile");
let mut root_only = Vec::with_capacity(256);
root_only.extend_from_slice(
RecordFrame {
magic: FRAME_MAGIC,
span_blocks: 1u32.to_le_bytes(),
record_kind: CAPABILITY_PROOF_MAGIC,
}
.as_bytes(),
);
root_only.extend_from_slice(
&one_edge.as_bytes()[CAPABILITY_PROOF_MAGIC.len()..CAPABILITY_PROOF_HEADER_LEN],
);
root_only.resize(256, 0);
append_test_bytes(&root_only_path, &root_only);
let mut pile = Pile::open(&root_only_path).unwrap();
assert!(matches!(
pile.refresh(),
Err(ReadError::CorruptPile { valid_length: 0 })
));
pile.close().unwrap();
let padded_path = fresh_empty_pile_path(&dir, "bad-padding.pile");
let two_edge = one_edge
.delegate(
&SigningKey::from_bytes(&[22; 32]),
Inline::new([23; 32]),
SigningKey::from_bytes(&[24; 32]).verifying_key(),
)
.unwrap();
let padding_offset = 32 + two_edge.as_bytes().len();
let mut pile = Pile::open(&padded_path).unwrap();
pile.insert_proof(two_edge).unwrap();
pile.close().unwrap();
let mut bytes = std::fs::read(&padded_path).unwrap();
assert_eq!(bytes.len(), 512);
bytes[padding_offset] = 1;
std::fs::write(&padded_path, bytes).unwrap();
let mut pile = Pile::open(&padded_path).unwrap();
assert!(matches!(
pile.refresh(),
Err(ReadError::CorruptPile { valid_length: 0 })
));
pile.close().unwrap();
}
#[test]
fn reframe_preserves_native_capability_proofs() {
let dir = tempfile::tempdir().unwrap();
let source_path = fresh_empty_pile_path(&dir, "proof-source.pile");
let destination_path = fresh_empty_pile_path(&dir, "proof-destination.pile");
let proof = capability_fixture(31, [32; 32]);
let mut source = Pile::open(&source_path).unwrap();
source.insert_proof(proof.clone()).unwrap();
source.close().unwrap();
let mut destination = Pile::open(&destination_path).unwrap();
let stats = reframe_into(&source_path, &mut destination).unwrap();
assert_eq!(stats.capability_proofs, 1);
assert_eq!(stats.dropped_inert, 0);
let snapshot = destination.snapshot().unwrap();
assert_eq!(
snapshot
.proofs()
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap(),
vec![proof]
);
destination.close().unwrap();
}
#[test]
fn retained_rewrite_preserves_proofs_without_treating_resources_as_blob_roots() {
let dir = tempfile::tempdir().unwrap();
let source_path = fresh_empty_pile_path(&dir, "proof-retention-source.pile");
let destination_path = fresh_empty_pile_path(&dir, "proof-retention-destination.pile");
let mut source = Pile::open(&source_path).unwrap();
let valid_attachment = source
.put::<UnknownBlob, _>(Bytes::from_source(b"valid attachment".to_vec()))
.unwrap();
let invalid_attachment = source
.put::<UnknownBlob, _>(Bytes::from_source(b"invalid attachment".to_vec()))
.unwrap();
let definition = source.put::<SimpleArchive, _>(TribleSet::new()).unwrap();
let invalid_definition = source
.put::<SimpleArchive, _>(
entity! { crate::metadata::tag: crate::metadata::KIND_BLOB_ENCODING }
.facts()
.clone(),
)
.unwrap();
let valid_proof = CapabilityProof::new(
CapabilityResource::new(valid_attachment.raw),
&SigningKey::from_bytes(&[41; 32]),
definition,
SigningKey::from_bytes(&[42; 32]).verifying_key(),
);
let invalid_proof = CapabilityProof::new(
CapabilityResource::new(invalid_attachment.raw),
&SigningKey::from_bytes(&[51; 32]),
invalid_definition,
SigningKey::from_bytes(&[52; 32]).verifying_key(),
);
invalid_proof.verify_signatures().unwrap();
source.insert_proof(valid_proof.clone()).unwrap();
source.insert_proof(invalid_proof.clone()).unwrap();
let mut destination = Pile::open(&destination_path).unwrap();
let stats = source
.rewrite_retained_into(
&mut destination,
&RetentionRoots::new(),
WantRewritePolicy::Drop,
)
.unwrap();
assert_eq!(stats.retained_blobs, 2);
assert_eq!(stats.capability_proofs, 2);
let reader = destination.snapshot().unwrap();
assert!(reader.get::<Blob<SimpleArchive>, _>(definition).is_ok());
assert!(reader
.get::<Blob<SimpleArchive>, _>(invalid_definition)
.is_ok());
assert!(reader
.get::<Blob<UnknownBlob>, _>(valid_attachment)
.is_err());
assert!(reader
.get::<Blob<UnknownBlob>, _>(invalid_attachment)
.is_err());
assert!(
reader.wants().unwrap().next().is_none(),
"retention never creates WANTs"
);
drop(reader);
let stored = destination
.snapshot()
.unwrap()
.proofs()
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap();
assert_eq!(stored.len(), 2);
assert!(stored.contains(&valid_proof));
assert!(stored.contains(&invalid_proof));
source.close().unwrap();
destination.close().unwrap();
}
#[test]
fn capability_proof_insert_rejects_bad_signature_before_appending() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "bad-proof-signature.pile");
let proof = capability_fixture(71, [72; 32]);
let mut bytes = proof.as_bytes().to_vec();
*bytes.last_mut().unwrap() ^= 1;
let invalid = CapabilityProof::from_bytes(&bytes).unwrap();
let mut pile = Pile::open(&path).unwrap();
let before = std::fs::metadata(&path).unwrap().len();
assert!(matches!(
pile.insert_proof(invalid),
Err(CapabilityProofInsertError::Invalid(_))
));
assert_eq!(std::fs::metadata(&path).unwrap().len(), before);
assert_eq!(pile.snapshot().unwrap().proofs().unwrap().count(), 0);
pile.close().unwrap();
}
fn fixed_collection_header(bytes: &[u8]) -> [u8; V3_HEADER_LEN] {
bytes
.try_into()
.expect("collection headers are exactly one fixed pile header")
}
fn legacy_collection_test_headers() -> Vec<(LegacyCollectionRecordKindV3, [u8; V3_HEADER_LEN])>
{
vec![
(
LegacyCollectionRecordKindV3::Definition,
fixed_collection_header(
CollectionDefinitionHeaderV3 {
magic_marker: MAGIC_MARKER_COLLECTION_DEFINITION_V3,
scope: [1; 16],
representation: [2; 16],
recipe: [3; 16],
reserved: [0; 192],
}
.as_bytes(),
),
),
(
LegacyCollectionRecordKindV3::Commit,
fixed_collection_header(
CollectionCommitHeaderV3 {
magic_marker: MAGIC_MARKER_COLLECTION_COMMIT_V3,
collection: [4; 16],
data: [5; 32],
metadata: [6; 32],
public_key: [7; 32],
signature_r: [8; 32],
signature_s: [9; 32],
reserved: [0; 64],
}
.as_bytes(),
),
),
(
LegacyCollectionRecordKindV3::Merge,
fixed_collection_header(
CollectionMergeHeaderV3 {
magic_marker: MAGIC_MARKER_COLLECTION_MERGE_V3,
collection: [10; 16],
low: [11; 32],
high: [12; 32],
result: [13; 32],
reserved: [0; 128],
}
.as_bytes(),
),
),
(
LegacyCollectionRecordKindV3::Derive,
fixed_collection_header(
CollectionDeriveHeaderV3 {
magic_marker: MAGIC_MARKER_COLLECTION_DERIVE_V3,
source: [14; 16],
target: [15; 16],
input: [16; 32],
output: [17; 32],
reserved: [0; 144],
}
.as_bytes(),
),
),
]
}
fn legacy_collection_headers_at(
path: &Path,
) -> Vec<(LegacyCollectionRecordKindV3, [u8; V3_HEADER_LEN])> {
let mut records = PileRecords::open(path).unwrap();
let mut found = Vec::new();
while let Some(record) = records.next() {
let record = record.unwrap();
let PileRecordContent::LegacyCollectionV3 { kind } = record.content else {
continue;
};
let header = records.bytes()[record.offset..record.offset + record.len]
.try_into()
.unwrap();
found.push((kind, header));
}
found
}
fn sorted_collection_records(mut records: Vec<CollectionRecord>) -> Vec<CollectionRecord> {
records.sort_by_key(CollectionRecord::fingerprint);
records
}
#[test]
fn legacy_collection_header_index_uses_the_full_256_byte_key() {
let low = [0u8; V3_HEADER_LEN];
let mut high = low;
high[V3_HEADER_LEN - 1] = 1;
let mut index = LegacyCollectionHeaderIndex::new();
index.insert(&Entry::new(&high));
index.insert(&Entry::new(&low));
index.insert(&Entry::new(&high));
assert_eq!(index.len(), 2);
assert_eq!(
index.iter_ordered().copied().collect::<Vec<_>>(),
vec![low, high]
);
}
fn invalidate_collection_commit(commit: CollectionCommit) -> CollectionCommit {
let (signature_r, signature_s) = commit.signature();
let mut forged_r = signature_r.raw;
forged_r[0] ^= 1;
let forged = CollectionCommit::from_parts(
commit.collection(),
commit.data(),
commit.metadata(),
commit.public_key(),
Inline::new(forged_r),
signature_s,
);
assert!(forged.verify_strict().is_err());
forged
}
#[test]
fn every_retired_equation_generation_is_opaque_inert_and_carried_exactly() {
let dir = tempfile::tempdir().unwrap();
let source_path = fresh_empty_pile_path(&dir, "retired-signed-source.pile");
let destination_path = fresh_empty_pile_path(&dir, "retired-signed-retained.pile");
let frames = [
test_envelope_bytes(record_kind::KIND_COLLECTION_MERGE_SIGNED_V2, 2, 512),
test_envelope_bytes(record_kind::KIND_COLLECTION_DERIVE_SIGNED_V2, 1, 256),
test_envelope_bytes(record_kind::KIND_COLLECTION_MERGE_WITNESSED_V6, 2, 512),
test_envelope_bytes(record_kind::KIND_COLLECTION_DERIVE_WITNESSED_V7, 2, 512),
];
for frame in &frames {
append_test_bytes(&source_path, frame);
}
let mut source = Pile::open(&source_path).unwrap();
let unmodeled_dependency = source
.put::<UnknownBlob, _>(Bytes::from_source(b"retired record may own this".to_vec()))
.unwrap();
let snapshot = source.snapshot().unwrap();
assert_eq!(snapshot.opaque_record_count(), frames.len());
assert_eq!(snapshot.records().unwrap().count(), 0);
let mut destination = Pile::open(&destination_path).unwrap();
let stats = source
.rewrite_retained_into(
&mut destination,
&RetentionRoots::new(),
WantRewritePolicy::Drop,
)
.unwrap();
assert_eq!(stats.opaque_frames, frames.len());
let retained = destination.snapshot().unwrap();
assert_eq!(retained.records().unwrap().count(), 0);
assert!(retained
.get::<Blob<UnknownBlob>, _>(unmodeled_dependency)
.is_ok());
let mut physical = PileRecords::open(&destination_path).unwrap();
let carried: Vec<_> = physical
.by_ref()
.filter_map(|record| {
let record = record.unwrap();
matches!(record.content, PileRecordContent::Opaque { .. })
.then_some((record.offset, record.len))
})
.collect();
assert_eq!(carried.len(), frames.len());
for ((offset, len), expected) in carried.into_iter().zip(&frames) {
assert_eq!(&physical.bytes()[offset..offset + len], expected);
}
drop((snapshot, retained, physical));
destination.close().unwrap();
source.close().unwrap();
}
#[test]
fn merge_framing_rejects_payload_inputs_out_of_canonical_order() {
let signer = SigningKey::from_bytes(&[81; 32]);
let collection = collection_test_collection(82);
let low = collection_test_hash(83);
let high = collection_test_hash(84);
assert!(low < high);
let merge = CollectionRecord::Merge(
CollectionMerge::sign(&signer, collection, [low, high], collection_test_hash(85))
.unwrap(),
);
let mut frame = collection_record_header(&merge);
assert_eq!(&frame[256..288], low.raw.as_slice());
frame[256..288].copy_from_slice(&high.raw);
frame[288..320].copy_from_slice(&low.raw);
assert!(matches!(
decode_record(&frame, 0),
Err(ReadError::CorruptPile { valid_length: 0 })
));
}
#[test]
fn n_ary_merge_frames_span_by_arity_and_refuse_noncanonical_framing() {
let key = SigningKey::from_bytes(&[91; 32]);
let collection = collection_test_collection(92);
let merge = |count: u8| {
CollectionMerge::sign(
&key,
collection,
(1..=count).map(collection_test_hash),
collection_test_hash(200),
)
.unwrap()
};
for (count, blocks) in [(2u8, 2usize), (7, 2), (8, 2), (9, 3), (16, 3)] {
let record = CollectionRecord::Merge(merge(count));
let frame = collection_record_header(&record);
assert_eq!(frame.len(), blocks * ENVELOPE_BLOCK_LEN, "{count} inputs");
let decoded = decode_record(&frame, 0).unwrap();
assert_eq!(decoded.len, frame.len());
assert!(matches!(
decoded.content,
PileRecordContent::Collection { record: decoded } if decoded == record
));
let dense_end = FRAME_BODY_OFFSET + collection_merge_bytes_len(count as usize);
assert_eq!(
&frame[FRAME_BODY_OFFSET..dense_end],
&record.to_bytes()[1..]
);
assert!(frame[dense_end..].iter().all(|byte| *byte == 0));
}
assert_eq!(
FRAME_BODY_OFFSET + collection_merge_bytes_len(8),
2 * ENVELOPE_BLOCK_LEN
);
let frame = collection_record_header(&CollectionRecord::Merge(merge(3)));
let corrupt = |frame: &[u8]| {
matches!(
decode_record(frame, 0),
Err(ReadError::CorruptPile { valid_length: 0 })
)
};
let mut long = frame.clone();
long[FRAME_MAGIC_LEN..FRAME_MAGIC_LEN + 4].copy_from_slice(&3u32.to_le_bytes());
long.extend_from_slice(&[0; ENVELOPE_BLOCK_LEN]);
assert!(corrupt(&long));
for count in [0u32, 1, 17, 4] {
let mut arity = frame.clone();
arity[252..256].copy_from_slice(&count.to_be_bytes());
assert!(corrupt(&arity), "count {count}");
}
let mut pad = frame.clone();
pad[224] = 1;
assert!(corrupt(&pad));
let mut tail = frame.clone();
*tail.last_mut().unwrap() = 1;
assert!(corrupt(&tail));
let mut duplicate = frame.clone();
let (first, second) = (256..288, 288..320);
let copy = duplicate[first].to_vec();
duplicate[second].copy_from_slice(©);
assert!(corrupt(&duplicate));
}
#[test]
fn a_locator_derive_frame_is_one_block_naming_its_locator() {
let key = SigningKey::from_bytes(&[93; 32]);
let source_payload = collection_test_hash(94);
let derive = CollectionDerive::sign(
&key,
collection_test_collection(95),
crate::collection::SourceLocator::of(source_payload.raw),
collection_test_hash(96),
);
let frame = collection_record_header(&CollectionRecord::Derive(derive));
assert_eq!(frame.len(), ENVELOPE_BLOCK_LEN);
assert_eq!(
&frame[FRAME_BODY_OFFSET - 32..FRAME_BODY_OFFSET],
record_kind::KIND_COLLECTION_DERIVE.as_slice()
);
assert_eq!(&frame[96..128], derive.input().as_bytes().as_slice());
assert_ne!(&frame[96..128], source_payload.raw.as_slice());
assert!(matches!(
decode_record(&frame, 0).unwrap().content,
PileRecordContent::Collection {
record: CollectionRecord::Derive(decoded)
} if decoded == derive
));
}
#[test]
fn a_map_frame_is_one_block_naming_its_node_and_replays() {
let key = SigningKey::from_bytes(&[93; 32]);
let map = CollectionMap::sign(
&key,
collection_test_collection(97),
collection_test_hash(98),
collection_test_hash(99),
);
let frame = collection_record_header(&CollectionRecord::Map(map));
assert_eq!(frame.len(), ENVELOPE_BLOCK_LEN);
assert_eq!(
&frame[FRAME_BODY_OFFSET - 32..FRAME_BODY_OFFSET],
record_kind::KIND_COLLECTION_MAP.as_slice()
);
assert_eq!(&frame[FRAME_BODY_OFFSET..], map.to_bytes().as_slice());
assert!(matches!(
decode_record(&frame, 0).unwrap().content,
PileRecordContent::Collection {
record: CollectionRecord::Map(decoded)
} if decoded == map
));
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "map.pile");
let mut pile = Pile::open(&path).unwrap();
pile.insert(CollectionRecord::Map(map)).unwrap();
pile.insert(CollectionRecord::Map(map)).unwrap();
pile.close().unwrap();
assert_eq!(
std::fs::metadata(&path).unwrap().len(),
ENVELOPE_BLOCK_LEN as u64
);
let mut pile = Pile::open(&path).unwrap();
let snapshot = pile.snapshot().unwrap();
for selector in [
CollectionRecordSelector::Collection(map.attached()),
CollectionRecordSelector::ProducedMember(map.attached(), map.attachment()),
] {
assert_eq!(
snapshot
.select_records(&BTreeSet::from([selector]))
.unwrap(),
vec![CollectionRecord::Map(map)]
);
}
assert!(snapshot
.select_records(&BTreeSet::from([CollectionRecordSelector::ProducedMember(
map.attached(),
map.node(),
)]))
.unwrap()
.is_empty());
drop(snapshot);
pile.close().unwrap();
}
#[test]
fn retired_live_equations_are_inert_indexed_and_carried_exactly() {
let dir = tempfile::tempdir().unwrap();
let source_path = fresh_empty_pile_path(&dir, "retired-live-source.pile");
let mut source = Pile::open(&source_path).unwrap();
let named = source
.put::<UnknownBlob, _>(Bytes::from_source(b"a retired derive input".to_vec()))
.unwrap();
let unrelated = source
.put::<UnknownBlob, _>(Bytes::from_source(b"nothing names this".to_vec()))
.unwrap();
source.close().unwrap();
let key = SigningKey::from_bytes(&[97; 32]);
let collection = collection_test_collection(98);
let merge_v8 = RetiredCollectionEquation::sign_merge_v8(
&key,
collection,
collection_test_hash(3),
collection_test_hash(2),
collection_test_hash(4),
);
let derive_v9 = RetiredCollectionEquation::sign_derive_v9(
&key,
collection_test_collection(99),
Inline::new(named.raw),
collection_test_hash(5),
);
let frames = [
retired_equation_frame(&merge_v8),
retired_equation_frame(&derive_v9),
];
assert_eq!(frames[0].len(), 2 * ENVELOPE_BLOCK_LEN);
assert_eq!(frames[1].len(), ENVELOPE_BLOCK_LEN);
for frame in &frames {
append_test_bytes(&source_path, frame);
}
append_test_bytes(&source_path, &frames[0]);
let decoded: Vec<_> = PileRecords::open(&source_path)
.unwrap()
.filter_map(|record| match record.unwrap().content {
PileRecordContent::RetiredCollectionEquation { equation } => Some(equation),
_ => None,
})
.collect();
assert_eq!(decoded, vec![merge_v8, derive_v9, merge_v8]);
decoded[0].verify_strict().unwrap();
decoded[1].verify_strict().unwrap();
let mut source = Pile::open(&source_path).unwrap();
let snapshot = source.snapshot().unwrap();
assert_eq!(snapshot.opaque_record_count(), 0);
assert_eq!(snapshot.records().unwrap().count(), 0);
let mut retired: Vec<_> = snapshot.retired_collection_equations().collect();
retired.sort();
let mut expected = vec![merge_v8, derive_v9];
expected.sort();
assert_eq!(retired, expected);
drop(snapshot);
let carried_frames = |path: &Path| -> Vec<Vec<u8>> {
let mut records = PileRecords::open(path).unwrap();
let spans: Vec<_> = records
.by_ref()
.filter_map(|record| {
let record = record.unwrap();
matches!(
record.content,
PileRecordContent::RetiredCollectionEquation { .. }
)
.then_some((record.offset, record.len))
})
.collect();
let mut frames: Vec<_> = spans
.into_iter()
.map(|(offset, len)| records.bytes()[offset..offset + len].to_vec())
.collect();
frames.sort();
frames
};
let mut sorted_frames = frames.to_vec();
sorted_frames.sort();
let rewritten_path = fresh_empty_pile_path(&dir, "retired-live-rewritten.pile");
let mut rewritten = Pile::open(&rewritten_path).unwrap();
let stats = source
.rewrite_retained_into(
&mut rewritten,
&RetentionRoots::new(),
WantRewritePolicy::Drop,
)
.unwrap();
assert_eq!(stats.retired_equations, 2);
assert_eq!(stats.opaque_frames, 0);
let reader = rewritten.snapshot().unwrap();
assert!(reader.get::<Blob<UnknownBlob>, _>(named).is_ok());
assert!(reader.get::<Blob<UnknownBlob>, _>(unrelated).is_err());
assert_eq!(reader.retired_collection_equations().count(), 2);
drop(reader);
let length = std::fs::metadata(&rewritten_path).unwrap().len();
source
.rewrite_retained_into(
&mut rewritten,
&RetentionRoots::new(),
WantRewritePolicy::Drop,
)
.unwrap();
assert_eq!(std::fs::metadata(&rewritten_path).unwrap().len(), length);
rewritten.close().unwrap();
assert_eq!(carried_frames(&rewritten_path), sorted_frames);
let reframed_path = fresh_empty_pile_path(&dir, "retired-live-reframed.pile");
let mut reframed = Pile::open(&reframed_path).unwrap();
let stats = reframe_into(&source_path, &mut reframed).unwrap();
assert_eq!(stats.retired_equations, 3);
reframed.close().unwrap();
assert_eq!(carried_frames(&reframed_path), sorted_frames);
source.close().unwrap();
}
#[test]
fn enveloped_collection_record_headers_are_fixed_zero_padded_and_roundtrip() {
let records = collection_test_records();
let expected = [
(record_kind::KIND_COLLECTION_COMMIT, 1, None),
(record_kind::KIND_COLLECTION_MERGE, 2, Some(320usize)),
(record_kind::KIND_COLLECTION_DERIVE, 1, None),
(record_kind::KIND_COLLECTION_MAP, 1, None),
];
let map = CollectionRecord::Map(CollectionMap::sign(
&SigningKey::from_bytes(&[7; 32]),
collection_test_collection(3),
collection_test_hash(8),
collection_test_hash(10),
));
let records = records.into_iter().chain([map]);
for (record, (kind, blocks, reserved_start)) in records.zip(expected) {
let header = collection_record_header(&record);
assert_eq!(header.len(), blocks as usize * ENVELOPE_BLOCK_LEN);
assert_eq!(&header[..FRAME_MAGIC_LEN], FRAME_MAGIC.as_slice());
assert_eq!(
u32::from_le_bytes(
header[FRAME_MAGIC_LEN..FRAME_BODY_OFFSET - 32]
.try_into()
.unwrap()
),
blocks
);
assert_eq!(
&header[FRAME_BODY_OFFSET - 32..FRAME_BODY_OFFSET],
kind.as_slice()
);
let decoded = decode_record(&header, 0).unwrap();
assert_eq!(decoded.len, header.len());
assert!(matches!(
decoded.content,
PileRecordContent::Collection { record: decoded } if decoded == record
));
let Some(reserved_start) = reserved_start else {
continue;
};
assert!(header[reserved_start..].iter().all(|byte| *byte == 0));
let mut nonzero_padding = header;
nonzero_padding[reserved_start] = 1;
assert!(matches!(
decode_record(&nonzero_padding, 0),
Err(ReadError::CorruptPile { valid_length: 0 })
));
}
}
#[test]
fn every_current_write_path_uses_the_generic_envelope() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "all-enveloped.pile");
let mut pile = Pile::open(&path).unwrap();
let blob_data = vec![0x42; ENVELOPE_BLOCK_LEN + 1];
let blob = pile
.put::<UnknownBlob, _>(Bytes::from_source(blob_data.clone()))
.unwrap();
let branch_id = Id::new([1; 16]).unwrap();
let branch_head = Inline::<Handle<SimpleArchive>>::new([2; 32]);
pile.append_legacy_pin_for_test(branch_id, None, Some(branch_head))
.unwrap();
pile.append_legacy_pin_for_test(branch_id, Some(branch_head), None)
.unwrap();
let wanted = Inline::<Handle<UnknownBlob>>::new([5; 32]);
pile.want(WantRequest::blob(wanted)).unwrap();
let collection_records = collection_test_records();
for record in &collection_records {
pile.insert(*record).unwrap();
}
pile.close().unwrap();
let expected = [
(record_kind::KIND_BLOB, 3u32),
(record_kind::KIND_PIN_HEAD, 1),
(record_kind::KIND_PIN_TOMBSTONE, 1),
(record_kind::KIND_WANT, 1),
(record_kind::KIND_COLLECTION_COMMIT, 1),
(record_kind::KIND_COLLECTION_MERGE, 2),
(record_kind::KIND_COLLECTION_DERIVE, 1),
];
let mut records = PileRecords::open(&path).unwrap();
let decoded = (&mut records).collect::<Result<Vec<_>, _>>().unwrap();
assert_eq!(decoded.len(), expected.len());
for (record, (kind, blocks)) in decoded.iter().zip(expected) {
let raw = &records.bytes()[record.offset..record.offset + record.len];
assert_eq!(&raw[..FRAME_MAGIC_LEN], FRAME_MAGIC.as_slice());
assert_eq!(
u32::from_le_bytes(
raw[FRAME_MAGIC_LEN..FRAME_BODY_OFFSET - 32]
.try_into()
.unwrap()
),
blocks
);
assert_eq!(
&raw[FRAME_BODY_OFFSET - 32..FRAME_BODY_OFFSET],
kind.as_slice()
);
assert_eq!(record.len, blocks as usize * ENVELOPE_BLOCK_LEN);
assert_eq!(record.offset % ENVELOPE_BLOCK_LEN, 0);
}
let blob_raw = &records.bytes()[decoded[0].offset..decoded[0].offset + decoded[0].len];
assert_eq!(&blob_raw[28..32], &3u32.to_le_bytes());
assert_eq!(&blob_raw[72..80], &(blob_data.len() as u64).to_le_bytes());
let mut reopened = Pile::open(&path).unwrap();
let fetched: Blob<UnknownBlob> = reopened.snapshot().unwrap().get(blob).unwrap();
assert_eq!(fetched.bytes.as_ref(), blob_data);
assert_eq!(reopened.legacy_pin_head_for_test(branch_id).unwrap(), None);
assert_eq!(
reopened
.snapshot()
.unwrap()
.wants()
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap(),
vec![WantRequest::blob(wanted)]
);
assert_eq!(
sorted_collection_records(
reopened
.snapshot()
.unwrap()
.records()
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap()
),
sorted_collection_records(collection_records)
);
reopened.close().unwrap();
}
#[test]
fn reframe_re_encodes_every_live_record_and_drops_only_inert_ones() {
let dir = tempfile::tempdir().unwrap();
let source_path = fresh_empty_pile_path(&dir, "mixed.pile");
let retired_derive = CollectionDeriveHeaderEnvelopeV1V4 {
envelope_marker: MAGIC_MARKER_ENVELOPE,
record_kind: MAGIC_MARKER_COLLECTION_DERIVE_V4,
span_blocks: ENVELOPE_HEADER_BLOCKS.to_le_bytes(),
source: [1; 32],
target: [2; 32],
input: [3; 32],
output: [4; 32],
reserved: [0; 92],
};
append_test_bytes(&source_path, retired_derive.as_bytes());
let payloads: Vec<Vec<u8>> = vec![vec![7u8; 5], vec![8u8; ENVELOPE_BLOCK_LEN + 3]];
let mut handles = Vec::new();
let mut source = Pile::open(&source_path).unwrap();
for payload in &payloads {
handles.push(
source
.put::<UnknownBlob, _>(Bytes::from_source(payload.clone()))
.unwrap(),
);
}
let moved = Id::new([21; 16]).unwrap();
let cleared = Id::new([22; 16]).unwrap();
let first = Inline::<Handle<SimpleArchive>>::new([31; 32]);
let second = Inline::<Handle<SimpleArchive>>::new([32; 32]);
source
.append_legacy_pin_for_test(moved, None, Some(first))
.unwrap();
source
.append_legacy_pin_for_test(moved, Some(first), Some(second))
.unwrap();
source
.append_legacy_pin_for_test(cleared, None, Some(first))
.unwrap();
source
.append_legacy_pin_for_test(cleared, Some(first), None)
.unwrap();
let current_want = WantRequest::blob(Inline::<Handle<UnknownBlob>>::new([41; 32]));
let retired_kept = WantRequest::blob(Inline::<Handle<UnknownBlob>>::new([42; 32]));
let retired_dropped = WantRequest::blob(Inline::<Handle<UnknownBlob>>::new([43; 32]));
source.want(current_want).unwrap();
let records = collection_test_records();
for record in &records {
source.insert(*record).unwrap();
}
let source_timestamps: Vec<u64> = {
let reader = source.snapshot().unwrap();
handles
.iter()
.map(|handle| reader.metadata(*handle).unwrap().unwrap().timestamp)
.collect()
};
source.close().unwrap();
append_test_bytes(
&source_path,
retired_blob_want_record(retired_kept, true).as_bytes(),
);
append_test_bytes(
&source_path,
retired_blob_want_record(retired_dropped, true).as_bytes(),
);
append_test_bytes(
&source_path,
retired_blob_want_record(retired_dropped, false).as_bytes(),
);
append_test_bytes(
&source_path,
retired_typed_derive_v1_record(
collection_test_collection(44),
collection_test_collection(45),
collection_test_hash(46),
true,
)
.as_bytes(),
);
append_test_bytes(
&source_path,
retired_typed_derive_v1_record(
collection_test_collection(47),
collection_test_collection(45),
collection_test_hash(46),
false,
)
.as_bytes(),
);
let dest_path = fresh_empty_pile_path(&dir, "reframed.pile");
let mut destination = Pile::open(&dest_path).unwrap();
let stats = reframe_into(&source_path, &mut destination).unwrap();
assert_eq!(stats.blobs, payloads.len());
assert_eq!(stats.pin_updates, 4);
assert_eq!(stats.wants, 2);
assert_eq!(stats.retired_want_records, 5);
assert_eq!(stats.collection_records, records.len());
assert_eq!(stats.dropped_inert, 1);
destination.close().unwrap();
let mut reframed = PileRecords::open(&dest_path).unwrap();
let decoded = (&mut reframed).collect::<Result<Vec<_>, _>>().unwrap();
for record in &decoded {
let raw = &reframed.bytes()[record.offset..record.offset + record.len];
assert_eq!(&raw[..FRAME_MAGIC_LEN], FRAME_MAGIC.as_slice());
assert_eq!(record.offset % ENVELOPE_BLOCK_LEN, 0);
}
let mut result = Pile::open(&dest_path).unwrap();
result.refresh().unwrap();
assert_eq!(result.opaque_record_count().unwrap(), 0);
assert_eq!(
result.legacy_pin_head_for_test(moved).unwrap(),
Some(second)
);
assert_eq!(result.legacy_pin_head_for_test(cleared).unwrap(), None);
assert_eq!(
result
.snapshot()
.unwrap()
.wants()
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap(),
vec![current_want, retired_kept]
);
assert_eq!(
sorted_collection_records(
result
.snapshot()
.unwrap()
.records()
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap()
),
sorted_collection_records(records)
);
let reader = result.snapshot().unwrap();
let newest_source = source_timestamps.iter().copied().max().unwrap();
for (handle, payload) in handles.iter().zip(&payloads) {
let blob: Blob<UnknownBlob> = reader.get(*handle).unwrap();
assert_eq!(blob.bytes.as_ref(), payload.as_slice());
assert!(reader.metadata(*handle).unwrap().unwrap().timestamp >= newest_source);
}
drop(reader);
result.close().unwrap();
}
#[test]
fn reframing_a_signed_commit_does_not_invalidate_it() {
let dir = tempfile::tempdir().unwrap();
let source_path = fresh_empty_pile_path(&dir, "signed.pile");
let mut source = Pile::open(&source_path).unwrap();
let signed: Vec<CollectionCommit> = collection_test_records()
.into_iter()
.filter_map(|record| match record {
CollectionRecord::Commit(commit) => Some(commit),
_ => None,
})
.collect();
assert!(!signed.is_empty());
for commit in &signed {
assert!(commit.verify_strict().is_ok());
source.insert(CollectionRecord::Commit(*commit)).unwrap();
}
source.close().unwrap();
let dest_path = fresh_empty_pile_path(&dir, "signed-reframed.pile");
let mut destination = Pile::open(&dest_path).unwrap();
reframe_into(&source_path, &mut destination).unwrap();
destination.close().unwrap();
let mut result = Pile::open(&dest_path).unwrap();
let mut verified = 0usize;
let snapshot = result.snapshot().unwrap();
for record in snapshot.records().unwrap() {
let CollectionRecord::Commit(commit) = record.unwrap() else {
continue;
};
commit
.verify_strict()
.expect("a re-encoded commit still verifies");
verified += 1;
}
assert_eq!(verified, signed.len());
result.close().unwrap();
}
#[test]
fn written_record_kinds_resolve_to_their_descriptions() {
use crate::inline::encodings::genid::GenId;
use crate::metadata;
use crate::prelude::find;
use crate::prelude::pattern;
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "self-describing.pile");
let mut pile = Pile::open(&path).unwrap();
let description_blob_count = description_blobs().len();
assert_eq!(
pile.publish_record_kind_descriptions().unwrap(),
description_blob_count
);
let branch_id = Id::new([3; 16]).unwrap();
pile.append_legacy_pin_for_test(branch_id, None, Some(Inline::new([4; 32])))
.unwrap();
pile.want(WantRequest::blob(Inline::<Handle<UnknownBlob>>::new(
[5; 32],
)))
.unwrap();
for record in &collection_test_records() {
pile.insert(*record).unwrap();
}
let reader = pile.snapshot().unwrap();
let mut records = PileRecords::open(&path).unwrap();
let decoded = (&mut records).collect::<Result<Vec<_>, _>>().unwrap();
let mut resolved = 0usize;
for record in &decoded {
let raw = &records.bytes()[record.offset..record.offset + record.len];
assert_eq!(&raw[..FRAME_MAGIC_LEN], FRAME_MAGIC.as_slice());
let kind: RawInline = raw[FRAME_BODY_OFFSET - 32..FRAME_BODY_OFFSET]
.try_into()
.unwrap();
let description: TribleSet = reader
.get::<TribleSet, SimpleArchive>(Inline::<Handle<SimpleArchive>>::new(kind))
.expect("every written record kind resolves in the pile that wrote it");
let tag: Inline<GenId> = crate::inline::IntoInline::to_inline(KIND_PILE_RECORD);
let named = find!(
(name: Inline<Handle<crate::blob::encodings::utf8string::UTF8String>>),
pattern!(&description, [{
metadata::tag: Inline::<GenId>::new(tag.raw),
metadata::name: ?name
}])
)
.count();
assert_eq!(named, 1, "a record kind describes exactly one record kind");
resolved += 1;
}
assert!(resolved >= 17);
drop(reader);
pile.close().unwrap();
}
#[test]
fn retained_rewrite_keeps_descriptions_of_every_emitted_non_blob_kind() {
let dir = tempfile::tempdir().unwrap();
let source_path = fresh_empty_pile_path(&dir, "kind-retention-source.pile");
let destination_path = fresh_empty_pile_path(&dir, "kind-retention-destination.pile");
let mut source = Pile::open(&source_path).unwrap();
let mut destination = Pile::open(&destination_path).unwrap();
source.publish_record_kind_descriptions().unwrap();
source
.append_legacy_pin_for_test(collection_test_id(71), None, Some(Inline::new([72; 32])))
.unwrap();
source
.want(WantRequest::blob(Inline::<Handle<UnknownBlob>>::new(
[73; 32],
)))
.unwrap();
let collection_records = collection_test_records();
for record in collection_records.iter().copied() {
source.insert(record).unwrap();
}
let proof = capability_fixture(74, [75; 32]);
source.insert_proof(proof).unwrap();
source
.rewrite_retained_into(
&mut destination,
&RetentionRoots::new(),
WantRewritePolicy::Preserve,
)
.unwrap();
let reader = destination.snapshot().unwrap();
let mut expected = vec![
pin_head_record_kind(),
want_record_kind(),
capability_proof_record_kind(),
];
expected.extend(collection_records.into_iter().map(collection_record_kind));
for kind in expected {
assert_record_kind_description_resident(&reader, kind);
}
assert_record_kind_description_resident(&reader, blob_record_kind());
drop(reader);
destination.close().unwrap();
source.close().unwrap();
}
#[test]
fn blob_frames_do_not_self_root_the_blob_kind_description() {
let dir = tempfile::tempdir().unwrap();
let source_path = fresh_empty_pile_path(&dir, "blob-kind-source.pile");
let empty_destination_path = fresh_empty_pile_path(&dir, "blob-kind-empty.pile");
let rooted_destination_path = fresh_empty_pile_path(&dir, "blob-kind-rooted.pile");
let mut source = Pile::open(&source_path).unwrap();
let mut empty_destination = Pile::open(&empty_destination_path).unwrap();
let mut rooted_destination = Pile::open(&rooted_destination_path).unwrap();
source.publish_record_kind_descriptions().unwrap();
let payload = source
.put::<UnknownBlob, _>(Bytes::from_source(b"independently rooted".to_vec()))
.unwrap();
let stats = source
.rewrite_retained_into(
&mut empty_destination,
&RetentionRoots::new(),
WantRewritePolicy::Drop,
)
.unwrap();
assert_eq!(stats.retained_blobs, 0);
assert!(!empty_destination
.snapshot()
.unwrap()
.contains_blob(blob_record_kind())
.unwrap());
let mut roots = RetentionRoots::new();
roots.retain_direct(payload);
source
.rewrite_retained_into(&mut rooted_destination, &roots, WantRewritePolicy::Drop)
.unwrap();
let reader = rooted_destination.snapshot().unwrap();
assert!(reader.contains_blob(payload).unwrap());
assert_record_kind_description_resident(&reader, blob_record_kind());
drop(reader);
empty_destination.close().unwrap();
rooted_destination.close().unwrap();
source.close().unwrap();
}
#[test]
fn dropped_wants_do_not_root_their_historical_kind_description() {
let dir = tempfile::tempdir().unwrap();
let source_path = fresh_empty_pile_path(&dir, "dropped-want-kind-source.pile");
let destination_path = fresh_empty_pile_path(&dir, "dropped-want-kind-destination.pile");
let mut source = Pile::open(&source_path).unwrap();
let mut destination = Pile::open(&destination_path).unwrap();
source.publish_record_kind_descriptions().unwrap();
source
.want(WantRequest::blob(Inline::<Handle<UnknownBlob>>::new(
[76; 32],
)))
.unwrap();
source
.rewrite_retained_into(
&mut destination,
&RetentionRoots::new(),
WantRewritePolicy::Drop,
)
.unwrap();
let reader = destination.snapshot().unwrap();
assert!(reader.wants().unwrap().next().is_none());
assert!(!reader.contains_blob(want_record_kind()).unwrap());
assert!(!reader.contains_blob(blob_record_kind()).unwrap());
drop(reader);
destination.close().unwrap();
source.close().unwrap();
}
#[test]
fn envelope_numeric_fields_are_canonical_little_endian() {
let header = BlobRecordHeader::new(
0x0102_0304,
0x1112_1314_1516_1718,
0x2122_2324_2526_2728,
collection_test_hash(9),
);
let bytes = header.as_bytes();
assert_eq!(&bytes[28..32], &0x0102_0304u32.to_le_bytes());
assert_eq!(&bytes[64..72], &0x1112_1314_1516_1718u64.to_le_bytes());
assert_eq!(&bytes[72..80], &0x2122_2324_2526_2728u64.to_le_bytes());
assert_eq!(envelope_blocks_for_payload(0), Some(1));
assert_eq!(envelope_blocks_for_payload(255), Some(2));
assert_eq!(envelope_blocks_for_payload(256), Some(2));
assert_eq!(envelope_blocks_for_payload(257), Some(3));
#[cfg(target_pointer_width = "64")]
{
let largest_payload = (u32::MAX as usize - 1) * ENVELOPE_BLOCK_LEN;
assert_eq!(envelope_blocks_for_payload(largest_payload), Some(u32::MAX));
assert_eq!(envelope_blocks_for_payload(largest_payload + 1), None);
}
assert_eq!(envelope_blocks_for_payload(usize::MAX), None);
}
#[test]
fn legacy_v4_commit_remains_current_and_unsigned_equations_are_inert() {
for record in collection_test_records() {
let mut header = [0u8; V3_HEADER_LEN];
match record {
CollectionRecord::Commit(commit) => {
let (signature_r, signature_s) = commit.signature();
header.copy_from_slice(
CollectionCommitHeaderV4 {
magic_marker: MAGIC_MARKER_COLLECTION_COMMIT_V4,
collection: commit.collection().raw,
data: commit.data().raw,
metadata: commit.metadata().raw,
public_key: commit.public_key().raw,
signature_r: signature_r.raw,
signature_s: signature_s.raw,
reserved: [0; 48],
}
.as_bytes(),
);
}
CollectionRecord::Merge(merge) => {
let &[low, high] = merge.inputs() else {
unreachable!("binary fixture merge")
};
header.copy_from_slice(
CollectionMergeHeaderV4 {
magic_marker: MAGIC_MARKER_COLLECTION_MERGE_V4,
collection: merge.collection().raw,
low: low.raw,
high: high.raw,
result: merge.result().raw,
reserved: [0; 112],
}
.as_bytes(),
);
}
CollectionRecord::Derive(derive) => {
let (input, output) = (derive.input(), derive.output());
header.copy_from_slice(
CollectionDeriveHeaderV4 {
magic_marker: MAGIC_MARKER_COLLECTION_DERIVE_V4,
source: [0; 32],
target: derive.collection().raw,
input: input.raw(),
output: output.raw,
reserved: [0; 112],
}
.as_bytes(),
);
}
CollectionRecord::Map(_) => unreachable!("the fixture holds no MAP"),
}
let original = header;
let decoded = decode_record(&header, 0).unwrap();
assert_eq!(decoded.len, V3_HEADER_LEN);
match record {
CollectionRecord::Derive(_) => assert!(matches!(
decoded.content,
PileRecordContent::RetiredCollectionDeriveV4
)),
CollectionRecord::Merge(merge) => {
let &[low, high] = merge.inputs() else {
unreachable!("binary fixture merge")
};
let expected = LegacyUnsignedCollectionEquation::Merge {
collection: merge.collection(),
low,
high,
result: merge.result(),
};
assert!(matches!(decoded.content,
PileRecordContent::LegacyUnsignedCollectionEquation { equation }
if equation == expected
));
}
CollectionRecord::Commit(_) => assert!(matches!(
decoded.content,
PileRecordContent::Collection { record: decoded } if decoded == record
)),
CollectionRecord::Map(_) => unreachable!("the fixture holds no MAP"),
}
assert_eq!(header, original);
}
}
#[test]
fn signed_native_replay_and_reframe_preserve_bad_signature_evidence() {
let dir = tempfile::tempdir().unwrap();
let source_path = fresh_empty_pile_path(&dir, "signed-audit-source.pile");
for record in collection_test_records() {
let mut header = collection_record_header(&record);
header[FRAME_BODY_OFFSET + record.to_bytes().len() - 2] ^= 1;
append_test_bytes(&source_path, &header);
}
let mut source = Pile::open(&source_path).unwrap();
let evidence = source
.snapshot()
.unwrap()
.records()
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap();
assert_eq!(evidence.len(), 3);
for record in &evidence {
assert!(record.verify_strict().is_err());
assert!(CollectionRecord::from_bytes(&record.to_bytes()).is_err());
}
let destination_path = fresh_empty_pile_path(&dir, "signed-audit-reframed.pile");
let mut destination = Pile::open(&destination_path).unwrap();
let stats = reframe_into(&source_path, &mut destination).unwrap();
assert_eq!(stats.collection_records, 3);
assert_eq!(
destination
.snapshot()
.unwrap()
.records()
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap(),
evidence
);
destination.close().unwrap();
source.close().unwrap();
}
#[test]
fn reframe_rejects_opaque_ownership_before_any_output() {
let dir = tempfile::tempdir().unwrap();
let source_path = fresh_empty_pile_path(&dir, "opaque-reframe-source.pile");
let destination_path = fresh_empty_pile_path(&dir, "opaque-reframe-destination.pile");
append_test_bytes(
&source_path,
&collection_record_header(&collection_test_records()[0]),
);
append_test_bytes(
&source_path,
&test_envelope_bytes(TEST_UNKNOWN_KIND_A, 1, ENVELOPE_BLOCK_LEN),
);
let mut destination = Pile::open(&destination_path).unwrap();
assert!(matches!(
reframe_into(&source_path, &mut destination),
Err(PileReframeError::OpaqueRecord {
offset: ENVELOPE_BLOCK_LEN,
..
})
));
assert_eq!(std::fs::metadata(&destination_path).unwrap().len(), 0);
destination.close().unwrap();
}
#[test]
fn unsigned_equations_survive_rewrite_with_resident_dependency_closure() {
let dir = tempfile::tempdir().unwrap();
let source_path = fresh_empty_pile_path(&dir, "unsigned-source.pile");
let mut source = Pile::open(&source_path).unwrap();
let child = source
.put::<UnknownBlob, _>(Bytes::from_source(b"legacy child".to_vec()))
.unwrap();
let input = source
.put::<UnknownBlob, _>(Bytes::from_source(child.raw.to_vec()))
.unwrap();
let output = source
.put::<UnknownBlob, _>(Bytes::from_source(b"legacy output".to_vec()))
.unwrap();
let collection = source
.put::<SimpleArchive, _>(TribleSet::new().to_blob())
.unwrap();
let orphan = source
.put::<UnknownBlob, _>(Bytes::from_source(b"unowned".to_vec()))
.unwrap();
source.close().unwrap();
let merge = LegacyCollectionMergeRecordHeader {
magic: FRAME_MAGIC,
span_blocks: ENVELOPE_HEADER_BLOCKS.to_le_bytes(),
record_kind: record_kind::KIND_COLLECTION_MERGE_UNSIGNED,
collection: collection.raw,
low: [0; 32],
high: input.raw,
result: output.raw,
reserved: [0; 64],
};
let derive = CollectionDeriveHeaderEnvelopeV1 {
envelope_marker: MAGIC_MARKER_ENVELOPE,
record_kind: MAGIC_MARKER_COLLECTION_DERIVE_V5,
span_blocks: ENVELOPE_HEADER_BLOCKS.to_le_bytes(),
target: collection.raw,
input: input.raw,
output: output.raw,
reserved: [0; 124],
};
append_test_bytes(&source_path, merge.as_bytes());
append_test_bytes(&source_path, derive.as_bytes());
let mut source = Pile::open(&source_path).unwrap();
let before = source.snapshot().unwrap();
assert_eq!(before.legacy_unsigned_collection_equations().count(), 2);
assert!(before.records().unwrap().next().is_none());
assert_eq!(before.opaque_record_count(), 0);
let original_headers = [
fixed_collection_header(merge.as_bytes()),
fixed_collection_header(derive.as_bytes()),
];
for retained in [false, true] {
let path = fresh_empty_pile_path(
&dir,
if retained {
"unsigned-retained.pile"
} else {
"unsigned-reframed.pile"
},
);
let mut destination = Pile::open(&path).unwrap();
if retained {
source
.rewrite_retained_into(
&mut destination,
&RetentionRoots::new(),
WantRewritePolicy::Drop,
)
.unwrap();
} else {
let stats = reframe_into(&source_path, &mut destination).unwrap();
assert_eq!(stats.legacy_unsigned_equations, 2);
assert_eq!(stats.collection_records, 0);
}
let reader = destination.snapshot().unwrap();
assert!(reader.records().unwrap().next().is_none());
assert_eq!(reader.legacy_unsigned_collection_equations().count(), 2);
for handle in [child, input, output, collection.transmute()] {
assert!(reader.contains_blob(handle).unwrap());
}
assert_eq!(reader.contains_blob(orphan).unwrap(), !retained);
for header in original_headers {
assert!(reader.legacy_collection_headers.get(&header).is_some());
}
drop(reader);
destination.close().unwrap();
}
source.close().unwrap();
}
#[test]
fn opaque_envelopes_are_raw_visible_and_writers_cross_them() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "opaque-crossing.pile");
let header_only = test_envelope_bytes(TEST_UNKNOWN_KIND_A, 1, ENVELOPE_BLOCK_LEN);
let multi_block = test_envelope_bytes(TEST_UNKNOWN_KIND_B, 2, 2 * ENVELOPE_BLOCK_LEN);
let mut pile = Pile::open(&path).unwrap();
pile.refresh().unwrap();
{
let mut external = OpenOptions::new().append(true).open(&path).unwrap();
external.write_all(&header_only).unwrap();
external.write_all(&multi_block).unwrap();
external.sync_all().unwrap();
}
let known_payload = b"known after opaque".to_vec();
let known = pile
.put::<UnknownBlob, _>(Bytes::from_source(known_payload.clone()))
.unwrap();
let branch_id = Id::new([9; 16]).unwrap();
pile.append_legacy_pin_for_test(branch_id, None, Some(known.transmute()))
.unwrap();
pile.close().unwrap();
let mut records = PileRecords::open(&path).unwrap();
let decoded = (&mut records).collect::<Result<Vec<_>, _>>().unwrap();
assert_eq!(decoded.len(), 4);
assert!(matches!(
decoded[0],
PileRecord {
len: ENVELOPE_BLOCK_LEN,
content: PileRecordContent::Opaque {
kind: OpaqueKind::Described(TEST_UNKNOWN_KIND_A)
},
..
}
));
assert!(matches!(
decoded[1],
PileRecord {
len,
content: PileRecordContent::Opaque {
kind: OpaqueKind::Described(TEST_UNKNOWN_KIND_B)
},
..
} if len == 2 * ENVELOPE_BLOCK_LEN
));
assert!(matches!(decoded[2].content, PileRecordContent::Blob { .. }));
assert!(matches!(
decoded[3].content,
PileRecordContent::Branch { .. }
));
assert_eq!(&records.bytes()[..ENVELOPE_BLOCK_LEN], &header_only);
assert_eq!(
&records.bytes()[ENVELOPE_BLOCK_LEN..3 * ENVELOPE_BLOCK_LEN],
&multi_block
);
let mut reopened = Pile::open(&path).unwrap();
reopened.refresh().unwrap();
assert_eq!(reopened.opaque_records, 2);
let fetched: Blob<UnknownBlob> = reopened.snapshot().unwrap().get(known).unwrap();
assert_eq!(fetched.bytes.as_ref(), known_payload);
assert_eq!(
reopened.legacy_pin_head_for_test(branch_id).unwrap(),
Some(known.transmute())
);
reopened.close().unwrap();
}
#[test]
fn retired_local_cell_records_are_opaque_migration_evidence() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "retired-local-cells.pile");
let mut unenveloped = [0u8; V3_HEADER_LEN];
unenveloped[..16].copy_from_slice(&MAGIC_MARKER_LOCAL_CELL_V3);
let enveloped = test_envelope_v1_bytes(
MAGIC_MARKER_LOCAL_CELL_TOMBSTONE_V3,
ENVELOPE_HEADER_BLOCKS,
ENVELOPE_HEADER_LEN,
);
append_test_bytes(&path, &unenveloped);
append_test_bytes(&path, &enveloped);
let mut records = PileRecords::open(&path).unwrap();
let decoded = (&mut records).collect::<Result<Vec<_>, _>>().unwrap();
assert_eq!(decoded.len(), 2);
assert!(matches!(
decoded[0].content,
PileRecordContent::Opaque {
kind: OpaqueKind::Legacy(MAGIC_MARKER_LOCAL_CELL_V3)
}
));
assert!(matches!(
decoded[1].content,
PileRecordContent::Opaque {
kind: OpaqueKind::Legacy(MAGIC_MARKER_LOCAL_CELL_TOMBSTONE_V3)
}
));
assert_eq!(decoded[0].len, V3_HEADER_LEN);
assert_eq!(decoded[1].len, ENVELOPE_HEADER_LEN);
let mut pile = Pile::open(&path).unwrap();
pile.refresh().unwrap();
assert_eq!(pile.opaque_record_count().unwrap(), 2);
pile.close().unwrap();
}
#[test]
fn retired_v4_derive_is_known_inert_and_does_not_block_retention() {
let dir = tempfile::tempdir().unwrap();
let source_path = fresh_empty_pile_path(&dir, "retired-v4-derive.pile");
let destination_path = fresh_empty_pile_path(&dir, "retired-v4-destination.pile");
let header = CollectionDeriveHeaderEnvelopeV1V4 {
envelope_marker: MAGIC_MARKER_ENVELOPE,
record_kind: MAGIC_MARKER_COLLECTION_DERIVE_V4,
span_blocks: ENVELOPE_HEADER_BLOCKS.to_le_bytes(),
source: [1; 32],
target: [2; 32],
input: [3; 32],
output: [4; 32],
reserved: [0; 92],
};
append_test_bytes(&source_path, header.as_bytes());
let mut records = PileRecords::open(&source_path).unwrap();
let decoded = records.next().unwrap().unwrap();
assert_eq!(decoded.len, ENVELOPE_HEADER_LEN);
assert!(matches!(
decoded.content,
PileRecordContent::RetiredCollectionDeriveV4
));
assert!(records.next().is_none());
let mut source = Pile::open(&source_path).unwrap();
assert_eq!(source.opaque_record_count().unwrap(), 0);
assert!(source
.snapshot()
.unwrap()
.records()
.unwrap()
.next()
.is_none());
let mut destination = Pile::open(&destination_path).unwrap();
source
.rewrite_retained_into(
&mut destination,
&RetentionRoots::new(),
WantRewritePolicy::Drop,
)
.unwrap();
destination.close().unwrap();
source.close().unwrap();
assert_eq!(std::fs::metadata(destination_path).unwrap().len(), 0);
}
#[test]
fn truncated_retired_unenveloped_cell_is_corrupt_not_an_applied_record() {
for kind in [
MAGIC_MARKER_LOCAL_CELL_V3,
MAGIC_MARKER_LOCAL_CELL_TOMBSTONE_V3,
] {
for len in [16usize, 17, V3_HEADER_LEN - 1] {
let mut bytes = vec![0u8; len];
bytes[..16].copy_from_slice(&kind);
assert!(matches!(
decode_record(&bytes, 37),
Err(ReadError::CorruptPile { valid_length: 37 })
));
}
}
}
#[test]
fn retired_enveloped_cell_requires_its_historical_fixed_span() {
for kind in [
MAGIC_MARKER_LOCAL_CELL_V3,
MAGIC_MARKER_LOCAL_CELL_TOMBSTONE_V3,
] {
let bytes = test_envelope_v1_bytes(kind, 2, 2 * ENVELOPE_BLOCK_LEN);
assert!(matches!(
decode_record(&bytes, 41),
Err(ReadError::CorruptPile { valid_length: 41 })
));
}
}
#[test]
fn opaque_projection_preserves_branches_and_cutover_resolves_retired_want_order() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "opaque-lww.pile");
let opaque = test_envelope_bytes(TEST_UNKNOWN_KIND_A, 1, ENVELOPE_BLOCK_LEN);
let mut pile = Pile::open(&path).unwrap();
let branch_cleared = Id::new([11; 16]).unwrap();
let branch_restored = Id::new([12; 16]).unwrap();
let branch_head = Inline::<Handle<SimpleArchive>>::new([13; 32]);
pile.append_legacy_pin_for_test(branch_cleared, None, Some(branch_head))
.unwrap();
append_test_bytes(&path, &opaque);
pile.append_legacy_pin_for_test(branch_cleared, Some(branch_head), None)
.unwrap();
append_test_bytes(
&path,
PinTombstoneRecordHeader::new(branch_restored).as_bytes(),
);
append_test_bytes(&path, &opaque);
pile.append_legacy_pin_for_test(branch_restored, None, Some(branch_head))
.unwrap();
let want_retracted = Inline::<Handle<UnknownBlob>>::new([17; 32]);
let want_restored = Inline::<Handle<UnknownBlob>>::new([18; 32]);
append_test_bytes(
&path,
retired_blob_want_record(WantRequest::blob(want_retracted), true).as_bytes(),
);
append_test_bytes(&path, &opaque);
append_test_bytes(
&path,
retired_blob_want_record(WantRequest::blob(want_retracted), false).as_bytes(),
);
append_test_bytes(
&path,
retired_blob_want_record(WantRequest::blob(want_restored), false).as_bytes(),
);
append_test_bytes(&path, &opaque);
append_test_bytes(
&path,
retired_blob_want_record(WantRequest::blob(want_restored), true).as_bytes(),
);
pile.close().unwrap();
let mut reopened = Pile::open(&path).unwrap();
reopened.refresh().unwrap();
assert_eq!(reopened.opaque_record_count().unwrap(), 4);
assert_eq!(
reopened.legacy_pin_head_for_test(branch_cleared).unwrap(),
None
);
assert_eq!(
reopened.legacy_pin_head_for_test(branch_restored).unwrap(),
Some(branch_head)
);
assert!(reopened
.snapshot()
.unwrap()
.wants()
.unwrap()
.next()
.is_none());
assert_eq!(
reopened.want_cutover_status().unwrap(),
WantCutoverStatus {
retired_records: 4,
resolved_active: 1,
already_current: 0,
missing_current: 1,
}
);
reopened.migrate_retired_wants().unwrap();
let wants = reopened
.snapshot()
.unwrap()
.wants()
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap();
assert_eq!(wants, vec![WantRequest::blob(want_restored)]);
reopened.close().unwrap();
}
#[test]
fn legacy_v3_and_enveloped_piles_concatenate_without_reframing() {
let dir = tempfile::tempdir().unwrap();
let legacy_path = fresh_empty_pile_path(&dir, "legacy-v3.pile");
let current_path = fresh_empty_pile_path(&dir, "enveloped.pile");
let merged_path = dir.path().join("mixed-cat.pile");
let legacy_payload = b"legacy V3".to_vec();
let legacy_handle =
Blob::<UnknownBlob>::new(Bytes::from_source(legacy_payload.clone())).get_handle();
append_v3_blob_candidate(&legacy_path, legacy_handle.into(), &legacy_payload, 17);
let current_payload = b"current envelope".to_vec();
let mut current = Pile::open(¤t_path).unwrap();
let current_handle = current
.put::<UnknownBlob, _>(Bytes::from_source(current_payload.clone()))
.unwrap();
current.close().unwrap();
let mut merged = std::fs::read(&legacy_path).unwrap();
merged.extend_from_slice(&std::fs::read(¤t_path).unwrap());
std::fs::write(&merged_path, merged).unwrap();
let mut pile = Pile::open(&merged_path).unwrap();
pile.refresh().unwrap();
let reader = pile.snapshot().unwrap();
let legacy: Blob<UnknownBlob> = reader.get(legacy_handle).unwrap();
let current: Blob<UnknownBlob> = reader.get(current_handle).unwrap();
assert_eq!(legacy.bytes.as_ref(), legacy_payload);
assert_eq!(current.bytes.as_ref(), current_payload);
drop(reader);
pile.close().unwrap();
let mut records = PileRecords::open(&merged_path).unwrap();
let decoded = (&mut records).collect::<Result<Vec<_>, _>>().unwrap();
assert_eq!(decoded.len(), 2);
assert_eq!(
&records.bytes()[decoded[0].offset..decoded[0].offset + 16],
MAGIC_MARKER_BLOB_V3.as_slice()
);
assert_eq!(
&records.bytes()[decoded[1].offset..decoded[1].offset + FRAME_MAGIC_LEN],
FRAME_MAGIC.as_slice()
);
}
#[test]
fn envelope_span_rejects_zero_maximum_truncation_and_kind_mismatch() {
let complete_header = test_envelope_bytes(TEST_UNKNOWN_KIND_A, 1, ENVELOPE_HEADER_LEN);
for truncated_at in [0usize, 1, 15, 16, 27, 28, 31, 32, 63, 64, 255] {
assert!(matches!(
decode_record(&complete_header[..truncated_at], 11),
Err(ReadError::CorruptPile { valid_length: 11 })
));
}
for malformed in [
test_envelope_bytes(TEST_UNKNOWN_KIND_A, 0, ENVELOPE_BLOCK_LEN),
test_envelope_bytes(TEST_UNKNOWN_KIND_A, u32::MAX, ENVELOPE_BLOCK_LEN),
test_envelope_bytes(TEST_UNKNOWN_KIND_A, 2, 2 * ENVELOPE_BLOCK_LEN - 1),
test_envelope_bytes(record_kind::KIND_PIN_HEAD, 2, 2 * ENVELOPE_BLOCK_LEN),
] {
assert!(matches!(
decode_record(&malformed, 17),
Err(ReadError::CorruptPile { valid_length: 17 })
));
}
let prefix_only = test_envelope_bytes(TEST_UNKNOWN_KIND_A, 1, FRAME_BODY_OFFSET);
assert!(matches!(
decode_record(&prefix_only, 23),
Err(ReadError::CorruptPile { valid_length: 23 })
));
let hash = collection_test_hash(7);
let mut wrong_blob_span = vec![0u8; 2 * ENVELOPE_BLOCK_LEN];
wrong_blob_span[..ENVELOPE_HEADER_LEN].copy_from_slice(
BlobRecordHeader::new(1, 42, (ENVELOPE_BLOCK_LEN + 1) as u64, hash).as_bytes(),
);
assert!(matches!(
decode_record(&wrong_blob_span, 29),
Err(ReadError::CorruptPile { valid_length: 29 })
));
}
#[test]
fn amputation_crosses_complete_opaque_and_truncates_torn_opaque_tail() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "opaque-amputation.pile");
let complete = test_envelope_bytes(TEST_UNKNOWN_KIND_A, 2, 2 * ENVELOPE_BLOCK_LEN);
let torn = test_envelope_bytes(TEST_UNKNOWN_KIND_B, 2, ENVELOPE_BLOCK_LEN);
{
let mut file = OpenOptions::new().append(true).open(&path).unwrap();
file.write_all(&complete).unwrap();
file.write_all(&torn).unwrap();
}
let mut pile = Pile::open(&path).unwrap();
assert!(matches!(
pile.refresh(),
Err(ReadError::CorruptPile { valid_length })
if valid_length == complete.len()
));
pile.amputate().unwrap();
assert_eq!(
std::fs::metadata(&path).unwrap().len(),
complete.len() as u64
);
pile.refresh().unwrap();
assert_eq!(pile.opaque_records, 1);
pile.close().unwrap();
let mut records = PileRecords::open(&path).unwrap();
let only = records.next().unwrap().unwrap();
assert_eq!(only.len, complete.len());
assert!(matches!(
only.content,
PileRecordContent::Opaque {
kind: OpaqueKind::Described(TEST_UNKNOWN_KIND_A)
}
));
assert!(records.next().is_none());
}
#[test]
fn amputation_at_refuses_a_stale_boundary_before_truncation() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "boundary-guarded-amputation.pile");
let complete = test_envelope_bytes(TEST_UNKNOWN_KIND_A, 2, 2 * ENVELOPE_BLOCK_LEN);
let torn = test_envelope_bytes(TEST_UNKNOWN_KIND_B, 2, ENVELOPE_BLOCK_LEN);
let mut bytes = complete.clone();
bytes.extend_from_slice(&torn);
std::fs::write(&path, &bytes).unwrap();
let mut pile = Pile::open(&path).unwrap();
assert!(matches!(
pile.amputate_at(0),
Err(ReadError::CorruptPile { valid_length })
if valid_length == complete.len()
));
assert_eq!(std::fs::read(&path).unwrap(), bytes);
assert!(pile.amputate_at(complete.len()).unwrap());
assert_eq!(std::fs::read(&path).unwrap(), complete);
pile.close().unwrap();
}
#[test]
fn opaque_frame_widens_retention_to_every_resident_blob() {
let dir = tempfile::tempdir().unwrap();
for planted in [false, true] {
let source_path = fresh_empty_pile_path(&dir, &format!("widen-{planted}-source.pile"));
let destination_path =
fresh_empty_pile_path(&dir, &format!("widen-{planted}-destination.pile"));
let mut source = Pile::open(&source_path).unwrap();
let unrooted = source
.put::<UnknownBlob, _>(Bytes::from_source(b"named by no known record".to_vec()))
.unwrap();
source.flush().unwrap();
if planted {
append_test_bytes(
&source_path,
&test_envelope_bytes(TEST_UNKNOWN_KIND_A, 1, ENVELOPE_BLOCK_LEN),
);
}
let mut destination = Pile::open(&destination_path).unwrap();
let stats = source
.rewrite_retained_into(
&mut destination,
&RetentionRoots::new(),
WantRewritePolicy::Drop,
)
.unwrap();
assert_eq!(stats.opaque_frames, usize::from(planted));
assert_eq!(stats.retained_blobs, usize::from(planted));
let kept = destination
.snapshot()
.unwrap()
.get::<Blob<UnknownBlob>, _>(unrooted)
.is_ok();
assert_eq!(kept, planted);
destination.close().unwrap();
source.close().unwrap();
}
}
#[test]
fn opaque_frames_are_carried_by_rewrite_and_refused_by_yard() {
let dir = tempfile::tempdir().unwrap();
let source_path = fresh_empty_pile_path(&dir, "opaque-source.pile");
let destination_path = fresh_empty_pile_path(&dir, "opaque-destination.pile");
std::fs::write(
&source_path,
test_envelope_bytes(TEST_UNKNOWN_KIND_A, 1, ENVELOPE_BLOCK_LEN),
)
.unwrap();
let mut source = Pile::open(&source_path).unwrap();
let retained = source
.put::<UnknownBlob, _>(Bytes::from_source(b"possibly owned".to_vec()))
.unwrap();
let mut destination = Pile::open(&destination_path).unwrap();
destination
.put::<UnknownBlob, _>(Bytes::from_source(b"sentinel".to_vec()))
.unwrap();
destination.flush().unwrap();
let destination_before = std::fs::read(&destination_path).unwrap();
let frame = test_envelope_bytes(TEST_UNKNOWN_KIND_A, 1, ENVELOPE_BLOCK_LEN);
let stats = source
.rewrite_retained_into(
&mut destination,
&RetentionRoots::new(),
WantRewritePolicy::Drop,
)
.unwrap();
assert_eq!(stats.opaque_frames, 1);
assert_eq!(stats.retained_blobs, 1);
let destination_after = std::fs::read(&destination_path).unwrap();
assert!(destination_after.starts_with(&destination_before));
assert!(destination_after.ends_with(&frame));
let frame_offset = destination_after.len() - frame.len();
assert_eq!(destination.opaque_records, 1);
assert_eq!(destination.opaque_frames, vec![(frame_offset, frame.len())]);
let carried: Blob<UnknownBlob> = destination.snapshot().unwrap().get(retained).unwrap();
assert_eq!(carried.bytes.as_ref(), b"possibly owned");
let stats = source
.rewrite_retained_into(
&mut destination,
&RetentionRoots::new(),
WantRewritePolicy::Drop,
)
.unwrap();
assert_eq!(stats.opaque_frames, 1);
assert_eq!(std::fs::read(&destination_path).unwrap(), destination_after);
let fetched: Blob<UnknownBlob> = source.snapshot().unwrap().get(retained).unwrap();
assert_eq!(fetched.bytes.as_ref(), b"possibly owned");
destination.close().unwrap();
source.close().unwrap();
let mut reopened = Pile::open(&destination_path).unwrap();
reopened.refresh().unwrap();
assert_eq!(reopened.opaque_records, 1);
assert_eq!(reopened.opaque_frames, vec![(frame_offset, frame.len())]);
reopened.close().unwrap();
let old_path = fresh_empty_pile_path(&dir, "opaque-owned-old.pile");
let mut old = Pile::open(&old_path).unwrap();
let cross_generation = old
.put::<UnknownBlob, _>(Bytes::from_source(
b"possibly owned across generations".to_vec(),
))
.unwrap();
old.close().unwrap();
let young_before = std::fs::read(&source_path).unwrap();
let old_before = std::fs::read(&old_path).unwrap();
let mut yard = Yard::open([&source_path, &old_path], YardConfig::default()).unwrap();
assert!(matches!(
yard.collect(&RetentionRoots::new()),
Err(YardCollectError::OpaqueRecords { count: 1 })
));
assert!(matches!(
yard.compact(&RetentionRoots::new()),
Err(YardCollectError::OpaqueRecords { count: 1 })
));
assert!(matches!(
yard.reclaim(),
Err(YardReclaimError::OpaqueRecords { count: 1 })
));
assert_eq!(std::fs::read(&source_path).unwrap(), young_before);
assert_eq!(std::fs::read(&old_path).unwrap(), old_before);
let fetched: Blob<UnknownBlob> = yard.snapshot().unwrap().get(retained).unwrap();
assert_eq!(fetched.bytes.as_ref(), b"possibly owned");
let fetched: Blob<UnknownBlob> = yard.snapshot().unwrap().get(cross_generation).unwrap();
assert_eq!(fetched.bytes.as_ref(), b"possibly owned across generations");
yard.close().unwrap();
}
#[test]
fn legacy_v3_collection_headers_are_inert_and_preserved_by_rewrite() {
let dir = tempfile::tempdir().unwrap();
let source_path = fresh_empty_pile_path(&dir, "legacy-source.pile");
let destination_path = fresh_empty_pile_path(&dir, "legacy-destination.pile");
let expected = legacy_collection_test_headers();
{
let mut file = OpenOptions::new().append(true).open(&source_path).unwrap();
for (_, header) in &expected {
file.write_all(header).unwrap();
}
}
assert_eq!(
legacy_collection_headers_at(&source_path)
.into_iter()
.collect::<BTreeSet<_>>(),
expected.iter().copied().collect::<BTreeSet<_>>()
);
let mut malformed = expected[0].1;
malformed[64] = 1;
assert!(matches!(
decode_record(&malformed, 0),
Err(ReadError::CorruptPile { valid_length: 0 })
));
let mut source = Pile::open(&source_path).unwrap();
let mut destination = Pile::open(&destination_path).unwrap();
assert_eq!(source.snapshot().unwrap().records().unwrap().count(), 0);
let stats = source
.rewrite_retained_into(
&mut destination,
&RetentionRoots::new(),
WantRewritePolicy::Drop,
)
.unwrap();
assert_eq!(stats.retained_blobs, 0);
assert_eq!(
destination.snapshot().unwrap().records().unwrap().count(),
0
);
let rewritten = legacy_collection_headers_at(&destination_path);
assert_eq!(
rewritten.into_iter().collect::<BTreeSet<_>>(),
expected.iter().copied().collect::<BTreeSet<_>>()
);
let once = std::fs::metadata(&destination_path).unwrap().len();
source
.rewrite_retained_into(
&mut destination,
&RetentionRoots::new(),
WantRewritePolicy::Drop,
)
.unwrap();
assert_eq!(std::fs::metadata(&destination_path).unwrap().len(), once);
destination.close().unwrap();
source.close().unwrap();
}
#[test]
fn reframe_preserves_earlier_v3_collection_evidence_without_interpreting_it() {
let dir = tempfile::tempdir().unwrap();
let source_path = fresh_empty_pile_path(&dir, "v3-reframe-source.pile");
let destination_path = fresh_empty_pile_path(&dir, "v3-reframe-destination.pile");
let expected = legacy_collection_test_headers();
for (_, header) in &expected {
append_test_bytes(&source_path, header);
}
let mut destination = Pile::open(&destination_path).unwrap();
let stats = reframe_into(&source_path, &mut destination).unwrap();
assert_eq!(stats.legacy_v3_records, expected.len());
assert_eq!(stats.legacy_unsigned_equations, 0);
assert_eq!(stats.collection_records, 0);
assert_eq!(stats.dropped_inert, 0);
assert!(destination
.snapshot()
.unwrap()
.records()
.unwrap()
.next()
.is_none());
assert_eq!(
legacy_collection_headers_at(&destination_path)
.into_iter()
.collect::<BTreeSet<_>>(),
expected.into_iter().collect::<BTreeSet<_>>()
);
destination.close().unwrap();
}
#[test]
fn yard_reclaim_preserves_inert_legacy_v3_collection_headers() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "legacy-yard.pile");
let expected = legacy_collection_test_headers();
{
let mut file = OpenOptions::new().append(true).open(&path).unwrap();
for (_, header) in &expected {
file.write_all(header).unwrap();
}
}
let mut yard = Yard::open([&path], YardConfig::default()).unwrap();
yard.reclaim().unwrap();
yard.close().unwrap();
assert_eq!(
legacy_collection_headers_at(&path)
.into_iter()
.collect::<BTreeSet<_>>(),
expected.into_iter().collect::<BTreeSet<_>>()
);
}
#[test]
fn native_collection_records_replay_once_after_reopen() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "collections.pile");
let records = collection_test_records();
let expected = sorted_collection_records(records.clone());
let mut pile = Pile::open(&path).unwrap();
for record in records {
pile.insert(record).unwrap();
}
pile.close().unwrap();
let mut reopened = Pile::open(&path).unwrap();
let snapshot = reopened.snapshot().unwrap();
let actual = sorted_collection_records(
snapshot
.records()
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap(),
);
assert_eq!(actual, expected);
let probe = expected[1];
let producers = BTreeSet::from([CollectionRecordSelector::ProducedMember(
probe.collection(),
collection_record_output(&probe),
)]);
assert!(snapshot
.select_records(&producers)
.unwrap()
.contains(&probe));
reopened.close().unwrap();
}
#[test]
fn selected_record_delta_reads_only_changed_bodies_across_append_and_remap() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "selected-delta.pile");
let mut pile = PileFile::open(&path).unwrap();
let key = ed25519_dalek::SigningKey::from_bytes(&[7; 32]);
let collection = collection_test_collection(21);
let selectors = BTreeSet::from([CollectionRecordSelector::Collection(collection)]);
for number in 0_u64..256 {
let mut raw = [0; 32];
raw[..8].copy_from_slice(&number.to_le_bytes());
pile.insert(CollectionRecord::Commit(CollectionCommit::sign(
&key,
collection,
Inline::new(raw),
empty_metadata_handle(),
)))
.unwrap();
}
let before = pile.snapshot().unwrap();
pile.ensure_mapped(pile.mmap.len() + 1).unwrap();
let added = CollectionRecord::Commit(CollectionCommit::sign(
&key,
collection,
collection_test_hash(90),
empty_metadata_handle(),
));
pile.insert(added).unwrap();
pile.insert(collection_test_records()[0]).unwrap();
let after = pile.snapshot().unwrap();
assert!(!Arc::ptr_eq(&before.mmap, &after.mmap));
assert!(Arc::ptr_eq(&before.record_origin, &after.record_origin));
COLLECTION_RECORD_READS.with(|reads| reads.set(0));
assert_eq!(
after.select_record_changes(&before, &selectors).unwrap(),
(vec![added], vec![])
);
assert_eq!(
COLLECTION_RECORD_READS.with(|reads| reads.get()),
1,
"unchanged and unrelated record bodies must not be enumerated"
);
COLLECTION_RECORD_READS.with(|reads| reads.set(0));
assert_eq!(
before.select_record_changes(&after, &selectors).unwrap(),
(vec![], vec![added])
);
assert_eq!(COLLECTION_RECORD_READS.with(|reads| reads.get()), 1);
COLLECTION_RECORD_READS.with(|reads| reads.set(0));
assert_eq!(
after.select_record_changes(&after, &selectors).unwrap(),
(vec![], vec![])
);
assert_eq!(COLLECTION_RECORD_READS.with(|reads| reads.get()), 0);
pile.close().unwrap();
}
#[test]
fn selected_record_delta_handles_rewrites_reopens_and_duplicate_occurrences() {
let dir = tempfile::tempdir().unwrap();
let first_path = fresh_empty_pile_path(&dir, "record-old.pile");
let second_path = fresh_empty_pile_path(&dir, "record-new.pile");
let record = collection_test_records()[0];
let CollectionRecord::Commit(commit) = record else {
panic!("fixture commit")
};
let replacement = CollectionRecord::Commit(CollectionCommit::sign(
&ed25519_dalek::SigningKey::from_bytes(&[99; 32]),
commit.collection(),
commit.data(),
commit.metadata(),
));
let selectors = BTreeSet::from([CollectionRecordSelector::Collection(record.collection())]);
let mut first = PileFile::open(&first_path).unwrap();
first.insert(record).unwrap();
append_test_bytes(&first_path, &collection_record_header(&record));
let before = first.snapshot().unwrap();
let mut second = PileFile::open(&second_path).unwrap();
second.insert(replacement).unwrap();
let replaced = second.snapshot().unwrap();
assert_eq!(before.collection_records.len(), 2);
assert!(
replaced
.collection_records
.difference(&before.collection_records)
.is_empty(),
"control: identical C/member/offset keys can hold different record bodies"
);
assert_eq!(
replaced.select_record_changes(&before, &selectors).unwrap(),
(vec![replacement], vec![record])
);
let mut reopened = PileFile::open(&first_path).unwrap();
let replay = reopened.snapshot().unwrap();
assert!(!Arc::ptr_eq(&before.record_origin, &replay.record_origin));
assert_eq!(
replay.select_record_changes(&before, &selectors).unwrap(),
(vec![], vec![])
);
let compact_path = fresh_empty_pile_path(&dir, "record-one-occurrence.pile");
let mut compact = PileFile::open(&compact_path).unwrap();
let empty = compact.snapshot().unwrap();
compact.insert(record).unwrap();
let one = compact.snapshot().unwrap();
assert_eq!(
one.select_record_changes(&before, &selectors).unwrap(),
(vec![], vec![]),
"dropping the duplicate physical occurrence must not remove the record"
);
assert_eq!(
empty.select_record_changes(&before, &selectors).unwrap(),
(vec![], vec![record])
);
compact.close().unwrap();
reopened.close().unwrap();
second.close().unwrap();
first.close().unwrap();
}
#[test]
fn native_collection_record_insert_is_physically_idempotent() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "idempotent.pile");
let record = collection_test_records()[0];
let mut pile = Pile::open(&path).unwrap();
pile.insert(record).unwrap();
let once = std::fs::metadata(&path).unwrap().len();
pile.insert(record).unwrap();
let twice = std::fs::metadata(&path).unwrap().len();
assert_eq!(once, ENVELOPE_HEADER_LEN as u64);
assert_eq!(twice, once);
assert_eq!(pile.snapshot().unwrap().records().unwrap().count(), 1);
pile.close().unwrap();
}
#[test]
fn replay_reads_no_earlier_producer_of_a_member() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "many-producers.pile");
let key = ed25519_dalek::SigningKey::from_bytes(&[7; 32]);
let collection = collection_test_collection(60);
let output = collection_test_hash(61);
let producers = 64_u64;
let mut pile = PileFile::open(&path).unwrap();
for number in 0..producers {
let mut input = [0; 32];
input[..8].copy_from_slice(&number.to_le_bytes());
pile.insert(CollectionRecord::Derive(CollectionDerive::sign(
&key,
collection,
crate::collection::SourceLocator::of(input),
output,
)))
.unwrap();
}
pile.close().unwrap();
COLLECTION_RECORD_READS.with(|reads| reads.set(0));
let mut reopened = PileFile::open(&path).unwrap();
let snapshot = reopened.snapshot().unwrap();
assert_eq!(
COLLECTION_RECORD_READS.with(|reads| reads.get()),
0,
"replay read earlier records back"
);
let member = BTreeSet::from([CollectionRecordSelector::ProducedMember(collection, output)]);
assert_eq!(
snapshot.select_records(&member).unwrap().len() as u64,
producers
);
reopened.close().unwrap();
}
#[test]
fn a_repeated_frame_changes_no_read() {
let dir = tempfile::tempdir().unwrap();
let single_path = fresh_empty_pile_path(&dir, "repeat-single.pile");
let doubled_path = dir.path().join("repeat-doubled.pile");
let compact_path = fresh_empty_pile_path(&dir, "repeat-compact.pile");
let authority = SigningKey::from_bytes(&[0xAA; 32]);
let low = collection_test_hash(6);
let high = collection_test_hash(7);
let result = collection_test_hash(8);
let (lineage, records) = {
let mut pile = Pile::open_as(&single_path, authority.verifying_key()).unwrap();
let lineage = register_simplearchive_collection(&mut pile, "repeat").handle();
let mut records: Vec<_> = [low, high]
.into_iter()
.map(|payload| {
CollectionRecord::Commit(CollectionCommit::sign(
&authority,
lineage,
payload,
empty_metadata_handle(),
))
})
.collect();
records.push(CollectionRecord::Merge(
CollectionMerge::sign(&authority, lineage, [low, high], result).unwrap(),
));
for record in &records {
pile.insert(*record).unwrap();
}
pile.close().unwrap();
(lineage, records)
};
let bytes = std::fs::read(&single_path).unwrap();
let mut doubled = bytes.clone();
doubled.extend_from_slice(&bytes);
std::fs::write(&doubled_path, doubled).unwrap();
let mut single = Pile::open_as(&single_path, authority.verifying_key()).unwrap();
let mut repeated = Pile::open_as(&doubled_path, authority.verifying_key()).unwrap();
let one = single.snapshot().unwrap();
let two = repeated.snapshot().unwrap();
assert_eq!(
two.collection_records.len(),
2 * one.collection_records.len()
);
assert_eq!(
two.records()
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap(),
one.records()
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap()
);
let selectors = BTreeSet::from([
CollectionRecordSelector::Collection(lineage),
CollectionRecordSelector::ProducedMember(lineage, result),
]);
assert_eq!(
two.select_records(&selectors).unwrap(),
one.select_records(&selectors).unwrap()
);
assert_eq!(
one.coverage_index()
.coverage(lineage, result)
.map(|row| row.len()),
Some(2),
"control: the fixture derives something"
);
let (one_coverage, two_coverage) = (one.coverage(), two.coverage());
assert_eq!(two_coverage, one_coverage);
assert_eq!(
one_coverage.frontier(lineage).collect::<Vec<_>>(),
vec![result],
"control: the merge consumed both commits"
);
assert_eq!(
two_coverage.frontier(lineage).collect::<Vec<_>>(),
one_coverage.frontier(lineage).collect::<Vec<_>>()
);
let length = std::fs::metadata(&doubled_path).unwrap().len();
for record in &records {
repeated.insert(*record).unwrap();
}
assert_eq!(std::fs::metadata(&doubled_path).unwrap().len(), length);
let mut compacted = Pile::open(&compact_path).unwrap();
repeated
.rewrite_retained_into(
&mut compacted,
&RetentionRoots::new(),
WantRewritePolicy::Drop,
)
.unwrap();
let three = compacted.snapshot().unwrap();
assert_eq!(three.collection_records.len(), one.collection_records.len());
assert_eq!(
sorted_collection_records(
three
.records()
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap()
),
sorted_collection_records(records)
);
compacted.close().unwrap();
repeated.close().unwrap();
single.close().unwrap();
}
#[test]
fn collection_selector_indexes_variants_and_isolates_snapshots() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "collection-selector.pile");
let collection = collection_test_collection(40);
let unrelated_collection = collection_test_collection(41);
let key = SigningKey::from_bytes(&[42; 32]);
let records = vec![
CollectionRecord::Commit(CollectionCommit::sign(
&key,
collection,
collection_test_hash(43),
empty_metadata_handle(),
)),
CollectionRecord::Merge(
CollectionMerge::sign(
&ed25519_dalek::SigningKey::from_bytes(&[7; 32]),
collection,
[collection_test_hash(43), collection_test_hash(44)],
collection_test_hash(45),
)
.unwrap(),
),
CollectionRecord::Derive(CollectionDerive::sign(
&ed25519_dalek::SigningKey::from_bytes(&[7; 32]),
collection,
crate::collection::SourceLocator::of(collection_test_hash(45).raw),
collection_test_hash(46),
)),
];
let unrelated = CollectionRecord::Derive(CollectionDerive::sign(
&ed25519_dalek::SigningKey::from_bytes(&[7; 32]),
unrelated_collection,
crate::collection::SourceLocator::of(collection_test_hash(45).raw),
collection_test_hash(47),
));
let selector = BTreeSet::from([CollectionRecordSelector::Collection(collection)]);
let mut pile = Pile::open(&path).unwrap();
pile.insert(unrelated).unwrap();
let before = pile.snapshot().unwrap();
for record in records.iter().rev().copied() {
pile.insert(record).unwrap();
}
pile.insert(records[0]).unwrap();
assert!(before.select_records(&selector).unwrap().is_empty());
let after = pile.snapshot().unwrap();
assert_eq!(
sorted_collection_records(after.select_records(&selector).unwrap()),
sorted_collection_records(records.clone())
);
let collection_union = BTreeSet::from([
CollectionRecordSelector::Collection(collection),
CollectionRecordSelector::Collection(unrelated_collection),
]);
let mut expected_union = records.clone();
expected_union.push(unrelated);
let selected_union =
sorted_collection_records(after.select_records(&collection_union).unwrap());
assert_eq!(selected_union, sorted_collection_records(expected_union));
let mixed = BTreeSet::from([
CollectionRecordSelector::Collection(collection),
CollectionRecordSelector::ProducedMember(
unrelated_collection,
collection_test_hash(47),
),
]);
let mut expected_mixed = records;
expected_mixed.push(unrelated);
assert_eq!(
sorted_collection_records(after.select_records(&mixed).unwrap()),
sorted_collection_records(expected_mixed)
);
drop(after);
drop(before);
pile.close().unwrap();
let mut reopened = Pile::open(&path).unwrap();
assert_eq!(
reopened
.snapshot()
.unwrap()
.select_records(&selector)
.unwrap()
.len(),
3
);
reopened.close().unwrap();
}
#[test]
fn record_relationship_indexes_keep_raw_aliases_and_frozen_absent_producers() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "record-relationships.pile");
let collection = collection_test_collection(40);
let other = collection_test_collection(41);
let key = SigningKey::from_bytes(&[42; 32]);
let second_key = SigningKey::from_bytes(&[43; 32]);
let input = collection_test_hash(44);
let second_input = collection_test_hash(45);
let output = collection_test_hash(46);
let a = CollectionCommit::sign(&key, collection, input, empty_metadata_handle());
let b = CollectionCommit::sign(&second_key, collection, input, empty_metadata_handle());
let c = CollectionCommit::sign(&key, collection, second_input, empty_metadata_handle());
let direct = CollectionRecord::Commit(CollectionCommit::sign(
&key,
collection,
output,
empty_metadata_handle(),
));
let merge_a = CollectionRecord::Merge(
CollectionMerge::sign(&key, collection, [a.data(), c.data()], output).unwrap(),
);
let merge_b = CollectionRecord::Merge(
CollectionMerge::sign(&second_key, collection, [a.data(), c.data()], output).unwrap(),
);
let repeated = CollectionRecord::Merge(
CollectionMerge::sign(&key, collection, [a.data(), output], output).unwrap(),
);
let derive_a = CollectionRecord::Derive(CollectionDerive::sign(
&key,
collection,
crate::collection::SourceLocator::of(a.data().raw),
output,
));
let derive_b = CollectionRecord::Derive(CollectionDerive::sign(
&second_key,
collection,
crate::collection::SourceLocator::of(a.data().raw),
output,
));
let other_producer = CollectionRecord::Derive(CollectionDerive::sign(
&key,
other,
crate::collection::SourceLocator::of(a.data().raw),
output,
));
let descendant_output = collection_test_hash(47);
let descendant = CollectionRecord::Derive(CollectionDerive::sign(
&key,
other,
crate::collection::SourceLocator::of(output.raw),
descendant_output,
));
let records = vec![
direct,
merge_a,
merge_b,
repeated,
derive_a,
derive_b,
other_producer,
descendant,
];
let produced =
BTreeSet::from([CollectionRecordSelector::ProducedMember(collection, output)]);
let expected_produced =
sorted_collection_records(vec![direct, merge_a, merge_b, repeated, derive_a, derive_b]);
let mut reader = Pile::open(&path).unwrap();
let empty = reader.snapshot().unwrap();
let mut writer = Pile::open(&path).unwrap();
for record in records.iter().rev().copied() {
writer.insert(record).unwrap();
}
let middle = reader.snapshot().unwrap();
let cloned = middle.clone();
assert!(cloned
.collection_records
.shares_root(&middle.collection_records));
let input_producers =
BTreeSet::from([CollectionRecordSelector::ProducedMember(collection, input)]);
assert!(empty.select_records(&produced).unwrap().is_empty());
assert_eq!(middle.blobs().count(), 0);
assert_eq!(middle.proofs().unwrap().count(), 0);
assert!(middle.select_records(&input_producers).unwrap().is_empty());
assert_eq!(
sorted_collection_records(middle.select_records(&produced).unwrap()),
expected_produced
);
assert_eq!(middle.collection_records.len(), records.len() as u64);
let once = std::fs::metadata(&path).unwrap().len();
writer.insert(merge_a).unwrap();
assert_eq!(std::fs::metadata(&path).unwrap().len(), once);
for record in [a, b, c] {
writer.insert(CollectionRecord::Commit(record)).unwrap();
}
let after = reader.snapshot().unwrap();
assert!(after
.select_records(&input_producers)
.unwrap()
.contains(&CollectionRecord::Commit(a)));
assert!(middle.select_records(&input_producers).unwrap().is_empty());
assert_eq!(
sorted_collection_records(after.select_records(&input_producers).unwrap()),
sorted_collection_records(vec![
CollectionRecord::Commit(a),
CollectionRecord::Commit(b)
]),
);
struct ScanOnly(Vec<CollectionRecord>);
impl CollectionRead for ScanOnly {
type RecordsError = Infallible;
type RecordIter<'a> = std::vec::IntoIter<Result<CollectionRecord, Infallible>>;
fn records<'a>(&'a self) -> Result<Self::RecordIter<'a>, Self::RecordsError> {
Ok(self
.0
.iter()
.copied()
.map(Ok)
.collect::<Vec<_>>()
.into_iter())
}
fn collections(&self) -> Result<Vec<CollectionHandle>, Self::RecordsError> {
Ok(crate::collection::distinct_collections(
self.0.iter().copied(),
))
}
}
let selectors = [
CollectionRecordSelector::ProducedMember(collection, output),
CollectionRecordSelector::ProducedMember(other, input),
CollectionRecordSelector::ProducedMember(other, descendant_output),
CollectionRecordSelector::CommitMember(collection, output),
CollectionRecordSelector::MergeCollection(collection),
CollectionRecordSelector::DeriveTarget(other),
CollectionRecordSelector::Collection(other),
];
let mut queries = vec![BTreeSet::new(), selectors.into_iter().collect()];
for left in selectors {
for right in selectors {
queries.push(BTreeSet::from([left, right]));
}
}
for snapshot in [&middle, &after] {
let fallback = ScanOnly(
snapshot
.records()
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap(),
);
for query in &queries {
let selected = sorted_collection_records(snapshot.select_records(query).unwrap());
assert_eq!(
selected,
sorted_collection_records(fallback.select_records(query).unwrap()),
"{query:?}"
);
assert!(selected
.windows(2)
.all(|pair| pair[0].fingerprint() < pair[1].fingerprint()));
}
}
writer.close().unwrap();
reader.close().unwrap();
let mut reopened = Pile::open(&path).unwrap();
let replayed = reopened.snapshot().unwrap();
for query in &queries {
assert_eq!(
sorted_collection_records(replayed.select_records(query).unwrap()),
sorted_collection_records(after.select_records(query).unwrap())
);
}
assert_eq!(
sorted_collection_records(middle.select_records(&produced).unwrap()),
expected_produced
);
reopened.close().unwrap();
}
#[test]
fn native_collection_records_cat_as_an_order_independent_set_union() {
let dir = tempfile::tempdir().unwrap();
let path_a = fresh_empty_pile_path(&dir, "a.pile");
let path_b = fresh_empty_pile_path(&dir, "b.pile");
let path_ab = dir.path().join("ab.pile");
let path_ba = dir.path().join("ba.pile");
let records = collection_test_records();
let mut a = Pile::open(&path_a).unwrap();
a.insert(records[0]).unwrap();
a.insert(records[1]).unwrap();
a.close().unwrap();
let mut b = Pile::open(&path_b).unwrap();
b.insert(records[0]).unwrap();
b.insert(records[2]).unwrap();
b.close().unwrap();
let bytes_a = std::fs::read(&path_a).unwrap();
let bytes_b = std::fs::read(&path_b).unwrap();
let mut ab = bytes_a.clone();
ab.extend_from_slice(&bytes_b);
std::fs::write(&path_ab, ab).unwrap();
let mut ba = bytes_b;
ba.extend_from_slice(&bytes_a);
std::fs::write(&path_ba, ba).unwrap();
let expected = sorted_collection_records(records.clone());
let source_expected = sorted_collection_records(records[..2].to_vec());
let target_expected = vec![records[2]];
let source_selector = BTreeSet::from([CollectionRecordSelector::Collection(
collection_test_collection(1),
)]);
let target_selector = BTreeSet::from([CollectionRecordSelector::Collection(
collection_test_collection(2),
)]);
for path in [&path_ab, &path_ba] {
let mut pile = Pile::open(path).unwrap();
let snapshot = pile.snapshot().unwrap();
let actual = sorted_collection_records(
snapshot
.records()
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap(),
);
assert_eq!(actual, expected);
assert_eq!(
sorted_collection_records(snapshot.select_records(&source_selector).unwrap()),
source_expected
);
assert_eq!(
snapshot.select_records(&target_selector).unwrap(),
target_expected
);
pile.close().unwrap();
}
}
#[test]
fn collection_selection_survives_insert_reopen_and_concatenation() {
let dir = tempfile::tempdir().unwrap();
let path_a = fresh_empty_pile_path(&dir, "selection-a.pile");
let path_b = fresh_empty_pile_path(&dir, "selection-b.pile");
let path_ab = dir.path().join("selection-ab.pile");
let path_ba = dir.path().join("selection-ba.pile");
let target = collection_test_collection(2);
let input = collection_test_hash(8);
let records = collection_test_records();
let first = records[2];
let conflicting = CollectionRecord::Derive(CollectionDerive::sign(
&ed25519_dalek::SigningKey::from_bytes(&[7; 32]),
target,
crate::collection::SourceLocator::of(input.raw),
collection_test_hash(10),
));
let unrelated = CollectionRecord::Derive(CollectionDerive::sign(
&ed25519_dalek::SigningKey::from_bytes(&[7; 32]),
collection_test_collection(3),
crate::collection::SourceLocator::of(input.raw),
collection_test_hash(11),
));
let exact = [CollectionRecordSelector::DeriveTarget(target)]
.into_iter()
.collect();
let mut a = Pile::open(&path_a).unwrap();
for record in [records[0], records[1], first, unrelated] {
a.insert(record).unwrap();
}
assert_eq!(
a.snapshot().unwrap().select_records(&exact).unwrap(),
vec![first]
);
a.close().unwrap();
let mut reopened = Pile::open(&path_a).unwrap();
assert_eq!(
reopened.snapshot().unwrap().select_records(&exact).unwrap(),
vec![first]
);
reopened.close().unwrap();
let mut b = Pile::open(&path_b).unwrap();
for record in [first, conflicting] {
b.insert(record).unwrap();
}
b.close().unwrap();
let bytes_a = std::fs::read(&path_a).unwrap();
let bytes_b = std::fs::read(&path_b).unwrap();
let mut ab = bytes_a.clone();
ab.extend_from_slice(&bytes_b);
std::fs::write(&path_ab, ab).unwrap();
let mut ba = bytes_b;
ba.extend_from_slice(&bytes_a);
std::fs::write(&path_ba, ba).unwrap();
let mut expected = vec![first, conflicting];
expected.sort_unstable_by_key(CollectionRecord::fingerprint);
for path in [&path_ab, &path_ba] {
let mut pile = Pile::open(path).unwrap();
let snapshot = pile.snapshot().unwrap();
assert_eq!(
sorted_collection_records(snapshot.select_records(&exact).unwrap()),
expected
);
assert!(!snapshot
.select_records(&exact)
.unwrap()
.contains(&unrelated));
pile.close().unwrap();
}
}
#[test]
fn native_collection_record_torn_tail_is_detected_and_amputated() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "torn.pile");
let mut pile = Pile::open(&path).unwrap();
pile.insert(collection_test_records()[0]).unwrap();
pile.close().unwrap();
OpenOptions::new()
.write(true)
.open(&path)
.unwrap()
.set_len((ENVELOPE_HEADER_LEN - 1) as u64)
.unwrap();
let mut reopened = Pile::open(&path).unwrap();
assert!(matches!(
reopened.refresh(),
Err(ReadError::CorruptPile { valid_length: 0 })
));
reopened.amputate().unwrap();
assert_eq!(std::fs::metadata(&path).unwrap().len(), 0);
reopened.close().unwrap();
}
#[test]
fn retained_rewrite_composes_explicit_roots_strong_pins_and_wants() {
let dir = tempfile::tempdir().unwrap();
let source_path = fresh_empty_pile_path(&dir, "source.pile");
let destination_path = fresh_empty_pile_path(&dir, "destination.pile");
let mut source = Pile::open(&source_path).unwrap();
let mut destination = Pile::open(&destination_path).unwrap();
let legacy_attachment = source
.put::<UnknownBlob, _>(Bytes::from_source(b"legacy attachment".to_vec()))
.unwrap();
let legacy_head = source
.put::<SimpleArchive, _>(Blob::<SimpleArchive>::new(Bytes::from_source(
legacy_attachment.raw.to_vec(),
)))
.unwrap();
let pin_id = Id::new([9; 16]).unwrap();
source
.append_legacy_pin_for_test(pin_id, None, Some(legacy_head))
.unwrap();
let collection_attachment = source
.put::<UnknownBlob, _>(Bytes::from_source(b"collection attachment".to_vec()))
.unwrap();
let collection_data = source
.put::<UnknownBlob, _>(Bytes::from_source(collection_attachment.raw.to_vec()))
.unwrap();
let obsolete_input = source
.put::<UnknownBlob, _>(Bytes::from_source(b"obsolete input".to_vec()))
.unwrap();
let collection_record = source
.put::<UnknownBlob, _>(Bytes::from_source(obsolete_input.raw.to_vec()))
.unwrap();
let want_target = source
.put::<UnknownBlob, _>(Bytes::from_source(b"want target".to_vec()))
.unwrap();
source.want(WantRequest::blob(want_target)).unwrap();
let orphan = source
.put::<UnknownBlob, _>(Bytes::from_source(b"orphan".to_vec()))
.unwrap();
source.flush().unwrap();
let mut explicit = RetentionRoots::new();
explicit.retain_recursive(collection_data);
explicit.retain_direct(collection_record);
let stats = source
.rewrite_retained_into(&mut destination, &explicit, WantRewritePolicy::Preserve)
.unwrap();
assert_eq!(
stats,
PileRewriteStats {
retained_blobs: 6,
strong_pins: 1,
wants: 1,
capability_proofs: 0,
retired_capability_proofs: 0,
opaque_frames: 0,
superseded_equations: 0,
drained_frames: 0,
retired_equations: 0,
filtered_frames: 0,
repeated_frames: 0,
corrupt_blobs: BTreeSet::new(),
}
);
let reader = destination.snapshot().unwrap();
for retained in [
legacy_attachment,
legacy_head.transmute(),
collection_attachment,
collection_data,
collection_record,
want_target,
] {
assert!(reader.get::<Blob<UnknownBlob>, _>(retained).is_ok());
}
for collected in [obsolete_input, orphan] {
assert!(reader.get::<Blob<UnknownBlob>, _>(collected).is_err());
}
assert_eq!(
destination.legacy_pin_head_for_test(pin_id).unwrap(),
Some(legacy_head)
);
assert_eq!(
destination
.snapshot()
.unwrap()
.wants()
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap(),
vec![WantRequest::blob(want_target)]
);
drop(reader);
destination.close().unwrap();
source.close().unwrap();
}
#[test]
fn retained_rewrite_keeps_invalid_commit_resident_references() {
let dir = tempfile::tempdir().unwrap();
let source_path = fresh_empty_pile_path(&dir, "invalid-commit-source.pile");
let destination_path = fresh_empty_pile_path(&dir, "invalid-commit-destination.pile");
let mut source = Pile::open(&source_path).unwrap();
let mut destination = Pile::open(&destination_path).unwrap();
let forged_data = source
.put::<UnknownBlob, _>(Bytes::from_source(b"forged ownership data".to_vec()))
.unwrap();
let metadata_facts: TribleSet = entity! {
crate::metadata::tag: collection_test_id(20)
}
.into();
let forged_metadata = source
.put::<SimpleArchive, _>(metadata_facts.to_blob())
.unwrap();
let descriptor = named_for_tests("forged", collection_test_id(22));
let descriptor_handle = source
.put::<SimpleArchive, _>(crate::blob::IntoBlob::<SimpleArchive>::to_blob(
descriptor.into_facts(),
))
.unwrap();
let invalid = invalidate_collection_commit(CollectionCommit::sign(
&SigningKey::from_bytes(&[24; 32]),
descriptor_handle,
Inline::<Hash<Blake3>>::new(forged_data.raw),
forged_metadata,
));
let records = vec![CollectionRecord::Commit(invalid)];
for record in records.iter().copied() {
source.insert(record).unwrap();
}
source.flush().unwrap();
let stats = source
.rewrite_retained_into(
&mut destination,
&RetentionRoots::new(),
WantRewritePolicy::Drop,
)
.unwrap();
assert_eq!(stats.retained_blobs, 3);
assert_eq!(
sorted_collection_records(
destination
.snapshot()
.unwrap()
.records()
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap()
),
sorted_collection_records(records),
);
let reader = destination.snapshot().unwrap();
assert!(matches!(
reader.get::<Blob<UnknownBlob>, _>(forged_data),
Ok(_)
));
assert!(matches!(
reader.get::<Blob<SimpleArchive>, _>(forged_metadata),
Ok(_)
));
assert!(matches!(
reader.get::<Blob<SimpleArchive>, _>(descriptor_handle),
Ok(_)
));
drop(reader);
destination.close().unwrap();
source.close().unwrap();
}
#[test]
fn retained_rewrite_preserves_valid_dangling_commit_without_demanding_dependencies() {
let dir = tempfile::tempdir().unwrap();
let source_path = fresh_empty_pile_path(&dir, "dangling-commit-source.pile");
let destination_path = fresh_empty_pile_path(&dir, "dangling-commit-destination.pile");
let mut source = Pile::open(&source_path).unwrap();
let mut destination = Pile::open(&destination_path).unwrap();
let missing_descriptor = collection_test_collection(25);
let missing_data = collection_test_hash(28);
let missing_metadata = Inline::<Handle<SimpleArchive>>::new([29; 32]);
let commit = CollectionCommit::sign(
&SigningKey::from_bytes(&[30; 32]),
missing_descriptor,
missing_data,
missing_metadata,
);
commit.verify_strict().unwrap();
let records = vec![CollectionRecord::Commit(commit)];
for record in records.iter().copied() {
source.insert(record).unwrap();
}
source.flush().unwrap();
let stats = source
.rewrite_retained_into(
&mut destination,
&RetentionRoots::new(),
WantRewritePolicy::Drop,
)
.unwrap();
assert_eq!(stats.retained_blobs, 0);
assert_eq!(
sorted_collection_records(
destination
.snapshot()
.unwrap()
.records()
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap()
),
sorted_collection_records(records),
);
let reader = destination.snapshot().unwrap();
assert!(!reader
.contains_blob(Handle::<UnknownBlob>::from_hash(missing_data))
.unwrap());
assert!(!reader.contains_blob(missing_metadata).unwrap());
assert!(!reader.contains_blob(missing_descriptor).unwrap());
drop(reader);
destination.close().unwrap();
source.close().unwrap();
}
#[test]
fn retained_rewrite_preserves_dangling_legacy_pin_without_demanding_head() {
let dir = tempfile::tempdir().unwrap();
let source_path = fresh_empty_pile_path(&dir, "dangling-pin-source.pile");
let destination_path = fresh_empty_pile_path(&dir, "dangling-pin-destination.pile");
let mut source = Pile::open(&source_path).unwrap();
let mut destination = Pile::open(&destination_path).unwrap();
let pin_id = Id::new([31; 16]).unwrap();
let missing_head = Inline::<Handle<SimpleArchive>>::new([32; 32]);
source
.append_legacy_pin_for_test(pin_id, None, Some(missing_head))
.unwrap();
source.flush().unwrap();
let stats = source
.rewrite_retained_into(
&mut destination,
&RetentionRoots::new(),
WantRewritePolicy::Drop,
)
.unwrap();
assert_eq!(stats.retained_blobs, 0);
assert_eq!(stats.strong_pins, 1);
assert_eq!(
destination.legacy_pin_head_for_test(pin_id).unwrap(),
Some(missing_head)
);
let reader = destination.snapshot().unwrap();
assert!(!reader.contains_blob(missing_head).unwrap());
drop(reader);
destination.close().unwrap();
source.close().unwrap();
}
#[test]
fn retained_rewrite_still_fails_loud_for_missing_explicit_root() {
let dir = tempfile::tempdir().unwrap();
let source_path = fresh_empty_pile_path(&dir, "missing-explicit-source.pile");
let destination_path = fresh_empty_pile_path(&dir, "missing-explicit-destination.pile");
let mut source = Pile::open(&source_path).unwrap();
let mut destination = Pile::open(&destination_path).unwrap();
let missing = Inline::<Handle<UnknownBlob>>::new([31; 32]);
let mut explicit = RetentionRoots::new();
explicit.retain_recursive(missing);
let error = source
.rewrite_retained_into(&mut destination, &explicit, WantRewritePolicy::Drop)
.unwrap_err();
assert!(matches!(
error,
PileRewriteError::Transfer(crate::repo::TransferError::Load(
GetBlobError::BlobNotFound(_)
))
));
destination.close().unwrap();
source.close().unwrap();
}
#[test]
fn retained_rewrite_keeps_witness_records_without_treating_fingerprints_as_blobs() {
let dir = tempfile::tempdir().unwrap();
let source_path = fresh_empty_pile_path(&dir, "witness-source.pile");
let destination_path = fresh_empty_pile_path(&dir, "witness-retained.pile");
let mut source = Pile::open(&source_path).unwrap();
let mut destination = Pile::open(&destination_path).unwrap();
let input = source
.put::<UnknownBlob, _>(Bytes::from_source(b"witness-owned input".to_vec()))
.unwrap();
let output = source
.put::<UnknownBlob, _>(Bytes::from_source(b"selected output".to_vec()))
.unwrap();
let orphan = source
.put::<UnknownBlob, _>(Bytes::from_source(b"not a record".to_vec()))
.unwrap();
let signer = SigningKey::from_bytes(&[87; 32]);
let base = collection_test_collection(88);
let target = collection_test_collection(89);
let commit = CollectionCommit::sign(
&signer,
base,
Inline::new(input.raw),
empty_metadata_handle(),
);
let derive = CollectionDerive::sign(
&signer,
target,
crate::collection::SourceLocator::of(commit.data().raw),
Inline::new(output.raw),
);
let merge = CollectionMerge::sign(
&signer,
target,
[derive.output(), collection_test_hash(90)],
derive.output(),
)
.unwrap();
source.insert(CollectionRecord::Merge(merge)).unwrap();
let before_witnesses = source.snapshot().unwrap();
assert_eq!(
before_witnesses
.records()
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap(),
vec![CollectionRecord::Merge(merge)]
);
assert_eq!(before_witnesses.proofs().unwrap().count(), 0);
source.insert(CollectionRecord::Derive(derive)).unwrap();
source.insert(CollectionRecord::Commit(commit)).unwrap();
let mut selected = RetentionRoots::new();
selected.retain_direct(output);
source
.rewrite_retained_into(&mut destination, &selected, WantRewritePolicy::Drop)
.unwrap();
let retained = destination.snapshot().unwrap();
let retained_records = retained
.records()
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap();
for record in [
CollectionRecord::Commit(commit),
CollectionRecord::Derive(derive),
CollectionRecord::Merge(merge),
] {
assert!(retained_records.contains(&record));
}
assert!(retained.get::<Blob<UnknownBlob>, _>(input).is_ok());
assert!(retained.get::<Blob<UnknownBlob>, _>(output).is_ok());
assert!(retained.get::<Blob<UnknownBlob>, _>(orphan).is_err());
drop((before_witnesses, retained));
destination.close().unwrap();
source.close().unwrap();
}
#[test]
fn retained_rewrite_preserves_native_records_and_their_owned_blobs() {
let dir = tempfile::tempdir().unwrap();
let source_path = fresh_empty_pile_path(&dir, "collection-source.pile");
let destination_path = fresh_empty_pile_path(&dir, "collection-destination.pile");
let mut source = Pile::open(&source_path).unwrap();
let mut destination = Pile::open(&destination_path).unwrap();
let attachment = source
.put::<UnknownBlob, _>(Bytes::from_source(b"owned attachment".to_vec()))
.unwrap();
let data = source
.put::<UnknownBlob, _>(Bytes::from_source(attachment.raw.to_vec()))
.unwrap();
let metadata = source
.put::<SimpleArchive, _>(TribleSet::new().to_blob())
.unwrap();
assert_eq!(metadata, empty_metadata_handle());
let orphan = source
.put::<UnknownBlob, _>(Bytes::from_source(b"unowned".to_vec()))
.unwrap();
let equation_owned = source
.put::<UnknownBlob, _>(Bytes::from_source(b"equation-owned".to_vec()))
.unwrap();
let descriptor = named_for_tests("retained", collection_test_id(11));
let descriptor_handle = source
.put::<SimpleArchive, _>(crate::blob::IntoBlob::<SimpleArchive>::to_blob(
descriptor.into_facts(),
))
.unwrap();
let key = SigningKey::from_bytes(&[13; 32]);
let commit = CollectionCommit::sign(
&key,
descriptor_handle,
Inline::<Hash<Blake3>>::new(data.raw),
metadata,
);
commit.verify_strict().unwrap();
let records = vec![
CollectionRecord::Commit(commit),
CollectionRecord::Merge(
CollectionMerge::sign(
&ed25519_dalek::SigningKey::from_bytes(&[7; 32]),
descriptor_handle,
[Inline::new(equation_owned.raw), collection_test_hash(15)],
collection_test_hash(16),
)
.unwrap(),
),
CollectionRecord::Derive(CollectionDerive::sign(
&ed25519_dalek::SigningKey::from_bytes(&[7; 32]),
collection_test_collection(17),
crate::collection::SourceLocator::of(collection_test_hash(16).raw),
collection_test_hash(18),
)),
];
for record in records.iter().copied() {
source.insert(record).unwrap();
}
source.flush().unwrap();
let stats = source
.rewrite_retained_into(
&mut destination,
&RetentionRoots::new(),
WantRewritePolicy::Drop,
)
.unwrap();
assert_eq!(stats.retained_blobs, 5);
let actual_records = sorted_collection_records(
destination
.snapshot()
.unwrap()
.records()
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap(),
);
assert_eq!(actual_records, sorted_collection_records(records));
let reader = destination.snapshot().unwrap();
for retained in [
attachment,
data,
metadata.transmute(),
descriptor_handle.transmute(),
equation_owned,
] {
assert!(reader.get::<Blob<UnknownBlob>, _>(retained).is_ok());
}
assert!(reader.get::<Blob<UnknownBlob>, _>(orphan).is_err());
drop(reader);
destination.close().unwrap();
source.close().unwrap();
}
fn append_v3_blob_candidate(
path: &Path,
hash: Inline<Hash<Blake3>>,
payload: &[u8],
timestamp: u64,
) -> usize {
let mut file = OpenOptions::new().append(true).open(path).unwrap();
let record_offset = file.metadata().unwrap().len() as usize;
let header = BlobHeaderV3::new(timestamp, payload.len() as u64, hash);
file.write_all(header.as_bytes()).unwrap();
file.write_all(payload).unwrap();
file.write_all(&vec![0; block_post_pad(payload.len())])
.unwrap();
file.sync_all().unwrap();
record_offset
}
#[test]
fn blob_occurrence_relation_groups_and_orders_offsets() {
let hash = [0xA5; 32];
let other_hash = [0x5A; 32];
let mut occurrences = PileBlobIndex::new();
for (key_hash, offset) in [
(hash, 65_536usize),
(other_hash, 17),
(hash, 1),
(hash, 256),
] {
let key = blob_occurrence_key(&key_hash, IndexEntry::new(offset));
occurrences.insert(&Entry::with_value(&key, CachedValidation::default()));
}
assert_eq!(occurrences.len(), 4);
assert_eq!(occurrences.segmented_len(&hash), 3);
assert_eq!(occurrences.segmented_len(&other_hash), 1);
assert_eq!(
occurrences
.iter_prefix_count::<32>()
.collect::<BTreeSet<_>>(),
BTreeSet::from([(hash, 3), (other_hash, 1)])
);
let first = first_blob_occurrence(&occurrences, &hash).unwrap();
let second = next_blob_occurrence(&occurrences, &hash, first).unwrap();
let third = next_blob_occurrence(&occurrences, &hash, second).unwrap();
assert_eq!(first.record_offset, 1);
assert_eq!(second.record_offset, 256);
assert_eq!(third.record_offset, 65_536);
assert!(next_blob_occurrence(&occurrences, &hash, third).is_none());
}
#[test]
fn semantic_blob_frontier_is_independent_of_occurrence_order_and_multiplicity() {
let dir = tempfile::tempdir().unwrap();
let path_a = fresh_empty_pile_path(&dir, "semantic-a.pile");
let path_b = fresh_empty_pile_path(&dir, "semantic-b.pile");
let payload_a = b"semantic a";
let payload_b = b"semantic b";
let hash_a: Inline<Hash<Blake3>> =
Blob::<UnknownBlob>::new(Bytes::from_source(payload_a.to_vec()))
.get_handle()
.into();
let hash_b: Inline<Hash<Blake3>> =
Blob::<UnknownBlob>::new(Bytes::from_source(payload_b.to_vec()))
.get_handle()
.into();
append_v3_blob_candidate(&path_a, hash_a, payload_a, 1);
append_v3_blob_candidate(&path_a, hash_b, payload_b, 2);
append_v3_blob_candidate(&path_a, hash_a, payload_a, 3);
append_v3_blob_candidate(&path_b, hash_b, payload_b, 4);
append_v3_blob_candidate(&path_b, hash_a, payload_a, 5);
let mut pile_a = Pile::open(&path_a).unwrap();
let mut pile_b = Pile::open(&path_b).unwrap();
let snapshot_a = pile_a.snapshot().unwrap();
let snapshot_b = pile_b.snapshot().unwrap();
assert_eq!(snapshot_a.blobs.len(), 3);
assert_eq!(snapshot_b.blobs.len(), 2);
let semantic_a = snapshot_a.blobs.prefix_set::<32>();
let semantic_b = snapshot_b.blobs.prefix_set::<32>();
assert_eq!(
semantic_a.iter().collect::<BTreeSet<_>>(),
BTreeSet::from([hash_a.raw, hash_b.raw])
);
assert_eq!(
semantic_a.iter().collect::<BTreeSet<_>>(),
semantic_b.iter().collect::<BTreeSet<_>>()
);
let absent = [0x5au8; 32];
let mut asked = PATCH::<32, IdentitySchema, ()>::new();
for key in [hash_a.raw, absent, hash_b.raw] {
asked.insert(&Entry::new(&key));
}
let resident = crate::repo::BlobStoreMeta::resident(&snapshot_a, &asked).unwrap();
assert_eq!(
resident.iter_ordered().copied().collect::<BTreeSet<_>>(),
BTreeSet::from([hash_a.raw, hash_b.raw])
);
assert!(resident <= asked);
assert!(crate::repo::BlobStoreMeta::metadata(
&snapshot_a,
Inline::<Handle<UnknownBlob>>::new(absent)
)
.unwrap()
.is_none());
assert!(snapshot_a.blobs_diff(&snapshot_b).next().is_none());
assert!(snapshot_b.blobs_diff(&snapshot_a).next().is_none());
drop(snapshot_a);
drop(snapshot_b);
pile_a.close().unwrap();
pile_b.close().unwrap();
}
#[test]
fn payload_validation_matches_and_rejects_around_parallel_threshold() {
for strategy in [
ValidationStrategy::Serial,
ValidationStrategy::ParallelIfLarge,
] {
for len in [
PARALLEL_BLAKE3_THRESHOLD - 1,
PARALLEL_BLAKE3_THRESHOLD,
PARALLEL_BLAKE3_THRESHOLD + 1,
] {
let bytes = Bytes::from_source(
(0..len)
.map(|position| (position.wrapping_mul(131) % 251) as u8)
.collect::<Vec<_>>(),
);
let expected = Hash::<Blake3>::digest(&bytes);
assert!(matches!(
compute_validation_state(&bytes, &expected, strategy),
ValidationState::Validated
));
let mut wrong = expected;
wrong.raw[0] ^= 1;
assert!(matches!(
compute_validation_state(&bytes, &wrong, strategy),
ValidationState::Invalid
));
}
}
}
#[test]
fn payload_validation_keeps_the_first_cached_result() {
let bytes = Bytes::from_source(vec![0x5A; PARALLEL_BLAKE3_THRESHOLD]);
let expected = Hash::<Blake3>::digest(&bytes);
let cache = CachedValidation::default();
assert!(matches!(
cache.state(&bytes, &expected, ValidationStrategy::Serial),
ValidationState::Validated
));
let mut wrong = expected;
wrong.raw[0] ^= 1;
assert!(matches!(
cache.state(&bytes, &wrong, ValidationStrategy::ParallelIfLarge),
ValidationState::Validated
));
assert_eq!(cache.cached(), Some(ValidationState::Validated));
}
#[test]
fn replay_keeps_inline_validation_lazy_and_snapshots_share_it() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "sparse-validation.pile");
let (first, second) = {
let mut writer = Pile::open(&path).unwrap();
let first = writer
.put::<UnknownBlob, _>(Bytes::from_source(b"first".to_vec()))
.unwrap();
let second = writer
.put::<UnknownBlob, _>(Bytes::from_source(b"second".to_vec()))
.unwrap();
writer.close().unwrap();
(first, second)
};
let mut replay = Pile::open(&path).unwrap();
replay.refresh().unwrap();
let first_entry = first_blob_occurrence(&replay.blobs, &first.raw).unwrap();
let second_entry = first_blob_occurrence(&replay.blobs, &second.raw).unwrap();
assert_eq!(
blob_occurrence_validation(&replay.blobs, &first.raw, first_entry).cached(),
None
);
assert_eq!(
blob_occurrence_validation(&replay.blobs, &second.raw, second_entry).cached(),
None
);
let reader = replay.snapshot().unwrap();
let cloned = reader.clone();
assert!(std::ptr::eq(
blob_occurrence_validation(&reader.blobs, &first.raw, first_entry),
blob_occurrence_validation(&cloned.blobs, &first.raw, first_entry),
));
let _: Blob<UnknownBlob> = reader.get(first).unwrap();
assert_eq!(
blob_occurrence_validation(&replay.blobs, &first.raw, first_entry).cached(),
Some(ValidationState::Validated)
);
let _: Blob<UnknownBlob> = cloned.get(first).unwrap();
assert_eq!(
blob_occurrence_validation(&replay.blobs, &first.raw, first_entry).cached(),
Some(ValidationState::Validated)
);
assert_eq!(
blob_occurrence_validation(&replay.blobs, &second.raw, second_entry).cached(),
None
);
drop(reader);
drop(cloned);
replay.close().unwrap();
}
#[test]
fn attaching_a_cold_simplearchive_root_does_not_validate_it_but_viewing_does() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "cold-cover-availability.pile");
let mut pile = Pile::open(&path).unwrap();
let blob = Blob::<SimpleArchive>::new(Bytes::from_source(Vec::<u8>::new()));
let handle = pile.put::<SimpleArchive, _>(blob).unwrap();
let occurrence = first_blob_occurrence(&pile.blobs, &handle.raw).unwrap();
assert_eq!(
blob_occurrence_validation(&pile.blobs, &handle.raw, occurrence).cached(),
None,
);
let collection = register_simplearchive_collection(&mut pile, "cold-cover-availability");
pile.insert(CollectionRecord::Commit(CollectionCommit::sign(
&SigningKey::from_bytes(&[0xAA; 32]),
collection.handle(),
Handle::<SimpleArchive>::to_hash(handle),
empty_metadata_handle(),
)))
.unwrap();
let snapshot = pile.snapshot().unwrap();
let attached = snapshot.collection(collection).unwrap();
assert_eq!(attached.cover(), &Cover::from_members(collection, [handle]));
assert_eq!(
blob_occurrence_validation(&pile.blobs, &handle.raw, occurrence).cached(),
None,
);
let viewed: TribleSet = attached.view().unwrap();
assert!(viewed.is_empty());
assert_eq!(
blob_occurrence_validation(&pile.blobs, &handle.raw, occurrence).cached(),
Some(ValidationState::Validated),
);
drop(attached);
drop(snapshot);
pile.close().unwrap();
}
#[test]
fn attaching_a_corrupt_root_does_not_bypass_view_validation() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "corrupt-cover-availability.pile");
let expected = Blob::<SimpleArchive>::new(Bytes::from_source(Vec::<u8>::new()));
let handle = expected.get_handle();
append_v3_blob_candidate(&path, handle.into(), b"not the empty archive", 1);
let mut pile = Pile::open(&path).unwrap();
let collection = register_simplearchive_collection(&mut pile, "corrupt-cover-availability");
pile.insert(CollectionRecord::Commit(CollectionCommit::sign(
&SigningKey::from_bytes(&[0xAA; 32]),
collection.handle(),
Handle::<SimpleArchive>::to_hash(handle),
empty_metadata_handle(),
)))
.unwrap();
let snapshot = pile.snapshot().unwrap();
let occurrence = first_blob_occurrence(&snapshot.blobs, &handle.raw).unwrap();
let attached = snapshot.collection(collection).unwrap();
assert_eq!(attached.cover(), &Cover::from_members(collection, [handle]));
assert_eq!(
blob_occurrence_validation(&snapshot.blobs, &handle.raw, occurrence).cached(),
None,
);
assert!(attached.view::<TribleSet>().is_err());
assert_eq!(
blob_occurrence_validation(&snapshot.blobs, &handle.raw, occurrence).cached(),
Some(ValidationState::Invalid),
);
drop(attached);
drop(snapshot);
pile.close().unwrap();
}
#[test]
fn duplicate_replay_keeps_payload_validation_lazy() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "offset-validation.pile");
let payload = b"target";
let handle = Blob::<UnknownBlob>::new(Bytes::from_source(payload.to_vec())).get_handle();
let hash: Inline<Hash<Blake3>> = handle.into();
let first = append_v3_blob_candidate(&path, hash, payload, 1);
let second = append_v3_blob_candidate(&path, hash, b"bad-02", 2);
let third = append_v3_blob_candidate(&path, hash, b"bad-03", 3);
let mut pile = Pile::open(&path).unwrap();
pile.refresh().unwrap();
assert_eq!(
first_blob_occurrence(&pile.blobs, &hash.raw)
.unwrap()
.record_offset,
first
);
assert_eq!(pile.blobs.segmented_len(&hash.raw), 3);
for offset in [first, second, third] {
assert_eq!(
blob_occurrence_validation(&pile.blobs, &hash.raw, IndexEntry::new(offset))
.cached(),
None
);
}
let reader = pile.snapshot().unwrap();
let blob: Blob<UnknownBlob> = reader.get(handle).unwrap();
assert_eq!(blob.bytes.as_ref(), payload);
assert_eq!(
blob_occurrence_validation(&pile.blobs, &hash.raw, IndexEntry::new(first)).cached(),
Some(ValidationState::Validated)
);
assert_eq!(
blob_occurrence_validation(&pile.blobs, &hash.raw, IndexEntry::new(second)).cached(),
None
);
assert_eq!(
blob_occurrence_validation(&pile.blobs, &hash.raw, IndexEntry::new(third)).cached(),
None
);
drop(reader);
pile.close().unwrap();
}
#[test]
fn cold_replay_recovers_from_corrupt_primary_with_valid_duplicate() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "all-invalid.pile");
let payload = b"target";
let handle = Blob::<UnknownBlob>::new(Bytes::from_source(payload.to_vec())).get_handle();
let hash: Inline<Hash<Blake3>> = handle.into();
let first = append_v3_blob_candidate(&path, hash, b"bad-01", 1);
let second = append_v3_blob_candidate(&path, hash, payload, 2);
let third = append_v3_blob_candidate(&path, hash, b"bad-03", 3);
let mut pile = Pile::open(&path).unwrap();
pile.refresh().unwrap();
for offset in [first, second, third] {
assert_eq!(
blob_occurrence_validation(&pile.blobs, &hash.raw, IndexEntry::new(offset))
.cached(),
None
);
}
let reader = pile.snapshot().unwrap();
let blob: Blob<UnknownBlob> = reader.get(handle).unwrap();
assert_eq!(blob.bytes.as_ref(), payload);
let metadata = reader
.metadata(handle)
.unwrap()
.expect("valid fallback metadata");
assert_eq!(metadata.timestamp, 2);
assert_eq!(metadata.length, payload.len() as u64);
assert_eq!(
blob_occurrence_validation(&pile.blobs, &hash.raw, IndexEntry::new(first)).cached(),
Some(ValidationState::Invalid)
);
assert_eq!(
blob_occurrence_validation(&pile.blobs, &hash.raw, IndexEntry::new(second)).cached(),
Some(ValidationState::Validated)
);
assert_eq!(
blob_occurrence_validation(&pile.blobs, &hash.raw, IndexEntry::new(third)).cached(),
None
);
drop(reader);
pile.close().unwrap();
}
#[test]
fn all_invalid_duplicates_fail_after_lazy_exhaustion() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "all-invalid.pile");
let expected = b"target";
let handle = Blob::<UnknownBlob>::new(Bytes::from_source(expected.to_vec())).get_handle();
let hash: Inline<Hash<Blake3>> = handle.into();
let first = append_v3_blob_candidate(&path, hash, b"bad-01", 1);
let second = append_v3_blob_candidate(&path, hash, b"bad-02", 2);
let third = append_v3_blob_candidate(&path, hash, b"bad-03", 3);
let mut pile = Pile::open(&path).unwrap();
pile.refresh().unwrap();
for offset in [first, second, third] {
assert_eq!(
blob_occurrence_validation(&pile.blobs, &hash.raw, IndexEntry::new(offset))
.cached(),
None
);
}
let reader = pile.snapshot().unwrap();
assert!(matches!(
reader.get::<Blob<UnknownBlob>, UnknownBlob>(handle),
Err(GetBlobError::ValidationError(_))
));
assert!(reader.metadata(handle).unwrap().is_none());
for offset in [first, second, third] {
assert_eq!(
blob_occurrence_validation(&pile.blobs, &hash.raw, IndexEntry::new(offset))
.cached(),
Some(ValidationState::Invalid)
);
}
drop(reader);
pile.close().unwrap();
}
#[cfg(feature = "parallel")]
#[test]
fn parallel_validation_dispatch_respects_threshold_and_current_pool() {
let bytes = Bytes::from_source(vec![0xA5; PARALLEL_BLAKE3_THRESHOLD]);
let expected = Hash::<Blake3>::digest(&bytes);
let one_worker = rayon::ThreadPoolBuilder::new()
.num_threads(1)
.build()
.unwrap();
one_worker.install(|| {
assert!(!should_parallelize_validation(PARALLEL_BLAKE3_THRESHOLD));
assert!(matches!(
compute_validation_state(&bytes, &expected, ValidationStrategy::ParallelIfLarge),
ValidationState::Validated
));
});
let two_workers = rayon::ThreadPoolBuilder::new()
.num_threads(2)
.build()
.unwrap();
two_workers.install(|| {
assert!(!should_parallelize_validation(
PARALLEL_BLAKE3_THRESHOLD - 1
));
assert!(should_parallelize_validation(PARALLEL_BLAKE3_THRESHOLD));
assert!(matches!(
compute_validation_state(&bytes, &expected, ValidationStrategy::ParallelIfLarge),
ValidationState::Validated
));
assert!(matches!(
compute_validation_state(&bytes, &expected, ValidationStrategy::Serial),
ValidationState::Validated
));
});
}
#[cfg(feature = "parallel")]
#[test]
fn large_reader_get_uses_parallel_validation() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "large-reader.pile");
let payload = vec![0xC3; PARALLEL_BLAKE3_THRESHOLD + 17];
let mut writer = Pile::open(&path).unwrap();
let handle: Inline<Handle<UnknownBlob>> =
writer.put(Bytes::from_source(payload.clone())).unwrap();
writer.close().unwrap();
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(2)
.build()
.unwrap();
let mut replayed = Pile::open(&path).unwrap();
let reader = replayed.snapshot().unwrap();
pool.install(|| {
let blob = reader
.get::<Blob<UnknownBlob>, UnknownBlob>(handle)
.unwrap();
assert_eq!(blob.bytes.as_ref(), payload.as_slice());
});
drop(reader);
replayed.close().unwrap();
}
#[test]
fn open() {
const RECORD_LEN: usize = 1 << 10; const RECORD_COUNT: usize = 1 << 12;
let mut rng = rand::thread_rng();
let tmp_dir = tempfile::tempdir().unwrap();
let tmp_pile = fresh_empty_pile_path(&tmp_dir, "test.pile");
let mut pile: Pile = Pile::open(&tmp_pile).unwrap();
(0..RECORD_COUNT).for_each(|_| {
let mut record = Vec::with_capacity(RECORD_LEN);
rng.fill_bytes(&mut record);
let data: Blob<UnknownBlob> = Blob::new(Bytes::from_source(record));
pile.put::<UnknownBlob, _>(data).unwrap();
});
pile.close().unwrap();
let mut reopened: Pile = Pile::open(&tmp_pile).unwrap();
reopened.amputate().unwrap();
reopened.close().unwrap();
}
#[test]
fn put_enveloped_256_aligned_roundtrip() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "v3.pile");
let sizes = [1usize, 7, 33, 64, 100, 192, 255, 256, 257, 1000, 4096];
let mut hashes = Vec::new();
let mut datas: Vec<Vec<u8>> = Vec::new();
{
let mut pile: Pile = Pile::open(&path).unwrap();
for &sz in &sizes {
let data: Vec<u8> = (0..sz).map(|i| (i % 251) as u8).collect();
let blob: Blob<UnknownBlob> = Blob::new(Bytes::from_source(data.clone()));
let h = pile.put::<UnknownBlob, _>(blob).unwrap();
let hash: Inline<Hash<Blake3>> = h.into();
hashes.push(hash);
datas.push(data);
}
pile.close().unwrap();
}
let mut pile: Pile = Pile::open(&path).unwrap();
pile.amputate().unwrap();
for (hash, expected) in hashes.iter().zip(&datas) {
let entry = first_blob_occurrence(&pile.blobs, &hash.raw)
.expect("enveloped blob missing after reopen");
let record = indexed_blob_record(&pile.mmap, pile.applied_length, entry, hash);
assert_eq!(
record.payload_offset % GPU_DATA_ALIGNMENT,
0,
"enveloped data offset {} not {GPU_DATA_ALIGNMENT}-aligned (size {})",
record.payload_offset,
expected.len()
);
assert_eq!(
record.bytes.as_ref(),
&expected[..],
"enveloped roundtrip mismatch (size {})",
expected.len()
);
}
pile.close().unwrap();
}
#[test]
fn enveloped_cat_merge_preserves_all_blobs_and_alignment() {
let dir = tempfile::tempdir().unwrap();
let path_a = fresh_empty_pile_path(&dir, "a.pile");
let path_b = fresh_empty_pile_path(&dir, "b.pile");
let sizes = [1usize, 33, 100, 256, 257, 1000, 4096];
let mut handles: Vec<(Inline<Hash<Blake3>>, Vec<u8>)> = Vec::new();
{
let mut a: Pile = Pile::open(&path_a).unwrap();
for (k, &sz) in sizes.iter().enumerate() {
let data: Vec<u8> = (0..sz).map(|i| ((i + k) % 251) as u8).collect();
let blob: Blob<UnknownBlob> = Blob::new(Bytes::from_source(data.clone()));
let h: Inline<Hash<Blake3>> = a.put::<UnknownBlob, _>(blob).unwrap().into();
handles.push((h, data));
}
a.close().unwrap();
}
{
let mut b: Pile = Pile::open(&path_b).unwrap();
for (k, &sz) in sizes.iter().enumerate() {
let data: Vec<u8> = (0..sz).map(|i| ((i + k + 128) % 251) as u8).collect();
let blob: Blob<UnknownBlob> = Blob::new(Bytes::from_source(data.clone()));
let h: Inline<Hash<Blake3>> = b.put::<UnknownBlob, _>(blob).unwrap().into();
handles.push((h, data));
}
b.close().unwrap();
}
assert_eq!(
std::fs::metadata(&path_a).unwrap().len() % ENVELOPE_BLOCK_LEN as u64,
0
);
assert_eq!(
std::fs::metadata(&path_b).unwrap().len() % ENVELOPE_BLOCK_LEN as u64,
0
);
{
let a_bytes = std::fs::read(&path_a).unwrap();
let mut bf = std::fs::OpenOptions::new()
.append(true)
.open(&path_b)
.unwrap();
bf.write_all(&a_bytes).unwrap();
bf.sync_all().unwrap();
}
let merged_len = std::fs::metadata(&path_b).unwrap().len();
let mut merged: Pile = Pile::open(&path_b).unwrap();
merged.amputate().unwrap();
assert_eq!(
std::fs::metadata(&path_b).unwrap().len(),
merged_len,
"cat-merged pile was truncated — cat is not a valid framed merge"
);
for (hash, expected) in &handles {
let entry =
first_blob_occurrence(&merged.blobs, &hash.raw).expect("blob lost after cat-merge");
let record = indexed_blob_record(&merged.mmap, merged.applied_length, entry, hash);
assert_eq!(
record.payload_offset % ENVELOPE_BLOCK_LEN,
0,
"post-cat data offset not 256-aligned"
);
assert_eq!(
record.bytes.as_ref(),
&expected[..],
"blob bytes wrong after cat-merge"
);
}
assert_eq!(
std::fs::metadata(&path_b).unwrap().len() % ENVELOPE_BLOCK_LEN as u64,
0
);
merged.close().unwrap();
}
#[test]
fn v3_reader_still_reads_legacy_v1_records() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "legacy_v1.pile");
let data = vec![9u8; 40];
let blob: Blob<UnknownBlob> = Blob::new(Bytes::from_source(data.clone()));
let handle: Inline<Handle<UnknownBlob>> = blob.get_handle();
let hash: Inline<Hash<Blake3>> = handle.into();
{
let header = BlobHeader::new(42, data.len() as u64, hash);
let pad = padding_for_blob(data.len());
let mut f = std::fs::File::create(&path).unwrap();
f.write_all(header.as_bytes()).unwrap();
f.write_all(&data).unwrap();
f.write_all(&vec![0u8; pad]).unwrap();
f.sync_all().unwrap();
}
let mut pile: Pile = Pile::open(&path).unwrap();
pile.amputate().unwrap();
let reader = pile.snapshot().unwrap();
let fetched: Blob<UnknownBlob> = reader.get(handle).unwrap();
assert_eq!(
fetched.bytes.as_ref(),
data.as_slice(),
"legacy V1 blob not read by the current reader"
);
pile.close().unwrap();
}
#[test]
fn recover_shrink() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "pile.pile");
{
let mut pile: Pile = Pile::open(&path).unwrap();
let blob: Blob<UnknownBlob> = Blob::new(Bytes::from_source(vec![1u8; 20]));
pile.put::<UnknownBlob, _>(blob).unwrap();
pile.close().unwrap();
}
let file = OpenOptions::new().write(true).open(&path).unwrap();
let len = file.metadata().unwrap().len();
file.set_len(len - 10).unwrap();
let mut pile: Pile = Pile::open(&path).unwrap();
pile.amputate().unwrap();
pile.close().unwrap();
assert_eq!(std::fs::metadata(&path).unwrap().len(), 0);
}
#[test]
fn refresh_corrupt_reports_length() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "pile.pile");
{
let mut pile: Pile = Pile::open(&path).unwrap();
let blob: Blob<UnknownBlob> = Blob::new(Bytes::from_source(vec![1u8; 20]));
pile.put::<UnknownBlob, _>(blob).unwrap();
pile.close().unwrap();
}
let file_len = std::fs::metadata(&path).unwrap().len();
std::fs::OpenOptions::new()
.write(true)
.open(&path)
.unwrap()
.set_len(file_len - 10)
.unwrap();
let mut pile: Pile = Pile::open(&path).unwrap();
match pile.refresh() {
Err(ReadError::CorruptPile { valid_length }) => assert_eq!(valid_length, 0),
other => panic!("unexpected result: {other:?}"),
}
pile.close().unwrap();
}
#[test]
fn bounded_replay_stops_at_its_observed_prefix() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "bounded-refresh.pile");
let (first, second) = {
let mut writer = Pile::open(&path).unwrap();
let first = writer
.put::<UnknownBlob, _>(Bytes::from_source(b"first".to_vec()))
.unwrap();
let second = writer
.put::<UnknownBlob, _>(Bytes::from_source(b"second".to_vec()))
.unwrap();
writer.close().unwrap();
(first, second)
};
let first_end = {
let mut records = PileRecords::open(&path).unwrap();
let record = records.next().unwrap().unwrap();
record.offset + record.len
};
let mut replay = Pile::open(&path).unwrap();
assert!(matches!(
replay.apply_next_bounded(first_end).unwrap(),
Some(Applied::Blob { hash }) if hash.raw == first.raw
));
assert!(replay.apply_next_bounded(first_end).unwrap().is_none());
assert_eq!(replay.applied_length, first_end);
assert!(replay.blobs.has_prefix(&first.raw));
assert!(!replay.blobs.has_prefix(&second.raw));
replay.refresh().unwrap();
assert!(replay.blobs.has_prefix(&second.raw));
replay.close().unwrap();
}
#[test]
fn unknown_magic_reports_unsupported_without_amputation() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "pile.pile");
{
let mut pile: Pile = Pile::open(&path).unwrap();
let blob: Blob<UnknownBlob> = Blob::new(Bytes::from_source(vec![1u8; 20]));
pile.put::<UnknownBlob, _>(blob).unwrap();
pile.close().unwrap();
}
let valid_len = std::fs::metadata(&path).unwrap().len() as usize;
let unknown_marker = [0xA5u8; 16];
let mut unknown_record = [0u8; V3_HEADER_LEN];
unknown_record[..16].copy_from_slice(&unknown_marker);
std::fs::OpenOptions::new()
.append(true)
.open(&path)
.unwrap()
.write_all(&unknown_record)
.unwrap();
let length_with_unknown_record = std::fs::metadata(&path).unwrap().len();
let mut pile: Pile = Pile::open(&path).unwrap();
assert!(matches!(
pile.refresh(),
Err(ReadError::UnsupportedRecord { offset, marker })
if offset == valid_len && marker == unknown_marker
));
assert!(matches!(
pile.amputate(),
Err(ReadError::UnsupportedRecord { offset, marker })
if offset == valid_len && marker == unknown_marker
));
pile.close().unwrap();
assert_eq!(
std::fs::metadata(&path).unwrap().len(),
length_with_unknown_record,
"amputation must preserve a record whose marker this reader does not know"
);
}
#[test]
fn decoder_distinguishes_unknown_magic_from_truncated_known_record() {
let unknown_marker = [0xA5u8; 16];
assert!(matches!(
decode_record(&unknown_marker, ENVELOPE_BLOCK_LEN),
Err(ReadError::UnsupportedRecord { offset, marker })
if offset == ENVELOPE_BLOCK_LEN && marker == unknown_marker
));
assert!(matches!(
decode_record(&MAGIC_MARKER_BLOB_V3, ENVELOPE_BLOCK_LEN),
Err(ReadError::CorruptPile { valid_length })
if valid_length == ENVELOPE_BLOCK_LEN
));
}
#[test]
fn refresh_partial_header_reports_length() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "pile.pile");
{
let mut pile: Pile = Pile::open(&path).unwrap();
let blob: Blob<UnknownBlob> = Blob::new(Bytes::from_source(vec![1u8; 20]));
pile.put::<UnknownBlob, _>(blob).unwrap();
pile.close().unwrap();
}
let file_len = std::fs::metadata(&path).unwrap().len();
std::fs::OpenOptions::new()
.write(true)
.open(&path)
.unwrap()
.set_len(file_len + 8)
.unwrap();
let mut pile: Pile = Pile::open(&path).unwrap();
match pile.refresh() {
Err(ReadError::CorruptPile { valid_length }) => {
assert_eq!(valid_length as u64, file_len)
}
other => panic!("unexpected result: {other:?}"),
}
pile.close().unwrap();
}
#[test]
fn refresh_length_beyond_file_reports_length() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "pile.pile");
{
let mut pile: Pile = Pile::open(&path).unwrap();
let blob: Blob<UnknownBlob> = Blob::new(Bytes::from_source(vec![1u8; 20]));
pile.put::<UnknownBlob, _>(blob).unwrap();
pile.close().unwrap();
}
use std::io::Seek;
use std::io::SeekFrom;
use std::io::Write;
let mut file = OpenOptions::new()
.read(true)
.write(true)
.open(&path)
.unwrap();
file.seek(SeekFrom::Start(72)).unwrap();
file.write_all(&(1_000_000u64).to_le_bytes()).unwrap();
file.flush().unwrap();
drop(file);
let mut pile: Pile = Pile::open(&path).unwrap();
match pile.refresh() {
Err(ReadError::CorruptPile { valid_length }) => assert_eq!(valid_length, 0),
other => panic!("unexpected result: {other:?}"),
}
pile.close().unwrap();
}
#[test]
fn amputate_truncates_length_beyond_file() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "pile.pile");
{
let mut pile: Pile = Pile::open(&path).unwrap();
let blob: Blob<UnknownBlob> = Blob::new(Bytes::from_source(vec![1u8; 20]));
pile.put::<UnknownBlob, _>(blob).unwrap();
pile.close().unwrap();
}
use std::io::Seek;
use std::io::SeekFrom;
use std::io::Write;
let mut file = OpenOptions::new()
.read(true)
.write(true)
.open(&path)
.unwrap();
file.seek(SeekFrom::Start(72)).unwrap();
file.write_all(&(1_000_000u64).to_le_bytes()).unwrap();
file.flush().unwrap();
drop(file);
let mut pile: Pile = Pile::open(&path).unwrap();
pile.amputate().unwrap();
pile.close().unwrap();
assert_eq!(std::fs::metadata(&path).unwrap().len(), 0);
}
#[test]
fn put_and_get_preserves_blob_bytes() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "pile.pile");
let mut pile: Pile = Pile::open(&path).unwrap();
let data = vec![42u8; 100];
let blob: Blob<UnknownBlob> = Blob::new(Bytes::from_source(data.clone()));
let handle = pile.put::<UnknownBlob, _>(blob).unwrap();
{
let reader = pile.snapshot().unwrap();
let fetched: Blob<UnknownBlob> = reader.get(handle).unwrap();
assert_eq!(fetched.bytes.as_ref(), data.as_slice());
}
pile.close().unwrap();
let mut pile: Pile = Pile::open(&path).unwrap();
pile.amputate().unwrap();
let reader = pile.snapshot().unwrap();
let fetched: Blob<UnknownBlob> = reader.get(handle).unwrap();
assert_eq!(fetched.bytes.as_ref(), data.as_slice());
pile.close().unwrap();
}
#[test]
fn iter_lists_all_blobs_handles() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "pile.pile");
let mut pile: Pile = Pile::open(&path).unwrap();
let blobs = vec![vec![1u8; 3], vec![2u8; 4], vec![3u8; 5]];
let mut expected = HashMap::new();
for data in blobs {
let blob: Blob<UnknownBlob> = Blob::new(Bytes::from_source(data.clone()));
let handle = pile.put::<UnknownBlob, _>(blob).unwrap();
expected.insert(handle, data);
}
pile.flush().unwrap();
let reader = pile.snapshot().unwrap();
for item in reader.iter() {
let (handle, blob) = item.expect("infallible iteration");
let data = expected.remove(&handle).unwrap();
assert_eq!(blob.bytes.as_ref(), data.as_slice());
}
assert!(expected.is_empty());
pile.close().unwrap();
}
#[test]
fn blobs_diff_returns_only_new_handles() {
use crate::repo::BlobStoreList;
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "pile.pile");
let mut pile: Pile = Pile::open(&path).unwrap();
let mut baseline_handles: HashSet<Inline<Handle<UnknownBlob>>> = HashSet::new();
for data in [vec![1u8; 3], vec![2u8; 4], vec![3u8; 5]] {
let blob: Blob<UnknownBlob> = Blob::new(Bytes::from_source(data));
let handle = pile.put::<UnknownBlob, _>(blob).unwrap();
baseline_handles.insert(handle);
}
let baseline = pile.snapshot().unwrap();
let mut new_handles: HashSet<Inline<Handle<UnknownBlob>>> = HashSet::new();
for data in [vec![4u8; 6], vec![5u8; 7]] {
let blob: Blob<UnknownBlob> = Blob::new(Bytes::from_source(data));
let handle = pile.put::<UnknownBlob, _>(blob).unwrap();
new_handles.insert(handle);
}
let current = pile.snapshot().unwrap();
let diffed: HashSet<Inline<Handle<UnknownBlob>>> = current
.blobs_diff(&baseline)
.map(|r| r.expect("infallible diff iter").handle)
.collect();
assert_eq!(diffed, new_handles);
for h in &baseline_handles {
assert!(!diffed.contains(h), "baseline blob leaked into diff");
}
let empty: HashSet<_> = current
.blobs_diff(¤t)
.map(|r| r.expect("infallible").handle)
.collect();
assert!(empty.is_empty());
pile.close().unwrap();
}
#[test]
fn duplicate_occurrence_changes_physical_snapshot_but_not_semantic_diff() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "duplicate-diff.pile");
let payload = b"same semantic blob";
let handle = Blob::<UnknownBlob>::new(Bytes::from_source(payload.to_vec())).get_handle();
let hash: Inline<Hash<Blake3>> = handle.into();
let first = append_v3_blob_candidate(&path, hash, payload, 1);
let mut pile = Pile::open(&path).unwrap();
let baseline = pile.snapshot().unwrap();
assert_eq!(baseline.blobs.len(), 1);
assert_eq!(baseline.blobs.segmented_len(&hash.raw), 1);
assert_eq!(baseline.blobs().count(), 1);
assert_eq!(baseline.iter().count(), 1);
let second = append_v3_blob_candidate(&path, hash, payload, 2);
let current = pile.snapshot().unwrap();
assert!(second > first);
assert_eq!(baseline.blobs.len(), 1);
assert_eq!(current.blobs.len(), 2);
assert!(!baseline.blobs.shares_root(¤t.blobs));
assert_eq!(baseline.blobs.segmented_len(&hash.raw), 1);
assert_eq!(current.blobs.segmented_len(&hash.raw), 2);
assert_eq!(
baseline.blobs.prefix_set::<32>().iter().collect::<Vec<_>>(),
current.blobs.prefix_set::<32>().iter().collect::<Vec<_>>()
);
assert_eq!(current.changes_since(&baseline), StoreChanges::BLOBS);
assert_eq!(current.blobs().count(), 1);
assert_eq!(current.iter().count(), 1);
assert!(current.blobs_diff(&baseline).next().is_none());
assert!(baseline.blobs_diff(¤t).next().is_none());
pile.close().unwrap();
}
#[test]
fn keyed_blob_info_reports_index_length_without_payload_access() {
use crate::repo::BlobStoreList;
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "pile.pile");
let mut pile = Pile::open(&path).unwrap();
let handle = pile
.put::<UnknownBlob, _>(Blob::<UnknownBlob>::new(Bytes::from_source(vec![7u8; 13])))
.unwrap();
let reader = pile.snapshot().unwrap();
let info = BlobStoreList::blob_info(&reader, handle)
.unwrap()
.expect("stored blob has keyed information");
assert_eq!(info.handle, handle);
assert_eq!(info.length, 13);
let absent = Inline::<Handle<UnknownBlob>>::new([0xA5; 32]);
assert!(BlobStoreList::blob_info(&reader, absent).unwrap().is_none());
pile.close().unwrap();
}
#[test]
fn metadata_reflects_length_and_timestamp() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "pile.pile");
let mut pile: Pile = Pile::open(&path).unwrap();
let before = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis() as u64;
let data = vec![9u8; 10];
let blob: Blob<UnknownBlob> = Blob::new(Bytes::from_source(data.clone()));
let handle = pile.put::<UnknownBlob, _>(blob).unwrap();
pile.flush().unwrap();
let reader = pile.snapshot().unwrap();
let metadata = reader.metadata(handle).unwrap().expect("metadata");
assert_eq!(metadata.length, data.len() as u64);
let after = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis() as u64;
assert!(metadata.timestamp >= before && metadata.timestamp <= after);
pile.close().unwrap();
}
#[test]
fn listing_reports_header_lengths() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "pile.pile");
let mut pile: Pile = Pile::open(&path).unwrap();
let first = pile
.put::<UnknownBlob, _>(Blob::new(Bytes::from_source(vec![1u8; 3])))
.unwrap();
let second = pile
.put::<UnknownBlob, _>(Blob::new(Bytes::from_source(vec![2u8; 17])))
.unwrap();
let reader = pile.snapshot().unwrap();
let listed: HashMap<_, _> = reader
.blobs()
.map(|result| {
let info = result.expect("infallible listing");
(info.handle, info.length)
})
.collect();
assert_eq!(listed.get(&first), Some(&3));
assert_eq!(listed.get(&second), Some(&17));
pile.close().unwrap();
}
#[test]
fn metadata_returns_none_for_unflushed_blob() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "pile.pile");
let mut pile: Pile = Pile::open(&path).unwrap();
let reader = pile.snapshot().unwrap();
let blob: Blob<UnknownBlob> = Blob::new(Bytes::from_source(vec![1u8; 4]));
let handle = pile.put::<UnknownBlob, _>(blob).unwrap();
assert!(reader.metadata(handle).unwrap().is_none());
pile.flush().unwrap();
let reader = pile.snapshot().unwrap();
assert!(reader.metadata(handle).unwrap().is_some());
pile.close().unwrap();
}
#[test]
fn blob_after_branch_is_clean() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "pile.pile");
let mut pile: Pile = Pile::open(&path).unwrap();
let branch_id = Id::new([1; 16]).unwrap();
let head = Inline::<Handle<SimpleArchive>>::new([2; 32]);
pile.append_legacy_pin_for_test(branch_id, None, Some(head))
.unwrap();
let data = vec![3u8; 8];
let blob: Blob<UnknownBlob> = Blob::new(Bytes::from_source(data.clone()));
let handle = pile.put::<UnknownBlob, _>(blob).unwrap();
pile.flush().unwrap();
let stored: Blob<UnknownBlob> = pile.snapshot().unwrap().get(handle).unwrap();
assert_eq!(stored.bytes.as_ref(), &data[..]);
pile.close().unwrap();
}
#[test]
fn insert_after_branch_preserves_head() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "pile.pile");
let mut pile: Pile = Pile::open(&path).unwrap();
let blob1: Blob<UnknownBlob> = Blob::new(Bytes::from_source(vec![1u8; 5]));
let handle1 = pile.put::<UnknownBlob, _>(blob1).unwrap();
let branch_id = Id::new([1u8; 16]).unwrap();
pile.append_legacy_pin_for_test(branch_id, None, Some(handle1.transmute()))
.unwrap();
let blob2: Blob<UnknownBlob> = Blob::new(Bytes::from_source(vec![2u8; 5]));
pile.put::<UnknownBlob, _>(blob2).unwrap();
pile.close().unwrap();
let mut pile: Pile = Pile::open(&path).unwrap();
pile.amputate().unwrap();
let head = pile.legacy_pin_head_for_test(branch_id).unwrap();
assert_eq!(head, Some(handle1.transmute()));
pile.close().unwrap();
}
#[test]
fn branch_update_survives_manual_flush() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "pile.pile");
let branch_id = Id::new([1u8; 16]).unwrap();
let handle = {
let mut pile: Pile = Pile::open(&path).unwrap();
let blob: Blob<UnknownBlob> = Blob::new(Bytes::from_source(vec![3u8; 5]));
let handle = pile.put::<UnknownBlob, _>(blob).unwrap();
pile.append_legacy_pin_for_test(branch_id, None, Some(handle.transmute()))
.unwrap();
pile.flush().unwrap();
std::mem::forget(pile);
handle
};
let mut pile: Pile = Pile::open(&path).unwrap();
pile.amputate().unwrap();
assert_eq!(
pile.legacy_pin_head_for_test(branch_id).unwrap(),
Some(handle.transmute())
);
assert!(std::fs::metadata(&path).unwrap().len() > 0);
pile.close().unwrap();
}
#[test]
fn branch_tombstone_removes_head_and_listing() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "pile.pile");
let mut pile: Pile = Pile::open(&path).unwrap();
let blob: Blob<UnknownBlob> = Blob::new(Bytes::from_source(vec![1u8; 5]));
let h = pile.put::<UnknownBlob, _>(blob).unwrap();
let branch_id = Id::new([7u8; 16]).unwrap();
pile.append_legacy_pin_for_test(branch_id, None, Some(h.transmute()))
.unwrap();
pile.flush().unwrap();
assert_eq!(
pile.legacy_pin_head_for_test(branch_id).unwrap(),
Some(h.transmute())
);
pile.append_legacy_pin_for_test(branch_id, Some(h.transmute()), None)
.unwrap();
pile.flush().unwrap();
assert_eq!(pile.legacy_pin_head_for_test(branch_id).unwrap(), None);
pile.refresh().unwrap();
let branches: HashSet<Id> = pile
.branches
.iter_ordered()
.map(|raw| Id::new(*raw).expect("legacy pin ids are non-nil"))
.collect();
assert!(!branches.contains(&branch_id));
pile.close().unwrap();
}
#[test]
fn metadata_returns_length_and_timestamp() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "pile.pile");
let mut pile: Pile = Pile::open(&path).unwrap();
let blob: Blob<UnknownBlob> = Blob::new(Bytes::from_source(vec![7u8; 32]));
let handle = pile.put::<UnknownBlob, _>(blob).unwrap();
pile.close().unwrap();
let mut pile: Pile = Pile::open(&path).unwrap();
pile.amputate().unwrap();
let reader = pile.snapshot().unwrap();
let meta = reader.metadata(handle).unwrap().expect("metadata");
assert_eq!(meta.length, 32);
assert!(meta.timestamp > 0);
pile.close().unwrap();
}
#[test]
fn iter_lists_all_blobs() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "pile.pile");
let mut pile: Pile = Pile::open(&path).unwrap();
let blob1: Blob<UnknownBlob> = Blob::new(Bytes::from_source(vec![1u8; 4]));
let h1 = pile.put::<UnknownBlob, _>(blob1).unwrap();
let blob2: Blob<UnknownBlob> = Blob::new(Bytes::from_source(vec![2u8; 4]));
let h2 = pile.put::<UnknownBlob, _>(blob2).unwrap();
pile.flush().unwrap();
let reader = pile.snapshot().unwrap();
let handles: Vec<_> = reader
.iter()
.map(|res| res.expect("infallible iteration").0)
.collect();
assert!(handles.contains(&h1));
assert!(handles.contains(&h2));
assert_eq!(handles.len(), 2);
pile.close().unwrap();
}
#[test]
fn refresh_errors_on_malformed_append() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "pile.pile");
let mut pile: Pile = Pile::open(&path).unwrap();
let blob: Blob<UnknownBlob> = Blob::new(Bytes::from_source(vec![1u8; 4]));
pile.put::<UnknownBlob, _>(blob).unwrap();
pile.flush().unwrap();
use std::io::Write;
{
let mut file = std::fs::OpenOptions::new()
.append(true)
.open(&path)
.unwrap();
file.write_all(b"garbage").unwrap();
file.sync_all().unwrap();
}
assert!(pile.refresh().is_err());
pile.close().unwrap();
}
#[test]
fn amputate_truncates_corrupt_tail() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "pile.pile");
let mut pile: Pile = Pile::open(&path).unwrap();
let data = vec![1u8; 4];
let blob: Blob<UnknownBlob> = Blob::new(Bytes::from_source(data.clone()));
let handle = pile.put::<UnknownBlob, _>(blob).unwrap();
pile.flush().unwrap();
use std::io::Write;
{
let mut file = std::fs::OpenOptions::new()
.append(true)
.open(&path)
.unwrap();
file.write_all(b"garbage").unwrap();
file.sync_all().unwrap();
}
pile.amputate().unwrap();
let expected_len =
(super::ENVELOPE_HEADER_LEN + data.len() + super::block_post_pad(data.len())) as u64;
assert_eq!(std::fs::metadata(&path).unwrap().len(), expected_len);
let reader = pile.snapshot().unwrap();
let fetched: Blob<UnknownBlob> = reader.get(handle).unwrap();
assert_eq!(fetched.bytes.as_ref(), data.as_slice());
pile.close().unwrap();
}
#[test]
fn refresh_replaces_corrupt_blob_with_new_candidate() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "pile.pile");
let mut pile1: Pile = Pile::open(&path).unwrap();
let mut pile2: Pile = Pile::open(&path).unwrap();
let data = vec![1u8; 4];
let blob: Blob<UnknownBlob> = Blob::new(Bytes::from_source(data.clone()));
let handle = pile1.put(blob).unwrap();
pile1.flush().unwrap();
pile1.refresh().unwrap();
let before_replacement = pile1.snapshot().unwrap();
use std::io::Seek;
use std::io::SeekFrom;
use std::io::Write;
let mut file = std::fs::OpenOptions::new().write(true).open(&path).unwrap();
file.seek(SeekFrom::Start(ENVELOPE_HEADER_LEN as u64))
.unwrap();
file.write_all(&[9u8; 4]).unwrap();
file.sync_all().unwrap();
let blob_dup: Blob<UnknownBlob> = Blob::new(Bytes::from_source(data.clone()));
pile2.put::<UnknownBlob, _>(blob_dup).unwrap();
pile2.flush().unwrap();
let after_replacement = pile1.snapshot().unwrap();
assert_eq!(
after_replacement.changes_since(&before_replacement),
StoreChanges::BLOBS,
"same-handle backing replacement changes observable blob access",
);
let reader = pile1.snapshot().unwrap();
let fetched: Blob<UnknownBlob> = reader.get(handle).unwrap();
assert_eq!(fetched.bytes.as_ref(), data.as_slice());
pile1.close().unwrap();
pile2.close().unwrap();
}
#[test]
fn snapshot_changes_separate_external_wants_from_inventory_membership() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "revision.pile");
let mut observer = Pile::open(&path).unwrap();
let mut writer = Pile::open(&path).unwrap();
let empty = observer.snapshot().unwrap();
let unchanged = observer.snapshot().unwrap();
assert_eq!(empty.clone().changes_since(&empty), StoreChanges::NONE);
assert_eq!(unchanged.changes_since(&empty), StoreChanges::NONE);
writer
.want(WantRequest::blob(Inline::<Handle<UnknownBlob>>::new(
[0xA1; 32],
)))
.unwrap();
writer.flush().unwrap();
let after_external_want = observer.snapshot().unwrap();
assert_eq!(
after_external_want.changes_since(&empty),
StoreChanges::WANTS
);
assert!(empty.wants().unwrap().next().is_none());
assert_eq!(after_external_want.wants().unwrap().count(), 1);
writer
.put::<UnknownBlob, _>(Blob::new(Bytes::from_source(vec![0xB2; 17])))
.unwrap();
writer.flush().unwrap();
let after_external_blob = observer.snapshot().unwrap();
assert_eq!(
after_external_blob.changes_since(&after_external_want),
StoreChanges::BLOBS,
);
writer.insert(collection_test_records()[0]).unwrap();
writer.flush().unwrap();
let after_external_record = observer.snapshot().unwrap();
assert_eq!(
after_external_record.changes_since(&after_external_blob),
StoreChanges::COLLECTION_RECORDS,
);
let proof = capability_fixture(81, [82; 32]);
writer.insert_proof(proof).unwrap();
writer.flush().unwrap();
let after_external_proof = observer.snapshot().unwrap();
assert_eq!(
after_external_proof.changes_since(&after_external_record),
StoreChanges::CAPABILITY_PROOFS,
);
observer.close().unwrap();
writer.close().unwrap();
}
#[test]
fn snapshot_dependencies_track_late_raw_records_without_unrelated_collections() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "scoped-records.pile");
let mut observer = Pile::open(&path).unwrap();
let mut writer = Pile::open(&path).unwrap();
let key = SigningKey::from_bytes(&[91; 32]);
let source = collection_test_collection(92);
let target = collection_test_collection(93);
let other = collection_test_collection(94);
let input = CollectionCommit::sign(
&key,
source,
collection_test_hash(95),
empty_metadata_handle(),
);
let second_input = CollectionCommit::sign(
&key,
source,
collection_test_hash(96),
empty_metadata_handle(),
);
let output = collection_test_hash(97);
let produced = CollectionRecord::Derive(CollectionDerive::sign(
&key,
target,
crate::collection::SourceLocator::of(input.data().raw),
output,
));
let routes = [
CollectionRecordSelector::Collection(target),
CollectionRecordSelector::ProducedMember(target, output),
];
let untouched = CollectionRecordSelector::ProducedMember(source, input.data());
let dependencies = StoreDependencies {
records: routes.into_iter().collect(),
..StoreDependencies::default()
};
let empty = observer.snapshot().unwrap();
writer
.insert(CollectionRecord::Commit(CollectionCommit::sign(
&key,
other,
output,
empty_metadata_handle(),
)))
.unwrap();
let unrelated = observer.snapshot().unwrap();
assert_eq!(
unrelated.changes_for(&empty, &dependencies),
StoreChanges::NONE
);
assert_eq!(
unrelated.changes_for(
&empty,
&StoreDependencies {
all_records: true,
..StoreDependencies::default()
}
),
StoreChanges::COLLECTION_RECORDS,
);
writer.insert(produced).unwrap();
let realized = observer.snapshot().unwrap();
for route in routes {
let exact = StoreDependencies {
records: BTreeSet::from([route]),
..StoreDependencies::default()
};
assert_eq!(
realized.changes_for(&unrelated, &exact),
StoreChanges::COLLECTION_RECORDS,
"{route:?}",
);
assert!(unrelated.select_records(&exact.records).unwrap().is_empty());
assert_eq!(
realized.select_records(&exact.records).unwrap(),
vec![produced]
);
assert_eq!(
realized.changes_for(&realized.clone(), &exact),
StoreChanges::NONE
);
}
let alternative = CollectionRecord::Derive(CollectionDerive::sign(
&key,
target,
crate::collection::SourceLocator::of(second_input.data().raw),
output,
));
writer.insert(alternative).unwrap();
let alternate_witness = observer.snapshot().unwrap();
assert_eq!(
alternate_witness.changes_for(
&realized,
&StoreDependencies {
records: BTreeSet::from([CollectionRecordSelector::ProducedMember(
target, output
)]),
..StoreDependencies::default()
}
),
StoreChanges::COLLECTION_RECORDS,
);
for unchanged in [untouched] {
assert_eq!(
alternate_witness.changes_for(
&realized,
&StoreDependencies {
records: BTreeSet::from([unchanged]),
..StoreDependencies::default()
}
),
StoreChanges::NONE,
);
}
writer.insert(CollectionRecord::Commit(input)).unwrap();
let input_arrived = observer.snapshot().unwrap();
assert_eq!(
input_arrived.changes_for(&alternate_witness, &dependencies),
StoreChanges::NONE
);
assert_eq!(
input_arrived.changes_for(
&alternate_witness,
&StoreDependencies {
records: BTreeSet::from([untouched]),
..StoreDependencies::default()
}
),
StoreChanges::COLLECTION_RECORDS,
);
assert!(realized
.select_records(&BTreeSet::from([untouched]))
.unwrap()
.is_empty());
assert_eq!(
realized.select_records(&dependencies.records).unwrap(),
vec![produced]
);
assert_eq!(realized.blobs().count(), 0);
observer.close().unwrap();
writer.close().unwrap();
}
#[test]
fn snapshot_dependencies_notice_missing_and_recovered_blob_occurrences() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "scoped-blobs.pile");
let payload = b"desired content";
let handle = Blob::<UnknownBlob>::new(Bytes::from_source(payload.to_vec())).get_handle();
let hash: CollectionData = handle.into();
let dependencies = StoreDependencies {
blobs: BTreeSet::from([hash]),
..StoreDependencies::default()
};
let mut observer = Pile::open(&path).unwrap();
let empty = observer.snapshot().unwrap();
let other = b"unrelated content";
let other_hash = Blob::<UnknownBlob>::new(Bytes::from_source(other.to_vec()))
.get_handle()
.into();
append_v3_blob_candidate(&path, other_hash, other, 1);
let unrelated = observer.snapshot().unwrap();
assert_eq!(
unrelated.changes_for(&empty, &dependencies),
StoreChanges::NONE
);
assert_eq!(
unrelated.changes_for(
&empty,
&StoreDependencies {
all_blobs: true,
..StoreDependencies::default()
}
),
StoreChanges::BLOBS,
);
append_v3_blob_candidate(&path, hash, b"corrupt content", 2);
let invalid = observer.snapshot().unwrap();
assert_eq!(
invalid.changes_for(&unrelated, &dependencies),
StoreChanges::BLOBS
);
assert!(matches!(
unrelated.get::<Blob<UnknownBlob>, UnknownBlob>(handle),
Err(GetBlobError::BlobNotFound(_)),
));
assert!(matches!(
invalid.get::<Blob<UnknownBlob>, UnknownBlob>(handle),
Err(GetBlobError::ValidationError(_)),
));
append_v3_blob_candidate(&path, hash, payload, 3);
let recovered = observer.snapshot().unwrap();
assert_eq!(
recovered.changes_for(&invalid, &dependencies),
StoreChanges::BLOBS
);
assert!(recovered.blobs_diff(&invalid).next().is_none());
assert_eq!(
recovered
.get::<Blob<UnknownBlob>, UnknownBlob>(handle)
.unwrap()
.bytes
.as_ref(),
payload,
);
assert!(matches!(
invalid.get::<Blob<UnknownBlob>, UnknownBlob>(handle),
Err(GetBlobError::ValidationError(_)),
));
assert_eq!(
recovered.changes_for(
&invalid,
&StoreDependencies {
blobs: BTreeSet::from([other_hash]),
..StoreDependencies::default()
}
),
StoreChanges::NONE
);
assert_eq!(
recovered.changes_for(&recovered.clone(), &dependencies),
StoreChanges::NONE
);
observer.close().unwrap();
}
#[test]
fn snapshot_dependencies_include_only_requested_proofs_and_ignore_wants() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "scoped-proofs.pile");
let mut observer = Pile::open(&path).unwrap();
let mut writer = Pile::open(&path).unwrap();
let empty = observer.snapshot().unwrap();
let all_reads = StoreDependencies {
all_records: true,
all_blobs: true,
capability_proofs: true,
..StoreDependencies::default()
};
writer
.want(WantRequest::blob(Inline::<Handle<UnknownBlob>>::new(
[98; 32],
)))
.unwrap();
let wants_only = observer.snapshot().unwrap();
assert_eq!(wants_only.changes_since(&empty), StoreChanges::WANTS);
assert_eq!(
wants_only.changes_for(&empty, &all_reads),
StoreChanges::NONE
);
writer
.insert_proof(capability_fixture(99, [100; 32]))
.unwrap();
let proof_arrived = observer.snapshot().unwrap();
assert_eq!(
proof_arrived.changes_for(&wants_only, &all_reads),
StoreChanges::CAPABILITY_PROOFS
);
assert_eq!(
proof_arrived.changes_for(
&wants_only,
&StoreDependencies {
capability_proofs: false,
..all_reads.clone()
}
),
StoreChanges::NONE
);
assert_eq!(
proof_arrived.changes_for(&wants_only, &StoreDependencies::default()),
StoreChanges::NONE
);
assert_eq!(wants_only.proofs().unwrap().count(), 0);
assert_eq!(proof_arrived.proofs().unwrap().count(), 1);
observer.close().unwrap();
writer.close().unwrap();
}
#[test]
fn put_duplicate_blob_does_not_grow_file() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "pile.pile");
let mut pile: Pile = Pile::open(&path).unwrap();
let data = vec![9u8; 32];
let blob: Blob<UnknownBlob> = Blob::new(Bytes::from_source(data.clone()));
let handle1 = pile.put::<UnknownBlob, _>(blob).unwrap();
pile.flush().unwrap();
let len_after_first = std::fs::metadata(&path).unwrap().len();
let blob_dup: Blob<UnknownBlob> = Blob::new(Bytes::from_source(data));
let handle2 = pile.put(blob_dup).unwrap();
pile.flush().unwrap();
let len_after_second = std::fs::metadata(&path).unwrap().len();
assert_eq!(handle1, handle2);
assert_eq!(len_after_first, len_after_second);
pile.close().unwrap();
}
#[test]
fn iterator_reflects_snapshot_occurrence_relation() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "pile.pile");
let mut pile: Pile = Pile::open(&path).unwrap();
let blob1: Blob<UnknownBlob> = Blob::new(Bytes::from_source(b"hello".as_slice()));
let blob2: Blob<UnknownBlob> = Blob::new(Bytes::from_source(b"world".as_slice()));
let handle1 = pile.put::<UnknownBlob, _>(blob1).unwrap();
let handle2 = pile.put::<UnknownBlob, _>(blob2).unwrap();
pile.flush().unwrap();
let mut reader = pile.snapshot().unwrap();
let hash1: Inline<Hash<Blake3>> = handle1.into();
let entry = first_blob_occurrence(&reader.blobs, &hash1.raw).unwrap();
reader.blobs.remove(&blob_occurrence_key(&hash1.raw, entry));
let mut iter = reader.iter();
assert_eq!(iter.next().unwrap().unwrap().0, handle2);
assert!(iter.next().is_none());
pile.close().unwrap();
}
#[test]
fn metadata_reports_blob_length() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "pile.pile");
let mut pile: Pile = Pile::open(&path).unwrap();
let data = vec![7u8; 16];
let blob: Blob<UnknownBlob> = Blob::new(Bytes::from_source(data.clone()));
let handle = pile.put::<UnknownBlob, _>(blob).unwrap();
pile.flush().unwrap();
let reader = pile.snapshot().unwrap();
let meta = reader.metadata(handle).unwrap().expect("metadata");
assert_eq!(meta.length, data.len() as u64);
pile.close().unwrap();
}
#[test]
fn want_survives_reopen() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "pile.pile");
let wanted: Inline<Handle<UnknownBlob>> =
Blob::<UnknownBlob>::new(Bytes::from_source(vec![7u8; 21])).get_handle();
let mut pile: Pile = Pile::open(&path).unwrap();
pile.want(WantRequest::blob(wanted)).unwrap();
let pinned: HashSet<_> = pile
.snapshot()
.unwrap()
.wants()
.unwrap()
.map(|r| r.unwrap())
.collect();
assert!(pinned.contains(&WantRequest::blob(wanted)));
pile.close().unwrap();
let mut reopened: Pile = Pile::open(&path).unwrap();
reopened.amputate().unwrap();
let pinned: HashSet<_> = reopened
.snapshot()
.unwrap()
.wants()
.unwrap()
.map(|r| r.unwrap())
.collect();
assert_eq!(pinned.len(), 1);
assert!(
pinned.contains(&WantRequest::blob(wanted)),
"want lost across reopen — restart amnesia"
);
reopened.close().unwrap();
}
#[test]
fn blob_wants_use_the_current_marker() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "blob-want-projection.pile");
let handle = Inline::<Handle<UnknownBlob>>::new([47; 32]);
let mut pile = Pile::open(&path).unwrap();
pile.want(WantRequest::blob(handle)).unwrap();
pile.close().unwrap();
let records = PileRecords::open(&path)
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap();
assert!(matches!(
records[0].content,
PileRecordContent::Want { request }
if request == WantRequest::blob(handle)
));
assert_eq!(records.len(), 1);
}
#[test]
fn current_want_marker_accepts_blob_and_keeps_retired_kinds_inert() {
let request = WantRequest::blob(Inline::<Handle<UnknownBlob>>::new([48; 32]));
let header = WantRecordHeader::new(request);
assert!(matches!(
decode_record(header.as_bytes(), 0).unwrap().content,
PileRecordContent::Want { request: actual } if actual == request
));
let mut header = retired_typed_derive_v1_record(
collection_test_collection(49),
collection_test_collection(50),
collection_test_hash(51),
true,
);
header.record_kind = record_kind::KIND_WANT;
assert!(matches!(
decode_record(header.as_bytes(), 0).unwrap().content,
PileRecordContent::RetiredWantAssert { identity }
if identity[0] == WANT_REQUEST_KIND_DERIVE_V1
&& identity[1..33] == collection_test_collection(49).raw
));
let target = collection_test_collection(52);
let input = collection_test_hash(53);
let legacy = TypedWantHeaderEnvelopeV1 {
envelope_marker: MAGIC_MARKER_ENVELOPE,
record_kind: MAGIC_MARKER_WANT_ASSERT_V2,
span_blocks: ENVELOPE_HEADER_BLOCKS.to_le_bytes(),
request_kind: WANT_REQUEST_KIND_DERIVE_V1,
field_a: collection_test_collection(51).raw,
field_b: target.raw,
field_c: input.raw,
reserved: [0; 123],
};
assert!(matches!(
decode_record(legacy.as_bytes(), 0).unwrap().content,
PileRecordContent::RetiredWantAssert { identity }
if identity[0] == WANT_REQUEST_KIND_DERIVE_V1
&& identity[33..65] == target.raw
&& identity[65..97] == input.raw
));
}
#[test]
fn retired_wants_are_inert_until_idempotent_additive_cutover() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "pile.pile");
let a = WantRequest::blob(Inline::<Handle<UnknownBlob>>::new([61; 32]));
let b = WantRequest::blob(Inline::<Handle<UnknownBlob>>::new([62; 32]));
let c = WantRequest::blob(Inline::<Handle<UnknownBlob>>::new([63; 32]));
for record in [
retired_blob_want_record(a, true).as_bytes().to_vec(),
retired_blob_want_record(a, false).as_bytes().to_vec(),
retired_blob_want_record(a, true).as_bytes().to_vec(),
retired_blob_want_record(b, true).as_bytes().to_vec(),
retired_blob_want_record(b, false).as_bytes().to_vec(),
retired_blob_want_record(c, true).as_bytes().to_vec(),
retired_typed_derive_v1_record(
collection_test_collection(66),
collection_test_collection(67),
collection_test_hash(68),
true,
)
.as_bytes()
.to_vec(),
retired_typed_derive_v1_record(
collection_test_collection(69),
collection_test_collection(67),
collection_test_hash(68),
false,
)
.as_bytes()
.to_vec(),
] {
append_test_bytes(&path, &record);
}
let mut pile = Pile::open(&path).unwrap();
assert!(pile.snapshot().unwrap().wants().unwrap().next().is_none());
pile.want(c).unwrap();
let before = std::fs::metadata(&path).unwrap().len();
assert_eq!(
pile.want_cutover_status().unwrap(),
WantCutoverStatus {
retired_records: 8,
resolved_active: 2,
already_current: 1,
missing_current: 1,
}
);
let migrated = pile.migrate_retired_wants().unwrap();
assert_eq!(migrated.missing_current, 1);
assert_eq!(
std::fs::metadata(&path).unwrap().len(),
before + ENVELOPE_HEADER_LEN as u64
);
let expected = vec![a, c];
assert_eq!(
pile.snapshot()
.unwrap()
.wants()
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap(),
expected
);
assert_eq!(pile.want_cutover_status().unwrap().missing_current, 0);
let after_first = std::fs::metadata(&path).unwrap().len();
pile.migrate_retired_wants().unwrap();
assert_eq!(std::fs::metadata(&path).unwrap().len(), after_first);
pile.close().unwrap();
append_test_bytes(&path, retired_blob_want_record(a, false).as_bytes());
append_test_bytes(&path, retired_blob_want_record(b, true).as_bytes());
let mut reopened: Pile = Pile::open(&path).unwrap();
assert_eq!(
reopened
.snapshot()
.unwrap()
.wants()
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap(),
expected
);
reopened.close().unwrap();
}
#[test]
fn retired_equations_with_a_signed_twin_are_left_behind_by_rewrite() {
let dir = tempfile::tempdir().unwrap();
let source_path = fresh_empty_pile_path(&dir, "endorsed-source.pile");
let mut source = Pile::open(&source_path).unwrap();
let input = source
.put::<UnknownBlob, _>(Bytes::from_source(b"endorsed input".to_vec()))
.unwrap();
let output = source
.put::<UnknownBlob, _>(Bytes::from_source(b"endorsed output".to_vec()))
.unwrap();
let other = source
.put::<UnknownBlob, _>(Bytes::from_source(b"another output".to_vec()))
.unwrap();
let collection = source
.put::<SimpleArchive, _>(TribleSet::new().to_blob())
.unwrap();
source.close().unwrap();
let unsigned_merge = LegacyCollectionMergeRecordHeader {
magic: FRAME_MAGIC,
span_blocks: ENVELOPE_HEADER_BLOCKS.to_le_bytes(),
record_kind: record_kind::KIND_COLLECTION_MERGE_UNSIGNED,
collection: collection.raw,
low: [0; 32],
high: input.raw,
result: output.raw,
reserved: [0; 64],
};
let unsigned_derive = CollectionDeriveHeaderEnvelopeV1 {
envelope_marker: MAGIC_MARKER_ENVELOPE,
record_kind: MAGIC_MARKER_COLLECTION_DERIVE_V5,
span_blocks: ENVELOPE_HEADER_BLOCKS.to_le_bytes(),
target: collection.raw,
input: input.raw,
output: output.raw,
reserved: [0; 124],
};
let retired_frame = |kind: RawInline, blocks: u32, fields: &[[u8; 32]]| {
let mut frame = test_envelope_bytes(kind, blocks, blocks as usize * 256);
for (index, field) in fields.iter().enumerate() {
frame[FRAME_BODY_OFFSET + index * 32..FRAME_BODY_OFFSET + (index + 1) * 32]
.copy_from_slice(field);
}
frame
};
let witnessed_merge = retired_frame(
record_kind::KIND_COLLECTION_MERGE_WITNESSED_V6,
2,
&[collection.raw, [0; 32], input.raw, output.raw],
);
let signed_v2_derive = retired_frame(
record_kind::KIND_COLLECTION_DERIVE_SIGNED_V2,
1,
&[collection.raw, input.raw, other.raw],
);
let signed_v2_merge = retired_frame(
record_kind::KIND_COLLECTION_MERGE_SIGNED_V2,
2,
&[collection.raw, input.raw, output.raw, other.raw],
);
append_test_bytes(&source_path, unsigned_merge.as_bytes());
append_test_bytes(&source_path, unsigned_derive.as_bytes());
append_test_bytes(&source_path, &witnessed_merge);
append_test_bytes(&source_path, &signed_v2_derive);
append_test_bytes(&source_path, &signed_v2_merge);
let signer = SigningKey::from_bytes(&[9; 32]);
let derive_twin = RetiredCollectionEquation::sign_derive_v9(
&signer,
CollectionHandle::new(collection.raw),
Inline::<Hash<Blake3>>::new(input.raw),
Inline::<Hash<Blake3>>::new(other.raw),
);
append_test_bytes(&source_path, &retired_equation_frame(&derive_twin));
let mut source = Pile::open(&source_path).unwrap();
let merge_twin = CollectionRecord::Merge(
CollectionMerge::sign(
&signer,
CollectionHandle::new(collection.raw),
[
Inline::<Hash<Blake3>>::new([0; 32]),
Inline::<Hash<Blake3>>::new(input.raw),
],
Inline::<Hash<Blake3>>::new(output.raw),
)
.unwrap(),
);
source.insert(merge_twin).unwrap();
assert_eq!(
source
.snapshot()
.unwrap()
.legacy_unsigned_collection_equations()
.count(),
2
);
assert_eq!(source.opaque_record_count().unwrap(), 3);
let destination_path = fresh_empty_pile_path(&dir, "endorsed-destination.pile");
let mut destination = Pile::open(&destination_path).unwrap();
let stats = source
.rewrite_retained_into(
&mut destination,
&RetentionRoots::new(),
WantRewritePolicy::Drop,
)
.unwrap();
assert_eq!(stats.superseded_equations, 3);
assert_eq!(stats.opaque_frames, 1);
assert_eq!(stats.retired_equations, 1);
let reader = destination.snapshot().unwrap();
let carried: Vec<_> = reader.legacy_unsigned_collection_equations().collect();
assert_eq!(carried.len(), 1);
assert!(matches!(
carried[0],
LegacyUnsignedCollectionEquation::Derive { .. }
));
let records: Vec<_> = reader
.records()
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap();
assert!(records.contains(&merge_twin));
assert_eq!(
reader.retired_collection_equations().collect::<Vec<_>>(),
vec![derive_twin]
);
drop(reader);
assert_eq!(destination.opaque_record_count().unwrap(), 1);
destination.close().unwrap();
source.close().unwrap();
}
#[test]
fn a_drained_generation_and_its_derivations_are_left_behind_by_rewrite() {
use crate::collection::{AdmissionPolicy, CollectionPolicy, CollectionStoreExt};
let dir = tempfile::tempdir().unwrap();
let source_path = fresh_empty_pile_path(&dir, "drained-source.pile");
let mut source = Pile::open(&source_path).unwrap();
let key = SigningKey::from_bytes(&[9; 32]);
let other = SigningKey::from_bytes(&[10; 32]);
let policy = |signer: &SigningKey| {
CollectionPolicy::new(
AdmissionPolicy::direct(signer.verifying_key()),
AdmissionPolicy::direct(signer.verifying_key()),
)
};
let old = source.collection("gen", policy(&key)).unwrap();
let new = source.collection("gen", policy(&other)).unwrap();
assert_ne!(old.handle(), new.handle());
let derived = source
.derive::<crate::collection::test_support::TestImage>(old, (), policy(&key))
.unwrap();
let data = [collection_test_hash(1), collection_test_hash(2)];
for member in data {
source
.insert(CollectionRecord::Commit(CollectionCommit::sign(
&key,
old.handle(),
member,
empty_metadata_handle(),
)))
.unwrap();
}
source
.insert(CollectionRecord::Derive(CollectionDerive::sign(
&key,
derived.handle(),
crate::collection::SourceLocator::of((data[0]).raw),
collection_test_hash(3),
)))
.unwrap();
source.close().unwrap();
let unsigned = LegacyCollectionMergeRecordHeader {
magic: FRAME_MAGIC,
span_blocks: ENVELOPE_HEADER_BLOCKS.to_le_bytes(),
record_kind: record_kind::KIND_COLLECTION_MERGE_UNSIGNED,
collection: old.handle().raw,
low: data[0].raw,
high: data[1].raw,
result: collection_test_hash(4).raw,
reserved: [0; 64],
};
let mut retired =
test_envelope_bytes(record_kind::KIND_COLLECTION_DERIVE_SIGNED_V2, 1, 256);
retired[FRAME_BODY_OFFSET..FRAME_BODY_OFFSET + 32].copy_from_slice(&old.handle().raw);
append_test_bytes(&source_path, unsigned.as_bytes());
append_test_bytes(&source_path, &retired);
let mut source = Pile::open(&source_path).unwrap();
let pair = DrainedGeneration {
retired: old.handle(),
current: new.handle(),
};
source
.insert(CollectionRecord::Commit(CollectionCommit::sign(
&other,
new.handle(),
data[0],
empty_metadata_handle(),
)))
.unwrap();
let refused_path = fresh_empty_pile_path(&dir, "refused.pile");
let mut refused = Pile::open(&refused_path).unwrap();
assert!(matches!(
source.rewrite_retained_into_leaving(
&mut refused,
&RetentionRoots::new(),
WantRewritePolicy::Drop,
&[pair],
),
Err(PileRewriteError::Undrained { unreached: 1, .. })
));
refused.close().unwrap();
source
.insert(CollectionRecord::Commit(CollectionCommit::sign(
&other,
new.handle(),
data[1],
empty_metadata_handle(),
)))
.unwrap();
let destination_path = fresh_empty_pile_path(&dir, "drained-destination.pile");
let mut destination = Pile::open(&destination_path).unwrap();
let stats = source
.rewrite_retained_into_leaving(
&mut destination,
&RetentionRoots::new(),
WantRewritePolicy::Drop,
&[pair],
)
.unwrap();
assert_eq!(stats.drained_frames, 5);
assert_eq!(stats.opaque_frames, 0);
let reader = destination.snapshot().unwrap();
for handle in [old.handle(), derived.handle()] {
assert!(reader
.select_records(&BTreeSet::from([CollectionRecordSelector::Collection(
handle
)]))
.unwrap()
.is_empty());
}
assert_eq!(
reader
.select_records(&BTreeSet::from([CollectionRecordSelector::Collection(
new.handle()
)]))
.unwrap()
.len(),
2
);
assert_eq!(reader.legacy_unsigned_collection_equations().count(), 0);
drop(reader);
assert_eq!(destination.opaque_record_count().unwrap(), 0);
destination.close().unwrap();
source.close().unwrap();
}
#[test]
fn a_frame_filter_leaves_behind_what_it_refuses_in_every_generation() {
use crate::collection::SourceLocator;
struct DropMergesAndDerivesInto {
target: CollectionHandle,
asked: Vec<CollectionFrame>,
}
impl CollectionFrameFilter for DropMergesAndDerivesInto {
fn carries(&mut self, frame: &CollectionFrame) -> bool {
self.asked.push(*frame);
match frame.role {
CollectionFrameRole::Merge | CollectionFrameRole::Map => false,
CollectionFrameRole::Derive => frame.collection != Some(self.target),
CollectionFrameRole::Commit | CollectionFrameRole::Definition => true,
}
}
}
let dir = tempfile::tempdir().unwrap();
let source_path = fresh_empty_pile_path(&dir, "filter-source.pile");
let key = SigningKey::from_bytes(&[21; 32]);
let root = collection_test_collection(22);
let dropped = collection_test_collection(23);
let kept = collection_test_collection(24);
let mut source = Pile::open(&source_path).unwrap();
let payload = source
.put::<UnknownBlob, _>(Bytes::from_source(b"a committed payload".to_vec()))
.unwrap();
let image = source
.put::<UnknownBlob, _>(Bytes::from_source(
b"named only by a dropped derive".to_vec(),
))
.unwrap();
let joined = source
.put::<UnknownBlob, _>(Bytes::from_source(b"named only by a merge".to_vec()))
.unwrap();
let data = Inline::<Hash<Blake3>>::new(payload.raw);
let commit = CollectionRecord::Commit(CollectionCommit::sign(
&key,
root,
data,
empty_metadata_handle(),
));
let merge = CollectionRecord::Merge(
CollectionMerge::sign(
&key,
root,
[collection_test_hash(1), collection_test_hash(2)],
Inline::new(joined.raw),
)
.unwrap(),
);
let derive_dropped = CollectionRecord::Derive(CollectionDerive::sign(
&key,
dropped,
SourceLocator::of(data.raw),
Inline::new(image.raw),
));
let derive_kept = CollectionRecord::Derive(CollectionDerive::sign(
&key,
kept,
SourceLocator::of(data.raw),
collection_test_hash(5),
));
for record in [commit, merge, derive_dropped, derive_kept] {
source.insert(record).unwrap();
}
source.close().unwrap();
let derive_v9_kept = RetiredCollectionEquation::sign_derive_v9(
&key,
kept,
collection_test_hash(6),
collection_test_hash(8),
);
let retired = [
RetiredCollectionEquation::sign_merge_v8(
&key,
root,
collection_test_hash(3),
collection_test_hash(2),
collection_test_hash(4),
),
RetiredCollectionEquation::sign_derive_v9(
&key,
dropped,
collection_test_hash(6),
collection_test_hash(7),
),
derive_v9_kept,
];
for equation in &retired {
append_test_bytes(&source_path, &retired_equation_frame(equation));
}
let unsigned_merge = LegacyCollectionMergeRecordHeader {
magic: FRAME_MAGIC,
span_blocks: ENVELOPE_HEADER_BLOCKS.to_le_bytes(),
record_kind: record_kind::KIND_COLLECTION_MERGE_UNSIGNED,
collection: root.raw,
low: [9; 32],
high: [10; 32],
result: [11; 32],
reserved: [0; 64],
};
append_test_bytes(&source_path, unsigned_merge.as_bytes());
for (target, output) in [(dropped, 13u8), (kept, 14)] {
let unsigned_derive = CollectionDeriveHeaderEnvelopeV1 {
envelope_marker: MAGIC_MARKER_ENVELOPE,
record_kind: MAGIC_MARKER_COLLECTION_DERIVE_V5,
span_blocks: ENVELOPE_HEADER_BLOCKS.to_le_bytes(),
target: target.raw,
input: [12; 32],
output: [output; 32],
reserved: [0; 124],
};
append_test_bytes(&source_path, unsigned_derive.as_bytes());
}
let retired_frame = |kind: RawInline, blocks: u32, fields: &[[u8; 32]]| {
let mut frame = test_envelope_bytes(kind, blocks, blocks as usize * 256);
for (index, field) in fields.iter().enumerate() {
frame[FRAME_BODY_OFFSET + index * 32..FRAME_BODY_OFFSET + (index + 1) * 32]
.copy_from_slice(field);
}
frame
};
let kept_v7 = retired_frame(
record_kind::KIND_COLLECTION_DERIVE_WITNESSED_V7,
2,
&[kept.raw, [15; 32], [16; 32]],
);
let unknown = test_envelope_bytes(TEST_UNKNOWN_KIND_A, 1, 256);
for frame in [
retired_frame(
record_kind::KIND_COLLECTION_MERGE_SIGNED_V2,
2,
&[root.raw, [17; 32], [18; 32], [19; 32]],
),
retired_frame(
record_kind::KIND_COLLECTION_MERGE_WITNESSED_V6,
2,
&[root.raw, [20; 32], [21; 32], [22; 32]],
),
retired_frame(
record_kind::KIND_COLLECTION_DERIVE_SIGNED_V2,
1,
&[dropped.raw, [23; 32], [24; 32]],
),
kept_v7.clone(),
unknown.clone(),
] {
append_test_bytes(&source_path, &frame);
}
for (_, header) in legacy_collection_test_headers() {
append_test_bytes(&source_path, &header);
}
let mut source = Pile::open(&source_path).unwrap();
let snapshot = source.snapshot().unwrap();
let mut every_blob = RetentionRoots::new();
let mut source_blobs = Vec::new();
for info in snapshot.blobs() {
let info = info.unwrap();
every_blob.retain_direct(info.handle);
source_blobs.push(info.handle);
}
drop(snapshot);
let mut planning = DropMergesAndDerivesInto {
target: dropped,
asked: Vec::new(),
};
let plan = source
.plan_retained_rewrite(&every_blob, WantRewritePolicy::Drop, &[], &mut planning)
.unwrap();
assert_eq!(planning.asked.len(), 18);
for generation in [
CollectionFrameGeneration::Current,
CollectionFrameGeneration::RetiredV8V9,
CollectionFrameGeneration::RetiredOpaque,
CollectionFrameGeneration::LegacyUnsigned,
CollectionFrameGeneration::LegacyV3,
] {
assert!(planning
.asked
.iter()
.any(|frame| frame.generation == generation));
}
assert!(planning.asked.iter().all(|frame| (frame.generation
== CollectionFrameGeneration::LegacyV3)
== frame.collection.is_none()));
assert_eq!(
(
plan.retained_blobs,
plan.opaque_frames,
plan.superseded_equations,
plan.drained_frames,
plan.retired_equations,
plan.filtered_frames,
),
(source_blobs.len(), 2, 0, 0, 1, 10)
);
let destination_path = fresh_empty_pile_path(&dir, "filter-destination.pile");
let mut destination = Pile::open(&destination_path).unwrap();
let mut rewriting = DropMergesAndDerivesInto {
target: dropped,
asked: Vec::new(),
};
let stats = source
.rewrite_retained_into_filtered(
&mut destination,
&every_blob,
WantRewritePolicy::Drop,
&[],
&mut rewriting,
CorruptBlobPolicy::Refuse,
)
.unwrap();
assert_eq!(rewriting.asked, planning.asked);
assert_eq!(
(
stats.retained_blobs,
stats.opaque_frames,
stats.superseded_equations,
stats.drained_frames,
stats.retired_equations,
stats.filtered_frames,
),
(
plan.retained_blobs,
plan.opaque_frames,
plan.superseded_equations,
plan.drained_frames,
plan.retired_equations,
plan.filtered_frames,
)
);
let reader = destination.snapshot().unwrap();
assert_eq!(
sorted_collection_records(
reader
.records()
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap()
),
sorted_collection_records(vec![commit, derive_kept])
);
assert_eq!(
reader.retired_collection_equations().collect::<Vec<_>>(),
vec![derive_v9_kept]
);
let unsigned: Vec<_> = reader.legacy_unsigned_collection_equations().collect();
assert_eq!(unsigned.len(), 1);
assert_eq!(unsigned[0].collection(), kept);
for handle in &source_blobs {
assert!(reader.contains_blob(*handle).unwrap(), "every blob travels");
}
drop(reader);
destination.close().unwrap();
assert_eq!(
legacy_collection_headers_at(&destination_path)
.into_iter()
.map(|(kind, _)| kind)
.collect::<BTreeSet<_>>(),
BTreeSet::from([
LegacyCollectionRecordKindV3::Definition,
LegacyCollectionRecordKindV3::Commit,
LegacyCollectionRecordKindV3::Derive,
])
);
let mut physical = PileRecords::open(&destination_path).unwrap();
let opaque: Vec<_> = physical
.by_ref()
.filter_map(|record| {
let record = record.unwrap();
matches!(record.content, PileRecordContent::Opaque { .. })
.then_some((record.offset, record.len))
})
.collect();
let carried: BTreeSet<Vec<u8>> = opaque
.into_iter()
.map(|(offset, len)| physical.bytes()[offset..offset + len].to_vec())
.collect();
assert_eq!(carried, BTreeSet::from([kept_v7, unknown]));
drop(physical);
let before = std::fs::read(&destination_path).unwrap();
let mut destination = Pile::open(&destination_path).unwrap();
let again = source
.rewrite_retained_into_filtered(
&mut destination,
&every_blob,
WantRewritePolicy::Drop,
&[],
&mut DropMergesAndDerivesInto {
target: dropped,
asked: Vec::new(),
},
CorruptBlobPolicy::Refuse,
)
.unwrap();
assert_eq!(again.filtered_frames, stats.filtered_frames);
destination.close().unwrap();
assert_eq!(std::fs::read(&destination_path).unwrap(), before);
source.close().unwrap();
}
#[test]
fn a_filtered_rewrite_keeps_only_what_its_carried_frames_reach() {
use crate::collection::SourceLocator;
struct DropMergesAndDerivesInto(CollectionHandle);
impl CollectionFrameFilter for DropMergesAndDerivesInto {
fn carries(&mut self, frame: &CollectionFrame) -> bool {
match frame.role {
CollectionFrameRole::Merge | CollectionFrameRole::Map => false,
CollectionFrameRole::Derive => frame.collection != Some(self.0),
CollectionFrameRole::Commit | CollectionFrameRole::Definition => true,
}
}
}
let dir = tempfile::tempdir().unwrap();
let source_path = fresh_empty_pile_path(&dir, "reach-source.pile");
let key = SigningKey::from_bytes(&[31; 32]);
let root = collection_test_collection(32);
let dropped = collection_test_collection(33);
let mut source = Pile::open(&source_path).unwrap();
let put = |source: &mut Pile, bytes: Vec<u8>| {
source
.put::<UnknownBlob, _>(Bytes::from_source(bytes))
.unwrap()
};
let payload = put(&mut source, b"a committed payload".to_vec());
let child = put(&mut source, b"named by a kept image".to_vec());
let mut image_bytes = vec![0x11; 32];
image_bytes.extend_from_slice(&child.raw);
let image_kept = put(&mut source, image_bytes);
let image_dropped = put(&mut source, b"named only by a dropped derive".to_vec());
let joined = put(&mut source, b"named only by a merge".to_vec());
let merged_v8 = put(&mut source, b"named only by a retired merge".to_vec());
let unrelated = put(&mut source, b"named by nothing".to_vec());
let named_by_unknown = put(&mut source, b"named by an unknown frame".to_vec());
let attachment = put(&mut source, b"named only by a map".to_vec());
let kept = source
.put::<SimpleArchive, _>(TribleSet::new().to_blob())
.unwrap();
let data = Inline::<Hash<Blake3>>::new(payload.raw);
for record in [
CollectionRecord::Commit(CollectionCommit::sign(
&key,
root,
data,
empty_metadata_handle(),
)),
CollectionRecord::Merge(
CollectionMerge::sign(
&key,
root,
[collection_test_hash(1), collection_test_hash(2)],
Inline::new(joined.raw),
)
.unwrap(),
),
CollectionRecord::Derive(CollectionDerive::sign(
&key,
dropped,
SourceLocator::of(data.raw),
Inline::new(image_dropped.raw),
)),
CollectionRecord::Derive(CollectionDerive::sign(
&key,
kept,
SourceLocator::of(data.raw),
Inline::new(image_kept.raw),
)),
CollectionRecord::Map(crate::collection::CollectionMap::sign(
&key,
dropped,
data,
Inline::new(attachment.raw),
)),
] {
source.insert(record).unwrap();
}
source.close().unwrap();
append_test_bytes(
&source_path,
&retired_equation_frame(&RetiredCollectionEquation::sign_merge_v8(
&key,
root,
collection_test_hash(3),
collection_test_hash(4),
Inline::new(merged_v8.raw),
)),
);
let mut unknown = test_envelope_bytes(TEST_UNKNOWN_KIND_A, 1, 256);
unknown[FRAME_BODY_OFFSET + 32..FRAME_BODY_OFFSET + 64]
.copy_from_slice(&named_by_unknown.raw);
append_test_bytes(&source_path, &unknown);
let mut source = Pile::open(&source_path).unwrap();
let plan = source
.plan_retained_rewrite(
&RetentionRoots::new(),
WantRewritePolicy::Drop,
&[],
&mut DropMergesAndDerivesInto(dropped),
)
.unwrap();
let destination_path = fresh_empty_pile_path(&dir, "reach-destination.pile");
let mut destination = Pile::open(&destination_path).unwrap();
let stats = source
.rewrite_retained_into_filtered(
&mut destination,
&RetentionRoots::new(),
WantRewritePolicy::Drop,
&[],
&mut DropMergesAndDerivesInto(dropped),
CorruptBlobPolicy::Refuse,
)
.unwrap();
assert_eq!(stats.retained_blobs, plan.retained_blobs);
assert_eq!(
stats.filtered_frames, 4,
"the MERGE, the DERIVE, the MAP, the v8 MERGE"
);
let reader = destination.snapshot().unwrap();
for handle in [
payload,
child,
image_kept,
named_by_unknown,
kept.transmute(),
] {
assert!(
reader.contains_blob(handle).unwrap(),
"{handle:?} is reached"
);
}
for handle in [image_dropped, joined, merged_v8, unrelated, attachment] {
assert!(!reader.contains_blob(handle).unwrap(), "{handle:?} is not");
}
assert!(
reader
.records()
.unwrap()
.all(|record| !matches!(record.unwrap(), CollectionRecord::Map(_))),
"the MAP is left behind"
);
let mut bytes = 0u64;
let mut count = 0usize;
for info in reader.blobs() {
bytes += info.unwrap().length;
count += 1;
}
assert_eq!((count, bytes), (plan.retained_blobs, plan.retained_bytes));
drop(reader);
destination.close().unwrap();
let plain_path = fresh_empty_pile_path(&dir, "reach-plain.pile");
let mut plain = Pile::open(&plain_path).unwrap();
source
.rewrite_retained_into(&mut plain, &RetentionRoots::new(), WantRewritePolicy::Drop)
.unwrap();
let reader = plain.snapshot().unwrap();
assert!(reader.contains_blob(unrelated).unwrap());
assert!(reader.contains_blob(joined).unwrap());
assert!(reader.contains_blob(attachment).unwrap());
assert!(reader
.records()
.unwrap()
.any(|record| matches!(record.unwrap(), CollectionRecord::Map(_))));
drop(reader);
plain.close().unwrap();
source.close().unwrap();
}
#[test]
fn a_filtered_rewrite_keeps_what_carried_headers_wants_pins_and_proofs_name() {
struct DropMerges;
impl CollectionFrameFilter for DropMerges {
fn carries(&mut self, frame: &CollectionFrame) -> bool {
frame.role != CollectionFrameRole::Merge
}
}
let dir = tempfile::tempdir().unwrap();
let source_path = fresh_empty_pile_path(&dir, "legacy-reach-source.pile");
let mut source = Pile::open(&source_path).unwrap();
let put = |source: &mut Pile, bytes: Vec<u8>| {
source
.put::<UnknownBlob, _>(Bytes::from_source(bytes))
.unwrap()
};
let child = put(&mut source, b"named by the V3 commit's metadata".to_vec());
let v3_data = put(&mut source, b"a V3 commit's data".to_vec());
let mut metadata_bytes = vec![0x11; 32];
metadata_bytes.extend_from_slice(&child.raw);
let v3_metadata = put(&mut source, metadata_bytes);
let v3_input = put(&mut source, b"a V3 derive's input".to_vec());
let v3_output = put(&mut source, b"a V3 derive's output".to_vec());
let v3_first = put(&mut source, b"a V3 merge's first input".to_vec());
let v3_second = put(&mut source, b"a V3 merge's second input".to_vec());
let v3_result = put(&mut source, b"a V3 merge's result".to_vec());
let wanted = put(&mut source, b"named only by a WANT".to_vec());
let pinned = put(&mut source, b"named only by a legacy pin".to_vec());
let unrelated = put(&mut source, b"named by nothing".to_vec());
let definition = source.put::<SimpleArchive, _>(TribleSet::new()).unwrap();
let proof = CapabilityProof::new(
CapabilityResource::new([61; 32]),
&SigningKey::from_bytes(&[62; 32]),
definition,
SigningKey::from_bytes(&[63; 32]).verifying_key(),
);
source.insert_proof(proof.clone()).unwrap();
source.want(WantRequest::blob(wanted)).unwrap();
let pin_id = Id::new([64; 16]).unwrap();
source
.append_legacy_pin_for_test(pin_id, None, Some(pinned.transmute()))
.unwrap();
source.close().unwrap();
let (low, high) = if v3_first.raw <= v3_second.raw {
(v3_first.raw, v3_second.raw)
} else {
(v3_second.raw, v3_first.raw)
};
for header in [
fixed_collection_header(
CollectionDefinitionHeaderV3 {
magic_marker: MAGIC_MARKER_COLLECTION_DEFINITION_V3,
scope: [1; 16],
representation: [2; 16],
recipe: [3; 16],
reserved: [0; 192],
}
.as_bytes(),
),
fixed_collection_header(
CollectionCommitHeaderV3 {
magic_marker: MAGIC_MARKER_COLLECTION_COMMIT_V3,
collection: [4; 16],
data: v3_data.raw,
metadata: v3_metadata.raw,
public_key: [7; 32],
signature_r: [8; 32],
signature_s: [9; 32],
reserved: [0; 64],
}
.as_bytes(),
),
fixed_collection_header(
CollectionMergeHeaderV3 {
magic_marker: MAGIC_MARKER_COLLECTION_MERGE_V3,
collection: [10; 16],
low,
high,
result: v3_result.raw,
reserved: [0; 128],
}
.as_bytes(),
),
fixed_collection_header(
CollectionDeriveHeaderV3 {
magic_marker: MAGIC_MARKER_COLLECTION_DERIVE_V3,
source: [14; 16],
target: [15; 16],
input: v3_input.raw,
output: v3_output.raw,
reserved: [0; 144],
}
.as_bytes(),
),
] {
append_test_bytes(&source_path, &header);
}
let mut source = Pile::open(&source_path).unwrap();
let plan = source
.plan_retained_rewrite(
&RetentionRoots::new(),
WantRewritePolicy::Preserve,
&[],
&mut DropMerges,
)
.unwrap();
let destination_path = fresh_empty_pile_path(&dir, "legacy-reach-destination.pile");
let mut destination = Pile::open(&destination_path).unwrap();
let stats = source
.rewrite_retained_into_filtered(
&mut destination,
&RetentionRoots::new(),
WantRewritePolicy::Preserve,
&[],
&mut DropMerges,
CorruptBlobPolicy::Refuse,
)
.unwrap();
assert_eq!(
(
stats.filtered_frames,
stats.wants,
stats.strong_pins,
stats.capability_proofs,
stats.retained_blobs,
),
(1, 1, 1, 1, plan.retained_blobs)
);
assert_eq!(plan.filtered_frames, 1);
let reader = destination.snapshot().unwrap();
for handle in [
child,
v3_data,
v3_metadata,
v3_input,
v3_output,
wanted,
pinned,
definition.transmute(),
] {
assert!(
reader.contains_blob(handle).unwrap(),
"{handle:?} is reached"
);
}
for handle in [v3_first, v3_second, v3_result, unrelated] {
assert!(!reader.contains_blob(handle).unwrap(), "{handle:?} is not");
}
let held: BTreeSet<Inline<Handle<UnknownBlob>>> =
reader.blobs().map(|info| info.unwrap().handle).collect();
assert_eq!(held, plan.retained);
assert_eq!(
reader
.wants()
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap(),
vec![WantRequest::blob(wanted)]
);
assert_eq!(
reader
.proofs()
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap(),
vec![proof]
);
drop(reader);
assert_eq!(
destination.legacy_pin_head_for_test(pin_id).unwrap(),
Some(pinned.transmute())
);
destination.close().unwrap();
assert_eq!(
legacy_collection_headers_at(&destination_path)
.into_iter()
.map(|(kind, _)| kind)
.collect::<BTreeSet<_>>(),
BTreeSet::from([
LegacyCollectionRecordKindV3::Definition,
LegacyCollectionRecordKindV3::Commit,
LegacyCollectionRecordKindV3::Derive,
])
);
source.close().unwrap();
}
#[test]
fn a_pile_concatenated_with_itself_is_rewritten_as_the_original() {
use crate::collection::SourceLocator;
#[derive(Default)]
struct Recording(Vec<CollectionFrame>);
impl CollectionFrameFilter for Recording {
fn carries(&mut self, frame: &CollectionFrame) -> bool {
self.0.push(*frame);
frame.role != CollectionFrameRole::Merge
}
}
let dir = tempfile::tempdir().unwrap();
let single_path = fresh_empty_pile_path(&dir, "single.pile");
let key = SigningKey::from_bytes(&[71; 32]);
let root = collection_test_collection(72);
let derived = collection_test_collection(73);
let mut single = Pile::open(&single_path).unwrap();
let payload = single
.put::<UnknownBlob, _>(Bytes::from_source(b"a committed payload".to_vec()))
.unwrap();
let data = Inline::<Hash<Blake3>>::new(payload.raw);
for record in [
CollectionRecord::Commit(CollectionCommit::sign(
&key,
root,
data,
empty_metadata_handle(),
)),
CollectionRecord::Merge(
CollectionMerge::sign(
&key,
root,
[collection_test_hash(1), collection_test_hash(2)],
collection_test_hash(3),
)
.unwrap(),
),
CollectionRecord::Derive(CollectionDerive::sign(
&key,
derived,
SourceLocator::of(data.raw),
collection_test_hash(4),
)),
] {
single.insert(record).unwrap();
}
single.close().unwrap();
let mut retired =
test_envelope_bytes(record_kind::KIND_COLLECTION_DERIVE_WITNESSED_V7, 2, 512);
retired[FRAME_BODY_OFFSET..FRAME_BODY_OFFSET + 32].copy_from_slice(&derived.raw);
append_test_bytes(&single_path, &retired);
append_test_bytes(
&single_path,
&test_envelope_bytes(TEST_UNKNOWN_KIND_A, 1, 256),
);
let double_path = fresh_empty_pile_path(&dir, "double.pile");
let bytes = std::fs::read(&single_path).unwrap();
append_test_bytes(&double_path, &bytes);
append_test_bytes(&double_path, &bytes);
let rewrite = |source_path: &Path, name: &str| {
let mut source = Pile::open(source_path).unwrap();
let mut planning = Recording::default();
let plan = source
.plan_retained_rewrite(
&RetentionRoots::new(),
WantRewritePolicy::Drop,
&[],
&mut planning,
)
.unwrap();
let destination_path = fresh_empty_pile_path(&dir, name);
let mut destination = Pile::open(&destination_path).unwrap();
let mut rewriting = Recording::default();
let stats = source
.rewrite_retained_into_filtered(
&mut destination,
&RetentionRoots::new(),
WantRewritePolicy::Drop,
&[],
&mut rewriting,
CorruptBlobPolicy::Refuse,
)
.unwrap();
assert_eq!(planning.0, rewriting.0);
assert_eq!(plan.repeated_frames, stats.repeated_frames);
let reader = destination.snapshot().unwrap();
let records = sorted_collection_records(
reader
.records()
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap(),
);
let blobs: BTreeSet<_> = reader.blobs().map(|info| info.unwrap().handle).collect();
drop(reader);
destination.close().unwrap();
source.close().unwrap();
let frames: Vec<_> = PileRecords::open(&destination_path)
.unwrap()
.map(|record| {
let record = record.unwrap();
(record.offset, record.len)
})
.collect();
(rewriting.0, stats, records, blobs, frames)
};
let (asked_single, stats_single, records_single, blobs_single, frames_single) =
rewrite(&single_path, "single-rewritten.pile");
let (asked_double, stats_double, records_double, blobs_double, frames_double) =
rewrite(&double_path, "double-rewritten.pile");
assert_eq!(asked_single.len(), 4);
assert_eq!(asked_double, asked_single);
assert_eq!(stats_single.repeated_frames, 0);
assert_eq!(stats_double.repeated_frames, 5);
assert_eq!(
PileRewriteStats {
repeated_frames: 0,
..stats_double
},
stats_single
);
assert_eq!(records_double, records_single);
assert_eq!(blobs_double, blobs_single);
assert_eq!(frames_double, frames_single);
}
fn damage_blob_occurrences(
path: &Path,
handle: Inline<Handle<UnknownBlob>>,
damage: &[(usize, usize)],
) {
use std::io::{Seek, SeekFrom};
let offsets: Vec<usize> = PileRecords::open(path)
.unwrap()
.filter_map(|record| match record.unwrap().content {
PileRecordContent::Blob {
hash, data_offset, ..
} if hash.raw == handle.raw => Some(data_offset),
_ => None,
})
.collect();
let bytes = std::fs::read(path).unwrap();
let mut file = OpenOptions::new().write(true).open(path).unwrap();
for (nth, at) in damage {
let position = offsets[*nth] + at;
file.seek(SeekFrom::Start(position as u64)).unwrap();
file.write_all(&[bytes[position] ^ 0xFF]).unwrap();
}
file.sync_all().unwrap();
}
fn damage_blob_payload(path: &Path, handle: Inline<Handle<UnknownBlob>>) {
let count = PileRecords::open(path)
.unwrap()
.filter(|record| {
matches!(
record.as_ref().unwrap().content,
PileRecordContent::Blob { hash, .. } if hash.raw == handle.raw
)
})
.count();
assert!(count > 0, "{handle:?} has a blob record");
let damage: Vec<_> = (0..count).map(|nth| (nth, 0)).collect();
damage_blob_occurrences(path, handle, &damage);
}
#[test]
fn a_filtered_rewrite_keeps_what_a_carried_opaque_frame_names_off_alignment() {
let dir = tempfile::tempdir().unwrap();
let source_path = fresh_empty_pile_path(&dir, "unaligned-source.pile");
let mut source = Pile::open(&source_path).unwrap();
let put = |source: &mut Pile, bytes: &[u8]| {
source
.put::<UnknownBlob, _>(Bytes::from_source(bytes.to_vec()))
.unwrap()
};
let cell_archive = put(&mut source, b"named by a retired enveloped cell");
let v1_field = put(&mut source, b"named by an unknown V1-envelope kind");
let current_field = put(&mut source, b"named off alignment by an unknown kind");
let unrelated = put(&mut source, b"named by nothing");
source.close().unwrap();
let mut cell = test_envelope_v1_bytes(
MAGIC_MARKER_LOCAL_CELL_V3,
ENVELOPE_HEADER_BLOCKS,
ENVELOPE_HEADER_LEN,
);
cell[52..84].copy_from_slice(&cell_archive.raw);
append_test_bytes(&source_path, &cell);
let mut unknown_v1 = test_envelope_v1_bytes([0x5E; 16], 1, ENVELOPE_BLOCK_LEN);
unknown_v1[36..68].copy_from_slice(&v1_field.raw);
append_test_bytes(&source_path, &unknown_v1);
let mut unknown = test_envelope_bytes(TEST_UNKNOWN_KIND_B, 1, ENVELOPE_BLOCK_LEN);
unknown[FRAME_BODY_OFFSET + 7..FRAME_BODY_OFFSET + 39].copy_from_slice(¤t_field.raw);
append_test_bytes(&source_path, &unknown);
let mut source = Pile::open(&source_path).unwrap();
let plan = source
.plan_retained_rewrite(
&RetentionRoots::new(),
WantRewritePolicy::Drop,
&[],
&mut CarryEveryFrame,
)
.unwrap();
let destination_path = fresh_empty_pile_path(&dir, "unaligned-destination.pile");
let mut destination = Pile::open(&destination_path).unwrap();
let stats = source
.rewrite_retained_into_filtered(
&mut destination,
&RetentionRoots::new(),
WantRewritePolicy::Drop,
&[],
&mut CarryEveryFrame,
CorruptBlobPolicy::Refuse,
)
.unwrap();
assert_eq!(stats.opaque_frames, 3);
let reader = destination.snapshot().unwrap();
for handle in [cell_archive, v1_field, current_field] {
assert!(
reader.contains_blob(handle).unwrap(),
"{handle:?} is named by a carried frame"
);
}
assert!(!reader.contains_blob(unrelated).unwrap());
let held: BTreeSet<Inline<Handle<UnknownBlob>>> =
reader.blobs().map(|info| info.unwrap().handle).collect();
assert_eq!(held, plan.retained);
drop(reader);
destination.close().unwrap();
source.close().unwrap();
}
#[test]
fn retained_rewrites_carry_retired_capability_proofs_with_what_they_may_name() {
let dir = tempfile::tempdir().unwrap();
let source_path = fresh_empty_pile_path(&dir, "retired-proofs-source.pile");
let mut source = Pile::open(&source_path).unwrap();
let named = source
.put::<UnknownBlob, _>(Bytes::from_source(b"named inside a retired proof".to_vec()))
.unwrap();
let unrelated = source
.put::<UnknownBlob, _>(Bytes::from_source(b"named by nothing".to_vec()))
.unwrap();
source.close().unwrap();
let mut body = vec![0x3A; 160];
body[..32].copy_from_slice(&SigningKey::from_bytes(&[81; 32]).verifying_key().to_bytes());
body[32 + 5..32 + 37].copy_from_slice(&named.raw);
body[32 + 96..]
.copy_from_slice(&SigningKey::from_bytes(&[82; 32]).verifying_key().to_bytes());
let v1 = retired_capability_v1_record(&body);
let mut v2 = retired_capability_v1_record(&crate::capability::LEGACY_ACTION_PROOF_FIXTURE);
v2[FRAME_BODY_OFFSET - 32..FRAME_BODY_OFFSET]
.copy_from_slice(&record_kind::KIND_AUTH_PROOF_V2);
append_test_bytes(&source_path, &v1);
append_test_bytes(&source_path, &v2);
append_test_bytes(&source_path, &v1);
let retired_frames = |path: &Path| -> Vec<Vec<u8>> {
let mut records = PileRecords::open(path).unwrap();
let mut frames = Vec::new();
while let Some(record) = records.next() {
let record = record.unwrap();
if matches!(record.content, PileRecordContent::RetiredCapabilityProof) {
frames
.push(records.bytes()[record.offset..record.offset + record.len].to_vec());
}
}
frames
};
let mut source = Pile::open(&source_path).unwrap();
let plan = source
.plan_retained_rewrite(
&RetentionRoots::new(),
WantRewritePolicy::Drop,
&[],
&mut CarryEveryFrame,
)
.unwrap();
assert_eq!(
(plan.retired_capability_proofs, plan.repeated_frames),
(2, 1)
);
let filtered_path = fresh_empty_pile_path(&dir, "retired-proofs-filtered.pile");
let mut filtered = Pile::open(&filtered_path).unwrap();
let stats = source
.rewrite_retained_into_filtered(
&mut filtered,
&RetentionRoots::new(),
WantRewritePolicy::Drop,
&[],
&mut CarryEveryFrame,
CorruptBlobPolicy::Refuse,
)
.unwrap();
assert_eq!(
(stats.retired_capability_proofs, stats.repeated_frames),
(2, 1)
);
let before = std::fs::metadata(&filtered_path).unwrap().len();
source
.rewrite_retained_into_filtered(
&mut filtered,
&RetentionRoots::new(),
WantRewritePolicy::Drop,
&[],
&mut CarryEveryFrame,
CorruptBlobPolicy::Refuse,
)
.unwrap();
assert_eq!(std::fs::metadata(&filtered_path).unwrap().len(), before);
let reader = filtered.snapshot().unwrap();
assert!(reader.contains_blob(named).unwrap());
assert!(!reader.contains_blob(unrelated).unwrap());
drop(reader);
filtered.close().unwrap();
assert_eq!(retired_frames(&filtered_path), vec![v1.clone(), v2.clone()]);
let plain_path = fresh_empty_pile_path(&dir, "retired-proofs-plain.pile");
let mut plain = Pile::open(&plain_path).unwrap();
let stats = source
.rewrite_retained_into(&mut plain, &RetentionRoots::new(), WantRewritePolicy::Drop)
.unwrap();
assert_eq!(stats.retired_capability_proofs, 2);
let reader = plain.snapshot().unwrap();
assert!(reader.contains_blob(named).unwrap());
drop(reader);
plain.close().unwrap();
assert_eq!(retired_frames(&plain_path), vec![v1, v2]);
source.close().unwrap();
}
#[test]
fn a_nested_corrupt_blob_is_refused_or_set_aside_with_its_intact_children() {
let dir = tempfile::tempdir().unwrap();
let source_path = fresh_empty_pile_path(&dir, "nested-corrupt-source.pile");
let key = SigningKey::from_bytes(&[91; 32]);
let root = collection_test_collection(92);
let mut source = Pile::open(&source_path).unwrap();
let put = |source: &mut Pile, bytes: Vec<u8>| {
source
.put::<UnknownBlob, _>(Bytes::from_source(bytes))
.unwrap()
};
let intact = put(&mut source, b"named by an intact word".to_vec());
let lost = put(&mut source, b"named only by a damaged word".to_vec());
let mut child_bytes = vec![0x77; 32];
child_bytes.extend_from_slice(&intact.raw);
child_bytes.extend_from_slice(&lost.raw);
let child = put(&mut source, child_bytes);
let mut parent_bytes = vec![0x11; 32];
parent_bytes.extend_from_slice(&child.raw);
let parent = put(&mut source, parent_bytes);
source
.insert(CollectionRecord::Commit(CollectionCommit::sign(
&key,
root,
Inline::new(parent.raw),
empty_metadata_handle(),
)))
.unwrap();
source.close().unwrap();
damage_blob_occurrences(&source_path, child, &[(0, 0), (0, 64)]);
let mut source = Pile::open(&source_path).unwrap();
let plan = source
.plan_retained_rewrite(
&RetentionRoots::new(),
WantRewritePolicy::Drop,
&[],
&mut CarryEveryFrame,
)
.unwrap();
assert_eq!(plan.corrupt_blobs, BTreeSet::from([child]));
assert!(!plan.retained.contains(&lost));
let refused_path = fresh_empty_pile_path(&dir, "nested-corrupt-refused.pile");
let mut refused = Pile::open(&refused_path).unwrap();
let error = source
.rewrite_retained_into_filtered(
&mut refused,
&RetentionRoots::new(),
WantRewritePolicy::Drop,
&[],
&mut CarryEveryFrame,
CorruptBlobPolicy::Refuse,
)
.unwrap_err();
assert!(
matches!(&error, PileRewriteError::Corrupt { blobs } if *blobs == BTreeSet::from([child])),
"{error}"
);
refused.close().unwrap();
assert_eq!(std::fs::metadata(&refused_path).unwrap().len(), 0);
let destination_path = fresh_empty_pile_path(&dir, "nested-corrupt-destination.pile");
let mut destination = Pile::open(&destination_path).unwrap();
let stats = source
.rewrite_retained_into_filtered(
&mut destination,
&RetentionRoots::new(),
WantRewritePolicy::Drop,
&[],
&mut CarryEveryFrame,
CorruptBlobPolicy::SetAside,
)
.unwrap();
assert_eq!(stats.corrupt_blobs, BTreeSet::from([child]));
let reader = destination.snapshot().unwrap();
assert!(reader.contains_blob(parent).unwrap());
assert!(
reader.contains_blob(intact).unwrap(),
"a valid blob named by an intact word of a damaged one is kept"
);
assert!(!reader.contains_blob(child).unwrap());
assert!(
!reader.contains_blob(lost).unwrap(),
"a valid blob named only by a damaged word is left behind"
);
let held: BTreeSet<Inline<Handle<UnknownBlob>>> =
reader.blobs().map(|info| info.unwrap().handle).collect();
assert_eq!(held, plan.retained);
drop(reader);
destination.close().unwrap();
source.close().unwrap();
}
#[test]
fn a_corrupt_blob_a_kept_record_names_is_refused_or_set_aside() {
let dir = tempfile::tempdir().unwrap();
let source_path = fresh_empty_pile_path(&dir, "rooted-corrupt-source.pile");
let key = SigningKey::from_bytes(&[93; 32]);
let root = collection_test_collection(94);
let mut source = Pile::open(&source_path).unwrap();
let payload = source
.put::<UnknownBlob, _>(Bytes::from_source(b"committed, then damaged".to_vec()))
.unwrap();
let other = source
.put::<UnknownBlob, _>(Bytes::from_source(b"committed and intact".to_vec()))
.unwrap();
for data in [payload, other] {
source
.insert(CollectionRecord::Commit(CollectionCommit::sign(
&key,
root,
Inline::new(data.raw),
empty_metadata_handle(),
)))
.unwrap();
}
source.close().unwrap();
damage_blob_payload(&source_path, payload);
let mut source = Pile::open(&source_path).unwrap();
let plan = source
.plan_retained_rewrite(
&RetentionRoots::new(),
WantRewritePolicy::Drop,
&[],
&mut CarryEveryFrame,
)
.unwrap();
assert_eq!(plan.corrupt_blobs, BTreeSet::from([payload]));
assert!(!plan.retained.contains(&payload));
let refused_path = fresh_empty_pile_path(&dir, "rooted-corrupt-refused.pile");
let mut refused = Pile::open(&refused_path).unwrap();
assert!(matches!(
source.rewrite_retained_into_filtered(
&mut refused,
&RetentionRoots::new(),
WantRewritePolicy::Drop,
&[],
&mut CarryEveryFrame,
CorruptBlobPolicy::Refuse,
),
Err(PileRewriteError::Corrupt { blobs }) if blobs == BTreeSet::from([payload])
));
refused.close().unwrap();
assert_eq!(std::fs::metadata(&refused_path).unwrap().len(), 0);
let destination_path = fresh_empty_pile_path(&dir, "rooted-corrupt-destination.pile");
let mut destination = Pile::open(&destination_path).unwrap();
let stats = source
.rewrite_retained_into_filtered(
&mut destination,
&RetentionRoots::new(),
WantRewritePolicy::Drop,
&[],
&mut CarryEveryFrame,
CorruptBlobPolicy::SetAside,
)
.unwrap();
assert_eq!(stats.retained_blobs, plan.retained_blobs);
assert_eq!(stats.corrupt_blobs, plan.corrupt_blobs);
let reader = destination.snapshot().unwrap();
assert!(reader.contains_blob(other).unwrap());
assert!(!reader.contains_blob(payload).unwrap());
let held: BTreeSet<Inline<Handle<UnknownBlob>>> =
reader.blobs().map(|info| info.unwrap().handle).collect();
assert_eq!(held, plan.retained);
drop(reader);
destination.close().unwrap();
source.close().unwrap();
}
#[test]
fn a_corrupt_direct_root_is_refused_or_set_aside() {
let dir = tempfile::tempdir().unwrap();
let source_path = fresh_empty_pile_path(&dir, "direct-corrupt-source.pile");
let mut source = Pile::open(&source_path).unwrap();
let damaged = source
.put::<UnknownBlob, _>(Bytes::from_source(b"a direct root, damaged".to_vec()))
.unwrap();
let intact = source
.put::<UnknownBlob, _>(Bytes::from_source(b"a direct root, intact".to_vec()))
.unwrap();
source.close().unwrap();
damage_blob_payload(&source_path, damaged);
let mut roots = RetentionRoots::new();
roots.retain_direct(damaged);
roots.retain_direct(intact);
let mut source = Pile::open(&source_path).unwrap();
let plan = source
.plan_retained_rewrite(&roots, WantRewritePolicy::Drop, &[], &mut CarryEveryFrame)
.unwrap();
assert_eq!(plan.corrupt_blobs, BTreeSet::from([damaged]));
assert_eq!(plan.retained, BTreeSet::from([intact]));
let refused_path = fresh_empty_pile_path(&dir, "direct-corrupt-refused.pile");
let mut refused = Pile::open(&refused_path).unwrap();
assert!(matches!(
source.rewrite_retained_into_filtered(
&mut refused,
&roots,
WantRewritePolicy::Drop,
&[],
&mut CarryEveryFrame,
CorruptBlobPolicy::Refuse,
),
Err(PileRewriteError::Corrupt { blobs }) if blobs == BTreeSet::from([damaged])
));
refused.close().unwrap();
assert_eq!(std::fs::metadata(&refused_path).unwrap().len(), 0);
let destination_path = fresh_empty_pile_path(&dir, "direct-corrupt-destination.pile");
let mut destination = Pile::open(&destination_path).unwrap();
let stats = source
.rewrite_retained_into_filtered(
&mut destination,
&roots,
WantRewritePolicy::Drop,
&[],
&mut CarryEveryFrame,
CorruptBlobPolicy::SetAside,
)
.unwrap();
assert_eq!(stats.corrupt_blobs, BTreeSet::from([damaged]));
let reader = destination.snapshot().unwrap();
let held: BTreeSet<Inline<Handle<UnknownBlob>>> =
reader.blobs().map(|info| info.unwrap().handle).collect();
assert_eq!(held, BTreeSet::from([intact]));
drop(reader);
destination.close().unwrap();
source.close().unwrap();
}
#[test]
fn every_occurrence_of_a_corrupt_blob_is_read_for_children() {
let dir = tempfile::tempdir().unwrap();
let source_path = fresh_empty_pile_path(&dir, "two-occurrences-source.pile");
let key = SigningKey::from_bytes(&[95; 32]);
let mut source = Pile::open(&source_path).unwrap();
let put = |source: &mut Pile, bytes: Vec<u8>| {
source
.put::<UnknownBlob, _>(Bytes::from_source(bytes))
.unwrap()
};
let first = put(&mut source, b"named by the first word".to_vec());
let second = put(&mut source, b"named by the second word".to_vec());
let mut twice_bytes = first.raw.to_vec();
twice_bytes.extend_from_slice(&second.raw);
let twice = put(&mut source, twice_bytes);
source
.insert(CollectionRecord::Commit(CollectionCommit::sign(
&key,
collection_test_collection(96),
Inline::new(twice.raw),
empty_metadata_handle(),
)))
.unwrap();
source.close().unwrap();
let (offset, len) = PileRecords::open(&source_path)
.unwrap()
.map(|record| record.unwrap())
.find_map(|record| match record.content {
PileRecordContent::Blob { hash, .. } if hash.raw == twice.raw => {
Some((record.offset, record.len))
}
_ => None,
})
.unwrap();
let record = std::fs::read(&source_path).unwrap()[offset..offset + len].to_vec();
append_test_bytes(&source_path, &record);
damage_blob_occurrences(&source_path, twice, &[(0, 0), (1, 32)]);
let mut source = Pile::open(&source_path).unwrap();
let destination_path = fresh_empty_pile_path(&dir, "two-occurrences-destination.pile");
let mut destination = Pile::open(&destination_path).unwrap();
let stats = source
.rewrite_retained_into_filtered(
&mut destination,
&RetentionRoots::new(),
WantRewritePolicy::Drop,
&[],
&mut CarryEveryFrame,
CorruptBlobPolicy::SetAside,
)
.unwrap();
assert_eq!(stats.corrupt_blobs, BTreeSet::from([twice]));
let reader = destination.snapshot().unwrap();
let held: BTreeSet<Inline<Handle<UnknownBlob>>> =
reader.blobs().map(|info| info.unwrap().handle).collect();
assert_eq!(held, BTreeSet::from([first, second]));
drop(reader);
destination.close().unwrap();
source.close().unwrap();
}
#[test]
fn a_corrupt_blob_reached_only_through_an_unknown_frame_or_a_retired_proof_is_found() {
let dir = tempfile::tempdir().unwrap();
let source_path = fresh_empty_pile_path(&dir, "frame-corrupt-source.pile");
let mut source = Pile::open(&source_path).unwrap();
let put = |source: &mut Pile, bytes: Vec<u8>| {
source
.put::<UnknownBlob, _>(Bytes::from_source(bytes))
.unwrap()
};
let child = put(
&mut source,
b"named by a damaged blob's intact word".to_vec(),
);
let mut via_frame_bytes = vec![0x21; 32];
via_frame_bytes.extend_from_slice(&child.raw);
let via_frame = put(&mut source, via_frame_bytes);
let via_proof = put(&mut source, b"named only by a retired proof".to_vec());
source.close().unwrap();
let mut unknown = test_envelope_bytes(TEST_UNKNOWN_KIND_B, 1, ENVELOPE_BLOCK_LEN);
unknown[FRAME_BODY_OFFSET + 7..FRAME_BODY_OFFSET + 39].copy_from_slice(&via_frame.raw);
append_test_bytes(&source_path, &unknown);
let mut body = vec![0x3A; 160];
body[..32].copy_from_slice(&SigningKey::from_bytes(&[83; 32]).verifying_key().to_bytes());
body[32 + 5..32 + 37].copy_from_slice(&via_proof.raw);
body[32 + 96..]
.copy_from_slice(&SigningKey::from_bytes(&[84; 32]).verifying_key().to_bytes());
append_test_bytes(&source_path, &retired_capability_v1_record(&body));
damage_blob_payload(&source_path, via_frame);
damage_blob_payload(&source_path, via_proof);
let mut source = Pile::open(&source_path).unwrap();
let plan = source
.plan_retained_rewrite(
&RetentionRoots::new(),
WantRewritePolicy::Drop,
&[],
&mut CarryEveryFrame,
)
.unwrap();
assert_eq!(plan.corrupt_blobs, BTreeSet::from([via_frame, via_proof]));
let refused_path = fresh_empty_pile_path(&dir, "frame-corrupt-refused.pile");
let mut refused = Pile::open(&refused_path).unwrap();
assert!(matches!(
source.rewrite_retained_into_filtered(
&mut refused,
&RetentionRoots::new(),
WantRewritePolicy::Drop,
&[],
&mut CarryEveryFrame,
CorruptBlobPolicy::Refuse,
),
Err(PileRewriteError::Corrupt { .. })
));
refused.close().unwrap();
assert_eq!(std::fs::metadata(&refused_path).unwrap().len(), 0);
let destination_path = fresh_empty_pile_path(&dir, "frame-corrupt-destination.pile");
let mut destination = Pile::open(&destination_path).unwrap();
let stats = source
.rewrite_retained_into_filtered(
&mut destination,
&RetentionRoots::new(),
WantRewritePolicy::Drop,
&[],
&mut CarryEveryFrame,
CorruptBlobPolicy::SetAside,
)
.unwrap();
assert_eq!(stats.corrupt_blobs, plan.corrupt_blobs);
assert_eq!(
(stats.opaque_frames, stats.retired_capability_proofs),
(1, 1)
);
let reader = destination.snapshot().unwrap();
let held: BTreeSet<Inline<Handle<UnknownBlob>>> =
reader.blobs().map(|info| info.unwrap().handle).collect();
assert_eq!(held, BTreeSet::from([child]));
assert_eq!(held, plan.retained);
drop(reader);
destination.close().unwrap();
source.close().unwrap();
}
#[test]
fn a_plan_counts_the_bytes_of_the_occurrence_the_copy_takes() {
let dir = tempfile::tempdir().unwrap();
let source_path = fresh_empty_pile_path(&dir, "first-corrupt-source.pile");
let payload = vec![0x5C; 1024];
let hash = Inline::<Hash<Blake3>>::new(*blake3::hash(&payload).as_bytes());
let blocks = envelope_blocks_for_payload(1).unwrap();
let mut forged = BlobRecordHeader::new(blocks, 0, 1, hash)
.as_bytes()
.to_vec();
forged.push(0x5C);
forged.resize(blocks as usize * ENVELOPE_BLOCK_LEN, 0);
append_test_bytes(&source_path, &forged);
let key = SigningKey::from_bytes(&[97; 32]);
let mut source = Pile::open(&source_path).unwrap();
let handle = source
.put::<UnknownBlob, _>(Bytes::from_source(payload))
.unwrap();
assert_eq!(handle.raw, hash.raw);
source
.insert(CollectionRecord::Commit(CollectionCommit::sign(
&key,
collection_test_collection(98),
hash,
empty_metadata_handle(),
)))
.unwrap();
source.close().unwrap();
let mut source = Pile::open(&source_path).unwrap();
let plan = source
.plan_retained_rewrite(
&RetentionRoots::new(),
WantRewritePolicy::Drop,
&[],
&mut CarryEveryFrame,
)
.unwrap();
let destination_path = fresh_empty_pile_path(&dir, "first-corrupt-destination.pile");
let mut destination = Pile::open(&destination_path).unwrap();
source
.rewrite_retained_into_filtered(
&mut destination,
&RetentionRoots::new(),
WantRewritePolicy::Drop,
&[],
&mut CarryEveryFrame,
CorruptBlobPolicy::Refuse,
)
.unwrap();
let reader = destination.snapshot().unwrap();
let held: u64 = reader.blobs().map(|info| info.unwrap().length).sum();
drop(reader);
destination.close().unwrap();
source.close().unwrap();
assert_eq!(held, 1024);
assert_eq!(plan.retained_bytes, held);
}
#[test]
fn want_noop_does_not_grow_file() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "pile.pile");
let h: Inline<Handle<UnknownBlob>> =
Blob::<UnknownBlob>::new(Bytes::from_source(vec![3u8; 5])).get_handle();
let mut pile: Pile = Pile::open(&path).unwrap();
pile.want(WantRequest::blob(h)).unwrap();
let len_after_pin = std::fs::metadata(&path).unwrap().len();
assert_eq!(len_after_pin, ENVELOPE_HEADER_LEN as u64);
pile.want(WantRequest::blob(h)).unwrap();
assert_eq!(std::fs::metadata(&path).unwrap().len(), len_after_pin);
pile.close().unwrap();
}
#[test]
fn mixed_v1_enveloped_branch_and_weak_markers_interleave() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "mixed.pile");
let v1_data = vec![9u8; 40];
let v1_blob: Blob<UnknownBlob> = Blob::new(Bytes::from_source(v1_data.clone()));
let v1_handle: Inline<Handle<UnknownBlob>> = v1_blob.get_handle();
{
let v1_hash: Inline<Hash<Blake3>> = v1_handle.into();
let header = BlobHeader::new(42, v1_data.len() as u64, v1_hash);
let pad = padding_for_blob(v1_data.len());
let mut f = std::fs::OpenOptions::new()
.append(true)
.open(&path)
.unwrap();
f.write_all(header.as_bytes()).unwrap();
f.write_all(&v1_data).unwrap();
f.write_all(&vec![0u8; pad]).unwrap();
f.sync_all().unwrap();
}
let branch_id = Id::new([5u8; 16]).unwrap();
let want: Inline<Handle<UnknownBlob>> =
Blob::<UnknownBlob>::new(Bytes::from_source(vec![11u8; 13])).get_handle();
let mut pile: Pile = Pile::open(&path).unwrap();
pile.amputate().unwrap();
pile.want(WantRequest::blob(want)).unwrap();
let d1 = vec![1u8; 300];
let b1: Blob<UnknownBlob> = Blob::new(Bytes::from_source(d1.clone()));
let h1 = pile.put::<UnknownBlob, _>(b1).unwrap();
pile.append_legacy_pin_for_test(branch_id, None, Some(h1.transmute()))
.unwrap();
let d2 = vec![2u8; 77];
let b2: Blob<UnknownBlob> = Blob::new(Bytes::from_source(d2.clone()));
let h2 = pile.put::<UnknownBlob, _>(b2).unwrap();
pile.close().unwrap();
let mut pile: Pile = Pile::open(&path).unwrap();
pile.amputate().unwrap();
let reader = pile.snapshot().unwrap();
let got_v1: Blob<UnknownBlob> = reader.get(v1_handle).unwrap();
assert_eq!(got_v1.bytes.as_ref(), v1_data.as_slice());
let got1: Blob<UnknownBlob> = reader.get(h1).unwrap();
assert_eq!(got1.bytes.as_ref(), d1.as_slice());
let got2: Blob<UnknownBlob> = reader.get(h2).unwrap();
assert_eq!(got2.bytes.as_ref(), d2.as_slice());
drop(reader);
assert_eq!(
pile.legacy_pin_head_for_test(branch_id).unwrap(),
Some(h1.transmute())
);
let pinned: HashSet<_> = pile
.snapshot()
.unwrap()
.wants()
.unwrap()
.map(|r| r.unwrap())
.collect();
assert_eq!(pinned.len(), 1);
assert!(pinned.contains(&WantRequest::blob(want)));
pile.close().unwrap();
}
#[test]
fn pile_records_walks_mixed_pile_and_fails_loud() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "records.pile");
let v1_data = vec![9u8; 40];
let v1_blob: Blob<UnknownBlob> = Blob::new(Bytes::from_source(v1_data.clone()));
let v1_handle: Inline<Handle<UnknownBlob>> = v1_blob.get_handle();
{
let v1_hash: Inline<Hash<Blake3>> = v1_handle.into();
let header = BlobHeader::new(42, v1_data.len() as u64, v1_hash);
let pad = padding_for_blob(v1_data.len());
let mut f = std::fs::OpenOptions::new()
.append(true)
.open(&path)
.unwrap();
f.write_all(header.as_bytes()).unwrap();
f.write_all(&v1_data).unwrap();
f.write_all(&vec![0u8; pad]).unwrap();
f.sync_all().unwrap();
}
let branch_id = Id::new([5u8; 16]).unwrap();
let want: Inline<Handle<UnknownBlob>> =
Blob::<UnknownBlob>::new(Bytes::from_source(vec![11u8; 13])).get_handle();
let mut pile: Pile = Pile::open(&path).unwrap();
pile.amputate().unwrap();
let d1 = vec![1u8; 300];
let b1: Blob<UnknownBlob> = Blob::new(Bytes::from_source(d1.clone()));
let h1 = pile.put::<UnknownBlob, _>(b1).unwrap();
pile.append_legacy_pin_for_test(branch_id, None, Some(h1.transmute()))
.unwrap();
pile.want(WantRequest::blob(want)).unwrap();
pile.append_legacy_pin_for_test(branch_id, Some(h1.transmute()), None)
.unwrap();
pile.close().unwrap();
let mut records = PileRecords::open(&path).unwrap();
let bytes = records.bytes().clone();
let decoded: Vec<PileRecord> = (&mut records)
.map(|r| r.expect("well-formed pile decodes cleanly"))
.collect();
let mut expected_offset = 0;
for record in &decoded {
assert_eq!(record.offset, expected_offset);
expected_offset += record.len;
}
assert_eq!(expected_offset, bytes.len());
assert_eq!(decoded.len(), 5);
match decoded[0].content {
PileRecordContent::Blob {
timestamp,
hash,
data_offset,
data_len,
} => {
assert_eq!(timestamp, 42);
assert_eq!(hash, v1_handle.into());
assert_eq!(data_offset, BLOB_HEADER_LEN);
assert_eq!(&bytes[data_offset..data_offset + data_len], &v1_data[..]);
}
other => panic!("expected V1 blob record, got {other:?}"),
}
match decoded[1].content {
PileRecordContent::Blob {
hash,
data_offset,
data_len,
..
} => {
assert_eq!(hash, h1.into());
assert_eq!(data_offset, decoded[1].offset + ENVELOPE_HEADER_LEN);
assert_eq!(&bytes[data_offset..data_offset + data_len], &d1[..]);
}
other => panic!("expected enveloped blob record, got {other:?}"),
}
match decoded[2].content {
PileRecordContent::Branch {
branch_id: bid,
head,
} => {
assert_eq!(bid, branch_id);
assert_eq!(head, h1.transmute());
}
other => panic!("expected branch record, got {other:?}"),
}
match decoded[3].content {
PileRecordContent::Want { request } => {
assert_eq!(request, WantRequest::blob(want))
}
other => panic!("expected current WANT record, got {other:?}"),
}
match decoded[4].content {
PileRecordContent::BranchTombstone { branch_id: bid } => {
assert_eq!(bid, branch_id)
}
other => panic!("expected branch tombstone record, got {other:?}"),
}
let unknown_offset = std::fs::metadata(&path).unwrap().len() as usize;
let unknown_marker = [0xFFu8; 16];
{
let mut f = std::fs::OpenOptions::new()
.append(true)
.open(&path)
.unwrap();
f.write_all(&unknown_marker).unwrap();
f.sync_all().unwrap();
}
let mut records = PileRecords::open(&path).unwrap();
let mut ok = 0;
let err = loop {
match records.next() {
Some(Ok(_)) => ok += 1,
Some(Err(e)) => break e,
None => panic!("iterator ended without reporting the corrupt tail"),
}
};
assert_eq!(ok, 5);
match err {
ReadError::UnsupportedRecord { offset, marker } => {
assert_eq!(offset, unknown_offset);
assert_eq!(marker, unknown_marker);
}
other => panic!("expected UnsupportedRecord, got {other:?}"),
}
assert!(records.next().is_none(), "iterator must end after an error");
}
#[test]
fn retired_team_records_remain_readable_but_are_not_repository_state() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "retired-team-state.pile");
let mut observer = Pile::open(&path).unwrap();
let before = observer.snapshot().unwrap();
let mut bytes = retired_peer_record(1, 2);
bytes.extend_from_slice(&retired_store_scope_record(3));
std::fs::OpenOptions::new()
.append(true)
.open(&path)
.unwrap()
.write_all(&bytes)
.unwrap();
let after = observer.snapshot().unwrap();
assert_eq!(after.changes_since(&before), StoreChanges::NONE);
assert_eq!(observer.opaque_record_count().unwrap(), 0);
let records = PileRecords::open(&path)
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap();
assert!(matches!(
records[0].content,
PileRecordContent::RetiredPeerEvidenceV1
));
assert!(matches!(
records[1].content,
PileRecordContent::RetiredStoreScopeV1
));
observer.close().unwrap();
}
#[test]
fn retired_team_records_only_validate_physical_layout() {
let mut scope = retired_store_scope_record(4);
scope[96] = 1;
assert!(matches!(
decode_record(&scope, 0),
Err(ReadError::CorruptPile { valid_length: 0 })
));
let mut peer = retired_peer_record(5, 6);
peer[128] = 1;
assert!(matches!(
decode_record(&peer, 0),
Err(ReadError::CorruptPile { valid_length: 0 })
));
let mut arbitrary = retired_peer_record(7, 8);
arbitrary[64..128].fill(0xFF);
assert!(matches!(
decode_record(&arbitrary, 0).unwrap().content,
PileRecordContent::RetiredPeerEvidenceV1
));
}
#[test]
fn semantic_rewrites_drop_retired_team_records() {
let dir = tempfile::tempdir().unwrap();
let source_path = fresh_empty_pile_path(&dir, "source.pile");
let reframe_path = fresh_empty_pile_path(&dir, "reframed.pile");
let rewrite_path = fresh_empty_pile_path(&dir, "rewritten.pile");
let mut bytes = retired_peer_record(7, 8);
bytes.extend_from_slice(&retired_store_scope_record(9));
std::fs::write(&source_path, bytes).unwrap();
let mut reframed = Pile::open(&reframe_path).unwrap();
let stats = reframe_into(&source_path, &mut reframed).unwrap();
assert_eq!(stats.dropped_inert, 2);
reframed.close().unwrap();
assert_eq!(std::fs::metadata(&reframe_path).unwrap().len(), 0);
let mut source = Pile::open(&source_path).unwrap();
let mut rewritten = Pile::open(&rewrite_path).unwrap();
let stats = source
.rewrite_retained_into(
&mut rewritten,
&RetentionRoots::new(),
WantRewritePolicy::Drop,
)
.unwrap();
assert_eq!(stats.retained_blobs, 0);
source.close().unwrap();
rewritten.close().unwrap();
assert_eq!(std::fs::metadata(&rewrite_path).unwrap().len(), 0);
}
#[test]
#[ignore]
fn put_and_get_oversized_blob() {
let dir = tempfile::tempdir().unwrap();
let path = fresh_empty_pile_path(&dir, "pile.pile");
let size = ATOMIC_WRITE_LIMIT + 1_024;
let mut data = vec![0u8; size];
for (i, b) in data.iter_mut().enumerate() {
*b = (i as u8).wrapping_mul(13).wrapping_add(7);
}
let mut pile: Pile = Pile::open(&path).unwrap();
let blob: Blob<UnknownBlob> = Blob::new(Bytes::from_source(data.clone()));
let handle = pile.put::<UnknownBlob, _>(blob).unwrap();
{
let reader = pile.snapshot().unwrap();
let fetched: Blob<UnknownBlob> = reader.get(handle).unwrap();
assert_eq!(fetched.bytes.len(), size);
assert_eq!(fetched.bytes.as_ref(), data.as_slice());
}
pile.close().unwrap();
let mut pile: Pile = Pile::open(&path).unwrap();
pile.amputate().unwrap();
let reader = pile.snapshot().unwrap();
let fetched: Blob<UnknownBlob> = reader.get(handle).unwrap();
assert_eq!(fetched.bytes.as_ref(), data.as_slice());
pile.close().unwrap();
}
}