use crate::{
AbsUtf8PathBuf, CurrentDirError, RelUtf8PathBuf, ResolvePathError,
paths::{PathClass, classify_path},
};
use camino::Utf8Path;
use std::fmt;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct PathAnchor {
directory: AbsUtf8PathBuf,
}
impl PathAnchor {
#[must_use]
pub fn new(directory: AbsUtf8PathBuf) -> Self {
Self { directory }
}
pub fn current_dir() -> Result<Self, CurrentDirError> {
AbsUtf8PathBuf::current_dir().map(Self::new)
}
pub fn logical_current_dir() -> Result<Self, CurrentDirError> {
AbsUtf8PathBuf::logical_current_dir().map(Self::new)
}
#[must_use]
pub fn directory(&self) -> &AbsUtf8PathBuf {
&self.directory
}
#[must_use]
pub fn resolve_relative(&self, relative: RelUtf8PathBuf) -> AnchoredPath {
AnchoredPath {
absolute: self.directory.join(&relative),
relative: Some(relative),
}
}
#[must_use]
pub fn resolve_absolute(&self, absolute: AbsUtf8PathBuf) -> AnchoredPath {
let relative = absolute.strip_prefix(&self.directory);
AnchoredPath { absolute, relative }
}
pub fn resolve_input<P: AsRef<Utf8Path>>(
&self,
path: P,
) -> Result<AnchoredPath, ResolvePathError> {
match classify_path(path.as_ref().to_owned()) {
PathClass::Relative(relative) => Ok(self.resolve_relative(relative)),
PathClass::Absolute(absolute) => Ok(self.resolve_absolute(absolute)),
PathClass::RootRelative(path) | PathClass::DriveRelative(path) => {
let absolute = AbsUtf8PathBuf::resolve_against_current_dir(&path)?;
Ok(self.resolve_absolute(absolute))
}
PathClass::Malformed(path, kind) => Err(ResolvePathError::new(path, kind.into())),
}
}
}
#[derive(Clone, Debug)]
pub struct AnchoredPath {
absolute: AbsUtf8PathBuf,
relative: Option<RelUtf8PathBuf>,
}
impl AnchoredPath {
#[must_use]
pub fn absolute(&self) -> &AbsUtf8PathBuf {
&self.absolute
}
#[must_use]
pub fn into_absolute(self) -> AbsUtf8PathBuf {
self.absolute
}
#[must_use]
pub fn relative(&self) -> Option<&RelUtf8PathBuf> {
self.relative.as_ref()
}
#[must_use]
pub fn display(&self) -> DisplayPath<'_> {
DisplayPath(
self.relative
.as_ref()
.map_or(self.absolute.as_path(), RelUtf8PathBuf::as_path),
)
}
}
#[derive(Clone, Copy, Debug)]
pub struct DisplayPath<'a>(&'a Utf8Path);
impl<'a> DisplayPath<'a> {
#[must_use]
pub fn as_str(&self) -> &'a str {
self.0.as_str()
}
}
impl fmt::Display for DisplayPath<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self.0, f)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
ResolvePathErrorKind,
test_helpers::{absolute, assert_resolve_error, relative},
};
use camino::Utf8PathBuf;
#[cfg(unix)]
const BASE: &str = "/repo";
#[cfg(windows)]
const BASE: &str = r"C:\repo";
#[track_caller]
fn base(directory: impl Into<Utf8PathBuf>) -> PathAnchor {
PathAnchor::new(absolute(directory))
}
#[cfg(windows)]
#[test]
fn verbatim_and_device_bases_preserve_relative_inputs() {
for root in [r"\\?\C:\repo", r"\\?\UNC\server\share\repo", r"\\.\C:\repo"] {
let input = relative("../file/");
let resolved = base(root).resolve_relative(input.clone());
assert_eq!(
resolved.display().to_string(),
input.to_string(),
"{root:?}"
);
assert_eq!(resolved.relative(), Some(&input), "{root:?}");
}
}
#[cfg(windows)]
#[test]
fn verbatim_anchor_displays_absolute_inputs_relative() {
let anchor = base(r"\\?\C:\repo");
for (path, expected) in [
(r"\\?\C:\repo\src\lib.rs", r"src\lib.rs"),
(r"\\?\C:\repo\a/b", r"\\?\C:\repo\a/b"),
] {
let resolved = anchor.resolve_absolute(absolute(path));
assert_eq!(resolved.display().to_string(), expected, "{path:?}");
}
}
#[test]
fn resolve_input_rejects_invalid_input() {
let base = base(BASE);
let mut cases = vec![
(String::new(), ResolvePathErrorKind::Empty),
("a\0b".to_owned(), ResolvePathErrorKind::ContainsNul),
(format!("{BASE}/a\0b"), ResolvePathErrorKind::ContainsNul),
];
if cfg!(windows) {
cases.push(("C:a\0b".to_owned(), ResolvePathErrorKind::ContainsNul));
cases.push(("\\a\0b".to_owned(), ResolvePathErrorKind::ContainsNul));
}
for (input, expected) in cases {
assert_resolve_error(base.resolve_input(&input), &input, expected);
}
}
#[cfg(unix)]
#[test]
fn parent_component_after_symlink_is_not_collapsed() {
use std::{fs, os::unix::fs::symlink};
let temp = camino_tempfile::tempdir().expect("created temp dir");
let workspace = temp.path().join("workspace");
let target = temp.path().join("target");
fs::create_dir(&workspace).expect("created workspace");
fs::create_dir_all(target.join("child")).expect("created target/child");
symlink(target.join("child"), workspace.join("link")).expect("created workspace/link");
fs::write(workspace.join("config.toml"), "in workspace").expect("wrote workspace config");
fs::write(target.join("config.toml"), "in target").expect("wrote target config");
let resolved = base(&workspace).resolve_relative(relative("link/../config.toml"));
assert_eq!(
fs::read_to_string(resolved.absolute()).expect("read resolved config"),
"in target",
);
}
}