qualia_core_db/q42/volume/
cid.rs1use 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#[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}