/* * Copyright (c) 2020-2021, Sergey Bugaev * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include bool g_follow_symlinks = false; bool g_there_was_an_error = false; bool g_have_seen_action_command = false; bool g_print_hyperlinks = false; Optional g_max_depth = {}; Optional g_min_depth = {}; template [[noreturn]] static void fatal_error(CheckedFormatString&& fmtstr, Parameters const&... parameters) { warn("\033[31m"); warn(move(fmtstr), parameters...); warn("\033[0m"); warnln(); exit(1); } struct FileData { // The current path specified on the command line StringView root_path; // Path to the file relative to the current root path LexicalPath relative_path; // The parent directory of the file. int dirfd { -1 }; // The file's basename, relative to the directory. char const* basename { nullptr }; // Optionally, cached information as returned by stat/lstat/fstatat. struct stat stat { }; bool stat_is_valid : 1 { false }; // File type as returned from readdir(), or DT_UNKNOWN. unsigned char d_type { DT_UNKNOWN }; ByteString full_path() const { if (root_path.is_empty()) return relative_path.string(); if (relative_path.string() == ".") return root_path; // POSIX says that a single slash should be added between the root path and the relative portion if the root path doesn't end in one // any additional trailing slashes should be left unaltered. if (root_path.ends_with('/')) return ByteString::formatted("{}{}", root_path, relative_path.string()); return ByteString::formatted("{}/{}", root_path, relative_path.string()); } const struct stat* ensure_stat() { if (stat_is_valid) return &stat; int flags = g_follow_symlinks ? 0 : AT_SYMLINK_NOFOLLOW; int rc = fstatat(dirfd, basename, &stat, flags); if (rc < 0) { perror(full_path().characters()); g_there_was_an_error = true; return nullptr; } stat_is_valid = true; if (S_ISREG(stat.st_mode)) d_type = DT_REG; else if (S_ISDIR(stat.st_mode)) d_type = DT_DIR; else if (S_ISCHR(stat.st_mode)) d_type = DT_CHR; else if (S_ISBLK(stat.st_mode)) d_type = DT_BLK; else if (S_ISFIFO(stat.st_mode)) d_type = DT_FIFO; else if (S_ISLNK(stat.st_mode)) d_type = DT_LNK; else if (S_ISSOCK(stat.st_mode)) d_type = DT_SOCK; else VERIFY_NOT_REACHED(); return &stat; } }; template class NumericRange { public: NumericRange() = default; static ErrorOr> parse(StringView arg) { if (arg.is_empty()) return Error::from_errno(EINVAL); auto comparison_type = ComparisonType::Equal; if (arg[0] == '-') { comparison_type = ComparisonType::LessThan; arg = arg.substring_view(1); } else if (arg[0] == '+') { comparison_type = ComparisonType::GreaterThan; arg = arg.substring_view(1); } auto maybe_value = arg.to_number(); if (!maybe_value.has_value()) return Error::from_errno(EINVAL); return NumericRange(maybe_value.release_value(), comparison_type); } bool contains(T other) const { if (m_comparison_type == ComparisonType::LessThan) return other < m_value; if (m_comparison_type == ComparisonType::GreaterThan) return other > m_value; return other == m_value; } private: enum class ComparisonType { Equal, LessThan, GreaterThan }; NumericRange(T value, ComparisonType comparison_type) : m_value(value) , m_comparison_type(comparison_type) { } T m_value {}; ComparisonType m_comparison_type { ComparisonType::Equal }; }; class Command { public: virtual ~Command() = default; virtual bool evaluate(FileData& file_data) const = 0; }; class StatCommand : public Command { public: virtual bool evaluate(const struct stat&) const = 0; private: virtual bool evaluate(FileData& file_data) const override { const struct stat* stat = file_data.ensure_stat(); if (!stat) return false; return evaluate(*stat); } }; class MaxDepthCommand : public Command { public: MaxDepthCommand(char const* arg) { auto max_depth_string = StringView { arg, strlen(arg) }; auto maybe_max_depth = max_depth_string.to_number(); if (!maybe_max_depth.has_value()) fatal_error("-maxdepth: '{}' is not a valid non-negative integer", arg); g_max_depth = maybe_max_depth.value(); } virtual bool evaluate(FileData&) const override { return true; } }; class MinDepthCommand : public Command { public: MinDepthCommand(char const* arg) { auto min_depth_string = StringView { arg, strlen(arg) }; auto maybe_min_depth = min_depth_string.to_number(); if (!maybe_min_depth.has_value()) fatal_error("-mindepth: '{}' is not a valid non-negative integer", arg); g_min_depth = maybe_min_depth.value(); } virtual bool evaluate(FileData&) const override { return true; } }; class TypeCommand final : public Command { public: TypeCommand(char const* arg) { StringView type { arg, strlen(arg) }; if (type.length() != 1 || !"bcdlpfs"sv.contains(type[0])) fatal_error("Invalid mode: \033[1m{}", arg); m_type = type[0]; } private: virtual bool evaluate(FileData& file_data) const override { // First, make sure we have a type, but avoid calling // sys$stat() unless we need to. if (file_data.d_type == DT_UNKNOWN) { if (file_data.ensure_stat() == nullptr) return false; } auto type = file_data.d_type; switch (m_type) { case 'b': return type == DT_BLK; case 'c': return type == DT_CHR; case 'd': return type == DT_DIR; case 'l': return type == DT_LNK; case 'p': return type == DT_FIFO; case 'f': return type == DT_REG; case 's': return type == DT_SOCK; default: // We've verified this is a correct character before. VERIFY_NOT_REACHED(); } } char m_type { 0 }; }; class LinksCommand final : public StatCommand { public: LinksCommand(char const* arg) { StringView arg_string { arg, strlen(arg) }; auto links_or_error = NumericRange::parse(arg_string); if (links_or_error.is_error()) fatal_error("Invalid number: \033[1m{}", arg); m_links = links_or_error.release_value(); } private: virtual bool evaluate(const struct stat& stat) const override { return m_links.contains(stat.st_nlink); } NumericRange m_links {}; }; class UserCommand final : public StatCommand { public: UserCommand(char const* arg) { if (struct passwd* passwd = getpwnam(arg)) { m_uid = passwd->pw_uid; } else { // Attempt to parse it as decimal UID. auto number = StringView { arg, strlen(arg) }.to_number(); if (!number.has_value()) fatal_error("Invalid user: \033[1m{}", arg); m_uid = number.value(); } } private: virtual bool evaluate(const struct stat& stat) const override { return stat.st_uid == m_uid; } uid_t m_uid { 0 }; }; class GroupCommand final : public StatCommand { public: GroupCommand(char const* arg) { if (struct group* gr = getgrnam(arg)) { m_gid = gr->gr_gid; } else { // Attempt to parse it as decimal GID. auto number = StringView { arg, strlen(arg) }.to_number(); if (!number.has_value()) fatal_error("Invalid group: \033[1m{}", arg); m_gid = number.value(); } } private: virtual bool evaluate(const struct stat& stat) const override { return stat.st_gid == m_gid; } gid_t m_gid { 0 }; }; class SizeCommand final : public StatCommand { public: SizeCommand(char const* arg) { StringView view { arg, strlen(arg) }; auto suffix = view[view.length() - 1]; if (!is_ascii_digit(suffix)) { switch (suffix) { case 'c': m_unit_size = 1; break; case 'w': m_unit_size = 2; break; case 'k': m_unit_size = KiB; break; case 'M': m_unit_size = MiB; break; case 'G': m_unit_size = GiB; break; case 'b': // The behavior of this suffix is the same as no suffix. break; default: fatal_error("Invalid -size type '{}'", suffix); } view = view.substring_view(0, view.length() - 1); } auto number_or_error = NumericRange::parse(view); if (number_or_error.is_error()) fatal_error("Invalid size: \033[1m{}", arg); m_number_of_units = number_or_error.release_value(); } private: virtual bool evaluate(const struct stat& stat) const override { if (m_unit_size == 1) return m_number_of_units.contains(stat.st_size); auto size_divided_by_unit_rounded_up = (stat.st_size + m_unit_size - 1) / m_unit_size; return m_number_of_units.contains(size_divided_by_unit_rounded_up); } NumericRange m_number_of_units {}; u64 m_unit_size { 512 }; }; class EmptyCommand final : public Command { public: EmptyCommand() { } private: virtual bool evaluate(FileData& file_data) const override { struct stat const* stat = file_data.ensure_stat(); if (!stat) return false; if (S_ISREG(stat->st_mode)) return stat->st_size == 0; if (S_ISDIR(stat->st_mode)) { auto dir_iterator = Core::DirIterator(file_data.full_path(), Core::DirIterator::SkipDots); return !dir_iterator.has_next(); } return false; } }; class PathCommand : public Command { public: enum class PathPart { FullPath, Basename }; PathCommand(char const* pattern, CaseSensitivity case_sensitivity, PathPart path_part) : m_pattern(pattern, strlen(pattern)) , m_case_sensitivity(case_sensitivity) , m_path_part(path_part) { if (path_part == PathPart::FullPath && m_pattern.ends_with('/')) warnln("find: warning: path command will not match anything because it ends with '/'."); } private: virtual bool evaluate(FileData& file_data) const override { if (m_path_part == PathPart::Basename) return file_data.relative_path.basename().matches(m_pattern, m_case_sensitivity); return file_data.full_path().matches(m_pattern, m_case_sensitivity); } StringView m_pattern; CaseSensitivity m_case_sensitivity { CaseSensitivity::CaseSensitive }; PathPart m_path_part { PathPart::FullPath }; }; class RegexCommand : public Command { public: RegexCommand(Regex&& regex) : m_regex(move(regex)) { } private: virtual bool evaluate(FileData& file_data) const override { auto haystack = file_data.full_path(); auto match_result = m_regex.match(haystack); return match_result.success; } Regex m_regex; }; class AccessCommand final : public Command { public: AccessCommand(mode_t mode) : m_mode(mode) { } private: virtual bool evaluate(FileData& file_data) const override { auto maybe_error = Core::System::access(file_data.full_path(), m_mode); return !maybe_error.is_error(); } mode_t m_mode { 0 }; }; class NewerCommand final : public StatCommand { public: enum class TimestampType { LastAccess, Creation, LastModification }; NewerCommand(char const* arg, TimestampType timestamp_type) { auto stat_function = g_follow_symlinks ? Core::System::stat : Core::System::lstat; auto stat_or_error = stat_function({ arg, strlen(arg) }); if (stat_or_error.is_error()) fatal_error("find: '{}': {}", arg, strerror(stat_or_error.error().code())); m_reference_file_stat = stat_or_error.release_value(); m_timestamp_type = timestamp_type; } private: virtual bool evaluate(struct stat const& stat) const override { struct timespec current_file_timestamp; struct timespec reference_file_timestamp; switch (m_timestamp_type) { case TimestampType::LastAccess: current_file_timestamp = stat.st_atim; reference_file_timestamp = m_reference_file_stat.st_atim; break; case TimestampType::Creation: current_file_timestamp = stat.st_ctim; reference_file_timestamp = m_reference_file_stat.st_ctim; break; case TimestampType::LastModification: current_file_timestamp = stat.st_mtim; reference_file_timestamp = m_reference_file_stat.st_mtim; break; } return Duration::from_timespec(current_file_timestamp) > Duration::from_timespec(reference_file_timestamp); } struct stat m_reference_file_stat; TimestampType m_timestamp_type { TimestampType::LastModification }; }; class GidCommand final : public StatCommand { public: GidCommand(char const* arg) { auto gid_or_error = NumericRange::parse({ arg, strlen(arg) }); if (gid_or_error.is_error()) fatal_error("find: Invalid argument '{}' to '-gid'", arg); m_gid_range = gid_or_error.release_value(); } private: virtual bool evaluate(struct stat const& stat) const override { return m_gid_range.contains(stat.st_gid); } NumericRange m_gid_range {}; }; class UidCommand final : public StatCommand { public: UidCommand(char const* arg) { auto uid_or_error = NumericRange::parse({ arg, strlen(arg) }); if (uid_or_error.is_error()) fatal_error("find: Invalid argument '{}' to '-uid'", arg); m_uid_range = uid_or_error.release_value(); } private: virtual bool evaluate(struct stat const& stat) const override { return m_uid_range.contains(stat.st_uid); } NumericRange m_uid_range {}; }; class PrintCommand final : public Command { public: PrintCommand(char terminator = '\n') : m_terminator(terminator) { } private: virtual bool evaluate(FileData& file_data) const override { auto printed = false; if (g_print_hyperlinks) { auto full_path_or_error = FileSystem::real_path(file_data.full_path()); if (!full_path_or_error.is_error()) { auto fullpath = full_path_or_error.release_value(); auto url = URL::create_with_file_scheme(fullpath); out("\033]8;;{}\033\\{}{}\033]8;;\033\\", url.serialize(), file_data.full_path(), m_terminator); printed = true; } } if (!printed) out("{}{}", file_data.full_path(), m_terminator); return true; } char m_terminator { '\n' }; }; class ExecCommand final : public Command { public: enum class AwaitConfirmation { Yes, No }; ExecCommand(Vector&& argv, AwaitConfirmation await_confirmation = AwaitConfirmation::No) : m_argv(move(argv)) , m_await_confirmation(await_confirmation) { } private: virtual bool evaluate(FileData& file_data) const override { // Replace any occurrences of "{}" with the path. Vector full_paths; Vector argv = m_argv; for (auto& arg : argv) { if (StringView { arg, strlen(arg) } == "{}") { auto full_path = file_data.full_path(); full_paths.append(full_path); arg = const_cast(full_path.characters()); } } if (m_await_confirmation == AwaitConfirmation::Yes) { out(stderr, "\""); bool first = true; for (auto const& arg : argv) { if (!first) out(stderr, " "); out(stderr, "{}", arg); first = false; } out(stderr, "\"? "); fflush(stderr); Array buffer; auto nread_or_error = Core::System::read(STDIN_FILENO, buffer); if (nread_or_error.is_error()) { warn("Failed to read from stdin: {}", strerror(nread_or_error.error().code())); return false; } auto nread = nread_or_error.release_value(); if (nread <= 0 || (buffer[0] != 'y' && buffer[0] != 'Y')) return false; } pid_t pid = fork(); if (pid < 0) { perror("fork"); g_there_was_an_error = true; return false; } else if (pid == 0) { argv.append(nullptr); execvp(m_argv[0], argv.data()); perror("execvp"); exit(1); } else { int status; int rc = waitpid(pid, &status, 0); if (rc < 0) { perror("waitpid"); g_there_was_an_error = true; return false; } return WIFEXITED(status) && WEXITSTATUS(status) == 0; } } Vector m_argv; AwaitConfirmation m_await_confirmation { AwaitConfirmation::No }; }; class AndCommand final : public Command { public: AndCommand(NonnullOwnPtr&& lhs, NonnullOwnPtr&& rhs) : m_lhs(move(lhs)) , m_rhs(move(rhs)) { } private: virtual bool evaluate(FileData& file_data) const override { return m_lhs->evaluate(file_data) && m_rhs->evaluate(file_data); } NonnullOwnPtr m_lhs; NonnullOwnPtr m_rhs; }; class OrCommand final : public Command { public: OrCommand(NonnullOwnPtr&& lhs, NonnullOwnPtr&& rhs) : m_lhs(move(lhs)) , m_rhs(move(rhs)) { } private: virtual bool evaluate(FileData& file_data) const override { return m_lhs->evaluate(file_data) || m_rhs->evaluate(file_data); } NonnullOwnPtr m_lhs; NonnullOwnPtr m_rhs; }; class NotCommand final : public Command { public: NotCommand(NonnullOwnPtr&& operand) : m_operand(move(operand)) { } private: virtual bool evaluate(FileData& file_data) const override { return !m_operand->evaluate(file_data); } NonnullOwnPtr m_operand; }; static OwnPtr parse_complex_command(Vector& args); // Parse a simple command starting at optind; leave optind at its the last // argument. Return nullptr if we reach the end of arguments. static OwnPtr parse_simple_command(Vector& args) { if (args.is_empty()) return {}; char* raw_arg = args.take_first(); StringView arg { raw_arg, strlen(raw_arg) }; if (arg == "(") { auto command = parse_complex_command(args); if (command && !args.is_empty() && StringView { args.first(), strlen(args.first()) } == ")") return command; fatal_error("Unmatched \033[1m("); } else if (arg == "!") { if (args.is_empty()) fatal_error("Expected an expression after '!'"); auto command = parse_simple_command(args).release_nonnull(); return make(move(command)); } else if (arg == "-maxdepth"sv) { if (args.is_empty()) fatal_error("-maxdepth: requires additional arguments"); return make(args.take_first()); } else if (arg == "-mindepth"sv) { if (args.is_empty()) fatal_error("-mindepth: requires additional arguments"); return make(args.take_first()); } else if (arg == "-type") { if (args.is_empty()) fatal_error("-type: requires additional arguments"); return make(args.take_first()); } else if (arg == "-links") { if (args.is_empty()) fatal_error("-links: requires additional arguments"); return make(args.take_first()); } else if (arg == "-user") { if (args.is_empty()) fatal_error("-user: requires additional arguments"); return make(args.take_first()); } else if (arg == "-group") { if (args.is_empty()) fatal_error("-group: requires additional arguments"); return make(args.take_first()); } else if (arg == "-size") { if (args.is_empty()) fatal_error("-size: requires additional arguments"); return make(args.take_first()); } else if (arg == "-empty") { return make(); } else if (arg == "-path") { if (args.is_empty()) fatal_error("-path: requires additional arguments"); return make(args.take_first(), CaseSensitivity::CaseSensitive, PathCommand::PathPart::FullPath); } else if (arg == "-ipath") { if (args.is_empty()) fatal_error("-ipath: requires additional arguments"); return make(args.take_first(), CaseSensitivity::CaseInsensitive, PathCommand::PathPart::FullPath); } else if (arg == "-name") { if (args.is_empty()) fatal_error("-name: requires additional arguments"); return make(args.take_first(), CaseSensitivity::CaseSensitive, PathCommand::PathPart::Basename); } else if (arg == "-iname") { if (args.is_empty()) fatal_error("-iname: requires additional arguments"); return make(args.take_first(), CaseSensitivity::CaseInsensitive, PathCommand::PathPart::Basename); } else if (arg == "-regex") { if (args.is_empty()) fatal_error("-regex: requires additional arguments"); Regex regex { args.take_first(), PosixFlags::Default }; if (regex.parser_result.error != regex::Error::NoError) fatal_error("{}", regex.error_string()); return make(move(regex)); } else if (arg == "-iregex") { if (args.is_empty()) fatal_error("-iregex: requires additional arguments"); Regex regex { args.take_first(), PosixFlags::Insensitive }; if (regex.parser_result.error != regex::Error::NoError) fatal_error("{}", regex.error_string()); return make(move(regex)); } else if (arg == "-readable") { return make(R_OK); } else if (arg == "-writable") { return make(W_OK); } else if (arg == "-executable") { return make(X_OK); } else if (arg == "-newer") { if (args.is_empty()) fatal_error("-newer: requires additional arguments"); return make(args.take_first(), NewerCommand::TimestampType::LastModification); } else if (arg == "-anewer") { if (args.is_empty()) fatal_error("-anewer: requires additional arguments"); return make(args.take_first(), NewerCommand::TimestampType::LastAccess); } else if (arg == "-cnewer") { if (args.is_empty()) fatal_error("-cnewer: requires additional arguments"); return make(args.take_first(), NewerCommand::TimestampType::Creation); } else if (arg == "-gid") { if (args.is_empty()) fatal_error("-gid: requires additional arguments"); return make(args.take_first()); } else if (arg == "-uid") { if (args.is_empty()) fatal_error("-uid: requires additional arguments"); return make(args.take_first()); } else if (arg == "-print") { g_have_seen_action_command = true; return make(); } else if (arg == "-print0") { g_have_seen_action_command = true; return make(0); } else if (arg == "-exec" || arg == "-ok") { if (args.is_empty()) fatal_error("{}: requires additional arguments", arg); g_have_seen_action_command = true; Vector command_argv; bool terminator_found = false; while (!args.is_empty()) { char* next = args.take_first(); if (next[0] == ';' && next[1] == '\0') { terminator_found = true; break; } command_argv.append(next); } if (!terminator_found) fatal_error("{}: Terminating ';' not found", arg); auto await_confirmation = (arg == "-ok") ? ExecCommand::AwaitConfirmation::Yes : ExecCommand::AwaitConfirmation::No; return make(move(command_argv), await_confirmation); } else { fatal_error("Unsupported command \033[1m{}", arg); } } static OwnPtr parse_complex_command(Vector& args) { auto command = parse_simple_command(args); while (command && !args.is_empty()) { char* raw_arg = args.take_first(); StringView arg { raw_arg, strlen(raw_arg) }; enum { And, Or, } binary_operation { And }; if (arg == "-a") { binary_operation = And; } else if (arg == "-o") { binary_operation = Or; } else if (arg == ")") { // Ooops, looked too far. args.prepend(raw_arg); return command; } else { // Juxtaposition is an And too, and there's nothing to skip. args.prepend(raw_arg); binary_operation = And; } auto rhs = parse_complex_command(args); if (!rhs) fatal_error("Missing right-hand side"); if (binary_operation == And) command = make(command.release_nonnull(), rhs.release_nonnull()); else command = make(command.release_nonnull(), rhs.release_nonnull()); } return command; } static NonnullOwnPtr parse_all_commands(Vector& args) { auto command = parse_complex_command(args); if (g_have_seen_action_command) { VERIFY(command); return command.release_nonnull(); } if (!command) { return make(); } return make(command.release_nonnull(), make()); } static void walk_tree(FileData& root_data, Command& command, u32 depth = 0) { if (!g_min_depth.has_value() || g_min_depth.value() <= depth) command.evaluate(root_data); // We should try to read directory entries if either: // * This is a directory. // * This is a symlink (that could point to a directory), // and we're following symlinks. // * The type is unknown, so it could be a directory. switch (root_data.d_type) { case DT_DIR: case DT_UNKNOWN: break; case DT_LNK: if (g_follow_symlinks) break; return; default: return; } int dirfd = openat(root_data.dirfd, root_data.basename, O_RDONLY | O_DIRECTORY | O_CLOEXEC); if (dirfd < 0) { if (errno == ENOTDIR) { // Above we decided to try to open this file because it could // be a directory, but turns out it's not. This is fine though. return; } perror(root_data.full_path().characters()); g_there_was_an_error = true; return; } DIR* dir = fdopendir(dirfd); while (true) { errno = 0; auto* dirent = readdir(dir); if (!dirent) break; if (strcmp(dirent->d_name, ".") == 0 || strcmp(dirent->d_name, "..") == 0) continue; FileData file_data { root_data.root_path, root_data.relative_path.append({ dirent->d_name, strlen(dirent->d_name) }), dirfd, dirent->d_name, (struct stat) {}, false, dirent->d_type, }; bool should_increase_depth = false; if (g_max_depth.has_value() || g_min_depth.has_value()) { if (g_max_depth.has_value() && depth >= g_max_depth.value()) return; if (file_data.d_type == DT_UNKNOWN) file_data.ensure_stat(); if (file_data.d_type == DT_DIR) should_increase_depth = true; } walk_tree(file_data, command, should_increase_depth ? depth + 1 : depth); } if (errno != 0) { perror(root_data.full_path().characters()); g_there_was_an_error = true; } closedir(dir); } ErrorOr serenity_main(Main::Arguments arguments) { Vector args; args.append(arguments.argv + 1, arguments.argc - 1); OwnPtr command; Vector paths; while (!args.is_empty()) { char* raw_arg = args.take_first(); StringView arg { raw_arg, strlen(raw_arg) }; if (arg == "-L") { g_follow_symlinks = true; } else if (arg.starts_with('-') || arg == "!"sv) { args.prepend(raw_arg); command = parse_all_commands(args); } else { paths.append(arg); } } g_print_hyperlinks = TRY(Core::System::isatty(STDOUT_FILENO)); if (!command) command = make(); if (paths.is_empty()) paths.append("."sv); for (auto& path : paths) { LexicalPath lexical_path { path }; ByteString dirname = lexical_path.dirname(); ByteString basename = lexical_path.basename(); int dirfd = TRY(Core::System::open(dirname, O_RDONLY | O_DIRECTORY | O_CLOEXEC)); FileData file_data { path, LexicalPath { "" }, dirfd, basename.characters(), (struct stat) {}, false, DT_UNKNOWN, }; walk_tree(file_data, *command); close(dirfd); } return g_there_was_an_error ? 1 : 0; }