use std::fs;
use std::path::{Path, PathBuf};
use anyhow::Result;
use path_clean::PathClean;
use crate::err::Error;
pub(crate) fn check_is_path_allowed(path: &Path, allowed_paths: &[PathBuf]) -> Result<PathBuf> {
if allowed_paths.is_empty() {
return Err(Error::FileAccessDenied(path.to_string_lossy().to_string()).into());
}
let canonical_path = fs::canonicalize(path)?;
if allowed_paths.iter().any(|allowed| {
#[cfg(windows)]
{
const WINDOWS_CANONICAL_PREFIX: &str = "//?/";
let mut canonical_str =
canonical_path.to_string_lossy().to_lowercase().replace("\\", "/");
let allowed_str = allowed.to_string_lossy().to_lowercase().replace("\\", "/");
if canonical_str.starts_with(WINDOWS_CANONICAL_PREFIX) {
canonical_str = canonical_str
.strip_prefix(WINDOWS_CANONICAL_PREFIX)
.unwrap_or(&canonical_str)
.to_string();
}
canonical_str.starts_with(&allowed_str)
}
#[cfg(not(windows))]
{
canonical_path.starts_with(allowed)
}
}) {
Ok(canonical_path)
} else {
Err(Error::FileAccessDenied(path.to_string_lossy().to_string()).into())
}
}
pub(crate) fn extract_allowed_paths(
input: &str,
canonicalize: bool,
subject: &str,
) -> Vec<PathBuf> {
let delimiter = if cfg!(target_os = "windows") {
";"
} else {
":"
};
input
.split(delimiter)
.filter_map(|s| {
let trimmed = s.trim();
if trimmed.is_empty() {
None
} else {
let path = PathBuf::from(trimmed).clean();
let path = if canonicalize {
let Ok(path) = fs::canonicalize(&path) else {
warn!("Failed to canonicalize {subject} path: {}", path.to_string_lossy());
return None;
};
path
} else {
path
};
debug!("Allowed {subject} path: {}", path.to_string_lossy());
Some(path)
}
})
.collect()
}
#[cfg(test)]
mod tests {
use tempfile::tempdir;
use super::*;
#[test]
fn test_empty_allow_list_denies_access() {
let dir = tempdir().expect("failed to create temp dir");
let file_path = dir.path().join("test.txt");
fs::write(&file_path, "content").expect("failed to write file in file_path");
let result = check_is_path_allowed(&file_path, &[]);
assert!(result.is_err(), "File access must be denied when no allowlist is configured");
}
#[test]
fn test_allow_list_access() {
let delimiter = if cfg!(target_os = "windows") {
";"
} else {
":"
};
let (dir1, dir2, dir3) = (tempdir().unwrap(), tempdir().unwrap(), tempdir().unwrap());
let combined = format!(
"{}{}{}",
dir1.path().to_string_lossy(),
delimiter,
dir2.path().to_string_lossy()
);
let allowlist = extract_allowed_paths(&combined, true, "file");
let allowed_file1 = dir1.path().join("file1.txt");
fs::write(&allowed_file1, "content").expect("failed to write file in allowed_file1");
let allowed_file2 = dir2.path().join("file2.txt");
fs::write(&allowed_file2, "content").expect("failed to write file in allowed_file2");
let denied_file3 = dir3.path().join("file3.txt");
fs::write(&denied_file3, "content").expect("failed to write file in denied_file3");
let res1 = check_is_path_allowed(&allowed_file1, &allowlist);
let res2 = check_is_path_allowed(&allowed_file2, &allowlist);
assert!(res1.is_ok(), "File in the first allowed directory should be permitted");
assert!(res2.is_ok(), "File in the second allowed directory should be permitted");
let res_outside = check_is_path_allowed(&denied_file3, &allowlist);
assert!(res_outside.is_err(), "File outside allowed directories should be denied");
}
}