1use std::fs;
2use std::os::unix::fs::MetadataExt;
3
4pub fn evaluate(args: &[String]) -> Result<bool, String> {
12 if args.is_empty() {
13 return Ok(false);
14 }
15
16 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 if let Ok(result) = eval_binary(&args[0], &args[1], &args[2]) {
29 return Ok(result);
30 }
31 if args[0] == "!" {
33 return evaluate(&args[1..]).map(|v| !v);
34 }
35 if args[0] == "(" && args[2] == ")" {
37 return evaluate(&args[1..2]);
38 }
39 if args[1] == "-a" || args[1] == "-o" {
41 } else {
43 return Err(format!("test: {}: binary operator expected", args[1]));
44 }
45 }
46 4 => {
47 if args[0] == "!" {
49 return evaluate(&args[1..]).map(|v| !v);
50 }
51 }
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
68fn 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
101fn 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
129fn 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
244struct 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 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 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 let left = self.advance()?.to_string();
334
335 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 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}