haematite 0.6.1

Content-addressed, branchable, actor-native storage engine
Documentation
use std::collections::BTreeMap;

use crate::branch::{BranchKind, BranchRefRecord, BranchShardRef};
use crate::tree::Hash;

use super::BrowserSourceError;

const RECORD_MAGIC: &[u8; 4] = b"HBL1";
const NODE_MAGIC: &[u8; 4] = b"HBN1";
pub(super) const STORE_FORMAT: &[u8] = b"HAEMATITE-BROWSER-LOCAL\n1\n";

pub(super) fn encode_record(record: &BranchRefRecord, generation: &str) -> Vec<u8> {
    let mut bytes = RECORD_MAGIC.to_vec();
    push_bytes(&mut bytes, generation.as_bytes());
    push_bytes(&mut bytes, record.name.as_bytes());
    push_u64(&mut bytes, record.created);
    push_u64(&mut bytes, record.seq);
    push_u64(&mut bytes, record.timestamp);
    push_u64(&mut bytes, usize_to_u64(record.shards.len()));
    for shard in &record.shards {
        push_u64(&mut bytes, usize_to_u64(shard.shard_id));
        bytes.extend_from_slice(shard.fork_anchor.as_bytes());
        bytes.extend_from_slice(shard.head.as_bytes());
    }
    push_u64(&mut bytes, usize_to_u64(record.parents.len()));
    for parent in &record.parents {
        bytes.extend_from_slice(parent.as_bytes());
    }
    push_u64(
        &mut bytes,
        match record.kind {
            BranchKind::Work => 0,
            BranchKind::Namespace => 1,
        },
    );
    match &record.namespace_lineage {
        Some(lineage) => {
            push_u64(&mut bytes, 1);
            push_bytes(&mut bytes, lineage.as_bytes());
        }
        None => push_u64(&mut bytes, 0),
    }
    bytes
}

pub(super) fn decode_record(bytes: &[u8]) -> Result<(String, BranchRefRecord), BrowserSourceError> {
    let mut reader = Reader::new(bytes);
    reader.magic(RECORD_MAGIC)?;
    let generation = reader.string()?;
    let name = reader.string()?;
    let created = reader.u64()?;
    let seq = reader.u64()?;
    let timestamp = reader.u64()?;
    let shard_count = reader.len()?;
    let mut shards = Vec::with_capacity(shard_count);
    for _ in 0..shard_count {
        shards.push(BranchShardRef {
            shard_id: reader.len()?,
            fork_anchor: reader.hash()?,
            head: reader.hash()?,
        });
    }
    let parent_count = reader.len()?;
    let mut parents = Vec::with_capacity(parent_count);
    for _ in 0..parent_count {
        parents.push(reader.hash()?);
    }
    let kind = match reader.u64()? {
        0 => BranchKind::Work,
        1 => BranchKind::Namespace,
        value => {
            return Err(BrowserSourceError::codec(format!(
                "unknown branch kind {value}"
            )));
        }
    };
    let namespace_lineage = match reader.u64()? {
        0 => None,
        1 => Some(reader.string()?),
        value => {
            return Err(BrowserSourceError::codec(format!(
                "unknown lineage tag {value}"
            )));
        }
    };
    reader.finish()?;
    Ok((
        generation,
        BranchRefRecord {
            name,
            created,
            kind,
            namespace_lineage,
            seq,
            timestamp,
            shards,
            parents,
        },
    ))
}

pub(super) fn encode_nodes(nodes: &BTreeMap<Hash, Vec<u8>>) -> Vec<u8> {
    let mut bytes = NODE_MAGIC.to_vec();
    push_u64(&mut bytes, usize_to_u64(nodes.len()));
    for (hash, node) in nodes {
        bytes.extend_from_slice(hash.as_bytes());
        push_bytes(&mut bytes, node);
    }
    bytes
}

pub(super) fn decode_nodes(bytes: &[u8]) -> Result<BTreeMap<Hash, Vec<u8>>, BrowserSourceError> {
    let mut reader = Reader::new(bytes);
    reader.magic(NODE_MAGIC)?;
    let count = reader.len()?;
    let mut nodes = BTreeMap::new();
    for _ in 0..count {
        let hash = reader.hash()?;
        if nodes.insert(hash, reader.bytes()?.to_vec()).is_some() {
            return Err(BrowserSourceError::codec("duplicate node hash"));
        }
    }
    reader.finish()?;
    Ok(nodes)
}

pub(super) fn record_file_name(name: &str) -> String {
    format!("{}.hbr", blake3::hash(name.as_bytes()).to_hex())
}

pub(super) fn generation_id(name: &str) -> String {
    format!(
        "{:016x}-{}",
        super::core::browser_timestamp(),
        blake3::hash(name.as_bytes()).to_hex()
    )
}

fn push_u64(bytes: &mut Vec<u8>, value: u64) {
    bytes.extend_from_slice(&value.to_le_bytes());
}
fn push_bytes(bytes: &mut Vec<u8>, value: &[u8]) {
    push_u64(bytes, usize_to_u64(value.len()));
    bytes.extend_from_slice(value);
}
fn usize_to_u64(value: usize) -> u64 {
    u64::try_from(value).unwrap_or(u64::MAX)
}

struct Reader<'a> {
    bytes: &'a [u8],
    offset: usize,
}
impl<'a> Reader<'a> {
    const fn new(bytes: &'a [u8]) -> Self {
        Self { bytes, offset: 0 }
    }
    fn take(&mut self, count: usize) -> Result<&'a [u8], BrowserSourceError> {
        let end = self
            .offset
            .checked_add(count)
            .ok_or("record length overflow")?;
        let value = self.bytes.get(self.offset..end).ok_or("truncated record")?;
        self.offset = end;
        Ok(value)
    }
    fn magic(&mut self, expected: &[u8]) -> Result<(), BrowserSourceError> {
        if self.take(expected.len())? == expected {
            Ok(())
        } else {
            Err(BrowserSourceError::codec("record magic mismatch"))
        }
    }
    fn u64(&mut self) -> Result<u64, BrowserSourceError> {
        let raw: [u8; 8] = self.take(8)?.try_into().map_err(|_| "truncated integer")?;
        Ok(u64::from_le_bytes(raw))
    }
    fn len(&mut self) -> Result<usize, BrowserSourceError> {
        usize::try_from(self.u64()?).map_err(|_| BrowserSourceError::codec("length exceeds usize"))
    }
    fn bytes(&mut self) -> Result<&'a [u8], BrowserSourceError> {
        let len = self.len()?;
        self.take(len)
    }
    fn string(&mut self) -> Result<String, BrowserSourceError> {
        String::from_utf8(self.bytes()?.to_vec())
            .map_err(|error| BrowserSourceError::codec(error.to_string()))
    }
    fn hash(&mut self) -> Result<Hash, BrowserSourceError> {
        let raw: [u8; 32] = self.take(32)?.try_into().map_err(|_| "truncated hash")?;
        Ok(Hash::from_bytes(raw))
    }
    fn finish(self) -> Result<(), BrowserSourceError> {
        if self.offset == self.bytes.len() {
            Ok(())
        } else {
            Err(BrowserSourceError::codec("trailing record bytes"))
        }
    }
}