use cid::multibase::{decode as multibase_decode, Base};
use cid::{Cid, Version};
use multihash::Multihash;
use multihash_codetable::{Code, MultihashDigest};
use sha2::{Digest, Sha256};
use zeroize::Zeroizing;
use crate::{decode_dag_cbor, CborError};
pub const DAG_CBOR_CODEC: u64 = 0x71;
pub const MAX_CID_STRING_LEN: usize = 1024;
const CID_V0_STRING_LEN: usize = 46;
pub type ContentHash = [u8; 32];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DagCborMultihash(Multihash<64>);
impl DagCborMultihash {
pub fn code(&self) -> u64 {
self.0.code()
}
pub fn size(&self) -> u8 {
self.0.size()
}
pub fn digest(&self) -> &[u8] {
self.0.digest()
}
pub fn to_bytes(&self) -> Vec<u8> {
self.0.to_bytes()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedCid(Cid);
impl ParsedCid {
pub fn to_bytes(&self) -> Vec<u8> {
self.0.to_bytes()
}
}
impl core::fmt::Display for ParsedCid {
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
self.0.fmt(formatter)
}
}
pub fn sha2_256_content_hash(bytes: &[u8]) -> ContentHash {
Sha256::digest(bytes).into()
}
pub fn dag_cbor_multihash(bytes: &[u8]) -> DagCborMultihash {
DagCborMultihash(Code::Sha2_256.digest(bytes))
}
pub fn compute_cid_dag_cbor(bytes: &[u8]) -> String {
let hash = dag_cbor_multihash(bytes);
let cid = Cid::new_v1(DAG_CBOR_CODEC, hash.0);
cid.to_string()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CidVerificationStatus {
Match,
Mismatch,
NonCanonical,
InvalidCid,
}
#[must_use]
pub struct DagCborCidVerification {
status: CidVerificationStatus,
expected_cid: String,
actual_cid: String,
}
impl DagCborCidVerification {
pub const fn status(&self) -> CidVerificationStatus {
self.status
}
pub fn expected_cid(&self) -> &str {
self.expected_cid.as_str()
}
pub fn actual_cid(&self) -> &str {
self.actual_cid.as_str()
}
pub fn into_parts(self) -> (CidVerificationStatus, String, String) {
(self.status, self.expected_cid, self.actual_cid)
}
}
pub fn verify_dag_cbor_cid(
cid_str: &str,
bytes: &[u8],
) -> Result<DagCborCidVerification, CborError> {
let _validated = Zeroizing::new(decode_dag_cbor(bytes)?);
let expected_hash = dag_cbor_multihash(bytes);
let expected_cid = Cid::new_v1(DAG_CBOR_CODEC, expected_hash.0);
let expected = expected_cid.to_string();
let Some((actual_cid, _base)) = parse_cid_string(cid_str) else {
return Ok(DagCborCidVerification {
status: CidVerificationStatus::InvalidCid,
expected_cid: expected,
actual_cid: String::new(),
});
};
let actual = actual_cid.to_string();
let status = if cid_str != actual {
CidVerificationStatus::NonCanonical
} else if expected_cid == actual_cid {
CidVerificationStatus::Match
} else {
CidVerificationStatus::Mismatch
};
Ok(DagCborCidVerification {
status,
expected_cid: expected,
actual_cid: actual,
})
}
pub fn is_valid_cid_string(s: &str) -> bool {
try_parse_cid(s).is_some()
}
pub fn try_parse_cid(s: &str) -> Option<ParsedCid> {
parse_cid_string(s).map(|(cid, _base)| ParsedCid(cid))
}
fn parse_cid_string(s: &str) -> Option<(Cid, Option<Base>)> {
if s.len() > MAX_CID_STRING_LEN {
return None;
}
let (base, decoded) = if s.len() == CID_V0_STRING_LEN && s.starts_with("Qm") {
(None, Base::Base58Btc.decode(s).ok()?)
} else {
let (base, bytes) = multibase_decode(s).ok()?;
(Some(base), bytes)
};
let mut remaining = decoded.as_slice();
let cid = Cid::read_bytes(&mut remaining).ok()?;
if base.is_some() && cid.version() == Version::V0 {
return None;
}
if !remaining.is_empty() || cid.to_bytes() != decoded {
return None;
}
Some((cid, base))
}