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