/* * Copyright (c) 2020-2022, the SerenityOS developers. * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #include #include #include #include #include bool g_there_was_an_error = false; [[noreturn, gnu::format(printf, 1, 2)]] static void fatal_error(char const* format, ...) { fputs("\033[31m", stderr); va_list ap; va_start(ap, format); vfprintf(stderr, format, ap); va_end(ap); fputs("\033[0m\n", stderr); exit(126); } class Condition { public: virtual ~Condition() = default; virtual bool check() const = 0; }; class And : public Condition { public: And(NonnullOwnPtr lhs, NonnullOwnPtr rhs) : m_lhs(move(lhs)) , m_rhs(move(rhs)) { } private: virtual bool check() const override { return m_lhs->check() && m_rhs->check(); } NonnullOwnPtr m_lhs; NonnullOwnPtr m_rhs; }; class Or : public Condition { public: Or(NonnullOwnPtr lhs, NonnullOwnPtr rhs) : m_lhs(move(lhs)) , m_rhs(move(rhs)) { } private: virtual bool check() const override { return m_lhs->check() || m_rhs->check(); } NonnullOwnPtr m_lhs; NonnullOwnPtr m_rhs; }; class Not : public Condition { public: Not(NonnullOwnPtr cond) : m_cond(move(cond)) { } private: virtual bool check() const override { return !m_cond->check(); } NonnullOwnPtr m_cond; }; class FileIsOfKind : public Condition { public: enum Kind { BlockDevice, CharacterDevice, Directory, FIFO, Regular, Socket, SymbolicLink, }; FileIsOfKind(StringView path, Kind kind) : m_path(path) , m_kind(kind) { } private: virtual bool check() const override { struct stat statbuf; int rc; if (m_kind == SymbolicLink) rc = stat(m_path.characters(), &statbuf); else rc = lstat(m_path.characters(), &statbuf); if (rc < 0) { if (errno != ENOENT) { perror(m_path.characters()); g_there_was_an_error = true; } return false; } switch (m_kind) { case BlockDevice: return S_ISBLK(statbuf.st_mode); case CharacterDevice: return S_ISCHR(statbuf.st_mode); case Directory: return S_ISDIR(statbuf.st_mode); case FIFO: return S_ISFIFO(statbuf.st_mode); case Regular: return S_ISREG(statbuf.st_mode); case Socket: return S_ISSOCK(statbuf.st_mode); case SymbolicLink: return S_ISLNK(statbuf.st_mode); default: VERIFY_NOT_REACHED(); } } String m_path; Kind m_kind { Regular }; }; class UserHasPermission : public Condition { public: enum Permission { Any, Read, Write, Execute, }; UserHasPermission(StringView path, Permission kind) : m_path(path) , m_kind(kind) { } private: virtual bool check() const override { switch (m_kind) { case Read: return access(m_path.characters(), R_OK) == 0; case Write: return access(m_path.characters(), W_OK) == 0; case Execute: return access(m_path.characters(), X_OK) == 0; case Any: return access(m_path.characters(), F_OK) == 0; default: VERIFY_NOT_REACHED(); } } String m_path; Permission m_kind { Read }; }; class StringCompare : public Condition { public: enum Mode { Equal, NotEqual, }; StringCompare(StringView lhs, StringView rhs, Mode mode) : m_lhs(move(lhs)) , m_rhs(move(rhs)) , m_mode(mode) { } private: virtual bool check() const override { if (m_mode == Equal) return m_lhs == m_rhs; return m_lhs != m_rhs; } StringView m_lhs; StringView m_rhs; Mode m_mode { Equal }; }; class NumericCompare : public Condition { public: enum Mode { Equal, Greater, GreaterOrEqual, Less, LessOrEqual, NotEqual, }; NumericCompare(String lhs, String rhs, Mode mode) : m_mode(mode) { auto lhs_option = lhs.trim_whitespace().to_int(); auto rhs_option = rhs.trim_whitespace().to_int(); if (!lhs_option.has_value()) fatal_error("expected integer expression: '%s'", lhs.characters()); if (!rhs_option.has_value()) fatal_error("expected integer expression: '%s'", rhs.characters()); m_lhs = lhs_option.value(); m_rhs = rhs_option.value(); } private: virtual bool check() const override { switch (m_mode) { case Equal: return m_lhs == m_rhs; case Greater: return m_lhs > m_rhs; case GreaterOrEqual: return m_lhs >= m_rhs; case Less: return m_lhs < m_rhs; case LessOrEqual: return m_lhs <= m_rhs; case NotEqual: return m_lhs != m_rhs; default: VERIFY_NOT_REACHED(); } } int m_lhs { 0 }; int m_rhs { 0 }; Mode m_mode { Equal }; }; class FileCompare : public Condition { public: enum Mode { Same, ModificationTimestampGreater, ModificationTimestampLess, }; FileCompare(String lhs, String rhs, Mode mode) : m_lhs(move(lhs)) , m_rhs(move(rhs)) , m_mode(mode) { } private: virtual bool check() const override { struct stat statbuf_l; int rc = stat(m_lhs.characters(), &statbuf_l); if (rc < 0) { perror(m_lhs.characters()); g_there_was_an_error = true; return false; } struct stat statbuf_r; rc = stat(m_rhs.characters(), &statbuf_r); if (rc < 0) { perror(m_rhs.characters()); g_there_was_an_error = true; return false; } switch (m_mode) { case Same: return statbuf_l.st_dev == statbuf_r.st_dev && statbuf_l.st_ino == statbuf_r.st_ino; case ModificationTimestampLess: return statbuf_l.st_mtime < statbuf_r.st_mtime; case ModificationTimestampGreater: return statbuf_l.st_mtime > statbuf_r.st_mtime; default: VERIFY_NOT_REACHED(); } } String m_lhs; String m_rhs; Mode m_mode { Same }; }; static OwnPtr parse_complex_expression(char* argv[]); static bool should_treat_expression_as_single_string(StringView arg_after) { return arg_after.is_null() || arg_after == "-a" || arg_after == "-o"; } static OwnPtr parse_simple_expression(char* argv[]) { StringView arg = argv[optind]; if (arg.is_null()) { return {}; } if (arg == "(") { optind++; auto command = parse_complex_expression(argv); if (command && argv[optind] && StringView(argv[++optind]) == ")") return command; fatal_error("Unmatched \033[1m("); } // Try to read a unary op. if (arg.starts_with('-') && arg.length() == 2) { optind++; if (should_treat_expression_as_single_string(argv[optind])) { --optind; return make(move(arg), "", StringCompare::NotEqual); } StringView value = argv[optind]; switch (arg[1]) { case 'b': return make(value, FileIsOfKind::BlockDevice); case 'c': return make(value, FileIsOfKind::CharacterDevice); case 'd': return make(value, FileIsOfKind::Directory); case 'f': return make(value, FileIsOfKind::Regular); case 'h': case 'L': return make(value, FileIsOfKind::SymbolicLink); case 'p': return make(value, FileIsOfKind::FIFO); case 'S': return make(value, FileIsOfKind::Socket); case 'r': return make(value, UserHasPermission::Read); case 'w': return make(value, UserHasPermission::Write); case 'x': return make(value, UserHasPermission::Execute); case 'e': return make(value, UserHasPermission::Any); case 'o': case 'a': // '-a' and '-o' are boolean ops, which are part of a complex expression // so we have nothing to parse, simply return to caller. --optind; return {}; case 'n': return make("", value, StringCompare::NotEqual); case 'z': return make("", value, StringCompare::Equal); case 'g': case 'G': case 'k': case 'N': case 'O': case 's': fatal_error("Unsupported operator \033[1m%s", argv[optind]); default: --optind; break; } } // Try to read a binary op, this is either a op , op , or op . auto lhs = arg; arg = argv[++optind]; if (arg == "=") { StringView rhs = argv[++optind]; return make(lhs, rhs, StringCompare::Equal); } else if (arg == "!=") { StringView rhs = argv[++optind]; return make(lhs, rhs, StringCompare::NotEqual); } else if (arg == "-eq") { StringView rhs = argv[++optind]; return make(lhs, rhs, NumericCompare::Equal); } else if (arg == "-ge") { StringView rhs = argv[++optind]; return make(lhs, rhs, NumericCompare::GreaterOrEqual); } else if (arg == "-gt") { StringView rhs = argv[++optind]; return make(lhs, rhs, NumericCompare::Greater); } else if (arg == "-le") { StringView rhs = argv[++optind]; return make(lhs, rhs, NumericCompare::LessOrEqual); } else if (arg == "-lt") { StringView rhs = argv[++optind]; return make(lhs, rhs, NumericCompare::Less); } else if (arg == "-ne") { StringView rhs = argv[++optind]; return make(lhs, rhs, NumericCompare::NotEqual); } else if (arg == "-ef") { StringView rhs = argv[++optind]; return make(lhs, rhs, FileCompare::Same); } else if (arg == "-nt") { StringView rhs = argv[++optind]; return make(lhs, rhs, FileCompare::ModificationTimestampGreater); } else if (arg == "-ot") { StringView rhs = argv[++optind]; return make(lhs, rhs, FileCompare::ModificationTimestampLess); } else if (arg == "-o" || arg == "-a") { // '-a' and '-o' are boolean ops, which are part of a complex expression // put them back and return with lhs as string compare. --optind; return make("", lhs, StringCompare::NotEqual); } else { // Now that we know it's not a well-formed expression, see if it's actually a negation if (lhs == "!") { if (should_treat_expression_as_single_string(arg)) return make(move(lhs), "", StringCompare::NotEqual); auto command = parse_complex_expression(argv); if (!command) fatal_error("Expected an expression after \x1b[1m!"); return make(command.release_nonnull()); } --optind; return make("", lhs, StringCompare::NotEqual); } } static OwnPtr parse_complex_expression(char* argv[]) { auto command = parse_simple_expression(argv); while (argv[optind] && argv[optind + 1]) { if (!command && argv[optind]) fatal_error("expected an expression"); StringView arg = argv[++optind]; enum { AndOp, OrOp, } binary_operation { AndOp }; if (arg == "-a") { optind++; binary_operation = AndOp; } else if (arg == "-o") { optind++; binary_operation = OrOp; } else { // Ooops, looked too far. optind--; return command; } auto rhs = parse_complex_expression(argv); if (!rhs) fatal_error("Missing right-hand side"); if (binary_operation == AndOp) command = make(command.release_nonnull(), rhs.release_nonnull()); else command = make(command.release_nonnull(), rhs.release_nonnull()); } return command; } ErrorOr serenity_main(Main::Arguments arguments) { auto maybe_error = Core::System::pledge("stdio rpath"); if (maybe_error.is_error()) { warnln("{}", maybe_error.error()); return 126; } int argc = arguments.argc; if (LexicalPath::basename(arguments.strings[0]) == "[") { --argc; if (StringView { arguments.strings[argc] } != "]") fatal_error("test invoked as '[' requires a closing bracket ']'"); arguments.strings[argc] = nullptr; } // Exit false when no arguments are given. if (argc == 1) return 1; auto condition = parse_complex_expression(arguments.argv); if (optind != argc - 1) fatal_error("Too many arguments"); auto result = condition ? condition->check() : false; if (g_there_was_an_error) return 126; return result ? 0 : 1; }