use sha2::{Digest, Sha256};
use std::collections::HashMap;
pub fn is_car_file(data: &[u8]) -> bool {
data.len() >= 20 && data[1..20].windows(5).any(|w| w == b"roots")
}
fn parse_car_header_roots(header: &[u8]) -> Result<Vec<Vec<u8>>, String> {
let mut pos = 0;
let read_cbor_len =
|additional_info: u8, data: &[u8], offset: usize| -> Result<(usize, usize), String> {
if additional_info <= 23 {
Ok((additional_info as usize, 0))
} else if additional_info == 24 {
if offset >= data.len() {
return Err("CBOR truncated: expected 1-byte length".into());
}
Ok((data[offset] as usize, 1))
} else if additional_info == 25 {
if offset + 1 >= data.len() {
return Err("CBOR truncated: expected 2-byte length".into());
}
Ok((
((data[offset] as usize) << 8) | data[offset + 1] as usize,
2,
))
} else if additional_info == 26 {
if offset + 3 >= data.len() {
return Err("CBOR truncated: expected 4-byte length".into());
}
let v = ((data[offset] as usize) << 24)
| ((data[offset + 1] as usize) << 16)
| ((data[offset + 2] as usize) << 8)
| data[offset + 3] as usize;
Ok((v, 4))
} else {
Err(format!(
"unsupported CBOR additional info: {additional_info}"
))
}
};
if pos >= header.len() {
return Err("CAR header is empty".into());
}
let initial = header[pos];
pos += 1;
let major = initial >> 5;
let additional = initial & 0x1f;
if major != 5 {
return Err(format!("CAR header is not a CBOR map (major type {major})"));
}
let (map_len, extra) = read_cbor_len(additional, header, pos)?;
pos += extra;
const MAX_MAP_ENTRIES: usize = 8;
if map_len > MAX_MAP_ENTRIES {
return Err(format!(
"CAR header CBOR map has {map_len} entries, exceeding maximum {MAX_MAP_ENTRIES}"
));
}
let mut roots: Option<Vec<Vec<u8>>> = None;
for _ in 0..map_len {
if pos >= header.len() {
return Err("CBOR map key missing".into());
}
let key_initial = header[pos];
pos += 1;
let key_major = key_initial >> 5;
let key_additional = key_initial & 0x1f;
if key_major != 3 {
return Err(format!(
"CBOR map key is not a text string (major type {key_major})"
));
}
let (key_len, extra) = read_cbor_len(key_additional, header, pos)?;
pos += extra;
if pos + key_len > header.len() {
return Err("CBOR map key truncated".into());
}
let key = &header[pos..pos + key_len];
pos += key_len;
if key == b"roots" {
if pos >= header.len() {
return Err("CBOR roots value missing".into());
}
let arr_initial = header[pos];
pos += 1;
let arr_major = arr_initial >> 5;
let arr_additional = arr_initial & 0x1f;
if arr_major != 4 {
return Err(format!(
"CAR header 'roots' is not a CBOR array (major type {arr_major})"
));
}
let (arr_len, extra) = read_cbor_len(arr_additional, header, pos)?;
pos += extra;
const MAX_ROOTS: usize = 16;
if arr_len > MAX_ROOTS {
return Err(format!(
"CAR header declares {arr_len} roots, exceeding maximum {MAX_ROOTS}"
));
}
let mut cids = Vec::with_capacity(arr_len);
for _ in 0..arr_len {
if pos + 2 > header.len() {
return Err("CBOR tag-42 truncated in roots array".into());
}
if header[pos] != 0xd8 || header[pos + 1] != 0x2a {
return Err(format!(
"expected CBOR tag-42 (0xd8 0x2a), got 0x{:02x} 0x{:02x}",
header[pos],
header[pos + 1]
));
}
pos += 2;
if pos >= header.len() {
return Err("CBOR byte string missing after tag-42".into());
}
let bs_initial = header[pos];
pos += 1;
let bs_major = bs_initial >> 5;
let bs_additional = bs_initial & 0x1f;
if bs_major != 2 {
return Err(format!(
"tag-42 content is not a byte string (major type {bs_major})"
));
}
let (bs_len, extra) = read_cbor_len(bs_additional, header, pos)?;
pos += extra;
if pos + bs_len > header.len() {
return Err("CBOR byte string truncated in roots array".into());
}
if bs_len < 2 {
return Err("tag-42 byte string too short (must have 0x00 prefix + at least 1 CID byte)".into());
}
if header[pos] != 0x00 {
return Err(format!(
"tag-42 byte string has non-identity multibase prefix 0x{:02x}",
header[pos]
));
}
let cid = header[pos + 1..pos + bs_len].to_vec();
pos += bs_len;
cids.push(cid);
}
roots = Some(cids);
} else {
if pos >= header.len() {
return Err("CBOR map value missing".into());
}
let val_initial = header[pos];
pos += 1;
let val_major = val_initial >> 5;
let val_additional = val_initial & 0x1f;
match val_major {
0 | 1 => {
if val_additional >= 24 {
let (_, extra) = read_cbor_len(val_additional, header, pos)?;
pos += extra;
}
}
2 | 3 => {
let (len, extra) = read_cbor_len(val_additional, header, pos)?;
pos += extra + len;
}
6 => {
if val_additional >= 24 {
let (_, extra) = read_cbor_len(val_additional, header, pos)?;
pos += extra;
}
if pos < header.len() {
let inner = header[pos];
pos += 1;
let inner_major = inner >> 5;
let inner_additional = inner & 0x1f;
if inner_major == 2 || inner_major == 3 {
let (len, extra) = read_cbor_len(inner_additional, header, pos)?;
pos += extra + len;
} else if (inner_major == 0 || inner_major == 1) && inner_additional >= 24 {
let (_, extra) = read_cbor_len(inner_additional, header, pos)?;
pos += extra;
}
}
}
4 => {
let (len, extra) = read_cbor_len(val_additional, header, pos)?;
pos += extra;
for _ in 0..len {
if pos >= header.len() {
break;
}
let el = header[pos];
pos += 1;
let el_additional = el & 0x1f;
let el_major = el >> 5;
match el_major {
2 | 3 => {
let (elen, eextra) = read_cbor_len(el_additional, header, pos)?;
pos += eextra + elen;
}
0 | 1 => {
if el_additional >= 24 {
let (_, extra) = read_cbor_len(el_additional, header, pos)?;
pos += extra;
}
}
6 => {
if el_additional >= 24 {
let (_, extra) = read_cbor_len(el_additional, header, pos)?;
pos += extra;
}
if pos < header.len() {
let inner = header[pos];
pos += 1;
let inner_additional = inner & 0x1f;
let inner_major = inner >> 5;
if inner_major == 2 || inner_major == 3 {
let (ilen, iextra) =
read_cbor_len(inner_additional, header, pos)?;
pos += iextra + ilen;
}
}
}
_ => {} }
}
}
_ => {
return Err(format!(
"unexpected CBOR major type {val_major} for map value"
));
}
}
}
}
roots.ok_or_else(|| "CAR header CBOR map has no 'roots' key".into())
}
pub fn parse_car_to_assets(data: &[u8]) -> Result<HashMap<String, Vec<u8>>, String> {
let mut pos = 0;
let (header_len, n) = read_uvarint(&data[pos..])?;
pos += n;
if pos + header_len > data.len() {
return Err("CAR header length exceeds data".into());
}
let header_roots = parse_car_header_roots(&data[pos..pos + header_len])?;
if header_roots.is_empty() {
return Err("CAR header declares no roots".into());
}
pos += header_len;
let mut blocks: HashMap<Vec<u8>, Vec<u8>> = HashMap::new();
while pos < data.len() {
let (block_len, n) = read_uvarint(&data[pos..])?;
pos += n;
if pos + block_len > data.len() {
break;
}
let block_start = pos;
let (_version, n) = read_uvarint(&data[pos..])?;
pos += n;
let (_codec, n) = read_uvarint(&data[pos..])?;
pos += n;
let (hash_fn, n) = read_uvarint(&data[pos..])?;
pos += n;
let (digest_size, n) = read_uvarint(&data[pos..])?;
pos += n;
let digest_start = pos;
if pos + digest_size > data.len() || pos + digest_size > block_start + block_len {
return Err("CID digest extends beyond block boundary".into());
}
pos += digest_size;
let cid_bytes = data[block_start..pos].to_vec();
let block_data = data[pos..block_start + block_len].to_vec();
if hash_fn == 0x12 && digest_size == 32 {
let expected = &data[digest_start..digest_start + 32];
let actual = Sha256::digest(&block_data);
if actual.as_slice() != expected {
return Err(format!(
"CID integrity check failed: block hash mismatch (expected {}, got {})",
hex(&expected[..4]),
hex(&actual[..4]),
));
}
}
blocks.insert(cid_bytes, block_data);
pos = block_start + block_len;
}
log::info!("[car] parsed {} blocks", blocks.len());
let root = header_roots
.into_iter()
.next()
.expect("guaranteed non-empty by check above");
let root_block = blocks
.get(&root)
.ok_or("root CID declared in header not found in block section")?;
const MAX_DEPTH: usize = 32;
if !is_directory_node(root_block) {
if let Ok(file_data) = reassemble_file(&blocks, &root, MAX_DEPTH) {
if is_car_file(&file_data) {
log::info!(
"[car] root is a file ({} bytes) containing a nested CAR, parsing inner archive...",
file_data.len()
);
return parse_car_to_assets(&file_data);
}
log::info!("[car] root is a single file ({} bytes)", file_data.len());
let mut assets = HashMap::new();
assets.insert("index.html".into(), file_data);
return Ok(assets);
}
}
let links = parse_dagpb_links(root_block);
log::info!("[car] root directory has {} entries", links.len());
let mut assets = HashMap::new();
for (name, cid_bytes, _size) in &links {
match reassemble_file(&blocks, cid_bytes, MAX_DEPTH) {
Ok(content) => {
log::info!("[car] extracted: {name} ({} bytes)", content.len());
assets.insert(name.clone(), content);
}
Err(_) => {
if let Some(dir_block) = blocks.get(cid_bytes) {
let sub_links = parse_dagpb_links(dir_block);
if !sub_links.is_empty() {
extract_directory_recursive(
&blocks,
name,
&sub_links,
&mut assets,
MAX_DEPTH - 1,
);
}
}
}
}
}
if assets.is_empty() {
return Err("CAR file contained no extractable assets".into());
}
Ok(assets)
}
fn extract_directory_recursive(
blocks: &HashMap<Vec<u8>, Vec<u8>>,
prefix: &str,
links: &[(String, Vec<u8>, u64)],
assets: &mut HashMap<String, Vec<u8>>,
depth: usize,
) {
if depth == 0 {
log::warn!("[car] max recursion depth reached at {prefix}");
return;
}
for (name, cid_bytes, _size) in links {
let full_path = format!("{prefix}/{name}");
match reassemble_file(blocks, cid_bytes, depth - 1) {
Ok(content) => {
log::info!("[car] extracted: {full_path} ({} bytes)", content.len());
assets.insert(full_path, content);
}
Err(_) => {
if let Some(dir_block) = blocks.get(cid_bytes) {
let sub_links = parse_dagpb_links(dir_block);
if !sub_links.is_empty() {
extract_directory_recursive(
blocks,
&full_path,
&sub_links,
assets,
depth - 1,
);
}
}
}
}
}
}
pub fn parse_dagpb_links(pb: &[u8]) -> Vec<(String, Vec<u8>, u64)> {
let mut links = Vec::new();
let mut pos = 0;
while pos < pb.len() {
let tag = pb[pos];
let field_num = tag >> 3;
let wire_type = tag & 0x7;
pos += 1;
if wire_type == 2 {
let (length, n) = match read_uvarint(&pb[pos..]) {
Ok(v) => v,
Err(_) => break,
};
pos += n;
if pos + length > pb.len() {
break;
}
if field_num == 2 {
let inner = &pb[pos..pos + length];
let mut hash = Vec::new();
let mut name = String::new();
let mut tsize: u64 = 0;
let mut ipos = 0;
while ipos < inner.len() {
let itag = inner[ipos];
let inum = itag >> 3;
let iwire = itag & 0x7;
ipos += 1;
match iwire {
2 => {
let (ilen, n) = match read_uvarint(&inner[ipos..]) {
Ok(v) => v,
Err(_) => break,
};
ipos += n;
if ipos + ilen > inner.len() {
break;
}
match inum {
1 => hash = inner[ipos..ipos + ilen].to_vec(),
2 => {
name = String::from_utf8_lossy(&inner[ipos..ipos + ilen])
.to_string()
}
_ => {}
}
ipos += ilen;
}
0 => {
let (val, n) = match read_uvarint(&inner[ipos..]) {
Ok(v) => v,
Err(_) => break,
};
ipos += n;
if inum == 3 {
tsize = val as u64;
}
}
_ => break,
}
}
if !hash.is_empty() {
links.push((name, hash, tsize));
}
}
pos += length;
} else if wire_type == 0 {
let (_, n) = match read_uvarint(&pb[pos..]) {
Ok(v) => v,
Err(_) => break,
};
pos += n;
} else {
break;
}
}
links
}
#[derive(Debug, PartialEq)]
enum UnixFsType {
Raw,
Directory,
File,
Unknown,
}
fn unixfs_type(unixfs: &[u8]) -> UnixFsType {
let mut pos = 0;
while pos < unixfs.len() {
let tag = unixfs[pos];
let field_num = tag >> 3;
let wire_type = tag & 0x7;
pos += 1;
if wire_type == 0 {
let (val, n) = match read_uvarint(&unixfs[pos..]) {
Ok(v) => v,
Err(_) => return UnixFsType::Unknown,
};
pos += n;
if field_num == 1 {
return match val {
0 => UnixFsType::Raw,
1 => UnixFsType::Directory,
2 => UnixFsType::File,
_ => UnixFsType::Unknown,
};
}
} else if wire_type == 2 {
let (length, n) = match read_uvarint(&unixfs[pos..]) {
Ok(v) => v,
Err(_) => return UnixFsType::Unknown,
};
pos += n + length;
} else {
break;
}
}
UnixFsType::Unknown
}
fn is_directory_node(block: &[u8]) -> bool {
if let Some(data) = extract_dagpb_data(block) {
unixfs_type(&data) == UnixFsType::Directory
} else {
false
}
}
const ZSTD_MAGIC: [u8; 4] = [0x28, 0xB5, 0x2F, 0xFD];
fn maybe_decompress_zstd(data: Vec<u8>) -> Result<Vec<u8>, String> {
const MAX_DECOMPRESSED: usize = 256 * 1024 * 1024; if data.len() >= 4 && data[..4] == ZSTD_MAGIC {
use std::io::Read;
let mut decoder = ruzstd::decoding::StreamingDecoder::new(data.as_slice())
.map_err(|e| format!("zstd init failed: {e}"))?;
let mut out = Vec::new();
let mut buf = [0u8; 8192];
loop {
let n = decoder
.read(&mut buf)
.map_err(|e| format!("zstd decompress failed: {e}"))?;
if n == 0 {
break;
}
out.extend_from_slice(&buf[..n]);
if out.len() > MAX_DECOMPRESSED {
return Err(format!(
"zstd decompressed size exceeds {MAX_DECOMPRESSED} bytes"
));
}
}
Ok(out)
} else {
Ok(data)
}
}
fn reassemble_file(
blocks: &HashMap<Vec<u8>, Vec<u8>>,
cid_bytes: &[u8],
depth: usize,
) -> Result<Vec<u8>, String> {
if depth == 0 {
return Err("max recursion depth reached".into());
}
let block = blocks.get(cid_bytes).ok_or("block not found")?;
if is_directory_node(block) {
return Err("directory node".into());
}
let links = parse_dagpb_links(block);
if links.is_empty() {
if let Some(data) = extract_dagpb_data(block) {
return maybe_decompress_zstd(extract_unixfs_data(&data)?);
}
return maybe_decompress_zstd(block.clone());
}
let mut result = Vec::new();
for (_name, child_cid, _size) in &links {
let chunk = reassemble_file(blocks, child_cid, depth - 1)?;
result.extend_from_slice(&chunk);
}
Ok(result)
}
fn extract_dagpb_data(pb: &[u8]) -> Option<Vec<u8>> {
let mut pos = 0;
while pos < pb.len() {
let tag = pb[pos];
let field_num = tag >> 3;
let wire_type = tag & 0x7;
pos += 1;
if wire_type == 2 {
let (length, n) = read_uvarint(&pb[pos..]).ok()?;
pos += n;
if field_num == 1 {
return Some(pb[pos..pos + length].to_vec());
}
pos += length;
} else if wire_type == 0 {
let (_, n) = read_uvarint(&pb[pos..]).ok()?;
pos += n;
} else {
break;
}
}
None
}
fn extract_unixfs_data(unixfs: &[u8]) -> Result<Vec<u8>, String> {
let mut pos = 0;
while pos < unixfs.len() {
let tag = unixfs[pos];
let field_num = tag >> 3;
let wire_type = tag & 0x7;
pos += 1;
match wire_type {
2 => {
let (length, n) = read_uvarint(&unixfs[pos..]).map_err(|e| e.to_string())?;
pos += n;
if field_num == 2 {
return Ok(unixfs[pos..pos + length].to_vec());
}
pos += length;
}
0 => {
let (_, n) = read_uvarint(&unixfs[pos..]).map_err(|e| e.to_string())?;
pos += n;
}
_ => break,
}
}
Err("no data in UnixFS node".into())
}
fn hex(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
pub fn read_uvarint(data: &[u8]) -> Result<(usize, usize), String> {
if data.is_empty() {
return Err("empty data for uvarint".into());
}
let mut value: usize = 0;
let mut shift = 0;
for (i, &byte) in data.iter().enumerate() {
value |= ((byte & 0x7f) as usize) << shift;
if byte & 0x80 == 0 {
return Ok((value, i + 1));
}
shift += 7;
if shift > 63 {
return Err("uvarint too long".into());
}
}
Err("unterminated uvarint".into())
}
#[cfg(test)]
mod tests {
use super::*;
use sha2::{Digest, Sha256};
fn encode_uvarint(mut val: usize) -> Vec<u8> {
let mut buf = Vec::new();
loop {
let mut byte = (val & 0x7f) as u8;
val >>= 7;
if val != 0 {
byte |= 0x80;
}
buf.push(byte);
if val == 0 {
break;
}
}
buf
}
fn build_cidv1_sha256(data: &[u8]) -> Vec<u8> {
let digest = Sha256::digest(data);
let mut cid = Vec::new();
cid.extend_from_slice(&encode_uvarint(1));
cid.extend_from_slice(&encode_uvarint(0x70));
cid.extend_from_slice(&encode_uvarint(0x12));
cid.extend_from_slice(&encode_uvarint(32));
cid.extend_from_slice(digest.as_slice());
cid
}
fn cbor_text(s: &str) -> Vec<u8> {
let mut out = Vec::new();
let len = s.len();
if len <= 23 {
out.push(0x60 | len as u8);
} else {
out.push(0x78);
out.push(len as u8);
}
out.extend_from_slice(s.as_bytes());
out
}
fn cbor_bytes(b: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
let len = b.len();
if len <= 23 {
out.push(0x40 | len as u8);
} else {
out.push(0x58);
out.push(len as u8);
}
out.extend_from_slice(b);
out
}
fn cbor_tag42_cid(raw_cid: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
out.push(0xd8);
out.push(0x2a);
let mut payload = vec![0x00u8];
payload.extend_from_slice(raw_cid);
out.extend_from_slice(&cbor_bytes(&payload));
out
}
fn build_car_header(root_cids: &[&[u8]]) -> Vec<u8> {
let mut map_body = Vec::new();
map_body.extend_from_slice(&cbor_text("version"));
map_body.push(0x01);
map_body.extend_from_slice(&cbor_text("roots"));
let n = root_cids.len();
if n <= 23 {
map_body.push(0x80 | n as u8);
} else {
map_body.push(0x98);
map_body.push(n as u8);
}
for cid in root_cids {
map_body.extend_from_slice(&cbor_tag42_cid(cid));
}
let mut out = vec![0xa2u8];
out.extend_from_slice(&map_body);
out
}
fn build_car_block(cid: &[u8], data: &[u8]) -> Vec<u8> {
let content_len = cid.len() + data.len();
let mut out = encode_uvarint(content_len);
out.extend_from_slice(cid);
out.extend_from_slice(data);
out
}
fn build_car(header: &[u8], blocks: &[Vec<u8>]) -> Vec<u8> {
let mut out = encode_uvarint(header.len());
out.extend_from_slice(header);
for block in blocks {
out.extend_from_slice(block);
}
out
}
fn pb_field_bytes(field_num: u8, payload: &[u8]) -> Vec<u8> {
let tag = (field_num << 3) | 2;
let mut out = vec![tag];
out.extend_from_slice(&encode_uvarint(payload.len()));
out.extend_from_slice(payload);
out
}
fn pb_field_varint(field_num: u8, val: usize) -> Vec<u8> {
let tag = (field_num << 3) | 0;
let mut out = vec![tag];
out.extend_from_slice(&encode_uvarint(val));
out
}
fn build_unixfs_file_block(file_data: &[u8]) -> Vec<u8> {
let mut unixfs = pb_field_varint(1, 2);
unixfs.extend_from_slice(&pb_field_bytes(2, file_data));
pb_field_bytes(1, &unixfs)
}
fn build_unixfs_dir_block(child_cid: &[u8], name: &str, tsize: usize) -> Vec<u8> {
let mut link = pb_field_bytes(1, child_cid); link.extend_from_slice(&pb_field_bytes(2, name.as_bytes())); link.extend_from_slice(&pb_field_varint(3, tsize));
let unixfs_dir = pb_field_varint(1, 1);
let mut pb = pb_field_bytes(2, &link);
pb.extend_from_slice(&pb_field_bytes(1, &unixfs_dir));
pb
}
#[test]
fn test_root_block_last() {
let file_content = b"hello";
let leaf_block_data = build_unixfs_file_block(file_content);
let leaf_cid = build_cidv1_sha256(&leaf_block_data);
let dir_block_data = build_unixfs_dir_block(&leaf_cid, "test.txt", file_content.len());
let dir_cid = build_cidv1_sha256(&dir_block_data);
let header = build_car_header(&[dir_cid.as_slice()]);
let blocks = vec![
build_car_block(&leaf_cid, &leaf_block_data),
build_car_block(&dir_cid, &dir_block_data),
];
let car = build_car(&header, &blocks);
let assets = parse_car_to_assets(&car).expect("should parse successfully");
assert_eq!(
assets.get("test.txt").map(|v| v.as_slice()),
Some(file_content.as_slice()),
"file content should match"
);
}
#[test]
fn test_rejects_empty_roots() {
let header = build_car_header(&[]);
let car = build_car(&header, &[]);
let err = parse_car_to_assets(&car).expect_err("should fail for empty roots");
assert!(
err.contains("no roots"),
"error should mention 'no roots', got: {err}"
);
}
#[test]
fn test_rejects_missing_root_block() {
let fake_cid = build_cidv1_sha256(b"nonexistent block data");
let header = build_car_header(&[fake_cid.as_slice()]);
let car = build_car(&header, &[]);
let err = parse_car_to_assets(&car).expect_err("should fail for missing root block");
assert!(
err.contains("not found in block section"),
"error should mention 'not found in block section', got: {err}"
);
}
#[test]
fn test_parse_car_header_roots_single() {
let cid = build_cidv1_sha256(b"some block");
let header = build_car_header(&[cid.as_slice()]);
let roots = parse_car_header_roots(&header).expect("should parse header");
assert_eq!(roots.len(), 1);
assert_eq!(roots[0], cid, "extracted CID should match the input CID");
}
#[test]
fn test_parse_car_header_roots_rejects_truncated() {
let cid = build_cidv1_sha256(b"some block");
let header = build_car_header(&[cid.as_slice()]);
let truncated = &header[..header.len() / 2];
let result = parse_car_header_roots(truncated);
assert!(result.is_err(), "truncated header should return Err");
}
#[test]
fn test_nested_car_file_root() {
let inner_file_content = b"<html>hello</html>";
let inner_leaf = build_unixfs_file_block(inner_file_content);
let inner_leaf_cid = build_cidv1_sha256(&inner_leaf);
let inner_dir =
build_unixfs_dir_block(&inner_leaf_cid, "index.html", inner_file_content.len());
let inner_dir_cid = build_cidv1_sha256(&inner_dir);
let inner_header = build_car_header(&[inner_dir_cid.as_slice()]);
let inner_car = build_car(
&inner_header,
&[
build_car_block(&inner_leaf_cid, &inner_leaf),
build_car_block(&inner_dir_cid, &inner_dir),
],
);
let outer_leaf = build_unixfs_file_block(&inner_car);
let outer_leaf_cid = build_cidv1_sha256(&outer_leaf);
let outer_header = build_car_header(&[outer_leaf_cid.as_slice()]);
let outer_car = build_car(
&outer_header,
&[build_car_block(&outer_leaf_cid, &outer_leaf)],
);
let assets = parse_car_to_assets(&outer_car).expect("should parse nested CAR");
assert_eq!(
assets.get("index.html").map(|v| v.as_slice()),
Some(inner_file_content.as_slice()),
"nested CAR should yield the inner file"
);
}
#[test]
fn test_single_file_root() {
let file_content = b"<html>single page</html>";
let leaf = build_unixfs_file_block(file_content);
let leaf_cid = build_cidv1_sha256(&leaf);
let header = build_car_header(&[leaf_cid.as_slice()]);
let car = build_car(&header, &[build_car_block(&leaf_cid, &leaf)]);
let assets = parse_car_to_assets(&car).expect("should parse single file root");
assert_eq!(
assets.get("index.html").map(|v| v.as_slice()),
Some(file_content.as_slice()),
"single file root should be returned as index.html"
);
}
}