Skip to main content

coreutils_rs/test_cmd/
core.rs

1use std::fs;
2use std::os::unix::fs::MetadataExt;
3
4/// Evaluate a test expression given as a slice of string arguments.
5///
6/// Returns `Ok(true)` if the expression is true, `Ok(false)` if false,
7/// and `Err(msg)` on syntax/parse errors.
8///
9/// This implements a recursive descent parser for the POSIX test expression
10/// grammar with GNU extensions.
11pub fn evaluate(args: &[String]) -> Result<bool, String> {
12    if args.is_empty() {
13        return Ok(false);
14    }
15
16    // Special cases for 1, 2, 3, and 4 arguments for POSIX compliance.
17    // These bypass the parser to handle ambiguous cases correctly.
18    match args.len() {
19        1 => return Ok(!args[0].is_empty()),
20        2 => {
21            if args[0] == "!" {
22                return Ok(args[1].is_empty());
23            }
24            return eval_unary(&args[0], &args[1]);
25        }
26        3 => {
27            // Try binary operator first
28            if let Ok(result) = eval_binary(&args[0], &args[1], &args[2]) {
29                return Ok(result);
30            }
31            // Try ! unary
32            if args[0] == "!" {
33                return evaluate(&args[1..]).map(|v| !v);
34            }
35            // Try ( expr )
36            if args[0] == "(" && args[2] == ")" {
37                return evaluate(&args[1..2]);
38            }
39            return Err(format!("test: {}: binary operator expected", args[1]));
40        }
41        4 => {
42            // ! expr expr expr (3-arg expression negated)
43            if args[0] == "!" {
44                return evaluate(&args[1..]).map(|v| !v);
45            }
46            // ( expr ) with binary
47            // Fall through to general parser
48        }
49        _ => {}
50    }
51
52    let mut parser = Parser::new(args);
53    let result = parser.parse_expr()?;
54    if parser.pos < parser.args.len() {
55        return Err(format!(
56            "test: {}: unexpected argument",
57            parser.args[parser.pos]
58        ));
59    }
60    Ok(result)
61}
62
63/// Evaluate a unary operator expression.
64fn eval_unary(op: &str, arg: &str) -> Result<bool, String> {
65    match op {
66        "-e" => Ok(path_exists(arg)),
67        "-f" => Ok(is_regular_file(arg)),
68        "-d" => Ok(is_directory(arg)),
69        "-r" => Ok(is_readable(arg)),
70        "-w" => Ok(is_writable(arg)),
71        "-x" => Ok(is_executable(arg)),
72        "-s" => Ok(has_size(arg)),
73        "-L" | "-h" => Ok(is_symlink(arg)),
74        "-b" => Ok(is_block_special(arg)),
75        "-c" => Ok(is_char_special(arg)),
76        "-p" => Ok(is_fifo(arg)),
77        "-S" => Ok(is_socket(arg)),
78        "-g" => Ok(is_setgid(arg)),
79        "-u" => Ok(is_setuid(arg)),
80        "-k" => Ok(is_sticky(arg)),
81        "-O" => Ok(is_owned_by_euid(arg)),
82        "-G" => Ok(is_group_egid(arg)),
83        "-N" => Ok(is_modified_since_read(arg)),
84        "-z" => Ok(arg.is_empty()),
85        "-n" => Ok(!arg.is_empty()),
86        "-t" => {
87            let fd: i32 = arg
88                .parse()
89                .map_err(|_| format!("test: {}: integer expression expected", arg))?;
90            Ok(is_terminal(fd))
91        }
92        _ => Err(format!("test: {}: unary operator expected", op)),
93    }
94}
95
96/// Evaluate a binary operator expression.
97fn eval_binary(left: &str, op: &str, right: &str) -> Result<bool, String> {
98    match op {
99        "=" | "==" => Ok(left == right),
100        "!=" => Ok(left != right),
101        "<" => Ok(left < right),
102        ">" => Ok(left > right),
103        "-eq" => int_cmp(left, right, |a, b| a == b),
104        "-ne" => int_cmp(left, right, |a, b| a != b),
105        "-lt" => int_cmp(left, right, |a, b| a < b),
106        "-le" => int_cmp(left, right, |a, b| a <= b),
107        "-gt" => int_cmp(left, right, |a, b| a > b),
108        "-ge" => int_cmp(left, right, |a, b| a >= b),
109        "-nt" => Ok(file_newer_than(left, right)),
110        "-ot" => Ok(file_older_than(left, right)),
111        "-ef" => Ok(same_file(left, right)),
112        _ => Err(format!("test: {}: unknown binary operator", op)),
113    }
114}
115
116fn int_cmp(left: &str, right: &str, cmp: impl Fn(i64, i64) -> bool) -> Result<bool, String> {
117    let a: i64 = left
118        .parse()
119        .map_err(|_| format!("test: {}: integer expression expected", left))?;
120    let b: i64 = right
121        .parse()
122        .map_err(|_| format!("test: {}: integer expression expected", right))?;
123    Ok(cmp(a, b))
124}
125
126// ---- File test primitives ----
127
128fn path_exists(path: &str) -> bool {
129    fs::symlink_metadata(path).is_ok()
130}
131
132fn is_regular_file(path: &str) -> bool {
133    fs::metadata(path).map_or(false, |m| m.is_file())
134}
135
136fn is_directory(path: &str) -> bool {
137    fs::metadata(path).map_or(false, |m| m.is_dir())
138}
139
140fn is_readable(path: &str) -> bool {
141    unsafe { libc::access(to_cstr(path).as_ptr(), libc::R_OK) == 0 }
142}
143
144fn is_writable(path: &str) -> bool {
145    unsafe { libc::access(to_cstr(path).as_ptr(), libc::W_OK) == 0 }
146}
147
148fn is_executable(path: &str) -> bool {
149    unsafe { libc::access(to_cstr(path).as_ptr(), libc::X_OK) == 0 }
150}
151
152fn has_size(path: &str) -> bool {
153    fs::metadata(path).map_or(false, |m| m.len() > 0)
154}
155
156fn is_symlink(path: &str) -> bool {
157    fs::symlink_metadata(path).map_or(false, |m| m.file_type().is_symlink())
158}
159
160fn is_block_special(path: &str) -> bool {
161    use std::os::unix::fs::FileTypeExt;
162    fs::metadata(path).map_or(false, |m| m.file_type().is_block_device())
163}
164
165fn is_char_special(path: &str) -> bool {
166    use std::os::unix::fs::FileTypeExt;
167    fs::metadata(path).map_or(false, |m| m.file_type().is_char_device())
168}
169
170fn is_fifo(path: &str) -> bool {
171    use std::os::unix::fs::FileTypeExt;
172    fs::metadata(path).map_or(false, |m| m.file_type().is_fifo())
173}
174
175fn is_socket(path: &str) -> bool {
176    use std::os::unix::fs::FileTypeExt;
177    fs::metadata(path).map_or(false, |m| m.file_type().is_socket())
178}
179
180fn is_setgid(path: &str) -> bool {
181    fs::metadata(path).map_or(false, |m| m.mode() & 0o2000 != 0)
182}
183
184fn is_setuid(path: &str) -> bool {
185    fs::metadata(path).map_or(false, |m| m.mode() & 0o4000 != 0)
186}
187
188fn is_sticky(path: &str) -> bool {
189    fs::metadata(path).map_or(false, |m| m.mode() & 0o1000 != 0)
190}
191
192fn is_owned_by_euid(path: &str) -> bool {
193    fs::metadata(path).map_or(false, |m| m.uid() == unsafe { libc::geteuid() })
194}
195
196fn is_group_egid(path: &str) -> bool {
197    fs::metadata(path).map_or(false, |m| m.gid() == unsafe { libc::getegid() })
198}
199
200fn is_modified_since_read(path: &str) -> bool {
201    fs::metadata(path).map_or(false, |m| m.mtime() > m.atime())
202}
203
204fn is_terminal(fd: i32) -> bool {
205    unsafe { libc::isatty(fd) == 1 }
206}
207
208fn file_newer_than(a: &str, b: &str) -> bool {
209    let ma = fs::metadata(a).and_then(|m| m.modified());
210    let mb = fs::metadata(b).and_then(|m| m.modified());
211    match (ma, mb) {
212        (Ok(ta), Ok(tb)) => ta > tb,
213        (Ok(_), Err(_)) => true,
214        _ => false,
215    }
216}
217
218fn file_older_than(a: &str, b: &str) -> bool {
219    let ma = fs::metadata(a).and_then(|m| m.modified());
220    let mb = fs::metadata(b).and_then(|m| m.modified());
221    match (ma, mb) {
222        (Ok(ta), Ok(tb)) => ta < tb,
223        (Err(_), Ok(_)) => true,
224        _ => false,
225    }
226}
227
228fn same_file(a: &str, b: &str) -> bool {
229    let ma = fs::metadata(a);
230    let mb = fs::metadata(b);
231    match (ma, mb) {
232        (Ok(a), Ok(b)) => a.dev() == b.dev() && a.ino() == b.ino(),
233        _ => false,
234    }
235}
236
237fn to_cstr(s: &str) -> std::ffi::CString {
238    std::ffi::CString::new(s).unwrap_or_else(|_| std::ffi::CString::new("").unwrap())
239}
240
241// ---- Recursive descent parser ----
242//
243// Grammar (POSIX + GNU extensions):
244//   expr     := or_expr
245//   or_expr  := and_expr ( '-o' and_expr )*
246//   and_expr := not_expr ( '-a' not_expr )*
247//   not_expr := '!' not_expr | primary
248//   primary  := '(' expr ')' | unary_op OPERAND | OPERAND binary_op OPERAND | OPERAND
249
250struct Parser<'a> {
251    args: &'a [String],
252    pos: usize,
253}
254
255impl<'a> Parser<'a> {
256    fn new(args: &'a [String]) -> Self {
257        Self { args, pos: 0 }
258    }
259
260    fn peek(&self) -> Option<&str> {
261        self.args.get(self.pos).map(|s| s.as_str())
262    }
263
264    fn advance(&mut self) -> Result<&str, String> {
265        if self.pos >= self.args.len() {
266            return Err("test: missing argument".to_string());
267        }
268        let val = &self.args[self.pos];
269        self.pos += 1;
270        Ok(val.as_str())
271    }
272
273    fn parse_expr(&mut self) -> Result<bool, String> {
274        self.parse_or()
275    }
276
277    fn parse_or(&mut self) -> Result<bool, String> {
278        let mut result = self.parse_and()?;
279        while self.peek() == Some("-o") {
280            self.pos += 1;
281            let right = self.parse_and()?;
282            result = result || right;
283        }
284        Ok(result)
285    }
286
287    fn parse_and(&mut self) -> Result<bool, String> {
288        let mut result = self.parse_not()?;
289        while self.peek() == Some("-a") {
290            self.pos += 1;
291            let right = self.parse_not()?;
292            result = result && right;
293        }
294        Ok(result)
295    }
296
297    fn parse_not(&mut self) -> Result<bool, String> {
298        if self.peek() == Some("!") {
299            self.pos += 1;
300            let val = self.parse_not()?;
301            return Ok(!val);
302        }
303        self.parse_primary()
304    }
305
306    fn parse_primary(&mut self) -> Result<bool, String> {
307        let token = self
308            .peek()
309            .ok_or_else(|| "test: missing argument".to_string())?;
310
311        // Parenthesized expression
312        if token == "(" {
313            self.pos += 1;
314            let result = self.parse_expr()?;
315            if self.peek() != Some(")") {
316                return Err("test: missing ')'".to_string());
317            }
318            self.pos += 1;
319            return Ok(result);
320        }
321
322        // Check for unary operators
323        if is_unary_op(token) {
324            let op = self.advance()?.to_string();
325            let operand = self.advance()?;
326            return eval_unary(&op, operand);
327        }
328
329        // Otherwise it's an operand, which might be followed by a binary operator
330        let left = self.advance()?.to_string();
331
332        // Check if next token is a binary operator
333        if let Some(next) = self.peek() {
334            if is_binary_op(next) {
335                let op = self.advance()?.to_string();
336                let right = self.advance()?;
337                return eval_binary(&left, &op, right);
338            }
339        }
340
341        // Bare string: true if non-empty
342        Ok(!left.is_empty())
343    }
344}
345
346fn is_unary_op(s: &str) -> bool {
347    matches!(
348        s,
349        "-e" | "-f"
350            | "-d"
351            | "-r"
352            | "-w"
353            | "-x"
354            | "-s"
355            | "-L"
356            | "-h"
357            | "-b"
358            | "-c"
359            | "-p"
360            | "-S"
361            | "-g"
362            | "-u"
363            | "-k"
364            | "-O"
365            | "-G"
366            | "-N"
367            | "-z"
368            | "-n"
369            | "-t"
370    )
371}
372
373fn is_binary_op(s: &str) -> bool {
374    matches!(
375        s,
376        "=" | "=="
377            | "!="
378            | "<"
379            | ">"
380            | "-eq"
381            | "-ne"
382            | "-lt"
383            | "-le"
384            | "-gt"
385            | "-ge"
386            | "-nt"
387            | "-ot"
388            | "-ef"
389    )
390}