rust_ipfs/
path.rs

1//! [`IpfsPath`] related functionality for content addressed paths with links.
2
3use crate::error::{Error, TryError};
4use core::convert::{TryFrom, TryInto};
5use ipld_core::cid::Cid;
6use libp2p::PeerId;
7use std::fmt;
8use std::str::FromStr;
9
10/// Abstraction over Ipfs paths, which are used to target sub-trees or sub-documents on top of
11/// content addressable ([`Cid`]) trees. The most common use case is to specify a file under an
12/// unixfs tree from underneath a [`Cid`] forest.
13///
14/// In addition to being based on content addressing, IpfsPaths provide adaptation from other Ipfs
15/// (related) functionality which can be resolved to a [`Cid`] such as IPNS. IpfsPaths have similar
16/// structure to and can start with a "protocol" as [Multiaddr], except the protocols are
17/// different, and at the moment there can be at most one protocol.
18///
19/// This implementation supports:
20///
21/// - synonymous `/ipfs` and `/ipld` prefixes to point to a [`Cid`]
22/// - `/ipns` to point to either:
23///    - [`PeerId`] to signify an [IPNS] DHT record
24///    - domain name to signify an [DNSLINK] reachable record
25///
26/// See [`crate::Ipfs::resolve_ipns`] for the current IPNS resolving capabilities.
27///
28/// `IpfsPath` is usually created through the [`FromStr`] or [`From`] conversions.
29///
30/// [Multiaddr]: https://github.com/multiformats/multiaddr
31/// [IPNS]: https://github.com/ipfs/specs/blob/master/IPNS.md
32/// [DNSLINK]: https://dnslink.io/
33// TODO: it might be useful to split this into CidPath and IpnsPath, then have Ipns resolve through
34// latter into CidPath (recursively) and have dag.rs support only CidPath. Keep IpfsPath as a
35// common abstraction which can be either.
36#[derive(Clone, Debug, PartialEq, Eq, Hash)]
37pub struct IpfsPath {
38    root: PathRoot,
39    pub(crate) path: SlashedPath,
40}
41
42impl FromStr for IpfsPath {
43    type Err = Error;
44
45    fn from_str(string: &str) -> Result<Self, Error> {
46        let mut subpath = string.split('/');
47        let empty = subpath.next().expect("there's always the first split");
48
49        let root = if !empty.is_empty() {
50            // by default if there is no prefix it's an ipfs or ipld path
51            PathRoot::Ipld(Cid::try_from(empty)?)
52        } else {
53            let root_type = subpath.next();
54            let key = subpath.next();
55
56            match (empty, root_type, key) {
57                ("", Some("ipfs"), Some(key)) => PathRoot::Ipld(Cid::try_from(key)?),
58                ("", Some("ipld"), Some(key)) => PathRoot::Ipld(Cid::try_from(key)?),
59                ("", Some("ipns"), Some(key)) => match PeerId::from_str(key).ok() {
60                    Some(peer_id) => PathRoot::Ipns(peer_id),
61                    None => {
62                        let result = |key: &str| -> Result<PathRoot, Self::Err> {
63                            let p = PeerId::from_bytes(&Cid::from_str(key)?.hash().to_bytes())?;
64
65                            Ok(PathRoot::Ipns(p))
66                        };
67
68                        match result(key).ok() {
69                            Some(path) => path,
70                            None => PathRoot::Dns(key.to_string()),
71                        }
72                    }
73                },
74                _ => {
75                    return Err(IpfsPathError::InvalidPath(string.to_owned()).into());
76                }
77            }
78        };
79
80        let mut path = IpfsPath::new(root);
81        path.path
82            .push_split(subpath)
83            .map_err(|_| IpfsPathError::InvalidPath(string.to_owned()))?;
84        Ok(path)
85    }
86}
87
88impl IpfsPath {
89    /// Creates a new [`IpfsPath`] from a [`PathRoot`].
90    pub fn new(root: PathRoot) -> Self {
91        IpfsPath {
92            root,
93            path: Default::default(),
94        }
95    }
96
97    /// Returns the [`PathRoot`] "protocol" configured for the [`IpfsPath`].
98    pub fn root(&self) -> &PathRoot {
99        &self.root
100    }
101
102    pub(crate) fn push_str(&mut self, string: &str) -> Result<(), Error> {
103        self.path.push_path(string)?;
104        Ok(())
105    }
106
107    /// Returns a new [`IpfsPath`] with the given path segments appended, or an error, if a segment is
108    /// invalid.
109    pub fn sub_path(&self, segments: &str) -> Result<Self, Error> {
110        let mut path = self.to_owned();
111        path.push_str(segments)?;
112        Ok(path)
113    }
114
115    /// Returns an iterator over the path segments following the root.
116    pub fn iter(&self) -> impl Iterator<Item = &str> {
117        self.path.iter().map(|s| s.as_str())
118    }
119
120    pub(crate) fn into_shifted(self, shifted: usize) -> SlashedPath {
121        assert!(shifted <= self.path.len());
122
123        let mut p = self.path;
124        p.shift(shifted);
125        p
126    }
127
128    pub(crate) fn into_truncated(self, len: usize) -> SlashedPath {
129        assert!(len <= self.path.len());
130
131        let mut p = self.path;
132        p.truncate(len);
133        p
134    }
135}
136
137impl fmt::Display for IpfsPath {
138    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
139        write!(fmt, "{}", self.root)?;
140        if !self.path.is_empty() {
141            // slash is not included in the <SlashedPath as fmt::Display>::fmt impl as we need to,
142            // serialize it later in json *without* one
143            write!(fmt, "/{}", self.path)?;
144        }
145        Ok(())
146    }
147}
148
149impl TryFrom<&str> for IpfsPath {
150    type Error = Error;
151
152    fn try_from(string: &str) -> Result<Self, Self::Error> {
153        IpfsPath::from_str(string)
154    }
155}
156
157impl<T: Into<PathRoot>> From<T> for IpfsPath {
158    fn from(root: T) -> Self {
159        IpfsPath::new(root.into())
160    }
161}
162
163/// SlashedPath is internal to IpfsPath variants, and basically holds a unixfs-compatible path
164/// where segments do not contain slashes but can pretty much contain all other valid UTF-8.
165///
166/// UTF-8 originates likely from UnixFS related protobuf descriptions, where dag-pb links have
167/// UTF-8 names, which equal to SlashedPath segments.
168#[derive(Debug, PartialEq, Eq, Clone, Default, Hash)]
169pub struct SlashedPath {
170    path: Vec<String>,
171}
172
173impl SlashedPath {
174    fn push_path(&mut self, path: &str) -> Result<(), IpfsPathError> {
175        if path.is_empty() {
176            Ok(())
177        } else {
178            self.push_split(path.split('/'))
179                .map_err(|_| IpfsPathError::SegmentContainsSlash(path.to_owned()))
180        }
181    }
182
183    pub(crate) fn push_split<'a>(
184        &mut self,
185        split: impl Iterator<Item = &'a str>,
186    ) -> Result<(), ()> {
187        let mut split = split.peekable();
188        while let Some(sub_path) = split.next() {
189            if sub_path.is_empty() {
190                return if split.peek().is_none() {
191                    // trim trailing
192                    Ok(())
193                } else {
194                    // no empty segments in the middle
195                    Err(())
196                };
197            }
198            self.path.push(sub_path.to_owned());
199        }
200        Ok(())
201    }
202
203    /// Returns an iterator over the path segments
204    pub fn iter(&self) -> impl Iterator<Item = &String> {
205        self.path.iter()
206    }
207
208    /// Returns the number of segments
209    pub fn len(&self) -> usize {
210        // intentionally try to hide the fact that this is based on Vec<String> right now
211        self.path.len()
212    }
213
214    /// Returns true if len is zero
215    pub fn is_empty(&self) -> bool {
216        self.len() == 0
217    }
218
219    fn shift(&mut self, n: usize) {
220        self.path.drain(0..n);
221    }
222
223    fn truncate(&mut self, len: usize) {
224        self.path.truncate(len);
225    }
226}
227
228impl fmt::Display for SlashedPath {
229    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
230        let mut first = true;
231        self.path.iter().try_for_each(move |s| {
232            if first {
233                first = false;
234            } else {
235                write!(fmt, "/")?;
236            }
237
238            write!(fmt, "{s}")
239        })
240    }
241}
242
243impl<'a> PartialEq<[&'a str]> for SlashedPath {
244    fn eq(&self, other: &[&'a str]) -> bool {
245        // FIXME: failed at writing a blanket partialeq over anything which would PartialEq<str> or
246        // String
247        self.path.iter().zip(other.iter()).all(|(a, b)| a == b)
248    }
249}
250
251/// The "protocol" of [`IpfsPath`].
252#[derive(Clone, PartialEq, Eq, Hash)]
253pub enum PathRoot {
254    /// [`Cid`] based path is the simplest path, and is stable.
255    Ipld(Cid),
256    /// IPNS record based path which can point to different [`Cid`] based paths at different times.
257    Ipns(PeerId),
258    /// DNSLINK based path which can point to different [`Cid`] based paths at different times.
259    Dns(String),
260}
261
262impl fmt::Debug for PathRoot {
263    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
264        use PathRoot::*;
265
266        match self {
267            Ipld(cid) => write!(fmt, "{cid}"),
268            Ipns(pid) => write!(fmt, "{pid}"),
269            Dns(name) => write!(fmt, "{name:?}"),
270        }
271    }
272}
273
274impl PathRoot {
275    /// Returns the `Some(Cid)` if the [`Cid`] based path is present or `None`.
276    pub fn cid(&self) -> Option<&Cid> {
277        match self {
278            PathRoot::Ipld(cid) => Some(cid),
279            _ => None,
280        }
281    }
282}
283
284impl fmt::Display for PathRoot {
285    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
286        let (prefix, key) = match self {
287            PathRoot::Ipld(cid) => ("/ipfs/", cid.to_string()),
288            PathRoot::Ipns(peer_id) => ("/ipns/", peer_id.to_base58()),
289            PathRoot::Dns(domain) => ("/ipns/", domain.to_owned()),
290        };
291        write!(fmt, "{prefix}{key}")
292    }
293}
294
295impl From<Cid> for PathRoot {
296    fn from(cid: Cid) -> Self {
297        PathRoot::Ipld(cid)
298    }
299}
300
301impl From<&Cid> for PathRoot {
302    fn from(cid: &Cid) -> Self {
303        PathRoot::Ipld(*cid)
304    }
305}
306
307impl From<PeerId> for PathRoot {
308    fn from(peer_id: PeerId) -> Self {
309        PathRoot::Ipns(peer_id)
310    }
311}
312
313impl From<&PeerId> for PathRoot {
314    fn from(peer_id: &PeerId) -> Self {
315        PathRoot::Ipns(*peer_id)
316    }
317}
318
319impl TryInto<Cid> for PathRoot {
320    type Error = TryError;
321
322    fn try_into(self) -> Result<Cid, Self::Error> {
323        match self {
324            PathRoot::Ipld(cid) => Ok(cid),
325            _ => Err(TryError),
326        }
327    }
328}
329
330impl TryInto<PeerId> for PathRoot {
331    type Error = TryError;
332
333    fn try_into(self) -> Result<PeerId, Self::Error> {
334        match self {
335            PathRoot::Ipns(peer_id) => Ok(peer_id),
336            _ => Err(TryError),
337        }
338    }
339}
340
341/// The path mutation or parsing errors.
342#[derive(Debug, thiserror::Error)]
343#[non_exhaustive]
344pub enum IpfsPathError {
345    /// The given path cannot be parsed as IpfsPath.
346    #[error("Invalid path {0:?}")]
347    InvalidPath(String),
348
349    /// Path segment contains a slash, which is not allowed.
350    #[error("Invalid segment {0:?}")]
351    SegmentContainsSlash(String),
352}
353
354#[cfg(test)]
355mod tests {
356    use super::IpfsPath;
357    use std::convert::TryFrom;
358
359    #[test]
360    fn display() {
361        let input = [
362            (
363                "/ipld/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n",
364                Some("/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n"),
365            ),
366            ("/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n", None),
367            (
368                "/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a",
369                None,
370            ),
371            (
372                "/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a/",
373                Some("/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a"),
374            ),
375            (
376                "QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n",
377                Some("/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n"),
378            ),
379            ("/ipns/foobar.com", None),
380            ("/ipns/foobar.com/a", None),
381            ("/ipns/foobar.com/a/", Some("/ipns/foobar.com/a")),
382        ];
383
384        for (input, maybe_actual) in &input {
385            assert_eq!(
386                IpfsPath::try_from(*input).unwrap().to_string(),
387                maybe_actual.unwrap_or(input)
388            );
389        }
390    }
391
392    #[test]
393    fn good_paths() {
394        let good = [
395            ("/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n", 0),
396            ("/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a", 1),
397            (
398                "/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a/b/c/d/e/f",
399                6,
400            ),
401            (
402                "QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a/b/c/d/e/f",
403                6,
404            ),
405            ("QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n", 0),
406            ("/ipld/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n", 0),
407            ("/ipld/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a", 1),
408            (
409                "/ipld/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a/b/c/d/e/f",
410                6,
411            ),
412            ("/ipns/QmSrPmbaUKA3ZodhzPWZnpFgcPMFWF4QsxXbkWfEptTBJd", 0),
413            (
414                "/ipns/QmSrPmbaUKA3ZodhzPWZnpFgcPMFWF4QsxXbkWfEptTBJd/a/b/c/d/e/f",
415                6,
416            ),
417        ];
418
419        for &(good, len) in &good {
420            let p = IpfsPath::try_from(good).unwrap();
421            assert_eq!(p.iter().count(), len);
422        }
423    }
424
425    #[test]
426    fn bad_paths() {
427        let bad = [
428            "/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n",
429            "/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a",
430            "/ipfs/foo",
431            "/ipfs/",
432            "ipfs/",
433            "ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n",
434            "/ipld/foo",
435            "/ipld/",
436            "ipld/",
437            "ipld/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n",
438        ];
439
440        for &bad in &bad {
441            IpfsPath::try_from(bad).unwrap_err();
442        }
443    }
444
445    #[test]
446    fn trailing_slash_is_ignored() {
447        let paths = [
448            "/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/",
449            "QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/",
450        ];
451        for &path in &paths {
452            let p = IpfsPath::try_from(path).unwrap();
453            assert_eq!(p.iter().count(), 0, "{p:?} from {path:?}");
454        }
455    }
456
457    #[test]
458    fn multiple_slashes_are_not_deduplicated() {
459        // this used to be the behaviour in ipfs-http
460        IpfsPath::try_from("/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n///a").unwrap_err();
461    }
462
463    #[test]
464    fn shifting() {
465        let mut p = super::SlashedPath::default();
466        p.push_split(vec!["a", "b", "c"].into_iter()).unwrap();
467        p.shift(2);
468
469        assert_eq!(p.to_string(), "c");
470    }
471}