1use 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#[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 #[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 pub fn new(root: PathRoot) -> Self {
94 IpfsPath {
95 root,
96 path: Default::default(),
97 }
98 }
99
100 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 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 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 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#[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 Ok(())
196 } else {
197 Err(())
199 };
200 }
201 self.path.push(sub_path.to_owned());
202 }
203 Ok(())
204 }
205
206 pub fn iter(&self) -> impl Iterator<Item = &String> {
208 self.path.iter()
209 }
210
211 pub fn len(&self) -> usize {
213 self.path.len()
215 }
216
217 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 self.path.iter().zip(other.iter()).all(|(a, b)| a == b)
251 }
252}
253
254#[derive(Clone, PartialEq, Eq, Hash)]
256pub enum PathRoot {
257 Ipld(Cid),
259 Ipns(PeerId),
261 #[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 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#[derive(Debug, thiserror::Error)]
349#[non_exhaustive]
350pub enum IpfsPathError {
351 #[error("Invalid path {0:?}")]
353 InvalidPath(String),
354
355 #[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 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}