use std::collections::BTreeSet;
use std::path::Path;
use crate::pager::page_space::is_reserved;
use crate::snapshot::apply::{plan_delta_stream, write_delta_into_image};
use crate::snapshot::export::{SnapshotManifest, decode_manifest, encode_manifest};
use crate::vfs::tokio_backend::TokioVfs;
use proptest::prelude::*;
const DELTA_PAGE_SIZE: usize = 64;
const FOLLOWER_PAGE_COUNT: usize = 32;
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 block_on<F: std::future::Future>(future: F) -> F::Output {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(future)
}
fn valid_manifest() -> SnapshotManifest {
SnapshotManifest {
version: 1,
kind: 1,
target_commit: 9,
base_commit: 4,
file_id: [0x11; 16],
mk_epoch: 0,
kek_salt: [0x22; 16],
cipher_id: 1,
page_size: 4096,
next_page_id_at_target: 64,
segments_count: 2,
realm_id: [0x5A; 16],
target_active_root_page_id: 8,
target_catalog_root_page_id: 9,
}
}
fn write_follower_main_db(path: &Path) -> Vec<u8> {
let contents = vec![0xEE_u8; FOLLOWER_PAGE_COUNT * DELTA_PAGE_SIZE];
std::fs::write(path, &contents).unwrap();
contents
}
fn delta_record(page_id: u64, filler: u8) -> Vec<u8> {
let mut record = page_id.to_be_bytes().to_vec();
record.extend(std::iter::repeat_n(filler, DELTA_PAGE_SIZE));
record
}
proptest! {
#![proptest_config(config())]
#[test]
fn random_bytes_never_panic_the_manifest_decoder(
filler in prop::collection::vec(any::<u8>(), 0..=240),
key in any::<[u8; 32]>(),
) {
let mut buf = encode_manifest(&valid_manifest(), &[0u8; 32]);
for (slot, byte) in buf.iter_mut().zip(filler.iter()) {
*slot = *byte;
}
let _ = decode_manifest(&buf, &key);
}
#[test]
fn perturbed_valid_manifest_is_never_accepted(
edits in prop::collection::vec((any::<usize>(), any::<u8>()), 1..=8),
) {
let key = [0x33_u8; 32];
let manifest = valid_manifest();
let encoded = encode_manifest(&manifest, &key);
let decoded = decode_manifest(&encoded, &key).unwrap();
prop_assert_eq!(decoded.target_commit, manifest.target_commit);
let mut mutated = encoded;
let mut changed = false;
for (index, value) in edits {
let at = index % mutated.len();
changed |= mutated[at] != value;
mutated[at] = value;
}
if changed {
prop_assert!(
decode_manifest(&mutated, &key).is_err(),
"a tampered manifest must not authenticate"
);
}
}
#[test]
fn manifest_under_a_foreign_key_is_rejected(
producer_key in any::<[u8; 32]>(),
consumer_key in any::<[u8; 32]>(),
) {
prop_assume!(producer_key != consumer_key);
let encoded = encode_manifest(&valid_manifest(), &producer_key);
prop_assert!(decode_manifest(&encoded, &consumer_key).is_err());
}
#[test]
fn random_delta_bytes_never_panic_and_never_write(
bytes in prop::collection::vec(any::<u8>(), 0..=(4 * (8 + DELTA_PAGE_SIZE) + 8)),
target_next_page_id in 0u64..1024,
) {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("pages.delta"), &bytes).unwrap();
let main_db = dir.path().join("main.db");
let before = write_follower_main_db(&main_db);
let _ = block_on(plan_delta_stream(
dir.path(),
DELTA_PAGE_SIZE,
&BTreeSet::new(),
target_next_page_id,
));
let after = std::fs::read(&main_db).unwrap();
prop_assert_eq!(after, before, "planning a delta stream must write nothing");
}
#[test]
fn plausible_delta_records_never_panic_and_never_partially_write(
page_ids in prop::collection::vec(
prop_oneof![
0u64..8,
(FOLLOWER_PAGE_COUNT as u64 - 4)..(FOLLOWER_PAGE_COUNT as u64 + 4),
Just(u64::MAX),
Just(u64::MAX / 2),
],
0..=6,
),
target_next_page_id in 0u64..(FOLLOWER_PAGE_COUNT as u64 + 8),
base_live in prop::collection::vec(0u64..16, 0..=3),
filler in any::<u8>(),
) {
let dir = tempfile::tempdir().unwrap();
let mut stream = Vec::new();
for &page_id in &page_ids {
stream.extend_from_slice(&delta_record(page_id, filler));
}
std::fs::write(dir.path().join("pages.delta"), &stream).unwrap();
let main_db = dir.path().join("main.db");
let before = write_follower_main_db(&main_db);
let base_live: BTreeSet<u64> = base_live.into_iter().collect();
let outcome = block_on(plan_delta_stream(
dir.path(),
DELTA_PAGE_SIZE,
&base_live,
target_next_page_id,
));
if let Ok(plan) = outcome {
prop_assert_eq!(plan.record_count as usize, page_ids.len());
for page_id in &plan.page_ids {
prop_assert!(!is_reserved(*page_id));
prop_assert!(*page_id < target_next_page_id);
prop_assert!(!base_live.contains(page_id));
}
let vfs = TokioVfs::new(dir.path());
block_on(write_delta_into_image(
&vfs,
"/main.db.applying",
dir.path(),
DELTA_PAGE_SIZE,
&plan,
)).unwrap();
let after = std::fs::read(&main_db).unwrap();
prop_assert_eq!(after, before, "an applied delta must not touch main.db");
if !plan.page_ids.is_empty() {
let staged = std::fs::read(dir.path().join("main.db.applying")).unwrap();
for page_id in &plan.page_ids {
let offset = usize::try_from(*page_id).unwrap() * DELTA_PAGE_SIZE;
prop_assert_eq!(
&staged[offset..offset + DELTA_PAGE_SIZE],
&vec![filler; DELTA_PAGE_SIZE][..],
"delta page {} must be present in the staged image",
page_id
);
}
}
} else {
let after = std::fs::read(&main_db).unwrap();
prop_assert_eq!(
after,
before,
"a rejected delta stream must leave main.db untouched"
);
}
}
}