use std::path::{Path, PathBuf};
use std::sync::Arc;
use tessarikv_format::{FRAME_HEADER_BYTES, SEAL_HEADER_BYTES, frame_header, payload_checksum};
use super::Db;
use super::flush_tests::PATH;
use crate::log::fault::FaultStorage;
use crate::storage::Storage as _;
use crate::{
DbOptions, Error, KeyProvider, KeyspaceId, ReadOptions, RootKey, WalRecovery, WriteBatch,
WriteOptions,
};
#[derive(Debug)]
struct Key;
impl KeyProvider for Key {
fn root_key(&self, key_id: u32) -> Option<RootKey> {
(key_id == 1).then(|| RootKey::new([7; 32]))
}
}
fn options(encrypted: bool, recovery: WalRecovery) -> DbOptions {
let builder = DbOptions::builder().wal_recovery(recovery);
let builder = if encrypted {
builder.encryption(Arc::new(Key), 1)
} else {
builder
};
builder.build().expect("valid")
}
async fn write(db: &Db) {
for (index, size) in [(0_u32, 1), (1, 400)] {
let mut batch = WriteBatch::new();
for entry in 0..size {
batch
.put(
KeyspaceId::DEFAULT,
format!("secret-key-{index}-{entry:04}").as_bytes(),
format!("secret-value-{index}-{entry:04}").as_bytes(),
)
.expect("entry");
}
db.write(&batch, &WriteOptions::default())
.await
.expect("written");
}
}
async fn found(db: &Db, key: &str) -> bool {
db.get(KeyspaceId::DEFAULT, key.as_bytes(), &ReadOptions::default())
.await
.expect("read")
.is_some()
}
fn segments(storage: &FaultStorage) -> Vec<PathBuf> {
let mut names: Vec<String> = storage
.list(Path::new(PATH))
.expect("listable")
.into_iter()
.filter(|name| name.ends_with(".log"))
.collect();
names.sort();
names
.iter()
.map(|name| Path::new(PATH).join(name))
.collect()
}
fn contains(haystack: &[u8], needle: &[u8]) -> bool {
haystack
.windows(needle.len())
.any(|window| window == needle)
}
async fn crashed(encrypted: bool) -> FaultStorage {
let storage = FaultStorage::default();
let db = Db::open_with_storage(
PATH,
options(encrypted, WalRecovery::PointInTime),
Arc::new(storage.clone()),
)
.await
.expect("opened");
write(&db).await;
let image = storage.crash(Path::new(PATH), true);
db.close().await.expect("closed");
image
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_sealed_log_holds_no_plaintext_and_replays_after_a_crash() {
let image = crashed(true).await;
let logs = segments(&image);
assert!(!logs.is_empty());
for path in &logs {
let bytes = image.bytes(path);
assert_eq!(bytes.get(..4), Some(&b"TKVS"[..]), "{path:?} starts sealed");
assert!(!contains(&bytes, b"secret-"), "{path:?} shows plaintext");
}
let db = Db::open_with_storage(
PATH,
options(true, WalRecovery::PointInTime),
Arc::new(image),
)
.await
.expect("recovered");
assert!(found(&db, "secret-key-0-0000").await);
assert!(found(&db, "secret-key-1-0399").await);
db.close().await.expect("closed");
let plain = crashed(false).await;
let shown = segments(&plain)
.iter()
.any(|path| contains(&plain.bytes(path), b"secret-"));
assert!(shown, "control: a plain log shows its keys");
}
fn alter_first_frame(storage: &FaultStorage, fix_checksum: bool) {
let path = segments(storage).remove(0);
let log_number: u64 = path
.file_stem()
.and_then(|stem| stem.to_str())
.and_then(|stem| stem.parse().ok())
.expect("a log number");
let mut bytes = storage.bytes(&path);
let header_at = SEAL_HEADER_BYTES;
let payload_at = header_at + FRAME_HEADER_BYTES;
let payload_len = u32::from_le_bytes(bytes[header_at + 4..header_at + 8].try_into().unwrap());
let payload_end = payload_at + usize::try_from(payload_len).unwrap();
bytes[payload_at + 8] ^= 1;
if fix_checksum {
let checksum = payload_checksum(&bytes[payload_at..payload_end]);
let header = frame_header(log_number, payload_end - payload_at, checksum).unwrap();
bytes[header_at..payload_at].copy_from_slice(&header);
}
let mut file = storage.create(&path).expect("replaced");
file.append(&bytes).expect("written");
file.sync().expect("synced");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_frame_altered_under_a_valid_checksum_fails_the_open() {
for recovery in [WalRecovery::PointInTime, WalRecovery::Absolute] {
let image = crashed(true).await;
alter_first_frame(&image, true);
let opened =
Db::open_with_storage(PATH, options(true, recovery), Arc::new(image.clone())).await;
assert!(
matches!(&opened, Err(Error::Corruption { detail, .. }) if detail.contains("authentication")),
"{recovery:?}: {:?}",
opened.as_ref().err()
);
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_torn_sealed_frame_is_cut_like_any_torn_tail() {
let image = crashed(true).await;
alter_first_frame(&image, false);
let db = Db::open_with_storage(
PATH,
options(true, WalRecovery::PointInTime),
Arc::new(image),
)
.await
.expect("recovered to the valid prefix");
assert!(!found(&db, "secret-key-0-0000").await);
assert!(!found(&db, "secret-key-1-0000").await);
db.close().await.expect("closed");
}
fn sealed_batches(storage: &FaultStorage, path: &Path) -> Vec<Vec<u8>> {
let bytes = storage.bytes(path);
let mut at = SEAL_HEADER_BYTES;
let mut batches = Vec::new();
while at + FRAME_HEADER_BYTES <= bytes.len() {
let len = u32::from_le_bytes(bytes[at + 4..at + 8].try_into().expect("four bytes"));
let payload = at + FRAME_HEADER_BYTES;
let end = payload + usize::try_from(len).expect("fits");
batches.push(bytes[payload + 8..end].to_vec());
at = end;
}
batches
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn identical_batches_seal_differently() {
let storage = FaultStorage::default();
let db = Db::open_with_storage(
PATH,
options(true, WalRecovery::PointInTime),
Arc::new(storage.clone()),
)
.await
.expect("opened");
let mut batch = WriteBatch::new();
batch
.put(KeyspaceId::DEFAULT, b"same", b"bytes")
.expect("entry");
for _ in 0..2 {
db.write(&batch, &WriteOptions::default())
.await
.expect("written");
}
let batches = sealed_batches(&storage, &segments(&storage)[0]);
db.close().await.expect("closed");
assert_eq!(batches.len(), 2);
assert_eq!(batches[0].len(), batches[1].len());
assert_ne!(batches[0], batches[1]);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_segment_cut_inside_its_header_is_torn() {
let image = crashed(true).await;
let path = segments(&image).remove(0);
let bytes = image.bytes(&path);
let mut file = image.create(&path).expect("replaced");
file.append(&bytes[..20]).expect("written");
file.sync().expect("synced");
let absolute = Db::open_with_storage(
PATH,
options(true, WalRecovery::Absolute),
Arc::new(image.clone()),
)
.await;
assert!(
matches!(absolute, Err(Error::Corruption { .. })),
"{:?}",
absolute.as_ref().err()
);
let db = Db::open_with_storage(
PATH,
options(true, WalRecovery::PointInTime),
Arc::new(image),
)
.await
.expect("cut back to nothing");
assert!(!found(&db, "secret-key-0-0000").await);
db.close().await.expect("closed");
}