armdb 0.1.12

sharded bitcask key-value storage optimized for NVMe
Documentation
/// Slot status: the slot has never been written.
#[allow(dead_code)]
pub const SLOT_FREE: u8 = 0x00;
/// Slot status: the slot contains a valid key-value pair.
pub const SLOT_OCCUPIED: u8 = 0x01;
/// Slot status: the slot was previously occupied but has been deleted.
pub const SLOT_DELETED: u8 = 0x02;

/// Size of the slot header: [status: 1] [_pad: 3] [crc32: 4] = 8 bytes.
pub const SLOT_HEADER_SIZE: usize = 8;

/// Compute the total slot size for the given key and value lengths,
/// aligned up to 8 bytes.
pub const fn slot_size(key_len: usize, value_len: usize) -> usize {
    let raw = SLOT_HEADER_SIZE + key_len + value_len;
    // align up to 8
    (raw + 7) & !7
}

/// Compute CRC32 over the concatenation of key and value bytes.
pub fn compute_crc(key: &[u8], value: &[u8]) -> u32 {
    let mut h = crc32fast::Hasher::new();
    h.update(key);
    h.update(value);
    h.finalize()
}

/// Serialize an occupied slot into `buf`.
///
/// Layout: `[OCCUPIED] [0; 3] [crc32 LE] [key] [value] [0-pad to 8-align]`
///
/// # Panics
///
/// Panics if `buf.len() < slot_size(key.len(), value.len())`.
pub fn serialize_slot(buf: &mut [u8], key: &[u8], value: &[u8]) {
    let size = slot_size(key.len(), value.len());
    assert!(
        buf.len() >= size,
        "buffer too small: {} < {}",
        buf.len(),
        size
    );

    // Zero the entire slot (covers padding bytes).
    buf[..size].fill(0);

    // Status byte.
    buf[0] = SLOT_OCCUPIED;

    // CRC32 of key + value, stored as little-endian at offset 4.
    let crc = compute_crc(key, value);
    buf[4..8].copy_from_slice(&crc.to_le_bytes());

    // Key.
    buf[SLOT_HEADER_SIZE..SLOT_HEADER_SIZE + key.len()].copy_from_slice(key);

    // Value.
    let val_off = SLOT_HEADER_SIZE + key.len();
    buf[val_off..val_off + value.len()].copy_from_slice(value);
}

/// Write a deleted-slot marker into `buf`.
///
/// Sets the status byte to `SLOT_DELETED` and zeros the rest.
///
/// # Panics
///
/// Panics if `buf` is empty.
pub fn serialize_deleted_slot(buf: &mut [u8]) {
    assert!(!buf.is_empty(), "buffer must not be empty");
    buf.fill(0);
    buf[0] = SLOT_DELETED;
}

/// Read the status byte from a slot buffer.
///
/// # Panics
///
/// Panics if `buf` is empty.
pub fn slot_status(buf: &[u8]) -> u8 {
    buf[0]
}

/// Validate an occupied slot: check that the status is `OCCUPIED` and the
/// CRC matches, then return `(key, value)` slices.
///
/// Returns `None` if the slot is not occupied or the CRC does not match.
pub fn validate_slot(buf: &[u8], key_len: usize, value_len: usize) -> Option<(&[u8], &[u8])> {
    let size = slot_size(key_len, value_len);
    if buf.len() < size {
        return None;
    }
    if buf[0] != SLOT_OCCUPIED {
        return None;
    }

    let stored_crc = u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]);

    let key = &buf[SLOT_HEADER_SIZE..SLOT_HEADER_SIZE + key_len];
    let val_off = SLOT_HEADER_SIZE + key_len;
    let value = &buf[val_off..val_off + value_len];

    let expected_crc = compute_crc(key, value);
    if stored_crc != expected_crc {
        return None;
    }

    Some((key, value))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_slot_size_alignment() {
        // Header alone is 8 bytes, already aligned.
        assert_eq!(slot_size(0, 0), 8);

        // 8 + 4 + 4 = 16 — already aligned.
        assert_eq!(slot_size(4, 4), 16);

        // 8 + 8 + 8 = 24 — already aligned.
        assert_eq!(slot_size(8, 8), 24);

        // 8 + 1 + 0 = 9 -> align to 16.
        assert_eq!(slot_size(1, 0), 16);

        // 8 + 3 + 2 = 13 -> align to 16.
        assert_eq!(slot_size(3, 2), 16);

        // 8 + 10 + 7 = 25 -> align to 32.
        assert_eq!(slot_size(10, 7), 32);

        // Every result is a multiple of 8.
        for k in 0..64 {
            for v in 0..64 {
                assert_eq!(slot_size(k, v) % 8, 0);
            }
        }
    }

    #[test]
    fn test_serialize_validate_roundtrip() {
        let key = b"hello";
        let value = b"world!!!";
        let size = slot_size(key.len(), value.len());
        let mut buf = vec![0u8; size];

        serialize_slot(&mut buf, key, value);

        assert_eq!(slot_status(&buf), SLOT_OCCUPIED);

        let (k, v) = validate_slot(&buf, key.len(), value.len()).expect("validation should pass");
        assert_eq!(k, key);
        assert_eq!(v, value);
    }

    #[test]
    fn test_corrupted_slot_fails_validation() {
        let key = b"abc";
        let value = b"defgh";
        let size = slot_size(key.len(), value.len());
        let mut buf = vec![0u8; size];

        serialize_slot(&mut buf, key, value);

        // Corrupt a value byte.
        buf[SLOT_HEADER_SIZE + key.len()] ^= 0xFF;

        assert!(
            validate_slot(&buf, key.len(), value.len()).is_none(),
            "corrupted slot should fail validation"
        );
    }

    #[test]
    fn test_free_slot_not_valid() {
        let size = slot_size(4, 4);
        let buf = vec![0u8; size];

        assert_eq!(slot_status(&buf), SLOT_FREE);
        assert!(
            validate_slot(&buf, 4, 4).is_none(),
            "free slot should not validate"
        );
    }

    #[test]
    fn test_deleted_slot() {
        let key = b"key1";
        let value = b"val1";
        let size = slot_size(key.len(), value.len());
        let mut buf = vec![0u8; size];

        // First write an occupied slot.
        serialize_slot(&mut buf, key, value);
        assert_eq!(slot_status(&buf), SLOT_OCCUPIED);

        // Mark as deleted.
        serialize_deleted_slot(&mut buf);
        assert_eq!(slot_status(&buf), SLOT_DELETED);

        // Validate should return None.
        assert!(
            validate_slot(&buf, key.len(), value.len()).is_none(),
            "deleted slot should not validate"
        );
    }
}