Skip to main content

codec_cbor/
cid.rs

1// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved
2//
3// SPDX-License-Identifier: Apache-2.0
4
5use cid::multibase::{decode as multibase_decode, Base};
6use cid::Cid;
7use multihash::Multihash;
8use multihash_codetable::{Code, MultihashDigest};
9use sha2::{Digest, Sha256};
10
11/// dag-cbor multicodec code (IPLD)
12pub const DAG_CBOR_CODEC: u64 = 0x71;
13
14/// Hash output for sha2-256
15pub type ContentHash = [u8; 32];
16
17/// Multihash envelope size used by the CID stack for sha2-256 digests.
18pub type DagCborMultihash = Multihash<64>;
19
20/// Returns the raw sha2-256 digest of `bytes`.
21pub fn sha2_256_content_hash(bytes: &[u8]) -> ContentHash {
22    Sha256::digest(bytes).into()
23}
24
25/// Returns a sha2-256 multihash of `bytes` for use in a CID.
26pub fn dag_cbor_multihash(bytes: &[u8]) -> DagCborMultihash {
27    Code::Sha2_256.digest(bytes)
28}
29
30/// Computes the CIDv1 (dag-cbor, sha2-256) of `bytes` in canonical
31/// base32-lower string form.
32pub fn compute_cid_dag_cbor(bytes: &[u8]) -> String {
33    let hash = dag_cbor_multihash(bytes);
34    let cid = Cid::new_v1(DAG_CBOR_CODEC, hash);
35    cid.to_string()
36}
37
38/// Recomputes the CID of `bytes` and compares it to `cid_str`.
39///
40/// Returns whether the parsed CID values match, plus the expected CID and the
41/// parsed actual CID in canonical string form. Invalid CID input never matches
42/// and returns an empty actual string so unvalidated caller input does not cross
43/// diagnostic or FFI boundaries.
44pub fn verify_dag_cbor_cid(cid_str: &str, bytes: &[u8]) -> (bool, String, String) {
45    let expected_hash = dag_cbor_multihash(bytes);
46    let expected_cid = Cid::new_v1(DAG_CBOR_CODEC, expected_hash);
47    let expected = expected_cid.to_string();
48    let Some(actual_cid) = parse_verification_cid(cid_str) else {
49        return (false, expected, String::new());
50    };
51    let actual = actual_cid.to_string();
52    (expected_cid == actual_cid, expected, actual)
53}
54
55fn parse_verification_cid(cid_str: &str) -> Option<Cid> {
56    let actual = Cid::try_from(cid_str).ok()?;
57    let Ok((base, _decoded)) = multibase_decode(cid_str) else {
58        return Some(actual);
59    };
60    if rejects_case_variant_base(base) || has_uppercase_payload_for_lowercase_base(base, cid_str) {
61        return None;
62    }
63    Some(actual)
64}
65
66fn rejects_case_variant_base(base: Base) -> bool {
67    matches!(
68        base,
69        Base::Base16Upper
70            | Base::Base32Upper
71            | Base::Base32PadUpper
72            | Base::Base32HexUpper
73            | Base::Base32HexPadUpper
74            | Base::Base36Upper
75    )
76}
77
78fn has_uppercase_payload_for_lowercase_base(base: Base, cid_str: &str) -> bool {
79    if !matches!(
80        base,
81        Base::Base16Lower
82            | Base::Base32Lower
83            | Base::Base32PadLower
84            | Base::Base32HexLower
85            | Base::Base32HexPadLower
86            | Base::Base36Lower
87    ) {
88        return false;
89    }
90    cid_str
91        .get(1..)
92        .is_some_and(|payload| payload.bytes().any(|byte| byte.is_ascii_uppercase()))
93}
94
95/// Returns whether `s` parses as a valid CID string.
96pub fn is_valid_cid_string(s: &str) -> bool {
97    Cid::try_from(s).is_ok()
98}
99
100/// Parses `s` as a CID, returning `None` if it is not a valid CID string.
101pub fn try_parse_cid(s: &str) -> Option<Cid> {
102    Cid::try_from(s).ok()
103}