use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct PackagePath(String);
impl PackagePath {
pub(crate) fn new(path: impl Into<String>) -> Self {
Self(path.into())
}
pub(crate) fn child(&self, name: &str) -> Self {
Self(format!("{}/{}", self.0, name))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn to_native(&self, root: &std::path::Path) -> std::path::PathBuf {
let mut out = root.to_path_buf();
for segment in self.0.split('/') {
out.push(segment);
}
out
}
}
impl fmt::Display for PackagePath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl AsRef<str> for PackagePath {
fn as_ref(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct RelativePath(String);
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RelativePathError {
MissingDotSlash,
EscapesRoot,
}
impl fmt::Display for RelativePathError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::MissingDotSlash => f.write_str("plugin-relative paths must begin with `./`"),
Self::EscapesRoot => f.write_str("path resolves outside the plugin root"),
}
}
}
impl std::error::Error for RelativePathError {}
impl RelativePath {
pub fn parse(raw: &str) -> Result<Self, RelativePathError> {
let Some(rest) = raw.strip_prefix("./") else {
return Err(RelativePathError::MissingDotSlash);
};
if !lexically_contained(rest) {
return Err(RelativePathError::EscapesRoot);
}
Ok(Self(raw.to_owned()))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn resolve(&self, plugin_root: &std::path::Path) -> std::path::PathBuf {
let mut out = plugin_root.to_path_buf();
for segment in self.0[2..].split('/').filter(|s| !s.is_empty() && *s != ".") {
if segment == ".." {
out.pop();
} else {
out.push(segment);
}
}
out
}
}
impl fmt::Display for RelativePath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl std::str::FromStr for RelativePath {
type Err = RelativePathError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
pub(crate) fn lexically_contained(path: &str) -> bool {
let mut depth: i32 = 0;
for segment in path.split('/') {
match segment {
"" | "." => {}
".." => {
depth -= 1;
if depth < 0 {
return false;
}
}
_ => depth += 1,
}
}
true
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn relative_paths_require_dot_slash() {
assert_eq!(RelativePath::parse("bin/server"), Err(RelativePathError::MissingDotSlash));
assert_eq!(RelativePath::parse("../bin/server"), Err(RelativePathError::MissingDotSlash));
assert!(RelativePath::parse("./bin/server").is_ok());
}
#[test]
fn relative_paths_must_stay_inside() {
assert!(RelativePath::parse("./a/../b").is_ok());
assert_eq!(RelativePath::parse("./a/../../b"), Err(RelativePathError::EscapesRoot));
assert_eq!(RelativePath::parse("./.."), Err(RelativePathError::EscapesRoot));
}
#[test]
fn resolve_joins_lexically() {
let p = RelativePath::parse("./data/./cache").unwrap();
assert_eq!(
p.resolve(std::path::Path::new("/root")),
std::path::PathBuf::from("/root/data/cache")
);
}
}