use crate::catalog::codec::{
Catalog, REALM_QUOTAS_LEN, REKEY_INTENT_LEN, REKEY_SEGMENT_PROGRESS_LEN, RekeyIntent,
RekeySegmentProgress, RekeySegmentProgressState, RekeyStage, SEGMENT_META_LEN, SegmentKind,
};
use crate::crypto::CipherId;
use crate::{CommitId, Evictable, RealmId, RealmQuotas, SegmentMeta};
use proptest::prelude::*;
fn cases() -> u32 {
std::env::var("PAGEDB_PROPTEST_CASES")
.ok()
.and_then(|raw| raw.parse().ok())
.unwrap_or(32)
}
fn config() -> ProptestConfig {
ProptestConfig {
cases: cases(),
failure_persistence: None,
..ProptestConfig::default()
}
}
fn perturb(mut bytes: Vec<u8>, edits: &[(usize, u8)]) -> Vec<u8> {
if bytes.is_empty() {
return bytes;
}
for &(index, value) in edits {
let at = index % bytes.len();
bytes[at] = value;
}
bytes
}
fn decode_every_row_kind(bytes: &[u8]) {
let _ = Catalog::decode_segment_meta(bytes);
let _ = Catalog::decode_realm_quotas(bytes);
let _ = Catalog::decode_counter(bytes);
let _ = Catalog::decode_rekey_state(bytes);
let _ = Catalog::decode_rekey_segment_progress(bytes);
}
fn valid_segment_meta(
linked_commit: Option<u64>,
page_count: u64,
total_bytes: u64,
evictable: Evictable,
) -> SegmentMeta {
SegmentMeta {
segment_id: [0x77; 16],
segment_kind: SegmentKind::Unspecified,
realm_id: RealmId::new([0x5A; 16]),
parent_file_id: [0x11; 16],
linked_commit: linked_commit.map(CommitId::new),
page_count,
total_bytes,
final_counter: 42,
mk_epoch: 3,
cipher_id: CipherId::Aes256Gcm.as_byte(),
format_version: 1,
evictable,
}
}
fn valid_rekey_intent(source_mk_epoch: u64, target_mk_epoch: u64) -> RekeyIntent {
RekeyIntent {
source_mk_epoch,
target_mk_epoch,
source_cipher_id: CipherId::Aes256Gcm.as_byte(),
target_cipher_id: CipherId::Aes256Gcm.as_byte(),
same_kek: false,
stage: RekeyStage::Intent,
source_hk_proof: [0x01; 16],
target_hk_proof: [0x02; 16],
}
}
proptest! {
#![proptest_config(config())]
#[test]
fn random_bytes_never_panic_any_catalog_row_decoder(
bytes in prop::collection::vec(any::<u8>(), 0..=(SEGMENT_META_LEN + 8)),
) {
decode_every_row_kind(&bytes);
}
#[test]
fn random_bytes_at_exact_row_widths_never_panic(
width in prop::sample::select(vec![
8usize,
13,
REKEY_SEGMENT_PROGRESS_LEN,
REALM_QUOTAS_LEN,
REKEY_INTENT_LEN,
SEGMENT_META_LEN,
]),
filler in prop::collection::vec(any::<u8>(), 0..=SEGMENT_META_LEN + 8),
) {
let mut bytes = vec![0u8; width];
for (slot, byte) in bytes.iter_mut().zip(filler.iter()) {
*slot = *byte;
}
decode_every_row_kind(&bytes);
}
#[test]
fn perturbed_valid_segment_meta_never_panics(
linked_commit in prop::option::of(any::<u64>()),
page_count in any::<u64>(),
total_bytes in any::<u64>(),
replaceable in any::<bool>(),
edits in prop::collection::vec((any::<usize>(), any::<u8>()), 1..=8),
) {
let evictable = if replaceable {
Evictable::Replaceable
} else {
Evictable::Authoritative
};
let meta = valid_segment_meta(linked_commit, page_count, total_bytes, evictable);
let encoded = Catalog::encode_segment_meta(&meta).to_vec();
prop_assert_eq!(Catalog::decode_segment_meta(&encoded).unwrap(), meta);
let mutated = perturb(encoded, &edits);
decode_every_row_kind(&mutated);
}
#[test]
fn perturbed_valid_realm_quotas_never_panics(
max_pages in prop::option::of(any::<u64>()),
max_dirty_pages in prop::option::of(any::<u64>()),
max_scratch_pages in prop::option::of(any::<u64>()),
max_segment_bytes in prop::option::of(any::<u64>()),
edits in prop::collection::vec((any::<usize>(), any::<u8>()), 1..=8),
) {
let quotas = RealmQuotas {
max_pages,
max_dirty_pages,
max_scratch_pages,
max_segment_bytes,
};
let encoded = Catalog::encode_realm_quotas("as).to_vec();
prop_assert_eq!(Catalog::decode_realm_quotas(&encoded).unwrap(), quotas);
let mutated = perturb(encoded, &edits);
decode_every_row_kind(&mutated);
}
#[test]
fn perturbed_valid_rekey_intent_never_panics(
source_mk_epoch in any::<u64>(),
target_mk_epoch in any::<u64>(),
edits in prop::collection::vec((any::<usize>(), any::<u8>()), 1..=8),
) {
let intent = valid_rekey_intent(source_mk_epoch, target_mk_epoch);
let encoded = Catalog::encode_rekey_intent(&intent).to_vec();
if target_mk_epoch != 0 && target_mk_epoch > source_mk_epoch {
prop_assert_eq!(Catalog::decode_rekey_state(&encoded).unwrap(), intent);
} else {
prop_assert!(Catalog::decode_rekey_state(&encoded).is_err());
}
let mutated = perturb(encoded, &edits);
decode_every_row_kind(&mutated);
}
#[test]
fn off_width_rekey_state_rows_are_always_rejected(
head in any::<u64>(),
tail in any::<u8>(),
width in (0usize..=(REKEY_INTENT_LEN * 2)).prop_filter(
"the fixed width has its own round-trip property",
|width| *width != REKEY_INTENT_LEN,
),
) {
let mut bytes = vec![tail; width];
for (slot, byte) in bytes.iter_mut().zip(head.to_le_bytes()) {
*slot = byte;
}
prop_assert!(Catalog::decode_rekey_state(&bytes).is_err());
}
#[test]
fn perturbed_valid_rekey_segment_progress_never_panics(
replacement_segment_id in any::<[u8; 16]>(),
edits in prop::collection::vec((any::<usize>(), any::<u8>()), 1..=8),
) {
let progress = RekeySegmentProgress {
replacement_segment_id,
state: RekeySegmentProgressState::Sealed,
};
let encoded = Catalog::encode_rekey_segment_progress(progress).to_vec();
prop_assert_eq!(
Catalog::decode_rekey_segment_progress(&encoded).unwrap(),
progress
);
let mutated = perturb(encoded, &edits);
decode_every_row_kind(&mutated);
}
#[test]
fn counter_rows_accept_only_exact_width(
bytes in prop::collection::vec(any::<u8>(), 0..=24),
) {
match Catalog::decode_counter(&bytes) {
Ok(_) => prop_assert_eq!(bytes.len(), 8),
Err(_) => prop_assert_ne!(bytes.len(), 8),
}
}
#[test]
fn row_keys_never_panic(name in prop::collection::vec(any::<u8>(), 0..=2048)) {
let counter = Catalog::counter_key(&name);
prop_assert_eq!(counter.is_ok(), name.len() <= 1024);
if let Ok(key) = counter {
prop_assert_eq!(key.len(), name.len() + 1);
}
}
}