Skip to main content

kcode_kweb_db/
ids.rs

1use crate::{Error, Result};
2use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
3use ed25519_dalek::SigningKey;
4use sha2::{Digest, Sha256};
5use std::{fmt, str::FromStr};
6
7macro_rules! locator_type {
8    ($name:ident, $object:expr) => {
9        #[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
10        pub struct $name(pub(crate) [u8; 6]);
11
12        impl $name {
13            pub fn from_bytes(bytes: [u8; 6]) -> Result<Self> {
14                let id = Self(bytes);
15                if !id.valid_domain() {
16                    return Err(Error::invalid_input(concat!(
17                        stringify!($name),
18                        " has the wrong type domain"
19                    )));
20                }
21                Ok(id)
22            }
23
24            pub const fn to_bytes(self) -> [u8; 6] {
25                self.0
26            }
27
28            pub(crate) fn random() -> Self {
29                let mut bytes: [u8; 6] = rand::random();
30                if $object {
31                    bytes[0] |= 0x80;
32                } else {
33                    bytes[0] &= 0x7f;
34                }
35                Self(bytes)
36            }
37
38            pub(crate) fn valid_domain(self) -> bool {
39                (self.0[0] & 0x80 != 0) == $object
40            }
41        }
42
43        impl fmt::Debug for $name {
44            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
45                fmt::Display::fmt(self, formatter)
46            }
47        }
48
49        impl fmt::Display for $name {
50            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
51                formatter.write_str(&URL_SAFE_NO_PAD.encode(self.0))
52            }
53        }
54
55        impl FromStr for $name {
56            type Err = Error;
57
58            fn from_str(value: &str) -> Result<Self> {
59                if value.len() != 8
60                    || !value
61                        .bytes()
62                        .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-' || byte == b'_')
63                {
64                    return Err(Error::invalid_input(concat!(
65                        stringify!($name),
66                        " must be exactly eight URL-safe unpadded Base64 characters"
67                    )));
68                }
69                let bytes = URL_SAFE_NO_PAD
70                    .decode(value)
71                    .map_err(|_| Error::invalid_input("invalid locator Base64"))?;
72                let array: [u8; 6] = bytes
73                    .try_into()
74                    .map_err(|_| Error::invalid_input("locator has the wrong decoded length"))?;
75                let id = Self(array);
76                if !id.valid_domain() || id.to_string() != value {
77                    return Err(Error::invalid_input(concat!(
78                        stringify!($name),
79                        " has the wrong type domain or is not canonical"
80                    )));
81                }
82                Ok(id)
83            }
84        }
85    };
86}
87
88macro_rules! digest_type {
89    ($name:ident) => {
90        #[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
91        pub struct $name(pub(crate) [u8; 32]);
92
93        impl fmt::Debug for $name {
94            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
95                fmt::Display::fmt(self, formatter)
96            }
97        }
98
99        impl fmt::Display for $name {
100            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
101                formatter.write_str(&hex::encode(self.0))
102            }
103        }
104
105        impl FromStr for $name {
106            type Err = Error;
107
108            fn from_str(value: &str) -> Result<Self> {
109                if value.len() != 64
110                    || !value
111                        .bytes()
112                        .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
113                {
114                    return Err(Error::invalid_input(concat!(
115                        stringify!($name),
116                        " must be exact lowercase hex"
117                    )));
118                }
119                let array = hex::decode(value)
120                    .map_err(|_| Error::invalid_input("invalid digest hex"))?
121                    .try_into()
122                    .map_err(|_| Error::invalid_input("digest has the wrong length"))?;
123                Ok(Self(array))
124            }
125        }
126    };
127}
128
129locator_type!(NodeId, false);
130locator_type!(ObjectId, true);
131digest_type!(TransactionId);
132digest_type!(WriterId);
133
134impl TransactionId {
135    pub const fn from_bytes(bytes: [u8; 32]) -> Self {
136        Self(bytes)
137    }
138
139    pub const fn to_bytes(self) -> [u8; 32] {
140        self.0
141    }
142
143    pub(crate) fn for_signed_bytes(bytes: &[u8]) -> Self {
144        let mut hash = Sha256::new();
145        hash.update(b"kcode-kweb-db transaction v2\0");
146        hash.update(bytes);
147        Self(hash.finalize().into())
148    }
149}
150
151impl WriterId {
152    pub fn from_verifying_key(verifying_key: [u8; 32]) -> Result<Self> {
153        ed25519_dalek::VerifyingKey::from_bytes(&verifying_key)
154            .map_err(|_| Error::invalid_input("invalid Ed25519 verifying key"))?;
155        Ok(Self(verifying_key))
156    }
157
158    pub fn from_signing_key(signing_key: &[u8; 32]) -> Self {
159        let signing_key = SigningKey::from_bytes(signing_key);
160        Self(signing_key.verifying_key().to_bytes())
161    }
162
163    pub const fn to_bytes(self) -> [u8; 32] {
164        self.0
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    #[test]
173    fn locator_text_and_domains_are_canonical() {
174        let node = NodeId([0, 1, 2, 3, 4, 5]);
175        let object = ObjectId([128, 1, 2, 3, 4, 5]);
176        assert_eq!(node.to_string().len(), 8);
177        assert_eq!(object.to_string().len(), 8);
178        assert_eq!(node.to_string().parse::<NodeId>().unwrap(), node);
179        assert_eq!(object.to_string().parse::<ObjectId>().unwrap(), object);
180        assert!(object.to_string().parse::<NodeId>().is_err());
181        assert!(node.to_string().parse::<ObjectId>().is_err());
182    }
183}