Skip to main content

deaddrop_core/
ids.rs

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/// Cryptographic node identifier. Text form: `dd:` + 64 hex characters of the digest.
125#[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
142/// Daily rotating discovery identifier. Does not replace the cryptographic PeerId.
143pub 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}