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 return Err(format!("test: {}: binary operator expected", args[1]));
40 }
41 4 => {
42 if args[0] == "!" {
44 return evaluate(&args[1..]).map(|v| !v);
45 }
46 }
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
63fn 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
96fn 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
126fn 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
241struct 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 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 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 let left = self.advance()?.to_string();
331
332 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 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}