use std::cell::{Cell, RefCell};
use super::anchor::{self, Anchor, FolderLevel, Use};
use crate::parse::Token;
use crate::verdict::{SafetyLevel, Verdict};
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Note {
Path { path: String, anchor: Anchor, use_: Use, placed: bool },
Leaf { command: String, anchor: Option<Anchor>, admitted: bool },
}
#[derive(Default)]
struct Frame {
named_write: bool,
nested_accounted: bool,
declared: Option<Anchor>,
}
thread_local! {
static LEVEL: Cell<Option<FolderLevel>> = const { Cell::new(None) };
static FRAMES: RefCell<Vec<Frame>> = const { RefCell::new(Vec::new()) };
static NOTES: RefCell<Vec<Note>> = const { RefCell::new(Vec::new()) };
}
#[must_use]
pub fn enter(level: FolderLevel) -> Guard {
NOTES.with(|n| n.borrow_mut().clear());
Guard(LEVEL.with(|l| l.replace(Some(level))))
}
pub struct Guard(Option<FolderLevel>);
impl Drop for Guard {
fn drop(&mut self) {
LEVEL.with(|l| l.set(self.0));
}
}
pub fn level() -> Option<FolderLevel> {
LEVEL.with(Cell::get)
}
pub fn judges_writes() -> bool {
level().is_some_and(|l| l > FolderLevel::Reads)
}
pub fn notes() -> Vec<Note> {
NOTES.with(|n| n.borrow().clone())
}
pub fn is_unknown(cwd: &str) -> bool {
let base = crate::targets::UNKNOWN_WORKDIR;
cwd.strip_prefix(base).is_some_and(|rest| rest.is_empty() || rest.starts_with('/'))
}
pub(super) fn place(cwd: &str, path: &str, stated: Option<Use>) -> Option<String> {
let use_ = stated.unwrap_or(Use::Read);
let level = level()?;
if path.starts_with('/') || path.starts_with('~') || !is_unknown(cwd) {
return None;
}
let below = cwd[crate::targets::UNKNOWN_WORKDIR.len()..].trim_start_matches('/');
let relative = if below.is_empty() { path.to_string() } else { format!("{below}/{path}") };
let anchor = anchor::of_path(&relative);
let placed = anchor::placement(&relative, use_, level)
.filter(|p| !use_.mutates() || ((p != "." || declares_writes_in_its_folder()) && !items_in_scope()));
if level > FolderLevel::Reads && stated.is_some_and(Use::mutates) {
record(Note::Path { path: relative.clone(), anchor, use_, placed: placed.is_some() });
}
match placed {
None if anchor == Anchor::RelativeUnplaced || use_ == Use::Read => Some(NOWHERE.to_string()),
other => other,
}
}
fn declares_writes_in_its_folder() -> bool {
FRAMES.with(|f| {
f.borrow()
.last()
.and_then(|t| t.declared)
.is_some_and(|a| matches!(a, Anchor::ImplicitSource | Anchor::ImplicitOutput))
})
}
fn items_in_scope() -> bool {
super::stdin_item_repr().is_some() || super::LOOP_VARS.with(|v| !v.borrow().is_empty())
}
pub(super) fn note_named_write() {
FRAMES.with(|f| {
if let Some(top) = f.borrow_mut().last_mut() {
top.named_write = true;
}
});
}
pub(crate) fn judging(with_env: bool, classify: fn(&[Token]) -> Verdict) -> impl Fn(&[Token]) -> Verdict {
move |tokens| judge_leaf(tokens, with_env, || classify(tokens))
}
pub(crate) fn judge_leaf(tokens: &[Token], with_env: bool, classify: impl FnOnce() -> Verdict) -> Verdict {
let Some(level) = level().filter(|l| *l > FolderLevel::Reads) else {
return classify();
};
let here_unknown = super::cwd().is_some_and(|c| is_unknown(&c));
let declared = crate::registry::cwd_writes(tokens);
let frame = FrameGuard::push(declared);
let verdict = classify();
let frame = frame.pop();
let writes = matches!(verdict, Verdict::Allowed(l) if l > SafetyLevel::SafeRead);
if !writes {
return verdict;
}
if !here_unknown {
mark_parent_accounted();
return verdict;
}
let names_its_writes = declared == Some(Anchor::NamesItsWrites) && frame.named_write;
let implicit = !with_env && declared.is_some_and(|a| level.admits_implicit(a));
let accounted = implicit || names_its_writes || frame.nested_accounted;
let command = tokens.iter().take(4).map(Token::as_str).collect::<Vec<_>>().join(" ");
record(Note::Leaf { command, anchor: declared, admitted: accounted });
if accounted {
mark_parent_accounted();
verdict
} else {
Verdict::Denied
}
}
fn mark_parent_accounted() {
FRAMES.with(|f| {
if let Some(top) = f.borrow_mut().last_mut() {
top.nested_accounted = true;
}
});
}
fn record(note: Note) {
NOTES.with(|n| {
let mut notes = n.borrow_mut();
if notes.len() < MAX_NOTES && !notes.contains(¬e) {
notes.push(note);
}
});
}
const NOWHERE: &str = "__SAFE_CHAINS_CMDSUB__/outside-an-unknown-folder";
const MAX_NOTES: usize = 64;
struct FrameGuard(bool);
impl FrameGuard {
fn push(declared: Option<Anchor>) -> FrameGuard {
FRAMES.with(|f| f.borrow_mut().push(Frame { declared, ..Frame::default() }));
FrameGuard(true)
}
fn pop(mut self) -> Frame {
self.0 = false;
FRAMES.with(|f| f.borrow_mut().pop()).unwrap_or_default()
}
}
impl Drop for FrameGuard {
fn drop(&mut self) {
if self.0 {
FRAMES.with(|f| f.borrow_mut().pop());
}
}
}
#[cfg(test)]
#[path = "folder_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "folder_soundness_tests.rs"]
mod soundness;