use std::ffi::OsStr;
use std::path::{Component, Path, PathBuf};
use nutype::nutype;
use snafu::Snafu;
#[derive(Debug, Clone, PartialEq, Eq, Snafu)]
pub enum WorkspaceRootPathError {
#[snafu(display("workspace root path is not absolute"))]
NotAbsolute,
#[snafu(display("workspace root path contains a `..` component"))]
ContainsParentDir,
#[snafu(display("workspace root path is not lexically clean"))]
NotLexicallyClean,
}
#[allow(clippy::ptr_arg)]
fn validate_workspace_root_path(p: &PathBuf) -> Result<(), WorkspaceRootPathError> {
if !p.is_absolute() {
return Err(WorkspaceRootPathError::NotAbsolute);
}
for c in p.components() {
if matches!(c, Component::ParentDir) {
return Err(WorkspaceRootPathError::ContainsParentDir);
}
}
if !is_lexically_clean(p) {
return Err(WorkspaceRootPathError::NotLexicallyClean);
}
Ok(())
}
fn is_lexically_clean(p: &Path) -> bool {
let mut rebuilt = PathBuf::new();
for c in p.components() {
rebuilt.push(c.as_os_str());
}
let lhs: &OsStr = p.as_os_str();
let rhs: &OsStr = rebuilt.as_os_str();
lhs == rhs
}
#[nutype(
validate(with = validate_workspace_root_path, error = WorkspaceRootPathError),
derive(Debug, Clone, PartialEq, Eq, Hash, AsRef),
)]
pub struct WorkspaceRootPath(PathBuf);
impl WorkspaceRootPath {
#[must_use]
pub fn as_path(&self) -> &Path {
self.as_ref().as_path()
}
}
impl std::fmt::Display for WorkspaceRootPath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(&self.as_path().display(), f)
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use super::{WorkspaceRootPath, WorkspaceRootPathError};
#[cfg(unix)]
#[test]
fn accepts_absolute_clean_path() {
let r = WorkspaceRootPath::try_new(PathBuf::from("/srv/work")).unwrap();
assert_eq!(r.as_path(), std::path::Path::new("/srv/work"));
}
#[cfg(unix)]
#[test]
fn accepts_filesystem_root() {
let r = WorkspaceRootPath::try_new(PathBuf::from("/")).unwrap();
assert_eq!(r.as_path(), std::path::Path::new("/"));
}
#[test]
fn rejects_relative_path() {
let err = WorkspaceRootPath::try_new(PathBuf::from("relative/path")).unwrap_err();
assert!(matches!(err, WorkspaceRootPathError::NotAbsolute));
}
#[test]
fn rejects_empty_path() {
let err = WorkspaceRootPath::try_new(PathBuf::from("")).unwrap_err();
assert!(matches!(err, WorkspaceRootPathError::NotAbsolute));
}
#[cfg(unix)]
#[test]
fn rejects_path_with_parent_dir_component() {
let err = WorkspaceRootPath::try_new(PathBuf::from("/srv/../work")).unwrap_err();
assert!(matches!(err, WorkspaceRootPathError::ContainsParentDir));
}
#[cfg(unix)]
#[test]
fn rejects_mid_path_current_dir() {
let err = WorkspaceRootPath::try_new(PathBuf::from("/srv/./work")).unwrap_err();
assert!(matches!(err, WorkspaceRootPathError::NotLexicallyClean));
}
#[cfg(unix)]
#[test]
fn rejects_double_separator() {
let err = WorkspaceRootPath::try_new(PathBuf::from("/srv//work")).unwrap_err();
assert!(matches!(err, WorkspaceRootPathError::NotLexicallyClean));
}
#[cfg(unix)]
#[test]
fn rejects_trailing_separator() {
let err = WorkspaceRootPath::try_new(PathBuf::from("/srv/work/")).unwrap_err();
assert!(matches!(err, WorkspaceRootPathError::NotLexicallyClean));
}
#[cfg(unix)]
#[test]
fn rejects_trailing_dot() {
let err = WorkspaceRootPath::try_new(PathBuf::from("/srv/work/.")).unwrap_err();
assert!(matches!(err, WorkspaceRootPathError::NotLexicallyClean));
}
#[cfg(unix)]
#[test]
fn display_renders_as_path_text() {
let r = WorkspaceRootPath::try_new(PathBuf::from("/srv/work")).unwrap();
assert_eq!(r.to_string(), "/srv/work");
}
#[cfg(unix)]
#[test]
fn equality_on_inner_path_bytes() {
let a = WorkspaceRootPath::try_new(PathBuf::from("/srv/work")).unwrap();
let b = WorkspaceRootPath::try_new(PathBuf::from("/srv/work")).unwrap();
let c = WorkspaceRootPath::try_new(PathBuf::from("/srv/other")).unwrap();
assert_eq!(a, b);
assert_ne!(a, c);
}
}