use crate::error::{DbxError, DbxResult};
const MAGIC: u32 = 0xD8B_B00F;
const VERSION: u16 = 1;
#[derive(Clone)]
pub struct PageEntry {
pub key: Vec<u8>,
pub value: Vec<u8>,
pub deleted: bool,
}
pub struct WosPage {
pub entries: Vec<PageEntry>,
}
impl WosPage {
pub fn from_entries(entries: Vec<PageEntry>) -> Self {
Self { entries }
}
pub fn serialize(&self) -> DbxResult<Vec<u8>> {
let mut buf = Vec::new();
buf.extend_from_slice(&MAGIC.to_le_bytes());
buf.extend_from_slice(&VERSION.to_le_bytes());
buf.extend_from_slice(&(self.entries.len() as u32).to_le_bytes());
for e in &self.entries {
buf.extend_from_slice(&(e.key.len() as u32).to_le_bytes());
buf.extend_from_slice(&e.key);
buf.extend_from_slice(&(e.value.len() as u32).to_le_bytes());
buf.extend_from_slice(&e.value);
buf.push(e.deleted as u8);
}
let crc = crc32fast::hash(&buf);
buf.extend_from_slice(&crc.to_le_bytes());
Ok(buf)
}
pub fn deserialize(bytes: &[u8]) -> DbxResult<Self> {
if bytes.len() < 14 {
return Err(DbxError::Storage("WosPage: too short".into()));
}
let payload = &bytes[..bytes.len() - 4];
let stored_crc = u32::from_le_bytes(bytes[bytes.len() - 4..].try_into().unwrap());
if crc32fast::hash(payload) != stored_crc {
return Err(DbxError::Storage("WosPage: checksum mismatch".into()));
}
let mut cur = 0usize;
let _magic = u32::from_le_bytes(bytes[cur..cur + 4].try_into().unwrap());
cur += 4;
let _ver = u16::from_le_bytes(bytes[cur..cur + 2].try_into().unwrap());
cur += 2;
let count = u32::from_le_bytes(bytes[cur..cur + 4].try_into().unwrap()) as usize;
cur += 4;
let mut entries = Vec::with_capacity(count);
for _ in 0..count {
let klen = u32::from_le_bytes(bytes[cur..cur + 4].try_into().unwrap()) as usize;
cur += 4;
let key = bytes[cur..cur + klen].to_vec();
cur += klen;
let vlen = u32::from_le_bytes(bytes[cur..cur + 4].try_into().unwrap()) as usize;
cur += 4;
let value = bytes[cur..cur + vlen].to_vec();
cur += vlen;
let deleted = bytes[cur] != 0;
cur += 1;
entries.push(PageEntry {
key,
value,
deleted,
});
}
Ok(Self { entries })
}
}
#[cfg(test)]
mod tests {
use super::*;
fn entry(key: &[u8], value: &[u8]) -> PageEntry {
PageEntry {
key: key.to_vec(),
value: value.to_vec(),
deleted: false,
}
}
fn deleted(key: &[u8]) -> PageEntry {
PageEntry {
key: key.to_vec(),
value: vec![],
deleted: true,
}
}
#[test]
fn page_roundtrip() {
let entries = vec![entry(b"aaa", b"val1"), entry(b"bbb", b"val2")];
let page = WosPage::from_entries(entries);
let bytes = page.serialize().unwrap();
let decoded = WosPage::deserialize(&bytes).unwrap();
assert_eq!(decoded.entries.len(), 2);
assert_eq!(decoded.entries[0].key, b"aaa");
assert_eq!(decoded.entries[0].value, b"val1");
assert_eq!(decoded.entries[1].key, b"bbb");
assert_eq!(decoded.entries[1].value, b"val2");
}
#[test]
fn page_checksum_detected() {
let page = WosPage::from_entries(vec![entry(b"k", b"v")]);
let mut bytes = page.serialize().unwrap();
let len = bytes.len();
bytes[len - 1] ^= 0xFF; assert!(WosPage::deserialize(&bytes).is_err());
}
#[test]
fn deleted_entry_roundtrip() {
let entries = vec![entry(b"k1", b"v1"), deleted(b"k2")];
let page = WosPage::from_entries(entries);
let bytes = page.serialize().unwrap();
let decoded = WosPage::deserialize(&bytes).unwrap();
assert!(!decoded.entries[0].deleted);
assert!(decoded.entries[1].deleted);
assert!(decoded.entries[1].value.is_empty());
}
#[test]
fn empty_page_roundtrip() {
let page = WosPage::from_entries(vec![]);
let bytes = page.serialize().unwrap();
let decoded = WosPage::deserialize(&bytes).unwrap();
assert_eq!(decoded.entries.len(), 0);
}
#[test]
fn large_value_roundtrip() {
let big_val = vec![0xABu8; 65536];
let entries = vec![PageEntry {
key: b"bigkey".to_vec(),
value: big_val.clone(),
deleted: false,
}];
let page = WosPage::from_entries(entries);
let bytes = page.serialize().unwrap();
let decoded = WosPage::deserialize(&bytes).unwrap();
assert_eq!(decoded.entries[0].value, big_val);
}
}