Skip to main content

rust_ipfs/
dag.rs

1//! `ipfs.dag` interface implementation around [`Ipfs`].
2
3use crate::block::BlockCodec;
4use crate::error::Error;
5use crate::path::{IpfsPath, PathRoot, SlashedPath};
6use crate::repo::DefaultStorage;
7use crate::repo::Repo;
8use crate::{Block, Ipfs};
9use bytes::Bytes;
10use connexa::prelude::PeerId;
11use futures::FutureExt;
12use futures::future::BoxFuture;
13use ipld_core::cid::{Cid, Version};
14use ipld_core::codec::Codec;
15use ipld_core::ipld::Ipld;
16use ipld_core::serde::{from_ipld, to_ipld};
17use multihash_codetable::{Code, MultihashDigest};
18use rust_unixfs::{
19    MaybeResolved,
20    dagpb::{NodeData, wrap_node_data},
21    dir::{Cache, ShardedLookup},
22    resolve,
23};
24use serde::Serialize;
25use serde::de::DeserializeOwned;
26use std::borrow::Borrow;
27use std::convert::TryFrom;
28use std::error::Error as StdError;
29use std::iter::Peekable;
30use std::marker::PhantomData;
31use std::time::Duration;
32use thiserror::Error;
33use tracing::{Instrument, Span};
34
35pub mod car;
36
37#[derive(Debug, Error)]
38pub enum ResolveError {
39    /// Loading of the block on the path failed
40    #[error("block loading failed")]
41    Loading(Cid, #[source] crate::Error),
42
43    /// The document is unsupported; this can be a UnixFs directory structure which has unsupported
44    /// options, or IPLD parsing failed.
45    #[error("unsupported document")]
46    UnsupportedDocument(Cid, #[source] Box<dyn StdError + Send + Sync + 'static>),
47
48    /// Path contained an index which was out of range for the given [`Ipld::List`].
49    #[error("list index out of range 0..{elements}: {index}")]
50    ListIndexOutOfRange {
51        /// The document with the mismatched index
52        document: Cid,
53        /// The path up until the mismatched index
54        path: SlashedPath,
55        /// The index in original path
56        index: usize,
57        /// Total number of elements found
58        elements: usize,
59    },
60
61    /// Path attempted to resolve through e.g. a string or an integer.
62    #[error("tried to resolve through an object that had no links")]
63    NoLinks(Cid, SlashedPath),
64
65    /// Path attempted to resolve through a property, index or link which did not exist.
66    #[error("no link named {n:?} under {0}", n = .1.iter().last().unwrap())]
67    NotFound(Cid, SlashedPath),
68
69    /// Tried to use a path neiter containing nor resolving to a Cid.
70    #[error("the path neiter contains nor resolves to a Cid")]
71    NoCid(IpfsPath),
72
73    /// Couldn't resolve a path via IPNS.
74    #[error("can't resolve an IPNS path")]
75    IpnsResolutionFailed(IpfsPath),
76
77    #[error("path is not provided or is invalid")]
78    PathNotProvided,
79}
80
81#[derive(Debug, Error)]
82pub enum UnexpectedResolved {
83    #[error("path resolved to unexpected type of document: {:?} or {}", .0, .1.source())]
84    UnexpectedCodec(u64, Box<ResolvedNode>),
85    #[error("path did not resolve to a block on {}", .0.source())]
86    NonBlock(Box<ResolvedNode>),
87}
88
89/// Used internally before translating to ResolveError at the top level by using the IpfsPath.
90#[derive(Debug)]
91enum RawResolveLocalError {
92    Loading(Cid, crate::Error),
93    UnsupportedDocument(Cid, Box<dyn StdError + Send + Sync + 'static>),
94    ListIndexOutOfRange {
95        document: Cid,
96        segment_index: usize,
97        index: usize,
98        elements: usize,
99    },
100    InvalidIndex {
101        document: Cid,
102        segment_index: usize,
103    },
104    NoLinks {
105        document: Cid,
106        segment_index: usize,
107    },
108    NotFound {
109        document: Cid,
110        segment_index: usize,
111    },
112}
113
114impl RawResolveLocalError {
115    /// When resolving through multiple documents the local resolving functions `resolve_local_ipld`
116    /// and `resolve_local_dagpb` return local document indices; need to bump the indices with the
117    /// number of the already matched segments in the previous documents for the path.
118    fn add_starting_point_in_path(&mut self, start: usize) {
119        use RawResolveLocalError::*;
120        match self {
121            ListIndexOutOfRange { segment_index, .. }
122            | InvalidIndex { segment_index, .. }
123            | NoLinks { segment_index, .. }
124            | NotFound { segment_index, .. } => {
125                // NOTE: this is the **index** compared to the number of segments matched, i.e. **count**
126                // from `resolve_local`'s Ok return value.
127                *segment_index += start;
128            }
129            _ => {}
130        }
131    }
132
133    /// Use the given [`IpfsPath`] to create the truncated [`SlashedPath`] and convert into
134    /// [`ResolveError`]. The path is truncated so that the last segment is the one which failed to
135    /// match. No reason it couldn't also be signified with just an index.
136    fn with_path(self, path: IpfsPath) -> ResolveError {
137        use RawResolveLocalError::*;
138
139        match self {
140            // FIXME: I'd like to use Result<Result<_, ResolveError>, crate::Error> instead
141            Loading(cid, e) => ResolveError::Loading(cid, e),
142            UnsupportedDocument(cid, e) => ResolveError::UnsupportedDocument(cid, e),
143            ListIndexOutOfRange {
144                document,
145                segment_index,
146                index,
147                elements,
148            } => ResolveError::ListIndexOutOfRange {
149                document,
150                path: path.into_truncated(segment_index + 1),
151                index,
152                elements,
153            },
154            NoLinks {
155                document,
156                segment_index,
157            } => ResolveError::NoLinks(document, path.into_truncated(segment_index + 1)),
158            InvalidIndex {
159                document,
160                segment_index,
161            }
162            | NotFound {
163                document,
164                segment_index,
165            } => ResolveError::NotFound(document, path.into_truncated(segment_index + 1)),
166        }
167    }
168}
169
170/// `ipfs.dag` interface providing wrapper around Ipfs.
171#[derive(Clone, Debug)]
172pub struct IpldDag {
173    ipfs: Option<Ipfs>,
174    repo: Repo<DefaultStorage>,
175}
176
177impl From<Repo<DefaultStorage>> for IpldDag {
178    fn from(repo: Repo<DefaultStorage>) -> Self {
179        IpldDag { ipfs: None, repo }
180    }
181}
182
183impl IpldDag {
184    /// Creates a new `IpldDag` for DAG operations.
185    pub fn new(ipfs: Ipfs) -> Self {
186        let repo = ipfs.repo().clone();
187        IpldDag {
188            ipfs: Some(ipfs),
189            repo,
190        }
191    }
192
193    /// Puts an ipld node into the ipfs repo using `dag-cbor` codec and Sha2_256 hash.
194    ///
195    /// Returns Cid version 1 for the document
196    pub fn put_dag(&self, ipld: impl Serialize) -> DagPut {
197        self.put().serialize(ipld)
198    }
199
200    /// Gets an ipld node from the ipfs, fetching the block if necessary.
201    ///
202    /// See [`IpldDag::get`] for more information.
203    pub fn get_dag(&self, path: impl Into<IpfsPath>) -> DagGet {
204        self.get().path(path)
205    }
206
207    /// Returns the `Cid` of a newly inserted block.
208    ///
209    /// The block is created from the `data`, encoded with the `codec` and inserted into the repo.
210    pub fn put(&self) -> DagPut {
211        DagPut::new(self.clone())
212    }
213
214    /// Resolves a `Cid`-rooted path to a document "node."
215    ///
216    /// Returns the resolved node as `Ipld`.
217    pub fn get(&self) -> DagGet {
218        DagGet::new(self.clone())
219    }
220
221    pub(crate) async fn _get(
222        &self,
223        path: IpfsPath,
224        providers: &[PeerId],
225        local_only: bool,
226        timeout: Option<Duration>,
227    ) -> Result<Ipld, ResolveError> {
228        let resolved_path = resolve_path(self.ipfs.as_ref(), path).await?;
229
230        let cid = match resolved_path.root().cid() {
231            Some(cid) => cid,
232            None => return Err(ResolveError::NoCid(resolved_path)),
233        };
234
235        let mut iter = resolved_path.iter().peekable();
236
237        let (node, _) = match self
238            .resolve0(cid, &mut iter, true, providers, local_only, timeout)
239            .await
240        {
241            Ok(t) => t,
242            Err(e) => {
243                drop(iter);
244                return Err(e.with_path(resolved_path));
245            }
246        };
247
248        Ipld::try_from(node)
249    }
250
251    /// Resolves a `Cid`-rooted path to a document "node."
252    ///
253    /// The return value has two kinds of meanings depending on whether links should be followed or
254    /// not: when following links, the second returned value will be the path inside the last document;
255    /// when not following links, the second returned value will be the unmatched or "remaining"
256    /// path.
257    ///
258    /// Regardless of the `follow_links` option, HAMT-sharded directories will be resolved through
259    /// as a "single step" in the given IpfsPath.
260    ///
261    /// Returns a node and the remaining path or the path inside the last document.
262    pub async fn resolve(
263        &self,
264        path: IpfsPath,
265        follow_links: bool,
266        providers: &[PeerId],
267        local_only: bool,
268    ) -> Result<(ResolvedNode, SlashedPath), ResolveError> {
269        self._resolve(path, follow_links, providers, local_only, None)
270            .await
271    }
272
273    pub(crate) async fn _resolve(
274        &self,
275        path: IpfsPath,
276        follow_links: bool,
277        providers: &[PeerId],
278        local_only: bool,
279        timeout: Option<Duration>,
280    ) -> Result<(ResolvedNode, SlashedPath), ResolveError> {
281        let resolved_path = resolve_path(self.ipfs.as_ref(), path).await?;
282
283        let cid = match resolved_path.root().cid() {
284            Some(cid) => cid,
285            None => return Err(ResolveError::NoCid(resolved_path)),
286        };
287
288        let (node, matched_segments) = {
289            let mut iter = resolved_path.iter().peekable();
290            match self
291                .resolve0(cid, &mut iter, follow_links, providers, local_only, timeout)
292                .await
293            {
294                Ok(t) => t,
295                Err(e) => {
296                    drop(iter);
297                    return Err(e.with_path(resolved_path));
298                }
299            }
300        };
301
302        // we only care about returning this remaining_path with segments up until the last
303        // document but it can and should contain all of the following segments (if any). there
304        // could be more segments when `!follow_links`.
305        let remaining_path = resolved_path.into_shifted(matched_segments);
306
307        Ok((node, remaining_path))
308    }
309
310    /// Return the node where the resolving ended, and the **count** of segments matched.
311    #[allow(clippy::too_many_arguments)]
312    async fn resolve0<'a>(
313        &self,
314        cid: &Cid,
315        segments: &mut Peekable<impl Iterator<Item = &'a str>>,
316        follow_links: bool,
317        providers: &[PeerId],
318        local_only: bool,
319        timeout: Option<Duration>,
320    ) -> Result<(ResolvedNode, usize), RawResolveLocalError> {
321        use LocallyResolved::*;
322
323        let mut current = *cid;
324        let mut total = 0;
325
326        let mut cache = None;
327
328        loop {
329            let block = match self
330                .repo
331                .get_block(current)
332                .providers(providers)
333                .set_local(local_only)
334                .timeout(timeout)
335                .await
336            {
337                Ok(block) => block,
338                Err(e) => return Err(RawResolveLocalError::Loading(current, e)),
339            };
340
341            let start = total;
342
343            let (resolution, matched) = match resolve_local(block, segments, &mut cache) {
344                Ok(t) => t,
345                Err(mut e) => {
346                    e.add_starting_point_in_path(start);
347                    return Err(e);
348                }
349            };
350            total += matched;
351
352            let (src, dest) = match resolution {
353                Complete(ResolvedNode::Link(src, dest)) => (src, dest),
354                Incomplete(src, lookup) => match self
355                    .resolve_hamt(lookup, &mut cache, providers, local_only)
356                    .await
357                {
358                    Ok(dest) => (src, dest),
359                    Err(e) => return Err(RawResolveLocalError::UnsupportedDocument(src, e.into())),
360                },
361                Complete(other) => {
362                    // when following links we return the total of links matched before the
363                    // returned document.
364                    return Ok((other, start));
365                }
366            };
367
368            if !follow_links {
369                // when not following links we return the total of links matched
370                return Ok((ResolvedNode::Link(src, dest), total));
371            } else {
372                current = dest;
373            }
374        }
375    }
376
377    /// To resolve a segment through a HAMT-sharded directory we need to load more blocks, which is
378    /// why this is a method and not a free `fn` like the other resolving activities.
379    async fn resolve_hamt(
380        &self,
381        mut lookup: ShardedLookup<'_>,
382        cache: &mut Option<Cache>,
383        providers: &[PeerId],
384        local_only: bool,
385    ) -> Result<Cid, Error> {
386        use MaybeResolved::*;
387
388        loop {
389            let (next, _) = lookup.pending_links();
390
391            let block = self
392                .repo
393                .get_block(next)
394                .providers(providers)
395                .set_local(local_only)
396                .await?;
397
398            match lookup.continue_walk(block.data(), cache)? {
399                NeedToLoadMore(next) => lookup = next,
400                Found(cid) => return Ok(cid),
401                NotFound => return Err(anyhow::anyhow!("key not found: ???")),
402            }
403        }
404    }
405}
406
407#[must_use = "futures do nothing unless you `.await` or poll them"]
408pub struct DagGet {
409    dag_ipld: IpldDag,
410    path: Option<IpfsPath>,
411    providers: Vec<PeerId>,
412    local: bool,
413    timeout: Option<Duration>,
414    span: Option<Span>,
415}
416
417impl DagGet {
418    pub fn new(dag: IpldDag) -> Self {
419        Self {
420            dag_ipld: dag,
421            path: None,
422            providers: vec![],
423            local: false,
424            timeout: None,
425            span: None,
426        }
427    }
428
429    /// Path to object
430    pub fn path(mut self, path: impl Into<IpfsPath>) -> Self {
431        let path = path.into();
432        self.path = Some(path);
433        self
434    }
435
436    /// Peer that may contain the block
437    pub fn provider(mut self, peer_id: PeerId) -> Self {
438        if !self.providers.contains(&peer_id) {
439            self.providers.push(peer_id);
440        }
441        self
442    }
443
444    /// List of peers that may contain the block
445    pub fn providers(mut self, providers: &[PeerId]) -> Self {
446        self.providers = providers.into();
447        self
448    }
449
450    /// Resolve local block
451    pub fn local(mut self) -> Self {
452        self.local = true;
453        self
454    }
455
456    /// Set flag to resolve block locally
457    pub fn set_local(mut self, local: bool) -> Self {
458        self.local = local;
459        self
460    }
461
462    /// Timeout duration to resolve a block before returning an error
463    pub fn timeout(mut self, timeout: Duration) -> Self {
464        self.timeout = Some(timeout);
465        self
466    }
467
468    /// Deserialize to a serde-compatible object
469    pub fn deserialized<D: DeserializeOwned>(self) -> DagGetDeserialize<D> {
470        DagGetDeserialize {
471            dag_get: self,
472            _marker: PhantomData,
473        }
474    }
475
476    /// Set tracing span
477    pub fn span(mut self, span: Span) -> Self {
478        self.span = Some(span);
479        self
480    }
481}
482
483impl IntoFuture for DagGet {
484    type Output = Result<Ipld, ResolveError>;
485
486    type IntoFuture = BoxFuture<'static, Self::Output>;
487
488    fn into_future(self) -> Self::IntoFuture {
489        let span = self.span.unwrap_or(Span::current());
490        async move {
491            let path = self.path.ok_or(ResolveError::PathNotProvided)?;
492            self.dag_ipld
493                ._get(path, &self.providers, self.local, self.timeout)
494                .await
495        }
496        .instrument(span)
497        .boxed()
498    }
499}
500
501#[must_use = "futures do nothing unless you `.await` or poll them"]
502pub struct DagGetDeserialize<D> {
503    dag_get: DagGet,
504    _marker: PhantomData<D>,
505}
506
507impl<D> std::future::IntoFuture for DagGetDeserialize<D>
508where
509    D: DeserializeOwned,
510{
511    type Output = Result<D, anyhow::Error>;
512
513    type IntoFuture = BoxFuture<'static, Self::Output>;
514
515    fn into_future(self) -> Self::IntoFuture {
516        let fut = self.dag_get.into_future();
517        async move {
518            let document = fut.await?;
519            let data = from_ipld(document)?;
520            Ok(data)
521        }
522        .boxed()
523    }
524}
525
526#[must_use = "futures do nothing unless you `.await` or poll them"]
527pub struct DagPut {
528    dag_ipld: IpldDag,
529    codec: BlockCodec,
530    data: Box<dyn FnOnce() -> anyhow::Result<Ipld> + Send + 'static>,
531    hash: Code,
532    pinned: Option<bool>,
533    span: Span,
534    provide: bool,
535}
536
537impl DagPut {
538    pub fn new(dag: IpldDag) -> Self {
539        Self {
540            dag_ipld: dag,
541            codec: BlockCodec::DagCbor,
542            data: Box::new(|| anyhow::bail!("data not available")),
543            hash: Code::Sha2_256,
544            pinned: None,
545            span: Span::current(),
546            provide: false,
547        }
548    }
549
550    /// Set a ipld object
551    pub fn ipld(self, data: Ipld) -> Self {
552        self.serialize(data)
553    }
554
555    /// Set a serde-compatible object
556    pub fn serialize(mut self, data: impl serde::Serialize) -> Self {
557        let result = to_ipld(data).map_err(anyhow::Error::from);
558        self.data = Box::new(move || result);
559        self
560    }
561
562    /// Pin block
563    pub fn pin(mut self, recursive: bool) -> Self {
564        self.pinned = Some(recursive);
565        self
566    }
567
568    /// Provide block over DHT or (a future) content discovery protocol
569    pub fn provide(mut self) -> Self {
570        self.provide = true;
571        self
572    }
573
574    /// Set multihash type
575    pub fn hash(mut self, code: Code) -> Self {
576        self.hash = code;
577        self
578    }
579
580    /// Set codec for ipld
581    pub fn codec(mut self, codec: BlockCodec) -> Self {
582        self.codec = codec;
583        self
584    }
585
586    /// Set tracing span
587    pub fn span(mut self, span: Span) -> Self {
588        self.span = span;
589        self
590    }
591}
592
593impl IntoFuture for DagPut {
594    type Output = Result<Cid, anyhow::Error>;
595
596    type IntoFuture = BoxFuture<'static, Self::Output>;
597
598    fn into_future(self) -> Self::IntoFuture {
599        let span = self.span;
600        async move {
601            if self.provide && self.dag_ipld.ipfs.is_none() {
602                anyhow::bail!("Ipfs is offline");
603            }
604
605            let _g = self.dag_ipld.repo.gc_guard().await;
606
607            let data = (self.data)()?;
608            let bytes = match self.codec {
609                BlockCodec::Raw => from_ipld(data)?,
610                BlockCodec::DagCbor => {
611                    serde_ipld_dagcbor::codec::DagCborCodec::encode_to_vec(&data)?
612                }
613                BlockCodec::DagJson => {
614                    serde_ipld_dagjson::codec::DagJsonCodec::encode_to_vec(&data)?
615                }
616                BlockCodec::DagPb => ipld_dagpb::from_ipld(&data)?,
617            };
618
619            let code = self.hash;
620            let hash = code.digest(&bytes);
621            let version = if self.codec == BlockCodec::DagPb {
622                Version::V0
623            } else {
624                Version::V1
625            };
626            let cid = Cid::new(version, self.codec.into(), hash)?;
627            let block = Block::new(cid, bytes)?;
628            let cid = self.dag_ipld.repo.put_block(&block).await?;
629
630            if let Some(opt) = self.pinned {
631                if !self.dag_ipld.repo.is_pinned(&cid).await? {
632                    self.dag_ipld.repo.insert_pin(&cid, opt, true).await?;
633                }
634            }
635
636            if self.provide {
637                if let Some(ipfs) = &self.dag_ipld.ipfs {
638                    if let Err(e) = ipfs.provide(cid).await {
639                        error!("Failed to provide content over DHT: {e}")
640                    }
641                }
642            }
643
644            Ok(cid)
645        }
646        .instrument(span)
647        .boxed()
648    }
649}
650
651async fn resolve_path(
652    ipfs: Option<&Ipfs>,
653    path: impl Borrow<IpfsPath>,
654) -> Result<IpfsPath, ResolveError> {
655    let path = path.borrow().clone();
656    let resolved_path = match ipfs {
657        Some(ipfs) => ipfs
658            .resolve_ipns(&path, true)
659            .await
660            .map_err(|_| ResolveError::IpnsResolutionFailed(path))?,
661        None => {
662            if !matches!(path.root(), PathRoot::Ipld(_)) {
663                return Err(ResolveError::IpnsResolutionFailed(path));
664            }
665            path
666        }
667    };
668
669    Ok(resolved_path)
670}
671
672/// `IpfsPath`'s `Cid`-based variant can be resolved to the block, projections represented by this
673/// type.
674///
675/// Values can be converted to Ipld using `Ipld::try_from`.
676#[derive(Debug, PartialEq)]
677pub enum ResolvedNode {
678    /// Block which was loaded at the end of the path.
679    Block(Block),
680    /// Path ended in `Data` at a dag-pb node. This is usually not interesting and should be
681    /// treated as a "Not found" error since dag-pb node did not have a *link* called `Data`. The variant
682    /// exists as there are interface-ipfs-http tests which require this behaviour.
683    DagPbData(Cid, NodeData<Bytes>),
684    /// Path ended on a !dag-pb document which was projected.
685    Projection(Cid, Ipld),
686    /// Local resolving ended with a link
687    Link(Cid, Cid),
688}
689
690impl ResolvedNode {
691    /// Returns the `Cid` of the **source** document for the encapsulated document or projection of such.
692    pub fn source(&self) -> &Cid {
693        match self {
694            ResolvedNode::Block(block) => block.cid(),
695            ResolvedNode::DagPbData(cid, ..)
696            | ResolvedNode::Projection(cid, ..)
697            | ResolvedNode::Link(cid, ..) => cid,
698        }
699    }
700
701    /// Unwraps a dag-pb or raw block variant and turns others into UnexpectedResolved. A raw block
702    /// is a valid single-leaf unixfs file, so it is returned as-is for the caller to handle.
703    /// This is useful wherever unixfs operations are continued after resolving an IpfsPath.
704    pub fn into_unixfs_block(self) -> Result<Block, UnexpectedResolved> {
705        let codec = self.source().codec();
706        let dag_pb: u64 = BlockCodec::DagPb.into();
707        let raw: u64 = BlockCodec::Raw.into();
708        if codec != dag_pb && codec != raw {
709            Err(UnexpectedResolved::UnexpectedCodec(dag_pb, Box::new(self)))
710        } else {
711            match self {
712                ResolvedNode::Block(b) => Ok(b),
713                _ => Err(UnexpectedResolved::NonBlock(Box::new(self))),
714            }
715        }
716    }
717}
718
719impl TryFrom<ResolvedNode> for Ipld {
720    type Error = ResolveError;
721    fn try_from(r: ResolvedNode) -> Result<Ipld, Self::Error> {
722        use ResolvedNode::*;
723
724        match r {
725            Block(block) => Ok(block
726                .to_ipld()
727                .map_err(move |e| ResolveError::UnsupportedDocument(*block.cid(), e.into()))?),
728            DagPbData(_, node_data) => Ok(Ipld::Bytes(node_data.node_data().to_vec())),
729            Projection(_, ipld) => Ok(ipld),
730            Link(_, cid) => Ok(Ipld::Link(cid)),
731        }
732    }
733}
734
735/// Success variants for the `resolve_local` operation on an `Ipld` document.
736#[derive(Debug)]
737enum LocallyResolved<'a> {
738    /// Resolution completed.
739    Complete(ResolvedNode),
740
741    /// Resolving was attempted on a block which is a HAMT-sharded bucket, and needs to be
742    /// continued by loading other buckets.
743    Incomplete(Cid, ShardedLookup<'a>),
744}
745
746#[cfg(test)]
747impl LocallyResolved<'_> {
748    fn unwrap_complete(self) -> ResolvedNode {
749        match self {
750            LocallyResolved::Complete(rn) => rn,
751            x => unreachable!("{:?}", x),
752        }
753    }
754}
755
756impl From<ResolvedNode> for LocallyResolved<'static> {
757    fn from(r: ResolvedNode) -> LocallyResolved<'static> {
758        LocallyResolved::Complete(r)
759    }
760}
761
762/// Resolves the given path segments locally or inside the given document; in addition to
763/// `resolve_local_ipld` this fn also handles normal dag-pb and unixfs HAMTs.
764fn resolve_local<'a>(
765    block: Block,
766    segments: &mut Peekable<impl Iterator<Item = &'a str>>,
767    cache: &mut Option<Cache>,
768) -> Result<(LocallyResolved<'a>, usize), RawResolveLocalError> {
769    if segments.peek().is_none() {
770        return Ok((LocallyResolved::Complete(ResolvedNode::Block(block)), 0));
771    }
772
773    if block.cid().codec() == <BlockCodec as Into<u64>>::into(BlockCodec::DagPb) {
774        // special-case the dagpb since we need to do the HAMT lookup and going through the
775        // BTreeMaps of ipld for this is quite tiresome. if you are looking for that code for
776        // simple directories, you can find one in the history of ipfs-http.
777
778        // advancing is required here in order for us to determine if this was the last element.
779        // This should be ok as the only way we can continue resolving deeper is the case of Link
780        // being matched, and not the error or the DagPbData case.
781        let segment = segments.next().unwrap();
782        let (cid, data) = block.into_inner();
783        Ok(resolve_local_dagpb(
784            cid,
785            data,
786            segment,
787            segments.peek().is_none(),
788            cache,
789        )?)
790    } else {
791        let ipld = match block.to_ipld() {
792            Ok(ipld) => ipld,
793            Err(e) => {
794                return Err(RawResolveLocalError::UnsupportedDocument(
795                    *block.cid(),
796                    e.into(),
797                ));
798            }
799        };
800        resolve_local_ipld(*block.cid(), ipld, segments)
801    }
802}
803
804/// Resolving through dagpb documents is basically just mapping from [`MaybeResolved`] to the
805/// return value, with the exception that a path ending in "Data" is returned as
806/// `ResolvedNode::DagPbData`.
807fn resolve_local_dagpb<'a>(
808    cid: Cid,
809    data: Bytes,
810    segment: &'a str,
811    is_last: bool,
812    cache: &mut Option<Cache>,
813) -> Result<(LocallyResolved<'a>, usize), RawResolveLocalError> {
814    match resolve(&data, segment, cache) {
815        Ok(MaybeResolved::NeedToLoadMore(lookup)) => {
816            Ok((LocallyResolved::Incomplete(cid, lookup), 0))
817        }
818        Ok(MaybeResolved::Found(dest)) => {
819            Ok((LocallyResolved::Complete(ResolvedNode::Link(cid, dest)), 1))
820        }
821        Ok(MaybeResolved::NotFound) => {
822            if segment == "Data" && is_last {
823                let wrapped = wrap_node_data(data).expect("already deserialized once");
824                return Ok((
825                    LocallyResolved::Complete(ResolvedNode::DagPbData(cid, wrapped)),
826                    1,
827                ));
828            }
829            Err(RawResolveLocalError::NotFound {
830                document: cid,
831                segment_index: 0,
832            })
833        }
834        Err(rust_unixfs::ResolveError::UnexpectedType(ut)) if ut.is_file() => {
835            // this might even be correct: files we know are not supported, however not sure if
836            // symlinks are, let alone custom unxifs types should such exist
837            Err(RawResolveLocalError::NotFound {
838                document: cid,
839                segment_index: 0,
840            })
841        }
842        Err(e) => Err(RawResolveLocalError::UnsupportedDocument(cid, e.into())),
843    }
844}
845
846/// Resolves the given path segments locally or inside the given document. Resolving is terminated
847/// upon reaching a link or exhausting the path.
848///
849/// Returns the number of path segments matched -- the iterator might be consumed more than it was
850/// matched.
851///
852/// Note: Tried to initially work with this through Peekable but this would need two peeks.
853///
854/// # Limitations
855///
856/// Does not support dag-pb as segments are resolved differently on dag-pb than the general Ipld.
857fn resolve_local_ipld<'a>(
858    document: Cid,
859    mut ipld: Ipld,
860    segments: &mut Peekable<impl Iterator<Item = &'a str>>,
861) -> Result<(LocallyResolved<'a>, usize), RawResolveLocalError> {
862    let mut matched_count = 0;
863    loop {
864        ipld = match ipld {
865            Ipld::Link(cid) => {
866                if segments.peek() != Some(&".") {
867                    // there is something other than dot next in the path, we should silently match
868                    // over it.
869                    return Ok((ResolvedNode::Link(document, cid).into(), matched_count));
870                } else {
871                    Ipld::Link(cid)
872                }
873            }
874            ipld => ipld,
875        };
876
877        ipld = match (ipld, segments.next()) {
878            (Ipld::Link(cid), Some(".")) => {
879                return Ok((ResolvedNode::Link(document, cid).into(), matched_count + 1));
880            }
881            (Ipld::Link(_), Some(_)) => {
882                unreachable!("case already handled above before advancing the iterator")
883            }
884            (Ipld::Map(mut map), Some(segment)) => {
885                let found = match map.remove(segment) {
886                    Some(f) => f,
887                    None => {
888                        return Err(RawResolveLocalError::NotFound {
889                            document,
890                            segment_index: matched_count,
891                        });
892                    }
893                };
894                matched_count += 1;
895                found
896            }
897            (Ipld::List(mut vec), Some(segment)) => match segment.parse::<usize>() {
898                Ok(index) if index < vec.len() => {
899                    matched_count += 1;
900                    vec.swap_remove(index)
901                }
902                Ok(index) => {
903                    return Err(RawResolveLocalError::ListIndexOutOfRange {
904                        document,
905                        segment_index: matched_count,
906                        index,
907                        elements: vec.len(),
908                    });
909                }
910                Err(_) => {
911                    return Err(RawResolveLocalError::InvalidIndex {
912                        document,
913                        segment_index: matched_count,
914                    });
915                }
916            },
917            (_, Some(_)) => {
918                return Err(RawResolveLocalError::NoLinks {
919                    document,
920                    segment_index: matched_count,
921                });
922            }
923            // path has been consumed
924            (anything, None) => {
925                return Ok((
926                    ResolvedNode::Projection(document, anything).into(),
927                    matched_count,
928                ));
929            }
930        };
931    }
932}
933
934#[cfg(test)]
935mod tests {
936    use super::*;
937    use crate::Node;
938    use ipld_core::ipld;
939    use serde_ipld_dagcbor::codec::DagCborCodec;
940
941    #[tokio::test]
942    async fn test_resolve_root_cid() {
943        let Node { ipfs, .. } = Node::new("test_node").await;
944        let dag = IpldDag::new(ipfs);
945        let data = ipld!([1, 2, 3]);
946        let cid = dag.put_dag(data.clone()).await.unwrap();
947        let res = dag.get_dag(IpfsPath::from(cid)).await.unwrap();
948        assert_eq!(res, data);
949    }
950
951    #[tokio::test]
952    async fn test_resolve_array_elem() {
953        let Node { ipfs, .. } = Node::new("test_node").await;
954        let dag = IpldDag::new(ipfs);
955        let data = ipld!([1, 2, 3]);
956        let cid = dag.put_dag(data.clone()).await.unwrap();
957        let res = dag
958            .get_dag(IpfsPath::from(cid).sub_path("1").unwrap())
959            .await
960            .unwrap();
961        assert_eq!(res, ipld!(2));
962    }
963
964    #[tokio::test]
965    async fn test_resolve_nested_array_elem() {
966        let Node { ipfs, .. } = Node::new("test_node").await;
967        let dag = IpldDag::new(ipfs);
968        let data = ipld!([1, [2], 3,]);
969        let cid = dag.put_dag(data).await.unwrap();
970        let res = dag
971            .get_dag(IpfsPath::from(cid).sub_path("1/0").unwrap())
972            .await
973            .unwrap();
974        assert_eq!(res, ipld!(2));
975    }
976
977    #[tokio::test]
978    async fn test_resolve_object_elem() {
979        let Node { ipfs, .. } = Node::new("test_node").await;
980        let dag = IpldDag::new(ipfs);
981        let data = ipld!({
982            "key": false,
983        });
984        let cid = dag.put_dag(data).await.unwrap();
985        let res = dag
986            .get_dag(IpfsPath::from(cid).sub_path("key").unwrap())
987            .await
988            .unwrap();
989        assert_eq!(res, ipld!(false));
990    }
991
992    #[tokio::test]
993    async fn test_resolve_cid_elem() {
994        let Node { ipfs, .. } = Node::new("test_node").await;
995        let dag = IpldDag::new(ipfs);
996        let data1 = ipld!([1]);
997        let cid1 = dag.put_dag(data1).await.unwrap();
998        let data2 = ipld!([cid1]);
999        let cid2 = dag.put_dag(data2).await.unwrap();
1000        let res = dag
1001            .get_dag(IpfsPath::from(cid2).sub_path("0/0").unwrap())
1002            .await
1003            .unwrap();
1004        assert_eq!(res, ipld!(1));
1005    }
1006
1007    /// Returns an example ipld document with strings, ints, maps, lists, and a link. The link target is also
1008    /// returned.
1009    fn example_doc_and_cid() -> (Cid, Ipld, Cid) {
1010        let cid = Cid::try_from("QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n").unwrap();
1011        let doc = ipld!({
1012            "nested": {
1013                "even": [
1014                    {
1015                        "more": 5
1016                    },
1017                    {
1018                        "or": "this",
1019                    },
1020                    {
1021                        "or": cid,
1022                    },
1023                    {
1024                        "5": "or",
1025                    }
1026                ],
1027            }
1028        });
1029        let root =
1030            Cid::try_from("bafyreielwgy762ox5ndmhx6kpi6go6il3gzahz3ngagb7xw3bj3aazeita").unwrap();
1031        (root, doc, cid)
1032    }
1033
1034    #[test]
1035    fn resolve_cbor_locally_to_end() {
1036        let (root, example_doc, _) = example_doc_and_cid();
1037
1038        let good_examples = [
1039            (
1040                "bafyreielwgy762ox5ndmhx6kpi6go6il3gzahz3ngagb7xw3bj3aazeita/nested/even/0/more",
1041                Ipld::Integer(5),
1042            ),
1043            (
1044                "bafyreielwgy762ox5ndmhx6kpi6go6il3gzahz3ngagb7xw3bj3aazeita/nested/even/1/or",
1045                Ipld::from("this"),
1046            ),
1047            (
1048                "bafyreielwgy762ox5ndmhx6kpi6go6il3gzahz3ngagb7xw3bj3aazeita/nested/even/3/5",
1049                Ipld::from("or"),
1050            ),
1051        ];
1052
1053        for (path, expected) in &good_examples {
1054            let p = IpfsPath::try_from(*path).unwrap();
1055
1056            let (resolved, matched_segments) =
1057                resolve_local_ipld(root, example_doc.clone(), &mut p.iter().peekable()).unwrap();
1058
1059            assert_eq!(matched_segments, 4);
1060
1061            match resolved.unwrap_complete() {
1062                ResolvedNode::Projection(_, p) if &p == expected => {}
1063                x => unreachable!("unexpected {:?}", x),
1064            }
1065
1066            let remaining_path = p.iter().skip(matched_segments).collect::<Vec<&str>>();
1067            assert!(remaining_path.is_empty(), "{remaining_path:?}");
1068        }
1069    }
1070
1071    #[test]
1072    fn resolve_cbor_locally_to_link() {
1073        let (root, example_doc, target) = example_doc_and_cid();
1074
1075        let p = IpfsPath::try_from(
1076            "bafyreielwgy762ox5ndmhx6kpi6go6il3gzahz3ngagb7xw3bj3aazeita/nested/even/2/or/foobar/trailer", // counts:                                                    1      2   3 4
1077        )
1078        .unwrap();
1079
1080        let (resolved, matched_segments) =
1081            resolve_local_ipld(root, example_doc, &mut p.iter().peekable()).unwrap();
1082
1083        match resolved.unwrap_complete() {
1084            ResolvedNode::Link(_, cid) if cid == target => {}
1085            x => unreachable!("{:?}", x),
1086        }
1087
1088        assert_eq!(matched_segments, 4);
1089
1090        let remaining_path = p.iter().skip(matched_segments).collect::<Vec<&str>>();
1091        assert_eq!(remaining_path, &["foobar", "trailer"]);
1092    }
1093
1094    #[test]
1095    fn resolve_cbor_locally_to_link_with_dot() {
1096        let (root, example_doc, cid) = example_doc_and_cid();
1097
1098        let p = IpfsPath::try_from(
1099            "bafyreielwgy762ox5ndmhx6kpi6go6il3gzahz3ngagb7xw3bj3aazeita/nested/even/2/or/./foobar/trailer",
1100            // counts:                                                    1      2   3 4  5
1101        )
1102        .unwrap();
1103
1104        let (resolved, matched_segments) =
1105            resolve_local_ipld(root, example_doc, &mut p.iter().peekable()).unwrap();
1106        assert_eq!(resolved.unwrap_complete(), ResolvedNode::Link(root, cid));
1107        assert_eq!(matched_segments, 5);
1108
1109        let remaining_path = p.iter().skip(matched_segments).collect::<Vec<&str>>();
1110        assert_eq!(remaining_path, &["foobar", "trailer"]);
1111    }
1112
1113    #[test]
1114    fn resolve_cbor_locally_not_found_map_key() {
1115        let (root, example_doc, _) = example_doc_and_cid();
1116        let p = IpfsPath::try_from(
1117            "bafyreielwgy762ox5ndmhx6kpi6go6il3gzahz3ngagb7xw3bj3aazeita/foobar/trailer",
1118        )
1119        .unwrap();
1120
1121        let e = resolve_local_ipld(root, example_doc, &mut p.iter().peekable()).unwrap_err();
1122        assert!(
1123            matches!(
1124                e,
1125                RawResolveLocalError::NotFound {
1126                    segment_index: 0,
1127                    ..
1128                }
1129            ),
1130            "{e:?}"
1131        );
1132    }
1133
1134    #[test]
1135    fn resolve_cbor_locally_too_large_list_index() {
1136        let (root, example_doc, _) = example_doc_and_cid();
1137        let p = IpfsPath::try_from(
1138            "bafyreielwgy762ox5ndmhx6kpi6go6il3gzahz3ngagb7xw3bj3aazeita/nested/even/3000",
1139        )
1140        .unwrap();
1141
1142        let e = resolve_local_ipld(root, example_doc, &mut p.iter().peekable()).unwrap_err();
1143        assert!(
1144            matches!(
1145                e,
1146                RawResolveLocalError::ListIndexOutOfRange {
1147                    segment_index: 2,
1148                    index: 3000,
1149                    elements: 4,
1150                    ..
1151                }
1152            ),
1153            "{e:?}"
1154        );
1155    }
1156
1157    #[test]
1158    fn resolve_cbor_locally_non_usize_index() {
1159        let (root, example_doc, _) = example_doc_and_cid();
1160        let p = IpfsPath::try_from(
1161            "bafyreielwgy762ox5ndmhx6kpi6go6il3gzahz3ngagb7xw3bj3aazeita/nested/even/-1",
1162        )
1163        .unwrap();
1164
1165        // FIXME: errors, again the number of matched
1166        let e = resolve_local_ipld(root, example_doc, &mut p.iter().peekable()).unwrap_err();
1167        assert!(
1168            matches!(
1169                e,
1170                RawResolveLocalError::InvalidIndex {
1171                    segment_index: 2,
1172                    ..
1173                }
1174            ),
1175            "{e:?}"
1176        );
1177    }
1178
1179    #[tokio::test]
1180    async fn resolve_through_link() {
1181        let Node { ipfs, .. } = Node::new("test_node").await;
1182        let dag = IpldDag::new(ipfs);
1183        let ipld = ipld!([1]);
1184        let cid1 = dag.put_dag(ipld).await.unwrap();
1185        let ipld = ipld!([cid1]);
1186        let cid2 = dag.put_dag(ipld).await.unwrap();
1187
1188        let prefix = IpfsPath::from(cid2);
1189
1190        // the two should be equal, as dot can appear or not appear
1191        // FIXME: validate that go-ipfs still does this
1192        let equiv_paths = vec![
1193            prefix.sub_path("0/0").unwrap(),
1194            prefix.sub_path("0/./0").unwrap(),
1195        ];
1196
1197        for p in equiv_paths {
1198            let cloned = p.clone();
1199            match dag.resolve(p, true, &[], false).await.unwrap() {
1200                (ResolvedNode::Projection(_, Ipld::Integer(1)), remaining_path) => {
1201                    assert_eq!(remaining_path, ["0"][..], "{cloned}");
1202                }
1203                x => unreachable!("{:?}", x),
1204            }
1205        }
1206    }
1207
1208    #[tokio::test]
1209    async fn fail_resolving_first_segment() {
1210        let Node { ipfs, .. } = Node::new("test_node").await;
1211        let dag = IpldDag::new(ipfs);
1212        let ipld = ipld!([1]);
1213        let cid1 = dag.put_dag(ipld).await.unwrap();
1214        let ipld = ipld!({ "0": cid1 });
1215        let cid2 = dag.put_dag(ipld).await.unwrap();
1216
1217        let path = IpfsPath::from(cid2).sub_path("1/a").unwrap();
1218
1219        //let cloned = path.clone();
1220        let e = dag.resolve(path, true, &[], false).await.unwrap_err();
1221        assert_eq!(e.to_string(), format!("no link named \"1\" under {cid2}"));
1222    }
1223
1224    #[tokio::test]
1225    async fn fail_resolving_last_segment() {
1226        let Node { ipfs, .. } = Node::new("test_node").await;
1227        let dag = IpldDag::new(ipfs);
1228        let ipld = ipld!([1]);
1229        let cid1 = dag.put_dag(ipld).await.unwrap();
1230        let ipld = ipld!([cid1]);
1231        let cid2 = dag.put_dag(ipld).await.unwrap();
1232
1233        let path = IpfsPath::from(cid2).sub_path("0/a").unwrap();
1234
1235        //let cloned = path.clone();
1236        let e = dag.resolve(path, true, &[], false).await.unwrap_err();
1237        assert_eq!(e.to_string(), format!("no link named \"a\" under {cid1}"));
1238    }
1239
1240    #[tokio::test]
1241    async fn fail_resolving_through_file() {
1242        let Node { ipfs, .. } = Node::new("test_node").await;
1243
1244        // pin CIDv0 so the file is a dag-pb node; resolving through it yields the dag-pb specific
1245        // error asserted below (a raw-leaf root would fail earlier with "no links").
1246        let mut adder = rust_unixfs::file::adder::FileAdder::builder()
1247            .with_cid_version(Version::V0)
1248            .build();
1249        let (mut blocks, _) = adder.push(b"foobar\n");
1250        assert_eq!(blocks.next(), None);
1251
1252        let mut blocks = adder.finish();
1253
1254        let (cid, data) = blocks.next().unwrap();
1255        assert_eq!(blocks.next(), None);
1256
1257        ipfs.put_block(&Block::new(cid, data).unwrap())
1258            .await
1259            .unwrap();
1260
1261        let path = IpfsPath::from(cid).sub_path("anything-here").unwrap();
1262
1263        let e = ipfs
1264            .dag()
1265            .resolve(path, true, &[], false)
1266            .await
1267            .unwrap_err();
1268
1269        assert_eq!(
1270            e.to_string(),
1271            format!("no link named \"anything-here\" under {cid}")
1272        );
1273    }
1274
1275    #[tokio::test]
1276    async fn fail_resolving_through_dir() {
1277        let Node { ipfs, .. } = Node::new("test_node").await;
1278
1279        let mut adder = rust_unixfs::file::adder::FileAdder::default();
1280        let (mut blocks, _) = adder.push(b"foobar\n");
1281        assert_eq!(blocks.next(), None);
1282
1283        let mut blocks = adder.finish();
1284
1285        let (cid, data) = blocks.next().unwrap();
1286        assert_eq!(blocks.next(), None);
1287
1288        let total_size = data.len();
1289
1290        ipfs.put_block(&Block::new(cid, data).unwrap())
1291            .await
1292            .unwrap();
1293
1294        let mut opts = rust_unixfs::dir::builder::TreeOptions::default();
1295        opts.wrap_with_directory();
1296
1297        let mut tree = rust_unixfs::dir::builder::BufferingTreeBuilder::new(opts);
1298        tree.put_link("something/best-file-in-the-world", cid, total_size as u64)
1299            .unwrap();
1300
1301        let mut iter = tree.build();
1302        let mut cids = Vec::new();
1303
1304        while let Some(node) = iter.next_borrowed() {
1305            let node = node.unwrap();
1306            let block = Block::new(node.cid.to_owned(), node.block.to_vec()).unwrap();
1307
1308            ipfs.put_block(&block).await.unwrap();
1309
1310            cids.push(node.cid.to_owned());
1311        }
1312
1313        // reverse the cids because they now contain the root cid as the last.
1314        cids.reverse();
1315
1316        let path = IpfsPath::from(cids[0].to_owned())
1317            .sub_path("something/second-best-file")
1318            .unwrap();
1319
1320        let e = ipfs
1321            .dag()
1322            .resolve(path, true, &[], false)
1323            .await
1324            .unwrap_err();
1325
1326        assert_eq!(
1327            e.to_string(),
1328            format!("no link named \"second-best-file\" under {}", cids[1])
1329        );
1330    }
1331
1332    #[test]
1333    fn observes_strict_order_of_map_keys() {
1334        let map = ipld!({
1335            "omega": Ipld::Null,
1336            "bar": Ipld::Null,
1337            "alpha": Ipld::Null,
1338            "foo": Ipld::Null,
1339        });
1340
1341        let bytes = DagCborCodec::encode_to_vec(&map).unwrap();
1342
1343        assert_eq!(
1344            bytes.as_slice(),
1345            &[
1346                164, 99, 98, 97, 114, 246, 99, 102, 111, 111, 246, 101, 97, 108, 112, 104, 97, 246,
1347                101, 111, 109, 101, 103, 97, 246
1348            ]
1349        );
1350    }
1351}