Skip to main content

safe_chains/cst/
mod.rs

1pub(crate) mod check;
2mod display;
3mod eval;
4mod explain;
5mod parse;
6#[cfg(test)]
7mod proptests;
8
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct Script(pub Vec<Stmt>);
11
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct Stmt {
14    pub pipeline: Pipeline,
15    pub op: Option<ListOp>,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum ListOp {
20    And,
21    Or,
22    Semi,
23    Amp,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct Pipeline {
28    pub bang: bool,
29    pub commands: Vec<Cmd>,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub enum Cmd {
34    Simple(SimpleCmd),
35    Subshell {
36        body: Script,
37        redirs: Vec<Redir>,
38    },
39    BraceGroup {
40        body: Script,
41        redirs: Vec<Redir>,
42    },
43    For {
44        var: String,
45        items: Vec<Word>,
46        body: Script,
47        redirs: Vec<Redir>,
48    },
49    While {
50        cond: Script,
51        body: Script,
52        redirs: Vec<Redir>,
53    },
54    Until {
55        cond: Script,
56        body: Script,
57        redirs: Vec<Redir>,
58    },
59    If {
60        branches: Vec<Branch>,
61        else_body: Option<Script>,
62        redirs: Vec<Redir>,
63    },
64    DoubleBracket {
65        words: Vec<Word>,
66        redirs: Vec<Redir>,
67    },
68    /// `name() { body }` (or `function name { body }`). Defining a function has NO effect — it is
69    /// classified Inert. The body's safety matters only when the function is CALLED (resolved in
70    /// `check`), so it is stored, not flattened.
71    FunctionDef {
72        name: String,
73        body: Script,
74    },
75}
76
77#[derive(Debug, Clone, PartialEq, Eq)]
78pub struct Branch {
79    pub cond: Script,
80    pub body: Script,
81}
82
83#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct SimpleCmd {
85    pub env: Vec<(String, Word)>,
86    pub words: Vec<Word>,
87    pub redirs: Vec<Redir>,
88}
89
90#[derive(Debug, Clone, PartialEq, Eq)]
91pub struct Word(pub Vec<WordPart>);
92
93#[derive(Debug, Clone, PartialEq, Eq)]
94pub enum WordPart {
95    Lit(String),
96    Escape(char),
97    SQuote(String),
98    DQuote(Word),
99    CmdSub(Script),
100    ProcSub(Script),
101    Backtick(String),
102    Arith(String),
103}
104
105#[derive(Debug, Clone, PartialEq, Eq)]
106pub enum Redir {
107    Write {
108        fd: u32,
109        target: Word,
110        append: bool,
111    },
112    Read {
113        fd: u32,
114        target: Word,
115    },
116    HereStr(Word),
117    HereDoc {
118        delimiter: String,
119        strip_tabs: bool,
120    },
121    DupFd {
122        src: u32,
123        dst: String,
124    },
125}
126
127pub use check::{command_verdict, is_safe_command, is_safe_pipeline};
128pub use explain::{Explanation, SegmentReport, explain, explain_with_coverage};
129pub use parse::parse;
130
131impl Word {
132    pub fn eval(&self) -> String {
133        eval::eval_word(self)
134    }
135
136    /// The set of literal words this word produces under UNQUOTED brace expansion (`{a,b}` → two
137    /// words). Every produced word must be classified, so a braced alternative can't hide a system
138    /// path from the gate (`cat {/etc/shadow,x}`). Non-braced words expand to `[self.eval()]`.
139    pub fn expand(&self) -> Vec<String> {
140        eval::expand_word(self)
141    }
142
143    pub fn literal(s: &str) -> Self {
144        Word(vec![WordPart::Lit(s.to_string())])
145    }
146
147    pub fn normalize(&self) -> Self {
148        let mut parts = Vec::new();
149        for part in &self.0 {
150            let part = match part {
151                WordPart::DQuote(inner) => WordPart::DQuote(inner.normalize()),
152                WordPart::CmdSub(s) => WordPart::CmdSub(s.normalize()),
153                WordPart::ProcSub(s) => WordPart::ProcSub(s.normalize()),
154                other => other.clone(),
155            };
156            if let WordPart::Lit(s) = &part
157                && let Some(WordPart::Lit(prev)) = parts.last_mut()
158            {
159                prev.push_str(s);
160                continue;
161            }
162            parts.push(part);
163        }
164        Word(parts)
165    }
166}
167
168impl Script {
169    pub fn is_empty(&self) -> bool {
170        self.0.is_empty()
171    }
172
173    pub fn normalize(&self) -> Self {
174        Script(
175            self.0
176                .iter()
177                .map(|stmt| Stmt {
178                    pipeline: stmt.pipeline.normalize(),
179                    op: stmt.op,
180                })
181                .collect(),
182        )
183    }
184
185    pub fn normalize_as_body(&self) -> Self {
186        let mut s = self.normalize();
187        if let Some(last) = s.0.last_mut()
188            && last.op.is_none()
189        {
190            last.op = Some(ListOp::Semi);
191        }
192        s
193    }
194}
195
196impl Pipeline {
197    fn normalize(&self) -> Self {
198        Pipeline {
199            bang: self.bang,
200            commands: self.commands.iter().map(|c| c.normalize()).collect(),
201        }
202    }
203}
204
205impl Cmd {
206    fn normalize(&self) -> Self {
207        match self {
208            Cmd::Simple(s) => Cmd::Simple(s.normalize()),
209            Cmd::Subshell { body, redirs } => Cmd::Subshell {
210                body: body.normalize(),
211                redirs: normalize_redirs(redirs),
212            },
213            Cmd::BraceGroup { body, redirs } => Cmd::BraceGroup {
214                body: body.normalize_as_body(),
215                redirs: normalize_redirs(redirs),
216            },
217            Cmd::For { var, items, body, redirs } => Cmd::For {
218                var: var.clone(),
219                items: items.iter().map(|w| w.normalize()).collect(),
220                body: body.normalize_as_body(),
221                redirs: normalize_redirs(redirs),
222            },
223            Cmd::While { cond, body, redirs } => Cmd::While {
224                cond: cond.normalize_as_body(),
225                body: body.normalize_as_body(),
226                redirs: normalize_redirs(redirs),
227            },
228            Cmd::Until { cond, body, redirs } => Cmd::Until {
229                cond: cond.normalize_as_body(),
230                body: body.normalize_as_body(),
231                redirs: normalize_redirs(redirs),
232            },
233            Cmd::If { branches, else_body, redirs } => Cmd::If {
234                branches: branches
235                    .iter()
236                    .map(|b| Branch {
237                        cond: b.cond.normalize_as_body(),
238                        body: b.body.normalize_as_body(),
239                    })
240                    .collect(),
241                else_body: else_body.as_ref().map(|e| e.normalize_as_body()),
242                redirs: normalize_redirs(redirs),
243            },
244            Cmd::DoubleBracket { words, redirs } => Cmd::DoubleBracket {
245                words: words.iter().map(|w| w.normalize()).collect(),
246                redirs: normalize_redirs(redirs),
247            },
248            Cmd::FunctionDef { name, body } => Cmd::FunctionDef {
249                name: name.clone(),
250                body: body.normalize_as_body(),
251            },
252        }
253    }
254}
255
256impl SimpleCmd {
257    fn normalize(&self) -> Self {
258        SimpleCmd {
259            env: self
260                .env
261                .iter()
262                .map(|(k, v)| (k.clone(), v.normalize()))
263                .collect(),
264            words: self.words.iter().map(|w| w.normalize()).collect(),
265            redirs: normalize_redirs(&self.redirs),
266        }
267    }
268}
269
270fn normalize_redirs(redirs: &[Redir]) -> Vec<Redir> {
271    redirs
272        .iter()
273        .map(|r| match r {
274            Redir::Write { fd, target, append } => Redir::Write {
275                fd: *fd,
276                target: target.normalize(),
277                append: *append,
278            },
279            Redir::Read { fd, target } => Redir::Read {
280                fd: *fd,
281                target: target.normalize(),
282            },
283            Redir::HereStr(w) => Redir::HereStr(w.normalize()),
284            Redir::HereDoc { .. } | Redir::DupFd { .. } => r.clone(),
285        })
286        .collect()
287}