use crate::error::SoftPathError;
use crate::Config;
use std::collections::HashSet;
use std::fs;
use std::path::Path;
use std::sync::OnceLock;
static CONFIG: OnceLock<Config> = OnceLock::new();
pub(crate) fn get_config() -> &'static Config {
CONFIG.get_or_init(Config::default)
}
pub fn set_config(config: Config) {
let _ = CONFIG.set(config);
}
pub(crate) fn check_path_traversal(path: &Path) -> Result<(), SoftPathError> {
use std::path::Component;
let config = get_config();
if path.components().count() > config.max_path_depth {
return Err(SoftPathError::PathDepthExceeded(config.max_path_depth));
}
let absolute = if path.is_absolute() {
path.to_path_buf()
} else {
std::env::current_dir()?.join(path)
};
let mut depth = 0;
for component in absolute.components() {
match component {
Component::ParentDir => depth -= 1,
Component::Normal(_) => depth += 1,
Component::RootDir => depth = 0,
_ => {}
}
if depth < 0 {
return Err(SoftPathError::PathTraversal(
sanitize_path_for_error(&absolute),
));
}
}
Ok(())
}
pub(crate) fn check_symlink_cycles(path: &Path) -> Result<(), SoftPathError> {
let config = get_config();
let mut followed = 0;
let mut current = path.to_path_buf();
let mut visited = HashSet::new();
while current.is_symlink() {
if !visited.insert(current.clone()) {
return Err(SoftPathError::SymlinkCycleDetected(
sanitize_path_for_error(¤t).into(),
));
}
if followed >= config.max_symlink_depth {
return Err(SoftPathError::SymlinkDepthExceeded(
config.max_symlink_depth,
));
}
followed += 1;
current = fs::read_link(¤t)?;
}
Ok(())
}
fn sanitize_path_for_error(path: &Path) -> String {
#[cfg(debug_assertions)]
{
path.to_string_lossy().into()
}
#[cfg(not(debug_assertions))]
{
path.file_name()
.map(|name| format!("<path>/{}", name.to_string_lossy()))
.unwrap_or_else(|| "<path>".to_string())
}
}