Skip to main content

void_core/support/
cid.rs

1//! Content identifiers (CIDs) for void objects.
2//!
3//! Void uses a single canonical CID format for all content-addressed objects:
4//! - **Version**: CIDv1
5//! - **Codec**: raw (0x55)
6//! - **Multihash**: SHA-256 (0x12)
7//!
8//! This is intentionally restrictive. Void does not support CIDv0, dag-pb,
9//! dag-cbor, or other IPFS CID variants. When fetching from IPFS gateways,
10//! CIDs are validated to match this format before any content verification.
11//!
12//! The [`VoidCid`] newtype wraps `cid::Cid` and provides `Display`,
13//! eliminating the need for `to_string()` helper calls at every use site.
14
15use std::fmt;
16
17use cid::Cid;
18use multihash_codetable::{Code, MultihashDigest};
19
20use super::error::{Result, VoidError};
21
22/// Raw codec for CIDv1 (0x55).
23pub const CODEC_RAW: u64 = 0x55;
24
25/// SHA-256 multihash code (0x12).
26pub const MULTIHASH_SHA2_256: u64 = 0x12;
27
28// ---------------------------------------------------------------------------
29// VoidCid newtype
30// ---------------------------------------------------------------------------
31
32/// A content identifier for void objects.
33///
34/// Wraps [`cid::Cid`] with void-specific constructors, validation, and a
35/// `Display` implementation so CIDs can be used directly in format strings
36/// without calling a separate `to_string()` helper.
37///
38/// All void CIDs are CIDv1 with raw codec (0x55) and SHA-256 multihash.
39#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
40pub struct VoidCid(Cid);
41
42impl VoidCid {
43    /// Creates a CIDv1 from raw bytes using SHA-256.
44    pub fn create(data: &[u8]) -> Self {
45        let hash = Code::Sha2_256.digest(data);
46        Self(Cid::new_v1(CODEC_RAW, hash))
47    }
48
49    /// Parses a CID from its string representation.
50    pub fn parse(s: &str) -> Result<Self> {
51        s.parse::<Cid>()
52            .map(Self)
53            .map_err(|e| VoidError::InvalidCid(e.to_string()))
54    }
55
56    /// Parses a CID from raw bytes.
57    pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
58        Cid::try_from(bytes)
59            .map(Self)
60            .map_err(|e| VoidError::InvalidCid(e.to_string()))
61    }
62
63    /// Converts to the byte representation.
64    pub fn to_bytes(&self) -> Vec<u8> {
65        self.0.to_bytes()
66    }
67
68    /// Validates that this CID uses void's canonical format (CIDv1/raw/SHA-256).
69    pub fn validate(&self) -> Result<()> {
70        validate(&self.0)
71    }
72
73    /// Returns the inner `cid::Cid`.
74    pub fn inner(&self) -> &Cid {
75        &self.0
76    }
77
78    /// Returns the DHT key for this CID.
79    ///
80    /// Per IPFS convention, DHT provider records are keyed by the **multihash**
81    /// portion of the CID, not the full CID. This ensures the same content
82    /// stored with different CID versions (v0 vs v1) or codecs (raw vs dag-pb)
83    /// maps to the same DHT key.
84    pub fn dht_key(&self) -> &multihash::Multihash<64> {
85        self.0.hash()
86    }
87}
88
89impl fmt::Display for VoidCid {
90    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91        fmt::Display::fmt(&self.0, f)
92    }
93}
94
95impl fmt::Debug for VoidCid {
96    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97        write!(f, "VoidCid({})", self.0)
98    }
99}
100
101impl std::ops::Deref for VoidCid {
102    type Target = Cid;
103    fn deref(&self) -> &Cid {
104        &self.0
105    }
106}
107
108impl From<Cid> for VoidCid {
109    fn from(cid: Cid) -> Self {
110        Self(cid)
111    }
112}
113
114impl From<VoidCid> for Cid {
115    fn from(vcid: VoidCid) -> Cid {
116        vcid.0
117    }
118}
119
120impl PartialEq<Cid> for VoidCid {
121    fn eq(&self, other: &Cid) -> bool {
122        self.0 == *other
123    }
124}
125
126impl PartialEq<VoidCid> for Cid {
127    fn eq(&self, other: &VoidCid) -> bool {
128        *self == other.0
129    }
130}
131
132// ---------------------------------------------------------------------------
133// ToVoidCid extension trait for typed CID newtypes
134// ---------------------------------------------------------------------------
135
136/// Extension trait that adds `.to_void_cid()` and `.to_cid_string()` to typed
137/// CID newtypes from `void-crypto` (`CommitCid`, `MetadataCid`, `ShardCid`, etc.).
138///
139/// Replaces the verbose `cid::from_bytes(x.as_bytes())?` pattern.
140pub trait ToVoidCid {
141    /// Parse the serialized CID bytes into a `VoidCid`.
142    fn to_void_cid(&self) -> Result<VoidCid>;
143
144    /// Convert to a human-readable CID string (multibase-encoded).
145    ///
146    /// Convenience for `.to_void_cid()?.to_string()` at display boundaries.
147    fn to_cid_string(&self) -> String {
148        self.to_void_cid()
149            .map(|c| c.to_string())
150            .unwrap_or_else(|_| "<invalid-cid>".to_string())
151    }
152}
153
154macro_rules! impl_to_void_cid {
155    ($($ty:ty),+ $(,)?) => {
156        $(
157            impl ToVoidCid for $ty {
158                fn to_void_cid(&self) -> Result<VoidCid> {
159                    VoidCid::from_bytes(self.as_bytes())
160                }
161            }
162        )+
163    };
164}
165
166impl_to_void_cid!(
167    void_crypto::CommitCid,
168    void_crypto::MetadataCid,
169    void_crypto::ShardCid,
170    void_crypto::ManifestCid,
171    void_crypto::RepoManifestCid,
172);
173
174// ---------------------------------------------------------------------------
175// Free functions (kept for backward compatibility during migration)
176// ---------------------------------------------------------------------------
177
178/// Validates that a CID uses void's canonical format (CIDv1/raw/SHA-256).
179pub fn validate(cid: &Cid) -> Result<()> {
180    if cid.version() != cid::Version::V1 {
181        return Err(VoidError::InvalidCid(format!(
182            "unsupported CID version {:?}, void requires CIDv1",
183            cid.version()
184        )));
185    }
186    if cid.codec() != CODEC_RAW {
187        return Err(VoidError::InvalidCid(format!(
188            "unsupported codec 0x{:x}, void requires raw codec (0x55)",
189            cid.codec()
190        )));
191    }
192    if cid.hash().code() != MULTIHASH_SHA2_256 {
193        return Err(VoidError::InvalidCid(format!(
194            "unsupported hash algorithm 0x{:x}, void requires SHA-256 (0x12)",
195            cid.hash().code()
196        )));
197    }
198    Ok(())
199}
200
201/// Creates a CIDv1 from raw bytes using SHA-256.
202pub fn create(data: &[u8]) -> VoidCid {
203    VoidCid::create(data)
204}
205
206/// Parses a CID from its string representation.
207pub fn parse(s: &str) -> Result<VoidCid> {
208    VoidCid::parse(s)
209}
210
211/// Parses a CID from raw bytes.
212pub fn from_bytes(bytes: &[u8]) -> Result<VoidCid> {
213    VoidCid::from_bytes(bytes)
214}
215
216/// Converts a CID to its byte representation.
217pub fn to_bytes(cid: &Cid) -> Vec<u8> {
218    cid.to_bytes()
219}
220
221/// Converts a CID to its string representation.
222pub fn to_string(cid: &Cid) -> String {
223    cid.to_string()
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229
230    #[test]
231    fn create_deterministic() {
232        let cid1 = VoidCid::create(b"hello, void!");
233        let cid2 = VoidCid::create(b"hello, void!");
234        assert_eq!(cid1, cid2);
235    }
236
237    #[test]
238    fn create_different_data() {
239        let cid1 = VoidCid::create(b"hello");
240        let cid2 = VoidCid::create(b"world");
241        assert_ne!(cid1, cid2);
242    }
243
244    #[test]
245    fn roundtrip_string() {
246        let cid = VoidCid::create(b"test data");
247        let s = cid.to_string();
248        let parsed = VoidCid::parse(&s).unwrap();
249        assert_eq!(cid, parsed);
250    }
251
252    #[test]
253    fn roundtrip_bytes() {
254        let cid = VoidCid::create(b"test data");
255        let bytes = cid.to_bytes();
256        let parsed = VoidCid::from_bytes(&bytes).unwrap();
257        assert_eq!(cid, parsed);
258    }
259
260    #[test]
261    fn display_impl() {
262        let cid = VoidCid::create(b"test");
263        let displayed = format!("{cid}");
264        let to_stringed = cid.to_string();
265        assert_eq!(displayed, to_stringed);
266        assert!(!displayed.is_empty());
267    }
268
269    #[test]
270    fn cid_is_v1() {
271        let cid = VoidCid::create(b"test");
272        assert_eq!(cid.version(), cid::Version::V1);
273    }
274
275    #[test]
276    fn parse_invalid_fails() {
277        assert!(VoidCid::parse("not-a-valid-cid").is_err());
278    }
279
280    #[test]
281    fn validate_accepts_void_cid() {
282        let cid = VoidCid::create(b"test data");
283        assert!(cid.validate().is_ok());
284    }
285
286    #[test]
287    fn validate_rejects_cidv0() {
288        let cidv0 = "QmYwAPJzv5CZsnAzt8auVZRn4iiY5J5h6kWEriX4aSx1Dd";
289        let cid = VoidCid::parse(cidv0).unwrap();
290        let result = cid.validate();
291        assert!(result.is_err());
292        assert!(result.unwrap_err().to_string().contains("CIDv1"));
293    }
294
295    #[test]
296    fn validate_rejects_wrong_codec() {
297        let hash = Code::Sha2_256.digest(b"test");
298        let cid = VoidCid::from(Cid::new_v1(0x70, hash)); // dag-pb
299        let result = cid.validate();
300        assert!(result.is_err());
301        assert!(result.unwrap_err().to_string().contains("raw codec"));
302    }
303
304    #[test]
305    fn deref_to_inner_cid() {
306        let vcid = VoidCid::create(b"test");
307        let _inner: &Cid = &vcid; // Deref coercion
308        assert_eq!(vcid.codec(), CODEC_RAW);
309    }
310
311    #[test]
312    fn partial_eq_with_raw_cid() {
313        let vcid = VoidCid::create(b"test");
314        let raw: Cid = (*vcid).clone();
315        assert_eq!(vcid, raw);
316        assert_eq!(raw, vcid);
317    }
318}