1use cid::multibase::{decode as multibase_decode, Base};
6use cid::{Cid, Version};
7use multihash::Multihash;
8use multihash_codetable::{Code, MultihashDigest};
9use sha2::{Digest, Sha256};
10use zeroize::Zeroizing;
11
12use crate::{decode_dag_cbor, CborError};
13
14pub const DAG_CBOR_CODEC: u64 = 0x71;
16
17pub const MAX_CID_STRING_LEN: usize = 1024;
23
24const CID_V0_STRING_LEN: usize = 46;
25
26pub type ContentHash = [u8; 32];
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub struct DagCborMultihash(Multihash<64>);
35
36impl DagCborMultihash {
37 pub fn code(&self) -> u64 {
39 self.0.code()
40 }
41
42 pub fn size(&self) -> u8 {
44 self.0.size()
45 }
46
47 pub fn digest(&self) -> &[u8] {
49 self.0.digest()
50 }
51
52 pub fn to_bytes(&self) -> Vec<u8> {
54 self.0.to_bytes()
55 }
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct ParsedCid(Cid);
61
62impl ParsedCid {
63 pub fn to_bytes(&self) -> Vec<u8> {
65 self.0.to_bytes()
66 }
67}
68
69impl core::fmt::Display for ParsedCid {
70 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
71 self.0.fmt(formatter)
72 }
73}
74
75pub fn sha2_256_content_hash(bytes: &[u8]) -> ContentHash {
77 Sha256::digest(bytes).into()
78}
79
80pub fn dag_cbor_multihash(bytes: &[u8]) -> DagCborMultihash {
82 DagCborMultihash(Code::Sha2_256.digest(bytes))
83}
84
85pub fn compute_cid_dag_cbor(bytes: &[u8]) -> String {
91 let hash = dag_cbor_multihash(bytes);
92 let cid = Cid::new_v1(DAG_CBOR_CODEC, hash.0);
93 cid.to_string()
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98pub enum CidVerificationStatus {
99 Match,
101 Mismatch,
103 NonCanonical,
105 InvalidCid,
107}
108
109#[must_use]
111pub struct DagCborCidVerification {
112 status: CidVerificationStatus,
113 expected_cid: String,
114 actual_cid: String,
115}
116
117impl DagCborCidVerification {
118 pub const fn status(&self) -> CidVerificationStatus {
120 self.status
121 }
122
123 pub fn expected_cid(&self) -> &str {
125 self.expected_cid.as_str()
126 }
127
128 pub fn actual_cid(&self) -> &str {
130 self.actual_cid.as_str()
131 }
132
133 pub fn into_parts(self) -> (CidVerificationStatus, String, String) {
135 (self.status, self.expected_cid, self.actual_cid)
136 }
137}
138
139pub fn verify_dag_cbor_cid(
147 cid_str: &str,
148 bytes: &[u8],
149) -> Result<DagCborCidVerification, CborError> {
150 let _validated = Zeroizing::new(decode_dag_cbor(bytes)?);
151 let expected_hash = dag_cbor_multihash(bytes);
152 let expected_cid = Cid::new_v1(DAG_CBOR_CODEC, expected_hash.0);
153 let expected = expected_cid.to_string();
154 let Some((actual_cid, _base)) = parse_cid_string(cid_str) else {
155 return Ok(DagCborCidVerification {
156 status: CidVerificationStatus::InvalidCid,
157 expected_cid: expected,
158 actual_cid: String::new(),
159 });
160 };
161 let actual = actual_cid.to_string();
162 let status = if cid_str != actual {
163 CidVerificationStatus::NonCanonical
164 } else if expected_cid == actual_cid {
165 CidVerificationStatus::Match
166 } else {
167 CidVerificationStatus::Mismatch
168 };
169 Ok(DagCborCidVerification {
170 status,
171 expected_cid: expected,
172 actual_cid: actual,
173 })
174}
175
176pub fn is_valid_cid_string(s: &str) -> bool {
178 try_parse_cid(s).is_some()
179}
180
181pub fn try_parse_cid(s: &str) -> Option<ParsedCid> {
186 parse_cid_string(s).map(|(cid, _base)| ParsedCid(cid))
187}
188
189fn parse_cid_string(s: &str) -> Option<(Cid, Option<Base>)> {
190 if s.len() > MAX_CID_STRING_LEN {
191 return None;
192 }
193 let (base, decoded) = if s.len() == CID_V0_STRING_LEN && s.starts_with("Qm") {
196 (None, Base::Base58Btc.decode(s).ok()?)
197 } else {
198 let (base, bytes) = multibase_decode(s).ok()?;
199 (Some(base), bytes)
200 };
201 let mut remaining = decoded.as_slice();
202 let cid = Cid::read_bytes(&mut remaining).ok()?;
203 if base.is_some() && cid.version() == Version::V0 {
205 return None;
206 }
207 if !remaining.is_empty() || cid.to_bytes() != decoded {
210 return None;
211 }
212 Some((cid, base))
213}