Skip to content

Instantly share code, notes, and snippets.

@Screwtapello
Last active June 15, 2018 04:01
Show Gist options
  • Save Screwtapello/388e67543d27c9e31e91476e4baaa6ce to your computer and use it in GitHub Desktop.
Save Screwtapello/388e67543d27c9e31e91476e4baaa6ce to your computer and use it in GitHub Desktop.
Monotonic path implementation for Rust
//! Cleaned-up cross-platform path handling
//!
//! Most operating systems accept a complex syntax for specifying filesystem
//! paths, including special notation for things like "the current directory"
//! and "the parent directory" that make path-handling code intricate. If
//! filesystem paths always described a straight-line path from the root to
//! the file or directory in question, path-handling code could be much simpler.
//!
//! This module contains types representing exactly those kinds of paths.
//!
//! # Examples
//!
//! ```rust
//! # fn foo() -> Result<(), Box<std::error::Error>> {
//! let install_structure = vec![
//! Relative::new("bin")?;
//! Relative::new("lib")?;
//! Relative::new("share/applications")?;
//! Relative::new("share/icons")?;
//! Relative::new("share/man")?;
//! ];
//!
//! let raw_install_path = std::env::os_args().next()?;
//! let install_path = Absolute::new(raw_install_path)?;
//!
//! for each in install_structure.iter() {
//! std::fs::create_dir_all(install_path.join_relative(each))?;
//! }
//! # Ok(())
//! # }
//! ```
use std::collections;
use std::error;
use std::ffi;
use std::fmt;
use std::io;
use std::path;
/// An error encountered during path handling.
#[derive(Debug)]
pub enum Error {
/// An error returned by the operating system.
///
/// `err` is the underlying error returned by the operating system.
///
/// `at` is the path that provoked the error.
IoError {
err: io::Error,
at: path::PathBuf,
},
/// Returned by [`Absolute::new()`] when given a path that involves a
/// symlink loop.
///
/// [`Absolute::new()`]: struct.Absolute.html#method.new
SymlinkLoops(path::PathBuf),
/// Returned by [`Relative::new()`] when given a (partially or fully) absolute
/// path.
///
/// [`Relative::new()`]: struct.Relative.html#method.new
PathIsAbsolute(path::PathBuf),
/// Returned by [`Relative::new()`] when given a path with enough `/../`
/// components to escape whatever prefix it's joined to.
///
/// [`Relative::new()`]: struct.Relative.html#method.new
RelativePathEscapesPrefix(path::PathBuf),
#[doc(hidden)]
__NonExhaustive,
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
match self {
Error::IoError { err, at } => write!(f, "{}: {:?}", err, at),
Error::SymlinkLoops(p) => {
write!(f, "Found an infinite symlink loop: {:?}", p)
}
Error::PathIsAbsolute(p) => write!(
f,
"Tried to make a relative path from absolute path {:?}",
p
),
Error::RelativePathEscapesPrefix(p) => write!(
f,
"Tried to make a relative path with leading '..': {:?}",
p
),
_ => write!(f, "Unknown error"),
}
}
}
impl error::Error for Error {
fn description(&self) -> &str {
match self {
Error::IoError { err: e, at: _ } => e.description(),
Error::SymlinkLoops(_) => "Found an infinite symlink loop",
Error::PathIsAbsolute(_) => {
"Tried to make a relative path from an absolute path"
}
Error::RelativePathEscapesPrefix(_) => {
"Tried to make a relative path with leading '..'"
}
_ => "Unknown error",
}
}
}
/// Splits a path into a head and a tail.
///
/// The "head" consists of the Prefix and RootDir components, if any,
/// and is fully canonicalized.
///
/// The "tail" consists of all the other components that follow the head.
///
/// # Errors
///
/// Returns [`Error::IoError`] if the head cannot be canonicalized.
/// For example, if the process' current working directory has been deleted,
/// or the given path includes a syntactically invalid prefix.
///
/// [`Error::IoError`]: struct.Error.html#variant.IoError
fn split_head_and_tail<P: AsRef<path::Path>>(
path: P,
) -> Result<
(
path::PathBuf,
collections::VecDeque<ffi::OsString>,
),
Error,
> {
let path = path.as_ref();
debug!("Splitting head and tail of {:?}", path);
// The path's head is the prefix and root components (if any).
fn is_head_part(c: &path::Component) -> bool {
match c {
path::Component::Prefix(_) => true,
path::Component::RootDir => true,
_ => false,
}
}
let mut head: path::PathBuf = path.components()
.take_while(is_head_part)
.collect();
// If this path is purely relative, it's relative to our current
// directory.
if head.as_os_str() == "" {
head.push(".");
}
debug!("Raw head: {:?}", head);
// The path's head can be safely converted into an absolute
// path with .canonicalize(), since it must exist.
head = head.canonicalize()
.map_err(|err| Error::IoError {
err: err,
at: path.into(),
})?;
// The tail is kind of a queue of components to check. Since we will
// be adding and removing things from diffferent sources, we convert
// everything to an `OsString` to make the lifetimes easier.
let tail: collections::VecDeque<ffi::OsString> = path.components()
.skip_while(is_head_part)
.map(|each| each.as_os_str().to_os_string())
.collect();
debug!("Head: {:?}, tail: {:?}", head, tail);
Ok((head, tail))
}
/// Returns the target of the symlink at `path`, if it exists and is one.
///
/// If `path` is a symlink and we can read the target, returns
/// `Ok(Some(target))`.
/// If `path` is not a symlink, returns `Ok(None)`.
/// If `path` does not exist, it's still not a symlink and this function
/// returns `Ok(None)`.
/// Otherwise, returns the relevant error.
fn read_link_if_exists<P: AsRef<path::Path>>(
path: P,
) -> Result<Option<path::PathBuf>, Error> {
let path = path.as_ref();
// In theory, we could just call .read_link() directly and handle the error
// result, but:
//
// - on Windows, the "can't read this because it isn't a symlink"
// error is returned as an io::ErrorKind::Other rather than
// io::ErrorKind::InvalidInput.
// - on Windows, some not-smart filesystems (like VirtualBox's shared
// folders) don't understand the API call, and just return "invalid
// operation".
//
// Therefore, we'll check if the thing is a symlink before trying to read
// it.
path.symlink_metadata()
// If we successfully got metadata, check if it's a symlink.
.map(|metadata| metadata.file_type().is_symlink())
// If we failed to get metadata...
.or_else(|err| {
if err.kind() == io::ErrorKind::NotFound {
// ...and we failed because the path doesn't exist, by
// definition it cannot be a symlink.
debug!(
"{:?} does not exist, it's not a symlink",
path,
);
Ok(false)
} else {
// Any other error we can return as-is.
error!("Could not check {:?}", path);
Err(err)
}
})
// If we know for sure whether this is a symlink...
.and_then(|is_symlink| {
// ...and it *is* a symlink...
if is_symlink {
// ...let's read it to find the target.
debug!("{:?} exists and is a symlink", path);
Ok(Some(path.read_link()?))
// ...and it *isn't* a symlink...
} else {
// ...then obviously we don't have a target.
debug!("{:?} exists and isn't a symlink", path);
Ok(None)
}
})
.map_err(|err| Error::IoError {
err: err,
at: path.into(),
})
}
/// An absolute path that may or may not exist.
///
/// This path obeys the following invariants:
///
/// - It is absolute, having a prefix (on Windows) and a root directory
/// component.
/// - It contains only named path components, no `/./` or `/../` ones.
/// - It uses the platform-native path-component separator (`/` on POSIX,
/// `\` on Windows).
///
/// Therefore:
///
/// - It's always reasonably straight-forward for humans to understand.
/// - On Windows, it uses [extended-length path syntax], so cross-platform
/// applications don't need to worry about most traditional Windows path
/// limitations.
/// - You can join more named path components on the end without having to
/// check the filesystem or re-normalize the path.
///
/// Since this type implements `AsRef<Path>`, it can be used with almost any
/// standard library function that expects a path.
///
/// [extended-length path syntax]: https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx#maxpath
///
/// # Examples
///
/// ```rust
/// # fn example() -> Result<(), Box<std::error::Error>> {
/// use std::fs;
/// use std::fs::Write;
///
/// let log_storage = Absolute::new("/var/log/myapp")?;
///
/// let current_log = log_storage.join("events")?;
/// let previous_log = log_storage.join("events.old")?;
///
/// fs::rename(current_log, previous_log)?;
///
/// let log_file = fs::OpenOptions::new()
/// .write(true)
/// .create(true)
/// .open(current_log)?;
///
/// write!(&mut log_file, "Log rotated.")?;
///
/// # Ok(())
/// # }
/// ```
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Absolute(path::PathBuf);
impl Absolute {
/// Convert an arbitrary path to follow the rules for an `Absolute` path.
///
/// - If the path is relative, it is interpreted relative to the process'
/// current working directory.
/// - Any `/./` components in the path are removed.
/// - If a component that refers to an existing, readable symlink is
/// followed by a `/../` component, it will be resolved so that the
/// overall path's meaning is unchanged.
/// - If a component that does not exist in the filesystem, or which refers
/// to an ordinary file or directory, is followed by a `/../` component,
/// they cancel each other out and both are removed.
/// - Other components are left alone.
///
/// # Performance
///
/// In the best-case, the given path already follows the rules, and
/// we only call [`canonicalize()`] on the head (the prefix and root
/// directory, if any) to convert it to canonical syntax.
///
/// In general, we will call [`symlink_metadata()`] on every component
/// preceding a `/../` component, and (if it turns out to be a symlink)
/// [`read_link()`]. The process repeats if the symlink target includes
/// any `/../` components of its own.
///
/// [`canonicalize()`]: https://doc.rust-lang.org/stable/std/fs/fn.canonicalize.html
/// [`symlink_metadata()`]: https://doc.rust-lang.org/stable/std/fs/fn.symlink_metadata.html
/// [`read_link()`]: https://doc.rust-lang.org/stable/std/fs/fn.read_link.html
///
/// # Platform-specific behaviour
///
/// On Windows, this method correctly handles partially-absolute paths like
/// `D:foo\bar.txt` that are relative to a path other than the current
/// working directory.
///
/// On Windows, the resulting path uses [extended-length path syntax], so
/// it may confuse other applications not designed to handle such paths.
/// For example, if you pass such a path on another application's command
/// line, or write it to a configuration ile.
///
/// [extended-length path syntax]: https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx#maxpath
///
/// # Errors
///
/// Returns [`Error::IoError`] if the head of the given path cannot be
/// canonicalized. For example, if the process' current working directory
/// has been deleted, or the given path includes a syntactically invalid
/// prefix.
///
/// The same variant is returned if a problem is encountered while checking
/// if a given path is a symlink, or while trying to read a symlink. For
/// example, if the current user does not have permission to read the
/// directory containing it, or the path is on a network-mounted filesystem
/// that stopped responding.
///
/// Returns [`Error::SymlinkLoops`] if resolving a symlink takes us back
/// to a previously-resolved symlink. For example, if `/example/path/a`
/// is a symlink to `/example/path/b`, and `b` is a symlink back to `a`,
/// then giving this method a path like `/example/path/a/../c` will return
/// this error. It's like the POSIX `ELOOP` error, but cross-platform.
///
/// Note that "does not exist" is *not* a fatal error for this function;
/// path components that do not exist by definition are not symlinks, and
/// are treated the same way as every other component that is not a symlink.
///
/// [`Error::IoError`]: struct.Error.html#variant.IoError
/// [`Error::SymlinkLoops`]: struct.Error.html#variant.SymlinkLoops
///
/// # Examples
///
/// ```rust
/// # fn example() -> Result<(), Error> {
/// let real_current_directory = Absolute::new(std::env::current_dir()?)?;
/// # Ok(())
/// # }
/// ```
pub fn new<P: AsRef<path::Path>>(path: P) -> Result<Absolute, Error> {
// This function steps through the components of path, checking each one
// for monotonicity and building up a proper monotonic path in `res`.
// `tail` contains the path components we've yet to check and clean up.
let (mut res, mut tail) = split_head_and_tail(path)?;
let mut seen_paths = collections::BTreeSet::new();
// While there's still components left to check...
while tail.len() > 0 {
debug!(
"Monotonic path: {:?}, to check: {:?}",
res, tail
);
// Grab the component at the front of the tail, so we can check it.
let current = tail.pop_front()
.expect("len() > 0 but vec is empty?");
if &current == "." {
// A "." component can be ignored.
//
// blah/./blah -> blah/blah
} else if &current == ".." {
// If we get a ParentDir component, the component at the end of
// `res` might be a symlink. in which case we'll have to splice
// the target path in at the beginning of `tail` so we have the
// full, monotonic path.
//
// If `foo` is not a symlink:
//
// blah/foo/../blah -> blah/blah
//
// If `foo` is a symlink to `relative/target`:
//
// blah/symlink/../blah -> blah/relative/target/../blah
//
// If `foo` is a symlink to `/absolute/target`:
//
// blah/symlink/../blah -> /absolute/target/../blah
//
// But first, let's check for symlink loops. If we've already
// dereferenced this symlink before, we've hit a loop and we
// will never find a suitable answer.
if seen_paths.contains(&res) {
return Err(Error::SymlinkLoops(res));
}
// This is the first time we've dereferenced this symlink, note
// it down.
seen_paths.insert(res.clone());
match read_link_if_exists(&res)? {
// The component before the ParentDir component was not
// a symlink, so we don't need to do anything to `tail`.
None => (),
// The component before the ParentDir component *was* a
// symlink, so we need to add it to tail so we'll get around
// to checking it.
Some(target) => {
debug!("Symlink target: {:?}", target);
// We'll re-check this ".." component once we've
// checked all the components from target. Since we're
// pushing to the front, we're pushing components in
// reverse order, and so we have to push this first.
tail.push_front("..".into());
// If the target is an absolute path, then the target's
// head replaces our current result. Conveniently,
// Windows will only create symlinks with fully-relative
// or fully-absolute targets, not root-relative
// (`\\foo`) or drive-relative (`C:foo`) paths, so we
// don't need to handle those cases.
let target = if target.is_absolute() {
debug!("Target is absolute");
let (new_head, new_target) =
split_head_and_tail(target)?;
res.push(new_head);
new_target
// The target is relative, so we can use it as-is.
} else {
debug!("Target is relative");
target
.components()
.map(|each| each.as_os_str().to_os_string())
.collect::<collections::VecDeque<_>>()
};
// Push all the components of target in reverse order
// so we'll check them the next time through the loop.
for each in target.into_iter().rev() {
tail.push_front(each);
}
}
}
// If the component at the end of `res` is a symlink, its
// target has been pushed to the front of `tail` and will
// be processed in due course. If it's not a symlink, this
// ParentDir component nullifies it. Either way, we don't want
// the last component of `res` anymore.
res.pop();
} else {
// This component is a normal name. We'll trust it... for now.
res.push(current);
}
}
Ok(Absolute(res))
}
/// Clone this path, attempting to add an arbitrary relative path on the
/// end.
///
/// # Performance
///
/// An expression like:
///
/// ```rust
/// absolute_path.join(path)?
/// ```
///
/// ...is the same as doing:
///
/// ```rust
/// absolute_path.join_relative(&Relative::new(path)?)
/// ```
///
/// ...and therefore involves the same allocation and other costs as
/// calling [`Relative::new()`] yourself.
///
/// If you plan on joining the same relative path to many `Absolute` paths,
/// it's more efficient to call `Relative::new()` once yourself then use
/// [`.join_relative()`] each time.
///
/// [`Relative::new()`]: struct.Relative.html#method.new
/// [`.join_relative()`]: #method.join_relative
///
/// # Errors
///
/// Returns the same errors as [`Relative::new()`].
///
/// # Examples
///
/// ```rust
/// # fn example() -> Result<(), Error> {
/// let metadata_path = extraction_path.join("META-INF/MANIFEST.MF")?;
/// # Ok(())
/// # }
/// ```
pub fn join<P: AsRef<path::Path>>(
&self,
path: P,
) -> Result<Absolute, Error> {
Ok(self.join_relative(&Relative::new(path)?))
}
/// Clone this path, adding the given [`Relative`] path on the end.
///
/// If the thing you want to join isn't already a `Relative`, you may find
/// it more ergonomic to call [`.join()`] instead.
///
/// [`.join()`]: #method.join
///
/// # Performance
///
/// Since a `Relative` is guaranteed to follow the rules for `Absolute`
/// paths (except for being absolute), no additional checks or processing
/// need to be done, just straight concatenation.
///
/// [`Relative`]: struct.Relative.html
///
/// # Examples
///
/// ```rust
/// # fn example() -> Result<Absolute, Error> {
/// let search_path = vec![
/// Absolute::new("/usr/local/bin")?,
/// Absolute::new("/bin")?,
/// Absolute::new("/usr/bin")?,
/// Absolute::new("/sbin")?,
/// Absolute::new("/usr/sbin")?,
/// ];
///
/// let binary = Relative::new("cargo")?;
///
/// for prefix in search_path {
/// let guess = prefix.join_relative(binary);
///
/// if guess.as_path().is_file() {
/// return Ok(guess)
/// }
/// }
///
/// # Ok(Absolute::new(".").unwrap())
/// # }
/// ```
pub fn join_relative(&self, tail: &Relative) -> Absolute {
Absolute(self.0.join(tail))
}
/// Coerces to a [`Path`] slice.
///
/// Since `Absolute` implements `AsRef<Path>`, this method is not needed
/// very often—you can often just pass it directly to the thing that needs
/// a [`Path`].
///
/// [`Path`]: https://doc.rust-lang.org/stable/std/path/struct.Path.html
///
/// # Examples
///
/// If you really, really need to convert an `Absolute` to a [`PathBuf`]:
///
/// ```rust
/// let owned_path: std::path::PathBuf = absolute_path.as_path().into();
/// ```
///
/// [`PathBuf`]: https://doc.rust-lang.org/stable/std/path/struct.PathBuf.html
pub fn as_path(&self) -> &path::Path {
<Self as AsRef<path::Path>>::as_ref(self)
}
/// Coerces to an [`OsStr`] slice.
///
/// Since `Absolute` implements `AsRef<OsStr>`, this method is not needed
/// very often—you can often just pass it directly to the thing that needs
/// an [`OsStr`].
///
/// [`OsStr`]: https://doc.rust-lang.org/stable/std/ffi/struct.OsStr.html
///
/// # Examples
///
/// ```rust
/// let install_path = std::env::os_args()
/// .first()
/// .expect("Specify the installation path on the command line.");
///
/// let windows_path = Absolute::new("C:\\windows").unwrap();
///
/// if windows_path.as_os_str() == install_path {
/// panic!("No, you can't install to the Windows path.");
/// }
/// ```
pub fn as_os_str(&self) -> &ffi::OsStr {
<Self as AsRef<ffi::OsStr>>::as_ref(self)
}
}
impl AsRef<path::Path> for Absolute {
fn as_ref(&self) -> &path::Path {
self.0.as_path()
}
}
impl AsRef<ffi::OsStr> for Absolute {
fn as_ref(&self) -> &ffi::OsStr {
self.0.as_os_str()
}
}
/// A relative path that may be joined to an absolute path.
///
/// This path obeys the following invariants:
///
/// - It is relative, containing no prefix or root directory components.
/// - It contains only named path components, no `/./` or `/../` ones.
/// - It uses the platform-native path-component separator (`/` on POSIX,
/// `\` on Windows).
///
/// Therefore:
///
/// - It's always reasonably straight-forward for humans to understand.
/// - It can safely be appended to an [`Absolute`] path or another
/// `Relative` path without having to revalidatet the invariants.
/// - Joining a `Relative` to an `Absolute` will always produce a path that
/// refers to a child of the `Absolute`, unless the directory named by the
/// `Absolute` contains a symilnk to a directory outside it.
///
/// Since this type implements `AsRef<Path>`, it can be used with almost any
/// standard library function that expects a path, but you probably only want
/// to join it to an `Absolute` path.
///
/// [`Absolute`]: struct.Absolute.html
///
/// # Examples
///
/// ```rust
/// # fn example() -> Result<(), Error> {
/// let search_path = [
/// Absolute::new(get_user_config_path())?,
/// Absolute::new(get_system_config_path())?,
/// Absolute::new(get_default_config_path())?,
/// ];
///
/// let config_name = Relative::new("myapp/video.cfg")?;
///
/// for prefix in search_path {
/// let guess = prefix.join_relative(config_name);
///
/// if guess.as_path().is_file() {
/// return Ok(guess)
/// }
/// }
/// # Ok(())
/// # }
/// ```
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Relative(path::PathBuf);
impl Relative {
/// Convert an arbitrary path to follow the rules for a `Relative` path.
///
/// - Any `/./` components in the path are removed.
/// - If a named component is followed by a `/../` component, they cancel
/// each other out and both are removed. We cannot resolve symlinks here
/// since we do not know what absolute path this path is relative to.
///
/// # Performance
///
/// This validation is performed entirely in memory, with no reference to
/// the filesystem.
///
/// # Errors
///
/// Returns [`Error::PathIsAbsolute`] if the given path contains prefix or
/// root directory components, like `/usr/share` or `C:file.txt`.
///
/// Returns [`Error::RelativePathEscapesPrefix`] if any `/../` components
/// cannot be normalized away, like `a/b/../../../c`.
///
/// [`Error::PathIsAbsolute`]: enum.Error.html#variant.PathIsAbsolute
/// [`Error::RelativePathEscapesPrefix`]: enum.Error.html#variant.RelativePathEscapesPrefix
///
/// # Examples
///
/// ```rust
/// # fn example(
/// # config: std::collections::BTreeMap<String, String>,
/// # request: std::collections::BTreeMap<String, String>,
/// # )
/// # -> Result<(), Error> {
/// let web_root = Absolute::new(
/// config
/// .get("web_root")
/// .unwrap_or("/var/www/root")
/// )?;
///
/// let request_path = Relative::new(
/// request
/// .get("path")
/// unwrap_or("")
/// )?;
///
/// let mut data_path = web_root.join_relative(request_path);
///
/// if data_path.as_path().is_dir() {
/// data_path = data_path.join("index.html");
/// }
/// # Ok(())
/// # }
/// ```
pub fn new<P: AsRef<path::Path>>(path: P) -> Result<Relative, Error> {
let components = path.as_ref().components();
let mut res = path::PathBuf::new();
for each in components {
match each {
path::Component::Prefix(_) | path::Component::RootDir => {
return Err(Error::PathIsAbsolute(path.as_ref().into()));
}
path::Component::Normal(name) => res.push(name),
path::Component::ParentDir => {
if res.as_os_str() == "" {
return Err(Error::RelativePathEscapesPrefix(
path.as_ref().into(),
));
}
res.pop();
}
path::Component::CurDir => (),
}
}
Ok(Relative(res))
}
/// Clone this path, attempting to add an arbitrary relative path on the
/// end.
///
/// # Performance
///
/// An expression like:
///
/// ```rust
/// other_relative_path.join(path)?
/// ```
///
/// ...is the same as doing:
///
/// ```rust
/// other_relative_path.join_relative(&Relative::new(path)?)
/// ```
///
/// ...and therefore involves the same allocation and other costs as
/// calling [`Relative::new()`] yourself.
///
/// If you plan on joining the same relative path to many other `Relative`
/// paths, it's more efficient to call `Relative::new()` once yourself and
/// call [`.join_relative()`] each time.
///
/// [`Relative::new()`]: struct.Relative.html#method.new
/// [`.join_relative()`]: #method.join_relative
///
/// # Errors
///
/// Returns the same errors as [`Relative::new()`].
///
/// # Examples
///
/// ```rust
/// # fn example() -> Result<(), Error> {
/// let config_base = Relative::new("SuperSoftwareCo/MyCoolApp")?;
///
/// let video_config = config_base.join("video.cfg")?;
/// let audio_config = config_base.join("audio.cfg")?;
/// # Ok(())
/// # }
/// ```
pub fn join<P: AsRef<path::Path>>(
&self,
path: P,
) -> Result<Relative, Error> {
Ok(self.join_relative(&Relative::new(path)?))
}
/// Clone this path, adding the given `Relative` path on the end.
///
/// [`.join()`]: #method.join
///
/// # Performance
///
/// Since `Relative` paths all have the same invariants, no additional
/// checks or processing need to be done, just straight concatenation.
///
/// # Examples
///
/// ```rust
/// # fn example(testdir: Absolute) -> Result<(), Error> {
/// let browsers = [
/// Relative::new("Firefox")?,
/// Relative::new("Chrome")?,
/// Relative::new("Edge")?,
/// ];
///
/// let platforms = [
/// Relative::new("Windows")?,
/// Relative::new("Linux")?,
/// Relative::new("macOS")?,
/// ];
///
/// let result_paths: Vec<Relative> = browsers
/// .iter()
/// .flat_map(|name| {
/// platforms
/// .iter()
/// .map(name.join_relative)
/// })
/// .collect();
///
/// # Ok(())
/// # }
/// ```
pub fn join_relative(&self, tail: &Relative) -> Relative {
Relative(self.0.join(tail))
}
/// Coerces to a [`Path`] slice.
///
/// Since `Relative` implements `AsRef<Path>`, this method is not needed
/// very often—you can often just pass it directly to the thing that needs
/// a [`Path`].
///
/// [`Path`]: https://doc.rust-lang.org/stable/std/path/struct.Path.html
///
/// # Examples
///
/// If you really, really need to convert a `Relative` to a [`PathBuf`]:
///
/// ```rust
/// let owned_path: std::path::PathBuf = relative.as_path().into();
/// ```
///
/// [`PathBuf`]: https://doc.rust-lang.org/stable/std/path/struct.PathBuf.html
pub fn as_path(&self) -> &path::Path {
<Self as AsRef<path::Path>>::as_ref(self)
}
/// Coerces to an [`OsStr`] slice.
///
/// Since `Relative` implements `AsRef<OsStr>`, this method is not needed
/// very often—you can often just pass it directly to the thing that needs
/// an [`OsStr`].
///
/// [`OsStr`]: https://doc.rust-lang.org/stable/std/ffi/struct.OsStr.html
///
/// # Examples
///
/// If you really, really need to convert a `Relative` to a [`OsString`]:
///
/// ```rust
/// let owned_string: std::ffi::OsString = relative.as_os_str().into();
/// ```
///
/// [`OsString`]: https://doc.rust-lang.org/stable/std/ffi/struct.OsString.html
pub fn as_os_str(&self) -> &ffi::OsStr {
<Self as AsRef<ffi::OsStr>>::as_ref(self)
}
}
impl AsRef<path::Path> for Relative {
fn as_ref(&self) -> &path::Path {
self.0.as_path()
}
}
impl AsRef<ffi::OsStr> for Relative {
fn as_ref(&self) -> &ffi::OsStr {
self.0.as_os_str()
}
}
#[cfg(test)]
mod tests;
extern crate env_logger;
extern crate tempfile;
use super::*;
use std::env;
use std::fs;
#[test]
fn relative_accepts_empty_path() {
let actual = Relative::new("").expect("Could not parse empty path");
let expected = path::Path::new("");
assert_eq!(actual.0, expected);
}
#[cfg(windows)]
mod windows {
use super::*;
use std::os::windows::fs as winfs;
fn symlink_dir<P, Q>(src: P, dst: Q)
where
P: AsRef<path::Path>,
Q: AsRef<path::Path>,
{
let dst = dst.as_ref();
winfs::symlink_dir(src, &dst).expect(
"Could not create symbolic link. \
Run as Administrator, or on Windows 10 in Developer Mode",
);
let link_exists = dst.symlink_metadata().expect(
"Link creation succeeded, but can't read link? \
If you're using Wine, see bug 44948",
);
}
#[test]
fn absolute_path_gets_canonical_prefix() {
let _ = env_logger::try_init();
let path = Absolute::new("C:\\foo\\bar")
.expect("Could not handle an absolute path");
assert_eq!(path.0, path::Path::new("\\\\?\\C:\\foo\\bar"));
}
#[test]
fn missing_absolute_path_with_double_dot_is_normalized() {
let _ = env_logger::try_init();
let path = Absolute::new("C:\\foo\\..\\bar")
.expect("Could not handle an absolute path");
assert_eq!(path.0, path::Path::new("\\\\?\\C:\\bar"));
}
#[test]
fn absolute_path_with_single_dot_is_dropped() {
let _ = env_logger::try_init();
let path = Absolute::new("C:\\foo\\.\\bar")
.expect("Could not handle an absolute path");
assert_eq!(path.0, path::Path::new("\\\\?\\C:\\foo\\bar"));
}
#[test]
fn absolute_path_with_slashes_is_normalized() {
let _ = env_logger::try_init();
let path = Absolute::new("C:/foo/bar")
.expect("Could not handle an absolute path");
assert_eq!(path.0, path::Path::new("\\\\?\\C:\\foo\\bar"));
}
#[test]
fn relative_path_is_relative_to_current_directory() {
let _ = env_logger::try_init();
let path =
Absolute::new("foo").expect("Could not handle a relative path");
let current_dir = env::current_dir()
.expect("Could not read current directory.")
.canonicalize()
.expect("Could not canonicalize current directory.");
let expected = current_dir.join("foo");
assert_eq!(path.0, expected);
}
#[test]
fn leading_double_dot_in_relative_path() {
let _ = env_logger::try_init();
let path =
Absolute::new("..\\foo").expect("Could not handle a relative path");
let current_dir = env::current_dir()
.expect("Could not read current directory.")
.canonicalize()
.expect("Could not canonicalize current directory.");
let expected = current_dir
.parent()
.unwrap_or(path::Path::new("/"))
.join("foo");
assert_eq!(path.0, expected);
}
#[test]
fn skip_leading_dot_in_relative_path() {
let _ = env_logger::try_init();
let path =
Absolute::new(".\\foo").expect("Could not handle a relative path");
let current_dir = env::current_dir()
.expect("Could not read current directory.")
.canonicalize()
.expect("Could not canonicalize current directory.");
let expected = current_dir.join("foo");
assert_eq!(path.0, expected);
}
#[test]
fn drive_relative_path_is_relative_to_current_drive() {
let _ = env_logger::try_init();
let actual = Absolute::new("\\foo")
.expect("Could not handle a prefix-relative path");
let current_dir =
env::current_dir().expect("Could not read current directory.");
let mut current_drive_root = path::PathBuf::new();
current_drive_root.push(
current_dir
.components()
.next()
.expect("Current directory is an empty path?"),
);
current_drive_root.push("\\");
let expected = fs::canonicalize(current_drive_root)
.expect("Could not canonicalize current drive")
.join("foo");
assert_eq!(actual.0, expected);
}
/*
This test can't work without having more than one valid drive letter,
which we can't possibly guarantee.
#[test]
fn prefixed_relative_path_is_relative_to_drive_directory() {
let _ = env_logger::try_init();
// Save the current directory on the current drive.
let original_dir =
env::current_dir().expect("Could not read current directory.");
// Remember which drive that's on.
let original_drive = original_dir
.components()
.next()
.expect("Current directory is an empty path?")
.as_os_str();
// Switch to a different drive.
assert_ne!(original_drive, "A:");
env::set_current_dir("A:\\")
.expect("Could not set current directory to A:\\");
}
*/
#[test]
fn do_not_resolve_every_symlink() {
let _ = env_logger::try_init();
let temp =
tempfile::tempdir().expect("Could not make temporary directory.");
let temp_path = temp.into_path();
let target_path = temp_path.join("target");
let link_path = temp_path.join("link");
// Create an empty file as the target.
println!("Creating target path: {:?}", target_path);
fs::OpenOptions::new()
.create(true)
.write(true)
.open(&target_path)
.expect("Could not create target");
assert_eq!(target_path.exists(), true);
println!("Creating link path: {:?}", link_path);
symlink_dir(&target_path, &link_path);
println!("Absolutizing symlink path");
let actual =
Absolute::new(&link_path).expect("Could not handle symlink path");
println!("Actual: {:?}", actual.0);
let expected = temp_path
.canonicalize()
.expect("Could not canonicalize symlink path")
.join("link");
assert_eq!(actual.0, expected);
}
#[test]
fn resolve_symlink_before_parentdir_component() {
let _ = env_logger::try_init();
let temp =
tempfile::tempdir().expect("Could not make temporary directory.");
// apple/ant is a directory that actually exists on disk.
fs::create_dir_all(temp.path().join("apple/ant"))
.expect("Could not create Path A");
// banana/ exists, but banana/bat is just a symlink to apple/ant.
fs::create_dir_all(temp.path().join("banana"))
.expect("Could not create Path B");
symlink_dir("../apple/ant", temp.path().join("banana/bat"));
let actual = Absolute::new(temp.path().join("banana/bat/../tail"))
.expect("Could not handle a symlinked path.");
let expected = temp.path()
.canonicalize()
.expect("Could not canonicalize temp path")
.join("apple\\tail");
assert_eq!(actual.0, expected);
}
#[test]
fn cannot_go_up_from_root_directory() {
let _ = env_logger::try_init();
let actual = Absolute::new("C:\\..\\foo")
.expect("Could not handle going up from the root directory");
let expected = fs::canonicalize("C:\\")
.expect("No C:\\ directory?")
.join("foo");
assert_eq!(actual.0, expected);
}
#[test]
fn resolve_absolute_symlinks() {
let _ = env_logger::try_init();
let temp = tempfile::tempdir()
.expect("Could not make temporary directory.")
.into_path();
let link_path = temp.join("link");
debug!("Creating link at: {:?}", link_path);
symlink_dir("C:\\does\\not\\exist", &link_path);
let actual = Absolute::new(link_path.join("..\\bar"))
.expect("Could not handle symlink with absolute target");
let expected = fs::canonicalize("C:\\")
.expect("No C:\\ directory?")
.join("does\\not\\bar");
assert_eq!(actual.0, expected);
}
#[test]
fn absolute_rejects_dereferencing_cyclic_symlinks() {
let _ = env_logger::try_init();
let temp =
tempfile::tempdir().expect("Could not make temporary directory.");
symlink_dir("a", temp.path().join("b"));
symlink_dir("b", temp.path().join("a"));
let err = Absolute::new(temp.path().join("a/../tail"))
.expect_err("Cyclic symlinks dereferenced?");
match err {
// This is what we expected.
Error::SymlinkLoops(_) => (),
// Uh oh.
e => panic!("Got unexpected error: {:?}", e),
}
}
#[test]
fn absolute_accepts_navigating_cyclic_symlinks() {
let _ = env_logger::try_init();
let temp =
tempfile::tempdir().expect("Could not make temporary directory.");
symlink_dir(".", temp.path().join("cur"));
let actual = Absolute::new(temp.path().join("cur/cur/cur/cur/cur/a"))
.expect("Could not handle cyclic path.");
let expected = temp.path()
.canonicalize()
.expect("Could not canonicalize temp dir")
.join("cur\\cur\\cur\\cur\\cur\\a");
assert_eq!(actual.0, expected);
}
#[test]
fn absolute_rejects_invalid_prefix() {
let _ = env_logger::try_init();
let err = Absolute::new("\\\\?\\bogus\\path\\")
.map(|p| {
panic!("Parsing bogus path should have failed! {:?}", p);
})
.err()
.unwrap();
match err {
Error::IoError { err, at } => {
assert_eq!(err.kind(), io::ErrorKind::NotFound);
assert_eq!(at, path::Path::new("\\\\?\\bogus\\path"));
}
e => panic!("Got unexpected error {:?}", e),
}
}
#[test]
fn relative_accepts_monotonic_path() {
let _ = env_logger::try_init();
let actual = Relative::new("does\\not\\exist")
.expect("Could not parse monotonic path");
let expected = path::Path::new("does\\not\\exist");
assert_eq!(actual.0, expected);
}
#[test]
fn relative_normalises_slashes() {
let _ = env_logger::try_init();
let actual = Relative::new("does/not/exist")
.expect("Could not parse path with slashes");
let expected = path::Path::new("does\\not\\exist");
assert_eq!(actual.0, expected);
}
#[test]
fn relative_normalises_single_dots() {
let _ = env_logger::try_init();
let actual = Relative::new("does\\.\\not\\.\\exist")
.expect("Could not parse path with CurrentDir components");
let expected = path::Path::new("does\\not\\exist");
assert_eq!(actual.0, expected);
}
#[test]
fn relative_normalises_inner_double_dots() {
let _ = env_logger::try_init();
let actual = Relative::new("does\\..\\not\\exist")
.expect("Could not parse path with ParentDir component");
let expected = path::Path::new("not\\exist");
assert_eq!(actual.0, expected);
}
#[test]
fn relative_rejects_leading_double_dots() {
let _ = env_logger::try_init();
let err = Relative::new("..\\does\\not\\exist")
.err()
.expect("Parsing leading '..' should have failed!");
match err {
Error::RelativePathEscapesPrefix(p) => {
assert_eq!(p, path::Path::new("..\\does\\not\\exist"))
}
e => panic!("Got unexpected error {:?}", e),
}
}
#[test]
fn relative_rejects_fully_absolute_path() {
let _ = env_logger::try_init();
let err = Relative::new("C:\\absolute\\path")
.err()
.expect("Parsing an absolute path should have failed!");
match err {
Error::PathIsAbsolute(p) => {
assert_eq!(p, path::Path::new("C:\\absolute\\path"))
}
e => panic!("Got unexpected error {:?}", e),
}
}
#[test]
fn relative_rejects_prefix_relative_path() {
let _ = env_logger::try_init();
let err = Relative::new("\\relative\\path")
.err()
.expect("Parsing prefix-relative path should have failed!");
match err {
Error::PathIsAbsolute(p) => {
assert_eq!(p, path::Path::new("\\relative\\path"))
}
e => panic!("Got unexpected error {:?}", e),
}
}
#[test]
fn relative_rejects_drive_relative_path() {
let _ = env_logger::try_init();
let err = Relative::new("C:relative\\path")
.err()
.expect("Parsing drive-relative path should have failed!");
match err {
Error::PathIsAbsolute(p) => {
assert_eq!(p, path::Path::new("C:relative\\path"))
}
e => panic!("Got unexpected error {:?}", e),
}
}
#[test]
fn relative_rejects_relative_path_escaping_prefix() {
let _ = env_logger::try_init();
let err = Relative::new("inside\\..\\..\\outside")
.err()
.expect("Parsing escaping path should have failed!");
match err {
Error::RelativePathEscapesPrefix(p) => {
assert_eq!(p, path::Path::new("inside\\..\\..\\outside"))
}
e => panic!("Got unexpected error {:?}", e),
}
}
}
#[cfg(unix)]
mod unix {
use super::*;
use std::os::unix::fs as unixfs;
#[test]
fn missing_absolute_path_is_unchanged() {
let _ = env_logger::try_init();
let expected = path::Path::new("/some/absolute/path");
let path = Absolute::new(&expected)
.expect("Could not handle an absolute path");
assert_eq!(path.0, expected);
}
#[test]
fn missing_absolute_path_with_double_dot_is_normalized() {
let _ = env_logger::try_init();
let actual = Absolute::new("/some/absolute/../path")
.expect("Could not handle an absolute path");
let expected = path::Path::new("/some/path");
assert_eq!(actual.0, expected);
}
#[test]
fn existing_path_with_double_dot_is_normalized() {
let _ = env_logger::try_init();
let temp =
tempfile::tempdir().expect("Could not make temporary directory.");
fs::create_dir_all(temp.path().join("apple"))
.expect("Could not create apple directory.");
fs::create_dir_all(temp.path().join("banana"))
.expect("Could not create apple directory.");
let actual = Absolute::new(temp.path().join("banana/../apple"))
.expect("Could not resolve existing directories.");
let expected = temp.path().join("apple");
assert_eq!(actual.0, expected);
}
#[test]
fn missing_relative_path_is_relative_to_current_directory() {
let _ = env_logger::try_init();
let actual = Absolute::new("does/not/exist")
.expect("Could not handle a missing relative path");
let expected = env::current_dir()
.expect("Could not get current directory")
.join("does/not/exist");
assert_eq!(actual.0, expected);
}
#[test]
fn skip_leading_dots() {
let _ = env_logger::try_init();
let actual = Absolute::new("/some/./missing/./path")
.expect("Could not handle a path with dot components");
let expected = path::Path::new("/some/missing/path");
assert_eq!(actual.0, expected);
}
#[test]
fn leading_double_dot_in_relative_path() {
let _ = env_logger::try_init();
let actual = Absolute::new("../does/not/exist")
.expect("Could not handle a relative path");
let expected = env::current_dir()
.expect("Could not get current directory")
.parent()
.unwrap_or(path::Path::new("/"))
.join("does/not/exist");
assert_eq!(actual.0, expected);
}
#[test]
fn do_not_resolve_every_symlink() {
let _ = env_logger::try_init();
let temp =
tempfile::tempdir().expect("Could not make temporary directory.");
let target_path = temp.path().join("target");
let link_path = temp.path().join("link");
// Create an empty file as the target.
fs::OpenOptions::new()
.create(true)
.write(true)
.open(&target_path)
.expect("Could not create target");
unixfs::symlink(&target_path, &link_path)
.expect("Could not create symbolic link.");
let actual =
Absolute::new(&link_path).expect("Could not handle symlink path");
assert_eq!(actual.0, link_path);
}
#[test]
fn resolve_symlink_before_parentdir_component() {
let _ = env_logger::try_init();
let temp =
tempfile::tempdir().expect("Could not make temporary directory.");
// apple/ant is a directory that actually exists on disk.
fs::create_dir_all(temp.path().join("apple/ant"))
.expect("Could not create Path A");
// banana/ exists, but banana/bat is just a symlink to apple/ant.
fs::create_dir_all(temp.path().join("banana"))
.expect("Could not create Path B");
unixfs::symlink("../apple/ant", temp.path().join("banana/bat"))
.expect("Could not create symlink.");
let actual = Absolute::new(temp.path().join("banana/bat/../tail"))
.expect("Could not handle a symlinked path.");
let expected = temp.path().join("apple/tail");
assert_eq!(actual.0, expected);
}
#[test]
fn cannot_go_up_from_root_directory() {
let _ = env_logger::try_init();
let actual = Absolute::new("/../foo")
.expect("Could not handle going up from the root directory");
let expected = path::Path::new("/foo");
assert_eq!(actual.0, expected);
}
#[test]
fn resolve_absolute_symlinks() {
let _ = env_logger::try_init();
let temp =
tempfile::tempdir().expect("Could not make temporary directory.");
unixfs::symlink("/does/not/exist", temp.path().join("link"))
.expect("Could not create symlink");
let actual = Absolute::new(temp.path().join("link/../bar"))
.expect("Could not handle symlink with absolute target");
let expected = path::Path::new("/does/not/bar");
assert_eq!(actual.0, expected);
}
#[test]
fn absolute_rejects_dereferencing_cyclic_symlinks() {
let _ = env_logger::try_init();
let temp =
tempfile::tempdir().expect("Could not make temporary directory.");
unixfs::symlink("a", temp.path().join("b"))
.expect("Could not create symlink b->a");
unixfs::symlink("b", temp.path().join("a"))
.expect("Could not create symlink a->b");
let err = Absolute::new(temp.path().join("a/../tail"))
.expect_err("Cyclic symlinks dereferenced?");
match err {
// This is what we expected.
Error::SymlinkLoops(_) => (),
// Uh oh.
e => panic!("Got unexpected error: {:?}", e),
}
}
#[test]
fn absolute_accepts_navigating_cyclic_symlinks() {
let _ = env_logger::try_init();
let temp =
tempfile::tempdir().expect("Could not make temporary directory.");
unixfs::symlink(".", temp.path().join("cur"))
.expect("Could not create symilnk pointing at parent");
let expected = temp.path().join("cur/cur/cur/cur/cur/a");
let actual =
Absolute::new(&expected).expect("Could not handle cyclic path.");
assert_eq!(actual.0, expected);
}
#[test]
fn absolute_reports_permission_error() {
let _ = env_logger::try_init();
let temp =
tempfile::tempdir().expect("Could not make temporary directory.");
fs::create_dir_all(temp.path().join("dir/secret"))
.expect("Could not create directory");
// Make "dir" unreadable.
use self::unixfs::PermissionsExt;
fs::set_permissions(
temp.path().join("dir"),
fs::Permissions::from_mode(0),
).expect("Could not set permissions");
// Now, trying to stat 'secret' should give permission denied.
let err = Absolute::new(temp.path().join("dir/secret/.."))
.map(|p| {
panic!(
"Getting absolute path should have failed: {:?}",
p
);
})
.err()
.unwrap();
match err {
Error::IoError { err, at } => {
assert_eq!(err.kind(), io::ErrorKind::PermissionDenied);
assert_eq!(at, temp.path().join("dir/secret"));
}
e => panic!("Got unexpected error: {:?}", e),
}
}
#[test]
fn relative_accepts_monotonic_path() {
let actual = Relative::new("does/not/exist")
.expect("Could not parse monotonic path");
let expected = path::Path::new("does/not/exist");
assert_eq!(actual.0, expected);
}
#[test]
fn relative_normalises_single_dots() {
let actual = Relative::new("does/./not/./exist")
.expect("Could not parse path with CurrentDir components");
let expected = path::Path::new("does/not/exist");
assert_eq!(actual.0, expected);
}
#[test]
fn relative_normalises_inner_double_dots() {
let actual = Relative::new("does/../not/exist")
.expect("Could not parse path with ParentDir component");
let expected = path::Path::new("not/exist");
assert_eq!(actual.0, expected);
}
#[test]
fn relative_rejects_leading_double_dots() {
let err = Relative::new("../does/not/exist")
.err()
.expect("Parsing leading '..' should have failed!");
match err {
Error::RelativePathEscapesPrefix(p) => {
assert_eq!(p, path::Path::new("../does/not/exist"))
}
e => panic!("Got unexpected error {:?}", e),
}
}
#[test]
fn relative_rejects_relative_path_escaping_prefix() {
let err = Relative::new("inside/../../outside")
.err()
.expect("Parsing escaping path should have failed!");
match err {
Error::RelativePathEscapesPrefix(p) => {
assert_eq!(p, path::Path::new("inside/../../outside"))
}
e => panic!("Got unexpected error {:?}", e),
}
}
#[test]
fn relative_rejects_fully_absolute_path() {
let err = Relative::new("/absolute/path")
.err()
.expect("Parsing an absolute path should have failed!");
match err {
Error::PathIsAbsolute(p) => {
assert_eq!(p, path::Path::new("/absolute/path"))
}
e => panic!("Got unexpected error {:?}", e),
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment