1use crate::Result;
2use crate::error::DdError;
3use serde::{Deserialize, Serialize};
4use std::fmt;
5use std::str::FromStr;
6
7pub const DIGEST_LEN: usize = 32;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
10#[serde(rename_all = "snake_case")]
11pub enum HashAlgorithm {
12 #[default]
13 Blake3,
14 Sha256,
15}
16
17impl HashAlgorithm {
18 pub fn code(self) -> &'static str {
19 match self {
20 Self::Blake3 => "b3",
21 Self::Sha256 => "sha256",
22 }
23 }
24
25 pub fn parse_code(s: &str) -> Result<Self> {
26 match s {
27 "b3" | "blake3" => Ok(Self::Blake3),
28 "sha256" => Ok(Self::Sha256),
29 _ => Err(DdError::protocol(
30 crate::error::ErrorCode::Ddp1004UnsupportedHash,
31 format!("unknown hash algorithm {s}"),
32 )),
33 }
34 }
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
38pub struct Digest(#[serde(with = "serde_bytes")] pub [u8; DIGEST_LEN]);
39
40impl Digest {
41 pub fn as_bytes(&self) -> &[u8; DIGEST_LEN] {
42 &self.0
43 }
44}
45
46fn parse_prefixed(s: &str, expected: &str) -> Result<(HashAlgorithm, Digest)> {
47 let mut parts = s.splitn(3, ':');
48 let prefix = parts.next().unwrap_or("");
49 let alg = parts.next().unwrap_or("");
50 let hex = parts.next().unwrap_or("");
51 if prefix != expected {
52 return Err(DdError::protocol(
53 crate::error::ErrorCode::Ddp1005BadIdentifier,
54 format!("expected {expected}:…, got {s}"),
55 ));
56 }
57 let algorithm = HashAlgorithm::parse_code(alg)?;
58 let bytes = crate::hexutil::hex_decode(hex)?;
59 if bytes.len() != DIGEST_LEN {
60 return Err(DdError::protocol(
61 crate::error::ErrorCode::Ddp1005BadIdentifier,
62 "digest must be 32 bytes",
63 ));
64 }
65 let mut d = [0u8; DIGEST_LEN];
66 d.copy_from_slice(&bytes);
67 Ok((algorithm, Digest(d)))
68}
69
70fn format_prefixed(prefix: &str, alg: HashAlgorithm, digest: &Digest) -> String {
71 format!(
72 "{prefix}:{}:{}",
73 alg.code(),
74 crate::hexutil::hex_encode(&digest.0)
75 )
76}
77
78macro_rules! typed_id {
79 ($name:ident, $prefix:expr) => {
80 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
81 pub struct $name {
82 pub algorithm: HashAlgorithm,
83 pub digest: Digest,
84 }
85
86 impl $name {
87 pub fn new(algorithm: HashAlgorithm, digest: Digest) -> Self {
88 Self { algorithm, digest }
89 }
90
91 pub fn blake3(raw: [u8; DIGEST_LEN]) -> Self {
92 Self {
93 algorithm: HashAlgorithm::Blake3,
94 digest: Digest(raw),
95 }
96 }
97
98 pub fn as_bytes(&self) -> &[u8; DIGEST_LEN] {
99 self.digest.as_bytes()
100 }
101 }
102
103 impl fmt::Display for $name {
104 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105 f.write_str(&format_prefixed($prefix, self.algorithm, &self.digest))
106 }
107 }
108
109 impl FromStr for $name {
110 type Err = DdError;
111 fn from_str(s: &str) -> Result<Self> {
112 let (algorithm, digest) = parse_prefixed(s, $prefix)?;
113 Ok(Self { algorithm, digest })
114 }
115 }
116 };
117}
118
119typed_id!(ContentId, "ddc");
120typed_id!(ObjectId, "ddo");
121typed_id!(ChunkId, "ddk");
122typed_id!(ManifestId, "ddm");
123
124#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
126pub struct PeerId(#[serde(with = "serde_bytes")] pub [u8; DIGEST_LEN]);
127
128impl PeerId {
129 pub fn from_digest(d: [u8; DIGEST_LEN]) -> Self {
130 Self(d)
131 }
132
133 pub fn as_bytes(&self) -> &[u8; DIGEST_LEN] {
134 &self.0
135 }
136
137 pub fn short(&self) -> String {
138 format!("dd:{}…", crate::hexutil::hex_encode(&self.0[..4]))
139 }
140}
141
142pub fn ephemeral_discovery_id(peer: PeerId, now: u64) -> PeerId {
144 let day = now / 86400;
145 let mut hasher = blake3::Hasher::new();
146 hasher.update(b"ddp-eph-v1");
147 hasher.update(peer.as_bytes());
148 hasher.update(&day.to_be_bytes());
149 PeerId::from_digest(*hasher.finalize().as_bytes())
150}
151
152impl fmt::Display for PeerId {
153 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154 write!(f, "dd:{}", crate::hexutil::hex_encode(&self.0))
155 }
156}
157
158impl FromStr for PeerId {
159 type Err = DdError;
160 fn from_str(s: &str) -> Result<Self> {
161 let rest = s.strip_prefix("dd:").ok_or_else(|| {
162 DdError::protocol(
163 crate::error::ErrorCode::Ddp1005BadIdentifier,
164 "peer id must start with dd:",
165 )
166 })?;
167 if rest.len() != 64 {
168 return Err(DdError::protocol(
169 crate::error::ErrorCode::Ddp1005BadIdentifier,
170 "peer id must be dd: plus 64 hex characters",
171 ));
172 }
173 let bytes = crate::hexutil::hex_decode(rest)?;
174 let mut id = [0u8; DIGEST_LEN];
175 id.copy_from_slice(&bytes);
176 Ok(Self(id))
177 }
178}
179
180pub fn peer_id_matches_prefix(id: &PeerId, prefix_hex: &str) -> bool {
181 let full = crate::hexutil::hex_encode(&id.0);
182 full.starts_with(&prefix_hex.to_ascii_lowercase())
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 #[test]
190 fn typed_ids_roundtrip() {
191 let raw = [0xab; 32];
192 let c = ContentId::blake3(raw);
193 let s = c.to_string();
194 assert!(s.starts_with("ddc:b3:"));
195 assert_eq!(s.parse::<ContentId>().unwrap(), c);
196 let p = PeerId::from_digest(raw);
197 assert_eq!(p.to_string().parse::<PeerId>().unwrap(), p);
198 }
199}