common_primitives/
cid.rs

1#[cfg(test)]
2use cid::multibase::Base;
3use cid::{multibase, Cid};
4#[cfg(test)]
5use frame_support::assert_ok;
6use frame_support::ensure;
7use sp_io::hashing::sha2_256;
8use sp_runtime::Vec;
9
10/// Multihash type for wrapping digests (support up to 64-byte digests)
11pub type Multihash = cid::multihash::Multihash<64>;
12
13/// SHA2-256 multihash code
14const SHA2_256: u64 = 0x12;
15/// BLAKE3 multihash code
16const BLAKE3: u64 = 0x1e;
17
18/// List of hash algorithms supported by DSNP
19const DSNP_HASH_ALGORITHMS: &[u64] = &[SHA2_256, BLAKE3];
20
21/// Raw codec for CIDv1 (0x55)
22const RAW: u64 = 0x55;
23
24/// Error enum for CID validation
25#[derive(Debug, PartialEq)]
26pub enum CidError {
27	/// Unsupported CID version
28	UnsupportedCidVersion,
29	/// Unsupported CID hash algorithm
30	UnsupportedCidMultihash,
31	/// Multibase decoding error
32	MultibaseDecodeError,
33	/// UTF-8 decoding error
34	Utf8DecodeError,
35	/// CID parsing error
36	InvalidCid,
37}
38
39/// Computes a CIDv1 (RAW + SHA2-256 multihash)
40pub fn compute_cid_v1(bytes: &[u8]) -> Option<Vec<u8>> {
41	let digest = sha2_256(bytes);
42	let mh = Multihash::wrap(SHA2_256, &digest).ok()?;
43	let cid = Cid::new_v1(RAW, mh);
44	Some(cid.to_bytes())
45}
46
47/// Validates a CID to conform to IPFS CIDv1 (or higher) formatting and allowed multihashes (does not validate decoded CID fields)
48pub fn validate_cid(in_cid: &[u8]) -> Result<Vec<u8>, CidError> {
49	// Decode SCALE encoded CID into string slice
50	let cid_str: &str = core::str::from_utf8(in_cid).map_err(|_| CidError::Utf8DecodeError)?;
51	ensure!(cid_str.len() > 2, CidError::InvalidCid);
52	// starts_with handles Unicode multibyte characters safely
53	ensure!(!cid_str.starts_with("Qm"), CidError::UnsupportedCidVersion);
54
55	// Assume it's a multibase-encoded string. Decode it to a byte array so we can parse the CID.
56	let cid_b = multibase::decode(cid_str).map_err(|_| CidError::MultibaseDecodeError)?.1;
57	let cid = Cid::read_bytes(&cid_b[..]).map_err(|_| CidError::InvalidCid)?;
58	ensure!(DSNP_HASH_ALGORITHMS.contains(&cid.hash().code()), CidError::UnsupportedCidMultihash);
59
60	Ok(cid_b)
61}
62
63#[cfg(test)]
64const DUMMY_CID_SHA512: &str = "bafkrgqb76pscorjihsk77zpyst3p364zlti6aojlu4nga34vhp7t5orzwbwwytvp7ej44r5yhjzneanqwb5arcnvuvfwo2d4qgzyx5hymvto4";
65#[cfg(test)]
66const DUMMY_CID_SHA256: &str = "bagaaierasords4njcts6vs7qvdjfcvgnume4hqohf65zsfguprqphs3icwea";
67#[cfg(test)]
68const DUMMY_CID_BLAKE3: &str = "bafkr4ihn4xalcdzoyslzy2nvf5q6il7vwqjvdhhatpqpctijrxh6l5xzru";
69
70#[test]
71fn validate_cid_invalid_utf8_errors() {
72	let bad_cid = vec![0xfc, 0xa1, 0xa1, 0xa1, 0xa1, 0xa1];
73	assert_eq!(
74		validate_cid(&bad_cid).expect_err("Expected Utf8DecodeError"),
75		CidError::Utf8DecodeError
76	);
77}
78
79#[test]
80fn validate_cid_too_short_errors() {
81	let bad_cid = "a".as_bytes().to_vec();
82	assert_eq!(validate_cid(&bad_cid).expect_err("Expected InvalidCid"), CidError::InvalidCid);
83}
84
85#[test]
86fn validate_cid_v0_errors() {
87	let bad_cid = "Qmxxx".as_bytes().to_vec();
88	assert_eq!(
89		validate_cid(&bad_cid).expect_err("Expected UnsupportedCidVersion"),
90		CidError::UnsupportedCidVersion
91	);
92}
93
94#[test]
95fn validate_cid_invalid_multibase_errors() {
96	let bad_cid = "aaaa".as_bytes().to_vec();
97	assert_eq!(
98		validate_cid(&bad_cid).expect_err("Expected MultibaseDecodeError"),
99		CidError::MultibaseDecodeError
100	);
101}
102
103#[test]
104fn validate_cid_invalid_cid_errors() {
105	let bad_cid = multibase::encode(Base::Base32Lower, "foo").as_bytes().to_vec();
106	assert_eq!(validate_cid(&bad_cid).expect_err("Expected InvalidCid"), CidError::InvalidCid);
107}
108
109#[test]
110fn validate_cid_valid_cid_sha2_256_succeeds() {
111	let cid = DUMMY_CID_SHA256.as_bytes().to_vec();
112	assert_ok!(validate_cid(&cid));
113}
114
115#[test]
116fn validate_cid_valid_cid_blake3_succeeds() {
117	let cid = DUMMY_CID_BLAKE3.as_bytes().to_vec();
118	assert_ok!(validate_cid(&cid));
119}
120
121#[test]
122fn validate_cid_invalid_hash_function_errors() {
123	let bad_cid = DUMMY_CID_SHA512.as_bytes().to_vec();
124	assert_eq!(
125		validate_cid(&bad_cid).expect_err("Expected UnsupportedCidMultihash"),
126		CidError::UnsupportedCidMultihash
127	);
128}
129#[test]
130fn validate_cid_not_valid_multibase() {
131	// This should not panic, but should return an error.
132	let bad_cid = vec![55, 197, 136, 0, 0, 0, 0, 0, 0, 0, 0];
133	assert_eq!(
134		validate_cid(&bad_cid).expect_err("Expected MultibaseDecodeError"),
135		CidError::MultibaseDecodeError
136	);
137}
138
139#[test]
140fn validate_cid_not_correct_format_errors() {
141	// This should not panic, but should return an error.
142	let bad_cid = vec![0, 1, 0, 1, 203, 155, 0, 0, 0, 5, 67];
143	assert_eq!(validate_cid(&bad_cid).expect_err("Expected InvalidCid"), CidError::InvalidCid);
144
145	// This should not panic, but should return an error.
146	let another_bad_cid = vec![241, 0, 0, 0, 0, 0, 128, 132, 132, 132, 58];
147	assert_eq!(
148		validate_cid(&another_bad_cid).expect_err("Expected Utf8DecodeError"),
149		CidError::Utf8DecodeError
150	);
151}
152
153#[test]
154fn validate_cid_unwrap_errors() {
155	// This should not panic, but should return an error.
156	let bad_cid = vec![102, 70, 70, 70, 70, 70, 70, 70, 70, 48, 48, 48, 54, 53, 53, 48, 48];
157	assert_eq!(validate_cid(&bad_cid).expect_err("Expected InvalidCid"), CidError::InvalidCid);
158}