1#![no_std]
2
3extern crate alloc;
4
5use alloc::{
6 borrow::Cow,
7 format,
8 string::{String, ToString},
9 vec::Vec,
10};
11use core::fmt;
12
13#[derive(Clone, Debug, Eq, Hash, PartialEq)]
15pub struct RedoxScheme<'a>(Cow<'a, str>);
16
17impl<'a> RedoxScheme<'a> {
18 pub fn new<S: Into<Cow<'a, str>>>(scheme: S) -> Option<Self> {
20 let scheme = scheme.into();
21 if scheme.contains(&['\0', '/', ':']) {
23 return None;
24 }
25 Some(Self(scheme))
26 }
27}
28
29impl<'a> AsRef<str> for RedoxScheme<'a> {
30 fn as_ref(&self) -> &str {
31 self.0.as_ref()
32 }
33}
34
35impl<'a> fmt::Display for RedoxScheme<'a> {
36 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37 write!(f, "{}", self.0)
38 }
39}
40
41#[derive(Clone, Debug, Eq, Hash, PartialEq)]
43pub struct RedoxReference<'a>(Cow<'a, str>);
44
45impl<'a> RedoxReference<'a> {
46 pub fn new<S: Into<Cow<'a, str>>>(reference: S) -> Option<Self> {
48 let reference = reference.into();
49 if reference.contains(&['\0']) {
51 return None;
52 }
53 Some(Self(reference))
54 }
55
56 pub fn join<S: Into<Cow<'a, str>>>(&self, path: S) -> Option<Self> {
61 let path = path.into();
62 if path.starts_with('/') {
63 Self::new(path)
65 } else if path.is_empty() {
66 Self::new(self.0.clone())
68 } else {
69 let mut reference = self.0.clone().into_owned();
71 if !reference.is_empty() && !reference.ends_with('/') {
72 reference.push('/');
73 }
74 reference.push_str(&path);
75 Self::new(reference)
76 }
77 }
78
79 pub fn canonical(&self) -> Option<Self> {
83 let canonical = {
84 let parts = self
85 .0
86 .split('/')
87 .rev()
88 .scan(0, |nskip, part| {
89 if part == "." {
90 Some(None)
91 } else if part == ".." {
92 *nskip += 1;
93 Some(None)
94 } else if *nskip > 0 {
95 *nskip -= 1;
96 Some(None)
97 } else {
98 Some(Some(part))
99 }
100 })
101 .filter_map(|x| x)
102 .filter(|x| !x.is_empty())
103 .collect::<Vec<_>>();
104 parts.iter().rev().fold(String::new(), |mut string, &part| {
105 if !string.is_empty() && !string.ends_with('/') {
106 string.push('/');
107 }
108 string.push_str(part);
109 string
110 })
111 };
112 Self::new(canonical)
113 }
114
115 pub fn is_canon(&self) -> bool {
117 self.0.is_empty()
118 || self
119 .0
120 .split('/')
121 .all(|seg| seg != ".." && seg != "." && seg != "")
122 }
123}
124
125impl<'a> AsRef<str> for RedoxReference<'a> {
126 fn as_ref(&self) -> &str {
127 self.0.as_ref()
128 }
129}
130
131impl<'a> fmt::Display for RedoxReference<'a> {
132 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133 write!(f, "{}", self.0)
134 }
135}
136
137#[derive(Clone, Debug, Eq, Hash, PartialEq)]
139pub enum RedoxPath<'a> {
140 Standard(RedoxReference<'a>),
142 Legacy(RedoxScheme<'a>, RedoxReference<'a>),
144}
145
146impl<'a> RedoxPath<'a> {
147 pub fn from_absolute(path: &'a str) -> Option<Self> {
151 Some(if path.starts_with('/') {
152 Self::Standard(RedoxReference::new(&path[1..])?)
154 } else {
155 let mut parts = path.splitn(2, ':');
157 let scheme = RedoxScheme::new(parts.next()?)?;
158 let reference = RedoxReference::new(parts.next()?)?;
159 Self::Legacy(scheme, reference)
160 })
161 }
162
163 pub fn join(&self, path: &'a str) -> Option<Self> {
168 if path.starts_with('/') {
169 Self::from_absolute(path)
170 } else {
171 Some(match self {
172 Self::Standard(reference) => Self::Standard(reference.join(path)?),
173 Self::Legacy(scheme, reference) => {
174 Self::Legacy(scheme.clone(), reference.join(path)?)
175 }
176 })
177 }
178 }
179
180 pub fn canonical(&self) -> Option<Self> {
184 Some(match self {
185 Self::Standard(reference) => Self::Standard(reference.canonical()?),
186 Self::Legacy(scheme, reference) => {
187 Self::Legacy(scheme.clone(), reference.clone())
190 }
191 })
192 }
193
194 pub fn is_canon(&self) -> bool {
200 match self {
201 Self::Standard(reference) => reference.is_canon(),
202 Self::Legacy(_scheme, _reference) => true,
203 }
204 }
205
206 pub fn as_parts(&'a self) -> Option<(RedoxScheme<'a>, RedoxReference<'a>)> {
211 const SCHEME_PREFIX_LENGTH: usize = "scheme/".len();
212
213 if !self.is_canon() {
214 return None;
215 }
216 match self {
217 Self::Standard(reference) => {
218 let mut parts = reference.0.split('/');
220 loop {
221 match parts.next() {
222 Some("") => {
223 }
225 Some("scheme") => match parts.next() {
226 Some(scheme_name) => {
227 let scheme_length = SCHEME_PREFIX_LENGTH + scheme_name.len() + 1;
229 let remainder = reference.0.get(scheme_length..).unwrap_or("");
230
231 return Some((
232 RedoxScheme(Cow::from(scheme_name)),
233 RedoxReference(Cow::from(remainder)),
234 ));
235 }
236 None => {
237 return Some((
239 RedoxScheme(Cow::from("")),
240 RedoxReference(Cow::from("")),
241 ));
242 }
243 },
244 _ => {
245 return Some((RedoxScheme(Cow::from("file")), reference.clone()));
247 }
248 }
249 }
250 }
251 Self::Legacy(scheme, reference) => {
252 Some((scheme.clone(), reference.clone()))
254 }
255 }
256 }
257
258 pub fn matches_scheme(&self, other: &str) -> bool {
260 if let Some((scheme, _)) = self.as_parts() {
261 scheme.0 == other
262 } else {
263 false
264 }
265 }
266
267 pub fn is_scheme_category(&self, category: &str) -> bool {
269 if let Some((scheme, _)) = self.as_parts() {
270 let mut parts = scheme.0.splitn(2, '.');
271 if let Some(cat) = parts.next() {
272 cat == category && parts.next().is_some()
273 } else {
274 false
275 }
276 } else {
277 false
278 }
279 }
280
281 pub fn is_default_scheme(&self) -> bool {
283 self.matches_scheme("file")
284 }
285
286 pub fn is_legacy(&self) -> bool {
288 match self {
289 RedoxPath::Legacy(_, _) => true,
290 _ => false,
291 }
292 }
293
294 pub fn to_standard(&self) -> String {
296 match self {
297 RedoxPath::Standard(reference) => {
298 format!("/{}", reference.0)
299 }
300 RedoxPath::Legacy(scheme, reference) => {
301 format!("/scheme/{}/{}", scheme.0, reference.0)
302 }
303 }
304 }
305
306 pub fn to_standard_canon(&self) -> Option<String> {
309 Some(match self {
310 RedoxPath::Standard(reference) => {
311 format!("/{}", reference.canonical()?.0)
312 }
313 RedoxPath::Legacy(scheme, reference) => canonicalize_using_scheme(
314 scheme.as_ref(),
315 reference.as_ref().trim_start_matches('/'),
316 )?,
317 })
318 }
319}
320
321impl<'a> fmt::Display for RedoxPath<'a> {
322 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
323 match self {
324 RedoxPath::Standard(reference) => {
325 write!(f, "/{}", reference.0)
326 }
327 RedoxPath::Legacy(scheme, reference) => {
328 write!(f, "{}:{}", scheme.0, reference.0)
329 }
330 }
331 }
332}
333
334pub fn canonicalize_using_cwd<'a>(cwd_opt: Option<&str>, path: &'a str) -> Option<String> {
348 let absolute = match RedoxPath::from_absolute(path) {
349 Some(absolute) => absolute,
350 None => {
351 let cwd = cwd_opt?;
352 let absolute = RedoxPath::from_absolute(cwd)?;
353 absolute.join(path)?
354 }
355 };
356 let canonical = absolute.canonical()?;
357 Some(canonical.to_string())
358}
359
360pub fn canonicalize_to_standard<'a>(cwd_opt: Option<&str>, path: &'a str) -> Option<String> {
363 let absolute = match RedoxPath::from_absolute(path) {
364 Some(absolute) => absolute,
365 None => {
366 let cwd = cwd_opt?;
367 let absolute = RedoxPath::from_absolute(cwd)?;
368 absolute.join(path)?
369 }
370 };
371 absolute.to_standard_canon()
372}
373
374pub fn canonicalize_using_scheme<'a>(scheme: &str, path: &'a str) -> Option<String> {
380 canonicalize_using_cwd(Some(&scheme_path(scheme)?), path)
381}
382
383pub fn scheme_path(name: &str) -> Option<String> {
387 let _ = RedoxScheme::new(name)?;
388 canonicalize_using_cwd(Some("/scheme"), name)
389}
390
391pub fn make_scheme_name(category: &str, detail: &str) -> Option<String> {
395 let name = format!("{}.{}", category, detail);
396 let _ = RedoxScheme::new(&name)?;
397 Some(name)
398}
399
400#[cfg(test)]
401mod tests {
402 use super::*;
403 use alloc::{format, string::ToString};
404
405 #[test]
407 fn test_absolute() {
408 let cwd_opt = None;
409 assert_eq!(canonicalize_using_cwd(cwd_opt, "/"), Some("/".to_string()));
410 assert_eq!(
411 canonicalize_using_cwd(cwd_opt, "/file"),
412 Some("/file".to_string())
413 );
414 assert_eq!(
415 canonicalize_using_cwd(cwd_opt, "/folder/file"),
416 Some("/folder/file".to_string())
417 );
418 assert_eq!(
419 canonicalize_using_cwd(cwd_opt, "/folder/../file"),
420 Some("/file".to_string())
421 );
422 assert_eq!(
423 canonicalize_using_cwd(cwd_opt, "/folder/../.."),
424 Some("/".to_string())
425 );
426 assert_eq!(
427 canonicalize_using_cwd(cwd_opt, "/folder/../../../.."),
428 Some("/".to_string())
429 );
430 assert_eq!(
431 canonicalize_using_cwd(cwd_opt, "/.."),
432 Some("/".to_string())
433 );
434 }
435
436 #[test]
438 fn test_new_relative() {
439 let cwd_opt = Some("/scheme/foo");
440 assert_eq!(
441 canonicalize_using_cwd(cwd_opt, "file"),
442 Some("/scheme/foo/file".to_string())
443 );
444 assert_eq!(
445 canonicalize_using_cwd(cwd_opt, "folder/file"),
446 Some("/scheme/foo/folder/file".to_string())
447 );
448 assert_eq!(
449 canonicalize_using_cwd(cwd_opt, "folder/../file"),
450 Some("/scheme/foo/file".to_string())
451 );
452 assert_eq!(
453 canonicalize_using_cwd(cwd_opt, "folder/../.."),
454 Some("/scheme".to_string())
455 );
456 assert_eq!(
457 canonicalize_using_cwd(cwd_opt, "folder/../../../.."),
458 Some("/".to_string())
459 );
460 assert_eq!(
461 canonicalize_using_cwd(cwd_opt, ".."),
462 Some("/scheme".to_string())
463 );
464 }
465
466 #[test]
468 fn test_new_scheme() {
469 let cwd_opt = None;
470 assert_eq!(
471 canonicalize_using_cwd(cwd_opt, "/scheme/bar/"),
472 Some("/scheme/bar".to_string())
473 );
474 assert_eq!(
475 canonicalize_using_cwd(cwd_opt, "/scheme/bar/file"),
476 Some("/scheme/bar/file".to_string())
477 );
478 assert_eq!(
479 canonicalize_using_cwd(cwd_opt, "/scheme/bar/folder/file"),
480 Some("/scheme/bar/folder/file".to_string())
481 );
482 assert_eq!(
483 canonicalize_using_cwd(cwd_opt, "/scheme/bar/folder/../file"),
484 Some("/scheme/bar/file".to_string())
485 );
486 assert_eq!(
487 canonicalize_using_cwd(cwd_opt, "/scheme/bar/folder/../.."),
488 Some("/scheme".to_string())
489 );
490 assert_eq!(
491 canonicalize_using_cwd(cwd_opt, "/scheme/bar/folder/../../../.."),
492 Some("/".to_string())
493 );
494 assert_eq!(
495 canonicalize_using_cwd(cwd_opt, "/scheme/bar/.."),
496 Some("/scheme".to_string())
497 );
498
499 assert_eq!(
500 canonicalize_using_scheme("bar", ""),
501 Some("/scheme/bar".to_string())
502 );
503 assert_eq!(
504 canonicalize_using_scheme("bar", "foo"),
505 Some("/scheme/bar/foo".to_string())
506 );
507 assert_eq!(
508 canonicalize_using_scheme("bar", ".."),
509 Some("/scheme".to_string())
510 );
511 }
512
513 #[test]
515 fn test_old_relative() {
516 let cwd_opt = Some("foo:");
517 assert_eq!(
518 canonicalize_using_cwd(cwd_opt, "file"),
519 Some("foo:file".to_string())
520 );
521 assert_eq!(
522 canonicalize_using_cwd(cwd_opt, "folder/file"),
523 Some("foo:folder/file".to_string())
524 );
525 assert_eq!(
526 canonicalize_using_cwd(cwd_opt, "folder/../file"),
527 Some("foo:folder/../file".to_string())
528 );
529 assert_eq!(
530 canonicalize_using_cwd(cwd_opt, "folder/../.."),
531 Some("foo:folder/../..".to_string())
532 );
533 assert_eq!(
534 canonicalize_using_cwd(cwd_opt, "folder/../../../.."),
535 Some("foo:folder/../../../..".to_string())
536 );
537 assert_eq!(
538 canonicalize_using_cwd(cwd_opt, ".."),
539 Some("foo:..".to_string())
540 );
541 }
542
543 #[test]
545 fn test_old_scheme() {
546 let cwd_opt = None;
547 assert_eq!(
548 canonicalize_using_cwd(cwd_opt, "bar:"),
549 Some("bar:".to_string())
550 );
551 assert_eq!(
552 canonicalize_using_cwd(cwd_opt, "bar:file"),
553 Some("bar:file".to_string())
554 );
555 assert_eq!(
556 canonicalize_using_cwd(cwd_opt, "bar:folder/file"),
557 Some("bar:folder/file".to_string())
558 );
559 assert_eq!(
560 canonicalize_using_cwd(cwd_opt, "bar:folder/../file"),
561 Some("bar:folder/../file".to_string())
562 );
563 assert_eq!(
564 canonicalize_using_cwd(cwd_opt, "bar:folder/../.."),
565 Some("bar:folder/../..".to_string())
566 );
567 assert_eq!(
568 canonicalize_using_cwd(cwd_opt, "bar:folder/../../../.."),
569 Some("bar:folder/../../../..".to_string())
570 );
571 assert_eq!(
572 canonicalize_using_cwd(cwd_opt, "bar:.."),
573 Some("bar:..".to_string())
574 );
575 }
576
577 #[test]
579 fn test_orbital_scheme() {
580 for flag_str in &["", "abflrtu"] {
581 for x in &[-1, 0, 1] {
582 for y in &[-1, 0, 1] {
583 for w in &[0, 1] {
584 for h in &[0, 1] {
585 for title in &[
586 "",
587 "title",
588 "title/with/slashes",
589 "title:with:colons",
590 "title/../with/../dots/..",
591 ] {
592 let path = format!(
593 "orbital:{}/{}/{}/{}/{}/{}",
594 flag_str, x, y, w, h, title
595 );
596 assert_eq!(canonicalize_using_cwd(None, &path), Some(path));
597 }
598 }
599 }
600 }
601 }
602 }
603 }
604
605 #[test]
607 fn test_parts() {
608 for (path, scheme, reference) in &[
609 ("/foo/bar/baz", "file", "foo/bar/baz"),
610 ("/scheme/foo/bar/baz", "foo", "bar/baz"),
611 ("/", "file", ""),
612 ("/bar", "file", "bar"),
613 ("/...", "file", "..."),
614 ] {
615 let redox_path = RedoxPath::from_absolute(path).unwrap();
616 let parts = redox_path.as_parts();
617 assert_eq!(
618 (path, parts),
619 (
620 path,
621 Some((
622 RedoxScheme::new(*scheme).unwrap(),
623 RedoxReference::new(*reference).unwrap()
624 ))
625 )
626 );
627 let to_string = format!("/scheme/{scheme}");
628 let joined_path = RedoxPath::from_absolute(&to_string)
629 .unwrap()
630 .join(reference)
631 .unwrap();
632 if path.starts_with("/scheme") {
633 assert_eq!(path, &format!("{joined_path}"));
634 } else {
635 assert_eq!(path, &format!("/{reference}"));
636 }
637 }
638
639 assert_eq!(RedoxPath::from_absolute("not/absolute"), None);
641
642 for path in [
644 "//double/slash",
645 "/ending/in/slash/",
646 "/contains/dot/.",
647 "/contains/dotdot/..",
648 ] {
649 let redox_path = RedoxPath::from_absolute(path).unwrap();
650 let parts = redox_path.as_parts();
651 assert_eq!((path, parts), (path, None));
652 }
653 }
654
655 #[test]
656 fn test_old_scheme_parts() {
657 for (path, scheme, reference) in &[
658 ("foo:bar/baz", "foo", "bar/baz"),
659 ("emptyref:", "emptyref", ""),
660 (":emptyscheme", "", "emptyscheme"),
661 ] {
662 let redox_path = RedoxPath::from_absolute(path).unwrap();
663 let parts = redox_path.as_parts();
664 assert_eq!(
665 (path, parts),
666 (
667 path,
668 Some((
669 RedoxScheme::new(*scheme).unwrap(),
670 RedoxReference::new(*reference).unwrap()
671 ))
672 )
673 );
674 }
675
676 assert_eq!(RedoxPath::from_absolute("scheme/withslash:path"), None);
678 assert_eq!(RedoxPath::from_absolute(""), None)
680 }
681
682 #[test]
683 fn test_matches() {
684 assert!(RedoxPath::from_absolute("/scheme/foo")
685 .unwrap()
686 .matches_scheme("foo"));
687 assert!(RedoxPath::from_absolute("/scheme/foo/bar")
688 .unwrap()
689 .matches_scheme("foo"));
690 assert!(!RedoxPath::from_absolute("/scheme/foo")
691 .unwrap()
692 .matches_scheme("bar"));
693 assert!(RedoxPath::from_absolute("foo:")
694 .unwrap()
695 .matches_scheme("foo"));
696 assert!(RedoxPath::from_absolute(
697 &canonicalize_using_cwd(Some("/scheme/foo"), "bar").unwrap()
698 )
699 .unwrap()
700 .matches_scheme("foo"));
701 assert!(
702 RedoxPath::from_absolute(&canonicalize_using_cwd(Some("/foo"), "bar").unwrap())
703 .unwrap()
704 .matches_scheme("file")
705 );
706 assert!(RedoxPath::from_absolute(
707 &canonicalize_using_cwd(Some("/scheme"), "foo/bar").unwrap()
708 )
709 .unwrap()
710 .matches_scheme("foo"));
711 assert!(RedoxPath::from_absolute("foo:/bar")
712 .unwrap()
713 .matches_scheme("foo"));
714 assert!(!RedoxPath::from_absolute("foo:/bar")
715 .unwrap()
716 .matches_scheme("bar"));
717 assert!(RedoxPath::from_absolute("/scheme/file")
718 .unwrap()
719 .is_default_scheme());
720 assert!(!RedoxPath::from_absolute("/scheme/foo")
721 .unwrap()
722 .is_default_scheme());
723 assert!(RedoxPath::from_absolute("file:bar")
724 .unwrap()
725 .is_default_scheme());
726 assert!(RedoxPath::from_absolute("file:")
727 .unwrap()
728 .is_default_scheme());
729 assert!(!RedoxPath::from_absolute("foo:bar")
730 .unwrap()
731 .is_default_scheme());
732 assert!(RedoxPath::from_absolute("foo:bar").unwrap().is_legacy());
733 assert!(!RedoxPath::from_absolute("/foo/bar").unwrap().is_legacy());
734 }
735
736 #[test]
737 fn test_to_standard() {
738 assert_eq!(
739 &RedoxPath::from_absolute("foo:bar").unwrap().to_standard(),
740 "/scheme/foo/bar"
741 );
742 assert_eq!(
743 &RedoxPath::from_absolute("file:bar").unwrap().to_standard(),
744 "/scheme/file/bar"
745 );
746 assert_eq!(
747 &RedoxPath::from_absolute("/scheme/foo/bar")
748 .unwrap()
749 .to_standard(),
750 "/scheme/foo/bar"
751 );
752 assert_eq!(
753 &RedoxPath::from_absolute("/foo/bar").unwrap().to_standard(),
754 "/foo/bar"
755 );
756 assert_eq!(
757 &RedoxPath::from_absolute("foo:bar/../bar2")
758 .unwrap()
759 .to_standard_canon()
760 .unwrap(),
761 "/scheme/foo/bar2"
762 );
763 assert_eq!(
764 &RedoxPath::from_absolute("file:bar/./../bar2")
765 .unwrap()
766 .to_standard_canon()
767 .unwrap(),
768 "/scheme/file/bar2"
769 );
770 assert_eq!(
771 &RedoxPath::from_absolute("/scheme/file/bar/./../../foo/bar")
772 .unwrap()
773 .to_standard_canon()
774 .unwrap(),
775 "/scheme/foo/bar"
776 );
777 assert_eq!(
778 &RedoxPath::from_absolute("/foo/bar")
779 .unwrap()
780 .to_standard_canon()
781 .unwrap(),
782 "/foo/bar"
783 );
784 assert_eq!(
785 &canonicalize_to_standard(None, "/scheme/foo/bar").unwrap(),
786 "/scheme/foo/bar"
787 );
788 assert_eq!(
789 &canonicalize_to_standard(None, "foo:bar").unwrap(),
790 "/scheme/foo/bar"
791 );
792 assert_eq!(
793 &canonicalize_to_standard(None, "foo:bar/../..").unwrap(),
794 "/scheme"
795 );
796 assert_eq!(
797 &canonicalize_to_standard(None, "/scheme/foo/bar/..").unwrap(),
798 "/scheme/foo"
799 );
800 assert_eq!(
801 &canonicalize_to_standard(None, "foo:bar/bar2/..").unwrap(),
802 "/scheme/foo/bar"
803 );
804 }
805
806 #[test]
807 fn test_scheme_path() {
808 assert_eq!(scheme_path("foo"), Some("/scheme/foo".to_string()));
809 assert_eq!(scheme_path(""), Some("/scheme".to_string()));
810 assert_eq!(scheme_path("/foo"), None);
811 assert_eq!(scheme_path("foo/bar"), None);
812 assert_eq!(scheme_path("foo:"), None);
813 }
814
815 #[test]
816 fn test_category() {
817 assert_eq!(make_scheme_name("foo", "bar"), Some("foo.bar".to_string()));
818 assert_eq!(
819 RedoxPath::from_absolute(
820 &scheme_path(&make_scheme_name("foo", "bar").unwrap()).unwrap()
821 )
822 .unwrap(),
823 RedoxPath::Standard(RedoxReference::new("scheme/foo.bar").unwrap())
824 );
825 assert_eq!(make_scheme_name("foo", "/bar"), None);
826 assert_eq!(make_scheme_name("foo", ":bar"), None);
827 assert!(RedoxPath::from_absolute(
828 &scheme_path(&make_scheme_name("foo", "bar").unwrap()).unwrap()
829 )
830 .unwrap()
831 .is_scheme_category("foo"));
832 assert!(RedoxPath::from_absolute("/scheme/foo.bar/bar2")
833 .unwrap()
834 .is_scheme_category("foo"));
835 assert!(!RedoxPath::from_absolute("/scheme/foo/bar")
836 .unwrap()
837 .is_scheme_category("foo"));
838 assert!(!RedoxPath::from_absolute("/foo.bar/bar2")
839 .unwrap()
840 .is_scheme_category("foo"));
841 }
842}