Repositories / jai.git
jai.git
Clone (read-only): git clone http://git.guha-anderson.com/git/jai.git
@@ -2,8 +2,8 @@ bin_PROGRAMS = jai AM_CXXFLAGS = $(MOUNT_CFLAGS) $(LIBACL_CFLAGS) -jai_SOURCES = cred.cc default_conf.cc fs.cc jai.cc options.cc cred.h \ -argtype.h defer.h err.h fs.h jai.h +jai_SOURCES = complete.cc cred.cc default_conf.cc fs.cc jai.cc \ +options.cc cred.h argtype.h defer.h err.h fs.h jai.h jai_LDADD = $(MOUNT_LIBS) $(LIBACL_LIBS) man1_MANS = jai.1
@@ -0,0 +1,129 @@ +#include "jai.h" + +#include <cassert> +#include <dirent.h> +#include <print> + +using Completions = Options::Completions; + +struct CompSet { + const Completions &comp_; + std::set<std::string> out_; + + CompSet(const Completions &comp) noexcept : comp_(comp) {} + + auto opt() const { return comp_.arg(); } + auto arg() const { return comp_.arg(); } + + // Format one completion to be output + template<typename... Args> + void output(std::format_string<Args...> fmt, Args &&...args) + { + out_.insert( + std::format("{}{}", comp_.prepend(), + std::vformat(fmt.get(), std::make_format_args(args...)))); + } +}; + +static void +complete_path(int dfd, CompSet &c, bool dir_only) +{ + path arg(c.arg()); + std::string stem = arg.filename(); + + auto d = try_opendir(dfd, arg.parent_path(), kFollow); + if (!d) + return; + while (auto de = readdir(*d)) { + std::string_view name = d_name(de); + if (name == "." || name == ".." || !name.starts_with(stem)) + continue; + if (de->d_type == DT_UNKNOWN || de->d_type == DT_LNK) { + struct stat sb; + if (fstatat(dirfd(*d), d_name(de), &sb, 0) && S_ISDIR(sb.st_mode)) + de->d_type = DT_DIR; + } + if (dir_only && de->d_type != DT_DIR) + continue; + c.output("{}{}", (arg.parent_path() / d_name(de)).string(), + de->d_type == DT_DIR ? "/" : ""); + } +} + +static void +complete_config(int cfd, CompSet &c, std::string ext) +{ + auto d = try_opendir(cfd); + if (!d) + return; + while (auto de = readdir(*d)) { + path f = d_name(de); + if (f.extension() != ext) + continue; + std::string name = f.replace_extension(); + if (!name.starts_with(c.arg())) + continue; + c.output("{}", name); + } +} + +static void +complete_env(CompSet &c, bool eq) +{ + std::string_view arg; + for (char **e = environ; *e; ++e) { + std::string_view var = *e; + if (auto pos = var.find('='); pos == var.npos) + continue; + else + var = var.substr(0, eq ? pos + 1 : pos); + c.output("{}", var); + } +} + +int +Config::complete(Completions c) +{ + using enum Completions::Disposition; + if (c.kind >= 0) { + std::println("_command_offset {}", c.kind); + return 0; + } + if (c.kind == kNoCompletions) + return 1; + else if (c.kind == kRawCompletions) { + for (const auto &v : c.vals) + std::println("{}", v); + return 0; + } + assert(c.kind == kArgCompletions); + + std::string_view opt = c.vals[0], arg = c.vals[1], prefix = c.vals[2]; + CompSet cs(c); + + if (std::ranges::contains(std::array{"-m", "--mode"}, opt)) { + auto arg = c.arg(); + for (std::string_view sv : {"casual", "strict", "bare"}) { + if (sv.starts_with(arg)) + cs.output("{}", sv); + } + } + else if (std::ranges::contains( + std::array{"-d", "--dir", "-x", "--xdir", "--storage"}, opt)) + complete_path(AT_FDCWD, cs, true); + else if (std::ranges::contains(std::array{"--mask", "--unmask"}, opt)) + complete_path(home(), cs, false); + else if (std::ranges::contains(std::array{"-C", "--conf"}, opt)) + complete_config(home_jai(), cs, ".conf"); + else if (std::ranges::contains(std::array{"-j", "--jail"}, opt)) + complete_config(storage(), cs, ".jail"); + else if (opt == "--setenv") + complete_env(cs, true); + else if (opt == "--unsetenv") + complete_env(cs, false); + + for (const auto comp : cs.out_) + std::println("{}", comp); + + return 0; +}
@@ -229,21 +229,36 @@ xdup(int fd, int minfd = 3) return ret; } -inline RaiiHelper<closedir> -xopendir(int dfd, path file = {}, FollowLinks follow = kNoFollow) +inline std::expected<RaiiHelper<closedir>, std::system_error> +try_opendir(int dfd, path file = {}, FollowLinks follow = kNoFollow) { - Fd fd; if (file.empty()) - fd = xdup(dfd); - else - fd = xopenat(dfd, file, - O_RDONLY | O_DIRECTORY | - (follow == kNoFollow ? O_NOFOLLOW : 0)); + // re-open in case dfd is O_PATH and to avoid messing with the + // offset of dfd if we read the directory multiple times + file = "."; + Fd fd = + openat(dfd, file.c_str(), + O_RDONLY | O_DIRECTORY | (follow == kNoFollow ? O_NOFOLLOW : 0)); + if (!fd) + return std::unexpected{ + std::system_error(errno, std::system_category(), fdpath(dfd, file))}; + if (auto d = fdopendir(*fd)) { fd.release(); return d; } - syserr("fdopendir({})", fdpath(dfd, file)); + return std::unexpected{ + std::system_error(errno, std::system_category(), + std::format("{}: fdopendir", fdpath(*fd)))}; +} + +inline RaiiHelper<closedir> +xopendir(int dfd, path file = {}, FollowLinks follow = kNoFollow) +{ + if (auto r = try_opendir(dfd, file, follow)) + return std::move(*r); + else + throw r.error(); } // dirent::d_name is an array, so won't convert properly to types that
@@ -405,6 +405,16 @@ uses `\` to escape the next character. `--version` : Prints the version number and copyright and exit. +`--complete` +: This option is only valid as the first option on the command line. + It tells jai not to do anything, but it prints a list of completions + to assist shells in doing command completion. For example: `jai + --complete -m "c"` prints `casual`. If the commands line is + complete, then it will output the special string `_command_offset + `*N*, which indicates that argument *N* is the start of a new + command that should be completed according to the rules for that + command. + # ENVIRONMENT The following environment variables affect jai's operation:
@@ -602,7 +602,7 @@ clean_root_owned_dir(int dfd, path file) } } -void +bool Config::unmountall() { Fd lock; @@ -643,9 +643,9 @@ Config::unmountall() unlinkat(run_jai_user(), ".lock", 0); lock.reset(); unlinkat(run_jai(), user_.c_str(), AT_REMOVEDIR); -} -extern "C" char **environ; + return unmount_ok; +} std::vector<const char *> Config::make_env() @@ -1100,7 +1100,7 @@ version 3 or later; see the file named COPYING for details.)", exit(0); } -void +int do_main(int argc, char **argv) { Config conf; @@ -1137,6 +1137,9 @@ The default is CMD.conf if it exists, otherwise default.conf)", "Show default contents of $JAI_CONFIG_DIR/.defaults"); option_help = opts->help(); + if (argc > 2 && !strcmp(argv[1], "--complete")) + return conf.complete(opts->complete_args(2, argc, argv)); + std::vector<char *> cmd; try { cmd.assign_range(opts->parse_argv(argc, argv)); @@ -1160,7 +1163,7 @@ The default is CMD.conf if it exists, otherwise default.conf)", std::println( "Run {} --print-defaults to see the original contents of that file.", prog.filename().string()); - exit(0); + return 0; } if (opt_u) { @@ -1169,8 +1172,7 @@ The default is CMD.conf if it exists, otherwise default.conf)", usage(2); } restore.reset(); - conf.unmountall(); - return; + return conf.unmountall() ? 0 : 1; } if (!opt_C.empty()) { @@ -1208,6 +1210,7 @@ The default is CMD.conf if it exists, otherwise default.conf)", auto fd = conf.make_mnt_ns(); cmd.push_back(nullptr); conf.exec(*fd, cmd.data()); + return 0; } int @@ -1227,10 +1230,9 @@ main(int argc, char **argv) #endif try { - do_main(argc, argv); + exit(do_main(argc, argv)); } catch (const ToCatch &e) { warn("{}", e.what()); - return 1; } - return 0; + return 1; }
@@ -14,6 +14,8 @@ #include <sys/syscall.h> #include <unistd.h> +extern "C" char **environ; + inline const char * env_or_empty(std::string_view var) { @@ -115,9 +117,10 @@ struct Config { Fd make_mnt_ns(); void exec(int nsfd, char **argv); void unmount(); - void unmountall(); + bool unmountall(); std::unique_ptr<Options> opt_parser(bool dotjail = false); + int complete(Options::Completions c); void parse_config_fd(int fd, Options *opts = nullptr); bool parse_config_file(path file, Options *opts = nullptr); std::vector<const char *> make_env();
@@ -88,14 +88,15 @@ #include "err.h" +#include <cassert> #include <charconv> #include <concepts> #include <cstring> -#include <format> #include <functional> #include <initializer_list> #include <map> #include <memory> +#include <print> #include <span> #include <utility> #include <vector> @@ -188,15 +189,18 @@ struct Option : std::string_view { struct Options { struct Completions { - static constexpr int kNoCompletions = -1; - static constexpr int kRawCompletions = -3; - static constexpr int kArgCompletions = -4; + enum Disposition { + kNoCompletions = -1, + kRawCompletions = -2, + kArgCompletions = -3, + }; // If kind >= 0, then argv[kind] is the first non-option argument, - // meaning the first argument that is not a valid syntactic - // argument (at least 2 characters, first character '-', total - // length 2 if and only if the second argument is not '-'). If - // there is a "--" argument, then kind is the position after that. + // meaning the first argument that is not a valid syntactic option + // (at least 2 characters, first character '-', total length 2 if + // and only if the second argument is not '-') or the argument to + // such an option. If there is a "--" argument, then kind is the + // position after that. // // If kind is kNoCompletions, then something went wrong (an // invalid argument somewhere) and nothing can be completed. @@ -206,21 +210,22 @@ struct Options { // E.g., if completing "-", it would include all options (both // short and long). // - // If kind is kArgCompletions, then vals must contain exactly 3 - // elements as follows: + // If kind is kArgCompletions, then we are completing the argument + // to a fully determined option,and vals has 3 elements as + // follows: // - // - arg[0] is the argument to be completed (e.g., "-d" or "--dir") + // - opt() is the option being completed (e.g., "-d" or "--dir") // - // - arg[1] is the current prefix of the argument. E.g., if - // the argument being completed is "--dir=/usr/lo" or just - // "/usr/lo" following "--dir", then it would be "/usr/lo". + // - arg() is the current argument stem. E.g., if the argument + // being completed is "--dir=/usr/lo" or just "/usr/lo" + // following "--dir", then it would be "/usr/lo". // - // - arg[2] is the prefix to prepend to completions generated. - // In the case that the value is a separate argv element - // (e.g., completing {"--dir", "/usr/lo"}) arg[2] will be - // empty. In the case that the option and argument are in - // one argv element such as "--dir=/usr/lo" or "-mcasu", then - // it argv[2] would be "--dir=" or "-m". + // - prepend() is what to prepend to completions generated. In + // the case that the value is a separate argv element (e.g., + // completing {"--dir", "/usr/lo"}) prepend() will be empty. + // In the case that the option and argument are in one argv + // element such as "--dir=/usr/lo" or "-mcasu", then + // prepend() would be "--dir=" or "-m". int kind = kNoCompletions; std::vector<std::string> vals; @@ -230,6 +235,15 @@ struct Options { Completions(int k, std::vector<std::string> v) noexcept : kind(k), vals(std::move(v)) {} + + std::string_view index_vals(size_t n) const + { + assert(kind == kArgCompletions && vals.size() == 3 && n < 3); + return vals[n]; + } + auto opt() const { return index_vals(0); } + auto arg() const { return index_vals(1); } + auto prepend() const { return index_vals(2); } }; using enum Action::HasArg;