Skip to main content

ghoti_exec/builtins/
test.rs

1//! This is `test(1)`, not a testing mod.
2//!
3//! We use stricter syntax here and it's not POSIX- or fish-compatible.
4//! Specifically,
5//! 1. Unary primitives always consume the next argument which must be present.
6//!    They also cannot take a compound expression as argument.
7//! 2. Cannot use bare strings as booleans, `-z` or `-n` must be used.
8//! 3. `!`, `(`, `)` and strings starting with `-` are forbidden in binary operations.
9//!
10//! ```bnf
11//! ; Expr must not contains "]" in either form.
12//! FullArgs := "test" Expr
13//!           | "[" Expr "]"
14//!
15//! BoolExpr := ["-f" | "-z" | ..] STR
16//!           | STR_NOT_OP_LIKE ["-ne" | "-lt" | ..] STR_NOT_OP_LIKE
17//!           | "(" Expr ")"
18//! NotExpr  := BoolExpr
19//!           | "!" NotExpr
20//! AndExpr  := NotExpr
21//!           | NotExpr "-a" AndExpr
22//! OrExpr   := AndExpr
23//!           | AndExpr "-o" OrExpr
24//! Expr     := OrExpr
25//! ```
26
27use std::fs;
28use std::io::IsTerminal;
29use std::iter::Peekable;
30use std::os::unix::fs::{FileTypeExt, MetadataExt};
31use std::path::Path;
32
33use rustix::fs::{Access, Mode, access};
34
35use crate::{ExecContext, ExecResult};
36
37pub async fn test(_ctx: &mut ExecContext<'_>, mut args: &[String]) -> ExecResult<bool> {
38    let rbracket_pos = args.iter().position(|s| s == "]").unwrap_or(args.len());
39    if args[0] == "[" {
40        ensure!(rbracket_pos + 1 == args.len(), "must have ']' at last");
41        args = &args[1..rbracket_pos];
42    } else {
43        ensure!(rbracket_pos == args.len(), "must not have ']'");
44        args = &args[1..];
45    }
46
47    let mut iter = args.iter().map(|s| s.as_str()).peekable();
48    let ret = eval(&mut iter)?;
49    let tail = iter.next();
50    ensure!(tail.is_none(), "unexpected tail {tail:?}");
51    Ok(ret)
52}
53
54fn eval<'i>(iter: &mut Peekable<impl Iterator<Item = &'i str>>) -> ExecResult<bool> {
55    let mut fst = eval_and(iter)?;
56    while iter.next_if_eq(&"-o").is_some() {
57        fst |= eval_and(iter)?;
58    }
59    Ok(fst)
60}
61
62fn eval_and<'i>(iter: &mut Peekable<impl Iterator<Item = &'i str>>) -> ExecResult<bool> {
63    let mut fst = eval_atom(iter)?;
64    while iter.next_if_eq(&"-a").is_some() {
65        fst &= eval_atom(iter)?;
66    }
67    Ok(fst)
68}
69
70fn eval_atom<'i>(iter: &mut Peekable<impl Iterator<Item = &'i str>>) -> ExecResult<bool> {
71    let fst = iter.next().ok_or_else(|| "missing argument".to_owned())?;
72
73    let unary = match fst {
74        "-b" => path_is_block_dev,
75        "-c" => path_is_char_dev,
76        "-d" => path_is_dir,
77        "-e" => path_exists,
78        "-f" => path_is_regular_file,
79        "-g" => path_is_setgid,
80        "-G" => todo!(),
81        "-k" => path_is_sticky,
82        "-L" => path_is_symlink,
83        "-O" => todo!(),
84        "-p" => path_is_fifo,
85        "-r" => path_can_read,
86        "-s" => path_is_nonempty,
87        "-S" => path_is_socket,
88        "-t" => fd_is_tty,
89        "-u" => path_is_setuid,
90        "-w" => path_can_write,
91        "-x" => path_can_exec,
92
93        "-z" => str_is_empty,
94        "-n" => |a: &_| !str_is_empty(a),
95
96        "(" => {
97            let val = eval(iter)?;
98            let next = iter.next();
99            ensure!(next == Some(")"), "expecting ')' but got {next:?}");
100            return Ok(val);
101        }
102        "!" => return Ok(!eval_atom(iter)?),
103        _ if fst == ")" || fst.starts_with('-') => bail!("invalid string operand {fst:?}"),
104        _ => {
105            let binop = iter
106                .next()
107                .ok_or_else(|| format!("missing binary op after {fst:?}"))?;
108            let binary = match binop {
109                "-eq" => num_eq as fn(&str, &str) -> bool,
110                "-ne" => |a: &_, b: &_| !num_eq(a, b),
111                "-lt" => num_lt,
112                "-gt" => |a: &_, b: &_| num_lt(b, a),
113                "-ge" => |a: &_, b: &_| !num_lt(a, b),
114                "-le" => |a: &_, b: &_| !num_lt(b, a),
115                "=" => str_eq,
116                "!=" => |a: &_, b: &_| !str_eq(a, b),
117                op => bail!("expecting a binary op but got {op:?}"),
118            };
119            let snd = iter
120                .next()
121                .ok_or_else(|| format!("missing second operand for '{binop}'"))?;
122            ensure!(
123                !["(", "!", ")"].contains(&snd) && !snd.starts_with('-'),
124                "invalid string operand {snd:?}",
125            );
126            return Ok(binary(fst, snd));
127        }
128    };
129
130    let arg = iter
131        .next()
132        .ok_or_else(|| format!("missing arg for '{fst}'"))?;
133    Ok(unary(arg))
134}
135
136fn path_exists(path: &str) -> bool {
137    Path::new(path).exists()
138}
139
140fn path_is_symlink(path: &str) -> bool {
141    Path::new(path).is_symlink()
142}
143
144fn path_is_dir(path: &str) -> bool {
145    Path::new(path).is_dir()
146}
147
148fn path_is_regular_file(path: &str) -> bool {
149    Path::new(path).is_file()
150}
151
152fn path_is_nonempty(path: &str) -> bool {
153    fs::metadata(path).is_ok_and(|m| m.size() > 0)
154}
155
156fn path_is_block_dev(path: &str) -> bool {
157    fs::metadata(path).is_ok_and(|m| m.file_type().is_block_device())
158}
159
160fn path_is_char_dev(path: &str) -> bool {
161    fs::metadata(path).is_ok_and(|m| m.file_type().is_char_device())
162}
163
164fn path_is_fifo(path: &str) -> bool {
165    fs::metadata(path).is_ok_and(|m| m.file_type().is_fifo())
166}
167
168fn path_is_socket(path: &str) -> bool {
169    fs::metadata(path).is_ok_and(|m| m.file_type().is_socket())
170}
171
172fn path_is_setuid(path: &str) -> bool {
173    fs::metadata(path).is_ok_and(|m| Mode::from_bits_retain(m.mode()).contains(Mode::SUID))
174}
175
176fn path_is_setgid(path: &str) -> bool {
177    fs::metadata(path).is_ok_and(|m| Mode::from_bits_retain(m.mode()).contains(Mode::SGID))
178}
179
180fn path_is_sticky(path: &str) -> bool {
181    fs::metadata(path).is_ok_and(|m| Mode::from_bits_retain(m.mode()).contains(Mode::SVTX))
182}
183
184fn path_can_read(path: &str) -> bool {
185    access(path, Access::READ_OK).is_ok()
186}
187
188fn path_can_write(path: &str) -> bool {
189    access(path, Access::WRITE_OK).is_ok()
190}
191
192fn path_can_exec(path: &str) -> bool {
193    access(path, Access::EXEC_OK).is_ok()
194}
195
196fn fd_is_tty(fd: &str) -> bool {
197    match fd {
198        "0" => std::io::stdin().is_terminal(),
199        "1" => std::io::stdout().is_terminal(),
200        "2" => std::io::stderr().is_terminal(),
201        // Not safe.
202        _ => false,
203    }
204}
205
206fn str_is_empty(s: &str) -> bool {
207    s.is_empty()
208}
209
210fn str_eq(a: &str, b: &str) -> bool {
211    a == b
212}
213
214fn num_eq(a: &str, b: &str) -> bool {
215    match (a.parse::<f64>(), b.parse::<f64>()) {
216        (Ok(a), Ok(b)) => a == b,
217        _ => false,
218    }
219}
220
221fn num_lt(a: &str, b: &str) -> bool {
222    match (a.parse::<f64>(), b.parse::<f64>()) {
223        (Ok(a), Ok(b)) => a < b,
224        _ => false,
225    }
226}