use crate::error::{AtmosError, Result};
use bytes::Bytes;
use cid::Cid;
use ipld_core::ipld::Ipld;
use iroh_car::{CarHeader, CarReader, CarWriter};
use multihash::Multihash;
use serde_ipld_dagcbor as dagcbor;
use serde_ipld_dagjson as dagjson;
use sha2::Digest;
use std::collections::HashMap;
use std::io::Cursor;
use tokio::io::{AsyncRead, AsyncWrite};
pub struct CarImporter {
pub blocks: HashMap<Cid, Bytes>,
pub roots: Vec<Cid>,
}
impl CarImporter {
pub fn new() -> Self {
Self {
blocks: HashMap::new(),
roots: Vec::new(),
}
}
pub async fn import_from_reader<R: AsyncRead + Unpin>(&mut self, reader: R) -> Result<()> {
let mut car_reader = CarReader::new(reader).await?;
let header = car_reader.header().clone();
self.roots = header.roots().to_vec();
loop {
match car_reader.next_block().await? {
Some((cid, data)) => {
self.blocks.insert(cid, Bytes::copy_from_slice(&data));
}
None => break,
}
}
Ok(())
}
pub async fn import_from_bytes(&mut self, data: Bytes) -> Result<()> {
let cursor = Cursor::new(data);
self.import_from_reader(cursor).await
}
pub fn get_block(&self, cid: &Cid) -> Option<&Bytes> {
self.blocks.get(cid)
}
pub fn roots(&self) -> &[Cid] {
&self.roots
}
pub fn cids(&self) -> Vec<Cid> {
self.blocks.keys().cloned().collect()
}
pub fn decode_cbor(&self, cid: &Cid) -> Result<Ipld> {
let data = self
.get_block(cid)
.ok_or_else(|| AtmosError::block_not_found(cid))?;
let ipld: Ipld = dagcbor::from_slice(data)?;
Ok(ipld)
}
pub fn decode_json(&self, cid: &Cid) -> Result<Ipld> {
let data = self
.get_block(cid)
.ok_or_else(|| AtmosError::block_not_found(cid))?;
let ipld: Ipld = dagjson::from_slice(data)?;
Ok(ipld)
}
pub async fn export_to_writer<W: AsyncWrite + Unpin + Send>(&self, writer: W) -> Result<()> {
let header = CarHeader::new_v1(self.roots.clone());
let mut car_writer = CarWriter::new(header, writer);
for (cid, data) in &self.blocks {
car_writer.write(*cid, data).await?;
}
car_writer.finish().await?;
Ok(())
}
pub async fn export_to_bytes(&self) -> Result<Bytes> {
let mut buffer = Vec::new();
let cursor = Cursor::new(&mut buffer);
self.export_to_writer(cursor).await?;
Ok(Bytes::from(buffer))
}
pub fn add_block(&mut self, cid: Cid, data: Bytes) {
self.blocks.insert(cid, data);
}
pub fn add_root(&mut self, cid: Cid) {
if !self.roots.contains(&cid) {
self.roots.push(cid);
}
}
pub fn remove_block(&mut self, cid: &Cid) -> Option<Bytes> {
self.blocks.remove(cid)
}
pub fn contains(&self, cid: &Cid) -> bool {
self.blocks.contains_key(cid)
}
pub fn len(&self) -> usize {
self.blocks.len()
}
pub fn is_empty(&self) -> bool {
self.blocks.is_empty()
}
pub fn clear(&mut self) {
self.blocks.clear();
self.roots.clear();
}
pub fn is_mst_node(&self, cid: &Cid) -> bool {
if let Ok(ipld) = self.decode_cbor(cid) {
if let Ipld::Map(map) = ipld {
return map.contains_key("e")
&& map.contains_key("l")
&& !map.contains_key("$type");
}
}
false
}
pub fn get_mst_nodes(&self) -> Vec<Cid> {
self.cids()
.into_iter()
.filter(|cid| self.is_mst_node(cid))
.collect()
}
pub fn get_data_records(&self) -> Vec<Cid> {
self.cids()
.into_iter()
.filter(|cid| !self.is_mst_node(cid))
.collect()
}
}
impl Default for CarImporter {
fn default() -> Self {
Self::new()
}
}
pub struct CarBuilder {
importer: CarImporter,
}
impl CarBuilder {
pub fn new() -> Self {
Self {
importer: CarImporter::new(),
}
}
pub fn add_cbor(&mut self, ipld: &Ipld) -> Result<Cid> {
let data = dagcbor::to_vec(ipld)?;
let hash = Multihash::wrap(0x12, &sha2::Sha256::digest(&data)).unwrap();
let cid = Cid::new_v1(0x71, hash); self.importer.add_block(cid, Bytes::from(data));
Ok(cid)
}
pub fn add_json(&mut self, ipld: &Ipld) -> Result<Cid> {
let data = dagjson::to_vec(ipld)?;
let hash = Multihash::wrap(0x12, &sha2::Sha256::digest(&data)).unwrap();
let cid = Cid::new_v1(0x0129, hash); self.importer.add_block(cid, Bytes::from(data));
Ok(cid)
}
pub fn add_root(&mut self, cid: Cid) -> &mut Self {
self.importer.add_root(cid);
self
}
pub fn build(self) -> CarImporter {
self.importer
}
}
impl Default for CarBuilder {
fn default() -> Self {
Self::new()
}
}
pub mod utils {
use super::*;
use std::path::Path;
use tokio::fs::File;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
pub async fn load_car_file<P: AsRef<Path>>(path: P) -> Result<CarImporter> {
let mut file = File::open(path).await?;
let mut buffer = Vec::new();
file.read_to_end(&mut buffer).await?;
let mut importer = CarImporter::new();
importer.import_from_bytes(Bytes::from(buffer)).await?;
Ok(importer)
}
pub async fn save_car_file<P: AsRef<Path>>(importer: &CarImporter, path: P) -> Result<()> {
let data = importer.export_to_bytes().await?;
let mut file = File::create(path).await?;
file.write_all(&data).await?;
Ok(())
}
pub fn create_test_ipld() -> Ipld {
use std::collections::BTreeMap;
let mut map = BTreeMap::new();
map.insert("hello".to_string(), Ipld::String("world".to_string()));
map.insert("number".to_string(), Ipld::Integer(42));
map.insert(
"array".to_string(),
Ipld::List(vec![
Ipld::String("item1".to_string()),
Ipld::String("item2".to_string()),
]),
);
Ipld::Map(map)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tokio;
#[tokio::test]
async fn test_car_importer_basic() {
let mut importer = CarImporter::new();
let test_data = Bytes::from("hello world");
let hash = Multihash::wrap(0x12, &sha2::Sha256::digest(&test_data)).unwrap();
let cid = Cid::new_v1(0x71, hash);
importer.add_block(cid, test_data.clone());
importer.add_root(cid);
assert_eq!(importer.len(), 1);
assert!(importer.contains(&cid));
assert_eq!(importer.get_block(&cid), Some(&test_data));
assert_eq!(importer.roots(), &[cid]);
}
#[tokio::test]
async fn test_car_builder_cbor() -> Result<()> {
let mut builder = CarBuilder::new();
let test_ipld = utils::create_test_ipld();
let cid = builder.add_cbor(&test_ipld)?;
builder.add_root(cid);
let importer = builder.build();
let decoded = importer.decode_cbor(&cid)?;
assert_eq!(decoded, test_ipld);
Ok(())
}
#[tokio::test]
async fn test_export_import_roundtrip() -> Result<()> {
let mut builder = CarBuilder::new();
let test_ipld = utils::create_test_ipld();
let cid = builder.add_cbor(&test_ipld)?;
builder.add_root(cid);
let original = builder.build();
let exported_bytes = original.export_to_bytes().await?;
let mut imported = CarImporter::new();
imported.import_from_bytes(exported_bytes).await?;
assert_eq!(imported.len(), original.len());
assert_eq!(imported.roots(), original.roots());
let decoded = imported.decode_cbor(&cid)?;
assert_eq!(decoded, test_ipld);
Ok(())
}
#[tokio::test]
async fn test_multiple_blocks() -> Result<()> {
let mut builder = CarBuilder::new();
let ipld1 = utils::create_test_ipld();
let ipld2 = Ipld::String("another test".to_string());
let cid1 = builder.add_cbor(&ipld1)?;
let cid2 = builder.add_cbor(&ipld2)?;
builder.add_root(cid1);
builder.add_root(cid2);
let importer = builder.build();
assert_eq!(importer.len(), 2);
assert_eq!(importer.roots().len(), 2);
let decoded1 = importer.decode_cbor(&cid1)?;
let decoded2 = importer.decode_cbor(&cid2)?;
assert_eq!(decoded1, ipld1);
assert_eq!(decoded2, ipld2);
Ok(())
}
}