armdb 0.1.13

sharded bitcask key-value storage optimized for NVMe
Documentation
#![cfg(feature = "encryption")]

#[cfg(feature = "var-collections")]
use armdb::VarTree;
use armdb::{Config, ConstTree, DbError};
use tempfile::tempdir;

fn encrypted_config(key: [u8; 32]) -> Config {
    let mut config = Config::test();
    config.encryption_key = Some(key);
    config
}

#[test]
fn test_encrypted_const_tree_basic() {
    let dir = tempdir().unwrap();
    let key = [0x42u8; 32];
    let tree = ConstTree::<[u8; 8], 8>::open(dir.path(), encrypted_config(key)).unwrap();

    // Basic CRUD
    for i in 0..100u64 {
        let k = i.to_be_bytes();
        let v = (i * 10).to_be_bytes();
        tree.put(&k, &v).unwrap();
    }

    for i in 0..100u64 {
        let k = i.to_be_bytes();
        let expected = (i * 10).to_be_bytes();
        assert_eq!(tree.get(&k).unwrap(), expected);
    }

    // Delete
    let k = 50u64.to_be_bytes();
    tree.delete(&k).unwrap();
    assert!(tree.get(&k).is_none());
    assert_eq!(tree.len(), 99);
}

#[test]
fn test_encrypted_recovery() {
    let dir = tempdir().unwrap();
    let key = [0x42u8; 32];

    {
        let tree = ConstTree::<[u8; 8], 8>::open(dir.path(), encrypted_config(key)).unwrap();

        for i in 0..100u64 {
            let k = i.to_be_bytes();
            let v = (i * 10).to_be_bytes();
            tree.put(&k, &v).unwrap();
        }

        tree.close().unwrap();
    }

    // Reopen with same key
    {
        let tree = ConstTree::<[u8; 8], 8>::open(dir.path(), encrypted_config(key)).unwrap();

        assert_eq!(tree.len(), 100);

        for i in 0..100u64 {
            let k = i.to_be_bytes();
            let expected = (i * 10).to_be_bytes();
            assert_eq!(
                tree.get(&k).unwrap(),
                expected,
                "encrypted recovery mismatch at key {}",
                i
            );
        }
    }
}

#[test]
fn test_encrypted_wrong_key_fails() {
    let dir = tempdir().unwrap();
    let key_a = [0x42u8; 32];
    let key_b = [0x99u8; 32];

    {
        let tree = ConstTree::<[u8; 8], 8>::open(dir.path(), encrypted_config(key_a)).unwrap();

        for i in 0..50u64 {
            let k = i.to_be_bytes();
            let v = i.to_be_bytes();
            tree.put(&k, &v).unwrap();
        }

        tree.close().unwrap();
    }

    // Reopen with different key — should fail during recovery
    let result = ConstTree::<[u8; 8], 8>::open(dir.path(), encrypted_config(key_b));
    assert!(result.is_err(), "opening with wrong key should fail");
}

#[test]
fn test_encrypted_no_key_fails() {
    let dir = tempdir().unwrap();
    let key = [0x42u8; 32];

    {
        let tree = ConstTree::<[u8; 8], 8>::open(dir.path(), encrypted_config(key)).unwrap();
        tree.put(&1u64.to_be_bytes(), &1u64.to_be_bytes()).unwrap();
        tree.close().unwrap();
    }

    // Reopen without encryption key — should fail
    let result = ConstTree::<[u8; 8], 8>::open(dir.path(), Config::test());
    assert!(
        matches!(&result, Err(DbError::Config(_))),
        "opening encrypted db without key should return Config error, got {:?}",
        result.as_ref().err()
    );
}

#[test]
fn test_unencrypted_with_key_fails() {
    let dir = tempdir().unwrap();

    {
        let tree = ConstTree::<[u8; 8], 8>::open(dir.path(), Config::test()).unwrap();
        tree.put(&1u64.to_be_bytes(), &1u64.to_be_bytes()).unwrap();
        tree.close().unwrap();
    }

    // Reopen with encryption key on unencrypted db — should fail
    let key = [0x42u8; 32];
    let result = ConstTree::<[u8; 8], 8>::open(dir.path(), encrypted_config(key));
    assert!(
        matches!(&result, Err(DbError::Config(_))),
        "opening unencrypted db with key should return Config error, got {:?}",
        result.as_ref().err()
    );
}

#[test]
fn test_encrypted_compaction() {
    let dir = tempdir().unwrap();
    let key = [0x42u8; 32];
    let mut config = encrypted_config(key);
    config.max_file_size = 4096;
    config.compaction_threshold = 0.1;

    let tree = ConstTree::<[u8; 8], 8>::open(dir.path(), config).unwrap();

    // Write and overwrite to create dead bytes
    for i in 0..500u64 {
        let k = i.to_be_bytes();
        let v = i.to_be_bytes();
        tree.put(&k, &v).unwrap();
    }
    for i in 0..250u64 {
        let k = i.to_be_bytes();
        let v = (i + 1000).to_be_bytes();
        tree.put(&k, &v).unwrap();
    }

    tree.compact().unwrap();

    // Verify all data after compaction
    for i in 0..500u64 {
        let k = i.to_be_bytes();
        let expected = if i < 250 {
            (i + 1000).to_be_bytes()
        } else {
            i.to_be_bytes()
        };
        assert_eq!(
            tree.get(&k).unwrap(),
            expected,
            "encrypted compaction mismatch at key {}",
            i
        );
    }
}

#[cfg(feature = "var-collections")]
#[test]
fn test_encrypted_var_tree() {
    let dir = tempdir().unwrap();
    let key = [0x42u8; 32];
    let tree = VarTree::<[u8; 8]>::open(dir.path(), encrypted_config(key)).unwrap();

    for i in 0..100u64 {
        let k = i.to_be_bytes();
        let v = format!("encrypted_val_{i}");
        tree.put(&k, v.as_bytes()).unwrap();
    }

    for i in 0..100u64 {
        let k = i.to_be_bytes();
        let expected = format!("encrypted_val_{i}");
        assert_eq!(tree.get(&k).unwrap().as_ref(), expected.as_bytes());
    }

    // Delete and verify
    tree.delete(&50u64.to_be_bytes()).unwrap();
    assert!(tree.get(&50u64.to_be_bytes()).is_none());
    assert_eq!(tree.len(), 99);
}