use std::sync::Arc;
use spacedb_store::collection::{COLLECTION_FORMATS_TABLE, DEK_WRAPPINGS_TABLE};
use spacedb_store::{
open_meta, KeyEncode, wrap_fresh_dek, write_store_version, Collection, Compression, Durability,
KeyProvider, KvEngine, MemEngine, MetaStatus, Readable, StaticKeyProvider, StoreError, Table,
WrappedDek, WriteTx, STORE_FORMAT_VERSION,
};
const VAULT_KEY: [u8; 32] = [0x42; 32];
fn provider() -> Arc<dyn KeyProvider> {
Arc::new(StaticKeyProvider::new(VAULT_KEY))
}
fn compressible_value(n: usize) -> String {
format!("{{\"kind\":\"password\",\"site\":\"https://example.com/login\",\"user\":\"ada@example.com\",\"notes\":\"{}\"}}",
"the quick brown fox jumps over the lazy dog ".repeat(n))
}
const SEAL_OVERHEAD: usize = 12 + 16;
fn create_legacy_collection(engine: &MemEngine, name: &str) {
let (wrapped, _dek) = wrap_fresh_dek(&VAULT_KEY, name).unwrap();
let table: Table<String, WrappedDek> = Table::new(DEK_WRAPPINGS_TABLE);
let mut w = engine
.begin_write(Durability::Immediate)
.unwrap();
table.put(&mut w, &name.to_string(), &wrapped).unwrap();
w.commit().unwrap();
}
#[test]
fn new_collections_compress_on_the_engine_side() {
let engine = MemEngine::new();
let col: Collection<String, String> =
Collection::open_or_create(&engine, provider(), "vault", 1).unwrap();
let value = compressible_value(50); let mut w = engine
.begin_write(Durability::Immediate)
.unwrap();
col.put(&mut w, &"row1".to_string(), &value).unwrap();
w.commit().unwrap();
let r = engine.begin_read().unwrap();
let sealed = r.get_raw("vault", &KeyEncode::encode(&"row1".to_string())).unwrap().unwrap();
assert!(
sealed.len() < value.len() / 2,
"sealed {} bytes not < half of plaintext {} — compression did not engage",
sealed.len(),
value.len()
);
let got = col.get(&r, &"row1".to_string()).unwrap().unwrap();
assert_eq!(got, value);
}
#[test]
fn incompressible_rows_cost_exactly_one_byte() {
let engine = MemEngine::new();
let col: Collection<String, Vec<u8>> =
Collection::open_or_create(&engine, provider(), "blobs", 1).unwrap();
let mut state = 0xDEADBEEFCAFEu64;
let value: Vec<u8> = (0..2048u32)
.map(|_| {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
(state >> 56) as u8
})
.collect();
let mut w = engine
.begin_write(Durability::Immediate)
.unwrap();
col.put(&mut w, &"b".to_string(), &value).unwrap();
w.commit().unwrap();
let r = engine.begin_read().unwrap();
let sealed = r.get_raw("blobs", &KeyEncode::encode(&"b".to_string())).unwrap().unwrap();
let encoded_len = value.len() + 2;
assert_eq!(
sealed.len(),
encoded_len + 1 + SEAL_OVERHEAD,
"incompressible row should be exactly encoded + 1 format byte + AEAD overhead"
);
assert_eq!(col.get(&r, &"b".to_string()).unwrap().unwrap(), value);
}
#[test]
fn legacy_collections_stay_legacy_and_unprefixed() {
let engine = MemEngine::new();
create_legacy_collection(&engine, "oldvault");
let col: Collection<String, String> =
Collection::open(&engine, provider(), "oldvault", 1).unwrap();
let value = "short secret".to_string();
let mut w = engine
.begin_write(Durability::Immediate)
.unwrap();
col.put(&mut w, &"k".to_string(), &value).unwrap();
w.commit().unwrap();
let r = engine.begin_read().unwrap();
let sealed = r.get_raw("oldvault", &KeyEncode::encode(&"k".to_string())).unwrap().unwrap();
let encoded_len = value.len() + 1; assert_eq!(sealed.len(), encoded_len + SEAL_OVERHEAD, "legacy row grew a prefix");
assert_eq!(col.get(&r, &"k".to_string()).unwrap().unwrap(), value);
drop(r);
let again: Collection<String, String> =
Collection::open_or_create(&engine, provider(), "oldvault", 1).unwrap();
let r = engine.begin_read().unwrap();
assert_eq!(again.get(&r, &"k".to_string()).unwrap().unwrap(), value);
let fmt_table: Table<String, u8> = Table::new(COLLECTION_FORMATS_TABLE);
assert!(
fmt_table.get(&r, &"oldvault".to_string()).unwrap().is_none(),
"open_or_create silently converted a legacy collection"
);
}
#[test]
fn off_and_on_handles_interoperate_on_one_collection() {
let engine = MemEngine::new();
let on: Collection<String, String> =
Collection::open_or_create(&engine, provider(), "mixed", 1).unwrap();
let off: Collection<String, String> = Collection::open_with(
&engine,
provider(),
"mixed",
1,
Compression::Off,
)
.unwrap();
let value = compressible_value(20);
let mut w = engine
.begin_write(Durability::Immediate)
.unwrap();
on.put(&mut w, &"compressed".to_string(), &value).unwrap();
off.put(&mut w, &"raw".to_string(), &value).unwrap();
w.commit().unwrap();
let r = engine.begin_read().unwrap();
assert_eq!(off.get(&r, &"compressed".to_string()).unwrap().unwrap(), value);
assert_eq!(on.get(&r, &"raw".to_string()).unwrap().unwrap(), value);
let sealed_c = r.get_raw("mixed", &KeyEncode::encode(&"compressed".to_string())).unwrap().unwrap();
let sealed_r = r.get_raw("mixed", &KeyEncode::encode(&"raw".to_string())).unwrap().unwrap();
assert!(sealed_c.len() < sealed_r.len() / 2);
}
#[test]
fn v1_store_migrates_by_stamp_and_v3_is_refused() {
let engine = MemEngine::new();
write_store_version(&engine, 1).unwrap();
match open_meta(&engine).unwrap() {
MetaStatus::Migrated { from } => assert_eq!(from, 1),
other => panic!("expected Migrated from 1, got {other:?}"),
}
assert!(matches!(open_meta(&engine).unwrap(), MetaStatus::Current));
let engine = MemEngine::new();
write_store_version(&engine, STORE_FORMAT_VERSION + 1).unwrap();
match open_meta(&engine) {
Err(StoreError::SchemaTooNew { found, supported }) => {
assert_eq!(found, STORE_FORMAT_VERSION + 1);
assert_eq!(supported, STORE_FORMAT_VERSION);
}
other => panic!("expected SchemaTooNew, got {other:?}"),
}
}