use core::fmt;
use std::error;
use std::path::{Path, PathBuf};
use std::prelude::v1::*;
use crate::{Component, RelativePathBuf};
mod sealed {
use std::path::{Path, PathBuf};
pub trait Sealed {}
impl Sealed for Path {}
impl Sealed for PathBuf {}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RelativeToError {
kind: RelativeToErrorKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
enum RelativeToErrorKind {
NonUtf8,
PrefixMismatch,
AmbiguousTraversal,
IllegalComponent,
}
impl fmt::Display for RelativeToError {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
match self.kind {
RelativeToErrorKind::NonUtf8 => "path contains non-utf8 component".fmt(fmt),
RelativeToErrorKind::PrefixMismatch => {
"paths contain different absolute prefixes".fmt(fmt)
}
RelativeToErrorKind::AmbiguousTraversal => {
"path traversal cannot be determined".fmt(fmt)
}
RelativeToErrorKind::IllegalComponent => "path contains illegal components".fmt(fmt),
}
}
}
impl error::Error for RelativeToError {}
impl From<RelativeToErrorKind> for RelativeToError {
#[inline]
fn from(kind: RelativeToErrorKind) -> Self {
Self { kind }
}
}
pub trait PathExt: sealed::Sealed {
fn relative_to<P>(&self, root: P) -> Result<RelativePathBuf, RelativeToError>
where
P: AsRef<Path>;
}
impl PathExt for Path {
fn relative_to<P>(&self, root: P) -> Result<RelativePathBuf, RelativeToError>
where
P: AsRef<Path>,
{
use std::path::Component::{CurDir, Normal, ParentDir, Prefix, RootDir};
fn std_to_c(c: std::path::Component<'_>) -> Result<Component<'_>, RelativeToError> {
Ok(match c {
CurDir => Component::CurDir,
ParentDir => Component::ParentDir,
Normal(n) => Component::Normal(n.to_str().ok_or(RelativeToErrorKind::NonUtf8)?),
_ => return Err(RelativeToErrorKind::IllegalComponent.into()),
})
}
let root = root.as_ref();
let mut a_it = self.components();
let mut b_it = root.components();
let (a_head, b_head) = loop {
match (a_it.next(), b_it.next()) {
(Some(RootDir), Some(RootDir)) => (),
(Some(Prefix(a)), Some(Prefix(b))) if a == b => (),
(Some(Prefix(_) | RootDir), _) | (_, Some(Prefix(_) | RootDir)) => {
return Err(RelativeToErrorKind::PrefixMismatch.into());
}
(None, None) => break (None, None),
(a, b) if a != b => break (a, b),
_ => (),
}
};
let mut a_it = a_head.into_iter().chain(a_it);
let mut b_it = b_head.into_iter().chain(b_it);
let mut buf = RelativePathBuf::new();
loop {
let a = if let Some(a) = a_it.next() {
a
} else {
for _ in b_it {
buf.push(Component::ParentDir);
}
break;
};
match b_it.next() {
Some(CurDir) => buf.push(std_to_c(a)?),
Some(ParentDir) => {
return Err(RelativeToErrorKind::AmbiguousTraversal.into());
}
root => {
if root.is_some() {
buf.push(Component::ParentDir);
}
for comp in b_it {
match comp {
ParentDir => {
if !buf.pop() {
return Err(RelativeToErrorKind::AmbiguousTraversal.into());
}
}
CurDir => (),
_ => buf.push(Component::ParentDir),
}
}
buf.push(std_to_c(a)?);
for c in a_it {
buf.push(std_to_c(c)?);
}
break;
}
}
}
Ok(buf)
}
}
impl PathExt for PathBuf {
#[inline]
fn relative_to<P>(&self, root: P) -> Result<RelativePathBuf, RelativeToError>
where
P: AsRef<Path>,
{
self.as_path().relative_to(root)
}
}
#[cfg(test)]
mod tests {
use std::path::Path;
use super::{PathExt, RelativeToErrorKind};
use crate::{RelativePathBuf, RelativeToError};
macro_rules! assert_relative_to {
($path:expr, $base:expr, Ok($expected:expr) $(,)?) => {
assert_eq!(
Path::new($path).relative_to($base),
Ok(RelativePathBuf::from($expected))
);
};
($path:expr, $base:expr, Err($expected:ident) $(,)?) => {
assert_eq!(
Path::new($path).relative_to($base),
Err(RelativeToError::from(RelativeToErrorKind::$expected))
);
};
}
#[cfg(windows)]
macro_rules! abs {
($path:expr) => {
Path::new(concat!("C:\\", $path))
};
}
#[cfg(not(windows))]
macro_rules! abs {
($path:expr) => {
Path::new(concat!("/", $path))
};
}
#[test]
#[cfg(windows)]
fn test_different_prefixes() {
assert_relative_to!("C:\\repo", "D:\\repo", Err(PrefixMismatch),);
assert_relative_to!("C:\\repo", "C:\\repo", Ok(""));
assert_relative_to!(
"\\\\server\\share\\repo",
"\\\\server2\\share\\repo",
Err(PrefixMismatch),
);
}
#[test]
fn test_absolute() {
assert_relative_to!(abs!("foo"), abs!("bar"), Ok("../foo"));
assert_relative_to!("foo", "bar", Ok("../foo"));
assert_relative_to!(abs!("foo"), "bar", Err(PrefixMismatch));
assert_relative_to!("foo", abs!("bar"), Err(PrefixMismatch));
}
#[test]
fn test_identity() {
assert_relative_to!(".", ".", Ok(""));
assert_relative_to!("../foo", "../foo", Ok(""));
assert_relative_to!("./foo", "./foo", Ok(""));
assert_relative_to!("/foo", "/foo", Ok(""));
assert_relative_to!("foo", "foo", Ok(""));
assert_relative_to!("../foo/bar/baz", "../foo/bar/baz", Ok(""));
assert_relative_to!("foo/bar/baz", "foo/bar/baz", Ok(""));
}
#[test]
fn test_subset() {
assert_relative_to!("foo", "fo", Ok("../foo"));
assert_relative_to!("fo", "foo", Ok("../fo"));
}
#[test]
fn test_empty() {
assert_relative_to!("", "", Ok(""));
assert_relative_to!("foo", "", Ok("foo"));
assert_relative_to!("", "foo", Ok(".."));
}
#[test]
fn test_relative() {
assert_relative_to!("../foo", "../bar", Ok("../foo"));
assert_relative_to!("../foo", "../foo/bar/baz", Ok("../.."));
assert_relative_to!("../foo/bar/baz", "../foo", Ok("bar/baz"));
assert_relative_to!("foo/bar/baz", "foo", Ok("bar/baz"));
assert_relative_to!("foo/bar/baz", "foo/bar", Ok("baz"));
assert_relative_to!("foo/bar/baz", "foo/bar/baz", Ok(""));
assert_relative_to!("foo/bar/baz", "foo/bar/baz/", Ok(""));
assert_relative_to!("foo/bar/baz/", "foo", Ok("bar/baz"));
assert_relative_to!("foo/bar/baz/", "foo/bar", Ok("baz"));
assert_relative_to!("foo/bar/baz/", "foo/bar/baz", Ok(""));
assert_relative_to!("foo/bar/baz/", "foo/bar/baz/", Ok(""));
assert_relative_to!("foo/bar/baz", "foo/", Ok("bar/baz"));
assert_relative_to!("foo/bar/baz", "foo/bar/", Ok("baz"));
assert_relative_to!("foo/bar/baz", "foo/bar/baz", Ok(""));
}
#[test]
fn test_current_directory() {
assert_relative_to!(".", "foo", Ok("../."));
assert_relative_to!("foo", ".", Ok("foo"));
assert_relative_to!("/foo", "/.", Ok("foo"));
}
#[test]
fn assert_does_not_skip_parents() {
assert_relative_to!("some/path", "some/foo/baz/path", Ok("../../../path"));
assert_relative_to!("some/path", "some/foo/bar/../baz/path", Ok("../../../path"));
}
#[test]
fn test_ambiguous_paths() {
assert_relative_to!(".", "../..", Err(AmbiguousTraversal));
assert_relative_to!(".", "a/../..", Err(AmbiguousTraversal));
assert_relative_to!("../a/..", "../a/../b", Ok(".."));
assert_relative_to!("../a/../b", "../a/..", Ok("b"));
}
}