safe_chains/pathctx/
folder.rs1use std::cell::{Cell, RefCell};
17
18use super::anchor::{self, Anchor, FolderLevel, Use};
19use crate::parse::Token;
20use crate::verdict::{SafetyLevel, Verdict};
21
22#[derive(Clone, Debug, PartialEq, Eq)]
24pub enum Note {
25 Path { path: String, anchor: Anchor, use_: Use, placed: bool },
26 Leaf { command: String, anchor: Option<Anchor>, admitted: bool },
27}
28
29#[derive(Default)]
30struct Frame {
31 named_write: bool,
32 nested_accounted: bool,
33 declared: Option<Anchor>,
34}
35
36thread_local! {
37 static LEVEL: Cell<Option<FolderLevel>> = const { Cell::new(None) };
38 static FRAMES: RefCell<Vec<Frame>> = const { RefCell::new(Vec::new()) };
39 static NOTES: RefCell<Vec<Note>> = const { RefCell::new(Vec::new()) };
40}
41
42#[must_use]
44pub fn enter(level: FolderLevel) -> Guard {
45 NOTES.with(|n| n.borrow_mut().clear());
46 Guard(LEVEL.with(|l| l.replace(Some(level))))
47}
48
49pub struct Guard(Option<FolderLevel>);
50
51impl Drop for Guard {
52 fn drop(&mut self) {
53 LEVEL.with(|l| l.set(self.0));
54 }
55}
56
57pub fn level() -> Option<FolderLevel> {
59 LEVEL.with(Cell::get)
60}
61
62pub fn judges_writes() -> bool {
64 level().is_some_and(|l| l > FolderLevel::Reads)
65}
66
67pub fn notes() -> Vec<Note> {
69 NOTES.with(|n| n.borrow().clone())
70}
71
72pub fn is_unknown(cwd: &str) -> bool {
74 let base = crate::targets::UNKNOWN_WORKDIR;
75 cwd.strip_prefix(base).is_some_and(|rest| rest.is_empty() || rest.starts_with('/'))
76}
77
78pub(super) fn place(cwd: &str, path: &str, stated: Option<Use>) -> Option<String> {
85 let use_ = stated.unwrap_or(Use::Read);
86 let level = level()?;
87 if path.starts_with('/') || path.starts_with('~') || !is_unknown(cwd) {
88 return None;
89 }
90 let below = cwd[crate::targets::UNKNOWN_WORKDIR.len()..].trim_start_matches('/');
91 let relative = if below.is_empty() { path.to_string() } else { format!("{below}/{path}") };
92 let anchor = anchor::of_path(&relative);
93 let placed = anchor::placement(&relative, use_, level)
94 .filter(|p| !use_.mutates() || ((p != "." || declares_writes_in_its_folder()) && !items_in_scope()));
95 if level > FolderLevel::Reads && stated.is_some_and(Use::mutates) {
96 record(Note::Path { path: relative.clone(), anchor, use_, placed: placed.is_some() });
97 }
98 match placed {
99 None if anchor == Anchor::RelativeUnplaced || use_ == Use::Read => Some(NOWHERE.to_string()),
100 other => other,
101 }
102}
103
104fn declares_writes_in_its_folder() -> bool {
108 FRAMES.with(|f| {
109 f.borrow()
110 .last()
111 .and_then(|t| t.declared)
112 .is_some_and(|a| matches!(a, Anchor::ImplicitSource | Anchor::ImplicitOutput))
113 })
114}
115
116fn items_in_scope() -> bool {
120 super::stdin_item_repr().is_some() || super::LOOP_VARS.with(|v| !v.borrow().is_empty())
121}
122
123pub(super) fn note_named_write() {
125 FRAMES.with(|f| {
126 if let Some(top) = f.borrow_mut().last_mut() {
127 top.named_write = true;
128 }
129 });
130}
131
132pub(crate) fn judging(with_env: bool, classify: fn(&[Token]) -> Verdict) -> impl Fn(&[Token]) -> Verdict {
134 move |tokens| judge_leaf(tokens, with_env, || classify(tokens))
135}
136
137pub(crate) fn judge_leaf(tokens: &[Token], with_env: bool, classify: impl FnOnce() -> Verdict) -> Verdict {
140 let Some(level) = level().filter(|l| *l > FolderLevel::Reads) else {
141 return classify();
142 };
143 let here_unknown = super::cwd().is_some_and(|c| is_unknown(&c));
144 let declared = crate::registry::cwd_writes(tokens);
145 let frame = FrameGuard::push(declared);
146 let verdict = classify();
147 let frame = frame.pop();
148 let writes = matches!(verdict, Verdict::Allowed(l) if l > SafetyLevel::SafeRead);
149 if !writes {
150 return verdict;
151 }
152 if !here_unknown {
153 mark_parent_accounted();
154 return verdict;
155 }
156 let names_its_writes = declared == Some(Anchor::NamesItsWrites) && frame.named_write;
157 let implicit = !with_env && declared.is_some_and(|a| level.admits_implicit(a));
160 let accounted = implicit || names_its_writes || frame.nested_accounted;
161 let command = tokens.iter().take(4).map(Token::as_str).collect::<Vec<_>>().join(" ");
162 record(Note::Leaf { command, anchor: declared, admitted: accounted });
163 if accounted {
164 mark_parent_accounted();
165 verdict
166 } else {
167 Verdict::Denied
168 }
169}
170
171fn mark_parent_accounted() {
172 FRAMES.with(|f| {
173 if let Some(top) = f.borrow_mut().last_mut() {
174 top.nested_accounted = true;
175 }
176 });
177}
178
179fn record(note: Note) {
180 NOTES.with(|n| {
181 let mut notes = n.borrow_mut();
182 if notes.len() < MAX_NOTES && !notes.contains(¬e) {
183 notes.push(note);
184 }
185 });
186}
187
188const NOWHERE: &str = "__SAFE_CHAINS_CMDSUB__/outside-an-unknown-folder";
191
192const MAX_NOTES: usize = 64;
194
195struct FrameGuard(bool);
198
199impl FrameGuard {
200 fn push(declared: Option<Anchor>) -> FrameGuard {
201 FRAMES.with(|f| f.borrow_mut().push(Frame { declared, ..Frame::default() }));
202 FrameGuard(true)
203 }
204
205 fn pop(mut self) -> Frame {
206 self.0 = false;
207 FRAMES.with(|f| f.borrow_mut().pop()).unwrap_or_default()
208 }
209}
210
211impl Drop for FrameGuard {
212 fn drop(&mut self) {
213 if self.0 {
214 FRAMES.with(|f| f.borrow_mut().pop());
215 }
216 }
217}
218
219#[cfg(test)]
220#[path = "folder_tests.rs"]
221mod tests;
222
223#[cfg(test)]
224#[path = "folder_soundness_tests.rs"]
225mod soundness;