kcode-kweb-db 0.1.0

A convergent signed-DAG store for Kweb nodes and objects
Documentation
use chrono::Utc;
use kcode_kweb_db::{Config, KwebDb, NodeData, NoopGossip, Owner, Provenance, WriterId};
use std::{fs, io::Write, sync::Arc};

fn provenance(label: &str) -> Provenance {
    Provenance {
        author: "Recovery".into(),
        source: label.into(),
        source_created_at: Utc::now(),
        data: String::new(),
    }
}

fn data(name: &str, objects: Vec<kcode_kweb_db::ObjectId>) -> NodeData {
    NodeData {
        short_name: name.into(),
        short_description: String::new(),
        long_description: String::new(),
        owner: Owner::SelfNode,
        fixed_connections: [None; 3],
        objects,
    }
}

fn config(key: [u8; 32]) -> Config {
    Config {
        signing_key: key,
        writers_by_priority: vec![WriterId::from_signing_key(&key)],
        gossip: Arc::new(NoopGossip),
    }
}

#[test]
fn restart_repairs_torn_tail_and_rebuilds_nodes() {
    let root = tempfile::tempdir().unwrap();
    let key = [31; 32];
    let database = KwebDb::open(root.path(), config(key)).unwrap();
    let mut transaction = database.start_transaction(provenance("create")).unwrap();
    let node = transaction
        .create_node(data("Durable node", Vec::new()))
        .unwrap();
    transaction.finalize().unwrap();
    drop(database);

    fs::remove_dir_all(root.path().join("nodes")).unwrap();
    let mut ledger = fs::OpenOptions::new()
        .append(true)
        .open(root.path().join("ledger.kwl"))
        .unwrap();
    ledger.write_all(&123_u64.to_be_bytes()[..5]).unwrap();
    ledger.sync_all().unwrap();
    drop(ledger);

    let reopened = KwebDb::open(root.path(), config(key)).unwrap();
    assert_eq!(
        reopened.get_node(node).unwrap().data.short_name,
        "Durable node"
    );
    assert!(root.path().join("nodes").is_dir());
}

#[test]
fn complete_ledger_corruption_is_rejected() {
    let root = tempfile::tempdir().unwrap();
    let key = [32; 32];
    let database = KwebDb::open(root.path(), config(key)).unwrap();
    let mut transaction = database.start_transaction(provenance("create")).unwrap();
    transaction
        .create_node(data("Corrupt me", Vec::new()))
        .unwrap();
    transaction.finalize().unwrap();
    drop(database);

    let path = root.path().join("ledger.kwl");
    let mut bytes = fs::read(&path).unwrap();
    let last = bytes.len() - 1;
    bytes[last] ^= 1;
    fs::write(path, bytes).unwrap();
    assert!(KwebDb::open(root.path(), config(key)).is_err());
}

#[test]
fn object_corruption_is_detected_on_read_and_restart() {
    let root = tempfile::tempdir().unwrap();
    let key = [33; 32];
    let database = KwebDb::open(root.path(), config(key)).unwrap();
    let mut transaction = database.start_transaction(provenance("object")).unwrap();
    let object = transaction.create_object(b"payload".to_vec()).unwrap();
    transaction
        .create_node(data("Object node", vec![object]))
        .unwrap();
    let transaction_id = transaction.finalize().unwrap();

    let text = object.to_string();
    let path = root
        .path()
        .join("objects")
        .join(&text[..2])
        .join(&text[2..])
        .join(format!("{transaction_id}.kwo"));
    let mut bytes = fs::read(&path).unwrap();
    let last = bytes.len() - 1;
    bytes[last] ^= 1;
    fs::write(path, bytes).unwrap();
    assert!(database.get_object(object).is_err());
    drop(database);
    assert!(KwebDb::open(root.path(), config(key)).is_err());
}

#[test]
fn second_opener_is_rejected_until_first_drops() {
    let root = tempfile::tempdir().unwrap();
    let key = [34; 32];
    let first = KwebDb::open(root.path(), config(key)).unwrap();
    assert!(KwebDb::open(root.path(), config(key)).is_err());
    drop(first);
    assert!(KwebDb::open(root.path(), config(key)).is_ok());
}