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());
}
}