Skip to main content

object_store/path/
mod.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! Path abstraction for Object Storage
19
20use percent_encoding::percent_decode;
21use std::fmt::Formatter;
22#[cfg(not(target_arch = "wasm32"))]
23use url::Url;
24
25/// The delimiter to separate object namespaces, creating a directory structure.
26pub const DELIMITER: &str = "/";
27
28/// The path delimiter as a single byte
29pub const DELIMITER_BYTE: u8 = DELIMITER.as_bytes()[0];
30
31/// The path delimiter as a single char
32pub const DELIMITER_CHAR: char = DELIMITER_BYTE as char;
33
34mod parts;
35
36pub use parts::{InvalidPart, PathPart, PathParts};
37
38/// Error returned by [`Path::parse`]
39#[derive(Debug, thiserror::Error)]
40#[non_exhaustive]
41pub enum Error {
42    /// Error when there's an empty segment between two slashes `/` in the path
43    #[error("Path \"{}\" contained empty path segment", path)]
44    EmptySegment {
45        /// The source path
46        path: String,
47    },
48
49    /// Error when an invalid segment is encountered in the given path
50    #[error("Error parsing Path \"{}\": {}", path, source)]
51    BadSegment {
52        /// The source path
53        path: String,
54        /// The part containing the error
55        source: InvalidPart,
56    },
57
58    /// Error when path cannot be canonicalized
59    #[error("Failed to canonicalize path \"{}\": {}", path.display(), source)]
60    Canonicalize {
61        /// The source path
62        path: std::path::PathBuf,
63        /// The underlying error
64        source: std::io::Error,
65    },
66
67    /// Error when the path is not a valid URL
68    #[error("Unable to convert path \"{}\" to URL", path.display())]
69    InvalidPath {
70        /// The source path
71        path: std::path::PathBuf,
72    },
73
74    /// Error when a path contains non-unicode characters
75    #[error("Path \"{}\" contained non-unicode characters: {}", path, source)]
76    NonUnicode {
77        /// The source path
78        path: String,
79        /// The underlying `UTF8Error`
80        source: std::str::Utf8Error,
81    },
82
83    /// Error when the a path doesn't start with given prefix
84    #[error("Path {} does not start with prefix {}", path, prefix)]
85    PrefixMismatch {
86        /// The source path
87        path: String,
88        /// The mismatched prefix
89        prefix: String,
90    },
91}
92
93/// A parsed path representation that can be safely written to object storage
94///
95/// A [`Path`] maintains the following invariants:
96///
97/// * Paths are delimited by `/`
98/// * Paths do not contain leading or trailing `/`
99/// * Paths do not contain relative path segments, i.e. `.` or `..`
100/// * Paths do not contain empty path segments
101/// * Paths do not contain any ASCII control characters
102///
103/// There are no enforced restrictions on path length, however, it should be noted that most
104/// object stores do not permit paths longer than 1024 bytes, and many filesystems do not
105/// support path segments longer than 255 bytes.
106///
107/// # Encode
108///
109/// In theory object stores support any UTF-8 character sequence, however, certain character
110/// sequences cause compatibility problems with some applications and protocols. Additionally
111/// some filesystems may impose character restrictions, see [`LocalFileSystem`]. As such the
112/// naming guidelines for [S3], [GCS] and [Azure Blob Storage] all recommend sticking to a
113/// limited character subset.
114///
115/// [S3]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-keys.html
116/// [GCS]: https://cloud.google.com/storage/docs/naming-objects
117/// [Azure Blob Storage]: https://docs.microsoft.com/en-us/rest/api/storageservices/Naming-and-Referencing-Containers--Blobs--and-Metadata#blob-names
118///
119/// A string containing potentially problematic path segments can therefore be encoded to a [`Path`]
120/// using [`Path::from`] or [`Path::from_iter`]. This will percent encode any problematic
121/// segments according to [RFC 1738].
122///
123/// ```
124/// # use object_store::path::Path;
125/// assert_eq!(Path::from("foo/bar").as_ref(), "foo/bar");
126/// assert_eq!(Path::from("foo//bar").as_ref(), "foo/bar");
127/// assert_eq!(Path::from("foo/../bar").as_ref(), "foo/%2E%2E/bar");
128/// assert_eq!(Path::from("/").as_ref(), "");
129/// assert_eq!(Path::from_iter(["foo", "foo/bar"]).as_ref(), "foo/foo%2Fbar");
130/// ```
131///
132/// Note: if provided with an already percent encoded string, this will encode it again
133///
134/// ```
135/// # use object_store::path::Path;
136/// assert_eq!(Path::from("foo/foo%2Fbar").as_ref(), "foo/foo%252Fbar");
137/// ```
138///
139/// # Parse
140///
141/// Alternatively a [`Path`] can be parsed from an existing string, returning an
142/// error if it is invalid. Unlike the encoding methods above, this will permit
143/// arbitrary unicode, including percent encoded sequences.
144///
145/// ```
146/// # use object_store::path::Path;
147/// assert_eq!(Path::parse("/foo/foo%2Fbar").unwrap().as_ref(), "foo/foo%2Fbar");
148/// Path::parse("..").unwrap_err(); // Relative path segments are disallowed
149/// Path::parse("/foo//").unwrap_err(); // Empty path segments are disallowed
150/// Path::parse("\x00").unwrap_err(); // ASCII control characters are disallowed
151/// ```
152///
153/// [RFC 1738]: https://www.ietf.org/rfc/rfc1738.txt
154/// [`LocalFileSystem`]: crate::local::LocalFileSystem
155#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Ord, PartialOrd)]
156pub struct Path {
157    /// The raw path with no leading or trailing delimiters
158    raw: String,
159}
160
161impl Path {
162    /// An empty [`Path`] that points to the root of the store, equivalent to `Path::from("/")`.
163    ///
164    /// See also [`Path::is_root`].
165    ///
166    /// # Example
167    ///
168    /// ```
169    /// # use object_store::path::Path;
170    /// assert_eq!(Path::ROOT, Path::from("/"));
171    /// ```
172    pub const ROOT: Self = Self { raw: String::new() };
173
174    /// Parse a string as a [`Path`], returning a [`Error`] if invalid,
175    /// as defined on the docstring for [`Path`]
176    ///
177    /// Note: this will strip any leading `/` or trailing `/`
178    pub fn parse(path: impl AsRef<str>) -> Result<Self, Error> {
179        let path = path.as_ref();
180
181        let stripped = path.strip_prefix(DELIMITER).unwrap_or(path);
182        if stripped.is_empty() {
183            return Ok(Default::default());
184        }
185
186        let stripped = stripped.strip_suffix(DELIMITER).unwrap_or(stripped);
187
188        for segment in stripped.split(DELIMITER) {
189            if segment.is_empty() {
190                return Err(Error::EmptySegment { path: path.into() });
191            }
192
193            PathPart::parse(segment).map_err(|source| {
194                let path = path.into();
195                Error::BadSegment { source, path }
196            })?;
197        }
198
199        Ok(Self {
200            raw: stripped.to_string(),
201        })
202    }
203
204    #[cfg(not(target_arch = "wasm32"))]
205    /// Convert a filesystem path to a [`Path`] relative to the filesystem root
206    ///
207    /// This will return an error if the path contains illegal character sequences
208    /// as defined on the docstring for [`Path`] or does not exist
209    ///
210    /// Note: this will canonicalize the provided path, resolving any symlinks
211    pub fn from_filesystem_path(path: impl AsRef<std::path::Path>) -> Result<Self, Error> {
212        let absolute = std::fs::canonicalize(&path).map_err(|source| {
213            let path = path.as_ref().into();
214            Error::Canonicalize { source, path }
215        })?;
216
217        Self::from_absolute_path(absolute)
218    }
219
220    #[cfg(not(target_arch = "wasm32"))]
221    /// Convert an absolute filesystem path to a [`Path`] relative to the filesystem root
222    ///
223    /// This will return an error if the path contains illegal character sequences,
224    /// as defined on the docstring for [`Path`], or `base` is not an absolute path
225    pub fn from_absolute_path(path: impl AsRef<std::path::Path>) -> Result<Self, Error> {
226        Self::from_absolute_path_with_base(path, None)
227    }
228
229    #[cfg(not(target_arch = "wasm32"))]
230    /// Convert a filesystem path to a [`Path`] relative to the provided base
231    ///
232    /// This will return an error if the path contains illegal character sequences,
233    /// as defined on the docstring for [`Path`], or `base` does not refer to a parent
234    /// path of `path`, or `base` is not an absolute path
235    pub(crate) fn from_absolute_path_with_base(
236        path: impl AsRef<std::path::Path>,
237        base: Option<&Url>,
238    ) -> Result<Self, Error> {
239        let url = absolute_path_to_url(path)?;
240        let path = match base {
241            Some(prefix) => {
242                url.path()
243                    .strip_prefix(prefix.path())
244                    .ok_or_else(|| Error::PrefixMismatch {
245                        path: url.path().to_string(),
246                        prefix: prefix.to_string(),
247                    })?
248            }
249            None => url.path(),
250        };
251
252        // Reverse any percent encoding performed by conversion to URL
253        Self::from_url_path(path)
254    }
255
256    /// Parse a url encoded string as a [`Path`], returning a [`Error`] if invalid
257    ///
258    /// This will return an error if the path contains illegal character sequences
259    /// as defined on the docstring for [`Path`]
260    pub fn from_url_path(path: impl AsRef<str>) -> Result<Self, Error> {
261        let path = path.as_ref();
262        let decoded = percent_decode(path.as_bytes())
263            .decode_utf8()
264            .map_err(|source| {
265                let path = path.into();
266                Error::NonUnicode { source, path }
267            })?;
268
269        Self::parse(decoded)
270    }
271
272    /// Returns the number of [`PathPart`]s in this [`Path`]
273    ///
274    /// This is equivalent to calling `.parts().count()` manually.
275    ///
276    /// # Performance
277    ///
278    /// This operation is `O(n)`.
279    #[doc(alias = "len")]
280    pub fn parts_count(&self) -> usize {
281        self.raw.split_terminator(DELIMITER).count()
282    }
283
284    /// True if this [`Path`] points to the root of the store, equivalent to `Path::from("/")`.
285    ///
286    /// See also [`Path::ROOT`].
287    ///
288    /// # Example
289    ///
290    /// ```
291    /// # use object_store::path::Path;
292    /// assert!(Path::from("/").is_root());
293    /// assert!(Path::parse("").unwrap().is_root());
294    /// ```
295    pub fn is_root(&self) -> bool {
296        self.raw.is_empty()
297    }
298
299    /// Returns the [`PathPart`]s of this [`Path`]
300    ///
301    /// Equivalent to calling `.into_iter()` on a `&Path`.
302    pub fn parts(&self) -> PathParts<'_> {
303        PathParts::new(&self.raw)
304    }
305
306    /// Returns a copy of this [`Path`] with the last path segment removed
307    ///
308    /// Returns `None` if this path has zero segments.
309    pub fn parent(&self) -> Option<Self> {
310        if self.raw.is_empty() {
311            return None;
312        }
313
314        let Some((prefix, _filename)) = self.raw.rsplit_once(DELIMITER) else {
315            return Some(Self::ROOT);
316        };
317
318        Some(Self {
319            raw: prefix.to_string(),
320        })
321    }
322
323    /// Returns the last path segment containing the filename stored in this [`Path`]
324    ///
325    /// Returns `None` only if this path is the root path.
326    pub fn filename(&self) -> Option<&str> {
327        match self.raw.is_empty() {
328            true => None,
329            false => self.raw.rsplit(DELIMITER).next(),
330        }
331    }
332
333    /// Returns the extension of the file stored in this [`Path`], if any
334    pub fn extension(&self) -> Option<&str> {
335        self.filename()
336            .and_then(|f| f.rsplit_once('.'))
337            .and_then(|(_, extension)| {
338                if extension.is_empty() {
339                    None
340                } else {
341                    Some(extension)
342                }
343            })
344    }
345
346    /// Returns an iterator of the [`PathPart`] of this [`Path`] after `prefix`
347    ///
348    /// Returns `None` if the prefix does not match.
349    pub fn prefix_match(&self, prefix: &Self) -> Option<impl Iterator<Item = PathPart<'_>> + '_> {
350        let mut stripped = self.raw.strip_prefix(&prefix.raw)?;
351        if !stripped.is_empty() && !prefix.raw.is_empty() {
352            stripped = stripped.strip_prefix(DELIMITER)?;
353        }
354        Some(PathParts::new(stripped))
355    }
356
357    /// Returns true if this [`Path`] starts with `prefix`
358    pub fn prefix_matches(&self, prefix: &Self) -> bool {
359        self.prefix_match(prefix).is_some()
360    }
361
362    /// Creates a new child of this [`Path`]
363    #[deprecated = "use .join() or .clone().join() instead"]
364    pub fn child<'a>(&self, child: impl Into<PathPart<'a>>) -> Self {
365        self.clone().join(child)
366    }
367
368    /// Appends a single path segment to this [`Path`]
369    pub fn join<'a>(self, child: impl Into<PathPart<'a>>) -> Self {
370        let child_cow_str = child.into().raw;
371
372        let raw = if self.raw.is_empty() {
373            child_cow_str.to_string()
374        } else {
375            use std::fmt::Write;
376
377            let mut raw = self.raw;
378            write!(raw, "{DELIMITER}{child_cow_str}").expect("failed to append to string");
379            raw
380        };
381
382        Self { raw }
383    }
384}
385
386impl AsRef<str> for Path {
387    fn as_ref(&self) -> &str {
388        &self.raw
389    }
390}
391
392impl From<&str> for Path {
393    fn from(path: &str) -> Self {
394        Self::from_iter(path.split(DELIMITER))
395    }
396}
397
398impl From<String> for Path {
399    fn from(path: String) -> Self {
400        Self::from_iter(path.split(DELIMITER))
401    }
402}
403
404impl From<Path> for String {
405    fn from(path: Path) -> Self {
406        path.raw
407    }
408}
409
410impl std::fmt::Display for Path {
411    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
412        self.raw.fmt(f)
413    }
414}
415
416impl<'a, I> FromIterator<I> for Path
417where
418    I: Into<PathPart<'a>>,
419{
420    fn from_iter<T: IntoIterator<Item = I>>(iter: T) -> Self {
421        let mut this = Self::ROOT;
422        this.extend(iter);
423        this
424    }
425}
426
427/// See also [`Path::parts`]
428impl<'a> IntoIterator for &'a Path {
429    type Item = PathPart<'a>;
430    type IntoIter = PathParts<'a>;
431
432    fn into_iter(self) -> Self::IntoIter {
433        PathParts::new(&self.raw)
434    }
435}
436
437/// [`Path`] supports appending [`PathPart`]s of one `Path` to another `Path`.
438///
439/// # Examples
440///
441/// Suppose Alice is copying Bob's file to her own user directory.
442/// We could choose the full path of the new file by taking the original
443/// absolute path, making it relative to Bob's home
444///
445/// ```rust
446/// # use object_store::path::Path;
447/// let alice_home = Path::from("Users/alice");
448/// let bob_home = Path::from("Users/bob");
449/// let bob_file = Path::from("Users/bob/documents/file.txt");
450///
451/// let mut alice_file = alice_home;
452/// alice_file.extend(bob_file.prefix_match(&bob_home).unwrap());
453///
454/// assert_eq!(alice_file, Path::from("Users/alice/documents/file.txt"));
455/// ```
456impl<'a, I: Into<PathPart<'a>>> Extend<I> for Path {
457    fn extend<T: IntoIterator<Item = I>>(&mut self, iter: T) {
458        for s in iter {
459            let s = s.into();
460            if !s.raw.is_empty() {
461                if !self.raw.is_empty() {
462                    self.raw.push(DELIMITER_CHAR);
463                }
464                self.raw.push_str(&s.raw);
465            }
466        }
467    }
468}
469
470#[cfg(not(target_arch = "wasm32"))]
471/// Given an absolute filesystem path convert it to a URL representation without canonicalization
472pub(crate) fn absolute_path_to_url(path: impl AsRef<std::path::Path>) -> Result<Url, Error> {
473    Url::from_file_path(&path).map_err(|_| Error::InvalidPath {
474        path: path.as_ref().into(),
475    })
476}
477
478#[cfg(test)]
479mod tests {
480    use super::*;
481
482    #[test]
483    fn delimiter_char_is_forward_slash() {
484        assert_eq!(DELIMITER_CHAR, '/');
485    }
486
487    #[test]
488    fn cloud_prefix_with_trailing_delimiter() {
489        // Use case: files exist in object storage named `foo/bar.json` and
490        // `foo_test.json`. A search for the prefix `foo/` should return
491        // `foo/bar.json` but not `foo_test.json'.
492        let prefix = Path::from_iter(["test"]);
493        assert_eq!(prefix.as_ref(), "test");
494    }
495
496    #[test]
497    fn push_encodes() {
498        let location = Path::from_iter(["foo/bar", "baz%2Ftest"]);
499        assert_eq!(location.as_ref(), "foo%2Fbar/baz%252Ftest");
500    }
501
502    #[test]
503    fn test_parse() {
504        assert_eq!(Path::parse("/").unwrap().as_ref(), "");
505        assert_eq!(Path::parse("").unwrap().as_ref(), "");
506
507        let err = Path::parse("//").unwrap_err();
508        assert!(matches!(err, Error::EmptySegment { .. }));
509
510        assert_eq!(Path::parse("/foo/bar/").unwrap().as_ref(), "foo/bar");
511        assert_eq!(Path::parse("foo/bar/").unwrap().as_ref(), "foo/bar");
512        assert_eq!(Path::parse("foo/bar").unwrap().as_ref(), "foo/bar");
513
514        let err = Path::parse("foo///bar").unwrap_err();
515        assert!(matches!(err, Error::EmptySegment { .. }));
516    }
517
518    #[test]
519    fn convert_raw_before_partial_eq() {
520        // dir and file_name
521        let cloud = Path::from("test_dir/test_file.json");
522        let built = Path::from_iter(["test_dir", "test_file.json"]);
523
524        assert_eq!(built, cloud);
525
526        // dir and file_name w/o dot
527        let cloud = Path::from("test_dir/test_file");
528        let built = Path::from_iter(["test_dir", "test_file"]);
529
530        assert_eq!(built, cloud);
531
532        // dir, no file
533        let cloud = Path::from("test_dir/");
534        let built = Path::from_iter(["test_dir"]);
535        assert_eq!(built, cloud);
536
537        // file_name, no dir
538        let cloud = Path::from("test_file.json");
539        let built = Path::from_iter(["test_file.json"]);
540        assert_eq!(built, cloud);
541
542        // empty
543        let cloud = Path::from("");
544        let built = Path::from_iter(["", ""]);
545
546        assert_eq!(built, cloud);
547    }
548
549    #[test]
550    fn parts_after_prefix_behavior() {
551        let existing_path = Path::from("apple/bear/cow/dog/egg.json");
552
553        // Prefix with one directory
554        let prefix = Path::from("apple");
555        let expected_parts: Vec<PathPart<'_>> = vec!["bear", "cow", "dog", "egg.json"]
556            .into_iter()
557            .map(Into::into)
558            .collect();
559        let parts: Vec<_> = existing_path.prefix_match(&prefix).unwrap().collect();
560        assert_eq!(parts, expected_parts);
561
562        // Prefix with two directories
563        let prefix = Path::from("apple/bear");
564        let expected_parts: Vec<PathPart<'_>> = vec!["cow", "dog", "egg.json"]
565            .into_iter()
566            .map(Into::into)
567            .collect();
568        let parts: Vec<_> = existing_path.prefix_match(&prefix).unwrap().collect();
569        assert_eq!(parts, expected_parts);
570
571        // Not a prefix
572        let prefix = Path::from("cow");
573        assert!(existing_path.prefix_match(&prefix).is_none());
574
575        // Prefix with a partial directory
576        let prefix = Path::from("ap");
577        assert!(existing_path.prefix_match(&prefix).is_none());
578
579        // Prefix matches but there aren't any parts after it
580        let existing = Path::from("apple/bear/cow/dog");
581
582        assert_eq!(existing.prefix_match(&existing).unwrap().count(), 0);
583        assert_eq!(Path::default().parts().count(), 0);
584    }
585
586    #[test]
587    fn parts_count() {
588        assert_eq!(Path::ROOT.parts().count(), Path::ROOT.parts_count());
589
590        let path = path("foo/bar/baz");
591        assert_eq!(path.parts_count(), 3);
592        assert_eq!(path.parts_count(), path.parts().count());
593    }
594
595    #[test]
596    fn prefix_matches_raw_content() {
597        assert_eq!(Path::ROOT.parent(), None, "empty path must have no prefix");
598
599        assert_eq!(path("foo").parent().unwrap(), Path::ROOT);
600        assert_eq!(path("foo/bar").parent().unwrap(), path("foo"));
601        assert_eq!(path("foo/bar/baz").parent().unwrap(), path("foo/bar"));
602    }
603
604    #[test]
605    fn prefix_matches() {
606        let haystack = Path::from_iter(["foo/bar", "baz%2Ftest", "something"]);
607        // self starts with self
608        assert!(
609            haystack.prefix_matches(&haystack),
610            "{haystack:?} should have started with {haystack:?}"
611        );
612
613        // a longer prefix doesn't match
614        let needle = haystack.clone().join("longer now");
615        assert!(
616            !haystack.prefix_matches(&needle),
617            "{haystack:?} shouldn't have started with {needle:?}"
618        );
619
620        // one dir prefix matches
621        let needle = Path::from_iter(["foo/bar"]);
622        assert!(
623            haystack.prefix_matches(&needle),
624            "{haystack:?} should have started with {needle:?}"
625        );
626
627        // two dir prefix matches
628        let needle = needle.join("baz%2Ftest");
629        assert!(
630            haystack.prefix_matches(&needle),
631            "{haystack:?} should have started with {needle:?}"
632        );
633
634        // partial dir prefix doesn't match
635        let needle = Path::from_iter(["f"]);
636        assert!(
637            !haystack.prefix_matches(&needle),
638            "{haystack:?} should not have started with {needle:?}"
639        );
640
641        // one dir and one partial dir doesn't match
642        let needle = Path::from_iter(["foo/bar", "baz"]);
643        assert!(
644            !haystack.prefix_matches(&needle),
645            "{haystack:?} should not have started with {needle:?}"
646        );
647
648        // empty prefix matches
649        let needle = Path::from("");
650        assert!(
651            haystack.prefix_matches(&needle),
652            "{haystack:?} should have started with {needle:?}"
653        );
654    }
655
656    #[test]
657    fn prefix_matches_with_file_name() {
658        let haystack = Path::from_iter(["foo/bar", "baz%2Ftest", "something", "foo.segment"]);
659
660        // All directories match and file name is a prefix
661        let needle = Path::from_iter(["foo/bar", "baz%2Ftest", "something", "foo"]);
662
663        assert!(
664            !haystack.prefix_matches(&needle),
665            "{haystack:?} should not have started with {needle:?}"
666        );
667
668        // All directories match but file name is not a prefix
669        let needle = Path::from_iter(["foo/bar", "baz%2Ftest", "something", "e"]);
670
671        assert!(
672            !haystack.prefix_matches(&needle),
673            "{haystack:?} should not have started with {needle:?}"
674        );
675
676        // Not all directories match; file name is a prefix of the next directory; this
677        // does not match
678        let needle = Path::from_iter(["foo/bar", "baz%2Ftest", "s"]);
679
680        assert!(
681            !haystack.prefix_matches(&needle),
682            "{haystack:?} should not have started with {needle:?}"
683        );
684
685        // Not all directories match; file name is NOT a prefix of the next directory;
686        // no match
687        let needle = Path::from_iter(["foo/bar", "baz%2Ftest", "p"]);
688
689        assert!(
690            !haystack.prefix_matches(&needle),
691            "{haystack:?} should not have started with {needle:?}"
692        );
693    }
694
695    #[test]
696    fn path_containing_spaces() {
697        let a = Path::from_iter(["foo bar", "baz"]);
698        let b = Path::from("foo bar/baz");
699        let c = Path::parse("foo bar/baz").unwrap();
700
701        assert_eq!(a.raw, "foo bar/baz");
702        assert_eq!(a.raw, b.raw);
703        assert_eq!(b.raw, c.raw);
704    }
705
706    #[test]
707    fn from_url_path() {
708        let a = Path::from_url_path("foo%20bar").unwrap();
709        let b = Path::from_url_path("foo/%2E%2E/bar").unwrap_err();
710        let c = Path::from_url_path("foo%2F%252E%252E%2Fbar").unwrap();
711        let d = Path::from_url_path("foo/%252E%252E/bar").unwrap();
712        let e = Path::from_url_path("%48%45%4C%4C%4F").unwrap();
713        let f = Path::from_url_path("foo/%FF/as").unwrap_err();
714
715        assert_eq!(a.raw, "foo bar");
716        assert!(matches!(b, Error::BadSegment { .. }));
717        assert_eq!(c.raw, "foo/%2E%2E/bar");
718        assert_eq!(d.raw, "foo/%2E%2E/bar");
719        assert_eq!(e.raw, "HELLO");
720        assert!(matches!(f, Error::NonUnicode { .. }));
721    }
722
723    #[test]
724    fn filename_from_path() {
725        let a = Path::from("foo/bar");
726        let b = Path::from("foo/bar.baz");
727        let c = Path::from("foo.bar/baz");
728
729        assert_eq!(a.filename(), Some("bar"));
730        assert_eq!(b.filename(), Some("bar.baz"));
731        assert_eq!(c.filename(), Some("baz"));
732    }
733
734    #[test]
735    fn file_extension() {
736        let a = Path::from("foo/bar");
737        let b = Path::from("foo/bar.baz");
738        let c = Path::from("foo.bar/baz");
739        let d = Path::from("foo.bar/baz.qux");
740
741        assert_eq!(a.extension(), None);
742        assert_eq!(b.extension(), Some("baz"));
743        assert_eq!(c.extension(), None);
744        assert_eq!(d.extension(), Some("qux"));
745    }
746
747    #[test]
748    fn root_is_root() {
749        assert!(Path::ROOT.is_root());
750        assert!(Path::ROOT.parts().next().is_none());
751    }
752
753    /// Main test for `impl Extend for Path`, covers most cases.
754    #[test]
755    fn impl_extend() {
756        let mut p = Path::ROOT;
757
758        p.extend(&Path::ROOT);
759        assert_eq!(p, Path::ROOT);
760
761        p.extend(&path("foo"));
762        assert_eq!(p, path("foo"));
763
764        p.extend(&path("bar/baz"));
765        assert_eq!(p, path("foo/bar/baz"));
766
767        p.extend(&path("a/b/c"));
768        assert_eq!(p, path("foo/bar/baz/a/b/c"));
769    }
770
771    /// Test for `impl Extend for Path`, specifically covers addition of a single segment.
772    #[test]
773    fn impl_extend_for_one_segment() {
774        let mut p = Path::ROOT;
775
776        p.extend(&path("foo"));
777        assert_eq!(p, path("foo"));
778
779        p.extend(&path("bar"));
780        assert_eq!(p, path("foo/bar"));
781
782        p.extend(&path("baz"));
783        assert_eq!(p, path("foo/bar/baz"));
784    }
785
786    #[test]
787    fn parent() {
788        assert_eq!(Path::ROOT.parent(), None);
789        assert_eq!(path("foo").parent(), Some(Path::ROOT));
790        assert_eq!(path("foo/bar").parent(), Some(path("foo")));
791        assert_eq!(path("foo/bar/baz").parent(), Some(path("foo/bar")));
792    }
793
794    /// Construct a [`Path`] from a raw `&str`, or panic trying.
795    #[track_caller]
796    fn path(raw: &str) -> Path {
797        Path::parse(raw).unwrap()
798    }
799}