diff --git a/userspace/programs/CMakeLists.txt b/userspace/programs/CMakeLists.txt index 2ee8a792..8edcb772 100644 --- a/userspace/programs/CMakeLists.txt +++ b/userspace/programs/CMakeLists.txt @@ -17,6 +17,7 @@ set(USERSPACE_PROGRAMS echo env false + find getopt http-server id diff --git a/userspace/programs/find/CMakeLists.txt b/userspace/programs/find/CMakeLists.txt new file mode 100644 index 00000000..69dc99c4 --- /dev/null +++ b/userspace/programs/find/CMakeLists.txt @@ -0,0 +1,11 @@ +set(SOURCES + main.cpp + Node.cpp + Token.cpp +) + +add_executable(find ${SOURCES}) +banan_link_library(find ban) +banan_link_library(find libc) + +install(TARGETS find OPTIONAL) diff --git a/userspace/programs/find/Node.cpp b/userspace/programs/find/Node.cpp new file mode 100644 index 00000000..0c7a3213 --- /dev/null +++ b/userspace/programs/find/Node.cpp @@ -0,0 +1,433 @@ +#include "Node.h" + +#include +#include +#include +#include +#include + +bool g_mount_specified { false }; +bool g_xdev_specified { false }; +bool g_prune_specified { false }; +bool g_depth_specified { false }; + +static bool s_needs_implicit_print { false }; + +extern const char* g_argv0; + +Node::Node(Type type, decltype(Node::as) as) + : type(type), as(as) +{ + switch (type) + { + case Type::NOD_MOUNT: + g_mount_specified = true; + break; + case Type::NOD_XDEV: + g_xdev_specified = true; + break; + case Type::NOD_PRUNE: + g_prune_specified = true; + break; + case Type::NOD_DEPTH: + g_depth_specified = true; + break; + case Type::NOD_EXEC: + case Type::NOD_OK: + case Type::NOD_PRINT: + case Type::NOD_PRINT0: + s_needs_implicit_print = false; + break; + default: + break; + } +} + +Node::Node(Type type) + : Node(type, {}) +{ } + +static bool token_match(BAN::Span& tokens, Token::Type type) +{ + if (tokens[0].type != type) + return false; + tokens = tokens.slice(1); + return true; +} + +[[noreturn]] static void print_error_and_exit(const char* format, ...) +{ + va_list args; + va_start(args, format); + fprintf(stderr, "%s: ", g_argv0); + vfprintf(stderr, format, args); + va_end(args); + exit(1); +} + +static Node* compile_expr(BAN::Span& tokens); + +static Node* compile_primary(BAN::Span& tokens) +{ + constexpr Node::Type type_map[] { + [0]=(Node::Type)0, [1]=(Node::Type)0, + [2]=(Node::Type)0, [3]=(Node::Type)0, + [4]=(Node::Type)0, [5]=(Node::Type)0, + [6]=(Node::Type)0, [7]=(Node::Type)0, + [Token::TOK_NAME] = Node::Type::NOD_NAME, + [Token::TOK_INAME] = Node::Type::NOD_INAME, + [Token::TOK_PATH] = Node::Type::NOD_PATH, + [Token::TOK_NOUSER] = Node::Type::NOD_NOUSER, + [Token::TOK_NOGROUP] = Node::Type::NOD_NOGROUP, + [Token::TOK_MOUNT] = Node::Type::NOD_MOUNT, + [Token::TOK_XDEV] = Node::Type::NOD_XDEV, + [Token::TOK_PRUNE] = Node::Type::NOD_PRUNE, + [Token::TOK_PERM] = Node::Type::NOD_PERM, + [Token::TOK_TYPE] = Node::Type::NOD_TYPE, + [Token::TOK_LINKS] = Node::Type::NOD_LINKS, + [Token::TOK_USER] = Node::Type::NOD_USER, + [Token::TOK_GROUP] = Node::Type::NOD_GROUP, + [Token::TOK_SIZE] = Node::Type::NOD_SIZE, + [Token::TOK_ATIME] = Node::Type::NOD_ATIME, + [Token::TOK_CTIME] = Node::Type::NOD_CTIME, + [Token::TOK_MTIME] = Node::Type::NOD_MTIME, + [Token::TOK_EXEC] = Node::Type::NOD_EXEC, + [Token::TOK_OK] = Node::Type::NOD_OK, + [Token::TOK_PRINT] = Node::Type::NOD_PRINT, + [Token::TOK_PRINT0] = Node::Type::NOD_PRINT0, + [Token::TOK_NEWER] = Node::Type::NOD_NEWER, + [Token::TOK_DEPTH] = Node::Type::NOD_DEPTH, + }; + + const auto parse_perm = [](const char* string) { + const bool hyphen = (*string == '-'); + + char* endp; + const mode_t value = strtoul(string + hyphen, &endp, 8); + if (*endp != '\0') + print_error_and_exit("invalid perm '%s'\n", string); + + return hyphen ? value | 017777 : value & 07777; + }; + + const auto parse_type = [](const char* string) { + if (string[1] != '\0'); + else switch (*string) + { + case 'b': return S_IFBLK; + case 'c': return S_IFCHR; + case 'd': return S_IFDIR; + case 'l': return S_IFLNK; + case 'p': return S_IFIFO; + case 'f': return S_IFREG; + case 's': return S_IFSOCK; + } + print_error_and_exit("invalid type '%s'\n", string); + }; + + const auto parse_n = [](const char* string) -> decltype(Node::as.n) { + const bool plus = (*string == '+'); + const bool minus = (*string == '-'); + + char* endp; + const uint64_t value = strtoull(string + (plus || minus), &endp, 10); + if (*endp != '\0') + print_error_and_exit("invalid n '%s'\n", string); + + return { + .value = value, + .comp = plus ? Node::Comp::Gt : minus ? Node::Comp::Lt : Node::Comp::Eq, + }; + }; + + const auto parse_uid = [](const char* string) -> uid_t { + if (passwd* passwd = getpwnam(string)) + return passwd->pw_uid; + + char* endp; + const uid_t value = strtol(string, &endp, 10); + if (*endp != '\0') + print_error_and_exit("invalid user '%s'\n", string); + + return value; + }; + + const auto parse_gid = [](const char* string) -> gid_t { + if (group* group = getgrnam(string)) + return group->gr_gid; + + char* endp; + const uid_t value = strtol(string, &endp, 10); + if (*endp != '\0') + print_error_and_exit("invalid group '%s'\n", string); + + return value; + }; + + const auto parse_newer = [](const char* string) -> timespec { + // FIXME: this should stat/lstat based on -H/-L/-P + struct stat st; + if (stat(string, &st) == -1) + print_error_and_exit("'%s': %s\n", string, strerror(errno)); + return st.st_mtim; + }; + + const auto error_if_no_arg = [&] { + if (tokens[1].type == Token::TOK_END) + print_error_and_exit("missing argument to '%s'\n", tokens[0].string); + }; + + Node* node {}; + if (Token::TOK_NAME <= tokens[0].type && tokens[0].type <= Token::TOK_DEPTH) + node = new Node { type_map[tokens[0].type] }; + + switch (tokens[0].type) + { + case Token::TOK_LPAREN: + tokens = tokens.slice(1); + node = compile_expr(tokens); + if (!token_match(tokens, Token::TOK_RPAREN)) + print_error_and_exit("unexpected token '%s'\n", tokens[0].string); + return node; + case Token::TOK_NAME: + case Token::TOK_INAME: + case Token::TOK_PATH: + error_if_no_arg(); + node->as.pattern = tokens[1].string; + tokens = tokens.slice(2); + return node; + case Token::TOK_NOUSER: + case Token::TOK_NOGROUP: + case Token::TOK_MOUNT: + case Token::TOK_XDEV: + case Token::TOK_PRUNE: + tokens = tokens.slice(1); + return node; + case Token::TOK_PERM: + error_if_no_arg(); + node->as.mode = parse_perm(tokens[1].string); + tokens = tokens.slice(2); + return node; + case Token::TOK_TYPE: + error_if_no_arg(); + node->as.mode = parse_type(tokens[1].string); + tokens = tokens.slice(2); + return node; + case Token::TOK_LINKS: + case Token::TOK_ATIME: + case Token::TOK_CTIME: + case Token::TOK_MTIME: + error_if_no_arg(); + node->as.n = parse_n(tokens[1].string); + tokens = tokens.slice(2); + return node; + case Token::TOK_SIZE: + error_if_no_arg(); + node->as.n = parse_n(tokens[1].string); + // FIXME: support `-size c` + node->as.n.value *= 512; + tokens = tokens.slice(2); + return node; + case Token::TOK_USER: + error_if_no_arg(); + node->as.uid = parse_uid(tokens[1].string); + tokens = tokens.slice(2); + return node; + case Token::TOK_GROUP: + error_if_no_arg(); + node->as.uid = parse_gid(tokens[1].string); + tokens = tokens.slice(2); + return node; + case Token::TOK_EXEC: + case Token::TOK_OK: + print_error_and_exit("unsupported token '%s'\n", tokens[0].string); + case Token::TOK_PRINT: + case Token::TOK_PRINT0: + tokens = tokens.slice(1); + return node; + case Token::TOK_NEWER: + error_if_no_arg(); + node->as.mtime = parse_newer(tokens[1].string); + tokens = tokens.slice(2); + return node; + case Token::TOK_DEPTH: + tokens = tokens.slice(1); + return node; + default: + print_error_and_exit("unexpected token '%s'\n", tokens[0].string); + } +} + +static Node* compile_not(BAN::Span& tokens) +{ + if (!token_match(tokens, Token::TOK_NOT)) + return compile_primary(tokens); + + return new Node { + Node::Type::NOD_NOT, { + .child = compile_not(tokens), + }, + }; +} + +static Node* compile_and(BAN::Span& tokens) +{ + constexpr auto is_implicit_and = [](Token::Type type) { + switch (type) + { + case Token::TOK_AND: + case Token::TOK_OR: + case Token::TOK_RPAREN: + case Token::TOK_END: + return false; + default: + return true; + } + }; + + Node* node = compile_not(tokens); + + while (token_match(tokens, Token::TOK_AND) || is_implicit_and(tokens[0].type)) + { + node = new Node { + Node::Type::NOD_AND, { + .bin_op = { + .lhs = node, + .rhs = compile_not(tokens), + } + }, + }; + } + + return node; +} + +static Node* compile_or(BAN::Span& tokens) +{ + Node* node = compile_and(tokens); + + while (token_match(tokens, Token::TOK_OR)) + { + node = new Node { + Node::Type::NOD_OR, { + .bin_op = { + .lhs = node, + .rhs = compile_and(tokens), + }, + }, + }; + } + + return node; +} + +static Node* compile_expr(BAN::Span& tokens) +{ + return compile_or(tokens); +} + +Node* compile_tokens(BAN::Span tokens) +{ + s_needs_implicit_print = true; + + Node* node = compile_expr(tokens); + if (tokens[0].type != Token::TOK_END) + print_error_and_exit("unexpected token '%s'\n", tokens[0].string); + + if (s_needs_implicit_print) + { + node = new Node { + Node::Type::NOD_AND, { + .bin_op = { + .lhs = node, + .rhs = new Node { Node::Type::NOD_PRINT }, + }, + }, + }; + } + + return node; +} + +bool evaluate_node(Node* node, const char* path, struct stat& st) +{ + constexpr auto compare_n = [](uint64_t value, const decltype(Node::as.n)& n) { + if (n.comp == Node::Comp::Gt) + return value > n.value; + if (n.comp == Node::Comp::Lt) + return value < n.value; + return value == n.value; + }; + + const auto get_basename = [](const char* path) { + if (const char* slash = strrchr(path, '/')) + return slash + 1; + return path; + }; + + switch (node->type) + { + case Node::Type::NOD_AND: + return evaluate_node(node->as.bin_op.lhs, path, st) + && evaluate_node(node->as.bin_op.rhs, path, st); + case Node::Type::NOD_OR: + return evaluate_node(node->as.bin_op.lhs, path, st) + || evaluate_node(node->as.bin_op.rhs, path, st); + case Node::Type::NOD_NOT: + return !evaluate_node(node->as.child, path, st); + case Node::Type::NOD_NAME: + return !fnmatch(node->as.pattern, get_basename(path), 0); + case Node::Type::NOD_INAME: + return !fnmatch(node->as.pattern, get_basename(path), FNM_CASEFOLD); + case Node::Type::NOD_PATH: + return !fnmatch(node->as.pattern, path, 0); + case Node::Type::NOD_NOUSER: + return getpwuid(st.st_uid) == nullptr; + case Node::Type::NOD_NOGROUP: + return getgrgid(st.st_gid) == nullptr; + case Node::Type::NOD_TYPE: + return node->as.mode == (st.st_mode & S_IFMT); + case Node::Type::NOD_USER: + return node->as.uid == st.st_uid; + case Node::Type::NOD_GROUP: + return node->as.gid == st.st_gid; + case Node::Type::NOD_PERM: + if (const mode_t mode = node->as.mode & 07777; mode < node->as.mode) + return (mode & st.st_mode) == mode; + return node->as.mode == (st.st_mode & 07777); + case Node::Type::NOD_LINKS: + return compare_n(st.st_nlink, node->as.n); + case Node::Type::NOD_SIZE: + return compare_n(st.st_size, node->as.n); + case Node::Type::NOD_ATIME: + return compare_n((time(nullptr) - st.st_atime) / 86400, node->as.n); + case Node::Type::NOD_CTIME: + return compare_n((time(nullptr) - st.st_ctime) / 86400, node->as.n); + case Node::Type::NOD_MTIME: + return compare_n((time(nullptr) - st.st_mtime) / 86400, node->as.n); + case Node::Type::NOD_NEWER: + if (st.st_mtim.tv_sec > node->as.mtime.tv_sec) + return true; + if (st.st_mtim.tv_sec < node->as.mtime.tv_sec) + return false; + return st.st_mtim.tv_nsec > node->as.mtime.tv_nsec; + case Node::Type::NOD_EXEC: + case Node::Type::NOD_OK: + ASSERT_NOT_REACHED(); + case Node::Type::NOD_PRINT: + fputs(path, stdout); + fputc('\n', stdout); + return true; + case Node::Type::NOD_PRINT0: + fputs(path, stdout); + fputc('\0', stdout); + return true; + case Node::Type::NOD_MOUNT: + case Node::Type::NOD_XDEV: + case Node::Type::NOD_PRUNE: + case Node::Type::NOD_DEPTH: + return true; + } + + ASSERT_NOT_REACHED(); +} diff --git a/userspace/programs/find/Node.h b/userspace/programs/find/Node.h new file mode 100644 index 00000000..61d0f037 --- /dev/null +++ b/userspace/programs/find/Node.h @@ -0,0 +1,69 @@ +#pragma once + +#include "Token.h" + +#include +#include + +struct Node +{ + enum class Type + { + NOD_AND, NOD_OR, NOD_NOT, + NOD_NAME, NOD_INAME, NOD_PATH, + NOD_NOUSER, NOD_NOGROUP, NOD_MOUNT, + NOD_XDEV, NOD_PRUNE, NOD_PERM, + NOD_TYPE, NOD_LINKS, NOD_USER, + NOD_GROUP, NOD_SIZE, NOD_ATIME, + NOD_CTIME, NOD_MTIME, NOD_EXEC, + NOD_OK, NOD_PRINT, NOD_PRINT0, + NOD_NEWER, NOD_DEPTH, + }; + + enum class Comp + { + Eq, Gt, Lt + }; + + Type type; + union { + // AND, OR + struct { + Node* lhs; + Node* rhs; + } bin_op; + // NOT + Node* child; + // NAME, INAME, PATH + const char* pattern; + // PERM, TYPE + mode_t mode; + // USER + uid_t uid; + // GROUP + gid_t gid; + // LINKS, SIZE, ATIME, CTIME, MTIME + struct { + uint64_t value; + Comp comp; + } n; + // EXEC, OK + struct { + const char* args; + size_t count; + } args; + // NEWER + timespec mtime; + } as; + + Node(Type type, decltype(Node::as) as); + Node(Type type); +}; + +extern bool g_mount_specified; +extern bool g_xdev_specified; +extern bool g_prune_specified; +extern bool g_depth_specified; + +Node* compile_tokens(BAN::Span); +bool evaluate_node(Node*, const char* path, struct stat&); diff --git a/userspace/programs/find/Token.cpp b/userspace/programs/find/Token.cpp new file mode 100644 index 00000000..1f39c4f2 --- /dev/null +++ b/userspace/programs/find/Token.cpp @@ -0,0 +1,57 @@ +#include "Token.h" + +BAN::Vector tokenize(const char* const* strings) +{ + constexpr Token type_map[] { + { Token::TOK_NOT, "!" }, + { Token::TOK_LPAREN, "(" }, + { Token::TOK_RPAREN, ")" }, + { Token::TOK_CURLIES, "{}" }, + { Token::TOK_PLUS, "+" }, + { Token::TOK_SEMICOLON, ";" }, + { Token::TOK_AND, "-a" }, + { Token::TOK_OR, "-o" }, + { Token::TOK_NAME, "-name" }, + { Token::TOK_INAME, "-iname" }, + { Token::TOK_PATH, "-path" }, + { Token::TOK_NOUSER, "-nouser" }, + { Token::TOK_NOGROUP, "-nogroup" }, + { Token::TOK_MOUNT, "-mount" }, + { Token::TOK_XDEV, "-xdev" }, + { Token::TOK_PRUNE, "-prune" }, + { Token::TOK_PERM, "-perm" }, + { Token::TOK_TYPE, "-type" }, + { Token::TOK_LINKS, "-links" }, + { Token::TOK_USER, "-user" }, + { Token::TOK_GROUP, "-group" }, + { Token::TOK_SIZE, "-size" }, + { Token::TOK_ATIME, "-atime" }, + { Token::TOK_CTIME, "-ctime" }, + { Token::TOK_MTIME, "-mtime" }, + { Token::TOK_EXEC, "-exec" }, + { Token::TOK_OK, "-ok" }, + { Token::TOK_PRINT, "-print" }, + { Token::TOK_PRINT0, "-print0" }, + { Token::TOK_NEWER, "-newer" }, + { Token::TOK_DEPTH, "-depth" }, + }; + + BAN::Vector tokens; + + for (size_t i = 0; strings[i]; i++) + { + Token::Type type { Token::TOK_LITERAL }; + for (const auto& entry : type_map) + { + if (strcmp(strings[i], entry.string) != 0) + continue; + type = entry.type; + break; + } + MUST(tokens.push_back({ type, strings[i] })); + } + + MUST(tokens.push_back({ Token::TOK_END, "" })); + + return tokens; +} diff --git a/userspace/programs/find/Token.h b/userspace/programs/find/Token.h new file mode 100644 index 00000000..cc37d3bd --- /dev/null +++ b/userspace/programs/find/Token.h @@ -0,0 +1,24 @@ +#pragma once + +#include + +struct Token +{ + enum Type + { + TOK_NOT, TOK_LPAREN, TOK_RPAREN, TOK_CURLIES, + TOK_PLUS, TOK_SEMICOLON, TOK_AND, TOK_OR, + TOK_NAME, TOK_INAME, TOK_PATH, TOK_NOUSER, + TOK_NOGROUP, TOK_MOUNT, TOK_XDEV, TOK_PRUNE, + TOK_PERM, TOK_TYPE, TOK_LINKS, TOK_USER, + TOK_GROUP, TOK_SIZE, TOK_ATIME, TOK_CTIME, + TOK_MTIME, TOK_EXEC, TOK_OK, TOK_PRINT, + TOK_PRINT0, TOK_NEWER, TOK_DEPTH, TOK_LITERAL, + TOK_END + }; + + Type type; + const char* string; +}; + +BAN::Vector tokenize(const char* const* strings); diff --git a/userspace/programs/find/main.cpp b/userspace/programs/find/main.cpp new file mode 100644 index 00000000..62f45731 --- /dev/null +++ b/userspace/programs/find/main.cpp @@ -0,0 +1,137 @@ +#include "Node.h" +#include "Token.h" + +#include +#include +#include +#include +#include + +enum class LinkMode +{ + Physical, Logical, Half +}; + +const char* g_argv0 { nullptr }; + +static dev_t s_first_st_dev { 0 }; + +static void recurse_path(int fd, const char* name, const char* path, Node* node, LinkMode link_mode) +{ + int stat_flag = 0; + if (link_mode == LinkMode::Physical) + stat_flag = AT_SYMLINK_NOFOLLOW; + else if (link_mode == LinkMode::Half && (fd != AT_FDCWD)) + stat_flag = AT_SYMLINK_NOFOLLOW; + + struct stat st; + if (fstatat(fd, name, &st, stat_flag) == -1) + { + fprintf(stderr, "%s: '%s': %s\n", g_argv0, path, strerror(errno)); + return; + } + + if (fd == AT_FDCWD) + s_first_st_dev = st.st_dev; + + if (g_mount_specified && st.st_dev != s_first_st_dev) + return; + + if (!g_depth_specified || !S_ISDIR(st.st_mode)) + evaluate_node(node, path, st); + + if (S_ISDIR(st.st_mode) && (!g_prune_specified || g_depth_specified) && (!g_xdev_specified || st.st_dev == s_first_st_dev)) + { + int open_flag = stat_flag ? O_NOFOLLOW : 0; + + int dirfd = openat(fd, name, O_RDONLY | O_DIRECTORY | open_flag); + if (dirfd < 0) + fprintf(stderr, "%s: '%s': %s\n", g_argv0, path, strerror(errno)); + + DIR* dirp = nullptr; + if ((dirfd >= 0) && (dirp = fdopendir(dirfd))) + { + char buffer[PATH_MAX]; + + char* buffer_end = stpcpy(buffer, path); + if (*path && buffer_end[-1] != '/') + *buffer_end++ = '/'; + + dirent* dirent; + while ((errno = 0) || (dirent = readdir(dirp))) + { + if (strcmp(dirent->d_name, ".") == 0 || strcmp(dirent->d_name, "..") == 0) + continue; + strcpy(buffer_end, dirent->d_name); + recurse_path(dirfd, dirent->d_name, buffer, node, link_mode); + } + + if (errno) + fprintf(stderr, "%s: '%s': %s\n", g_argv0, path, strerror(errno)); + + closedir(dirp); + } + + if (dirp == nullptr && dirfd >= 0) + { + fprintf(stderr, "%s: '%s': %s\n", g_argv0, path, strerror(errno)); + close(dirfd); + } + } + + if (g_depth_specified && S_ISDIR(st.st_mode)) + evaluate_node(node, path, st); +} + +int main(int argc, char** argv) +{ + g_argv0 = argv[0]; + + LinkMode link_mode { LinkMode::Physical }; + + int idx = 1; + for (; idx < argc; idx++) + { + if (argv[idx][0] != '-') + break; + else if (strcmp(argv[idx], "-P") == 0) + link_mode = LinkMode::Physical; + else if (strcmp(argv[idx], "-L") == 0) + link_mode = LinkMode::Logical; + else if (strcmp(argv[idx], "-H") == 0) + link_mode = LinkMode::Half; + else if (strcmp(argv[idx], "--help") == 0) + { + fprintf(stderr, "usage: %s [-P|-L|-H] PATH... [EXPRESSION...]\n", argv[0]); + fprintf(stderr, " find files based on expressions\n"); + fprintf(stderr, "OPTIONS:\n"); + fprintf(stderr, " -P never follow symbolic links\n"); + fprintf(stderr, " -L follow symbolic links\n"); + fprintf(stderr, " -H only follow symbolic links in PATH...\n"); + fprintf(stderr, " --help show this message and exit\n"); + return 0; + } + else break; + } + + if (idx < argc && strcmp(argv[idx], "--") == 0) + idx++; + + constexpr auto is_path_start = [](char ch) { + return ch != '-' && ch != '!' && ch != '('; + }; + + size_t path_count = 0; + while (idx + path_count < static_cast(argc) && is_path_start(argv[idx + path_count][0])) + path_count++; + + auto tokens = tokenize(&argv[idx + path_count]); + auto* node = compile_tokens(tokens.span()); + + if (path_count == 0) + recurse_path(AT_FDCWD, ".", ".", node, link_mode); + else for (size_t i = 0; i < path_count; i++) + recurse_path(AT_FDCWD, argv[idx + i], argv[idx + i], node, link_mode); + + return 0; +}