Something went wrong. Try again.
fork of hydrant
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180use std::collections::BTreeMap;use std::io::Cursor;
use bytes::Bytes;use cid::Cid as IpldCid;use miette::{IntoDiagnostic, Result};
#[cfg_attr(not(feature = "indexer"), allow(dead_code))]#[derive(Debug)]pub(crate) struct ParsedCar { pub root: IpldCid, pub blocks: BTreeMap<IpldCid, Bytes>,}
#[cfg_attr(not(feature = "indexer"), allow(dead_code))]pub(crate) fn parse_car(data: Bytes) -> Result<ParsedCar> { let (header_start, blocks_start) = car_header_bounds(&data)?; let header = iroh_car::CarHeader::decode(&data[header_start..blocks_start]).into_diagnostic()?; let root = header .roots() .first() .copied() .ok_or_else(|| miette::miette!("CAR file has no roots"))?; let blocks = parse_car_blocks_from(data, blocks_start)?;
Ok(ParsedCar { root, blocks })}
#[cfg_attr(not(feature = "indexer"), allow(dead_code))]pub(crate) fn parse_car_blocks(data: Bytes) -> Result<BTreeMap<IpldCid, Bytes>> { let (_, blocks_start) = car_header_bounds(&data)?; parse_car_blocks_from(data, blocks_start)}
fn car_header_bounds(data: &[u8]) -> Result<(usize, usize)> { let mut offset = 0; let Some(header_len) = read_uvarint(data, &mut offset)? else { return Err(miette::miette!("empty CAR file")); }; let header_start = offset; let header_end = header_start .checked_add(header_len) .ok_or_else(|| miette::miette!("CAR header length overflow"))?; if header_end > data.len() { return Err(miette::miette!("truncated CAR header")); }
Ok((header_start, header_end))}
fn parse_car_blocks_from(data: Bytes, mut offset: usize) -> Result<BTreeMap<IpldCid, Bytes>> { let mut blocks = BTreeMap::new(); while let Some(section_len) = read_uvarint(&data, &mut offset)? { let section_end = offset .checked_add(section_len) .ok_or_else(|| miette::miette!("CAR block length overflow"))?; if section_end > data.len() { return Err(miette::miette!("truncated CAR block")); }
let section = data.slice(offset..section_end); offset = section_end;
let mut cursor = Cursor::new(section.as_ref()); let cid = IpldCid::read_bytes(&mut cursor).into_diagnostic()?; let block_start = cursor.position() as usize; if block_start >= section.len() { return Err(miette::miette!("CAR block has no payload for {cid}")); } blocks.insert(cid, section.slice(block_start..)); }
Ok(blocks)}
/// validates that each block's payload hashes to its claimed CID (sha2-256, dag-cbor).pub(crate) fn validate_block_cids<'a>( blocks: impl IntoIterator<Item = (&'a IpldCid, &'a Bytes)>,) -> Result<()> { for (claimed_cid, bytes) in blocks { let computed_cid = jacquard_repo::mst::util::compute_cid(bytes.as_ref()).into_diagnostic()?; if computed_cid != *claimed_cid { return Err(miette::miette!( "CAR block CID mismatch: claimed {claimed_cid}, computed {computed_cid}" )); } } Ok(())}
#[cfg_attr(not(feature = "indexer"), allow(dead_code))]fn read_uvarint(data: &[u8], offset: &mut usize) -> Result<Option<usize>> { if *offset == data.len() { return Ok(None); }
let mut value = 0u64; for shift in (0..64).step_by(7) { if *offset >= data.len() { return Err(miette::miette!("truncated uvarint in CAR file")); }
let byte = data[*offset]; *offset += 1; value |= u64::from(byte & 0x7f) << shift;
if byte & 0x80 == 0 { let len = usize::try_from(value).into_diagnostic()?; return Ok(Some(len)); } }
Err(miette::miette!("uvarint overflow in CAR file"))}
#[cfg(test)]mod tests { use super::*; use cid::multihash::Multihash; use jacquard_common::types::crypto::{DAG_CBOR, SHA2_256};
fn cid(byte: u8) -> IpldCid { let hash = [byte; 32]; let mh = Multihash::<64>::wrap(SHA2_256, &hash).unwrap(); IpldCid::new_v1(DAG_CBOR, mh) }
#[tokio::test] async fn parses_rootless_block_car() { let cid = cid(1); let mut buf = Vec::new(); let header = iroh_car::CarHeader::new_v1(Vec::new()); let mut writer = iroh_car::CarWriter::new(header, &mut buf); writer.write(cid, b"block".to_vec()).await.unwrap(); writer.finish().await.unwrap();
let blocks = parse_car_blocks(buf.into()).unwrap(); assert_eq!(blocks.get(&cid).unwrap().as_ref(), b"block"); }
#[tokio::test] async fn parses_rooted_car_with_zero_copy_payloads() { let root = cid(1); let block = cid(2); let mut buf = Vec::new(); let header = iroh_car::CarHeader::new_v1(vec![root]); let mut writer = iroh_car::CarWriter::new(header, &mut buf); writer.write(block, b"record".to_vec()).await.unwrap(); writer.finish().await.unwrap();
let data = Bytes::from(buf); let data_range = data.as_ptr() as usize..data.as_ptr() as usize + data.len(); let parsed = parse_car(data).unwrap(); let payload = parsed.blocks.get(&block).unwrap(); let payload_range = payload.as_ptr() as usize..payload.as_ptr() as usize + payload.len();
assert_eq!(parsed.root, root); assert_eq!(payload.as_ref(), b"record"); assert!(data_range.start <= payload_range.start); assert!(payload_range.end <= data_range.end); }
#[test] fn validate_block_cids_rejects_mismatched_cid() { let blocks = BTreeMap::from([(cid(1), Bytes::from_static(b"forged"))]); let err = validate_block_cids(&blocks).unwrap_err(); assert!(err.to_string().contains("CAR block CID mismatch")); }
#[test] fn validate_block_cids_accepts_matching_cid() { let bytes = Bytes::from_static(b"trusted"); let claimed = jacquard_repo::mst::util::compute_cid(bytes.as_ref()).unwrap(); let blocks = BTreeMap::from([(claimed, bytes)]); validate_block_cids(&blocks).unwrap(); }}