ghoti_exec/builtins/
test.rs1use 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 _ => 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}