diff options
Diffstat (limited to 'nihil.posix')
35 files changed, 2595 insertions, 0 deletions
diff --git a/nihil.posix/CMakeLists.txt b/nihil.posix/CMakeLists.txt new file mode 100644 index 0000000..62f6aaf --- /dev/null +++ b/nihil.posix/CMakeLists.txt @@ -0,0 +1,61 @@ +# This source code is released into the public domain. + +add_library(nihil.posix STATIC) +target_link_libraries(nihil.posix PRIVATE + nihil.core nihil.error nihil.flagset nihil.guard nihil.monad) + +target_sources(nihil.posix + PUBLIC FILE_SET modules TYPE CXX_MODULES FILES + posix.ccm + + argv.ccm + ensure_dir.ccm + exec.ccm + execv.ccm + executor.ccm + fd.ccm + find_in_path.ccm + getenv.ccm + open.ccm + process.ccm + read_file.ccm + rename.ccm + spawn.ccm + tempfile.ccm + write_file.ccm + + PRIVATE + argv.cc + ensure_dir.cc + exec.cc + execv.cc + getenv.cc + fd.cc + find_in_path.cc + open.cc + process.cc + rename.cc + tempfile.cc +) + +if(NIHIL_TESTS) + enable_testing() + + add_executable(nihil.posix.test + test.fd.cc + test.getenv.cc + test.spawn.cc + test.tempfile.cc + ) + + target_link_libraries(nihil.posix.test PRIVATE + nihil.posix + Catch2::Catch2WithMain + ) + + find_package(Catch2 REQUIRED) + + include(CTest) + include(Catch) + catch_discover_tests(nihil.posix.test) +endif() diff --git a/nihil.posix/argv.cc b/nihil.posix/argv.cc new file mode 100644 index 0000000..e6b1389 --- /dev/null +++ b/nihil.posix/argv.cc @@ -0,0 +1,65 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <memory> +#include <ranges> +#include <string> +#include <vector> + +module nihil.posix; + +namespace nihil { + +argv::argv() = default; +argv::argv(argv &&) noexcept = default; +auto argv::operator=(this argv &, argv &&) -> argv & = default; + +argv::~argv() +{ + for (auto *arg : m_args) + delete[] arg; +} + +auto argv::data(this argv const &self) -> char const * const * +{ + return self.m_args.data(); +} + +auto argv::data(this argv &self) -> char * const * +{ + return self.m_args.data(); +} + +auto argv::size(this argv const &self) +{ + return self.m_args.size(); +} + +auto argv::begin(this argv const &self) +{ + return self.m_args.begin(); +} + +auto argv::end(this argv const &self) +{ + return self.m_args.end(); +} + + +auto argv::add_arg(this argv &self, std::string_view arg) -> void +{ + // Create a nul-terminated C string. + auto ptr = std::make_unique<char[]>(arg.size() + 1); + std::ranges::copy(arg, ptr.get()); + ptr[arg.size()] = '\0'; + + // Ensure we won't throw when emplacing the pointer. + self.m_args.reserve(self.m_args.size() + 1); + self.m_args.emplace_back(ptr.release()); +} + +} // namespace nihil + diff --git a/nihil.posix/argv.ccm b/nihil.posix/argv.ccm new file mode 100644 index 0000000..6f60f4b --- /dev/null +++ b/nihil.posix/argv.ccm @@ -0,0 +1,78 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <memory> +#include <ranges> +#include <string> +#include <vector> + +export module nihil.posix:argv; + +namespace nihil { + +/* + * argv: stores a null-terminated array of nul-terminated C strings. + * argv::data() is suitable for passing to ::execv(). + * + * Create an argv using argv::from_range(), which takes a range of + * string-like objects. + */ + +export struct argv { + /* + * Create a new argv from a range. + */ + argv(std::from_range_t, std::ranges::range auto &&args) + { + for (auto &&arg : args) + add_arg(std::string_view(arg)); + + m_args.push_back(nullptr); + } + + /* + * Create an argv from an initializer list. + */ + template<typename T> + explicit argv(std::initializer_list<T> &&args) + : argv(std::from_range, std::forward<decltype(args)>(args)) + { + } + + // Movable. + argv(argv &&) noexcept; + auto operator=(this argv &, argv &&other) -> argv &; + + // Not copyable. TODO: for completeness, it probably should be. + argv(argv const &) = delete; + auto operator=(this argv &, argv const &other) -> argv& = delete; + + ~argv(); + + // Access the stored arguments. + [[nodiscard]] auto data(this argv const &self) -> char const * const *; + [[nodiscard]] auto data(this argv &self) -> char * const *; + [[nodiscard]] auto size(this argv const &self); + + // Range access + [[nodiscard]] auto begin(this argv const &self); + [[nodiscard]] auto end(this argv const &self); + +private: + // Use the from_range() factory method to create new instances. + argv(); + + // The argument pointers, including the null terminator. + // This can't be a vector<unique_ptr> because we need an array of + // char pointers to pass to exec. + std::vector<char *> m_args; + + // Add a new argument to the array. + auto add_arg(this argv &self, std::string_view arg) -> void; +}; + +} // namespace nihil + diff --git a/nihil.posix/ensure_dir.cc b/nihil.posix/ensure_dir.cc new file mode 100644 index 0000000..88e8898 --- /dev/null +++ b/nihil.posix/ensure_dir.cc @@ -0,0 +1,30 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <expected> +#include <filesystem> +#include <format> +#include <system_error> + +module nihil.posix; + +import nihil.error; + +namespace nihil { + +auto ensure_dir(std::filesystem::path const &dir) -> std::expected<void, error> +{ + auto err = std::error_code(); + + std::filesystem::create_directories(dir, err); + + if (err) + return std::unexpected(error(err)); + + return {}; +} + +} // namespace nihil diff --git a/nihil.posix/ensure_dir.ccm b/nihil.posix/ensure_dir.ccm new file mode 100644 index 0000000..fa92a90 --- /dev/null +++ b/nihil.posix/ensure_dir.ccm @@ -0,0 +1,23 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <expected> +#include <filesystem> + +export module nihil.posix:ensure_dir; + +import nihil.error; + +namespace nihil { + +/* + * Create the given directory and any parent directories. + */ +export [[nodiscard]] auto ensure_dir(std::filesystem::path const &dir) + -> std::expected<void, error>; + +} // namespace nihil + diff --git a/nihil.posix/exec.cc b/nihil.posix/exec.cc new file mode 100644 index 0000000..5bdcbf7 --- /dev/null +++ b/nihil.posix/exec.cc @@ -0,0 +1,71 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <coroutine> +#include <expected> +#include <format> +#include <string> +#include <utility> + +#include <err.h> +#include <fcntl.h> +#include <unistd.h> + +extern char **environ; + +module nihil.posix; + +import nihil.error; +import nihil.monad; + +namespace nihil { + +fexecv::fexecv(fd &&execfd, argv &&args) noexcept + : m_execfd(std::move(execfd)) + , m_args(std::move(args)) +{ +} + +auto fexecv::exec(this fexecv &self) + -> std::expected<void, error> +{ + ::fexecve(self.m_execfd.get(), self.m_args.data(), environ); + return std::unexpected(error("fexecve failed", + error(std::errc(errno)))); +} + +fexecv::fexecv(fexecv &&) noexcept = default; +auto fexecv::operator=(this fexecv &, fexecv &&) noexcept -> fexecv& = default; + +auto execv(std::string_view path, argv &&argv) + -> std::expected<fexecv, error> +{ + auto file = co_await open(path, O_EXEC) + .transform_error([&] (error cause) { + return error(std::format("could not open {}", path), + std::move(cause)); + }); + + co_return fexecv(std::move(file), std::move(argv)); +} + +auto execvp(std::string_view file, argv &&argv) + -> std::expected<fexecv, error> +{ + auto execfd = find_in_path(file); + if (!execfd) + return std::unexpected(error( + std::format("executable not found in path: {}", file))); + return fexecv(std::move(*execfd), std::move(argv)); +} + +auto shell(std::string_view const &command) + -> std::expected<fexecv, error> +{ + return execl("/bin/sh", "sh", "-c", command); +} + +} // namespace nihil diff --git a/nihil.posix/exec.ccm b/nihil.posix/exec.ccm new file mode 100644 index 0000000..6098318 --- /dev/null +++ b/nihil.posix/exec.ccm @@ -0,0 +1,105 @@ +/* + * This source code is released into the public domain. + */ + +module; + +/* + * Exec providers, mostly used for spawn(). + */ + +#include <expected> +#include <string> + +export module nihil.posix:exec; + +import nihil.error; +import :argv; +import :fd; + +namespace nihil { + +/* + * A concept to mark spawn executors. + */ + +export struct exec_tag{}; + +export template<typename T> +concept executor = + requires (T e) { + std::same_as<exec_tag, typename std::remove_cvref_t<T>::tag>; + { e.exec() }; + }; + +/* + * fexecv: use a file descriptor and an argument vector to call ::fexecve(). + * This is the lowest-level executor which all others are implemented + * in terms of. + * + * TODO: Should have a way to pass the environment (envp). + */ +export struct fexecv final { + using tag = exec_tag; + + fexecv(fd &&execfd, argv &&args) noexcept; + + [[nodiscard]] auto exec(this fexecv &self) + -> std::expected<void, error>; + + // Movable + fexecv(fexecv &&) noexcept; + auto operator=(this fexecv &, fexecv &&) noexcept -> fexecv&; + + // Not copyable (because we hold the open fd object) + fexecv(fexecv const &) = delete; + auto operator=(this fexecv &, fexecv const &) -> fexecv& = delete; + +private: + fd m_execfd; + argv m_args; +}; + +/* + * execv: equivalent to fexecv(), except the command is passed as + * a pathname instead of a file descriptor. Does not search $PATH. + */ +export [[nodiscard]] auto execv(std::string_view path, argv &&argv) + -> std::expected<fexecv, error>; + +/* + * execvp: equivalent to fexecv(), except the command is passed as + * a filename instead of a file descriptor. If the filename is not + * an absolute path, it will be searched for in $PATH. + */ +export [[nodiscard]] auto execvp(std::string_view file, argv &&argv) + -> std::expected<fexecv, error>; + +/* + * execl: equivalent to execv, except the arguments are passed as a + * variadic pack of string-like objects. + */ +export [[nodiscard]] auto execl(std::string_view path, auto &&...args) + -> std::expected<fexecv, error> +{ + return execv(path, argv({std::string_view(args)...})); +} + +/* + * execlp: equivalent to execvp, except the arguments are passed as a + * variadic pack of string-like objects. + */ +export [[nodiscard]] auto execlp(std::string_view file, auto &&...args) + -> std::expected<fexecv, error> +{ + return execvp(file, argv({std::string_view(args)...})); +} + +/* + * shell: run the process by invoking /bin/sh -c with the single argument, + * equivalent to system(3). + */ +export [[nodiscard]] auto shell(std::string_view const &command) + -> std::expected<fexecv, error>; + +} // namespace nihil diff --git a/nihil.posix/executor.ccm b/nihil.posix/executor.ccm new file mode 100644 index 0000000..f348dc8 --- /dev/null +++ b/nihil.posix/executor.ccm @@ -0,0 +1,27 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <concepts> +#include <type_traits> + +export module nihil.posix:executor; + +namespace nihil { + +/* + * A concept to mark spawn executors. + */ + +export struct exec_tag{}; + +export template<typename T> +concept executor = + requires (T e) { + std::same_as<exec_tag, typename std::remove_cvref_t<T>::tag>; + { e.exec() }; + }; + +} // namespace nihil diff --git a/nihil.posix/execv.cc b/nihil.posix/execv.cc new file mode 100644 index 0000000..63f9698 --- /dev/null +++ b/nihil.posix/execv.cc @@ -0,0 +1,43 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <coroutine> +#include <expected> +#include <format> +#include <string> +#include <utility> + +#include <err.h> +#include <fcntl.h> +#include <unistd.h> + +extern char **environ; + +module nihil.posix; + +import nihil.error; +import nihil.monad; + +namespace nihil { + +execv::execv(std::filesystem::path path, argv &&args) noexcept + : m_path(std::move(path)) + , m_args(std::move(args)) +{ +} + +auto execv::exec(this execv &self) -> std::expected<void, error> +{ + ::execve(self.m_path.string().c_str(), self.m_args.data(), environ); + return std::unexpected(error("execve failed", error(std::errc(errno)))); +} + +execv::execv(execv &&) noexcept = default; +execv::execv(execv const &) = default; +auto execv::operator=(this execv &, execv &&) -> execv & = default; +auto execv::operator=(this execv &, execv const &) -> execv & = default; + +} // namespace nihil diff --git a/nihil.posix/execv.ccm b/nihil.posix/execv.ccm new file mode 100644 index 0000000..ca036a1 --- /dev/null +++ b/nihil.posix/execv.ccm @@ -0,0 +1,47 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <expected> +#include <filesystem> +#include <string> + +export module nihil.posix:execv; + +import nihil.error; +import :argv; +import :executor; + +namespace nihil { + +/* + * execv: use a filename and an argument vector to call ::execve(). + * This is the lowest-level executor which all others are implemented + * in terms of, if fexecve is not available. + * + * TODO: Should have a way to pass the environment (envp). + */ +export struct execv final +{ + using tag = exec_tag; + + execv(std::filesystem::path, argv &&) noexcept; + + [[nodiscard]] auto exec(this execv &) -> std::expected<void, error>; + + // Movable + execv(execv &&) noexcept; + auto operator=(this execv &, execv &&) noexcept -> execv &; + + // Copyable. + execv(execv const &); + auto operator=(this execv &, execv const &) -> execv &; + +private: + std::filesystem::path m_path; + argv m_args; +}; + +} // namespace nihil diff --git a/nihil.posix/fd.cc b/nihil.posix/fd.cc new file mode 100644 index 0000000..6d5e54f --- /dev/null +++ b/nihil.posix/fd.cc @@ -0,0 +1,220 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <fcntl.h> +#include <unistd.h> + +#include <coroutine> +#include <expected> +#include <format> +#include <stdexcept> +#include <system_error> + +module nihil.posix; + +import nihil.error; +import nihil.monad; + +namespace nihil { + +fd::fd() noexcept = default; + +fd::fd(int fileno) noexcept + : m_fileno(fileno) +{ +} + +fd::~fd() +{ + if (*this) + std::ignore = this->close(); +} + +fd::fd(fd &&other) noexcept + : m_fileno(std::exchange(other.m_fileno, invalid_fileno)) +{ +} + +auto fd::operator=(this fd &self, fd &&other) noexcept -> fd & +{ + if (&self != &other) + self.m_fileno = std::exchange(other.m_fileno, invalid_fileno); + return self; +} + +fd::operator bool(this fd const &self) noexcept +{ + return self.m_fileno != invalid_fileno; +} + +auto fd::close(this fd &self) -> std::expected<void, error> +{ + auto const ret = ::close(self.get()); + self.m_fileno = invalid_fileno; + + if (ret == 0) + return {}; + + return std::unexpected(error(std::errc(errno))); +} + +auto fd::get(this fd const &self) -> int +{ + if (self) + return self.m_fileno; + throw std::logic_error("Attempt to call get() on invalid fd"); +} + +auto fd::release(this fd &&self) -> int +{ + if (self) + return std::exchange(self.m_fileno, invalid_fileno); + throw std::logic_error("Attempt to release an invalid fd"); +} + +auto dup(fd const &self) -> std::expected<fd, error> +{ + auto const newfd = ::dup(self.get()); + if (newfd != -1) + return newfd; + + return std::unexpected(error(std::errc(errno))); +} + +auto dup(fd const &self, int newfd) -> std::expected<fd, error> +{ + auto const ret = ::dup2(self.get(), newfd); + if (ret != -1) + return newfd; + + return std::unexpected(error(std::errc(errno))); +} + +auto raw_dup(fd const &self) -> std::expected<int, error> +{ + auto const newfd = ::dup(self.get()); + if (newfd != -1) + return newfd; + + return std::unexpected(error(std::errc(errno))); +} + +auto raw_dup(fd const &self, int newfd) -> std::expected<int, error> +{ + auto const ret = ::dup2(self.get(), newfd); + if (ret != -1) + return newfd; + + return std::unexpected(error(std::errc(errno))); +} + +auto getflags(fd const &self) -> std::expected<int, error> +{ + auto const flags = ::fcntl(self.get(), F_GETFL); + if (flags != -1) + return flags; + + return std::unexpected(error(std::errc(errno))); +} + +auto replaceflags(fd &self, int newflags) -> std::expected<void, error> +{ + auto const ret = ::fcntl(self.get(), F_SETFL, newflags); + if (ret == 0) + return {}; + + return std::unexpected(error(std::errc(errno))); +} + +auto setflags(fd &self, int newflags) -> std::expected<int, error> +{ + auto flags = co_await getflags(self); + + flags |= newflags; + co_await replaceflags(self, flags); + + co_return flags; +} + +auto clearflags(fd &self, int clrflags) -> std::expected<int, error> +{ + auto flags = co_await getflags(self); + + flags &= ~clrflags; + co_await replaceflags(self, flags); + + co_return flags; +} + +auto getfdflags(fd const &self) -> std::expected<int, error> +{ + auto const flags = ::fcntl(self.get(), F_GETFD); + if (flags != -1) + return flags; + + return std::unexpected(error(std::errc(errno))); +} + +auto replacefdflags(fd &self, int newflags) -> std::expected<void, error> +{ + auto const ret = ::fcntl(self.get(), F_SETFD, newflags); + if (ret != -1) + return {}; + + return std::unexpected(error(std::errc(errno))); +} + +auto setfdflags(fd &self, int newflags) -> std::expected<int, error> +{ + auto flags = co_await getfdflags(self); + + flags |= newflags; + co_await replacefdflags(self, flags); + + co_return flags; +} + +auto clearfdflags(fd &self, int clrflags) -> std::expected<int, error> +{ + auto flags = co_await getfdflags(self); + + flags &= ~clrflags; + co_await replacefdflags(self, flags); + + co_return flags; +} + +auto pipe() -> std::expected<std::pair<fd, fd>, error> +{ + auto fds = std::array<int, 2>{}; + + if (auto const ret = ::pipe(fds.data()); ret != 0) + return std::unexpected(error(std::errc(errno))); + + return {{fd(fds[0]), fd(fds[1])}}; +} + +auto fd::write(this fd &self, std::span<std::byte const> buffer) + -> std::expected<std::size_t, error> +{ + auto const ret = ::write(self.get(), buffer.data(), buffer.size()); + if (ret >= 0) + return ret; + + return std::unexpected(error(std::errc(errno))); +} + +auto fd::read(this fd &self, std::span<std::byte> buffer) + -> std::expected<std::span<std::byte>, error> +{ + auto const ret = ::read(self.get(), buffer.data(), buffer.size()); + if (ret >= 0) + return buffer.subspan(0, ret); + + return std::unexpected(error(std::errc(errno))); +} + +} // namespace nihil diff --git a/nihil.posix/fd.ccm b/nihil.posix/fd.ccm new file mode 100644 index 0000000..b937f46 --- /dev/null +++ b/nihil.posix/fd.ccm @@ -0,0 +1,157 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <coroutine> +#include <expected> +#include <ranges> +#include <span> +#include <stdexcept> +#include <system_error> + +export module nihil.posix:fd; + +import nihil.error; +import nihil.monad; + +namespace nihil { + +/* + * fd: a file descriptor. + */ + +export struct fd final { + // Construct an empty (invalid) fd. + fd() noexcept; + + // Construct an fd from an exising file destrictor, taking ownership. + fd(int fd_) noexcept; + + // Destructor. Close the fd, discarding any errors. + ~fd(); + + // Move from another fd, leaving the moved-from fd in an invalid state. + fd(fd &&other) noexcept; + auto operator=(this fd &, fd &&other) noexcept -> fd &; + + // Not copyable. + fd(fd const &) = delete; + fd& operator=(this fd &, fd const &) = delete; + + // Return true if this fd is valid (open). + [[nodiscard]] explicit operator bool(this fd const &self) noexcept; + + // Close the wrapped fd. + [[nodiscard]] auto close(this fd &self) -> std::expected<void, error>; + + // Return the stored fd. + [[nodiscard]] auto get(this fd const &self) -> int; + + // Release the stored fd and return it. The caller must close it. + [[nodiscard]] auto release(this fd &&self) -> int; + + // Write data from the provided buffer to the fd. Returns the + // number of bytes written. + [[nodiscard]] auto write(this fd &self, std::span<std::byte const>) + -> std::expected<std::size_t, error>; + + // Read data from the fd to the provided buffer. Returns a + // subspan containing the data which was read. + [[nodiscard]] auto read(this fd &self, std::span<std::byte>) + -> std::expected<std::span<std::byte>, error>; + +private: + static constexpr int invalid_fileno = -1; + + int m_fileno = invalid_fileno; +}; + +// Create a copy of this fd by calling dup(). +export [[nodiscard]] auto dup(fd const &self) -> std::expected<fd, error>; + +// Create a copy of this fd by calling dup2(). Note that because this results +// in the existing fd and the new fd both being managed by an fd instance, +// there are two potential cases that can cause problems: +// +// - dup()ing an fd to itself (a no-op) +// - dup()ing an fd to an fd which is already managed by an fd instance +// +// In both of these cases, either use raw_dup() instead, or immediately call +// release() on the returned fd to prevent the fd instance from closing it. +export [[nodiscard]] auto dup(fd const &self, int newfd) + -> std::expected<fd, error>; + +// Create a copy of this fd by calling dup(). +export [[nodiscard]] auto raw_dup(fd const &self) + -> std::expected<int, error>; + +// Create a copy of this fd by calling dup2(). +export [[nodiscard]] auto raw_dup(fd const &self, int newfd) + -> std::expected<int, error>; + +// Return the fnctl flags for this fd. +export [[nodiscard]] auto getflags(fd const &self) + -> std::expected<int, error>; + +// Replace the fnctl flags for this fd. +export [[nodiscard]] auto replaceflags(fd &self, int newflags) + -> std::expected<void, error>; + +// Add bits to the fcntl flags for this fd. Returns the new flags. +export [[nodiscard]] auto setflags(fd &self, int newflags) + -> std::expected<int, error>; + +// Remove bits from the fcntl flags for this fd. Returns the new flags. +export [[nodiscard]] auto clearflags(fd &self, int clrflags) + -> std::expected<int, error>; + +// Return the fd flags for this fd. +export [[nodiscard]] auto getfdflags(fd const &self) + -> std::expected<int, error>; + +// Replace the fd flags for this fd. +export [[nodiscard]] auto replacefdflags(fd &self, int newflags) + -> std::expected<void, error>; + +// Add bits to the fd flags for this fd. Returns the new flags. +export [[nodiscard]] auto setfdflags(fd &self, int newflags) + -> std::expected<int, error>; + +// Remove bits from the fd flags for this fd. Returns the new flags. +export [[nodiscard]] auto clearfdflags(fd &self, int clrflags) + -> std::expected<int, error>; + +// Create two fds by calling pipe() and return them. +export [[nodiscard]] auto pipe() -> std::expected<std::pair<fd, fd>, error>; + +/* + * Write data to a file descriptor from the provided range. Returns the + * number of bytes written. + */ +export [[nodiscard]] auto write(fd &file, + std::ranges::contiguous_range auto &&range) + -> std::expected<std::size_t, error> +requires(sizeof(std::ranges::range_value_t<decltype(range)>) == 1) +{ + return file.write(as_bytes(std::span(range))); +} + +/* + * Read data from a file descriptor into the provided buffer. Returns a + * span containing the data that was read. + */ +export [[nodiscard]] auto read(fd &file, + std::ranges::contiguous_range auto &&range) + -> std::expected< + std::span<std::ranges::range_value_t<decltype(range)>>, + error> +requires(sizeof(std::ranges::range_value_t<decltype(range)>) == 1) +{ + auto bspan = as_writable_bytes(std::span(range)); + auto rspan = co_await file.read(bspan); + co_return std::span(range).subspan(0, rspan.size()); +} + +} // namespace nihil diff --git a/nihil.posix/fexecv.ccm b/nihil.posix/fexecv.ccm new file mode 100644 index 0000000..5ad6c62 --- /dev/null +++ b/nihil.posix/fexecv.ccm @@ -0,0 +1,53 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <expected> +#include <string> + +#include "nihil.hh" + +#ifdef NIHIL_HAVE_FEXECVE + +export module nihil.posix:fexecv; + +import nihil.error; +import :argv; +import :executor; +import :fd; + +namespace nihil { + +/* + * fexecv: use a file descriptor and an argument vector to call ::fexecve(). + * This is the lowest-level executor which all others are implemented + * in terms of (if it's available). + * + * TODO: Should have a way to pass the environment (envp). + */ +export struct fexecv final { + using tag = exec_tag; + + fexecv(fd &&execfd, argv &&args) noexcept; + + [[nodiscard]] auto exec(this fexecv &self) + -> std::expected<void, error>; + + // Movable + fexecv(fexecv &&) noexcept; + auto operator=(this fexecv &, fexecv &&) noexcept -> fexecv&; + + // Not copyable (because we hold the open fd object) + fexecv(fexecv const &) = delete; + auto operator=(this fexecv &, fexecv const &) -> fexecv& = delete; + +private: + fd m_execfd; + argv m_args; +}; + +} // namespace nihil + +#endif // NIHIL_HAVE_FEXECVE
\ No newline at end of file diff --git a/nihil.posix/find_in_path.cc b/nihil.posix/find_in_path.cc new file mode 100644 index 0000000..7b03faa --- /dev/null +++ b/nihil.posix/find_in_path.cc @@ -0,0 +1,52 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <filesystem> +#include <optional> +#include <ranges> +#include <string> + +#include <fcntl.h> +#include <paths.h> +#include <unistd.h> + +module nihil.posix; + +namespace nihil { + +auto find_in_path(std::filesystem::path const &file) -> std::optional<std::filesystem::path> +{ + using namespace std::literals; + + auto try_return = [](std::filesystem::path file) + -> std::optional<std::filesystem::path> + { + auto ret = ::access(file.string().c_str(), X_OK); + if (ret == 0) + return {std::move(file)}; + return {}; + }; + + // Absolute pathname skips the search. + if (file.is_absolute()) + return try_return(file); + + auto path = getenv("PATH").value_or(_PATH_DEFPATH); + + for (auto &&dir : path | std::views::split(':')) { + // An empty $PATH element means cwd. + auto sdir = dir.empty() + ? std::filesystem::path(".") + : std::filesystem::path(std::string_view(dir)); + + if (auto ret = try_return(sdir / file); ret) + return ret; + } + + return {}; +} + +} // namespace nihil diff --git a/nihil.posix/find_in_path.ccm b/nihil.posix/find_in_path.ccm new file mode 100644 index 0000000..4988a12 --- /dev/null +++ b/nihil.posix/find_in_path.ccm @@ -0,0 +1,24 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <filesystem> +#include <optional> + +export module nihil.posix:find_in_path; + +import nihil.error; +import :fd; + +namespace nihil { + +/* + * Find an executable in $PATH and return the full path. If $PATH is not set, uses _PATH_DEFPATH. + * If the file can't be found or opened, returns std::nullopt. + */ +export [[nodiscard]] auto find_in_path(std::filesystem::path const &file) + -> std::optional<std::filesystem::path>; + +} // namespace nihil diff --git a/nihil.posix/getenv.cc b/nihil.posix/getenv.cc new file mode 100644 index 0000000..ad93305 --- /dev/null +++ b/nihil.posix/getenv.cc @@ -0,0 +1,54 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <cstdint> +#include <expected> +#include <string> +#include <system_error> +#include <vector> + +#include <unistd.h> + +#include "nihil.hh" + +module nihil.posix; + +import nihil.error; + +namespace nihil { + +auto getenv(std::string_view varname) -> std::expected<std::string, error> +{ + auto cvarname = std::string(varname); + +#ifdef NIHIL_HAVE_GETENV_R + // Start with a buffer of this size, and double it every iteration. + constexpr auto bufinc = std::size_t{1024}; + + auto buf = std::vector<char>(bufinc); + for (;;) { + auto const ret = ::getenv_r(cvarname.c_str(), + buf.data(), buf.size()); + + if (ret == 0) + return {std::string(buf.data())}; + + if (ret == -1 && errno == ERANGE) { + buf.resize(buf.size() * 2); + continue; + } + + return std::unexpected(error(std::errc(errno))); + } +#else // NIHIL_HAVE_GETENV_R + auto *v = ::getenv(cvarname.c_str()); + if (v == nullptr) + return std::unexpected(error(std::errc(errno))); + return {std::string(v)}; +#endif // NIHIL_HAVE_GETENV_R +} + +} // namespace nihil diff --git a/nihil.posix/getenv.ccm b/nihil.posix/getenv.ccm new file mode 100644 index 0000000..465f7e7 --- /dev/null +++ b/nihil.posix/getenv.ccm @@ -0,0 +1,23 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <expected> +#include <string> + +export module nihil.posix:getenv; + +import nihil.error; + +namespace nihil { + +/* + * Find a variable by the given name in the environment by calling getenv_r(). + */ + +export [[nodiscard]] auto getenv(std::string_view varname) + -> std::expected<std::string, error>; + +} // namespace nihil diff --git a/nihil.posix/open.cc b/nihil.posix/open.cc new file mode 100644 index 0000000..9ef6538 --- /dev/null +++ b/nihil.posix/open.cc @@ -0,0 +1,31 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <expected> +#include <filesystem> +#include <system_error> + +#include <fcntl.h> +#include <unistd.h> + +module nihil.posix; + +import nihil.error; +import :fd; + +namespace nihil { + +auto open(std::filesystem::path const &filename, int flags, int mode) + -> std::expected<fd, error> +{ + auto fdno = ::open(filename.c_str(), flags, mode); + if (fdno != -1) + return fd(fdno); + + return std::unexpected(error(std::errc(errno))); +} + +} // namespace nihil diff --git a/nihil.posix/open.ccm b/nihil.posix/open.ccm new file mode 100644 index 0000000..eaedacd --- /dev/null +++ b/nihil.posix/open.ccm @@ -0,0 +1,24 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <expected> +#include <filesystem> + +export module nihil.posix:open; + +import nihil.error; +import :fd; + +export namespace nihil { + +/* + * Open the given file and return an fd for it. + */ +[[nodiscard]] auto open(std::filesystem::path const &filename, + int flags, int mode = 0777) + -> std::expected<fd, error>; + +} // namespace nihil diff --git a/nihil.posix/open_in_path.cc b/nihil.posix/open_in_path.cc new file mode 100644 index 0000000..30021ca --- /dev/null +++ b/nihil.posix/open_in_path.cc @@ -0,0 +1,51 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <filesystem> +#include <optional> +#include <ranges> +#include <string> + +#include <fcntl.h> +#include <paths.h> + +module nihil.posix; + +namespace nihil { + +auto open_in_path(std::filesystem::path const &file) -> std::optional<fd> +{ + using namespace std::literals; + + auto try_open = + [](std::filesystem::path const &file) -> std::optional<fd> + { + auto ret = open(file, O_EXEC); + if (ret) + return {std::move(*ret)}; + return {}; + }; + + // Absolute pathname skips the search. + if (file.is_absolute()) + return try_open(file); + + auto path = getenv("PATH").value_or(_PATH_DEFPATH); + + for (auto &&dir : path | std::views::split(':')) { + // An empty $PATH element means cwd. + auto sdir = dir.empty() + ? std::filesystem::path(".") + : std::filesystem::path(std::string_view(dir)); + + if (auto ret = try_open(sdir / file); ret) + return ret; + } + + return {}; +} + +} // namespace nihil diff --git a/nihil.posix/open_in_path.ccm b/nihil.posix/open_in_path.ccm new file mode 100644 index 0000000..1fae56b --- /dev/null +++ b/nihil.posix/open_in_path.ccm @@ -0,0 +1,23 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <filesystem> +#include <optional> + +export module nihil.posix:find_in_path; + +import nihil.error; +import :fd; + +namespace nihil { + +/* + * Find an executable in $PATH and open it with O_EXEC. If $PATH is not set, uses _PATH_DEFPATH. + * If the file can't be found or opened, returns std::nullopt. + */ +export [[nodiscard]] auto open_in_path(std::filesystem::path const &file) -> std::optional<fd>; + +} // namespace nihil diff --git a/nihil.posix/posix.ccm b/nihil.posix/posix.ccm new file mode 100644 index 0000000..ea13f81 --- /dev/null +++ b/nihil.posix/posix.ccm @@ -0,0 +1,35 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <expected> +#include <filesystem> +#include <optional> +#include <string> + +#include "nihil.hh" + +export module nihil.posix; + +import nihil.error; + +export import :argv; +export import :ensure_dir; +export import :exec; +export import :execv; +export import :fd; +export import :find_in_path; +export import :getenv; +export import :open; +export import :process; +export import :read_file; +export import :rename; +export import :spawn; +export import :tempfile; +export import :write_file; + +#ifdef NIHIL_HAVE_FEXECVE +export import :fexecv; +#endif diff --git a/nihil.posix/process.cc b/nihil.posix/process.cc new file mode 100644 index 0000000..70e84b7 --- /dev/null +++ b/nihil.posix/process.cc @@ -0,0 +1,102 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <cerrno> +#include <cstring> +#include <expected> +#include <format> +#include <optional> +#include <system_error> +#include <utility> + +#include <sys/types.h> +#include <sys/wait.h> + +module nihil.posix; + +import nihil.error; + +namespace nihil { + +auto wait_result::okay(this wait_result const &self) -> bool +{ + return self.status() == 0; +} + +wait_result::operator bool(this wait_result const &self) +{ + return self.okay(); +} + +auto wait_result::status(this wait_result const &self) -> std::optional<int> +{ + if (WIFEXITED(self._status)) + return WEXITSTATUS(self._status); + return {}; +} + +auto wait_result::signal(this wait_result const &self) -> std::optional<int> +{ + if (WIFSIGNALED(self._status)) + return WTERMSIG(self._status); + return {}; +} + +wait_result::wait_result(int status) + : _status(status) +{} + +process::process(::pid_t pid) + : m_pid(pid) +{} + +process::~process() { + if (m_pid == -1) + return; + + auto status = int{}; + std::ignore = waitpid(m_pid, &status, WEXITED); +} + +process::process(process &&other) noexcept + : m_pid(std::exchange(other.m_pid, -1)) +{ +} + +auto process::operator=(this process &self, process &&other) noexcept + -> process & +{ + if (&self != &other) { + self.m_pid = std::exchange(other.m_pid, -1); + } + + return self; +} + +// Get the child's process id. +auto process::pid(this process const &self) noexcept -> ::pid_t +{ + return self.m_pid; +} + +auto process::wait(this process &&self) -> std::expected<wait_result, error> +{ + auto status = int{}; + auto ret = waitpid(self.m_pid, &status, WEXITED); + if (ret == -1) + return std::unexpected(error(std::errc(errno))); + + return wait_result(status); +} + +auto process::release(this process &&self) -> ::pid_t +{ + auto const ret = self.pid(); + self.m_pid = -1; + return ret; +} + +} // namespace nihil diff --git a/nihil.posix/process.ccm b/nihil.posix/process.ccm new file mode 100644 index 0000000..425deac --- /dev/null +++ b/nihil.posix/process.ccm @@ -0,0 +1,91 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <expected> +#include <optional> +#include <system_error> +#include <utility> + +#include <sys/types.h> + +export module nihil.posix:process; + +import nihil.error; + +namespace nihil { + +/* + * wait_result: the exit status of a process. + */ +export struct wait_result final { + // Return true if the process exited normally with an exit code of + // zero, otherwise false. + [[nodiscard]] auto okay(this wait_result const &self) -> bool; + [[nodiscard]] explicit operator bool(this wait_result const &self); + + // Return the exit status, if any. + [[nodiscard]] auto status(this wait_result const &self) + -> std::optional<int>; + + // Return the exit signal, if any. + [[nodiscard]] auto signal(this wait_result const &self) + -> std::optional<int>; + +private: + friend struct process; + + int _status; + + // Construct a new wait_result from the output of waitpid(). + wait_result(int status); +}; + +/* + * process: represents a process we created, which can be waited for. + */ +export struct process final { + process() = delete; + + /* + * Create a new process from a pid, which must be a child of the + * current process. + */ + process(::pid_t pid); + + // When destroyed, we automatically wait for the process to + // avoid creating zombie processes. + ~process(); + + // Movable. + process(process &&) noexcept; + auto operator=(this process &, process &&) noexcept -> process &; + + // Not copyable. + process(process const &) = delete; + auto operator=(this process &, process const &) -> process & = delete; + + // Get the child's process id. + [[nodiscard]] auto pid(this process const &self) noexcept -> ::pid_t; + + /* + * Wait for this process to exit (by calling waitpid()) and return + * its exit status. This destroys the process state, leaving this + * object in a moved-from state. + */ + [[nodiscard]] auto wait(this process &&self) + -> std::expected<wait_result, error>; + + /* + * Release this process so we won't try to wait for it when + * destroying this object. + */ + [[nodiscard]] auto release(this process &&self) -> ::pid_t; + +private: + ::pid_t m_pid; +}; + +} // namespace nihil diff --git a/nihil.posix/read_file.ccm b/nihil.posix/read_file.ccm new file mode 100644 index 0000000..be9e102 --- /dev/null +++ b/nihil.posix/read_file.ccm @@ -0,0 +1,49 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <algorithm> +#include <expected> +#include <filesystem> +#include <iterator> +#include <ranges> +#include <span> +#include <system_error> + +#include <fcntl.h> +#include <unistd.h> + +export module nihil.posix:read_file; + +import nihil.error; +import nihil.monad; +import :fd; +import :open; + +namespace nihil { + +/* + * Read the contents of a file into an output iterator. + */ +export [[nodiscard]] auto +read_file(std::filesystem::path const &filename, + std::output_iterator<char> auto &&iter) + -> std::expected<void, error> +{ + auto file = co_await open(filename, O_RDONLY); + + auto constexpr bufsize = std::size_t{1024}; + auto buffer = std::array<char, bufsize>{}; + + for (;;) { + auto read_buf = co_await(read(file, buffer)); + if (read_buf.empty()) + co_return {}; + + std::ranges::copy(read_buf, iter); + } +} + +} // namespace nihil diff --git a/nihil.posix/rename.cc b/nihil.posix/rename.cc new file mode 100644 index 0000000..9203d08 --- /dev/null +++ b/nihil.posix/rename.cc @@ -0,0 +1,34 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <expected> +#include <filesystem> + +module nihil.posix; + +import nihil.error; + +namespace nihil { + +/* + * Rename a file. + */ +auto rename_file(std::filesystem::path const &oldp, + std::filesystem::path const &newp) + -> std::expected<void, error> +{ + auto err = std::error_code(); + + std::filesystem::rename(oldp, newp, err); + + if (err) + return std::unexpected(error(err)); + + return {}; +} + + +} // namespace nihil diff --git a/nihil.posix/rename.ccm b/nihil.posix/rename.ccm new file mode 100644 index 0000000..796ec5b --- /dev/null +++ b/nihil.posix/rename.ccm @@ -0,0 +1,23 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <expected> +#include <filesystem> + +export module nihil.posix:rename; + +import nihil.error; + +namespace nihil { + +/* + * Rename a file (or directory). + */ +export [[nodiscard]] auto +rename(std::filesystem::path const &oldp, std::filesystem::path const &newp) + -> std::expected<void, error>; + +} // namespace nihil diff --git a/nihil.posix/spawn.ccm b/nihil.posix/spawn.ccm new file mode 100644 index 0000000..4cce334 --- /dev/null +++ b/nihil.posix/spawn.ccm @@ -0,0 +1,246 @@ +/* + * This source code is released into the public domain. + */ + +module; + +/* + * spawn(): fork and execute a child process. + */ + +#include <algorithm> +#include <cerrno> +#include <coroutine> +#include <expected> +#include <filesystem> +#include <format> +#include <iterator> +#include <print> +#include <string> +#include <utility> + +#include <sys/types.h> +#include <sys/wait.h> + +#include <fcntl.h> +#include <unistd.h> + +export module nihil.posix:spawn; + +import nihil.monad; +import :argv; +import :exec; +import :open; +import :process; + +namespace nihil { + +// Useful constants +export inline int constexpr stdin_fileno = STDIN_FILENO; +export inline int constexpr stdout_fileno = STDOUT_FILENO; +export inline int constexpr stderr_fileno = STDERR_FILENO; + +/* + * fd_pipe: create a pipe with one end in the child and the other in the + * parent. The child's side will be dup2()'d to the provided fd number. + * The parent side fd can be retrieved via parent_fd(); + */ +export struct fd_pipe final { + fd_pipe(int fdno, fd &&child_fd, fd &&parent_fd) + : m_fdno(fdno) + , m_child_fd(std::move(child_fd)) + , m_parent_fd(std::move(parent_fd)) + { + } + + auto run_in_child(this fd_pipe &self, process &) -> void + { + auto err = raw_dup(self.m_child_fd, self.m_fdno); + if (!err) { + std::print("dup: {}\n", err.error()); + _exit(1); + } + + /* + * We don't care about errors from close() since the fd + * is still closed. + */ + std::ignore = self.m_parent_fd.close(); + std::ignore = self.m_child_fd.close(); + } + + auto run_in_parent(this fd_pipe &self, process &) -> void + { + std::ignore = self.m_child_fd.close(); + } + + [[nodiscard]] auto parent_fd(this fd_pipe &self) -> fd & + { + return self.m_parent_fd; + } + +private: + int m_fdno; + fd m_child_fd; + fd m_parent_fd; +}; + +export [[nodiscard]] auto +make_fd_pipe(int fdno) -> std::expected<fd_pipe, error> +{ + auto fds = co_await pipe(); + co_return fd_pipe(fdno, std::move(fds.first), std::move(fds.second)); +} + +/* + * fd_file: open a file and provide it to the child as a file descriptor. + * open_flags and open_mode are as for ::open(). + */ +export struct fd_file final { + fd_file(int fdno, fd &&file_fd) + : m_fdno(fdno) + , m_file_fd(std::move(file_fd)) + { + } + + auto run_in_parent(this fd_file &self, process &) -> void + { + std::ignore = self.m_file_fd.close(); + } + + auto run_in_child(this fd_file &self, process &) -> void + { + auto err = raw_dup(self.m_file_fd, self.m_fdno); + if (!err) { + std::print("dup: {}\n", err.error()); + _exit(1); + } + + std::ignore = self.m_file_fd.close(); + } + +private: + int m_fdno; + fd m_file_fd; +}; + +export [[nodiscard]] auto +make_fd_file(int fdno, std::filesystem::path const &file, + int flags, int mode = 0777) + -> std::expected<fd_file, error> +{ + auto fd = co_await open(file, flags, mode); + co_return fd_file(fdno, std::move(fd)); +} + +/* + * Shorthand for fd_file with /dev/null as the file. + */ + +export [[nodiscard]] inline auto +stdin_devnull() -> std::expected<fd_file, error> +{ + return make_fd_file(stdin_fileno, "/dev/null", O_RDONLY); +} + +export [[nodiscard]] inline auto +stdout_devnull() -> std::expected<fd_file, error> +{ + return make_fd_file(stdout_fileno, "/dev/null", O_WRONLY); +} + +export [[nodiscard]] inline auto +stderr_devnull() -> std::expected<fd_file, error> +{ + return make_fd_file(stderr_fileno, "/dev/null", O_WRONLY); +} + +/* + * Capture the output of a pipe in the parent and read it into an + * output iterator. + */ +export template<std::output_iterator<char> Iterator> +struct fd_capture final { + fd_capture(fd_pipe &&pipe, Iterator it) + : m_pipe(std::move(pipe)) + , m_iterator(std::move(it)) + { + } + + auto run_in_child(this fd_capture &self, process &p) -> void + { + self.m_pipe.run_in_child(p); + } + + auto run_in_parent(this fd_capture &self, process &p) -> void + { + self.m_pipe.run_in_parent(p); + + auto constexpr bufsize = std::size_t{1024}; + auto buffer = std::array<char, bufsize>(); + auto &fd = self.m_pipe.parent_fd(); + for (;;) { + auto ret = read(fd, buffer); + if (!ret || ret->size() == 0) + break; + + std::ranges::copy(*ret, self.m_iterator); + } + + // We probably want to handle errors here somehow, + // but it's not clear what would be useful behaviour. + } + +private: + fd_pipe m_pipe; + Iterator m_iterator; +}; + +export [[nodiscard]] auto +make_capture(int fdno, std::output_iterator<char> auto &&it) + -> std::expected<fd_capture<decltype(it)>, error> +{ + auto pipe = co_await make_fd_pipe(fdno); + co_return fd_capture(std::move(pipe), + std::forward<decltype(it)>(it)); +} + +export [[nodiscard]] auto +make_capture(int fdno, std::string &str) + -> std::expected<fd_capture<decltype(std::back_inserter(str))>, error> +{ + auto pipe = co_await make_fd_pipe(fdno); + co_return fd_capture(std::move(pipe), std::back_inserter(str)); +} + +/* + * Spawn a new process with the given arguments and return a struct process. + * Throws exec_error() on failure. + */ +export [[nodiscard]] auto +spawn(executor auto &&executor, auto &&...actions) + -> std::expected<process, error> +{ + auto const pid = ::fork(); + if (pid == -1) + return std::unexpected(error("fork failed", + error(std::errc(errno)))); + + auto proc = process(pid); + + if (pid == 0) { + // We are in the child. + (actions.run_in_child(proc), ...); + std::ignore = std::move(proc).release(); + + auto err = executor.exec(); + std::print("{}\n", err.error()); + _exit(1); + } + + (actions.run_in_parent(proc), ...); + + return proc; +} + +} // namespace nihil diff --git a/nihil.posix/tempfile.cc b/nihil.posix/tempfile.cc new file mode 100644 index 0000000..b1d3dee --- /dev/null +++ b/nihil.posix/tempfile.cc @@ -0,0 +1,128 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <algorithm> +#include <coroutine> +#include <expected> +#include <filesystem> +#include <random> +#include <string> + +#include <fcntl.h> +#include <unistd.h> + +module nihil.posix; + +import nihil.flagset; +import :getenv; +import :open; + +namespace nihil { + +temporary_file::temporary_file(nihil::fd &&fd, + std::filesystem::path path) noexcept + : m_fd(std::move(fd)) + , m_path(std::move(path)) +{ +} + +temporary_file::temporary_file(nihil::fd &&fd) noexcept + : m_fd(std::move(fd)) +{ +} + +temporary_file::temporary_file(temporary_file &&other) noexcept + : m_fd(std::move(other.m_fd)) + , m_path(std::move(other.m_path)) +{ +} + +temporary_file::~temporary_file() //NOLINT(bugprone-exception-escape) +{ + if (m_fd) + release(); +} + +auto temporary_file::release(this temporary_file &self) -> void +{ + if (!self.m_fd) + throw std::logic_error( + "release() called on already-released tempfile"); + + if (!self.m_path.empty()) { + auto ec = std::error_code(); // ignore errors + remove(self.path(), ec); + + self.m_path.clear(); + } + + std::ignore = self.m_fd.close(); +} + +auto temporary_file::path(this temporary_file const &self) + -> std::filesystem::path const & +{ + if (self.m_path.empty()) + throw std::logic_error( + "path() called on unlinked temporary_file"); + + return self.m_path; +} + +auto temporary_file::fd(this temporary_file &self) -> nihil::fd & +{ + if (!self.m_fd) + throw std::logic_error("fd() called on empty temporary_file"); + + return self.m_fd; +} + +auto tempfile(tempfile_flags_t flags) -> std::expected<temporary_file, error> +{ + auto rng = std::default_random_engine(std::random_device{}()); + + auto random_name = [&] -> std::string { + auto constexpr length = std::size_t{12}; + auto constexpr randchars = std::string_view( + "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "0123456789"); + + auto dist = std::uniform_int_distribution<>( + 0, randchars.size() - 1); + auto ret = std::string(length, 0); + std::ranges::generate_n(ret.begin(), length, + [&] -> char { + return randchars[dist(rng)]; + }); + return ret; + }; + + auto dir = std::filesystem::path(getenv("TMPDIR").value_or("/tmp")); + + // Keep trying until we don't get EEXIST. + for (;;) { + auto filename = dir / (random_name() + ".tmp"); + auto fd = nihil::open(filename, O_RDWR | O_CREAT | O_EXCL, + 0600); + if (!fd) { + if (fd.error() == std::errc::file_exists) + continue; + return std::unexpected(fd.error()); + } + + if (flags & tempfile_unlink) { + auto ec = std::error_code(); + remove(filename, ec); + return temporary_file(std::move(*fd)); + } else { + return temporary_file(std::move(*fd), + std::move(filename)); + } + } +} + +} // namespace nihil diff --git a/nihil.posix/tempfile.ccm b/nihil.posix/tempfile.ccm new file mode 100644 index 0000000..82f3be4 --- /dev/null +++ b/nihil.posix/tempfile.ccm @@ -0,0 +1,87 @@ +/* + * This source code is released into the public domain. + */ + +module; + +/* + * tempfile: create a temporary file. + */ + +#include <cstdint> +#include <expected> +#include <filesystem> +#include <string> + +export module nihil.posix:tempfile; + +import nihil.error; +import nihil.flagset; +import :fd; + +namespace nihil { + +struct tempfile_flags_tag {}; +export using tempfile_flags_t = flagset<std::uint8_t, tempfile_flags_tag>; + +// No flags. +export inline constexpr auto tempfile_none = tempfile_flags_t(); + +// Unlink the tempfile immediately after creating it +export inline constexpr auto tempfile_unlink = tempfile_flags_t::bit<0>(); + +export struct temporary_file final { + /* + * Fetch the file's fd. + */ + [[nodiscard]] auto fd(this temporary_file &) -> nihil::fd &; + + /* + * Fetch the name of this file. If tempfile_unlink was specified, + * throws std::logic_error. + */ + [[nodiscard]] auto path(this temporary_file const &) + -> std::filesystem::path const &; + + /* + * Release this temporary file, causing it to be deleted immediately. + * Throws std::logic_error if the file has already been released. + */ + auto release(this temporary_file &) -> void; + + /* + * Destructor; unlink the file if we didn't already. + */ + ~temporary_file(); + + // Not copyable. + temporary_file(temporary_file const &) = delete; + + // Movable. + temporary_file(temporary_file &&other) noexcept; + + // Not assignable. + auto operator=(this temporary_file &, temporary_file const &) + -> temporary_file & = delete; + auto operator=(this temporary_file &, temporary_file &&) noexcept + -> temporary_file & = delete; + +private: + // The file descriptor for the file. + nihil::fd m_fd; + std::filesystem::path m_path; + + temporary_file(nihil::fd &&fd, std::filesystem::path) noexcept; + temporary_file(nihil::fd &&fd) noexcept; + + friend auto tempfile(tempfile_flags_t flags) + -> std::expected<temporary_file, error>; +}; + +/* + * Create a temporary file and return it. + */ +export [[nodiscard]] auto tempfile(tempfile_flags_t flags = tempfile_none) + -> std::expected<temporary_file, error>; + +} // namespace nihil diff --git a/nihil.posix/test.fd.cc b/nihil.posix/test.fd.cc new file mode 100644 index 0000000..6b6394b --- /dev/null +++ b/nihil.posix/test.fd.cc @@ -0,0 +1,199 @@ +/* + * This source code is released into the public domain. + */ + +#include <span> +#include <stdexcept> + +#include <stdio.h> +#include <fcntl.h> + +#include <catch2/catch_test_macros.hpp> + +import nihil.error; +import nihil.posix; + +using namespace std::literals; + +namespace { + +// Test if an fd is open. +auto fd_is_open(int fd) -> bool { + auto const ret = ::fcntl(fd, F_GETFL); + return ret == 0; +} + +} // anonymous namespace + +TEST_CASE("fd: construct empty", "[fd]") { + nihil::fd fd; + + REQUIRE(!fd); + REQUIRE_THROWS_AS(fd.get(), std::logic_error); +} + +TEST_CASE("fd: construct from fd", "[fd]") { + auto file = ::open("/dev/null", O_RDONLY); + REQUIRE(file > 0); + + { + auto fd = nihil::fd(file); + REQUIRE(fd_is_open(fd.get())); + } + + REQUIRE(!fd_is_open(file)); +} + +TEST_CASE("fd: close", "[fd]") { + auto file = ::open("/dev/null", O_RDONLY); + REQUIRE(file > 0); + + auto fd = nihil::fd(file); + REQUIRE(fd); + + auto const ret = fd.close(); + REQUIRE(ret); + REQUIRE(!fd_is_open(file)); +} + +TEST_CASE("fd: move construct", "[fd]") { + auto file = ::open("/dev/null", O_RDONLY); + REQUIRE(file > 0); + + auto fd1 = nihil::fd(file); + REQUIRE(fd_is_open(fd1.get())); + + auto fd2(std::move(fd1)); + REQUIRE(!fd1); //NOLINT + REQUIRE(fd2); + REQUIRE(fd2.get() == file); + REQUIRE(fd_is_open(file)); +} + +TEST_CASE("fd: move assign", "[fd]") { + auto file = ::open("/dev/null", O_RDONLY); + REQUIRE(file > 0); + + auto fd1 = nihil::fd(file); + REQUIRE(fd_is_open(fd1.get())); + + auto fd2 = nihil::fd(); + REQUIRE(!fd2); + + fd2 = std::move(fd1); + + REQUIRE(!fd1); //NOLINT + REQUIRE(fd2); + REQUIRE(fd2.get() == file); + REQUIRE(fd_is_open(file)); +} + +TEST_CASE("fd: release", "[fd]") { + auto file = ::open("/dev/null", O_RDONLY); + REQUIRE(file > 0); + + auto fd = nihil::fd(file); + auto fdesc = std::move(fd).release(); + REQUIRE(!fd); //NOLINT + REQUIRE(fdesc == file); +} + +TEST_CASE("fd: dup", "[fd]") { + auto file = ::open("/dev/null", O_RDONLY); + REQUIRE(file > 0); + + auto fd = nihil::fd(file); + REQUIRE(fd); + + auto fd2 = dup(fd); + REQUIRE(fd2); + REQUIRE(fd.get() != fd2->get()); +} + +TEST_CASE("fd: dup2", "[fd]") { + auto file = ::open("/dev/null", O_RDONLY); + REQUIRE(file > 0); + + REQUIRE(!fd_is_open(666)); + + auto fd = nihil::fd(file); + auto fd2 = dup(fd, 666); + + REQUIRE(fd); + REQUIRE(fd2); + REQUIRE(fd2->get() == 666); +} + +TEST_CASE("fd: flags", "[fd]") { + auto file = ::open("/dev/null", O_RDONLY); + REQUIRE(file > 0); + + auto fd = nihil::fd(file); + + { + auto const ret = replaceflags(fd, 0); + REQUIRE(ret); + REQUIRE(getflags(fd) == 0); + } + + { + auto const ret = setflags(fd, O_NONBLOCK); + REQUIRE(ret == O_NONBLOCK); + REQUIRE(getflags(fd) == O_NONBLOCK); + } + + { + auto const ret = setflags(fd, O_SYNC); + REQUIRE(ret == (O_NONBLOCK|O_SYNC)); + REQUIRE(getflags(fd) == (O_NONBLOCK|O_SYNC)); + } + + { + auto const ret = clearflags(fd, O_NONBLOCK); + REQUIRE(ret == O_SYNC); + REQUIRE(getflags(fd) == O_SYNC); + } +} + +TEST_CASE("fd: fdflags", "[fd]") { + auto file = ::open("/dev/null", O_RDONLY); + REQUIRE(file > 0); + + auto fd = nihil::fd(file); + + { + auto const ret = replacefdflags(fd, 0); + REQUIRE(ret); + REQUIRE(getfdflags(fd) == 0); + } + + { + auto const ret = setfdflags(fd, FD_CLOEXEC); + REQUIRE(ret == FD_CLOEXEC); + REQUIRE(getfdflags(fd) == FD_CLOEXEC); + } + + { + auto const ret = clearfdflags(fd, FD_CLOEXEC); + REQUIRE(ret == 0); + REQUIRE(getfdflags(fd) == 0); + } +} + +TEST_CASE("fd: pipe, read, write", "[fd]") { + auto fds = nihil::pipe(); + REQUIRE(fds); + + auto [fd1, fd2] = std::move(*fds); + + auto constexpr test_string = "test string"sv; + + auto ret = write(fd1, test_string); + REQUIRE(ret); + REQUIRE(*ret == test_string.size()); + + auto readbuf = std::array<char, test_string.size() * 2>{}; + auto read_buf = read(fd2, readbuf); + REQUIRE(read_buf); + REQUIRE(std::string_view(*read_buf) == test_string); +} diff --git a/nihil.posix/test.getenv.cc b/nihil.posix/test.getenv.cc new file mode 100644 index 0000000..9e10c16 --- /dev/null +++ b/nihil.posix/test.getenv.cc @@ -0,0 +1,50 @@ +/* + * This source code is released into the public domain. + */ + +#include <ranges> +#include <string> +#include <system_error> + +#include <unistd.h> + +#include <catch2/catch_test_macros.hpp> + +import nihil.error; +import nihil.posix; + +TEST_CASE("getenv: existing value", "[getenv]") +{ + auto constexpr *name = "NIHIL_TEST_VAR"; + auto constexpr *value = "test is a test"; + + REQUIRE(::setenv(name, value, 1) == 0); + + auto const s = nihil::getenv(name); + REQUIRE(s); + REQUIRE(*s == value); +} + +TEST_CASE("getenv: non-existing value", "[getenv]") +{ + auto constexpr *name = "NIHIL_TEST_VAR"; + + REQUIRE(::unsetenv(name) == 0); + + auto const s = nihil::getenv(name); + REQUIRE(!s); + REQUIRE(s.error() == std::errc::no_such_file_or_directory); +} + +// Force the call to getenv_r() to reallocate. +TEST_CASE("getenv: long value") +{ + auto constexpr *name = "NIHIL_TEST_VAR"; + auto const value = std::string(4096, 'a'); + + REQUIRE(::setenv(name, value.c_str(), 1) == 0); + + auto const s = nihil::getenv(name); + REQUIRE(s); + REQUIRE(*s == value); +} diff --git a/nihil.posix/test.spawn.cc b/nihil.posix/test.spawn.cc new file mode 100644 index 0000000..da321ff --- /dev/null +++ b/nihil.posix/test.spawn.cc @@ -0,0 +1,117 @@ +/* + * This source code is released into the public domain. + */ + +#include <catch2/catch_test_macros.hpp> + +import nihil.posix; + +TEST_CASE("spawn: system", "[spawn]") +{ + using namespace nihil; + + auto exec = shell("x=1; echo $x"); + REQUIRE(exec); + + auto output = std::string(); + auto capture = make_capture(stdout_fileno, output); + REQUIRE(capture); + + auto proc = spawn(*exec, *capture); + REQUIRE(proc); + + auto status = std::move(*proc).wait(); + REQUIRE(status); + + REQUIRE(status->okay()); + REQUIRE(output == "1\n"); +} + +TEST_CASE("spawn: execv", "[spawn]") { + using namespace nihil; + + auto args = argv({"sh", "-c", "x=1; echo $x"}); + auto exec = execv("/bin/sh", std::move(args)); + REQUIRE(exec); + + auto output = std::string(); + auto capture = make_capture(stdout_fileno, output); + REQUIRE(capture); + + auto proc = spawn(*exec, *capture); + REQUIRE(proc); + + auto status = std::move(*proc).wait(); + REQUIRE(status); + + REQUIRE(status->okay()); + REQUIRE(output == "1\n"); +} + +TEST_CASE("spawn: execvp", "[spawn]") { + using namespace nihil; + + auto args = argv({"sh", "-c", "x=1; echo $x"}); + auto exec = execvp("sh", std::move(args)); + REQUIRE(exec); + + auto output = std::string(); + auto capture = make_capture(stdout_fileno, output); + REQUIRE(capture); + + auto proc = spawn(*exec, *capture); + REQUIRE(proc); + + auto status = std::move(*proc).wait(); + REQUIRE(status); + + REQUIRE(status->okay()); + REQUIRE(output == "1\n"); +} + +TEST_CASE("spawn: execl", "[spawn]") { + using namespace nihil; + + auto exec = execl("/bin/sh", "sh", "-c", "x=1; echo $x"); + REQUIRE(exec); + + auto output = std::string(); + auto capture = make_capture(stdout_fileno, output); + REQUIRE(capture); + + auto proc = spawn(*exec, *capture); + REQUIRE(proc); + + auto status = std::move(*proc).wait(); + REQUIRE(status); + + REQUIRE(status->okay()); + REQUIRE(output == "1\n"); +} + +TEST_CASE("spawn: execlp", "[spawn]") { + using namespace nihil; + + auto exec = execlp("sh", "sh", "-c", "x=1; echo $x"); + REQUIRE(exec); + + auto output = std::string(); + auto capture = make_capture(stdout_fileno, output); + REQUIRE(capture); + + auto proc = spawn(*exec, *capture); + REQUIRE(proc); + + auto status = std::move(*proc).wait(); + REQUIRE(status); + + REQUIRE(status->okay()); + REQUIRE(output == "1\n"); +} + +TEST_CASE("spawn: execlp failure", "[spawn]") { + using namespace nihil; + + auto exec = execlp("nihil_no_such_executable", "x"); + REQUIRE(!exec); +} diff --git a/nihil.posix/test.tempfile.cc b/nihil.posix/test.tempfile.cc new file mode 100644 index 0000000..b1c7604 --- /dev/null +++ b/nihil.posix/test.tempfile.cc @@ -0,0 +1,90 @@ +/* + * This source code is released into the public domain. + */ + +#include <filesystem> + +#include <catch2/catch_test_macros.hpp> + +import nihil.posix; + +TEST_CASE("posix.tempfile: create", "[nihil][nihil.posix]") +{ + auto file = nihil::tempfile(); + REQUIRE(file); + REQUIRE(file->fd()); + + auto path = file->path(); + REQUIRE(exists(path) == true); +} + +TEST_CASE("posix.tempfile: create and release", "[nihil][nihil.posix]") +{ + auto file = nihil::tempfile(); + REQUIRE(file); + REQUIRE(file->fd()); + + auto path = file->path(); + REQUIRE(exists(path) == true); + + file->release(); + REQUIRE(exists(path) == false); + + REQUIRE_THROWS_AS(file->fd(), std::logic_error); + REQUIRE_THROWS_AS(file->path(), std::logic_error); +} + +TEST_CASE("posix.tempfile: create and double release", "[nihil][nihil.posix]") +{ + auto file = nihil::tempfile(); + REQUIRE(file->fd()); + + auto path = file->path(); + REQUIRE(exists(path) == true); + + file->release(); + REQUIRE(exists(path) == false); + + REQUIRE_THROWS_AS(file->fd(), std::logic_error); + REQUIRE_THROWS_AS(file->release(), std::logic_error); + REQUIRE_THROWS_AS(file->path(), std::logic_error); +} + +TEST_CASE("posix.tempfile: create unlinked", "[nihil][nihil.posix]") +{ + auto file = nihil::tempfile(nihil::tempfile_unlink); + REQUIRE(file); + REQUIRE(file->fd()); + + REQUIRE_THROWS_AS(file->path(), std::logic_error); +} + +TEST_CASE("posix.tempfile: create unlinked and release", + "[nihil][nihil.posix]") +{ + auto file = nihil::tempfile(nihil::tempfile_unlink); + REQUIRE(file); + REQUIRE(file->fd()); + + REQUIRE_THROWS_AS(file->path(), std::logic_error); + + file->release(); + + REQUIRE_THROWS_AS(file->fd(), std::logic_error); + REQUIRE_THROWS_AS(file->path(), std::logic_error); +} + +TEST_CASE("posix.tempfile: create unlinked and double release", + "[nihil][nihil.posix]") +{ + auto file = nihil::tempfile(nihil::tempfile_unlink); + REQUIRE(file->fd()); + + REQUIRE_THROWS_AS(file->path(), std::logic_error); + + file->release(); + + REQUIRE_THROWS_AS(file->fd(), std::logic_error); + REQUIRE_THROWS_AS(file->release(), std::logic_error); + REQUIRE_THROWS_AS(file->path(), std::logic_error); +} diff --git a/nihil.posix/write_file.ccm b/nihil.posix/write_file.ccm new file mode 100644 index 0000000..867e0db --- /dev/null +++ b/nihil.posix/write_file.ccm @@ -0,0 +1,82 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include <coroutine> +#include <expected> +#include <filesystem> +#include <ranges> +#include <system_error> +#include <vector> + +#include <fcntl.h> +#include <unistd.h> + +export module nihil.posix:write_file; + +import nihil.error; +import nihil.guard; +import nihil.monad; +import :fd; +import :open; +import :rename; + +namespace nihil { + +/* + * Write the contents of a range to a file. Returns the number of bytes + * written. + */ +export [[nodiscard]] +auto write_file(std::filesystem::path const &filename, + std::ranges::contiguous_range auto &&range, + int mode = 0777) + -> std::expected<std::size_t, error> +{ + auto file = co_await open(filename, O_CREAT|O_WRONLY, mode); + auto nbytes = co_await write(file, range); + co_return nbytes; +} + +/* + * Utility wrapper for non-contiguous ranges. + */ +export [[nodiscard]] +auto write_file(std::filesystem::path const &filename, + std::ranges::range auto &&range) + -> std::expected<std::size_t, error> +requires(!std::ranges::contiguous_range<decltype(range)>) +{ + return write_file(filename, std::vector(std::from_range, range)); +} + +/* + * Write the contents of a range to a file safely. The data will be written + * to "<filename>.tmp", and if the write succeeds, the temporary file will be + * renamed to the target filename. If an error occurs, the target file will + * not be modified. + */ +export [[nodiscard]] +auto safe_write_file(std::filesystem::path const &filename, + std::ranges::range auto &&range) + -> std::expected<void, error> +{ + auto tmpfile = filename; + tmpfile.remove_filename(); + tmpfile /= (filename.filename().native() + ".tmp"); + + auto tmpfile_guard = guard([&tmpfile] { + ::unlink(tmpfile.c_str()); + }); + + co_await write_file(tmpfile, range); + co_await nihil::rename(tmpfile, filename); + + tmpfile_guard.release(); + co_return {}; +} + + +} // namespace nihil |
