1use crate::policy::CompiledPolicy;
12use orbok_core::{HiddenFilePolicy, OrbokError, OrbokResult, SourceId, SymlinkPolicy};
13use orbok_db::repo::SourceRecord;
14use std::path::{Path, PathBuf};
15
16#[derive(Debug, Clone)]
18pub struct GuardedSource {
19 pub source_id: SourceId,
20 pub canonical_root: PathBuf,
21 pub policy: CompiledPolicy,
22}
23
24impl GuardedSource {
25 pub fn from_record(record: &SourceRecord) -> Self {
26 Self {
27 source_id: record.source_id.clone(),
28 canonical_root: PathBuf::from(&record.canonical_path),
29 policy: CompiledPolicy::from_source(record),
30 }
31 }
32}
33
34#[derive(Debug, Clone)]
37pub struct ValidatedPath {
38 pub source_id: SourceId,
39 pub canonical: PathBuf,
40}
41
42pub struct PathGuard {
44 sources: Vec<GuardedSource>,
45}
46
47impl PathGuard {
48 pub fn new(sources: Vec<GuardedSource>) -> Self {
51 Self { sources }
52 }
53
54 pub fn canonicalize(path: &Path) -> OrbokResult<PathBuf> {
58 std::fs::canonicalize(path)
59 .map_err(|e| OrbokError::PathCanonicalization(format!("{}: {e}", path.display())))
60 }
61
62 pub fn validate(&self, requested: &Path) -> OrbokResult<ValidatedPath> {
66 let canonical = Self::canonicalize(requested)?;
67
68 let source = self
69 .sources
70 .iter()
71 .find(|s| canonical.starts_with(&s.canonical_root))
72 .ok_or(OrbokError::PathOutsideSources)?;
73
74 if source.policy.symlink_policy == SymlinkPolicy::Ignore {
77 let requested_inside = requested.starts_with(&source.canonical_root);
78 if requested_inside && requested != canonical {
79 if is_symlinked_below(&source.canonical_root, requested)? {
83 return Err(OrbokError::PolicyBlocked("symlink_policy_blocked"));
84 }
85 }
86 }
87
88 if source.policy.hidden_file_policy == HiddenFilePolicy::Exclude
90 && hidden_below_root(&source.canonical_root, &canonical)
91 {
92 return Err(OrbokError::PolicyBlocked("hidden_file_excluded"));
93 }
94
95 if let Ok(metadata) = std::fs::metadata(&canonical) {
97 if metadata.is_file() && !source.policy.size_allowed(metadata.len()) {
98 return Err(OrbokError::PolicyBlocked("file_too_large"));
99 }
100 }
101
102 Ok(ValidatedPath {
103 source_id: source.source_id.clone(),
104 canonical,
105 })
106 }
107}
108
109fn hidden_below_root(root: &Path, canonical: &Path) -> bool {
111 let Ok(relative) = canonical.strip_prefix(root) else {
112 return false;
113 };
114 relative.components().any(|c| {
115 c.as_os_str()
116 .to_string_lossy()
117 .starts_with('.')
118 })
119}
120
121fn is_symlinked_below(root: &Path, path: &Path) -> OrbokResult<bool> {
123 let Ok(relative) = path.strip_prefix(root) else {
124 return Ok(false);
125 };
126 let mut current = root.to_path_buf();
127 for component in relative.components() {
128 current.push(component);
129 let metadata = std::fs::symlink_metadata(¤t)?;
130 if metadata.file_type().is_symlink() {
131 return Ok(true);
132 }
133 }
134 Ok(false)
135}