use crate::error::Error;
use crate::path::{IpfsPath, PathRoot, SlashedPath};
use crate::repo::Repo;
use crate::{Block, Ipfs};
use futures::future::BoxFuture;
use futures::FutureExt;
use libipld::serde::{from_ipld, to_ipld};
use libipld::{
cid::{
multihash::{Code, MultihashDigest},
Cid, Version,
},
codec::Codec,
Ipld, IpldCodec,
};
use libp2p::PeerId;
use rust_unixfs::{
dagpb::{wrap_node_data, NodeData},
dir::{Cache, ShardedLookup},
resolve, MaybeResolved,
};
use serde::de::DeserializeOwned;
use std::convert::TryFrom;
use std::error::Error as StdError;
use std::iter::Peekable;
use std::marker::PhantomData;
use std::time::Duration;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ResolveError {
#[error("block loading failed")]
Loading(Cid, #[source] crate::Error),
#[error("unsupported document")]
UnsupportedDocument(Cid, #[source] Box<dyn StdError + Send + Sync + 'static>),
#[error("list index out of range 0..{elements}: {index}")]
ListIndexOutOfRange {
document: Cid,
path: SlashedPath,
index: usize,
elements: usize,
},
#[error("tried to resolve through an object that had no links")]
NoLinks(Cid, SlashedPath),
#[error("no link named {:?} under {0}", .1.iter().last().unwrap())]
NotFound(Cid, SlashedPath),
#[error("the path neiter contains nor resolves to a Cid")]
NoCid(IpfsPath),
#[error("can't resolve an IPNS path")]
IpnsResolutionFailed(IpfsPath),
#[error("path is not provided or is invalid")]
PathNotProvided,
}
#[derive(Debug, Error)]
pub enum UnexpectedResolved {
#[error("path resolved to unexpected type of document: {:?} or {}", .0, .1.source())]
UnexpectedCodec(u64, ResolvedNode),
#[error("path did not resolve to a block on {}", .0.source())]
NonBlock(ResolvedNode),
}
#[derive(Debug)]
enum RawResolveLocalError {
Loading(Cid, crate::Error),
UnsupportedDocument(Cid, Box<dyn StdError + Send + Sync + 'static>),
ListIndexOutOfRange {
document: Cid,
segment_index: usize,
index: usize,
elements: usize,
},
InvalidIndex {
document: Cid,
segment_index: usize,
},
NoLinks {
document: Cid,
segment_index: usize,
},
NotFound {
document: Cid,
segment_index: usize,
},
}
impl RawResolveLocalError {
fn add_starting_point_in_path(&mut self, start: usize) {
use RawResolveLocalError::*;
match self {
ListIndexOutOfRange {
ref mut segment_index,
..
}
| InvalidIndex {
ref mut segment_index,
..
}
| NoLinks {
ref mut segment_index,
..
}
| NotFound {
ref mut segment_index,
..
} => {
*segment_index += start;
}
_ => {}
}
}
fn with_path(self, path: IpfsPath) -> ResolveError {
use RawResolveLocalError::*;
match self {
Loading(cid, e) => ResolveError::Loading(cid, e),
UnsupportedDocument(cid, e) => ResolveError::UnsupportedDocument(cid, e),
ListIndexOutOfRange {
document,
segment_index,
index,
elements,
} => ResolveError::ListIndexOutOfRange {
document,
path: path.into_truncated(segment_index + 1),
index,
elements,
},
NoLinks {
document,
segment_index,
} => ResolveError::NoLinks(document, path.into_truncated(segment_index + 1)),
InvalidIndex {
document,
segment_index,
}
| NotFound {
document,
segment_index,
} => ResolveError::NotFound(document, path.into_truncated(segment_index + 1)),
}
}
}
#[derive(Clone, Debug)]
pub struct IpldDag {
ipfs: Option<Ipfs>,
repo: Repo,
}
impl From<Repo> for IpldDag {
fn from(repo: Repo) -> Self {
IpldDag { ipfs: None, repo }
}
}
impl IpldDag {
pub fn new(ipfs: Ipfs) -> Self {
let repo = ipfs.repo().clone();
IpldDag {
ipfs: Some(ipfs),
repo,
}
}
pub fn put_dag(&self, ipld: Ipld) -> DagPut {
self.put().ipld(ipld)
}
pub fn get_dag(&self, path: IpfsPath) -> DagGet {
self.get().path(path)
}
pub fn put(&self) -> DagPut {
DagPut::new(self.clone())
}
pub fn get(&self) -> DagGet {
DagGet::new(self.clone())
}
pub(crate) async fn get_with_session(
&self,
session: Option<u64>,
path: IpfsPath,
providers: &[PeerId],
local_only: bool,
timeout: Option<Duration>,
) -> Result<Ipld, ResolveError> {
let resolved_path = match &self.ipfs {
Some(ipfs) => ipfs
.resolve_ipns(&path, true)
.await
.map_err(|_| ResolveError::IpnsResolutionFailed(path))?,
None => {
if !matches!(path.root(), PathRoot::Ipld(_)) {
return Err(ResolveError::IpnsResolutionFailed(path));
}
path
}
};
let cid = match resolved_path.root().cid() {
Some(cid) => cid,
None => return Err(ResolveError::NoCid(resolved_path)),
};
let mut iter = resolved_path.iter().peekable();
let (node, _) = match self
.resolve0(
session, cid, &mut iter, true, providers, local_only, timeout,
)
.await
{
Ok(t) => t,
Err(e) => {
drop(iter);
return Err(e.with_path(resolved_path));
}
};
Ipld::try_from(node)
}
pub async fn resolve(
&self,
path: IpfsPath,
follow_links: bool,
providers: &[PeerId],
local_only: bool,
) -> Result<(ResolvedNode, SlashedPath), ResolveError> {
self.resolve_with_session(None, path, follow_links, providers, local_only, None)
.await
}
pub(crate) async fn resolve_with_session(
&self,
session: Option<u64>,
path: IpfsPath,
follow_links: bool,
providers: &[PeerId],
local_only: bool,
timeout: Option<Duration>,
) -> Result<(ResolvedNode, SlashedPath), ResolveError> {
let resolved_path = match &self.ipfs {
Some(ipfs) => ipfs
.resolve_ipns(&path, true)
.await
.map_err(|_| ResolveError::IpnsResolutionFailed(path))?,
None => {
if !matches!(path.root(), PathRoot::Ipld(_)) {
return Err(ResolveError::IpnsResolutionFailed(path));
}
path
}
};
let cid = match resolved_path.root().cid() {
Some(cid) => cid,
None => return Err(ResolveError::NoCid(resolved_path)),
};
let (node, matched_segments) = {
let mut iter = resolved_path.iter().peekable();
match self
.resolve0(
session,
cid,
&mut iter,
follow_links,
providers,
local_only,
timeout,
)
.await
{
Ok(t) => t,
Err(e) => {
drop(iter);
return Err(e.with_path(resolved_path));
}
}
};
let remaining_path = resolved_path.into_shifted(matched_segments);
Ok((node, remaining_path))
}
#[allow(clippy::too_many_arguments)]
async fn resolve0<'a>(
&self,
session: Option<u64>,
cid: &Cid,
segments: &mut Peekable<impl Iterator<Item = &'a str>>,
follow_links: bool,
providers: &[PeerId],
local_only: bool,
timeout: Option<Duration>,
) -> Result<(ResolvedNode, usize), RawResolveLocalError> {
use LocallyResolved::*;
let mut current = *cid;
let mut total = 0;
let mut cache = None;
loop {
let block = match self
.repo
.get_block_with_session(session, ¤t, providers, local_only, timeout)
.await
{
Ok(block) => block,
Err(e) => return Err(RawResolveLocalError::Loading(current, e)),
};
let start = total;
let (resolution, matched) = match resolve_local(block, segments, &mut cache) {
Ok(t) => t,
Err(mut e) => {
e.add_starting_point_in_path(start);
return Err(e);
}
};
total += matched;
let (src, dest) = match resolution {
Complete(ResolvedNode::Link(src, dest)) => (src, dest),
Incomplete(src, lookup) => match self
.resolve_hamt(lookup, &mut cache, providers, local_only)
.await
{
Ok(dest) => (src, dest),
Err(e) => return Err(RawResolveLocalError::UnsupportedDocument(src, e.into())),
},
Complete(other) => {
return Ok((other, start));
}
};
if !follow_links {
return Ok((ResolvedNode::Link(src, dest), total));
} else {
current = dest;
}
}
}
async fn resolve_hamt(
&self,
mut lookup: ShardedLookup<'_>,
cache: &mut Option<Cache>,
providers: &[PeerId],
local_only: bool,
) -> Result<Cid, Error> {
use MaybeResolved::*;
loop {
let (next, _) = lookup.pending_links();
let block = self.repo.get_block(next, providers, local_only).await?;
match lookup.continue_walk(block.data(), cache)? {
NeedToLoadMore(next) => lookup = next,
Found(cid) => return Ok(cid),
NotFound => return Err(anyhow::anyhow!("key not found: ???")),
}
}
}
}
pub struct DagGet {
dag_ipld: IpldDag,
session: Option<u64>,
path: Option<IpfsPath>,
providers: Vec<PeerId>,
local: bool,
timeout: Option<Duration>,
}
impl DagGet {
pub fn new(dag: IpldDag) -> Self {
Self {
dag_ipld: dag,
session: None,
path: None,
providers: vec![],
local: false,
timeout: None,
}
}
#[allow(dead_code)]
pub(crate) fn session(mut self, session: u64) -> Self {
self.session = Some(session);
self
}
pub fn path<P: Into<IpfsPath>>(mut self, path: P) -> Self {
let path = path.into();
self.path = Some(path);
self
}
pub fn provider(mut self, peer_id: PeerId) -> Self {
self.providers.push(peer_id);
self
}
pub fn providers(mut self, providers: &[PeerId]) -> Self {
self.providers = providers.to_vec();
self
}
pub fn local(mut self) -> Self {
self.local = true;
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub fn deserialized<D: DeserializeOwned>(self) -> DagGetDeserialize<D> {
DagGetDeserialize {
dag_get: self,
_marker: PhantomData,
}
}
}
impl std::future::IntoFuture for DagGet {
type Output = Result<Ipld, ResolveError>;
type IntoFuture = BoxFuture<'static, Self::Output>;
fn into_future(self) -> Self::IntoFuture {
async move {
let path = self.path.ok_or(ResolveError::PathNotProvided)?;
self.dag_ipld
.get_with_session(
self.session,
path,
&self.providers,
self.local,
self.timeout,
)
.await
}
.boxed()
}
}
pub struct DagGetDeserialize<D> {
dag_get: DagGet,
_marker: PhantomData<D>,
}
impl<D> std::future::IntoFuture for DagGetDeserialize<D>
where
D: DeserializeOwned,
{
type Output = Result<D, anyhow::Error>;
type IntoFuture = BoxFuture<'static, Self::Output>;
fn into_future(self) -> Self::IntoFuture {
let fut = self.dag_get.into_future();
async move {
let document = fut.await?;
let data = from_ipld(document)?;
Ok(data)
}
.boxed()
}
}
pub struct DagPut {
dag_ipld: IpldDag,
codec: IpldCodec,
data: Option<Ipld>,
hash: Option<Code>,
pinned: Option<bool>,
provide: bool,
}
impl DagPut {
pub fn new(dag: IpldDag) -> Self {
Self {
dag_ipld: dag,
codec: IpldCodec::DagCbor,
data: None,
hash: None,
pinned: None,
provide: false,
}
}
pub fn ipld(mut self, data: Ipld) -> Self {
self.data = Some(data);
self
}
pub fn serialize<S: serde::Serialize>(mut self, data: S) -> Result<Self, Error> {
let data = to_ipld(data)?;
self.data = Some(data);
Ok(self)
}
pub fn pin(mut self, recursive: bool) -> Self {
self.pinned = Some(recursive);
self
}
pub fn provide(mut self) -> Self {
self.provide = true;
self
}
pub fn hash(mut self, code: Code) -> Self {
self.hash = Some(code);
self
}
pub fn codec(mut self, codec: IpldCodec) -> Self {
self.codec = codec;
self
}
}
impl std::future::IntoFuture for DagPut {
type Output = Result<Cid, anyhow::Error>;
type IntoFuture = BoxFuture<'static, Self::Output>;
fn into_future(self) -> Self::IntoFuture {
async move {
if self.provide && self.dag_ipld.ipfs.is_none() {
anyhow::bail!("Ipfs is offline");
}
let data = self.data.ok_or(anyhow::anyhow!("Ipld was not provided"))?;
let bytes = self.codec.encode(&data)?;
let code = self.hash.unwrap_or(Code::Sha2_256);
let hash = code.digest(&bytes);
let version = if self.codec == IpldCodec::DagPb {
Version::V0
} else {
Version::V1
};
let cid = Cid::new(version, self.codec.into(), hash)?;
let block = Block::new(cid, bytes)?;
let (cid, _) = self.dag_ipld.repo.put_block(block).await?;
if let Some(opt) = self.pinned {
if !self.dag_ipld.repo.is_pinned(&cid).await? {
self.dag_ipld.repo.insert_pin(&cid, opt, true).await?;
}
}
if self.provide {
if let Some(ipfs) = &self.dag_ipld.ipfs {
if let Err(e) = ipfs.provide(cid).await {
error!("Failed to provide content over DHT: {e}")
}
}
}
Ok(cid)
}
.boxed()
}
}
#[derive(Debug, PartialEq)]
pub enum ResolvedNode {
Block(Block),
DagPbData(Cid, NodeData<Box<[u8]>>),
Projection(Cid, Ipld),
Link(Cid, Cid),
}
impl ResolvedNode {
pub fn source(&self) -> &Cid {
match self {
ResolvedNode::Block(block) => block.cid(),
ResolvedNode::DagPbData(cid, ..)
| ResolvedNode::Projection(cid, ..)
| ResolvedNode::Link(cid, ..) => cid,
}
}
pub fn into_unixfs_block(self) -> Result<Block, UnexpectedResolved> {
if self.source().codec() != <IpldCodec as Into<u64>>::into(IpldCodec::DagPb) {
Err(UnexpectedResolved::UnexpectedCodec(
IpldCodec::DagPb.into(),
self,
))
} else {
match self {
ResolvedNode::Block(b) => Ok(b),
_ => Err(UnexpectedResolved::NonBlock(self)),
}
}
}
}
impl TryFrom<ResolvedNode> for Ipld {
type Error = ResolveError;
fn try_from(r: ResolvedNode) -> Result<Ipld, Self::Error> {
use ResolvedNode::*;
match r {
Block(block) => Ok(block
.decode::<IpldCodec, Ipld>()
.map_err(move |e| ResolveError::UnsupportedDocument(*block.cid(), e.into()))?),
DagPbData(_, node_data) => Ok(Ipld::Bytes(node_data.node_data().to_vec())),
Projection(_, ipld) => Ok(ipld),
Link(_, cid) => Ok(Ipld::Link(cid)),
}
}
}
#[derive(Debug)]
enum LocallyResolved<'a> {
Complete(ResolvedNode),
Incomplete(Cid, ShardedLookup<'a>),
}
#[cfg(test)]
impl LocallyResolved<'_> {
fn unwrap_complete(self) -> ResolvedNode {
match self {
LocallyResolved::Complete(rn) => rn,
x => unreachable!("{:?}", x),
}
}
}
impl From<ResolvedNode> for LocallyResolved<'static> {
fn from(r: ResolvedNode) -> LocallyResolved<'static> {
LocallyResolved::Complete(r)
}
}
fn resolve_local<'a>(
block: Block,
segments: &mut Peekable<impl Iterator<Item = &'a str>>,
cache: &mut Option<Cache>,
) -> Result<(LocallyResolved<'a>, usize), RawResolveLocalError> {
if segments.peek().is_none() {
return Ok((LocallyResolved::Complete(ResolvedNode::Block(block)), 0));
}
if block.cid().codec() == <IpldCodec as Into<u64>>::into(IpldCodec::DagPb) {
let segment = segments.next().unwrap();
let (cid, data) = block.into_inner();
Ok(resolve_local_dagpb(
cid,
data.into(),
segment,
segments.peek().is_none(),
cache,
)?)
} else {
let ipld = match block.decode::<IpldCodec, Ipld>() {
Ok(ipld) => ipld,
Err(e) => {
return Err(RawResolveLocalError::UnsupportedDocument(
*block.cid(),
e.into(),
))
}
};
resolve_local_ipld(*block.cid(), ipld, segments)
}
}
fn resolve_local_dagpb<'a>(
cid: Cid,
data: Box<[u8]>,
segment: &'a str,
is_last: bool,
cache: &mut Option<Cache>,
) -> Result<(LocallyResolved<'a>, usize), RawResolveLocalError> {
match resolve(&data, segment, cache) {
Ok(MaybeResolved::NeedToLoadMore(lookup)) => {
Ok((LocallyResolved::Incomplete(cid, lookup), 0))
}
Ok(MaybeResolved::Found(dest)) => {
Ok((LocallyResolved::Complete(ResolvedNode::Link(cid, dest)), 1))
}
Ok(MaybeResolved::NotFound) => {
if segment == "Data" && is_last {
let wrapped = wrap_node_data(data).expect("already deserialized once");
return Ok((
LocallyResolved::Complete(ResolvedNode::DagPbData(cid, wrapped)),
1,
));
}
Err(RawResolveLocalError::NotFound {
document: cid,
segment_index: 0,
})
}
Err(rust_unixfs::ResolveError::UnexpectedType(ut)) if ut.is_file() => {
Err(RawResolveLocalError::NotFound {
document: cid,
segment_index: 0,
})
}
Err(e) => Err(RawResolveLocalError::UnsupportedDocument(cid, e.into())),
}
}
fn resolve_local_ipld<'a>(
document: Cid,
mut ipld: Ipld,
segments: &mut Peekable<impl Iterator<Item = &'a str>>,
) -> Result<(LocallyResolved<'a>, usize), RawResolveLocalError> {
let mut matched_count = 0;
loop {
ipld = match ipld {
Ipld::Link(cid) => {
if segments.peek() != Some(&".") {
return Ok((ResolvedNode::Link(document, cid).into(), matched_count));
} else {
Ipld::Link(cid)
}
}
ipld => ipld,
};
ipld = match (ipld, segments.next()) {
(Ipld::Link(cid), Some(".")) => {
return Ok((ResolvedNode::Link(document, cid).into(), matched_count + 1));
}
(Ipld::Link(_), Some(_)) => {
unreachable!("case already handled above before advancing the iterator")
}
(Ipld::Map(mut map), Some(segment)) => {
let found = match map.remove(segment) {
Some(f) => f,
None => {
return Err(RawResolveLocalError::NotFound {
document,
segment_index: matched_count,
})
}
};
matched_count += 1;
found
}
(Ipld::List(mut vec), Some(segment)) => match segment.parse::<usize>() {
Ok(index) if index < vec.len() => {
matched_count += 1;
vec.swap_remove(index)
}
Ok(index) => {
return Err(RawResolveLocalError::ListIndexOutOfRange {
document,
segment_index: matched_count,
index,
elements: vec.len(),
});
}
Err(_) => {
return Err(RawResolveLocalError::InvalidIndex {
document,
segment_index: matched_count,
})
}
},
(_, Some(_)) => {
return Err(RawResolveLocalError::NoLinks {
document,
segment_index: matched_count,
});
}
(anything, None) => {
return Ok((
ResolvedNode::Projection(document, anything).into(),
matched_count,
))
}
};
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Node;
use libipld::{cbor::DagCborCodec, ipld};
#[tokio::test]
async fn test_resolve_root_cid() {
let Node { ipfs, .. } = Node::new("test_node").await;
let dag = IpldDag::new(ipfs);
let data = ipld!([1, 2, 3]);
let cid = dag.put_dag(data.clone()).await.unwrap();
let res = dag.get_dag(IpfsPath::from(cid)).await.unwrap();
assert_eq!(res, data);
}
#[tokio::test]
async fn test_resolve_array_elem() {
let Node { ipfs, .. } = Node::new("test_node").await;
let dag = IpldDag::new(ipfs);
let data = ipld!([1, 2, 3]);
let cid = dag.put_dag(data.clone()).await.unwrap();
let res = dag
.get_dag(IpfsPath::from(cid).sub_path("1").unwrap())
.await
.unwrap();
assert_eq!(res, ipld!(2));
}
#[tokio::test]
async fn test_resolve_nested_array_elem() {
let Node { ipfs, .. } = Node::new("test_node").await;
let dag = IpldDag::new(ipfs);
let data = ipld!([1, [2], 3,]);
let cid = dag.put_dag(data).await.unwrap();
let res = dag
.get_dag(IpfsPath::from(cid).sub_path("1/0").unwrap())
.await
.unwrap();
assert_eq!(res, ipld!(2));
}
#[tokio::test]
async fn test_resolve_object_elem() {
let Node { ipfs, .. } = Node::new("test_node").await;
let dag = IpldDag::new(ipfs);
let data = ipld!({
"key": false,
});
let cid = dag.put_dag(data).await.unwrap();
let res = dag
.get_dag(IpfsPath::from(cid).sub_path("key").unwrap())
.await
.unwrap();
assert_eq!(res, ipld!(false));
}
#[tokio::test]
async fn test_resolve_cid_elem() {
let Node { ipfs, .. } = Node::new("test_node").await;
let dag = IpldDag::new(ipfs);
let data1 = ipld!([1]);
let cid1 = dag.put_dag(data1).await.unwrap();
let data2 = ipld!([cid1]);
let cid2 = dag.put_dag(data2).await.unwrap();
let res = dag
.get_dag(IpfsPath::from(cid2).sub_path("0/0").unwrap())
.await
.unwrap();
assert_eq!(res, ipld!(1));
}
fn example_doc_and_cid() -> (Cid, Ipld, Cid) {
let cid = Cid::try_from("QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n").unwrap();
let doc = ipld!({
"nested": {
"even": [
{
"more": 5
},
{
"or": "this",
},
{
"or": cid,
},
{
"5": "or",
}
],
}
});
let root =
Cid::try_from("bafyreielwgy762ox5ndmhx6kpi6go6il3gzahz3ngagb7xw3bj3aazeita").unwrap();
(root, doc, cid)
}
#[test]
fn resolve_cbor_locally_to_end() {
let (root, example_doc, _) = example_doc_and_cid();
let good_examples = [
(
"bafyreielwgy762ox5ndmhx6kpi6go6il3gzahz3ngagb7xw3bj3aazeita/nested/even/0/more",
Ipld::Integer(5),
),
(
"bafyreielwgy762ox5ndmhx6kpi6go6il3gzahz3ngagb7xw3bj3aazeita/nested/even/1/or",
Ipld::from("this"),
),
(
"bafyreielwgy762ox5ndmhx6kpi6go6il3gzahz3ngagb7xw3bj3aazeita/nested/even/3/5",
Ipld::from("or"),
),
];
for (path, expected) in &good_examples {
let p = IpfsPath::try_from(*path).unwrap();
let (resolved, matched_segments) =
super::resolve_local_ipld(root, example_doc.clone(), &mut p.iter().peekable())
.unwrap();
assert_eq!(matched_segments, 4);
match resolved.unwrap_complete() {
ResolvedNode::Projection(_, p) if &p == expected => {}
x => unreachable!("unexpected {:?}", x),
}
let remaining_path = p.iter().skip(matched_segments).collect::<Vec<&str>>();
assert!(remaining_path.is_empty(), "{remaining_path:?}");
}
}
#[test]
fn resolve_cbor_locally_to_link() {
let (root, example_doc, target) = example_doc_and_cid();
let p = IpfsPath::try_from(
"bafyreielwgy762ox5ndmhx6kpi6go6il3gzahz3ngagb7xw3bj3aazeita/nested/even/2/or/foobar/trailer"
).unwrap();
let (resolved, matched_segments) =
super::resolve_local_ipld(root, example_doc, &mut p.iter().peekable()).unwrap();
match resolved.unwrap_complete() {
ResolvedNode::Link(_, cid) if cid == target => {}
x => unreachable!("{:?}", x),
}
assert_eq!(matched_segments, 4);
let remaining_path = p.iter().skip(matched_segments).collect::<Vec<&str>>();
assert_eq!(remaining_path, &["foobar", "trailer"]);
}
#[test]
fn resolve_cbor_locally_to_link_with_dot() {
let (root, example_doc, cid) = example_doc_and_cid();
let p = IpfsPath::try_from(
"bafyreielwgy762ox5ndmhx6kpi6go6il3gzahz3ngagb7xw3bj3aazeita/nested/even/2/or/./foobar/trailer",
)
.unwrap();
let (resolved, matched_segments) =
super::resolve_local_ipld(root, example_doc, &mut p.iter().peekable()).unwrap();
assert_eq!(resolved.unwrap_complete(), ResolvedNode::Link(root, cid));
assert_eq!(matched_segments, 5);
let remaining_path = p.iter().skip(matched_segments).collect::<Vec<&str>>();
assert_eq!(remaining_path, &["foobar", "trailer"]);
}
#[test]
fn resolve_cbor_locally_not_found_map_key() {
let (root, example_doc, _) = example_doc_and_cid();
let p = IpfsPath::try_from(
"bafyreielwgy762ox5ndmhx6kpi6go6il3gzahz3ngagb7xw3bj3aazeita/foobar/trailer",
)
.unwrap();
let e = super::resolve_local_ipld(root, example_doc, &mut p.iter().peekable()).unwrap_err();
assert!(
matches!(
e,
RawResolveLocalError::NotFound {
segment_index: 0,
..
}
),
"{e:?}"
);
}
#[test]
fn resolve_cbor_locally_too_large_list_index() {
let (root, example_doc, _) = example_doc_and_cid();
let p = IpfsPath::try_from(
"bafyreielwgy762ox5ndmhx6kpi6go6il3gzahz3ngagb7xw3bj3aazeita/nested/even/3000",
)
.unwrap();
let e = super::resolve_local_ipld(root, example_doc, &mut p.iter().peekable()).unwrap_err();
assert!(
matches!(
e,
RawResolveLocalError::ListIndexOutOfRange {
segment_index: 2,
index: 3000,
elements: 4,
..
}
),
"{e:?}"
);
}
#[test]
fn resolve_cbor_locally_non_usize_index() {
let (root, example_doc, _) = example_doc_and_cid();
let p = IpfsPath::try_from(
"bafyreielwgy762ox5ndmhx6kpi6go6il3gzahz3ngagb7xw3bj3aazeita/nested/even/-1",
)
.unwrap();
let e = super::resolve_local_ipld(root, example_doc, &mut p.iter().peekable()).unwrap_err();
assert!(
matches!(
e,
RawResolveLocalError::InvalidIndex {
segment_index: 2,
..
}
),
"{e:?}"
);
}
#[tokio::test]
async fn resolve_through_link() {
let Node { ipfs, .. } = Node::new("test_node").await;
let dag = IpldDag::new(ipfs);
let ipld = ipld!([1]);
let cid1 = dag.put_dag(ipld).await.unwrap();
let ipld = ipld!([cid1]);
let cid2 = dag.put_dag(ipld).await.unwrap();
let prefix = IpfsPath::from(cid2);
let equiv_paths = vec![
prefix.sub_path("0/0").unwrap(),
prefix.sub_path("0/./0").unwrap(),
];
for p in equiv_paths {
let cloned = p.clone();
match dag.resolve(p, true, &[], false).await.unwrap() {
(ResolvedNode::Projection(_, Ipld::Integer(1)), remaining_path) => {
assert_eq!(remaining_path, ["0"][..], "{cloned}");
}
x => unreachable!("{:?}", x),
}
}
}
#[tokio::test]
async fn fail_resolving_first_segment() {
let Node { ipfs, .. } = Node::new("test_node").await;
let dag = IpldDag::new(ipfs);
let ipld = ipld!([1]);
let cid1 = dag.put_dag(ipld).await.unwrap();
let ipld = ipld!({ "0": cid1 });
let cid2 = dag.put_dag(ipld).await.unwrap();
let path = IpfsPath::from(cid2).sub_path("1/a").unwrap();
let e = dag.resolve(path, true, &[], false).await.unwrap_err();
assert_eq!(e.to_string(), format!("no link named \"1\" under {cid2}"));
}
#[tokio::test]
async fn fail_resolving_last_segment() {
let Node { ipfs, .. } = Node::new("test_node").await;
let dag = IpldDag::new(ipfs);
let ipld = ipld!([1]);
let cid1 = dag.put_dag(ipld).await.unwrap();
let ipld = ipld!([cid1]);
let cid2 = dag.put_dag(ipld).await.unwrap();
let path = IpfsPath::from(cid2).sub_path("0/a").unwrap();
let e = dag.resolve(path, true, &[], false).await.unwrap_err();
assert_eq!(e.to_string(), format!("no link named \"a\" under {cid1}"));
}
#[tokio::test]
async fn fail_resolving_through_file() {
let Node { ipfs, .. } = Node::new("test_node").await;
let mut adder = rust_unixfs::file::adder::FileAdder::default();
let (mut blocks, _) = adder.push(b"foobar\n");
assert_eq!(blocks.next(), None);
let mut blocks = adder.finish();
let (cid, data) = blocks.next().unwrap();
assert_eq!(blocks.next(), None);
ipfs.put_block(Block::new(cid, data).unwrap())
.await
.unwrap();
let path = IpfsPath::from(cid).sub_path("anything-here").unwrap();
let e = ipfs
.dag()
.resolve(path, true, &[], false)
.await
.unwrap_err();
assert_eq!(
e.to_string(),
format!("no link named \"anything-here\" under {cid}")
);
}
#[tokio::test]
async fn fail_resolving_through_dir() {
let Node { ipfs, .. } = Node::new("test_node").await;
let mut adder = rust_unixfs::file::adder::FileAdder::default();
let (mut blocks, _) = adder.push(b"foobar\n");
assert_eq!(blocks.next(), None);
let mut blocks = adder.finish();
let (cid, data) = blocks.next().unwrap();
assert_eq!(blocks.next(), None);
let total_size = data.len();
ipfs.put_block(Block::new(cid, data).unwrap())
.await
.unwrap();
let mut opts = rust_unixfs::dir::builder::TreeOptions::default();
opts.wrap_with_directory();
let mut tree = rust_unixfs::dir::builder::BufferingTreeBuilder::new(opts);
tree.put_link("something/best-file-in-the-world", cid, total_size as u64)
.unwrap();
let mut iter = tree.build();
let mut cids = Vec::new();
while let Some(node) = iter.next_borrowed() {
let node = node.unwrap();
let block = Block::new(node.cid.to_owned(), node.block.into()).unwrap();
ipfs.put_block(block).await.unwrap();
cids.push(node.cid.to_owned());
}
cids.reverse();
let path = IpfsPath::from(cids[0].to_owned())
.sub_path("something/second-best-file")
.unwrap();
let e = ipfs
.dag()
.resolve(path, true, &[], false)
.await
.unwrap_err();
assert_eq!(
e.to_string(),
format!("no link named \"second-best-file\" under {}", cids[1])
);
}
#[test]
fn observes_strict_order_of_map_keys() {
let map = ipld!({
"omega": Ipld::Null,
"bar": Ipld::Null,
"alpha": Ipld::Null,
"foo": Ipld::Null,
});
let bytes = DagCborCodec.encode(&map).unwrap();
assert_eq!(
bytes.as_slice(),
&[
164, 99, 98, 97, 114, 246, 99, 102, 111, 111, 246, 101, 97, 108, 112, 104, 97, 246,
101, 111, 109, 101, 103, 97, 246
]
);
}
}