Skip to main content

qualia_core_db/q42/volume/
cid.rs

1//! CID parse and block verification for IPFS-published Q42 segments.
2//!
3//! A gateway is only a transport. The CID's multihash is the authority: a
4//! decoded block is accepted only when `sha2-256(block) == CID digest`.
5//! CIDv0 (`Qm…`) and CIDv1 sha2-256 (`bafy…` / `bafk…` / raw hex) are supported.
6
7use std::io;
8
9use sha2::{Digest, Sha256};
10
11fn invalid(message: impl Into<String>) -> io::Error {
12    io::Error::new(io::ErrorKind::InvalidData, message.into())
13}
14
15/// A verified content identifier. Only sha2-256 (32-byte) digests are accepted.
16#[derive(Clone, Copy, Debug, Eq, PartialEq)]
17pub struct CidSha256 {
18    pub version: u8,
19    pub codec: u64,
20    pub digest: [u8; 32],
21}
22
23impl CidSha256 {
24    pub const RAW: u64 = 0x55;
25    pub const DAG_PB: u64 = 0x70;
26
27    pub fn parse(text: &str) -> io::Result<Self> {
28        let text = text.trim();
29        if text.is_empty() {
30            return Err(invalid("empty CID"));
31        }
32        if text.starts_with("Qm") {
33            return parse_cidv0(text);
34        }
35        if let Some(hex) = text.strip_prefix("f") {
36            return parse_cidv1(&decode_hex(hex)?);
37        }
38        if let Some(b32) = text.strip_prefix("b") {
39            return parse_cidv1(&decode_base32(b32)?);
40        }
41        Err(invalid(
42            "CID must be CIDv0 (Qm…) or CIDv1 base32 (b…) / hex (f…)",
43        ))
44    }
45
46    pub fn for_raw_block(block: &[u8]) -> Self {
47        Self {
48            version: 1,
49            codec: Self::RAW,
50            digest: sha256(block),
51        }
52    }
53
54    pub fn verify_block(&self, block: &[u8]) -> io::Result<()> {
55        let actual = sha256(block);
56        if actual != self.digest {
57            return Err(invalid("block bytes do not match CID sha2-256 digest"));
58        }
59        Ok(())
60    }
61
62    pub fn encode_base32(&self) -> String {
63        let mut raw = Vec::new();
64        raw.push(self.version);
65        encode_varint(&mut raw, self.codec);
66        raw.push(0x12);
67        raw.push(32);
68        raw.extend_from_slice(&self.digest);
69        let mut out = String::from("b");
70        out.push_str(&encode_base32(&raw));
71        out
72    }
73}
74
75fn parse_cidv0(text: &str) -> io::Result<CidSha256> {
76    let bytes = decode_base58btc(text)?;
77    if bytes.len() != 34 || bytes[0] != 0x12 || bytes[1] != 32 {
78        return Err(invalid("CIDv0 must be sha2-256 (34 bytes)"));
79    }
80    let mut digest = [0u8; 32];
81    digest.copy_from_slice(&bytes[2..]);
82    Ok(CidSha256 {
83        version: 0,
84        codec: CidSha256::DAG_PB,
85        digest,
86    })
87}
88
89fn parse_cidv1(bytes: &[u8]) -> io::Result<CidSha256> {
90    if bytes.first() != Some(&0x01) {
91        return Err(invalid("CIDv1 version byte must be 0x01"));
92    }
93    let (codec, used) = decode_varint(&bytes[1..])?;
94    let rest = &bytes[1 + used..];
95    if rest.len() != 34 || rest[0] != 0x12 || rest[1] != 32 {
96        return Err(invalid("only sha2-256 CIDv1 is accepted"));
97    }
98    let mut digest = [0u8; 32];
99    digest.copy_from_slice(&rest[2..]);
100    Ok(CidSha256 {
101        version: 1,
102        codec,
103        digest,
104    })
105}
106
107pub fn sha256(bytes: &[u8]) -> [u8; 32] {
108    Sha256::digest(bytes).into()
109}
110
111fn encode_varint(out: &mut Vec<u8>, mut value: u64) {
112    loop {
113        let mut byte = (value & 0x7f) as u8;
114        value >>= 7;
115        if value != 0 {
116            byte |= 0x80;
117        }
118        out.push(byte);
119        if value == 0 {
120            return;
121        }
122    }
123}
124
125fn decode_varint(bytes: &[u8]) -> io::Result<(u64, usize)> {
126    let mut value = 0u64;
127    let mut shift = 0u32;
128    for (index, byte) in bytes.iter().copied().enumerate() {
129        value |= u64::from(byte & 0x7f) << shift;
130        if byte & 0x80 == 0 {
131            return Ok((value, index + 1));
132        }
133        shift += 7;
134        if shift >= 64 {
135            return Err(invalid("CID varint overflow"));
136        }
137    }
138    Err(invalid("truncated CID varint"))
139}
140
141fn decode_hex(text: &str) -> io::Result<Vec<u8>> {
142    if text.len() % 2 != 0 {
143        return Err(invalid("odd-length hex CID"));
144    }
145    let mut out = Vec::with_capacity(text.len() / 2);
146    let bytes = text.as_bytes();
147    for pair in bytes.chunks(2) {
148        let hi = hex_nibble(pair[0])?;
149        let lo = hex_nibble(pair[1])?;
150        out.push((hi << 4) | lo);
151    }
152    Ok(out)
153}
154
155fn hex_nibble(byte: u8) -> io::Result<u8> {
156    match byte {
157        b'0'..=b'9' => Ok(byte - b'0'),
158        b'a'..=b'f' => Ok(byte - b'a' + 10),
159        b'A'..=b'F' => Ok(byte - b'A' + 10),
160        _ => Err(invalid("invalid hex digit in CID")),
161    }
162}
163
164fn decode_base32(text: &str) -> io::Result<Vec<u8>> {
165    let mut bits: u32 = 0;
166    let mut nbits = 0u32;
167    let mut out = Vec::new();
168    for byte in text.bytes() {
169        if byte == b'=' {
170            continue;
171        }
172        let value = match byte {
173            b'a'..=b'z' => byte - b'a',
174            b'A'..=b'Z' => byte - b'A',
175            b'2'..=b'7' => byte - b'2' + 26,
176            _ => return Err(invalid("invalid base32 digit in CID")),
177        };
178        bits = (bits << 5) | u32::from(value);
179        nbits += 5;
180        if nbits >= 8 {
181            nbits -= 8;
182            out.push((bits >> nbits) as u8);
183            bits &= (1 << nbits) - 1;
184        }
185    }
186    Ok(out)
187}
188
189fn encode_base32(bytes: &[u8]) -> String {
190    const ALPHABET: &[u8] = b"abcdefghijklmnopqrstuvwxyz234567";
191    let mut bits: u32 = 0;
192    let mut nbits = 0u32;
193    let mut out = String::new();
194    for byte in bytes {
195        bits = (bits << 8) | u32::from(*byte);
196        nbits += 8;
197        while nbits >= 5 {
198            nbits -= 5;
199            out.push(ALPHABET[((bits >> nbits) & 0x1f) as usize] as char);
200            bits &= (1 << nbits) - 1;
201        }
202    }
203    if nbits > 0 {
204        out.push(ALPHABET[((bits << (5 - nbits)) & 0x1f) as usize] as char);
205    }
206    out
207}
208
209fn decode_base58btc(text: &str) -> io::Result<Vec<u8>> {
210    const ALPHABET: &[u8] = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
211    let mut acc = vec![0u8; 1];
212    for byte in text.bytes() {
213        let digit = ALPHABET
214            .iter()
215            .position(|candidate| *candidate == byte)
216            .ok_or_else(|| invalid("invalid base58 digit in CID"))?;
217        let mut carry = digit;
218        for slot in acc.iter_mut().rev() {
219            let value = *slot as usize * 58 + carry;
220            *slot = (value & 0xff) as u8;
221            carry = value >> 8;
222        }
223        while carry > 0 {
224            acc.insert(0, (carry & 0xff) as u8);
225            carry >>= 8;
226        }
227    }
228    let leading = text.bytes().take_while(|b| *b == b'1').count();
229    let mut out = vec![0u8; leading];
230    let skip = acc.iter().position(|b| *b != 0).unwrap_or(acc.len());
231    out.extend_from_slice(&acc[skip..]);
232    Ok(out)
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238
239    #[test]
240    fn raw_block_round_trip_and_tamper_is_detected() {
241        let block = b"q42-superblock-bytes";
242        let cid = CidSha256::for_raw_block(block);
243        let encoded = cid.encode_base32();
244        assert!(encoded.starts_with('b'));
245        let parsed = CidSha256::parse(&encoded).unwrap();
246        parsed.verify_block(block).unwrap();
247        assert!(parsed.verify_block(b"tampered").is_err());
248    }
249
250    #[test]
251    fn hex_cidv1_parses() {
252        let block = b"hello-q42";
253        let digest = sha256(block);
254        let mut raw = vec![0x01, 0x55, 0x12, 32];
255        raw.extend_from_slice(&digest);
256        let mut hex = String::from("f");
257        for byte in &raw {
258            hex.push_str(&format!("{byte:02x}"));
259        }
260        let cid = CidSha256::parse(&hex).unwrap();
261        assert_eq!(cid.codec, CidSha256::RAW);
262        cid.verify_block(block).unwrap();
263    }
264}