mod declared;
mod glob;
mod items;
mod walk;
pub(crate) use declared::declared_names;
pub(crate) use items::{enter_loop, enter_read_var, enter_stdin, prints_only_paths, stage_shape};
use walk::shape;
use super::{SimpleCmd, WordPart};
use crate::parse::Token;
use crate::pathctx::Facts;
use crate::verdict::Verdict;
pub(crate) const FLAG_PROBE: &str = "--safe-chains-opaque";
pub(crate) const SHORT_PROBE: &str = "-%";
const NUMBER_PROBE: &str = "-1234567890";
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) enum Lead {
#[default]
No,
Number,
Any,
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum Parts {}
pub(crate) const SPLIT: Facts<Parts> = Facts::bit(0);
pub(crate) const VANISH: Facts<Parts> = Facts::bit(1);
pub(crate) const OPAQUE: Facts<Parts> = Facts::bit(2);
pub(crate) const GLOB: Facts<Parts> = Facts::bit(3);
pub(crate) const BLANK: Facts<Parts> = Facts::bit(4);
#[derive(Debug, Default)]
pub(crate) struct Shape {
pub(crate) lead: Lead,
pub(crate) facts: Facts<Parts>,
pub(crate) concrete: Option<Vec<String>>,
}
#[derive(Clone, Copy)]
enum Probe {
Lead(&'static str),
Split(&'static str),
Vanish,
Concrete,
}
impl Shape {
fn wants(&self, probe: Probe) -> bool {
match probe {
Probe::Lead(_) => self.lead != Lead::No,
Probe::Split(_) => self.facts.has(SPLIT),
Probe::Vanish => self.facts.has(VANISH),
Probe::Concrete => self.concrete.is_some(),
}
}
fn tokens(&self, probe: Probe, default: &[Token]) -> Vec<Token> {
let fallback = || match &self.concrete {
Some(words) => words.iter().cloned().map(Token::from_raw).collect(),
None => default.to_vec(),
};
match probe {
Probe::Lead(flag) => match self.lead {
Lead::Any => vec![Token::from_raw(flag.to_string())],
Lead::Number => vec![Token::from_raw(NUMBER_PROBE.to_string())],
Lead::No => fallback(),
},
Probe::Split(flag) if self.wants(probe) => default.iter().cloned().chain([Token::from_raw(flag.to_string())]).collect(),
Probe::Vanish if self.wants(probe) => Vec::new(),
_ => fallback(),
}
}
}
pub(crate) fn probed_verdict(cmd: &SimpleCmd, words: &[Vec<Token>], classify: impl Fn(&[Token]) -> Verdict) -> Verdict {
let mut verdict = classify(&words.concat());
if !verdict.is_allowed() {
return verdict;
}
let shapes: Vec<Option<Shape>> = cmd
.words
.iter()
.zip(words)
.enumerate()
.map(|(i, (w, toks))| (i > 0 && !declares(cmd, w)).then(|| shape(w, toks.len())))
.collect();
let probes = [FLAG_PROBE, SHORT_PROBE]
.map(Probe::Lead)
.into_iter()
.chain([FLAG_PROBE, SHORT_PROBE].map(Probe::Split));
for probe in probes.chain([Probe::Vanish, Probe::Concrete]) {
if !shapes.iter().flatten().any(|s| s.wants(probe)) {
continue;
}
let tokens: Vec<Token> = words
.iter()
.zip(&shapes)
.flat_map(|(toks, s)| s.as_ref().map_or_else(|| toks.clone(), |s| s.tokens(probe, toks)))
.collect();
if !super::check::charge_classify_work(u32::try_from(tokens.len()).unwrap_or(u32::MAX)) {
return Verdict::Denied;
}
verdict = verdict.combine(classify(&tokens));
if !verdict.is_allowed() {
return verdict;
}
}
verdict
}
pub(crate) fn probe_is_value(tokens: &[Token], idx: usize, valued_flags: &[String]) -> bool {
let valued = |f: &str| valued_flags.iter().any(|a| a == f);
let (Some(t), Some(prev)) = (tokens.get(idx), idx.checked_sub(1).and_then(|p| tokens.get(p))) else {
return false;
};
let prev = prev.as_str();
if !is_flag_probe(t.as_str()) || !prev.starts_with('-') || prev.contains('=') {
return false;
}
valued(prev) || (!prev.starts_with("--") && prev.len() > 2 && prev.chars().last().is_some_and(|c| valued(&format!("-{c}"))))
}
pub(crate) fn is_flag_probe(s: &str) -> bool {
s == FLAG_PROBE || s == SHORT_PROBE
}
pub(super) fn smuggles_a_flag(cmd: &SimpleCmd) -> bool {
cmd.words.iter().skip(1).any(|w| {
w.0.iter().any(|part| {
let WordPart::Lit(raw) = part else { return false };
if !raw.contains('$') {
return false;
}
let expanded = crate::pathctx::expand_vars(raw, false);
expanded.split([' ', '\t', '\n']).skip(1).any(|piece| piece.starts_with('-'))
|| (expanded.split([' ', '\t', '\n']).count() > 1 && expanded.starts_with('-'))
})
})
}
fn declares(cmd: &SimpleCmd, word: &super::Word) -> bool {
let builtin = cmd.words.first().map(super::Word::eval);
let Some(WordPart::Lit(head)) = word.0.first() else { return false };
let name = head.split('=').next().unwrap_or_default();
matches!(builtin.as_deref(), Some("export" | "declare" | "typeset" | "local" | "readonly"))
&& head.contains('=')
&& name.starts_with(|c: char| c.is_ascii_alphabetic() || c == '_')
&& name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
}