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
79impl fmt::Display for VoidCid {
80    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81        fmt::Display::fmt(&self.0, f)
82    }
83}
84
85impl fmt::Debug for VoidCid {
86    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87        write!(f, "VoidCid({})", self.0)
88    }
89}
90
91impl std::ops::Deref for VoidCid {
92    type Target = Cid;
93    fn deref(&self) -> &Cid {
94        &self.0
95    }
96}
97
98impl From<Cid> for VoidCid {
99    fn from(cid: Cid) -> Self {
100        Self(cid)
101    }
102}
103
104impl From<VoidCid> for Cid {
105    fn from(vcid: VoidCid) -> Cid {
106        vcid.0
107    }
108}
109
110impl PartialEq<Cid> for VoidCid {
111    fn eq(&self, other: &Cid) -> bool {
112        self.0 == *other
113    }
114}
115
116impl PartialEq<VoidCid> for Cid {
117    fn eq(&self, other: &VoidCid) -> bool {
118        *self == other.0
119    }
120}
121
122// ---------------------------------------------------------------------------
123// ToVoidCid extension trait for typed CID newtypes
124// ---------------------------------------------------------------------------
125
126/// Extension trait that adds `.to_void_cid()` and `.to_cid_string()` to typed
127/// CID newtypes from `void-crypto` (`CommitCid`, `MetadataCid`, `ShardCid`, etc.).
128///
129/// Replaces the verbose `cid::from_bytes(x.as_bytes())?` pattern.
130pub trait ToVoidCid {
131    /// Parse the serialized CID bytes into a `VoidCid`.
132    fn to_void_cid(&self) -> Result<VoidCid>;
133
134    /// Convert to a human-readable CID string (multibase-encoded).
135    ///
136    /// Convenience for `.to_void_cid()?.to_string()` at display boundaries.
137    fn to_cid_string(&self) -> String {
138        self.to_void_cid()
139            .map(|c| c.to_string())
140            .unwrap_or_else(|_| "<invalid-cid>".to_string())
141    }
142}
143
144macro_rules! impl_to_void_cid {
145    ($($ty:ty),+ $(,)?) => {
146        $(
147            impl ToVoidCid for $ty {
148                fn to_void_cid(&self) -> Result<VoidCid> {
149                    VoidCid::from_bytes(self.as_bytes())
150                }
151            }
152        )+
153    };
154}
155
156impl_to_void_cid!(
157    void_crypto::CommitCid,
158    void_crypto::MetadataCid,
159    void_crypto::ShardCid,
160    void_crypto::ManifestCid,
161    void_crypto::RepoManifestCid,
162);
163
164// ---------------------------------------------------------------------------
165// Free functions (kept for backward compatibility during migration)
166// ---------------------------------------------------------------------------
167
168/// Validates that a CID uses void's canonical format (CIDv1/raw/SHA-256).
169pub fn validate(cid: &Cid) -> Result<()> {
170    if cid.version() != cid::Version::V1 {
171        return Err(VoidError::InvalidCid(format!(
172            "unsupported CID version {:?}, void requires CIDv1",
173            cid.version()
174        )));
175    }
176    if cid.codec() != CODEC_RAW {
177        return Err(VoidError::InvalidCid(format!(
178            "unsupported codec 0x{:x}, void requires raw codec (0x55)",
179            cid.codec()
180        )));
181    }
182    if cid.hash().code() != MULTIHASH_SHA2_256 {
183        return Err(VoidError::InvalidCid(format!(
184            "unsupported hash algorithm 0x{:x}, void requires SHA-256 (0x12)",
185            cid.hash().code()
186        )));
187    }
188    Ok(())
189}
190
191/// Creates a CIDv1 from raw bytes using SHA-256.
192pub fn create(data: &[u8]) -> VoidCid {
193    VoidCid::create(data)
194}
195
196/// Parses a CID from its string representation.
197pub fn parse(s: &str) -> Result<VoidCid> {
198    VoidCid::parse(s)
199}
200
201/// Parses a CID from raw bytes.
202pub fn from_bytes(bytes: &[u8]) -> Result<VoidCid> {
203    VoidCid::from_bytes(bytes)
204}
205
206/// Converts a CID to its byte representation.
207pub fn to_bytes(cid: &Cid) -> Vec<u8> {
208    cid.to_bytes()
209}
210
211/// Converts a CID to its string representation.
212pub fn to_string(cid: &Cid) -> String {
213    cid.to_string()
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    #[test]
221    fn create_deterministic() {
222        let cid1 = VoidCid::create(b"hello, void!");
223        let cid2 = VoidCid::create(b"hello, void!");
224        assert_eq!(cid1, cid2);
225    }
226
227    #[test]
228    fn create_different_data() {
229        let cid1 = VoidCid::create(b"hello");
230        let cid2 = VoidCid::create(b"world");
231        assert_ne!(cid1, cid2);
232    }
233
234    #[test]
235    fn roundtrip_string() {
236        let cid = VoidCid::create(b"test data");
237        let s = cid.to_string();
238        let parsed = VoidCid::parse(&s).unwrap();
239        assert_eq!(cid, parsed);
240    }
241
242    #[test]
243    fn roundtrip_bytes() {
244        let cid = VoidCid::create(b"test data");
245        let bytes = cid.to_bytes();
246        let parsed = VoidCid::from_bytes(&bytes).unwrap();
247        assert_eq!(cid, parsed);
248    }
249
250    #[test]
251    fn display_impl() {
252        let cid = VoidCid::create(b"test");
253        let displayed = format!("{cid}");
254        let to_stringed = cid.to_string();
255        assert_eq!(displayed, to_stringed);
256        assert!(!displayed.is_empty());
257    }
258
259    #[test]
260    fn cid_is_v1() {
261        let cid = VoidCid::create(b"test");
262        assert_eq!(cid.version(), cid::Version::V1);
263    }
264
265    #[test]
266    fn parse_invalid_fails() {
267        assert!(VoidCid::parse("not-a-valid-cid").is_err());
268    }
269
270    #[test]
271    fn validate_accepts_void_cid() {
272        let cid = VoidCid::create(b"test data");
273        assert!(cid.validate().is_ok());
274    }
275
276    #[test]
277    fn validate_rejects_cidv0() {
278        let cidv0 = "QmYwAPJzv5CZsnAzt8auVZRn4iiY5J5h6kWEriX4aSx1Dd";
279        let cid = VoidCid::parse(cidv0).unwrap();
280        let result = cid.validate();
281        assert!(result.is_err());
282        assert!(result.unwrap_err().to_string().contains("CIDv1"));
283    }
284
285    #[test]
286    fn validate_rejects_wrong_codec() {
287        let hash = Code::Sha2_256.digest(b"test");
288        let cid = VoidCid::from(Cid::new_v1(0x70, hash)); // dag-pb
289        let result = cid.validate();
290        assert!(result.is_err());
291        assert!(result.unwrap_err().to_string().contains("raw codec"));
292    }
293
294    #[test]
295    fn deref_to_inner_cid() {
296        let vcid = VoidCid::create(b"test");
297        let _inner: &Cid = &vcid; // Deref coercion
298        assert_eq!(vcid.codec(), CODEC_RAW);
299    }
300
301    #[test]
302    fn partial_eq_with_raw_cid() {
303        let vcid = VoidCid::create(b"test");
304        let raw: Cid = (*vcid).clone();
305        assert_eq!(vcid, raw);
306        assert_eq!(raw, vcid);
307    }
308}