use crate::btree::internal::{Internal, InternalEntry};
use crate::btree::leaf::{Leaf, LeafValue};
use crate::btree::node::{
HEADER_LEN, NodeKind, OFF_PREFIX_LEN, OFF_SLOT_COUNT, body_capacity, validate_node_body,
};
use proptest::prelude::*;
const PAGE: usize = 4096;
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 inline_records() -> impl Strategy<Value = Vec<(Vec<u8>, Vec<u8>)>> {
prop::collection::vec(
(
prop::collection::vec(any::<u8>(), 1..=12),
prop::collection::vec(any::<u8>(), 0..=24),
),
0..=12,
)
}
fn encoded_leaf(records: &[(Vec<u8>, Vec<u8>)], overflow_every: Option<u64>) -> Option<Vec<u8>> {
let mut leaf = Leaf::new();
for (index, (key, value)) in records.iter().enumerate() {
let value = match overflow_every {
Some(root_page_id) if index % 2 == 0 => LeafValue::Overflow {
total_len: value.len() as u64,
root_page_id,
},
_ => LeafValue::Inline(value.clone().into()),
};
leaf.upsert(key, value);
}
let mut body = vec![0u8; body_capacity(PAGE)];
leaf.encode(&mut body).ok()?;
Some(body)
}
fn encoded_internal(leftmost_child: u64, mut entries: Vec<(Vec<u8>, u64)>) -> Option<Vec<u8>> {
entries.sort();
entries.dedup_by(|a, b| a.0 == b.0);
let internal = Internal {
leftmost_child,
entries: entries
.into_iter()
.map(|(key, right_child)| InternalEntry { key, right_child })
.collect(),
};
let mut body = vec![0u8; body_capacity(PAGE)];
internal.encode(&mut body).ok()?;
Some(body)
}
fn slot_directory_span(body: &[u8]) -> Option<(usize, usize)> {
if body.len() < HEADER_LEN {
return None;
}
let slot_count = usize::from(u16::from_le_bytes([
body[OFF_SLOT_COUNT],
body[OFF_SLOT_COUNT + 1],
]));
let prefix_len = usize::from(u16::from_le_bytes([
body[OFF_PREFIX_LEN],
body[OFF_PREFIX_LEN + 1],
]));
Some((HEADER_LEN + prefix_len, slot_count))
}
proptest! {
#![proptest_config(config())]
#[test]
fn random_bytes_never_panic_any_node_decoder(
body in prop::collection::vec(any::<u8>(), 0..=body_capacity(PAGE)),
) {
let _ = validate_node_body(&body);
let _ = Leaf::decode(&body);
let _ = Internal::decode(&body);
}
#[test]
fn random_bytes_under_a_legal_kind_byte_never_panic(
kind in prop::sample::select(vec![NodeKind::Internal, NodeKind::Leaf]),
tail in prop::collection::vec(any::<u8>(), 0..=body_capacity(PAGE)),
) {
let mut body = vec![kind.as_byte()];
body.extend_from_slice(&tail);
let _ = validate_node_body(&body);
let _ = Leaf::decode(&body);
let _ = Internal::decode(&body);
}
#[test]
fn perturbed_valid_leaf_never_panics(
records in inline_records(),
overflow_root in prop::option::of(4u64..u64::MAX),
edits in prop::collection::vec((any::<usize>(), any::<u8>()), 1..=24),
) {
let Some(body) = encoded_leaf(&records, overflow_root) else {
return Ok(());
};
let mutated = perturb(body, &edits);
let _ = validate_node_body(&mutated);
let _ = Leaf::decode(&mutated);
let _ = Internal::decode(&mutated);
}
#[test]
fn perturbed_valid_internal_never_panics(
leftmost_child in any::<u64>(),
entries in prop::collection::vec(
(prop::collection::vec(any::<u8>(), 1..=12), any::<u64>()),
0..=12,
),
edits in prop::collection::vec((any::<usize>(), any::<u8>()), 1..=24),
) {
let Some(body) = encoded_internal(leftmost_child, entries) else {
return Ok(());
};
let mutated = perturb(body, &edits);
let _ = validate_node_body(&mutated);
let _ = Internal::decode(&mutated);
let _ = Leaf::decode(&mutated);
}
#[test]
fn hostile_header_and_slot_directory_never_panic(
records in inline_records(),
as_internal in any::<bool>(),
slot_count in any::<u16>(),
prefix_len in any::<u16>(),
slot_values in prop::collection::vec(any::<u16>(), 0..=16),
) {
let body = if as_internal {
encoded_internal(
7,
records.iter().map(|(key, _)| (key.clone(), 9)).collect(),
)
} else {
encoded_leaf(&records, None)
};
let Some(mut body) = body else { return Ok(()) };
if let Some((directory_start, original_slots)) = slot_directory_span(&body) {
for (slot, value) in slot_values
.iter()
.enumerate()
.take(original_slots.min(slot_values.len()))
{
let at = directory_start + slot * 2;
if at + 2 <= body.len() {
body[at..at + 2].copy_from_slice(&value.to_le_bytes());
}
}
}
body[OFF_SLOT_COUNT..OFF_SLOT_COUNT + 2].copy_from_slice(&slot_count.to_le_bytes());
body[OFF_PREFIX_LEN..OFF_PREFIX_LEN + 2].copy_from_slice(&prefix_len.to_le_bytes());
if validate_node_body(&body).is_ok() {
let _ = Leaf::decode(&body);
let _ = Internal::decode(&body);
}
let _ = Leaf::decode(&body);
let _ = Internal::decode(&body);
}
#[test]
fn truncated_valid_node_never_panics(
records in inline_records(),
keep in 0usize..=body_capacity(PAGE),
) {
let Some(body) = encoded_leaf(&records, None) else {
return Ok(());
};
let truncated = &body[..keep.min(body.len())];
let _ = validate_node_body(truncated);
let _ = Leaf::decode(truncated);
let _ = Internal::decode(truncated);
}
#[test]
fn node_kind_byte_dispatch_is_total(byte in any::<u8>()) {
match NodeKind::from_byte(byte) {
Ok(kind) => prop_assert_eq!(kind.as_byte(), byte),
Err(_) => prop_assert!(byte > 0x01),
}
}
}