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}