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    pub fn literal(s: &str) -> Self {
130        Word(vec![WordPart::Lit(s.to_string())])
131    }
132
133    pub fn normalize(&self) -> Self {
134        let mut parts = Vec::new();
135        for part in &self.0 {
136            let part = match part {
137                WordPart::DQuote(inner) => WordPart::DQuote(inner.normalize()),
138                WordPart::CmdSub(s) => WordPart::CmdSub(s.normalize()),
139                WordPart::ProcSub(s) => WordPart::ProcSub(s.normalize()),
140                other => other.clone(),
141            };
142            if let WordPart::Lit(s) = &part
143                && let Some(WordPart::Lit(prev)) = parts.last_mut()
144            {
145                prev.push_str(s);
146                continue;
147            }
148            parts.push(part);
149        }
150        Word(parts)
151    }
152}
153
154impl Script {
155    pub fn is_empty(&self) -> bool {
156        self.0.is_empty()
157    }
158
159    pub fn normalize(&self) -> Self {
160        Script(
161            self.0
162                .iter()
163                .map(|stmt| Stmt {
164                    pipeline: stmt.pipeline.normalize(),
165                    op: stmt.op,
166                })
167                .collect(),
168        )
169    }
170
171    pub fn normalize_as_body(&self) -> Self {
172        let mut s = self.normalize();
173        if let Some(last) = s.0.last_mut()
174            && last.op.is_none()
175        {
176            last.op = Some(ListOp::Semi);
177        }
178        s
179    }
180}
181
182impl Pipeline {
183    fn normalize(&self) -> Self {
184        Pipeline {
185            bang: self.bang,
186            commands: self.commands.iter().map(|c| c.normalize()).collect(),
187        }
188    }
189}
190
191impl Cmd {
192    fn normalize(&self) -> Self {
193        match self {
194            Cmd::Simple(s) => Cmd::Simple(s.normalize()),
195            Cmd::Subshell { body, redirs } => Cmd::Subshell {
196                body: body.normalize(),
197                redirs: normalize_redirs(redirs),
198            },
199            Cmd::BraceGroup { body, redirs } => Cmd::BraceGroup {
200                body: body.normalize_as_body(),
201                redirs: normalize_redirs(redirs),
202            },
203            Cmd::For { var, items, body, redirs } => Cmd::For {
204                var: var.clone(),
205                items: items.iter().map(|w| w.normalize()).collect(),
206                body: body.normalize_as_body(),
207                redirs: normalize_redirs(redirs),
208            },
209            Cmd::While { cond, body, redirs } => Cmd::While {
210                cond: cond.normalize_as_body(),
211                body: body.normalize_as_body(),
212                redirs: normalize_redirs(redirs),
213            },
214            Cmd::Until { cond, body, redirs } => Cmd::Until {
215                cond: cond.normalize_as_body(),
216                body: body.normalize_as_body(),
217                redirs: normalize_redirs(redirs),
218            },
219            Cmd::If { branches, else_body, redirs } => Cmd::If {
220                branches: branches
221                    .iter()
222                    .map(|b| Branch {
223                        cond: b.cond.normalize_as_body(),
224                        body: b.body.normalize_as_body(),
225                    })
226                    .collect(),
227                else_body: else_body.as_ref().map(|e| e.normalize_as_body()),
228                redirs: normalize_redirs(redirs),
229            },
230            Cmd::DoubleBracket { words, redirs } => Cmd::DoubleBracket {
231                words: words.iter().map(|w| w.normalize()).collect(),
232                redirs: normalize_redirs(redirs),
233            },
234        }
235    }
236}
237
238impl SimpleCmd {
239    fn normalize(&self) -> Self {
240        SimpleCmd {
241            env: self
242                .env
243                .iter()
244                .map(|(k, v)| (k.clone(), v.normalize()))
245                .collect(),
246            words: self.words.iter().map(|w| w.normalize()).collect(),
247            redirs: normalize_redirs(&self.redirs),
248        }
249    }
250}
251
252fn normalize_redirs(redirs: &[Redir]) -> Vec<Redir> {
253    redirs
254        .iter()
255        .map(|r| match r {
256            Redir::Write { fd, target, append } => Redir::Write {
257                fd: *fd,
258                target: target.normalize(),
259                append: *append,
260            },
261            Redir::Read { fd, target } => Redir::Read {
262                fd: *fd,
263                target: target.normalize(),
264            },
265            Redir::HereStr(w) => Redir::HereStr(w.normalize()),
266            Redir::HereDoc { .. } | Redir::DupFd { .. } => r.clone(),
267        })
268        .collect()
269}