1use 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#[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 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 pub fn new(root: PathRoot) -> Self {
91 IpfsPath {
92 root,
93 path: Default::default(),
94 }
95 }
96
97 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 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 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 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#[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 Ok(())
193 } else {
194 Err(())
196 };
197 }
198 self.path.push(sub_path.to_owned());
199 }
200 Ok(())
201 }
202
203 pub fn iter(&self) -> impl Iterator<Item = &String> {
205 self.path.iter()
206 }
207
208 pub fn len(&self) -> usize {
210 self.path.len()
212 }
213
214 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 self.path.iter().zip(other.iter()).all(|(a, b)| a == b)
248 }
249}
250
251#[derive(Clone, PartialEq, Eq, Hash)]
253pub enum PathRoot {
254 Ipld(Cid),
256 Ipns(PeerId),
258 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 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#[derive(Debug, thiserror::Error)]
343#[non_exhaustive]
344pub enum IpfsPathError {
345 #[error("Invalid path {0:?}")]
347 InvalidPath(String),
348
349 #[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 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}