Serenity Operating System
1/*
2 * Copyright (c) 2020-2022, the SerenityOS developers.
3 *
4 * SPDX-License-Identifier: BSD-2-Clause
5 */
6
7#include <AK/Assertions.h>
8#include <AK/LexicalPath.h>
9#include <AK/NonnullOwnPtr.h>
10#include <AK/OwnPtr.h>
11#include <LibCore/System.h>
12#include <LibMain/Main.h>
13#include <stdio.h>
14#include <sys/stat.h>
15#include <unistd.h>
16
17bool g_there_was_an_error = false;
18
19[[noreturn, gnu::format(printf, 1, 2)]] static void fatal_error(char const* format, ...)
20{
21 fputs("\033[31m", stderr);
22
23 va_list ap;
24 va_start(ap, format);
25 vfprintf(stderr, format, ap);
26 va_end(ap);
27
28 fputs("\033[0m\n", stderr);
29 exit(126);
30}
31
32class Condition {
33public:
34 virtual ~Condition() = default;
35 virtual bool check() const = 0;
36};
37
38class And : public Condition {
39public:
40 And(NonnullOwnPtr<Condition> lhs, NonnullOwnPtr<Condition> rhs)
41 : m_lhs(move(lhs))
42 , m_rhs(move(rhs))
43 {
44 }
45
46private:
47 virtual bool check() const override
48 {
49 return m_lhs->check() && m_rhs->check();
50 }
51
52 NonnullOwnPtr<Condition> m_lhs;
53 NonnullOwnPtr<Condition> m_rhs;
54};
55
56class Or : public Condition {
57public:
58 Or(NonnullOwnPtr<Condition> lhs, NonnullOwnPtr<Condition> rhs)
59 : m_lhs(move(lhs))
60 , m_rhs(move(rhs))
61 {
62 }
63
64private:
65 virtual bool check() const override
66 {
67 return m_lhs->check() || m_rhs->check();
68 }
69
70 NonnullOwnPtr<Condition> m_lhs;
71 NonnullOwnPtr<Condition> m_rhs;
72};
73
74class Not : public Condition {
75public:
76 Not(NonnullOwnPtr<Condition> cond)
77 : m_cond(move(cond))
78 {
79 }
80
81private:
82 virtual bool check() const override
83 {
84 return !m_cond->check();
85 }
86
87 NonnullOwnPtr<Condition> m_cond;
88};
89
90class FileIsOfKind : public Condition {
91public:
92 enum Kind {
93 BlockDevice,
94 CharacterDevice,
95 Directory,
96 FIFO,
97 Regular,
98 Socket,
99 SymbolicLink,
100 };
101 FileIsOfKind(StringView path, Kind kind)
102 : m_path(path)
103 , m_kind(kind)
104 {
105 }
106
107private:
108 virtual bool check() const override
109 {
110 struct stat statbuf;
111 int rc;
112
113 if (m_kind == SymbolicLink)
114 rc = stat(m_path.characters(), &statbuf);
115 else
116 rc = lstat(m_path.characters(), &statbuf);
117
118 if (rc < 0) {
119 if (errno != ENOENT) {
120 perror(m_path.characters());
121 g_there_was_an_error = true;
122 }
123 return false;
124 }
125
126 switch (m_kind) {
127 case BlockDevice:
128 return S_ISBLK(statbuf.st_mode);
129 case CharacterDevice:
130 return S_ISCHR(statbuf.st_mode);
131 case Directory:
132 return S_ISDIR(statbuf.st_mode);
133 case FIFO:
134 return S_ISFIFO(statbuf.st_mode);
135 case Regular:
136 return S_ISREG(statbuf.st_mode);
137 case Socket:
138 return S_ISSOCK(statbuf.st_mode);
139 case SymbolicLink:
140 return S_ISLNK(statbuf.st_mode);
141 default:
142 VERIFY_NOT_REACHED();
143 }
144 }
145
146 DeprecatedString m_path;
147 Kind m_kind { Regular };
148};
149
150class UserHasPermission : public Condition {
151public:
152 enum Permission {
153 Any,
154 Read,
155 Write,
156 Execute,
157 };
158 UserHasPermission(StringView path, Permission kind)
159 : m_path(path)
160 , m_kind(kind)
161 {
162 }
163
164private:
165 virtual bool check() const override
166 {
167 switch (m_kind) {
168 case Read:
169 return access(m_path.characters(), R_OK) == 0;
170 case Write:
171 return access(m_path.characters(), W_OK) == 0;
172 case Execute:
173 return access(m_path.characters(), X_OK) == 0;
174 case Any:
175 return access(m_path.characters(), F_OK) == 0;
176 default:
177 VERIFY_NOT_REACHED();
178 }
179 }
180
181 DeprecatedString m_path;
182 Permission m_kind { Read };
183};
184
185class FileHasFlag : public Condition {
186public:
187 enum Flag {
188 SGID,
189 SUID,
190 SVTX,
191 };
192 FileHasFlag(StringView path, Flag kind)
193 : m_path(path)
194 , m_kind(kind)
195 {
196 }
197
198private:
199 virtual bool check() const override
200 {
201 struct stat statbuf;
202 int rc = stat(m_path.characters(), &statbuf);
203
204 if (rc < 0) {
205 if (errno != ENOENT) {
206 perror(m_path.characters());
207 g_there_was_an_error = true;
208 }
209 return false;
210 }
211
212 switch (m_kind) {
213 case SGID:
214 return statbuf.st_mode & S_ISGID;
215 case SUID:
216 return statbuf.st_mode & S_ISUID;
217 case SVTX:
218 return statbuf.st_mode & S_ISVTX;
219 default:
220 VERIFY_NOT_REACHED();
221 }
222 }
223
224 DeprecatedString m_path;
225 Flag m_kind { SGID };
226};
227
228class FileIsOwnedBy : public Condition {
229public:
230 enum Owner {
231 EffectiveGID,
232 EffectiveUID,
233 };
234 FileIsOwnedBy(StringView path, Owner kind)
235 : m_path(path)
236 , m_kind(kind)
237 {
238 }
239
240private:
241 virtual bool check() const override
242 {
243 struct stat statbuf;
244 int rc = stat(m_path.characters(), &statbuf);
245
246 if (rc < 0) {
247 if (errno != ENOENT) {
248 perror(m_path.characters());
249 g_there_was_an_error = true;
250 }
251 return false;
252 }
253
254 switch (m_kind) {
255 case EffectiveGID:
256 return statbuf.st_gid == getgid();
257 case EffectiveUID:
258 return statbuf.st_uid == getuid();
259 default:
260 VERIFY_NOT_REACHED();
261 }
262 }
263
264 DeprecatedString m_path;
265 Owner m_kind { EffectiveGID };
266};
267
268class StringCompare : public Condition {
269public:
270 enum Mode {
271 Equal,
272 NotEqual,
273 };
274
275 StringCompare(StringView lhs, StringView rhs, Mode mode)
276 : m_lhs(move(lhs))
277 , m_rhs(move(rhs))
278 , m_mode(mode)
279 {
280 }
281
282private:
283 virtual bool check() const override
284 {
285 if (m_mode == Equal)
286 return m_lhs == m_rhs;
287 return m_lhs != m_rhs;
288 }
289
290 StringView m_lhs;
291 StringView m_rhs;
292 Mode m_mode { Equal };
293};
294
295class NumericCompare : public Condition {
296public:
297 enum Mode {
298 Equal,
299 Greater,
300 GreaterOrEqual,
301 Less,
302 LessOrEqual,
303 NotEqual,
304 };
305
306 NumericCompare(DeprecatedString lhs, DeprecatedString rhs, Mode mode)
307 : m_mode(mode)
308 {
309 auto lhs_option = lhs.trim_whitespace().to_int();
310 auto rhs_option = rhs.trim_whitespace().to_int();
311
312 if (!lhs_option.has_value())
313 fatal_error("expected integer expression: '%s'", lhs.characters());
314
315 if (!rhs_option.has_value())
316 fatal_error("expected integer expression: '%s'", rhs.characters());
317
318 m_lhs = lhs_option.value();
319 m_rhs = rhs_option.value();
320 }
321
322private:
323 virtual bool check() const override
324 {
325 switch (m_mode) {
326 case Equal:
327 return m_lhs == m_rhs;
328 case Greater:
329 return m_lhs > m_rhs;
330 case GreaterOrEqual:
331 return m_lhs >= m_rhs;
332 case Less:
333 return m_lhs < m_rhs;
334 case LessOrEqual:
335 return m_lhs <= m_rhs;
336 case NotEqual:
337 return m_lhs != m_rhs;
338 default:
339 VERIFY_NOT_REACHED();
340 }
341 }
342
343 int m_lhs { 0 };
344 int m_rhs { 0 };
345 Mode m_mode { Equal };
346};
347
348class FileCompare : public Condition {
349public:
350 enum Mode {
351 Same,
352 ModificationTimestampGreater,
353 ModificationTimestampLess,
354 };
355
356 FileCompare(DeprecatedString lhs, DeprecatedString rhs, Mode mode)
357 : m_lhs(move(lhs))
358 , m_rhs(move(rhs))
359 , m_mode(mode)
360 {
361 }
362
363private:
364 virtual bool check() const override
365 {
366 struct stat statbuf_l;
367 int rc = stat(m_lhs.characters(), &statbuf_l);
368
369 if (rc < 0) {
370 perror(m_lhs.characters());
371 g_there_was_an_error = true;
372 return false;
373 }
374
375 struct stat statbuf_r;
376 rc = stat(m_rhs.characters(), &statbuf_r);
377
378 if (rc < 0) {
379 perror(m_rhs.characters());
380 g_there_was_an_error = true;
381 return false;
382 }
383
384 switch (m_mode) {
385 case Same:
386 return statbuf_l.st_dev == statbuf_r.st_dev && statbuf_l.st_ino == statbuf_r.st_ino;
387 case ModificationTimestampLess:
388 return statbuf_l.st_mtime < statbuf_r.st_mtime;
389 case ModificationTimestampGreater:
390 return statbuf_l.st_mtime > statbuf_r.st_mtime;
391 default:
392 VERIFY_NOT_REACHED();
393 }
394 }
395
396 DeprecatedString m_lhs;
397 DeprecatedString m_rhs;
398 Mode m_mode { Same };
399};
400
401static OwnPtr<Condition> parse_complex_expression(char* argv[]);
402
403static bool should_treat_expression_as_single_string(StringView arg_after)
404{
405 return arg_after.is_null() || arg_after == "-a" || arg_after == "-o";
406}
407
408static OwnPtr<Condition> parse_simple_expression(char* argv[])
409{
410 StringView arg { argv[optind], strlen(argv[optind]) };
411 if (arg.is_null()) {
412 return {};
413 }
414
415 if (arg == "(") {
416 optind++;
417 auto command = parse_complex_expression(argv);
418 if (command && argv[optind]) {
419 auto const* next_option = argv[++optind];
420 if (StringView { next_option, strlen(next_option) } == ")")
421 return command;
422 }
423
424 fatal_error("Unmatched \033[1m(");
425 }
426
427 // Try to read a unary op.
428 if (arg.starts_with('-') && arg.length() == 2) {
429 if (argv[++optind] == nullptr)
430 fatal_error("expected an argument");
431 if (should_treat_expression_as_single_string({ argv[optind], strlen(argv[optind]) })) {
432 --optind;
433 return make<StringCompare>(move(arg), ""sv, StringCompare::NotEqual);
434 }
435
436 StringView value { argv[optind], strlen(argv[optind]) };
437 switch (arg[1]) {
438 case 'b':
439 return make<FileIsOfKind>(value, FileIsOfKind::BlockDevice);
440 case 'c':
441 return make<FileIsOfKind>(value, FileIsOfKind::CharacterDevice);
442 case 'd':
443 return make<FileIsOfKind>(value, FileIsOfKind::Directory);
444 case 'f':
445 return make<FileIsOfKind>(value, FileIsOfKind::Regular);
446 case 'h':
447 case 'L':
448 return make<FileIsOfKind>(value, FileIsOfKind::SymbolicLink);
449 case 'p':
450 return make<FileIsOfKind>(value, FileIsOfKind::FIFO);
451 case 'S':
452 return make<FileIsOfKind>(value, FileIsOfKind::Socket);
453 case 'r':
454 return make<UserHasPermission>(value, UserHasPermission::Read);
455 case 'w':
456 return make<UserHasPermission>(value, UserHasPermission::Write);
457 case 'x':
458 return make<UserHasPermission>(value, UserHasPermission::Execute);
459 case 'e':
460 return make<UserHasPermission>(value, UserHasPermission::Any);
461 case 'g':
462 return make<FileHasFlag>(value, FileHasFlag::SGID);
463 case 'k':
464 return make<FileHasFlag>(value, FileHasFlag::SVTX);
465 case 'u':
466 return make<FileHasFlag>(value, FileHasFlag::SUID);
467 case 'o':
468 case 'a':
469 // '-a' and '-o' are boolean ops, which are part of a complex expression
470 // so we have nothing to parse, simply return to caller.
471 --optind;
472 return {};
473 case 'n':
474 return make<StringCompare>(""sv, value, StringCompare::NotEqual);
475 case 'z':
476 return make<StringCompare>(""sv, value, StringCompare::Equal);
477 case 'G':
478 return make<FileIsOwnedBy>(value, FileIsOwnedBy::EffectiveGID);
479 case 'O':
480 return make<FileIsOwnedBy>(value, FileIsOwnedBy::EffectiveUID);
481 case 'N':
482 case 's':
483 // 'optind' has been incremented to refer to the argument after the
484 // operator, while we want to print the operator itself.
485 fatal_error("Unsupported operator \033[1m%s", argv[optind - 1]);
486 default:
487 --optind;
488 break;
489 }
490 }
491
492 auto get_next_arg = [&argv]() -> StringView {
493 auto const* next_arg = argv[++optind];
494 if (next_arg == NULL)
495 return StringView {};
496 return StringView { next_arg, strlen(next_arg) };
497 };
498
499 // Try to read a binary op, this is either a <string> op <string>, <integer> op <integer>, or <file> op <file>.
500 auto lhs = arg;
501 arg = get_next_arg();
502
503 if (arg == "=") {
504 StringView rhs = get_next_arg();
505 return make<StringCompare>(lhs, rhs, StringCompare::Equal);
506 } else if (arg == "!=") {
507 StringView rhs = get_next_arg();
508 return make<StringCompare>(lhs, rhs, StringCompare::NotEqual);
509 } else if (arg == "-eq") {
510 StringView rhs = get_next_arg();
511 return make<NumericCompare>(lhs, rhs, NumericCompare::Equal);
512 } else if (arg == "-ge") {
513 StringView rhs = get_next_arg();
514 return make<NumericCompare>(lhs, rhs, NumericCompare::GreaterOrEqual);
515 } else if (arg == "-gt") {
516 StringView rhs = get_next_arg();
517 return make<NumericCompare>(lhs, rhs, NumericCompare::Greater);
518 } else if (arg == "-le") {
519 StringView rhs = get_next_arg();
520 return make<NumericCompare>(lhs, rhs, NumericCompare::LessOrEqual);
521 } else if (arg == "-lt") {
522 StringView rhs = get_next_arg();
523 return make<NumericCompare>(lhs, rhs, NumericCompare::Less);
524 } else if (arg == "-ne") {
525 StringView rhs = get_next_arg();
526 return make<NumericCompare>(lhs, rhs, NumericCompare::NotEqual);
527 } else if (arg == "-ef") {
528 StringView rhs = get_next_arg();
529 return make<FileCompare>(lhs, rhs, FileCompare::Same);
530 } else if (arg == "-nt") {
531 StringView rhs = get_next_arg();
532 return make<FileCompare>(lhs, rhs, FileCompare::ModificationTimestampGreater);
533 } else if (arg == "-ot") {
534 StringView rhs = get_next_arg();
535 return make<FileCompare>(lhs, rhs, FileCompare::ModificationTimestampLess);
536 } else if (arg == "-o" || arg == "-a") {
537 // '-a' and '-o' are boolean ops, which are part of a complex expression
538 // put them back and return with lhs as string compare.
539 --optind;
540 return make<StringCompare>(""sv, lhs, StringCompare::NotEqual);
541 } else {
542 // Now that we know it's not a well-formed expression, see if it's actually a negation
543 if (lhs == "!") {
544 if (should_treat_expression_as_single_string(arg))
545 return make<StringCompare>(move(lhs), ""sv, StringCompare::NotEqual);
546
547 auto command = parse_complex_expression(argv);
548 if (!command)
549 fatal_error("Expected an expression after \x1b[1m!");
550
551 return make<Not>(command.release_nonnull());
552 }
553 --optind;
554 return make<StringCompare>(""sv, lhs, StringCompare::NotEqual);
555 }
556}
557
558static OwnPtr<Condition> parse_complex_expression(char* argv[])
559{
560 auto command = parse_simple_expression(argv);
561
562 while (argv[optind] && argv[optind + 1]) {
563 if (!command && argv[optind])
564 fatal_error("expected an expression");
565
566 auto const* arg_ptr = argv[++optind];
567 StringView arg { arg_ptr, strlen(arg_ptr) };
568
569 enum {
570 AndOp,
571 OrOp,
572 } binary_operation { AndOp };
573
574 if (arg == "-a") {
575 if (argv[++optind] == nullptr)
576 fatal_error("expected an expression");
577 binary_operation = AndOp;
578 } else if (arg == "-o") {
579 if (argv[++optind] == nullptr)
580 fatal_error("expected an expression");
581 binary_operation = OrOp;
582 } else {
583 // Ooops, looked too far.
584 optind--;
585 return command;
586 }
587 auto rhs = parse_complex_expression(argv);
588 if (!rhs)
589 fatal_error("Missing right-hand side");
590
591 if (binary_operation == AndOp)
592 command = make<And>(command.release_nonnull(), rhs.release_nonnull());
593 else
594 command = make<Or>(command.release_nonnull(), rhs.release_nonnull());
595 }
596
597 return command;
598}
599
600ErrorOr<int> serenity_main(Main::Arguments arguments)
601{
602 auto maybe_error = Core::System::pledge("stdio rpath");
603 if (maybe_error.is_error()) {
604 warnln("{}", maybe_error.error());
605 return 126;
606 }
607
608 int argc = arguments.argc;
609 if (LexicalPath::basename(arguments.strings[0]) == "[") {
610 --argc;
611 if (StringView { arguments.strings[argc] } != "]")
612 fatal_error("test invoked as '[' requires a closing bracket ']'");
613 arguments.strings[argc] = {};
614 }
615
616 // Exit false when no arguments are given.
617 if (argc == 1)
618 return 1;
619
620 auto condition = parse_complex_expression(arguments.argv);
621 if (optind != argc - 1)
622 fatal_error("Too many arguments");
623 auto result = condition ? condition->check() : false;
624
625 if (g_there_was_an_error)
626 return 126;
627 return result ? 0 : 1;
628}