Skip to main content

redox_path/
lib.rs

1#![no_std]
2
3extern crate alloc;
4
5use alloc::{
6    borrow::Cow,
7    format,
8    string::{String, ToString},
9};
10use core::{ffi::CStr, fmt};
11
12/// The default scheme. Currently hardcoded.
13const DEFAULT_SCHEME: &str = "file";
14
15/// The name of a scheme
16#[derive(Clone, Debug, Eq, Hash, PartialEq)]
17pub struct RedoxScheme<'a>(Cow<'a, str>);
18
19impl<'a> RedoxScheme<'a> {
20    /// Create a new [`RedoxScheme`], ensuring there are no invalid characters
21    pub fn new<S: Into<Cow<'a, str>>>(scheme: S) -> Option<Self> {
22        let scheme = scheme.into();
23        // Scheme cannot have NUL, /, or :
24        if scheme.contains(&['\0', '/', ':']) {
25            return None;
26        }
27        Some(Self(scheme))
28    }
29
30    /// Similar to [`canonicalize_using_scheme`]
31    pub fn canonicalize_as_scheme<'b>(&self, path: RedoxStr<'b>) -> RedoxPath<'b> {
32        canonicalize_using_scheme_checked(self, path)
33    }
34
35    pub fn into_owned<'b>(self) -> RedoxScheme<'b> {
36        RedoxScheme(self.0.into_owned().into())
37    }
38}
39
40impl<'a> AsRef<str> for RedoxScheme<'a> {
41    fn as_ref(&self) -> &str {
42        self.0.as_ref()
43    }
44}
45
46impl<'a> fmt::Display for RedoxScheme<'a> {
47    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48        write!(f, "{}", self.0)
49    }
50}
51
52/// The part of a path that is sent to each scheme
53#[derive(Clone, Debug, Eq, Hash, PartialEq)]
54pub struct RedoxReference<'a>(Cow<'a, str>);
55
56impl<'a> RedoxReference<'a> {
57    /// Create a new [`RedoxReference`], ensuring there are no invalid characters.
58    /// An empty path is considered valid and equivalent to '/'.
59    /// A path not starting with '/' will be considered not canonical.
60    pub fn new<S: Into<Cow<'a, str>>>(reference: S) -> Option<Self> {
61        let reference = reference.into();
62        // Reference cannot have NUL
63        // as_bytes(): https://github.com/rust-lang/rust/issues/158229
64        // this is safe: https://stackoverflow.com/a/6907327/3908409
65        if reference.as_bytes().iter().any(|&b| b == b'\0') {
66            return None;
67        }
68        Some(Self(reference))
69    }
70
71    /// Create a new [`RedoxReference`] from C string, ensuring it is a valid UTF-8
72    pub fn new_from_c<S: Into<&'a CStr>>(reference: S) -> Option<Self> {
73        let reference = reference.into();
74        Some(Self(Cow::Borrowed(reference.to_str().ok()?)))
75    }
76
77    /// Join a [`RedoxReference`] with a path. Relative paths will be joined, absolute paths will
78    /// be returned as-is.
79    ///
80    /// Returns `Some` on success and `None` if the path is not valid
81    pub fn join<'b, S: Into<Cow<'b, str>>>(&self, path: S) -> Option<RedoxReference<'b>> {
82        let path = path.into();
83        Some(self.join_checked(RedoxReference::new(path)?))
84    }
85
86    /// Join, but checked from NUL
87    pub fn join_checked<'b>(&self, path: RedoxReference<'b>) -> RedoxReference<'b> {
88        if !path.is_relative() {
89            // Absolute path, referenced
90            path
91        } else if path.0.is_empty() {
92            // Empty path, return prior reference cloned
93            RedoxReference(self.0.to_string().into())
94        } else {
95            // Relative path, append to reference
96            let mut reference = self.0.clone().into_owned();
97            if !reference.is_empty() && !reference.ends_with('/') {
98                reference.push('/');
99            }
100            reference.push_str(&path.0);
101            RedoxReference(reference.into())
102        }
103    }
104
105    /// Canonicalize [`RedoxReference`], removing `.`, `..` and empty subpaths.
106    /// In this function, relative path will be remain relative and the path remain not canon.
107    ///
108    /// Paths that goes upward beyond the root path will be truncated
109    /// i.e. `/..` converted to `/` and `../foo/` converted to `foo`.
110    pub fn canonical(self) -> Self {
111        let mut canonicalized = true;
112        let mut is_absolute = false;
113        let mut parts = [""; 4096]; // PATH_MAX
114        let mut parts_len = 0;
115        for (i, part) in self.0.split('/').enumerate() {
116            if part.is_empty() {
117                if i == 0 {
118                    is_absolute = true;
119                } else {
120                    canonicalized = false;
121                }
122            } else if part == "." {
123                canonicalized = false;
124            } else if part == ".." {
125                if parts_len > 0 {
126                    parts_len -= 1;
127                }
128                canonicalized = false;
129            } else {
130                if parts_len >= 4096 {
131                    break;
132                }
133                parts[parts_len] = part;
134                parts_len += 1;
135            }
136        }
137
138        if canonicalized {
139            return self;
140        }
141        let mut string = String::with_capacity(self.0.len());
142        if is_absolute {
143            string.push('/');
144        }
145        for (i, part) in parts[..parts_len].iter().enumerate() {
146            if i > 0 {
147                string.push('/');
148            }
149            string.push_str(part);
150        }
151
152        // Does not use ::new() as &self is checked from NUL
153        Self(Cow::Owned(string))
154    }
155
156    /// Normalize [`RedoxReference`], removing `.`, `..` (if possible) and empty subpaths.
157    /// This function will return [`None`] if the path is absolute or escapes root.
158    ///
159    /// This function is useful to validate if the path still remain in scope within root.
160    /// To make this works within symlink, `max_upward` is provided, and also returning an integer
161    /// for next `canonical_max_upward(max_upward)`. The amount of `..` will never exceed `max_upward`.
162    pub fn canonical_max_upward(self, max_upward: usize) -> Option<(Self, usize)> {
163        let mut canonicalized = true;
164        let mut parts = [""; 4096]; // PATH_MAX
165        let mut parts_len = 0;
166        let mut upward_depth = 0;
167        for (i, part) in self.0.split('/').enumerate() {
168            if part.is_empty() {
169                if i == 0 {
170                    // absolute path
171                    return None;
172                }
173                canonicalized = false;
174            } else if part == "." {
175                canonicalized = false;
176            } else if part == ".." {
177                if parts_len == 0 {
178                    upward_depth += 1;
179                    if upward_depth > max_upward {
180                        // escapes root
181                        return None;
182                    }
183                } else {
184                    parts_len -= 1;
185                }
186
187                canonicalized = false;
188            } else {
189                if parts_len + upward_depth >= 4096 {
190                    break;
191                }
192                parts[parts_len] = part;
193                parts_len += 1;
194            }
195        }
196
197        if canonicalized {
198            return Some((self, max_upward + parts_len));
199        }
200        let mut string = String::with_capacity(self.0.len());
201        for _ in 0..upward_depth {
202            if !string.is_empty() {
203                string.push('/');
204            }
205            string.push_str("..");
206        }
207        for part in parts[..parts_len].iter() {
208            if !string.is_empty() {
209                string.push('/');
210            }
211            string.push_str(part);
212        }
213
214        // Does not use ::new() as &self is checked from NUL
215        Some((Self(string.into()), max_upward + parts_len - upward_depth))
216    }
217
218    /// Convert [`RedoxReference`] into relative path if it's not already.
219    ///
220    /// Converting the path to relative allows using [`Self::canonical_max_upward`]
221    pub fn to_relative(self) -> Self {
222        if self.is_relative() {
223            return self;
224        }
225        match self.0 {
226            Cow::Borrowed(s) => Self(s[1..].into()),
227            Cow::Owned(s) => Self(s[1..].to_string().into()),
228        }
229    }
230
231    /// Verify that the reference is canonicalized
232    pub fn is_canon(&self) -> bool {
233        (self.0.is_empty() || self.0 == "/")
234            || (!self.is_relative()
235                && self
236                    .0
237                    .split('/')
238                    .skip(1)
239                    .all(|seg| seg != ".." && seg != "." && seg != ""))
240    }
241
242    /// Get path upward one time
243    pub fn dirname<'b>(&self) -> RedoxReference<'b> {
244        // TODO: canonical() recreates a new string
245        self.join_checked(RedoxReference("..".into())).canonical()
246    }
247
248    /// Is this path relative?
249    pub fn is_relative(&self) -> bool {
250        !self.0.starts_with('/')
251    }
252
253    /// Copy the content into owned
254    pub fn into_owned<'b>(self) -> RedoxReference<'b> {
255        RedoxReference(self.0.into_owned().into())
256    }
257}
258
259impl<'a> AsRef<str> for RedoxReference<'a> {
260    fn as_ref(&self) -> &str {
261        self.0.as_ref()
262    }
263}
264
265impl<'a> fmt::Display for RedoxReference<'a> {
266    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
267        write!(f, "{}", self.0)
268    }
269}
270
271impl<'a> From<RedoxReference<'a>> for Cow<'a, str> {
272    fn from(value: RedoxReference<'a>) -> Self {
273        value.0
274    }
275}
276
277/// A fully qualified Redox path
278#[derive(Clone, Debug, Eq, Hash, PartialEq)]
279pub enum RedoxPath<'a> {
280    /// Standard UNIX compatible format
281    Standard(RedoxReference<'a>),
282    /// Legacy URI format
283    Legacy(RedoxScheme<'a>, RedoxReference<'a>),
284}
285
286impl<'a> RedoxPath<'a> {
287    /// Create [`RedoxPath`] from absolute path
288    ///
289    /// Returns `Some` on success and `None` if the path is not valid
290    pub fn from_absolute<S: Into<Cow<'a, str>>>(path: S) -> Option<Self> {
291        Self::from_absolute_checked(path).and_then(|s| s.abs())
292    }
293
294    /// Create a new [`RedoxPath`] from C string, ensuring it is a valid UTF-8
295    pub fn from_absolute_c<S: Into<&'a CStr>>(path: S) -> Option<Self> {
296        Self::from_absolute_c_checked(path).and_then(|s| s.abs())
297    }
298
299    /// Create a new [`RedoxPath`] from [`RedoxReference`] if the reference is absolute
300    pub fn from_reference(path: RedoxReference<'a>) -> Option<Self> {
301        RedoxStr::from(path).abs()
302    }
303
304    /// Create [`RedoxPath`] from C string, or return relative path if valid
305    fn from_absolute_c_checked<S: Into<&'a CStr>>(path: S) -> Option<RedoxStr<'a>> {
306        let path = path.into();
307        if matches!(path.to_bytes_with_nul().first(), Some(b'/')) {
308            // New /path or /scheme/scheme_name/path format, referenced
309            Some(RedoxStr::Absolute(Self::Standard(
310                RedoxReference::new_from_c(path)?,
311            )))
312        } else {
313            // Old scheme_name:path (copied) or relative format (referenced)
314            let s = path.to_str().ok()?;
315            // TODO: A way to skip NUL check here
316            Self::from_absolute_checked(s)
317        }
318    }
319
320    /// Create [`RedoxPath`] from absolute path, or return relative path if valid
321    fn from_absolute_checked<S: Into<Cow<'a, str>>>(path: S) -> Option<RedoxStr<'a>> {
322        let path = path.into();
323        let r = if path.starts_with('/') {
324            // New /path or /scheme/scheme_name/path format, referenced
325            Self::Standard(RedoxReference::new(path)?)
326        } else {
327            let mut parts = path.as_ref().splitn(2, ':');
328            let (first, second) = match (parts.next().and_then(RedoxScheme::new), parts.next()) {
329                (Some(a), Some(b)) => (a, b),
330                // Neither an old scheme, so it's relative, referenced
331                _ => return Some(RedoxStr::Relative(RedoxReference::new(path)?)),
332            };
333            // Old scheme_name:path format, copied
334            let scheme = first.into_owned();
335            let reference = RedoxReference::new(second.to_string())?;
336            Self::Legacy(scheme, reference)
337        };
338        Some(RedoxStr::Absolute(r))
339    }
340
341    /// Join a [`RedoxPath`] with a &str path. Relative paths will be appended to self,
342    /// absolute paths will be returned as a RedoxPath, ignoring self.
343    ///
344    /// Returns `Some` on success and `None` if the path is not valid
345    pub fn join<'b, S: Into<Cow<'b, str>>>(&self, path: S) -> Option<RedoxPath<'b>> {
346        let path = path.into();
347        Some(self.join_checked(RedoxReference::new(path)?))
348    }
349
350    /// Join, but checked from NUL
351    pub fn join_checked<'b>(&self, path: RedoxReference<'b>) -> RedoxPath<'b> {
352        if !path.is_relative() {
353            // Absolute path, replaces reference
354            RedoxPath::Standard(path)
355        } else {
356            match self {
357                Self::Standard(reference) => RedoxPath::Standard(reference.join_checked(path)),
358                Self::Legacy(scheme, reference) => RedoxPath::Legacy(
359                    RedoxScheme(scheme.0.to_string().into()),
360                    reference.join_checked(path),
361                ),
362            }
363        }
364    }
365
366    /// Canonicalize path, see [`RedoxReference::canonical`]
367    pub fn canonical(self) -> Self {
368        match self {
369            Self::Standard(reference) => Self::Standard(reference.canonical()),
370            Self::Legacy(scheme, reference) => {
371                // We cannot canonicalize legacy paths since they may need to preserve dots and
372                // slashes
373                Self::Legacy(scheme, reference)
374            }
375        }
376    }
377
378    /// Verify that the path is canonicalized.
379    ///
380    /// Returns false if any segment is ".", ".." or "".
381    /// A path that is empty is allowed and is interpreted as "/".
382    /// Legacy paths are assumed to be canonical.
383    pub fn is_canon(&self) -> bool {
384        match self {
385            Self::Standard(reference) => reference.is_canon(),
386            Self::Legacy(_scheme, _reference) => true,
387        }
388    }
389
390    /// Convert into a [`RedoxScheme`] and [`RedoxReference`].
391    /// - Standard paths will parse `/scheme/scheme_name/reference`, and anything not starting
392    ///   with `/scheme` will be parsed as being part of the `file` scheme
393    /// - Legacy paths can be instantly converted
394    pub fn as_parts(&'a self) -> Option<(RedoxScheme<'a>, RedoxReference<'a>)> {
395        let (scheme, path) = self.get_scheme_prefixed(true)?;
396        Some((scheme, RedoxReference(path.into())))
397    }
398
399    /// Get a [`RedoxScheme`] if the reference contained `/scheme/scheme_name`.
400    /// Does not fallback to default scheme if absent.
401    pub fn get_scheme(&'a self) -> Option<RedoxScheme<'a>> {
402        Some(self.get_scheme_prefixed(false)?.0)
403    }
404
405    fn get_scheme_prefixed(&'a self, fallback: bool) -> Option<(RedoxScheme<'a>, &'a str)> {
406        const SCHEME_PREFIX_LENGTH: usize = "/scheme/".len();
407        match self {
408            Self::Standard(reference) => {
409                let mut parts = reference.0.split('/');
410                if parts.next() == Some("") && parts.next() == Some("scheme") {
411                    match parts.next() {
412                        Some(scheme_name) => {
413                            // Path is in /scheme/scheme_name
414                            let scheme_length = SCHEME_PREFIX_LENGTH + scheme_name.len() + 1;
415                            let remainder = reference.0.get(scheme_length..).unwrap_or("");
416
417                            return Some((RedoxScheme::new(scheme_name)?, remainder));
418                        }
419                        None => {
420                            // Path is the root scheme
421                            return Some((RedoxScheme(Cow::from("")), ""));
422                        }
423                    }
424                }
425                if !fallback {
426                    return None;
427                }
428                // If path has no special processing, it is inside the file scheme
429                let remainder = reference.0.get(1..).unwrap_or("");
430                return Some((RedoxScheme(Cow::from(DEFAULT_SCHEME)), remainder));
431            }
432            Self::Legacy(scheme, reference) => {
433                // Legacy paths are already split
434                Some((scheme.clone(), reference.as_ref()))
435            }
436        }
437    }
438
439    /// Is the scheme for this path the same as the given string?
440    pub fn matches_scheme(&self, other: &str) -> bool {
441        if let Some((scheme, _)) = self.get_scheme_prefixed(true) {
442            scheme.0 == other
443        } else {
444            false
445        }
446    }
447
448    /// Does the scheme match the given category, e.g. "disk-"
449    pub fn is_scheme_category(&self, category: &str) -> bool {
450        if let Some((scheme, _)) = self.get_scheme_prefixed(true) {
451            let mut parts = scheme.0.splitn(2, '.');
452            if let Some(cat) = parts.next() {
453                cat == category && parts.next().is_some()
454            } else {
455                false
456            }
457        } else {
458            false
459        }
460    }
461
462    /// Is this the default scheme, "/scheme/file"?
463    pub fn is_default_scheme(&self) -> bool {
464        self.matches_scheme(DEFAULT_SCHEME)
465    }
466
467    /// Is this a Legacy format path?
468    pub fn is_legacy(&self) -> bool {
469        matches!(self, RedoxPath::Legacy(_, _))
470    }
471
472    /// Format a [`RedoxPath`] into a UNIX style path
473    pub fn to_standard(self) -> Cow<'a, str> {
474        self.to_standard_checked().0
475    }
476
477    fn to_standard_checked(self) -> RedoxReference<'a> {
478        match self {
479            RedoxPath::Standard(reference) => reference,
480            RedoxPath::Legacy(scheme, reference) => {
481                RedoxReference(format!("/scheme/{}/{}", scheme.0, reference.0).into())
482            }
483        }
484    }
485
486    /// Format a [`RedoxPath`] into a UNIX style path,
487    /// ensuring it is canonicalized
488    pub fn to_standard_canon(self) -> RedoxPath<'a> {
489        match self {
490            RedoxPath::Standard(reference) => RedoxPath::Standard(reference.canonical()),
491            RedoxPath::Legacy(scheme, reference) => {
492                canonicalize_using_scheme_checked(&scheme, RedoxStr::Relative(reference))
493            }
494        }
495    }
496
497    /// Get path upward one time
498    pub fn dirname<'b>(&self) -> RedoxPath<'b> {
499        match self {
500            RedoxPath::Standard(redox_reference) => RedoxPath::Standard(redox_reference.dirname()),
501            RedoxPath::Legacy(redox_scheme, redox_reference) => RedoxPath::Legacy(
502                RedoxScheme(redox_scheme.to_string().into()),
503                redox_reference.dirname(),
504            ),
505        }
506    }
507
508    /// Convert into [`RedoxReference`], if it an old scheme, the scheme part will be discarded.
509    pub fn to_reference(self) -> RedoxReference<'a> {
510        match self {
511            RedoxPath::Standard(redox_reference) => redox_reference,
512            RedoxPath::Legacy(_, redox_reference) => redox_reference,
513        }
514    }
515
516    /// Get [`RedoxReference`], if it an old scheme, the scheme part will be discarded.
517    pub fn as_reference(&self) -> &RedoxReference<'a> {
518        match self {
519            RedoxPath::Standard(redox_reference) => redox_reference,
520            RedoxPath::Legacy(_, redox_reference) => redox_reference,
521        }
522    }
523
524    /// Similar to [`canonicalize_using_cwd`]
525    pub fn canonicalize_as_cwd<'b>(&self, path: RedoxStr<'b>) -> RedoxPath<'b> {
526        // SAFETY: canonicalize_using_cwd_checked will None only if cwd_opt is None
527        canonicalize_using_cwd_checked(Some(self), path, false).unwrap()
528    }
529
530    /// Similar to [`canonicalize_to_standard`]
531    pub fn canonicalize_as_cwd_standard<'b>(&self, path: RedoxStr<'b>) -> RedoxPath<'b> {
532        // SAFETY: canonicalize_to_standard_checked will None only if cwd_opt is None
533        canonicalize_to_standard_checked(Some(self), path).unwrap()
534    }
535
536    /// Similar to [`canonicalize_using_cwd_with_max_upward`]
537    pub fn canonicalize_as_cwd_with_max_upward<'b>(
538        &self,
539        relpath: RedoxReference<'b>,
540        max_upward: usize,
541    ) -> Option<(RedoxPath<'b>, usize)> {
542        let (relpath, max_upward) = relpath.canonical_max_upward(max_upward)?;
543        let newpath = self.join_checked(relpath);
544        Some((newpath, max_upward))
545    }
546
547    /// Copy the content into owned
548    pub fn into_owned<'b>(self) -> RedoxPath<'b> {
549        match self {
550            RedoxPath::Standard(redox_reference) => {
551                RedoxPath::Standard(redox_reference.into_owned())
552            }
553            RedoxPath::Legacy(redox_scheme, redox_reference) => {
554                RedoxPath::Legacy(redox_scheme.into_owned(), redox_reference.into_owned())
555            }
556        }
557    }
558}
559
560impl<'a> fmt::Display for RedoxPath<'a> {
561    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
562        match self {
563            RedoxPath::Standard(reference) => {
564                write!(f, "{}", reference.0)
565            }
566            RedoxPath::Legacy(scheme, reference) => {
567                write!(f, "{}:{}", scheme.0, reference.0)
568            }
569        }
570    }
571}
572
573impl<'a> From<RedoxPath<'a>> for Cow<'a, str> {
574    fn from(value: RedoxPath<'a>) -> Self {
575        match value {
576            RedoxPath::Standard(redox_reference) => redox_reference.0,
577            a => Cow::Owned(a.to_string()),
578        }
579    }
580}
581
582/// A valid path (checked against NUL) that can be fully qualified [`RedoxPath`] or relative [`RedoxReference`]
583#[derive(Clone, Debug, Eq, Hash, PartialEq)]
584pub enum RedoxStr<'a> {
585    Absolute(RedoxPath<'a>),
586    Relative(RedoxReference<'a>),
587}
588
589impl<'a> RedoxStr<'a> {
590    /// Create a [`RedoxStr`] from a string, returns [`None`] if it contains NUL
591    pub fn new<S: Into<Cow<'a, str>>>(path: S) -> Option<RedoxStr<'a>> {
592        RedoxPath::from_absolute_checked(path)
593    }
594    /// Create a [`RedoxStr`] from a string, returns [`None`] if it not a valid UTF-8
595    pub fn new_c<S: Into<&'a CStr>>(path: S) -> Option<RedoxStr<'a>> {
596        RedoxPath::from_absolute_c_checked(path)
597    }
598    /// Get the absolute path
599    pub fn abs(self) -> Option<RedoxPath<'a>> {
600        match self {
601            RedoxStr::Absolute(redox_path) => Some(redox_path),
602            _ => None,
603        }
604    }
605    /// Get the relative path
606    pub fn rel(self) -> Option<RedoxReference<'a>> {
607        match self {
608            RedoxStr::Relative(redox_reference) => Some(redox_reference),
609            _ => None,
610        }
611    }
612    /// Is this path canonicalized?
613    pub fn is_canon(&self) -> bool {
614        match self {
615            RedoxStr::Absolute(redox_path) => redox_path.is_canon(),
616            RedoxStr::Relative(redox_reference) => redox_reference.is_canon(),
617        }
618    }
619    /// Convert into canonicalized path
620    pub fn canonical(self) -> Self {
621        match self {
622            RedoxStr::Absolute(redox_path) => RedoxStr::from(redox_path.canonical()),
623            RedoxStr::Relative(redox_reference) => RedoxStr::from(redox_reference.canonical()),
624        }
625    }
626    /// Get path upward one time
627    pub fn dirname<'b>(&self) -> RedoxStr<'b> {
628        match self {
629            RedoxStr::Absolute(redox_path) => RedoxStr::from(redox_path.dirname()),
630            RedoxStr::Relative(redox_reference) => RedoxStr::from(redox_reference.dirname()),
631        }
632    }
633    /// This is path empty?
634    pub fn is_empty(&self) -> bool {
635        match self {
636            RedoxStr::Absolute(_) => false, // Absolute path always have `/` for POSIX path or `:` for legacy path
637            RedoxStr::Relative(redox_reference) => redox_reference.as_ref().is_empty(),
638        }
639    }
640    /// Join two path
641    pub fn join<'b, S: Into<Cow<'b, str>>>(&self, path: S) -> Option<RedoxStr<'b>> {
642        match self {
643            RedoxStr::Absolute(redox_path) => Some(RedoxStr::from(redox_path.join(path)?)),
644            RedoxStr::Relative(redox_reference) => {
645                Some(RedoxStr::from(redox_reference.join(path)?))
646            }
647        }
648    }
649    /// Join two path
650    pub fn join_checked<'b>(&self, path: RedoxReference<'b>) -> RedoxStr<'b> {
651        match self {
652            RedoxStr::Absolute(redox_path) => RedoxStr::from(redox_path.join_checked(path)),
653            RedoxStr::Relative(redox_reference) => {
654                RedoxStr::from(redox_reference.join_checked(path))
655            }
656        }
657    }
658    /// Is this path relative?
659    pub fn is_relative(&self) -> bool {
660        // Does no need to check RedoxReference::is_relative because it's checked from From or new function
661        matches!(self, RedoxStr::Relative(_))
662    }
663
664    /// Copy the content into owned
665    pub fn into_owned<'b>(self) -> RedoxStr<'b> {
666        match self {
667            RedoxStr::Absolute(redox_path) => RedoxStr::Absolute(redox_path.into_owned()),
668            RedoxStr::Relative(redox_reference) => RedoxStr::Relative(redox_reference.into_owned()),
669        }
670    }
671}
672
673impl<'a> From<RedoxPath<'a>> for RedoxStr<'a> {
674    fn from(value: RedoxPath<'a>) -> Self {
675        Self::Absolute(value)
676    }
677}
678
679impl<'a> From<RedoxReference<'a>> for RedoxStr<'a> {
680    fn from(value: RedoxReference<'a>) -> Self {
681        // try to check if it absolute
682        if !value.is_relative() {
683            RedoxStr::Absolute(RedoxPath::Standard(value))
684        } else {
685            RedoxStr::Relative(value)
686        }
687    }
688}
689impl<'a> From<RedoxStr<'a>> for Cow<'a, str> {
690    fn from(value: RedoxStr<'a>) -> Self {
691        match value {
692            RedoxStr::Absolute(redox_path) => redox_path.into(),
693            RedoxStr::Relative(redox_reference) => redox_reference.into(),
694        }
695    }
696}
697
698/// Make a relative path absolute using an optional current working directory.
699///
700/// Given a cwd of "/scheme/scheme_name/dir_name", this function will turn
701/// path "foo" into /scheme/scheme_name/dir_name/foo".
702/// "/foo" will be left as is, because it is already absolute.
703/// "." and empty segments "//" will be removed.
704/// ".." will be resolved by backing up one directory, except at the root,
705/// where ".." will be ignored and removed.
706///
707/// For old format schemes,
708/// given a cwd of "scheme:/path", this function will turn "foo" into "scheme:/path/foo".
709/// "/foo" will turn into "file:/foo". "bar:/foo" will be used directly, as it is already
710/// absolute.
711pub fn canonicalize_using_cwd<'a>(cwd_opt: Option<&'a str>, path: &'a str) -> Option<Cow<'a, str>> {
712    let cwd_opt = cwd_opt.and_then(|s| RedoxPath::from_absolute(Cow::Borrowed(s)));
713    let absolute = canonicalize_using_cwd_checked(
714        cwd_opt.as_ref(),
715        RedoxPath::from_absolute_checked(path)?,
716        false,
717    )?;
718    Some(absolute.into())
719}
720
721/// `canonicalize_using_cwd` but for internal use.
722/// `in_scheme_root`: Treat cwd_opt as the root of scheme, similar to
723///  openat RESOLVE_IN_ROOT but still allowing access to other scheme root.
724fn canonicalize_using_cwd_checked<'a, 'b>(
725    cwd_opt: Option<&RedoxPath<'a>>,
726    path: RedoxStr<'b>,
727    in_scheme_root: bool,
728) -> Option<RedoxPath<'b>> {
729    let absolute = match (path, in_scheme_root) {
730        (RedoxStr::Absolute(absolute), true) if absolute.get_scheme().is_some() => absolute,
731        (RedoxStr::Absolute(absolute), true) => {
732            cwd_opt?.join_checked(absolute.to_standard_checked().to_relative())
733        }
734        (RedoxStr::Absolute(absolute), false) => absolute,
735        (RedoxStr::Relative(relative), true) => cwd_opt?.join_checked(relative.canonical()),
736        (RedoxStr::Relative(relative), false) => cwd_opt?.join_checked(relative),
737    };
738    Some(absolute.canonical())
739}
740
741/// Canonicalize as in [`canonicalize_using_cwd`], but Legacy format paths
742/// are canonicalized into UNIX format.
743pub fn canonicalize_to_standard<'a>(
744    cwd_opt: Option<&'a str>,
745    path: &'a str,
746) -> Option<Cow<'a, str>> {
747    let absolute = match RedoxPath::from_absolute(path) {
748        Some(absolute) => absolute,
749        None => {
750            let absolute = RedoxPath::from_absolute(cwd_opt?)?;
751            absolute.join(path)?
752        }
753    };
754    Some(absolute.to_standard_canon().into())
755}
756
757/// [`canonicalize_to_standard`] but for internal use.
758fn canonicalize_to_standard_checked<'a, 'b>(
759    cwd_opt: Option<&RedoxPath<'a>>,
760    path: RedoxStr<'b>,
761) -> Option<RedoxPath<'b>> {
762    let path = match path {
763        RedoxStr::Absolute(absolute) => absolute,
764        RedoxStr::Relative(relative) => cwd_opt?.join_checked(relative),
765    }
766    .to_standard_canon();
767    Some(path)
768}
769
770/// Make a path that is relative to the root of a scheme into a full path,
771/// following the rules of [`canonicalize_using_cwd`].
772///
773/// Returns the Some if a valid path can be constructed,
774/// None if the scheme name is not valid or some other error occurs.
775/// The returned value is guaranteed to be UNIX, but not guaranteed to be equal with given scheme.
776pub fn canonicalize_using_scheme<'a>(scheme: &'a str, path: &'a str) -> Option<Cow<'a, str>> {
777    let path = RedoxPath::from_absolute_checked(path)?;
778    Some(canonicalize_using_scheme_checked(&RedoxScheme::new(scheme)?, path).into())
779}
780
781/// [`canonicalize_using_scheme`] but for internal use.
782fn canonicalize_using_scheme_checked<'a, 'b>(
783    scheme: &RedoxScheme<'_>,
784    path: RedoxStr<'b>,
785) -> RedoxPath<'b> {
786    let path = match path {
787        RedoxStr::Absolute(s) => {
788            if !s.is_legacy() && s.matches_scheme(&scheme.0) {
789                // Canonicalized and being in the same scheme, return early
790                // to avoid allocation from scheme_path_checked
791                return s;
792            }
793            RedoxStr::Absolute(s)
794        }
795        path => path,
796    };
797    // SAFETY: canonicalize_using_cwd_checked will only None if cwd_opt is None
798    canonicalize_using_cwd_checked(Some(&scheme_path_checked(&scheme)), path, true).unwrap()
799}
800
801/// Make a path that is relative to the root of a scheme into a full path,
802/// following the rules of [`canonicalize_using_cwd`], but with upward_max to ensure
803/// the returned path stays within the true root.
804pub fn canonicalize_using_cwd_with_max_upward<'a>(
805    cwd: &str,
806    relpath: &'a str,
807    max_upward: usize,
808) -> Option<(Cow<'a, str>, usize)> {
809    // Using RedoxStr so cwd using relative path (as in scheme) is also accepted
810    let cwd = RedoxStr::new(Cow::Borrowed(cwd))?;
811    let (absolute, max_upward) = canonicalize_using_cwd_with_max_upward_checked(
812        &cwd,
813        RedoxStr::new(relpath)?.rel()?,
814        max_upward,
815    )?;
816    Some((absolute.into(), max_upward))
817}
818
819/// [`canonicalize_using_cwd_with_max_upward`] but for internal use.
820fn canonicalize_using_cwd_with_max_upward_checked<'a, 'b>(
821    cwd: &RedoxStr<'a>,
822    relpath: RedoxReference<'b>,
823    max_upward: usize,
824) -> Option<(RedoxStr<'b>, usize)> {
825    let (relpath, max_upward) = relpath.canonical_max_upward(max_upward)?;
826    let relnew = cwd.join_checked(relpath);
827    Some((relnew, max_upward))
828}
829
830/// The standard path for a given scheme name
831///
832/// Returns Some if the scheme name is valid, None otherwise
833pub fn scheme_path(name: &str) -> Option<RedoxPath<'_>> {
834    let scheme = RedoxScheme::new(name)?;
835    Some(scheme_path_checked(&scheme))
836}
837
838/// [`scheme_path`] but for internal use.
839fn scheme_path_checked<'a, 'b>(scheme: &'a RedoxScheme<'a>) -> RedoxPath<'b> {
840    if scheme.0.is_empty() {
841        return RedoxPath::Standard(RedoxReference("/scheme".into()));
842    }
843    RedoxPath::Standard(RedoxReference(format!("/scheme/{}", scheme.0).into()))
844}
845
846/// Make a scheme name (not a full path) for a device of a given category
847///
848/// Returns Some if the resulting scheme name is valid, None otherwise
849pub fn make_scheme_name<'a, 'b>(category: &'a str, detail: &'a str) -> Option<RedoxScheme<'b>> {
850    let name = format!("{}.{}", category, detail);
851    RedoxScheme::new(name)
852}
853
854#[cfg(test)]
855mod tests {
856    use super::*;
857    use alloc::format;
858
859    // Tests absolute paths without scheme
860    #[test]
861    fn test_absolute() {
862        let cwd_opt = None;
863        assert_eq!(
864            canonicalize_using_cwd(cwd_opt, "/"),
865            Some(Cow::Borrowed("/"))
866        );
867        assert_eq!(
868            canonicalize_using_cwd(cwd_opt, "/file"),
869            Some(Cow::Borrowed("/file"))
870        );
871        assert_eq!(
872            canonicalize_using_cwd(cwd_opt, "/folder/file"),
873            Some(Cow::Borrowed("/folder/file"))
874        );
875        assert_eq!(
876            canonicalize_using_cwd(cwd_opt, "/folder/file/"),
877            Some(Cow::Borrowed("/folder/file"))
878        );
879        assert_eq!(
880            canonicalize_using_cwd(cwd_opt, "/folder/file//"),
881            Some(Cow::Borrowed("/folder/file"))
882        );
883        assert_eq!(
884            canonicalize_using_cwd(cwd_opt, "/folder/../file"),
885            Some(Cow::Borrowed("/file"))
886        );
887        assert_eq!(
888            canonicalize_using_cwd(cwd_opt, "/folder/../.."),
889            Some(Cow::Borrowed("/"))
890        );
891        assert_eq!(
892            canonicalize_using_cwd(cwd_opt, "/folder/../../../.."),
893            Some(Cow::Borrowed("/"))
894        );
895        assert_eq!(
896            canonicalize_using_cwd(cwd_opt, "/.."),
897            Some(Cow::Borrowed("/"))
898        );
899    }
900
901    // Test relative paths using new scheme
902    #[test]
903    fn test_new_relative() {
904        let cwd_opt = Some("/scheme/foo");
905        assert_eq!(
906            canonicalize_using_cwd(cwd_opt, "file"),
907            Some(Cow::Borrowed("/scheme/foo/file"))
908        );
909        assert_eq!(
910            canonicalize_using_cwd(cwd_opt, "folder/file"),
911            Some(Cow::Borrowed("/scheme/foo/folder/file"))
912        );
913        assert_eq!(
914            canonicalize_using_cwd(cwd_opt, "folder/../file"),
915            Some(Cow::Borrowed("/scheme/foo/file"))
916        );
917        assert_eq!(
918            canonicalize_using_cwd(cwd_opt, "folder/../.."),
919            Some(Cow::Borrowed("/scheme"))
920        );
921        assert_eq!(
922            canonicalize_using_cwd(cwd_opt, "folder/../../../.."),
923            Some(Cow::Borrowed("/"))
924        );
925        assert_eq!(
926            canonicalize_using_cwd(cwd_opt, ".."),
927            Some(Cow::Borrowed("/scheme"))
928        );
929    }
930
931    // Tests paths prefixed with /scheme/
932    #[test]
933    fn test_new_scheme() {
934        let cwd_opt = None;
935        assert_eq!(
936            canonicalize_using_cwd(cwd_opt, "/scheme/bar/"),
937            Some(Cow::Borrowed("/scheme/bar"))
938        );
939        assert_eq!(
940            canonicalize_using_cwd(cwd_opt, "/scheme/bar/file"),
941            Some(Cow::Borrowed("/scheme/bar/file"))
942        );
943        assert_eq!(
944            canonicalize_using_cwd(cwd_opt, "/scheme/bar/folder/file"),
945            Some(Cow::Borrowed("/scheme/bar/folder/file"))
946        );
947        assert_eq!(
948            canonicalize_using_cwd(cwd_opt, "/scheme/bar/folder/../file"),
949            Some(Cow::Borrowed("/scheme/bar/file"))
950        );
951        assert_eq!(
952            canonicalize_using_cwd(cwd_opt, "/scheme/bar/folder/../.."),
953            Some(Cow::Borrowed("/scheme"))
954        );
955        assert_eq!(
956            canonicalize_using_cwd(cwd_opt, "/scheme/bar/folder/../../../.."),
957            Some(Cow::Borrowed("/"))
958        );
959        assert_eq!(
960            canonicalize_using_cwd(cwd_opt, "/scheme/bar/.."),
961            Some(Cow::Borrowed("/scheme"))
962        );
963
964        assert_eq!(
965            canonicalize_using_scheme("bar", ""),
966            Some(Cow::Borrowed("/scheme/bar"))
967        );
968        assert_eq!(
969            canonicalize_using_scheme("bar", "foo"),
970            Some(Cow::Borrowed("/scheme/bar/foo"))
971        );
972        assert_eq!(
973            canonicalize_using_scheme("foo", "/bar"),
974            Some(Cow::Borrowed("/scheme/foo/bar"))
975        );
976        assert_eq!(
977            canonicalize_using_scheme("bar", ".."),
978            Some(Cow::Borrowed("/scheme/bar"))
979        );
980        assert_eq!(
981            canonicalize_using_scheme("file", "/scheme/foo/bar"),
982            Some(Cow::Borrowed("/scheme/foo/bar"))
983        );
984    }
985
986    // Test relative paths using old scheme
987    #[test]
988    fn test_old_relative() {
989        let cwd_opt = Some("foo:");
990        assert_eq!(
991            canonicalize_using_cwd(cwd_opt, "file"),
992            Some(Cow::Borrowed("foo:file"))
993        );
994        assert_eq!(
995            canonicalize_using_cwd(cwd_opt, "folder/file"),
996            Some(Cow::Borrowed("foo:folder/file"))
997        );
998        assert_eq!(
999            canonicalize_using_cwd(cwd_opt, "folder/../file"),
1000            Some(Cow::Borrowed("foo:folder/../file"))
1001        );
1002        assert_eq!(
1003            canonicalize_using_cwd(cwd_opt, "folder/../.."),
1004            Some(Cow::Borrowed("foo:folder/../.."))
1005        );
1006        assert_eq!(
1007            canonicalize_using_cwd(cwd_opt, "folder/../../../.."),
1008            Some(Cow::Borrowed("foo:folder/../../../.."))
1009        );
1010        assert_eq!(
1011            canonicalize_using_cwd(cwd_opt, ".."),
1012            Some(Cow::Borrowed("foo:.."))
1013        );
1014    }
1015
1016    // Tests paths prefixed with scheme_name:
1017    #[test]
1018    fn test_old_scheme() {
1019        let cwd_opt = None;
1020        assert_eq!(
1021            canonicalize_using_cwd(cwd_opt, "bar:"),
1022            Some(Cow::Borrowed("bar:"))
1023        );
1024        assert_eq!(
1025            canonicalize_using_cwd(cwd_opt, "bar:file"),
1026            Some(Cow::Borrowed("bar:file"))
1027        );
1028        assert_eq!(
1029            canonicalize_using_cwd(cwd_opt, "bar:folder/file"),
1030            Some(Cow::Borrowed("bar:folder/file"))
1031        );
1032        assert_eq!(
1033            canonicalize_using_cwd(cwd_opt, "bar:folder/../file"),
1034            Some(Cow::Borrowed("bar:folder/../file"))
1035        );
1036        assert_eq!(
1037            canonicalize_using_cwd(cwd_opt, "bar:folder/../.."),
1038            Some(Cow::Borrowed("bar:folder/../.."))
1039        );
1040        assert_eq!(
1041            canonicalize_using_cwd(cwd_opt, "bar:folder/../../../.."),
1042            Some(Cow::Borrowed("bar:folder/../../../.."))
1043        );
1044        assert_eq!(
1045            canonicalize_using_cwd(cwd_opt, "bar:.."),
1046            Some(Cow::Borrowed("bar:.."))
1047        );
1048    }
1049
1050    // Tests max upward for symlink
1051    #[test]
1052    fn test_max_upward() {
1053        // new format
1054        assert_eq!(
1055            canonicalize_using_cwd_with_max_upward("/", "foo/bar", 0),
1056            Some((Cow::Borrowed("/foo/bar"), 2))
1057        );
1058        assert_eq!(
1059            canonicalize_using_cwd_with_max_upward("/foo/", "bar/../baz/qux", 0),
1060            Some((Cow::Borrowed("/foo/baz/qux"), 2))
1061        );
1062        assert_eq!(
1063            canonicalize_using_cwd_with_max_upward("foo/../bar", "baz/./../qux", 0),
1064            Some((Cow::Borrowed("foo/../bar/qux"), 1))
1065        );
1066        assert_eq!(
1067            canonicalize_using_cwd_with_max_upward("/foo", "../bar", 1),
1068            Some((Cow::Borrowed("/foo/../bar"), 1))
1069        );
1070        assert_eq!(
1071            canonicalize_using_cwd_with_max_upward("foo/..", "../bar/..", 2),
1072            Some((Cow::Borrowed("foo/../.."), 1))
1073        );
1074        // old format
1075        assert_eq!(
1076            canonicalize_using_cwd_with_max_upward("foo:", "bar/baz", 0),
1077            Some((Cow::Borrowed("foo:bar/baz"), 2))
1078        );
1079        assert_eq!(
1080            canonicalize_using_cwd_with_max_upward("foo:", "bar/baz//../../", 0),
1081            Some((Cow::Borrowed("foo:"), 0))
1082        );
1083        assert_eq!(
1084            canonicalize_using_cwd_with_max_upward("foo:/bar/..", "baz/./qux/..", 0),
1085            Some((Cow::Borrowed("foo:/bar/../baz"), 1))
1086        );
1087        // neg tests
1088        assert_eq!(
1089            canonicalize_using_cwd_with_max_upward("/foo", "../bar", 0),
1090            None
1091        );
1092        assert_eq!(
1093            canonicalize_using_cwd_with_max_upward("/foo", "/bar", 0),
1094            None
1095        );
1096        assert_eq!(
1097            canonicalize_using_cwd_with_max_upward("/foo/..", "../bar/..", 0),
1098            None
1099        );
1100    }
1101    // Test whether paths are borrowed or owned
1102    #[test]
1103    fn test_cow_borrowing() {
1104        let cwd_opt = None;
1105        for (path, should_owned) in [
1106            ("/foo/bar/baz", false),
1107            ("/scheme/foo/bar/baz", false),
1108            ("foo/bar/baz", false), // this is None(Cow::Borrowed)
1109            ("/scheme/foo/../baz", true),
1110            ("foo:bar", true),
1111        ] {
1112            assert_eq!(
1113                matches!(canonicalize_using_cwd(cwd_opt, path), Some(Cow::Owned(_))),
1114                should_owned
1115            );
1116        }
1117    }
1118    // Test whether dirname works
1119    #[test]
1120    fn test_dirname() {
1121        for (a, b) in [
1122            ("/foo/bar/baz", "/foo/bar"),
1123            ("/baz/foo/bar/", "/baz/foo"),
1124            ("/", "/"),
1125            ("/scheme/foo", "/scheme"),
1126            ("foo:bar/baz/", "foo:bar"),
1127            ("foo:bar", "foo:"),
1128            ("foo:", "foo:"),
1129        ] {
1130            assert_eq!(
1131                RedoxPath::from_absolute(a).unwrap().dirname(),
1132                RedoxPath::from_absolute(b).unwrap()
1133            );
1134        }
1135    }
1136
1137    // Tests paths that may be used with orbital:
1138    #[test]
1139    fn test_orbital_scheme() {
1140        for flag_str in &["", "abflrtu"] {
1141            for x in &[-1, 0, 1] {
1142                for y in &[-1, 0, 1] {
1143                    for w in &[0, 1] {
1144                        for h in &[0, 1] {
1145                            for title in &[
1146                                "",
1147                                "title",
1148                                "title/with/slashes",
1149                                "title:with:colons",
1150                                "title/../with/../dots/..",
1151                            ] {
1152                                let path = format!(
1153                                    "orbital:{}/{}/{}/{}/{}/{}",
1154                                    flag_str, x, y, w, h, title
1155                                );
1156                                assert_eq!(
1157                                    canonicalize_using_cwd(None, &path),
1158                                    Some((&path).into())
1159                                );
1160                            }
1161                        }
1162                    }
1163                }
1164            }
1165        }
1166    }
1167
1168    // Tests path splitting to parts
1169    #[test]
1170    fn test_parts() {
1171        for (path, scheme, reference) in [
1172            ("/foo/bar/baz", "file", "foo/bar/baz"),
1173            ("/scheme/foo/bar/baz", "foo", "bar/baz"),
1174            ("/", "file", ""),
1175            ("/bar", "file", "bar"),
1176            ("/...", "file", "..."),
1177            ("//double/slash", "file", "/double/slash"),
1178            ("/ending/in/slash/", "file", "ending/in/slash/"),
1179            ("/contains/dot/.", "file", "contains/dot/."),
1180            ("/contains/dotdot/..", "file", "contains/dotdot/.."),
1181        ] {
1182            let redox_path = RedoxPath::from_absolute(path).unwrap();
1183            let parts = redox_path.as_parts();
1184            assert_eq!(
1185                (path, parts),
1186                (
1187                    path,
1188                    Some((
1189                        RedoxScheme::new(scheme).unwrap(),
1190                        RedoxReference::new(reference).unwrap()
1191                    ))
1192                )
1193            );
1194            if path.starts_with("/scheme") {
1195                let to_string = format!("/scheme/{scheme}");
1196                let joined_path = RedoxPath::from_absolute(&to_string)
1197                    .unwrap()
1198                    .join(reference)
1199                    .unwrap();
1200                assert_eq!(path, &format!("{joined_path}"));
1201            } else {
1202                assert_eq!(path, &format!("/{reference}"));
1203            }
1204        }
1205
1206        // fail if the path is not absolute
1207        assert_eq!(RedoxPath::from_absolute("not/absolute"), None);
1208
1209        // fail if the scheme is not properly canonicalized
1210        for path in ["/scheme/use:colon/"] {
1211            let redox_path = RedoxPath::from_absolute(path).unwrap();
1212            let parts = redox_path.as_parts();
1213            assert_eq!((path, parts), (path, None));
1214        }
1215    }
1216
1217    #[test]
1218    fn test_old_scheme_parts() {
1219        for (path, scheme, reference) in [
1220            ("foo:bar/baz", "foo", "bar/baz"),
1221            ("emptyref:", "emptyref", ""),
1222            (":emptyscheme", "", "emptyscheme"),
1223        ] {
1224            let redox_path = RedoxPath::from_absolute(path).unwrap();
1225            let parts = redox_path.as_parts();
1226            assert_eq!(
1227                (path, parts),
1228                (
1229                    path,
1230                    Some((
1231                        RedoxScheme::new(scheme).unwrap(),
1232                        RedoxReference::new(reference).unwrap()
1233                    ))
1234                )
1235            );
1236        }
1237
1238        // slash is not allowed in scheme names
1239        assert_eq!(RedoxPath::from_absolute("scheme/withslash:path"), None);
1240        // empty path is not allowed for from_absolute
1241        assert_eq!(RedoxPath::from_absolute(""), None)
1242    }
1243
1244    #[test]
1245    fn test_matches() {
1246        assert!(RedoxPath::from_absolute("/scheme/foo")
1247            .unwrap()
1248            .matches_scheme("foo"));
1249        assert!(RedoxPath::from_absolute("/scheme/foo/bar")
1250            .unwrap()
1251            .matches_scheme("foo"));
1252        assert!(!RedoxPath::from_absolute("/scheme/foo")
1253            .unwrap()
1254            .matches_scheme("bar"));
1255        assert!(RedoxPath::from_absolute("foo:")
1256            .unwrap()
1257            .matches_scheme("foo"));
1258        assert!(RedoxPath::from_absolute(
1259            canonicalize_using_cwd(Some("/scheme/foo"), "bar").unwrap()
1260        )
1261        .unwrap()
1262        .matches_scheme("foo"));
1263        assert!(
1264            RedoxPath::from_absolute(canonicalize_using_cwd(Some("/foo"), "bar").unwrap())
1265                .unwrap()
1266                .matches_scheme("file")
1267        );
1268        assert!(RedoxPath::from_absolute(
1269            canonicalize_using_cwd(Some("/scheme"), "foo/bar").unwrap()
1270        )
1271        .unwrap()
1272        .matches_scheme("foo"));
1273        assert!(RedoxPath::from_absolute("foo:/bar")
1274            .unwrap()
1275            .matches_scheme("foo"));
1276        assert!(!RedoxPath::from_absolute("foo:/bar")
1277            .unwrap()
1278            .matches_scheme("bar"));
1279        assert!(RedoxPath::from_absolute("/scheme/file")
1280            .unwrap()
1281            .is_default_scheme());
1282        assert!(!RedoxPath::from_absolute("/scheme/foo")
1283            .unwrap()
1284            .is_default_scheme());
1285        assert!(RedoxPath::from_absolute("file:bar")
1286            .unwrap()
1287            .is_default_scheme());
1288        assert!(RedoxPath::from_absolute("file:")
1289            .unwrap()
1290            .is_default_scheme());
1291        assert!(!RedoxPath::from_absolute("foo:bar")
1292            .unwrap()
1293            .is_default_scheme());
1294        assert!(RedoxPath::from_absolute("foo:bar").unwrap().is_legacy());
1295        assert!(!RedoxPath::from_absolute("/foo/bar").unwrap().is_legacy());
1296    }
1297
1298    #[test]
1299    fn test_to_standard() {
1300        assert_eq!(
1301            &RedoxPath::from_absolute("foo:bar").unwrap().to_standard(),
1302            "/scheme/foo/bar"
1303        );
1304        assert_eq!(
1305            &RedoxPath::from_absolute("file:bar").unwrap().to_standard(),
1306            "/scheme/file/bar"
1307        );
1308        assert_eq!(
1309            &RedoxPath::from_absolute("/scheme/foo/bar")
1310                .unwrap()
1311                .to_standard(),
1312            "/scheme/foo/bar"
1313        );
1314        assert_eq!(
1315            &RedoxPath::from_absolute("/foo/bar").unwrap().to_standard(),
1316            "/foo/bar"
1317        );
1318        assert_eq!(
1319            &RedoxPath::from_absolute("foo:bar/../bar2")
1320                .unwrap()
1321                .to_standard_canon()
1322                .to_string(),
1323            "/scheme/foo/bar2"
1324        );
1325        assert_eq!(
1326            &RedoxPath::from_absolute("file:bar/./../bar2")
1327                .unwrap()
1328                .to_standard_canon()
1329                .to_string(),
1330            "/scheme/file/bar2"
1331        );
1332        assert_eq!(
1333            &RedoxPath::from_absolute("/scheme/file/bar/./../../foo/bar")
1334                .unwrap()
1335                .to_standard_canon()
1336                .to_string(),
1337            "/scheme/foo/bar"
1338        );
1339        assert_eq!(
1340            &RedoxPath::from_absolute("/foo/bar")
1341                .unwrap()
1342                .to_standard_canon()
1343                .to_string(),
1344            "/foo/bar"
1345        );
1346        assert_eq!(
1347            &canonicalize_to_standard(None, "/scheme/foo/bar").unwrap(),
1348            "/scheme/foo/bar"
1349        );
1350        assert_eq!(
1351            &canonicalize_to_standard(None, "foo:bar").unwrap(),
1352            "/scheme/foo/bar"
1353        );
1354        assert_eq!(
1355            &canonicalize_to_standard(None, "foo:bar/../..").unwrap(),
1356            "/scheme/foo"
1357        );
1358        assert_eq!(
1359            &canonicalize_to_standard(None, "/scheme/foo/bar/..").unwrap(),
1360            "/scheme/foo"
1361        );
1362        assert_eq!(
1363            &canonicalize_to_standard(None, "foo:bar/bar2/..").unwrap(),
1364            "/scheme/foo/bar"
1365        );
1366    }
1367
1368    #[test]
1369    fn test_scheme_path() {
1370        assert_eq!(
1371            scheme_path("foo"),
1372            Some(RedoxPath::from_absolute("/scheme/foo").unwrap())
1373        );
1374        assert_eq!(
1375            scheme_path(""),
1376            Some(RedoxPath::from_absolute("/scheme").unwrap())
1377        );
1378        assert_eq!(scheme_path("/foo"), None);
1379        assert_eq!(scheme_path("foo/bar"), None);
1380        assert_eq!(scheme_path("foo:"), None);
1381    }
1382
1383    #[test]
1384    fn test_category() {
1385        assert_eq!(
1386            make_scheme_name("foo", "bar"),
1387            Some(RedoxScheme::new("foo.bar").unwrap())
1388        );
1389        assert_eq!(
1390            RedoxPath::from_absolute(
1391                scheme_path(make_scheme_name("foo", "bar").unwrap().as_ref()).unwrap()
1392            )
1393            .unwrap(),
1394            RedoxPath::Standard(RedoxReference::new("/scheme/foo.bar").unwrap())
1395        );
1396        assert_eq!(make_scheme_name("foo", "/bar"), None);
1397        assert_eq!(make_scheme_name("foo", ":bar"), None);
1398        assert!(RedoxPath::from_absolute(
1399            scheme_path(make_scheme_name("foo", "bar").unwrap().as_ref()).unwrap()
1400        )
1401        .unwrap()
1402        .is_scheme_category("foo"));
1403        assert!(RedoxPath::from_absolute("/scheme/foo.bar/bar2")
1404            .unwrap()
1405            .is_scheme_category("foo"));
1406        assert!(!RedoxPath::from_absolute("/scheme/foo/bar")
1407            .unwrap()
1408            .is_scheme_category("foo"));
1409        assert!(!RedoxPath::from_absolute("/foo.bar/bar2")
1410            .unwrap()
1411            .is_scheme_category("foo"));
1412    }
1413}