use std::path::{Component, Path, PathBuf};
use crate::error::PackError;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Contained(PathBuf);
impl Contained {
pub(crate) fn entry(rel: &Path) -> Result<Self, PackError> {
if plain_components(rel) {
Ok(Self(rel.to_path_buf()))
} else {
Err(PackError::EscapingArchivePath(rel.display().to_string()))
}
}
pub(crate) fn path(field: &str, raw: &str) -> Result<Self, PackError> {
let candidate = Path::new(raw);
if plain_components(candidate) {
Ok(Self(candidate.to_path_buf()))
} else {
Err(PackError::EscapingManifestPath {
field: field.to_string(),
value: raw.to_string(),
})
}
}
pub(crate) fn name(field: &str, raw: &str) -> Result<Self, PackError> {
let checked = Self::path(field, raw)?;
if checked.0.components().count() == 1 {
Ok(checked)
} else {
Err(PackError::EscapingManifestPath {
field: field.to_string(),
value: raw.to_string(),
})
}
}
pub(crate) fn join_onto(&self, base: &Path) -> PathBuf {
base.join(&self.0)
}
pub(crate) fn as_path(&self) -> &Path {
&self.0
}
}
fn plain_components(path: &Path) -> bool {
let mut any = false;
for component in path.components() {
if !matches!(component, Component::Normal(_)) {
return false;
}
any = true;
}
any
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_accepts_relative_paths() {
let checked = Contained::path("worktrees[].path", ".worktrees/feature").expect("contained");
assert_eq!(checked.as_path(), Path::new(".worktrees/feature"));
assert_eq!(
checked.join_onto(Path::new("/dest")),
PathBuf::from("/dest/.worktrees/feature")
);
}
#[test]
fn test_refuses_every_escape() {
for raw in ["..", "../sibling", "a/../../b", "/etc", "", ".", "./a"] {
assert!(
Contained::path("worktrees[].path", raw).is_err(),
"must refuse {raw:?}"
);
}
}
#[test]
fn test_accepts_interior_current_dir() {
let checked = Contained::path("worktrees[].path", "a/./b").expect("contained");
assert_eq!(
checked.join_onto(Path::new("/dest")),
PathBuf::from("/dest/a/./b"),
"which resolves to /dest/a/b — still inside"
);
}
#[test]
fn test_refuses_absolute_which_would_replace_the_base() {
let err =
Contained::path("worktrees[].path", "/opt/other-project").expect_err("must refuse");
assert!(
matches!(err, PackError::EscapingManifestPath { ref field, .. } if field == "worktrees[].path"),
"got {err:?}"
);
}
#[test]
fn test_name_takes_exactly_one_component() {
assert!(Contained::name("worktrees[].name", "feature").is_ok());
assert!(Contained::name("worktrees[].name", "nested/feature").is_err());
assert!(Contained::name("worktrees[].name", "../feature").is_err());
}
#[test]
fn test_entry_reports_itself_as_an_entry() {
let err = Contained::entry(Path::new("../evil")).expect_err("must refuse");
assert!(
matches!(err, PackError::EscapingArchivePath(_)),
"got {err:?}"
);
}
}