armdb 0.6.1

sharded bitcask key-value storage optimized for NVMe
Documentation
//! Shared on-disk `db.meta` codec for both durability backends.
//!
//! Format (16 bytes):
//! ```text
//! [0..4]   magic "ARMD"
//! [4]      version (= 2)
//! [5]      backend: 0 = Bitcask, 1 = FixedStore
//! [6]      shard_count
//! [7]      shard_prefix_bits
//! [8]      flags: Bitcask bit0 = encrypted; FixedStore must be 0
//! [9..16]  reserved, must be zero
//! ```
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;
        // [9..16] reserved — already zero.
        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() {
        // Must stay byte-identical to the pre-refactor FixedStore layout so
        // existing FixedStore databases keep opening.
        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; // bit1 undefined
        assert!(matches!(
            DbMeta::decode(&b),
            Err(DbError::FormatMismatch(_))
        ));
    }
}