use anyhow::{anyhow, Context, Result};
use camino::{Utf8Component, Utf8Path, Utf8PathBuf};
use pathdiff::diff_utf8_paths;
pub fn path_relative_from(
path: impl AsRef<Utf8Path>,
base: impl AsRef<Utf8Path>,
) -> Result<Utf8PathBuf> {
fn inner(path: &Utf8Path, base: &Utf8Path) -> Result<Utf8PathBuf> {
let path = normalized_absolute_path(path)
.with_context(|| format!("converting path to normalized absolute path: {path}"))?;
let base = normalized_absolute_path(base)
.with_context(|| format!("converting base to normalized absolute path: {base}"))?;
diff_utf8_paths(&path, &base)
.ok_or_else(|| anyhow!("unable to compute relative path to {path} from {base}"))
}
inner(path.as_ref(), base.as_ref())
}
pub fn path_relative_from_current_dir(path: impl AsRef<Utf8Path>) -> Result<Utf8PathBuf> {
fn inner(path: &Utf8Path) -> Result<Utf8PathBuf> {
let current_dir = std::env::current_dir()?;
path_relative_from(path, Utf8PathBuf::try_from(current_dir)?)
}
inner(path.as_ref())
}
pub fn path_relative_from_file(
path: impl AsRef<Utf8Path>,
file: impl AsRef<Utf8Path>,
) -> Result<Utf8PathBuf> {
fn inner(path: &Utf8Path, file: &Utf8Path) -> Result<Utf8PathBuf> {
let base = file.parent().ok_or_else(|| {
anyhow!(
"The path to the file to be relative to does not appear to be the path to a file: {}",
file
)
})?;
path_relative_from(path, base)
}
inner(path.as_ref(), file.as_ref())
}
fn normalized_absolute_path(path: &Utf8Path) -> Result<Utf8PathBuf> {
if path.is_relative() {
let current_dir = std::env::current_dir()?;
normalize_path_impl(Utf8PathBuf::try_from(current_dir)?.join(path).components())
} else {
normalize_path_impl(path.components())
}
}
pub fn resolve_path_from_file(
path: impl AsRef<Utf8Path>,
resolve_from: impl AsRef<Utf8Path>,
) -> Result<Utf8PathBuf> {
fn inner(path: &Utf8Path, resolve_from: &Utf8Path) -> Result<Utf8PathBuf> {
let resolve_from_dir = resolve_from
.parent()
.with_context(|| format!("Not a path to a file: {resolve_from}"))?;
resolve_path(path, resolve_from_dir)
}
inner(path.as_ref(), resolve_from.as_ref())
}
pub fn resolve_path(
path: impl AsRef<Utf8Path>,
resolve_from: impl AsRef<Utf8Path>,
) -> Result<Utf8PathBuf> {
fn inner(path: &Utf8Path, resolve_from: &Utf8Path) -> Result<Utf8PathBuf> {
if path.is_absolute() {
Ok(path.to_owned())
} else {
normalize_path_impl(resolve_from.components().chain(path.components()))
.with_context(|| format!("resolving {} from {}", path, resolve_from))
}
}
inner(path.as_ref(), resolve_from.as_ref())
}
pub fn normalize_path(path: impl AsRef<Utf8Path>) -> Result<Utf8PathBuf> {
fn inner(path: &Utf8Path) -> Result<Utf8PathBuf> {
normalize_path_impl(path.components()).with_context(|| format!("Normalizing: {}", path))
}
inner(path.as_ref())
}
fn normalize_path_impl<'a>(
path_components: impl IntoIterator<Item = Utf8Component<'a>>,
) -> Result<Utf8PathBuf> {
let result =
path_components.into_iter().try_fold(Vec::new(), |mut components, component| {
match component {
value @ Utf8Component::Normal(_) => components.push(value),
Utf8Component::CurDir => {}
Utf8Component::ParentDir => {
let popped = components.pop();
match popped {
None => components.push(Utf8Component::ParentDir),
Some(Utf8Component::Normal(_)) => {}
Some(value @ Utf8Component::ParentDir) => {
components.push(value);
components.push(component);
}
Some(Utf8Component::RootDir) | Some(Utf8Component::Prefix(_)) => {
return Err(anyhow!("Attempted to get parent of path root"))
}
Some(Utf8Component::CurDir) => unreachable!(),
}
}
abs_root @ Utf8Component::RootDir | abs_root @ Utf8Component::Prefix(_) => {
if components.is_empty() {
components.push(abs_root);
} else {
return Err(anyhow!(
"Encountered a path root that wasn't in the root position"
));
}
}
}
Ok(components)
})?;
Ok(result.iter().collect())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolve_path_from_file_simple() {
let result = resolve_path_from_file("an/internal/path", "path/to/manifest.txt").unwrap();
assert_eq!(result, Utf8PathBuf::from("path/to/an/internal/path"))
}
#[test]
fn resolve_path_from_file_fails_root() {
let result = resolve_path_from_file("an/internal/path", "/");
assert!(result.is_err());
}
#[test]
fn resolve_path_simple() {
let result = resolve_path("an/internal/path", "path/to/manifest_dir").unwrap();
assert_eq!(result, Utf8PathBuf::from("path/to/manifest_dir/an/internal/path"))
}
#[test]
fn resolve_path_with_abs_manifest_path_stays_abs() {
let result = resolve_path("an/internal/path", "/path/to/manifest_dir").unwrap();
assert_eq!(result, Utf8PathBuf::from("/path/to/manifest_dir/an/internal/path"))
}
#[test]
fn resolve_path_removes_cur_dirs() {
let result = resolve_path("./an/./internal/path", "./path/to/./manifest_dir").unwrap();
assert_eq!(result, Utf8PathBuf::from("path/to/manifest_dir/an/internal/path"))
}
#[test]
fn resolve_path_with_simple_parent_dirs() {
let result = resolve_path("../../an/internal/path", "path/to/manifest_dir").unwrap();
assert_eq!(result, Utf8PathBuf::from("path/an/internal/path"))
}
#[test]
fn resolve_path_with_parent_dirs_past_manifest_start() {
let result = resolve_path("../../../../an/internal/path", "path/to/manifest_dir").unwrap();
assert_eq!(result, Utf8PathBuf::from("../an/internal/path"))
}
#[test]
fn resolve_path_with_abs_internal_path() {
let result = resolve_path("/an/absolute/path", "path/to/manifest_dir").unwrap();
assert_eq!(result, Utf8PathBuf::from("/an/absolute/path"))
}
#[test]
fn resolve_path_fails_with_parent_dirs_past_abs_manifest() {
let result = resolve_path("../../../../an/internal/path", "/path/to/manifest_dir");
assert!(result.is_err())
}
#[test]
fn test_relative_from_absolute_when_already_relative() {
let cwd = Utf8PathBuf::try_from(std::env::current_dir().unwrap()).unwrap();
let base = cwd.join("path/to/base/dir");
let path = "path/but/to/another/dir";
let relative_path = path_relative_from(path, base).unwrap();
assert_eq!(relative_path, Utf8PathBuf::from("../../../but/to/another/dir"));
}
#[test]
fn test_relative_from_absolute_when_absolute() {
let cwd = Utf8PathBuf::try_from(std::env::current_dir().unwrap()).unwrap();
let base = cwd.join("path/to/base/dir");
let path = cwd.join("path/but/to/another/dir");
let relative_path = path_relative_from(path, base).unwrap();
assert_eq!(relative_path, Utf8PathBuf::from("../../../but/to/another/dir"));
}
#[test]
fn test_relative_from_relative_when_absolute_and_different_from_root() {
let base = "../some/relative/path";
let path = "/an/absolute/path";
let cwd = Utf8PathBuf::try_from(std::env::current_dir().unwrap()).unwrap();
let expected_path = Utf8PathBuf::from_iter(
cwd.components()
.into_iter()
.filter_map(|comp| match comp {
Utf8Component::Normal(_) => Some(Utf8Component::ParentDir),
_ => None,
})
.skip(1),
)
.join("../../../")
.join("an/absolute/path");
let relative_path = path_relative_from(path, base).unwrap();
assert_eq!(relative_path, expected_path);
}
#[test]
fn test_relative_from_relative_when_absolute_and_shared_root_path() {
let cwd = Utf8PathBuf::try_from(std::env::current_dir().unwrap()).unwrap();
let base = "some/relative/path";
let path = cwd.join("foo/bar");
let relative_path = path_relative_from(path, base).unwrap();
assert_eq!(relative_path, Utf8PathBuf::from("../../../foo/bar"));
}
#[test]
fn test_relative_from_relative_when_relative() {
let base = "some/relative/path";
let path = "another/relative/path";
let relative_path = path_relative_from(path, base).unwrap();
assert_eq!(relative_path, Utf8PathBuf::from("../../../another/relative/path"));
}
#[test]
fn test_relative_from_when_base_has_parent_component() {
assert_eq!(
path_relative_from("foo/bar", "baz/different_thing").unwrap(),
Utf8PathBuf::from("../../foo/bar")
);
assert_eq!(
path_relative_from("foo/bar", "baz/thing/../different_thing").unwrap(),
Utf8PathBuf::from("../../foo/bar")
);
}
#[test]
fn test_relative_from_file_simple() {
let file = "some/path/to/file.txt";
let path = "some/path/to/data/file";
let relative_path = path_relative_from_file(path, file).unwrap();
assert_eq!(relative_path, Utf8PathBuf::from("data/file"));
}
#[test]
fn test_relative_from_file_when_file_not_a_file() {
let file = "/";
let path = "some/path/to/data/file";
let relative_path = path_relative_from_file(path, file);
assert!(relative_path.is_err());
}
#[test]
fn test_relative_from_current_dir() {
let cwd = Utf8PathBuf::try_from(std::env::current_dir().unwrap()).unwrap();
let base = "some/relative/path";
let path = cwd.join(base);
let relative_path = path_relative_from_current_dir(path).unwrap();
assert_eq!(relative_path, Utf8PathBuf::from(base));
}
}