Skip to main content

qualia_core_db/q42/volume/
car.rs

1//! Trustless partial-CAR verification for IPFS-published Q42 bytes.
2//!
3//! Q42 offsets address the UnixFS file entity, never the serialized CAR.
4//! Every CAR block is checked against its CID before any payload is copied
5//! into a caller buffer. `entity-bytes` is inclusive on the wire; this
6//! decoder accepts a half-open `[start, start+len)` after checked conversion.
7
8use std::io;
9
10use super::cid::{sha256, CidSha256};
11use super::range::Q42ByteRange;
12
13fn invalid(message: impl Into<String>) -> io::Error {
14    io::Error::new(io::ErrorKind::InvalidData, message.into())
15}
16
17/// One verified raw-leaf block from a CARv1 stream.
18#[derive(Clone, Debug, Eq, PartialEq)]
19pub struct VerifiedCarBlock {
20    pub cid: CidSha256,
21    pub data: Vec<u8>,
22}
23
24/// Decode a CARv1 buffer, verify every block CID, and concatenate raw (0x55)
25/// leaves as the UnixFS entity. Non-raw blocks are verified but not appended
26/// (dag-pb roots are proofs, not Q42 bytes).
27pub fn decode_and_verify_car(car: &[u8]) -> io::Result<Vec<VerifiedCarBlock>> {
28    let (header_len, header_size) = read_varint(car)?;
29    let header_end = header_size
30        .checked_add(header_len as usize)
31        .ok_or_else(|| invalid("CAR header overflow"))?;
32    if header_end > car.len() {
33        return Err(invalid("CAR header truncated"));
34    }
35    let mut offset = header_end;
36    let mut blocks = Vec::new();
37    while offset < car.len() {
38        let (block_len, used) = read_varint(&car[offset..])?;
39        offset += used;
40        let end = offset
41            .checked_add(block_len as usize)
42            .ok_or_else(|| invalid("CAR block overflow"))?;
43        if end > car.len() {
44            return Err(invalid("CAR block truncated"));
45        }
46        let (cid, cid_len) = parse_embedded_cid(&car[offset..end])?;
47        let data = &car[offset + cid_len..end];
48        cid.verify_block(data)?;
49        blocks.push(VerifiedCarBlock {
50            cid,
51            data: data.to_vec(),
52        });
53        offset = end;
54    }
55    Ok(blocks)
56}
57
58/// Extract a half-open entity-byte interval from verified raw leaves.
59pub fn extract_entity_bytes(
60    blocks: &[VerifiedCarBlock],
61    range: Q42ByteRange,
62    out: &mut [u8],
63) -> io::Result<()> {
64    if out.len() != range.length {
65        return Err(invalid("entity-bytes output length mismatch"));
66    }
67    let mut entity = Vec::new();
68    for block in blocks {
69        if block.cid.codec == CidSha256::RAW {
70            entity.extend_from_slice(&block.data);
71        }
72    }
73    let end = range.end()? as usize;
74    if end > entity.len() {
75        return Err(invalid("entity-bytes range exceeds reconstructed file"));
76    }
77    out.copy_from_slice(&entity[range.offset as usize..end]);
78    Ok(())
79}
80
81/// Inclusive `entity-bytes=start:end` (IPIP-0402) → half-open Q42 range.
82pub fn inclusive_entity_bytes(start: u64, end_inclusive: u64) -> io::Result<Q42ByteRange> {
83    if end_inclusive < start {
84        return Err(invalid("entity-bytes end is before start"));
85    }
86    let length = (end_inclusive - start)
87        .checked_add(1)
88        .ok_or_else(|| invalid("entity-bytes length overflow"))?;
89    Ok(Q42ByteRange {
90        offset: start,
91        length: usize::try_from(length).map_err(|_| invalid("entity-bytes exceeds platform"))?,
92    })
93}
94
95/// Encode a one-block raw CARv1 used by tests and fixtures.
96pub fn encode_raw_car(blocks: &[&[u8]]) -> Vec<u8> {
97    let mut out = Vec::new();
98    // Minimal CBOR map {version:1, roots:[]} = a2 67 76 65 72 73 69 6f 6e 01 65 72 6f 6f 74 73 80
99    let header = b"\xa2gversion\x01eroots\x80";
100    write_varint(&mut out, header.len() as u64);
101    out.extend_from_slice(header);
102    for block in blocks {
103        let cid = CidSha256::for_raw_block(block);
104        let mut cid_bytes = vec![0x01];
105        write_varint(&mut cid_bytes, cid.codec);
106        cid_bytes.push(0x12);
107        cid_bytes.push(32);
108        cid_bytes.extend_from_slice(&cid.digest);
109        write_varint(&mut out, (cid_bytes.len() + block.len()) as u64);
110        out.extend_from_slice(&cid_bytes);
111        out.extend_from_slice(block);
112    }
113    out
114}
115
116fn parse_embedded_cid(bytes: &[u8]) -> io::Result<(CidSha256, usize)> {
117    if bytes.len() >= 34 && bytes[0] == 0x12 && bytes[1] == 32 {
118        let mut digest = [0u8; 32];
119        digest.copy_from_slice(&bytes[2..34]);
120        return Ok((
121            CidSha256 {
122                version: 0,
123                codec: CidSha256::DAG_PB,
124                digest,
125            },
126            34,
127        ));
128    }
129    if bytes.first() != Some(&0x01) {
130        return Err(invalid("CAR block CID is neither CIDv0 nor CIDv1"));
131    }
132    let (codec, used) = read_varint(&bytes[1..])?;
133    let mh = 1 + used;
134    if bytes.len() < mh + 34 || bytes[mh] != 0x12 || bytes[mh + 1] != 32 {
135        return Err(invalid("CAR block is not sha2-256"));
136    }
137    let mut digest = [0u8; 32];
138    digest.copy_from_slice(&bytes[mh + 2..mh + 34]);
139    // Confirm digest independently of the CID bytes we just parsed.
140    let _ = sha256;
141    Ok((
142        CidSha256 {
143            version: 1,
144            codec,
145            digest,
146        },
147        mh + 34,
148    ))
149}
150
151fn write_varint(out: &mut Vec<u8>, mut value: u64) {
152    loop {
153        let mut byte = (value & 0x7f) as u8;
154        value >>= 7;
155        if value != 0 {
156            byte |= 0x80;
157        }
158        out.push(byte);
159        if value == 0 {
160            return;
161        }
162    }
163}
164
165fn read_varint(bytes: &[u8]) -> io::Result<(u64, usize)> {
166    let mut value = 0u64;
167    let mut shift = 0u32;
168    for (index, byte) in bytes.iter().copied().enumerate() {
169        value |= u64::from(byte & 0x7f) << shift;
170        if byte & 0x80 == 0 {
171            return Ok((value, index + 1));
172        }
173        shift += 7;
174        if shift >= 64 {
175            return Err(invalid("CAR varint overflow"));
176        }
177    }
178    Err(invalid("truncated CAR varint"))
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184
185    #[test]
186    fn car_verifies_cids_and_extracts_entity_bytes() {
187        let first = b"Q42-HEADER-BYTES";
188        let second = b"Q42-BLOCK-PAYLOAD";
189        let car = encode_raw_car(&[first.as_slice(), second.as_slice()]);
190        let blocks = decode_and_verify_car(&car).unwrap();
191        assert_eq!(blocks.len(), 2);
192        let range = inclusive_entity_bytes(4, 19).unwrap();
193        let mut out = vec![0u8; range.length];
194        extract_entity_bytes(&blocks, range, &mut out).unwrap();
195        let mut expected = Vec::new();
196        expected.extend_from_slice(first);
197        expected.extend_from_slice(second);
198        assert_eq!(out, expected[4..20]);
199    }
200
201    #[test]
202    fn tampered_car_block_is_rejected() {
203        let mut car = encode_raw_car(&[b"intact".as_slice()]);
204        *car.last_mut().unwrap() ^= 0xff;
205        assert!(decode_and_verify_car(&car).is_err());
206    }
207}