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