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}
69
70#[derive(Debug, Clone, PartialEq, Eq)]
71pub struct Branch {
72    pub cond: Script,
73    pub body: Script,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq)]
77pub struct SimpleCmd {
78    pub env: Vec<(String, Word)>,
79    pub words: Vec<Word>,
80    pub redirs: Vec<Redir>,
81}
82
83#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct Word(pub Vec<WordPart>);
85
86#[derive(Debug, Clone, PartialEq, Eq)]
87pub enum WordPart {
88    Lit(String),
89    Escape(char),
90    SQuote(String),
91    DQuote(Word),
92    CmdSub(Script),
93    ProcSub(Script),
94    Backtick(String),
95    Arith(String),
96}
97
98#[derive(Debug, Clone, PartialEq, Eq)]
99pub enum Redir {
100    Write {
101        fd: u32,
102        target: Word,
103        append: bool,
104    },
105    Read {
106        fd: u32,
107        target: Word,
108    },
109    HereStr(Word),
110    HereDoc {
111        delimiter: String,
112        strip_tabs: bool,
113    },
114    DupFd {
115        src: u32,
116        dst: String,
117    },
118}
119
120pub use check::{command_verdict, is_safe_command, is_safe_pipeline};
121pub use explain::{Explanation, SegmentReport, explain, explain_with_coverage};
122pub use parse::parse;
123
124impl Word {
125    pub fn eval(&self) -> String {
126        eval::eval_word(self)
127    }
128
129    /// The set of literal words this word produces under UNQUOTED brace expansion (`{a,b}` → two
130    /// words). Every produced word must be classified, so a braced alternative can't hide a system
131    /// path from the gate (`cat {/etc/shadow,x}`). Non-braced words expand to `[self.eval()]`.
132    pub fn expand(&self) -> Vec<String> {
133        eval::expand_word(self)
134    }
135
136    pub fn literal(s: &str) -> Self {
137        Word(vec![WordPart::Lit(s.to_string())])
138    }
139
140    pub fn normalize(&self) -> Self {
141        let mut parts = Vec::new();
142        for part in &self.0 {
143            let part = match part {
144                WordPart::DQuote(inner) => WordPart::DQuote(inner.normalize()),
145                WordPart::CmdSub(s) => WordPart::CmdSub(s.normalize()),
146                WordPart::ProcSub(s) => WordPart::ProcSub(s.normalize()),
147                other => other.clone(),
148            };
149            if let WordPart::Lit(s) = &part
150                && let Some(WordPart::Lit(prev)) = parts.last_mut()
151            {
152                prev.push_str(s);
153                continue;
154            }
155            parts.push(part);
156        }
157        Word(parts)
158    }
159}
160
161impl Script {
162    pub fn is_empty(&self) -> bool {
163        self.0.is_empty()
164    }
165
166    pub fn normalize(&self) -> Self {
167        Script(
168            self.0
169                .iter()
170                .map(|stmt| Stmt {
171                    pipeline: stmt.pipeline.normalize(),
172                    op: stmt.op,
173                })
174                .collect(),
175        )
176    }
177
178    pub fn normalize_as_body(&self) -> Self {
179        let mut s = self.normalize();
180        if let Some(last) = s.0.last_mut()
181            && last.op.is_none()
182        {
183            last.op = Some(ListOp::Semi);
184        }
185        s
186    }
187}
188
189impl Pipeline {
190    fn normalize(&self) -> Self {
191        Pipeline {
192            bang: self.bang,
193            commands: self.commands.iter().map(|c| c.normalize()).collect(),
194        }
195    }
196}
197
198impl Cmd {
199    fn normalize(&self) -> Self {
200        match self {
201            Cmd::Simple(s) => Cmd::Simple(s.normalize()),
202            Cmd::Subshell { body, redirs } => Cmd::Subshell {
203                body: body.normalize(),
204                redirs: normalize_redirs(redirs),
205            },
206            Cmd::BraceGroup { body, redirs } => Cmd::BraceGroup {
207                body: body.normalize_as_body(),
208                redirs: normalize_redirs(redirs),
209            },
210            Cmd::For { var, items, body, redirs } => Cmd::For {
211                var: var.clone(),
212                items: items.iter().map(|w| w.normalize()).collect(),
213                body: body.normalize_as_body(),
214                redirs: normalize_redirs(redirs),
215            },
216            Cmd::While { cond, body, redirs } => Cmd::While {
217                cond: cond.normalize_as_body(),
218                body: body.normalize_as_body(),
219                redirs: normalize_redirs(redirs),
220            },
221            Cmd::Until { cond, body, redirs } => Cmd::Until {
222                cond: cond.normalize_as_body(),
223                body: body.normalize_as_body(),
224                redirs: normalize_redirs(redirs),
225            },
226            Cmd::If { branches, else_body, redirs } => Cmd::If {
227                branches: branches
228                    .iter()
229                    .map(|b| Branch {
230                        cond: b.cond.normalize_as_body(),
231                        body: b.body.normalize_as_body(),
232                    })
233                    .collect(),
234                else_body: else_body.as_ref().map(|e| e.normalize_as_body()),
235                redirs: normalize_redirs(redirs),
236            },
237            Cmd::DoubleBracket { words, redirs } => Cmd::DoubleBracket {
238                words: words.iter().map(|w| w.normalize()).collect(),
239                redirs: normalize_redirs(redirs),
240            },
241        }
242    }
243}
244
245impl SimpleCmd {
246    fn normalize(&self) -> Self {
247        SimpleCmd {
248            env: self
249                .env
250                .iter()
251                .map(|(k, v)| (k.clone(), v.normalize()))
252                .collect(),
253            words: self.words.iter().map(|w| w.normalize()).collect(),
254            redirs: normalize_redirs(&self.redirs),
255        }
256    }
257}
258
259fn normalize_redirs(redirs: &[Redir]) -> Vec<Redir> {
260    redirs
261        .iter()
262        .map(|r| match r {
263            Redir::Write { fd, target, append } => Redir::Write {
264                fd: *fd,
265                target: target.normalize(),
266                append: *append,
267            },
268            Redir::Read { fd, target } => Redir::Read {
269                fd: *fd,
270                target: target.normalize(),
271            },
272            Redir::HereStr(w) => Redir::HereStr(w.normalize()),
273            Redir::HereDoc { .. } | Redir::DupFd { .. } => r.clone(),
274        })
275        .collect()
276}