kcode-kweb-db 1.0.4

A disk-first convergent signed-DAG store for Kweb nodes and objects
Documentation
use crate::{Error, Result};
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
use ed25519_dalek::SigningKey;
use sha2::{Digest, Sha256};
use std::{fmt, str::FromStr};

macro_rules! locator_type {
    ($name:ident, $object:expr) => {
        #[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
        pub struct $name(pub(crate) [u8; 6]);

        impl $name {
            pub fn from_bytes(bytes: [u8; 6]) -> Result<Self> {
                let id = Self(bytes);
                if !id.valid_domain() {
                    return Err(Error::invalid_input(concat!(
                        stringify!($name),
                        " has the wrong type domain"
                    )));
                }
                Ok(id)
            }

            pub const fn to_bytes(self) -> [u8; 6] {
                self.0
            }

            pub(crate) fn random() -> Self {
                let mut bytes: [u8; 6] = rand::random();
                if $object {
                    bytes[0] |= 0x80;
                } else {
                    bytes[0] &= 0x7f;
                }
                Self(bytes)
            }

            pub(crate) fn valid_domain(self) -> bool {
                (self.0[0] & 0x80 != 0) == $object
            }
        }

        impl fmt::Debug for $name {
            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
                fmt::Display::fmt(self, formatter)
            }
        }

        impl fmt::Display for $name {
            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
                formatter.write_str(&URL_SAFE_NO_PAD.encode(self.0))
            }
        }

        impl FromStr for $name {
            type Err = Error;

            fn from_str(value: &str) -> Result<Self> {
                if value.len() != 8
                    || !value
                        .bytes()
                        .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-' || byte == b'_')
                {
                    return Err(Error::invalid_input(concat!(
                        stringify!($name),
                        " must be exactly eight URL-safe unpadded Base64 characters"
                    )));
                }
                let bytes = URL_SAFE_NO_PAD
                    .decode(value)
                    .map_err(|_| Error::invalid_input("invalid locator Base64"))?;
                let array: [u8; 6] = bytes
                    .try_into()
                    .map_err(|_| Error::invalid_input("locator has the wrong decoded length"))?;
                let id = Self(array);
                if !id.valid_domain() || id.to_string() != value {
                    return Err(Error::invalid_input(concat!(
                        stringify!($name),
                        " has the wrong type domain or is not canonical"
                    )));
                }
                Ok(id)
            }
        }
    };
}

macro_rules! digest_type {
    ($name:ident) => {
        #[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
        pub struct $name(pub(crate) [u8; 32]);

        impl fmt::Debug for $name {
            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
                fmt::Display::fmt(self, formatter)
            }
        }

        impl fmt::Display for $name {
            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
                formatter.write_str(&hex::encode(self.0))
            }
        }

        impl FromStr for $name {
            type Err = Error;

            fn from_str(value: &str) -> Result<Self> {
                if value.len() != 64
                    || !value
                        .bytes()
                        .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
                {
                    return Err(Error::invalid_input(concat!(
                        stringify!($name),
                        " must be exact lowercase hex"
                    )));
                }
                let array = hex::decode(value)
                    .map_err(|_| Error::invalid_input("invalid digest hex"))?
                    .try_into()
                    .map_err(|_| Error::invalid_input("digest has the wrong length"))?;
                Ok(Self(array))
            }
        }
    };
}

locator_type!(NodeId, false);
locator_type!(ObjectId, true);
digest_type!(TransactionId);
digest_type!(WriterId);

impl TransactionId {
    pub const fn from_bytes(bytes: [u8; 32]) -> Self {
        Self(bytes)
    }

    pub const fn to_bytes(self) -> [u8; 32] {
        self.0
    }

    pub(crate) fn for_signed_bytes(bytes: &[u8]) -> Self {
        let mut hash = Sha256::new();
        hash.update(b"kcode-kweb-db transaction v2\0");
        hash.update(bytes);
        Self(hash.finalize().into())
    }
}

impl WriterId {
    pub fn from_verifying_key(verifying_key: [u8; 32]) -> Result<Self> {
        ed25519_dalek::VerifyingKey::from_bytes(&verifying_key)
            .map_err(|_| Error::invalid_input("invalid Ed25519 verifying key"))?;
        Ok(Self(verifying_key))
    }

    pub fn from_signing_key(signing_key: &[u8; 32]) -> Self {
        let signing_key = SigningKey::from_bytes(signing_key);
        Self(signing_key.verifying_key().to_bytes())
    }

    pub const fn to_bytes(self) -> [u8; 32] {
        self.0
    }
}

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

    #[test]
    fn locator_text_and_domains_are_canonical() {
        let node = NodeId([0, 1, 2, 3, 4, 5]);
        let object = ObjectId([128, 1, 2, 3, 4, 5]);
        assert_eq!(node.to_string().len(), 8);
        assert_eq!(object.to_string().len(), 8);
        assert_eq!(node.to_string().parse::<NodeId>().unwrap(), node);
        assert_eq!(object.to_string().parse::<ObjectId>().unwrap(), object);
        assert!(object.to_string().parse::<NodeId>().is_err());
        assert!(node.to_string().parse::<ObjectId>().is_err());
    }
}