use std::path::{Component, Path, PathBuf};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GeneralizedPath {
pub raw: PathBuf,
pub rendered: String,
pub risk: RiskTag,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RiskTag {
Low,
Broad,
HomeScope,
TmpScope,
Redacted,
}
pub fn generalize_path(
raw: &Path,
cwd: &Path,
home: Option<&Path>,
assay_tmp: Option<&Path>,
) -> GeneralizedPath {
let raw_str = match raw.to_str() {
Some(s) => s,
None => return redacted(raw, "non-unicode"),
};
if raw_str.len() > 1024 {
return redacted(raw, "length > 1024");
}
let normalized = normalize_path(raw);
let norm_cwd = normalize_path(cwd);
let norm_home = home.map(normalize_path);
let norm_tmp = assay_tmp.map(normalize_path);
if let Some(tmp) = norm_tmp {
if let Ok(rel) = normalized.strip_prefix(&tmp) {
let s = format!("${{ASSAY_TMP}}/{}", rel.display());
return GeneralizedPath {
raw: raw.to_path_buf(),
rendered: s,
risk: RiskTag::TmpScope,
};
}
}
if let Ok(rel) = normalized.strip_prefix(&norm_cwd) {
let s = format!("./{}", rel.display());
return GeneralizedPath {
raw: raw.to_path_buf(),
rendered: s,
risk: RiskTag::Low,
};
}
if let Some(h) = norm_home {
if let Ok(rel) = normalized.strip_prefix(&h) {
let s = format!("~/{}", rel.display());
return GeneralizedPath {
raw: raw.to_path_buf(),
rendered: s,
risk: RiskTag::HomeScope,
};
}
}
GeneralizedPath {
raw: raw.to_path_buf(),
rendered: normalized.display().to_string(),
risk: RiskTag::Broad,
}
}
fn redacted(raw: &Path, reason: &str) -> GeneralizedPath {
GeneralizedPath {
raw: raw.to_path_buf(),
rendered: format!(
"${{REDACTED_PATH_TYPE_{}}}",
reason.to_uppercase().replace(" ", "_")
),
risk: RiskTag::Redacted,
}
}
fn normalize_path(path: &Path) -> PathBuf {
let mut out = PathBuf::new();
for comp in path.components() {
match comp {
Component::CurDir => {}
Component::ParentDir => {
if let Some(last) = out.components().next_back() {
if last != Component::RootDir {
out.pop();
}
}
}
c => out.push(c),
}
}
if out.as_os_str().is_empty() {
if path.is_absolute() {
} else {
out.push(".");
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generalize_scopes() {
let cwd = Path::new("/app");
let home = Path::new("/home/user");
let tmp = Path::new("/tmp/assay-123");
let p = generalize_path(Path::new("/app/src/main.rs"), cwd, Some(home), Some(tmp));
assert_eq!(p.rendered, "./src/main.rs");
assert_eq!(p.risk, RiskTag::Low);
let p = generalize_path(Path::new("/home/user/.config"), cwd, Some(home), Some(tmp));
assert_eq!(p.rendered, "~/.config");
let p = generalize_path(Path::new("/tmp/assay-123/sock"), cwd, Some(home), Some(tmp));
assert_eq!(p.rendered, "${ASSAY_TMP}/sock");
let p = generalize_path(Path::new("/usr/bin/ls"), cwd, Some(home), Some(tmp));
assert_eq!(p.rendered, "/usr/bin/ls");
}
#[test]
fn test_normalization() {
let cwd = Path::new("/app");
let p = generalize_path(Path::new("/app/./foo/../bar"), cwd, None, None);
assert_eq!(p.rendered, "./bar");
}
#[test]
fn test_path_traversal_protection() {
let cwd = Path::new("/app");
let p = generalize_path(Path::new("/app/../../etc/passwd"), cwd, None, None);
assert_eq!(p.rendered, "/etc/passwd");
let p = normalize_path(Path::new("foo/../../bar"));
assert_eq!(p, PathBuf::from("bar"));
let p = normalize_path(Path::new("../../../secret"));
assert_eq!(p, PathBuf::from("secret"));
}
}