use crate::error::{DbError, DbResult};
pub(crate) const META_SIZE: usize = 16;
const META_MAGIC: &[u8; 4] = b"ARMD";
pub(crate) const META_VERSION: u8 = 2;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Backend {
Bitcask = 0,
FixedStore = 1,
}
impl Backend {
fn from_byte(b: u8) -> Option<Self> {
match b {
0 => Some(Backend::Bitcask),
1 => Some(Backend::FixedStore),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct DbMeta {
pub version: u8,
pub backend: Backend,
pub shard_count: u8,
pub shard_prefix_bits: u8,
pub flags: u8,
}
impl DbMeta {
pub fn encode(&self) -> [u8; META_SIZE] {
let mut buf = [0u8; META_SIZE];
buf[0..4].copy_from_slice(META_MAGIC);
buf[4] = self.version;
buf[5] = self.backend as u8;
buf[6] = self.shard_count;
buf[7] = self.shard_prefix_bits;
buf[8] = self.flags;
buf
}
pub fn decode(bytes: &[u8]) -> DbResult<Self> {
if bytes.len() != META_SIZE {
let hint = if bytes.len() == 4 {
"; pre-versioned 4-byte db.meta is no longer supported, recreate the database"
} else {
""
};
return Err(DbError::FormatMismatch(format!(
"db.meta: unexpected size: expected {META_SIZE}, got {}{hint}",
bytes.len()
)));
}
if &bytes[0..4] != META_MAGIC {
return Err(DbError::FormatMismatch(
"db.meta: bad magic (expected ARMD)".into(),
));
}
let version = bytes[4];
if version != META_VERSION {
return Err(DbError::FormatMismatch(format!(
"db.meta: version mismatch: stored {version}, expected {META_VERSION}"
)));
}
let backend = Backend::from_byte(bytes[5]).ok_or_else(|| {
DbError::FormatMismatch(format!("db.meta: unknown backend byte {}", bytes[5]))
})?;
if bytes[9..16].iter().any(|&b| b != 0) {
return Err(DbError::FormatMismatch(
"db.meta: nonzero reserved bytes".into(),
));
}
let flags = bytes[8];
match backend {
Backend::FixedStore if flags != 0 => {
return Err(DbError::FormatMismatch(format!(
"db.meta: FixedStore flags must be 0, got {flags:#x}"
)));
}
Backend::Bitcask if flags & !0x01 != 0 => {
return Err(DbError::FormatMismatch(format!(
"db.meta: Bitcask has unknown flag bits set: {flags:#x}"
)));
}
_ => {}
}
Ok(DbMeta {
version,
backend,
shard_count: bytes[6],
shard_prefix_bits: bytes[7],
flags,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample(backend: Backend, flags: u8) -> DbMeta {
DbMeta {
version: META_VERSION,
backend,
shard_count: 4,
shard_prefix_bits: 2,
flags,
}
}
#[test]
fn roundtrip_bitcask_plain() {
let m = sample(Backend::Bitcask, 0);
assert_eq!(DbMeta::decode(&m.encode()).unwrap(), m);
}
#[test]
fn roundtrip_bitcask_encrypted() {
let m = sample(Backend::Bitcask, 1);
assert_eq!(DbMeta::decode(&m.encode()).unwrap(), m);
}
#[test]
fn roundtrip_fixedstore() {
let m = sample(Backend::FixedStore, 0);
assert_eq!(DbMeta::decode(&m.encode()).unwrap(), m);
}
#[test]
fn fixedstore_byte_compat() {
let m = sample(Backend::FixedStore, 0);
assert_eq!(
m.encode(),
[b'A', b'R', b'M', b'D', 2, 1, 4, 2, 0, 0, 0, 0, 0, 0, 0, 0]
);
}
#[test]
fn reject_legacy_4_bytes() {
let err = DbMeta::decode(&[4, 2, 0, 0]).unwrap_err();
assert!(matches!(err, DbError::FormatMismatch(_)));
}
#[test]
fn reject_bad_magic() {
let mut b = sample(Backend::Bitcask, 0).encode();
b[0] = b'X';
assert!(matches!(
DbMeta::decode(&b),
Err(DbError::FormatMismatch(_))
));
}
#[test]
fn reject_bad_version() {
let mut b = sample(Backend::Bitcask, 0).encode();
b[4] = 99;
assert!(matches!(
DbMeta::decode(&b),
Err(DbError::FormatMismatch(_))
));
}
#[test]
fn reject_unknown_backend() {
let mut b = sample(Backend::Bitcask, 0).encode();
b[5] = 7;
assert!(matches!(
DbMeta::decode(&b),
Err(DbError::FormatMismatch(_))
));
}
#[test]
fn reject_nonzero_reserved() {
let mut b = sample(Backend::Bitcask, 0).encode();
b[12] = 1;
assert!(matches!(
DbMeta::decode(&b),
Err(DbError::FormatMismatch(_))
));
}
#[test]
fn reject_fixedstore_nonzero_flags() {
let mut b = sample(Backend::FixedStore, 0).encode();
b[8] = 1;
assert!(matches!(
DbMeta::decode(&b),
Err(DbError::FormatMismatch(_))
));
}
#[test]
fn reject_bitcask_unknown_flag_bits() {
let mut b = sample(Backend::Bitcask, 0).encode();
b[8] = 0x02; assert!(matches!(
DbMeta::decode(&b),
Err(DbError::FormatMismatch(_))
));
}
}