Skip to main content

rust_ipfs/
path.rs

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