use crate::policy::CompiledPolicy;
use orbok_core::{HiddenFilePolicy, OrbokError, OrbokResult, SourceId, SymlinkPolicy};
use orbok_db::repo::SourceRecord;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct GuardedSource {
pub source_id: SourceId,
pub canonical_root: PathBuf,
pub policy: CompiledPolicy,
}
impl GuardedSource {
pub fn from_record(record: &SourceRecord) -> Self {
Self {
source_id: record.source_id.clone(),
canonical_root: PathBuf::from(&record.canonical_path),
policy: CompiledPolicy::from_source(record),
}
}
}
#[derive(Debug, Clone)]
pub struct ValidatedPath {
pub source_id: SourceId,
pub canonical: PathBuf,
}
pub struct PathGuard {
sources: Vec<GuardedSource>,
}
impl PathGuard {
pub fn new(sources: Vec<GuardedSource>) -> Self {
Self { sources }
}
pub fn canonicalize(path: &Path) -> OrbokResult<PathBuf> {
std::fs::canonicalize(path)
.map_err(|e| OrbokError::PathCanonicalization(format!("{}: {e}", path.display())))
}
pub fn validate(&self, requested: &Path) -> OrbokResult<ValidatedPath> {
let canonical = Self::canonicalize(requested)?;
let source = self
.sources
.iter()
.find(|s| canonical.starts_with(&s.canonical_root))
.ok_or(OrbokError::PathOutsideSources)?;
if source.policy.symlink_policy == SymlinkPolicy::Ignore {
let requested_inside = requested.starts_with(&source.canonical_root);
if requested_inside && requested != canonical {
if is_symlinked_below(&source.canonical_root, requested)? {
return Err(OrbokError::PolicyBlocked("symlink_policy_blocked"));
}
}
}
if source.policy.hidden_file_policy == HiddenFilePolicy::Exclude
&& hidden_below_root(&source.canonical_root, &canonical)
{
return Err(OrbokError::PolicyBlocked("hidden_file_excluded"));
}
if let Ok(metadata) = std::fs::metadata(&canonical) {
if metadata.is_file() && !source.policy.size_allowed(metadata.len()) {
return Err(OrbokError::PolicyBlocked("file_too_large"));
}
}
Ok(ValidatedPath {
source_id: source.source_id.clone(),
canonical,
})
}
}
fn hidden_below_root(root: &Path, canonical: &Path) -> bool {
let Ok(relative) = canonical.strip_prefix(root) else {
return false;
};
relative.components().any(|c| {
c.as_os_str()
.to_string_lossy()
.starts_with('.')
})
}
fn is_symlinked_below(root: &Path, path: &Path) -> OrbokResult<bool> {
let Ok(relative) = path.strip_prefix(root) else {
return Ok(false);
};
let mut current = root.to_path_buf();
for component in relative.components() {
current.push(component);
let metadata = std::fs::symlink_metadata(¤t)?;
if metadata.file_type().is_symlink() {
return Ok(true);
}
}
Ok(false)
}