| // Licensed to the Apache Software Foundation (ASF) under one |
| // or more contributor license agreements. See the NOTICE file |
| // distributed with this work for additional information |
| // regarding copyright ownership. The ASF licenses this file |
| // to you under the Apache License, Version 2.0 (the |
| // "License"); you may not use this file except in compliance |
| // with the License. You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, |
| // software distributed under the License is distributed on an |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| // KIND, either express or implied. See the License for the |
| // specific language governing permissions and limitations |
| // under the License. |
| |
| //! Path abstraction for Object Storage |
| |
| use percent_encoding::percent_decode; |
| use std::fmt::Formatter; |
| #[cfg(not(target_arch = "wasm32"))] |
| use url::Url; |
| |
| /// The delimiter to separate object namespaces, creating a directory structure. |
| pub const DELIMITER: &str = "/"; |
| |
| /// The path delimiter as a single byte |
| pub const DELIMITER_BYTE: u8 = DELIMITER.as_bytes()[0]; |
| |
| /// The path delimiter as a single char |
| pub const DELIMITER_CHAR: char = DELIMITER_BYTE as char; |
| |
| mod parts; |
| |
| pub use parts::{InvalidPart, PathPart, PathParts}; |
| |
| /// Error returned by [`Path::parse`] |
| #[derive(Debug, thiserror::Error)] |
| #[non_exhaustive] |
| pub enum Error { |
| /// Error when there's an empty segment between two slashes `/` in the path |
| #[error("Path \"{}\" contained empty path segment", path)] |
| EmptySegment { |
| /// The source path |
| path: String, |
| }, |
| |
| /// Error when an invalid segment is encountered in the given path |
| #[error("Error parsing Path \"{}\": {}", path, source)] |
| BadSegment { |
| /// The source path |
| path: String, |
| /// The part containing the error |
| source: InvalidPart, |
| }, |
| |
| /// Error when path cannot be canonicalized |
| #[error("Failed to canonicalize path \"{}\": {}", path.display(), source)] |
| Canonicalize { |
| /// The source path |
| path: std::path::PathBuf, |
| /// The underlying error |
| source: std::io::Error, |
| }, |
| |
| /// Error when the path is not a valid URL |
| #[error("Unable to convert path \"{}\" to URL", path.display())] |
| InvalidPath { |
| /// The source path |
| path: std::path::PathBuf, |
| }, |
| |
| /// Error when a path contains non-unicode characters |
| #[error("Path \"{}\" contained non-unicode characters: {}", path, source)] |
| NonUnicode { |
| /// The source path |
| path: String, |
| /// The underlying `UTF8Error` |
| source: std::str::Utf8Error, |
| }, |
| |
| /// Error when the a path doesn't start with given prefix |
| #[error("Path {} does not start with prefix {}", path, prefix)] |
| PrefixMismatch { |
| /// The source path |
| path: String, |
| /// The mismatched prefix |
| prefix: String, |
| }, |
| } |
| |
| /// A parsed path representation that can be safely written to object storage |
| /// |
| /// A [`Path`] maintains the following invariants: |
| /// |
| /// * Paths are delimited by `/` |
| /// * Paths do not contain leading or trailing `/` |
| /// * Paths do not contain relative path segments, i.e. `.` or `..` |
| /// * Paths do not contain empty path segments |
| /// * Paths do not contain any ASCII control characters |
| /// |
| /// There are no enforced restrictions on path length, however, it should be noted that most |
| /// object stores do not permit paths longer than 1024 bytes, and many filesystems do not |
| /// support path segments longer than 255 bytes. |
| /// |
| /// # Encode |
| /// |
| /// In theory object stores support any UTF-8 character sequence, however, certain character |
| /// sequences cause compatibility problems with some applications and protocols. Additionally |
| /// some filesystems may impose character restrictions, see [`LocalFileSystem`]. As such the |
| /// naming guidelines for [S3], [GCS] and [Azure Blob Storage] all recommend sticking to a |
| /// limited character subset. |
| /// |
| /// [S3]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-keys.html |
| /// [GCS]: https://cloud.google.com/storage/docs/naming-objects |
| /// [Azure Blob Storage]: https://docs.microsoft.com/en-us/rest/api/storageservices/Naming-and-Referencing-Containers--Blobs--and-Metadata#blob-names |
| /// |
| /// A string containing potentially problematic path segments can therefore be encoded to a [`Path`] |
| /// using [`Path::from`] or [`Path::from_iter`]. This will percent encode any problematic |
| /// segments according to [RFC 1738]. |
| /// |
| /// ``` |
| /// # use object_store::path::Path; |
| /// assert_eq!(Path::from("foo/bar").as_ref(), "foo/bar"); |
| /// assert_eq!(Path::from("foo//bar").as_ref(), "foo/bar"); |
| /// assert_eq!(Path::from("foo/../bar").as_ref(), "foo/%2E%2E/bar"); |
| /// assert_eq!(Path::from("/").as_ref(), ""); |
| /// assert_eq!(Path::from_iter(["foo", "foo/bar"]).as_ref(), "foo/foo%2Fbar"); |
| /// ``` |
| /// |
| /// Note: if provided with an already percent encoded string, this will encode it again |
| /// |
| /// ``` |
| /// # use object_store::path::Path; |
| /// assert_eq!(Path::from("foo/foo%2Fbar").as_ref(), "foo/foo%252Fbar"); |
| /// ``` |
| /// |
| /// # Parse |
| /// |
| /// Alternatively a [`Path`] can be parsed from an existing string, returning an |
| /// error if it is invalid. Unlike the encoding methods above, this will permit |
| /// arbitrary unicode, including percent encoded sequences. |
| /// |
| /// ``` |
| /// # use object_store::path::Path; |
| /// assert_eq!(Path::parse("/foo/foo%2Fbar").unwrap().as_ref(), "foo/foo%2Fbar"); |
| /// Path::parse("..").unwrap_err(); // Relative path segments are disallowed |
| /// Path::parse("/foo//").unwrap_err(); // Empty path segments are disallowed |
| /// Path::parse("\x00").unwrap_err(); // ASCII control characters are disallowed |
| /// ``` |
| /// |
| /// [RFC 1738]: https://www.ietf.org/rfc/rfc1738.txt |
| /// [`LocalFileSystem`]: crate::local::LocalFileSystem |
| #[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Ord, PartialOrd)] |
| pub struct Path { |
| /// The raw path with no leading or trailing delimiters |
| raw: String, |
| } |
| |
| impl Path { |
| /// An empty [`Path`] that points to the root of the store, equivalent to `Path::from("/")`. |
| /// |
| /// See also [`Path::is_root`]. |
| /// |
| /// # Example |
| /// |
| /// ``` |
| /// # use object_store::path::Path; |
| /// assert_eq!(Path::ROOT, Path::from("/")); |
| /// ``` |
| pub const ROOT: Self = Self { raw: String::new() }; |
| |
| /// Parse a string as a [`Path`], returning a [`Error`] if invalid, |
| /// as defined on the docstring for [`Path`] |
| /// |
| /// Note: this will strip any leading `/` or trailing `/` |
| pub fn parse(path: impl AsRef<str>) -> Result<Self, Error> { |
| let path = path.as_ref(); |
| |
| let stripped = path.strip_prefix(DELIMITER).unwrap_or(path); |
| if stripped.is_empty() { |
| return Ok(Default::default()); |
| } |
| |
| let stripped = stripped.strip_suffix(DELIMITER).unwrap_or(stripped); |
| |
| for segment in stripped.split(DELIMITER) { |
| if segment.is_empty() { |
| return Err(Error::EmptySegment { path: path.into() }); |
| } |
| |
| PathPart::parse(segment).map_err(|source| { |
| let path = path.into(); |
| Error::BadSegment { source, path } |
| })?; |
| } |
| |
| Ok(Self { |
| raw: stripped.to_string(), |
| }) |
| } |
| |
| #[cfg(not(target_arch = "wasm32"))] |
| /// Convert a filesystem path to a [`Path`] relative to the filesystem root |
| /// |
| /// This will return an error if the path contains illegal character sequences |
| /// as defined on the docstring for [`Path`] or does not exist |
| /// |
| /// Note: this will canonicalize the provided path, resolving any symlinks |
| pub fn from_filesystem_path(path: impl AsRef<std::path::Path>) -> Result<Self, Error> { |
| let absolute = std::fs::canonicalize(&path).map_err(|source| { |
| let path = path.as_ref().into(); |
| Error::Canonicalize { source, path } |
| })?; |
| |
| Self::from_absolute_path(absolute) |
| } |
| |
| #[cfg(not(target_arch = "wasm32"))] |
| /// Convert an absolute filesystem path to a [`Path`] relative to the filesystem root |
| /// |
| /// This will return an error if the path contains illegal character sequences, |
| /// as defined on the docstring for [`Path`], or `base` is not an absolute path |
| pub fn from_absolute_path(path: impl AsRef<std::path::Path>) -> Result<Self, Error> { |
| Self::from_absolute_path_with_base(path, None) |
| } |
| |
| #[cfg(not(target_arch = "wasm32"))] |
| /// Convert a filesystem path to a [`Path`] relative to the provided base |
| /// |
| /// This will return an error if the path contains illegal character sequences, |
| /// as defined on the docstring for [`Path`], or `base` does not refer to a parent |
| /// path of `path`, or `base` is not an absolute path |
| pub(crate) fn from_absolute_path_with_base( |
| path: impl AsRef<std::path::Path>, |
| base: Option<&Url>, |
| ) -> Result<Self, Error> { |
| let url = absolute_path_to_url(path)?; |
| let path = match base { |
| Some(prefix) => { |
| url.path() |
| .strip_prefix(prefix.path()) |
| .ok_or_else(|| Error::PrefixMismatch { |
| path: url.path().to_string(), |
| prefix: prefix.to_string(), |
| })? |
| } |
| None => url.path(), |
| }; |
| |
| // Reverse any percent encoding performed by conversion to URL |
| Self::from_url_path(path) |
| } |
| |
| /// Parse a url encoded string as a [`Path`], returning a [`Error`] if invalid |
| /// |
| /// This will return an error if the path contains illegal character sequences |
| /// as defined on the docstring for [`Path`] |
| pub fn from_url_path(path: impl AsRef<str>) -> Result<Self, Error> { |
| let path = path.as_ref(); |
| let decoded = percent_decode(path.as_bytes()) |
| .decode_utf8() |
| .map_err(|source| { |
| let path = path.into(); |
| Error::NonUnicode { source, path } |
| })?; |
| |
| Self::parse(decoded) |
| } |
| |
| /// Returns the number of [`PathPart`]s in this [`Path`] |
| /// |
| /// This is equivalent to calling `.parts().count()` manually. |
| /// |
| /// # Performance |
| /// |
| /// This operation is `O(n)`. |
| #[doc(alias = "len")] |
| pub fn parts_count(&self) -> usize { |
| self.raw.split_terminator(DELIMITER).count() |
| } |
| |
| /// True if this [`Path`] points to the root of the store, equivalent to `Path::from("/")`. |
| /// |
| /// See also [`Path::ROOT`]. |
| /// |
| /// # Example |
| /// |
| /// ``` |
| /// # use object_store::path::Path; |
| /// assert!(Path::from("/").is_root()); |
| /// assert!(Path::parse("").unwrap().is_root()); |
| /// ``` |
| pub fn is_root(&self) -> bool { |
| self.raw.is_empty() |
| } |
| |
| /// Returns the [`PathPart`]s of this [`Path`] |
| /// |
| /// Equivalent to calling `.into_iter()` on a `&Path`. |
| pub fn parts(&self) -> PathParts<'_> { |
| PathParts::new(&self.raw) |
| } |
| |
| /// Returns a copy of this [`Path`] with the last path segment removed |
| /// |
| /// Returns `None` if this path has zero segments. |
| pub fn parent(&self) -> Option<Self> { |
| if self.raw.is_empty() { |
| return None; |
| } |
| |
| let Some((prefix, _filename)) = self.raw.rsplit_once(DELIMITER) else { |
| return Some(Self::ROOT); |
| }; |
| |
| Some(Self { |
| raw: prefix.to_string(), |
| }) |
| } |
| |
| /// Returns the last path segment containing the filename stored in this [`Path`] |
| /// |
| /// Returns `None` only if this path is the root path. |
| pub fn filename(&self) -> Option<&str> { |
| match self.raw.is_empty() { |
| true => None, |
| false => self.raw.rsplit(DELIMITER).next(), |
| } |
| } |
| |
| /// Returns the extension of the file stored in this [`Path`], if any |
| pub fn extension(&self) -> Option<&str> { |
| self.filename() |
| .and_then(|f| f.rsplit_once('.')) |
| .and_then(|(_, extension)| { |
| if extension.is_empty() { |
| None |
| } else { |
| Some(extension) |
| } |
| }) |
| } |
| |
| /// Returns an iterator of the [`PathPart`] of this [`Path`] after `prefix` |
| /// |
| /// Returns `None` if the prefix does not match. |
| pub fn prefix_match(&self, prefix: &Self) -> Option<impl Iterator<Item = PathPart<'_>> + '_> { |
| let mut stripped = self.raw.strip_prefix(&prefix.raw)?; |
| if !stripped.is_empty() && !prefix.raw.is_empty() { |
| stripped = stripped.strip_prefix(DELIMITER)?; |
| } |
| Some(PathParts::new(stripped)) |
| } |
| |
| /// Returns true if this [`Path`] starts with `prefix` |
| pub fn prefix_matches(&self, prefix: &Self) -> bool { |
| self.prefix_match(prefix).is_some() |
| } |
| |
| /// Creates a new child of this [`Path`] |
| #[deprecated = "use .join() or .clone().join() instead"] |
| pub fn child<'a>(&self, child: impl Into<PathPart<'a>>) -> Self { |
| self.clone().join(child) |
| } |
| |
| /// Appends a single path segment to this [`Path`] |
| pub fn join<'a>(self, child: impl Into<PathPart<'a>>) -> Self { |
| let child_cow_str = child.into().raw; |
| |
| let raw = if self.raw.is_empty() { |
| child_cow_str.to_string() |
| } else { |
| use std::fmt::Write; |
| |
| let mut raw = self.raw; |
| write!(raw, "{DELIMITER}{child_cow_str}").expect("failed to append to string"); |
| raw |
| }; |
| |
| Self { raw } |
| } |
| } |
| |
| impl AsRef<str> for Path { |
| fn as_ref(&self) -> &str { |
| &self.raw |
| } |
| } |
| |
| impl From<&str> for Path { |
| fn from(path: &str) -> Self { |
| Self::from_iter(path.split(DELIMITER)) |
| } |
| } |
| |
| impl From<String> for Path { |
| fn from(path: String) -> Self { |
| Self::from_iter(path.split(DELIMITER)) |
| } |
| } |
| |
| impl From<Path> for String { |
| fn from(path: Path) -> Self { |
| path.raw |
| } |
| } |
| |
| impl std::fmt::Display for Path { |
| fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { |
| self.raw.fmt(f) |
| } |
| } |
| |
| impl<'a, I> FromIterator<I> for Path |
| where |
| I: Into<PathPart<'a>>, |
| { |
| fn from_iter<T: IntoIterator<Item = I>>(iter: T) -> Self { |
| let mut this = Self::ROOT; |
| this.extend(iter); |
| this |
| } |
| } |
| |
| /// See also [`Path::parts`] |
| impl<'a> IntoIterator for &'a Path { |
| type Item = PathPart<'a>; |
| type IntoIter = PathParts<'a>; |
| |
| fn into_iter(self) -> Self::IntoIter { |
| PathParts::new(&self.raw) |
| } |
| } |
| |
| /// [`Path`] supports appending [`PathPart`]s of one `Path` to another `Path`. |
| /// |
| /// # Examples |
| /// |
| /// Suppose Alice is copying Bob's file to her own user directory. |
| /// We could choose the full path of the new file by taking the original |
| /// absolute path, making it relative to Bob's home |
| /// |
| /// ```rust |
| /// # use object_store::path::Path; |
| /// let alice_home = Path::from("Users/alice"); |
| /// let bob_home = Path::from("Users/bob"); |
| /// let bob_file = Path::from("Users/bob/documents/file.txt"); |
| /// |
| /// let mut alice_file = alice_home; |
| /// alice_file.extend(bob_file.prefix_match(&bob_home).unwrap()); |
| /// |
| /// assert_eq!(alice_file, Path::from("Users/alice/documents/file.txt")); |
| /// ``` |
| impl<'a, I: Into<PathPart<'a>>> Extend<I> for Path { |
| fn extend<T: IntoIterator<Item = I>>(&mut self, iter: T) { |
| for s in iter { |
| let s = s.into(); |
| if !s.raw.is_empty() { |
| if !self.raw.is_empty() { |
| self.raw.push(DELIMITER_CHAR); |
| } |
| self.raw.push_str(&s.raw); |
| } |
| } |
| } |
| } |
| |
| #[cfg(not(target_arch = "wasm32"))] |
| /// Given an absolute filesystem path convert it to a URL representation without canonicalization |
| pub(crate) fn absolute_path_to_url(path: impl AsRef<std::path::Path>) -> Result<Url, Error> { |
| Url::from_file_path(&path).map_err(|_| Error::InvalidPath { |
| path: path.as_ref().into(), |
| }) |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| |
| #[test] |
| fn delimiter_char_is_forward_slash() { |
| assert_eq!(DELIMITER_CHAR, '/'); |
| } |
| |
| #[test] |
| fn cloud_prefix_with_trailing_delimiter() { |
| // Use case: files exist in object storage named `foo/bar.json` and |
| // `foo_test.json`. A search for the prefix `foo/` should return |
| // `foo/bar.json` but not `foo_test.json'. |
| let prefix = Path::from_iter(["test"]); |
| assert_eq!(prefix.as_ref(), "test"); |
| } |
| |
| #[test] |
| fn push_encodes() { |
| let location = Path::from_iter(["foo/bar", "baz%2Ftest"]); |
| assert_eq!(location.as_ref(), "foo%2Fbar/baz%252Ftest"); |
| } |
| |
| #[test] |
| fn test_parse() { |
| assert_eq!(Path::parse("/").unwrap().as_ref(), ""); |
| assert_eq!(Path::parse("").unwrap().as_ref(), ""); |
| |
| let err = Path::parse("//").unwrap_err(); |
| assert!(matches!(err, Error::EmptySegment { .. })); |
| |
| assert_eq!(Path::parse("/foo/bar/").unwrap().as_ref(), "foo/bar"); |
| assert_eq!(Path::parse("foo/bar/").unwrap().as_ref(), "foo/bar"); |
| assert_eq!(Path::parse("foo/bar").unwrap().as_ref(), "foo/bar"); |
| |
| let err = Path::parse("foo///bar").unwrap_err(); |
| assert!(matches!(err, Error::EmptySegment { .. })); |
| } |
| |
| #[test] |
| fn convert_raw_before_partial_eq() { |
| // dir and file_name |
| let cloud = Path::from("test_dir/test_file.json"); |
| let built = Path::from_iter(["test_dir", "test_file.json"]); |
| |
| assert_eq!(built, cloud); |
| |
| // dir and file_name w/o dot |
| let cloud = Path::from("test_dir/test_file"); |
| let built = Path::from_iter(["test_dir", "test_file"]); |
| |
| assert_eq!(built, cloud); |
| |
| // dir, no file |
| let cloud = Path::from("test_dir/"); |
| let built = Path::from_iter(["test_dir"]); |
| assert_eq!(built, cloud); |
| |
| // file_name, no dir |
| let cloud = Path::from("test_file.json"); |
| let built = Path::from_iter(["test_file.json"]); |
| assert_eq!(built, cloud); |
| |
| // empty |
| let cloud = Path::from(""); |
| let built = Path::from_iter(["", ""]); |
| |
| assert_eq!(built, cloud); |
| } |
| |
| #[test] |
| fn parts_after_prefix_behavior() { |
| let existing_path = Path::from("apple/bear/cow/dog/egg.json"); |
| |
| // Prefix with one directory |
| let prefix = Path::from("apple"); |
| let expected_parts: Vec<PathPart<'_>> = vec!["bear", "cow", "dog", "egg.json"] |
| .into_iter() |
| .map(Into::into) |
| .collect(); |
| let parts: Vec<_> = existing_path.prefix_match(&prefix).unwrap().collect(); |
| assert_eq!(parts, expected_parts); |
| |
| // Prefix with two directories |
| let prefix = Path::from("apple/bear"); |
| let expected_parts: Vec<PathPart<'_>> = vec!["cow", "dog", "egg.json"] |
| .into_iter() |
| .map(Into::into) |
| .collect(); |
| let parts: Vec<_> = existing_path.prefix_match(&prefix).unwrap().collect(); |
| assert_eq!(parts, expected_parts); |
| |
| // Not a prefix |
| let prefix = Path::from("cow"); |
| assert!(existing_path.prefix_match(&prefix).is_none()); |
| |
| // Prefix with a partial directory |
| let prefix = Path::from("ap"); |
| assert!(existing_path.prefix_match(&prefix).is_none()); |
| |
| // Prefix matches but there aren't any parts after it |
| let existing = Path::from("apple/bear/cow/dog"); |
| |
| assert_eq!(existing.prefix_match(&existing).unwrap().count(), 0); |
| assert_eq!(Path::default().parts().count(), 0); |
| } |
| |
| #[test] |
| fn parts_count() { |
| assert_eq!(Path::ROOT.parts().count(), Path::ROOT.parts_count()); |
| |
| let path = path("foo/bar/baz"); |
| assert_eq!(path.parts_count(), 3); |
| assert_eq!(path.parts_count(), path.parts().count()); |
| } |
| |
| #[test] |
| fn prefix_matches_raw_content() { |
| assert_eq!(Path::ROOT.parent(), None, "empty path must have no prefix"); |
| |
| assert_eq!(path("foo").parent().unwrap(), Path::ROOT); |
| assert_eq!(path("foo/bar").parent().unwrap(), path("foo")); |
| assert_eq!(path("foo/bar/baz").parent().unwrap(), path("foo/bar")); |
| } |
| |
| #[test] |
| fn prefix_matches() { |
| let haystack = Path::from_iter(["foo/bar", "baz%2Ftest", "something"]); |
| // self starts with self |
| assert!( |
| haystack.prefix_matches(&haystack), |
| "{haystack:?} should have started with {haystack:?}" |
| ); |
| |
| // a longer prefix doesn't match |
| let needle = haystack.clone().join("longer now"); |
| assert!( |
| !haystack.prefix_matches(&needle), |
| "{haystack:?} shouldn't have started with {needle:?}" |
| ); |
| |
| // one dir prefix matches |
| let needle = Path::from_iter(["foo/bar"]); |
| assert!( |
| haystack.prefix_matches(&needle), |
| "{haystack:?} should have started with {needle:?}" |
| ); |
| |
| // two dir prefix matches |
| let needle = needle.join("baz%2Ftest"); |
| assert!( |
| haystack.prefix_matches(&needle), |
| "{haystack:?} should have started with {needle:?}" |
| ); |
| |
| // partial dir prefix doesn't match |
| let needle = Path::from_iter(["f"]); |
| assert!( |
| !haystack.prefix_matches(&needle), |
| "{haystack:?} should not have started with {needle:?}" |
| ); |
| |
| // one dir and one partial dir doesn't match |
| let needle = Path::from_iter(["foo/bar", "baz"]); |
| assert!( |
| !haystack.prefix_matches(&needle), |
| "{haystack:?} should not have started with {needle:?}" |
| ); |
| |
| // empty prefix matches |
| let needle = Path::from(""); |
| assert!( |
| haystack.prefix_matches(&needle), |
| "{haystack:?} should have started with {needle:?}" |
| ); |
| } |
| |
| #[test] |
| fn prefix_matches_with_file_name() { |
| let haystack = Path::from_iter(["foo/bar", "baz%2Ftest", "something", "foo.segment"]); |
| |
| // All directories match and file name is a prefix |
| let needle = Path::from_iter(["foo/bar", "baz%2Ftest", "something", "foo"]); |
| |
| assert!( |
| !haystack.prefix_matches(&needle), |
| "{haystack:?} should not have started with {needle:?}" |
| ); |
| |
| // All directories match but file name is not a prefix |
| let needle = Path::from_iter(["foo/bar", "baz%2Ftest", "something", "e"]); |
| |
| assert!( |
| !haystack.prefix_matches(&needle), |
| "{haystack:?} should not have started with {needle:?}" |
| ); |
| |
| // Not all directories match; file name is a prefix of the next directory; this |
| // does not match |
| let needle = Path::from_iter(["foo/bar", "baz%2Ftest", "s"]); |
| |
| assert!( |
| !haystack.prefix_matches(&needle), |
| "{haystack:?} should not have started with {needle:?}" |
| ); |
| |
| // Not all directories match; file name is NOT a prefix of the next directory; |
| // no match |
| let needle = Path::from_iter(["foo/bar", "baz%2Ftest", "p"]); |
| |
| assert!( |
| !haystack.prefix_matches(&needle), |
| "{haystack:?} should not have started with {needle:?}" |
| ); |
| } |
| |
| #[test] |
| fn path_containing_spaces() { |
| let a = Path::from_iter(["foo bar", "baz"]); |
| let b = Path::from("foo bar/baz"); |
| let c = Path::parse("foo bar/baz").unwrap(); |
| |
| assert_eq!(a.raw, "foo bar/baz"); |
| assert_eq!(a.raw, b.raw); |
| assert_eq!(b.raw, c.raw); |
| } |
| |
| #[test] |
| fn from_url_path() { |
| let a = Path::from_url_path("foo%20bar").unwrap(); |
| let b = Path::from_url_path("foo/%2E%2E/bar").unwrap_err(); |
| let c = Path::from_url_path("foo%2F%252E%252E%2Fbar").unwrap(); |
| let d = Path::from_url_path("foo/%252E%252E/bar").unwrap(); |
| let e = Path::from_url_path("%48%45%4C%4C%4F").unwrap(); |
| let f = Path::from_url_path("foo/%FF/as").unwrap_err(); |
| |
| assert_eq!(a.raw, "foo bar"); |
| assert!(matches!(b, Error::BadSegment { .. })); |
| assert_eq!(c.raw, "foo/%2E%2E/bar"); |
| assert_eq!(d.raw, "foo/%2E%2E/bar"); |
| assert_eq!(e.raw, "HELLO"); |
| assert!(matches!(f, Error::NonUnicode { .. })); |
| } |
| |
| #[test] |
| fn filename_from_path() { |
| let a = Path::from("foo/bar"); |
| let b = Path::from("foo/bar.baz"); |
| let c = Path::from("foo.bar/baz"); |
| |
| assert_eq!(a.filename(), Some("bar")); |
| assert_eq!(b.filename(), Some("bar.baz")); |
| assert_eq!(c.filename(), Some("baz")); |
| } |
| |
| #[test] |
| fn file_extension() { |
| let a = Path::from("foo/bar"); |
| let b = Path::from("foo/bar.baz"); |
| let c = Path::from("foo.bar/baz"); |
| let d = Path::from("foo.bar/baz.qux"); |
| |
| assert_eq!(a.extension(), None); |
| assert_eq!(b.extension(), Some("baz")); |
| assert_eq!(c.extension(), None); |
| assert_eq!(d.extension(), Some("qux")); |
| } |
| |
| #[test] |
| fn root_is_root() { |
| assert!(Path::ROOT.is_root()); |
| assert!(Path::ROOT.parts().next().is_none()); |
| } |
| |
| /// Main test for `impl Extend for Path`, covers most cases. |
| #[test] |
| fn impl_extend() { |
| let mut p = Path::ROOT; |
| |
| p.extend(&Path::ROOT); |
| assert_eq!(p, Path::ROOT); |
| |
| p.extend(&path("foo")); |
| assert_eq!(p, path("foo")); |
| |
| p.extend(&path("bar/baz")); |
| assert_eq!(p, path("foo/bar/baz")); |
| |
| p.extend(&path("a/b/c")); |
| assert_eq!(p, path("foo/bar/baz/a/b/c")); |
| } |
| |
| /// Test for `impl Extend for Path`, specifically covers addition of a single segment. |
| #[test] |
| fn impl_extend_for_one_segment() { |
| let mut p = Path::ROOT; |
| |
| p.extend(&path("foo")); |
| assert_eq!(p, path("foo")); |
| |
| p.extend(&path("bar")); |
| assert_eq!(p, path("foo/bar")); |
| |
| p.extend(&path("baz")); |
| assert_eq!(p, path("foo/bar/baz")); |
| } |
| |
| #[test] |
| fn parent() { |
| assert_eq!(Path::ROOT.parent(), None); |
| assert_eq!(path("foo").parent(), Some(Path::ROOT)); |
| assert_eq!(path("foo/bar").parent(), Some(path("foo"))); |
| assert_eq!(path("foo/bar/baz").parent(), Some(path("foo/bar"))); |
| } |
| |
| /// Construct a [`Path`] from a raw `&str`, or panic trying. |
| #[track_caller] |
| fn path(raw: &str) -> Path { |
| Path::parse(raw).unwrap() |
| } |
| } |