From a2d7181700ac64b8e7a4472ec26dfa253b38f188 Mon Sep 17 00:00:00 2001 From: Lexi Winter Date: Sat, 28 Jun 2025 19:25:55 +0100 Subject: split nihil into separate modules --- nihil.posix/CMakeLists.txt | 51 +++++++++ nihil.posix/argv.cc | 65 ++++++++++++ nihil.posix/argv.ccm | 78 ++++++++++++++ nihil.posix/ensure_dir.cc | 30 ++++++ nihil.posix/ensure_dir.ccm | 23 ++++ nihil.posix/exec.cc | 71 +++++++++++++ nihil.posix/exec.ccm | 105 +++++++++++++++++++ nihil.posix/fd.cc | 220 ++++++++++++++++++++++++++++++++++++++ nihil.posix/fd.ccm | 157 ++++++++++++++++++++++++++++ nihil.posix/find_in_path.cc | 52 +++++++++ nihil.posix/getenv.cc | 45 ++++++++ nihil.posix/nihil.posix.ccm | 45 ++++++++ nihil.posix/open.cc | 31 ++++++ nihil.posix/open.ccm | 24 +++++ nihil.posix/process.cc | 102 ++++++++++++++++++ nihil.posix/process.ccm | 91 ++++++++++++++++ nihil.posix/read_file.ccm | 48 +++++++++ nihil.posix/rename.cc | 34 ++++++ nihil.posix/rename.ccm | 23 ++++ nihil.posix/spawn.ccm | 249 ++++++++++++++++++++++++++++++++++++++++++++ nihil.posix/test_fd.cc | 199 +++++++++++++++++++++++++++++++++++ nihil.posix/test_getenv.cc | 49 +++++++++ nihil.posix/test_spawn.cc | 117 +++++++++++++++++++++ nihil.posix/write_file.ccm | 82 +++++++++++++++ 24 files changed, 1991 insertions(+) create mode 100644 nihil.posix/CMakeLists.txt create mode 100644 nihil.posix/argv.cc create mode 100644 nihil.posix/argv.ccm create mode 100644 nihil.posix/ensure_dir.cc create mode 100644 nihil.posix/ensure_dir.ccm create mode 100644 nihil.posix/exec.cc create mode 100644 nihil.posix/exec.ccm create mode 100644 nihil.posix/fd.cc create mode 100644 nihil.posix/fd.ccm create mode 100644 nihil.posix/find_in_path.cc create mode 100644 nihil.posix/getenv.cc create mode 100644 nihil.posix/nihil.posix.ccm create mode 100644 nihil.posix/open.cc create mode 100644 nihil.posix/open.ccm create mode 100644 nihil.posix/process.cc create mode 100644 nihil.posix/process.ccm create mode 100644 nihil.posix/read_file.ccm create mode 100644 nihil.posix/rename.cc create mode 100644 nihil.posix/rename.ccm create mode 100644 nihil.posix/spawn.ccm create mode 100644 nihil.posix/test_fd.cc create mode 100644 nihil.posix/test_getenv.cc create mode 100644 nihil.posix/test_spawn.cc create mode 100644 nihil.posix/write_file.ccm (limited to 'nihil.posix') diff --git a/nihil.posix/CMakeLists.txt b/nihil.posix/CMakeLists.txt new file mode 100644 index 0000000..db5e5aa --- /dev/null +++ b/nihil.posix/CMakeLists.txt @@ -0,0 +1,51 @@ +# This source code is released into the public domain. + +add_library(nihil.posix STATIC) +target_link_libraries(nihil.posix PRIVATE nihil.error nihil.guard nihil.monad) + +target_sources(nihil.posix + PUBLIC FILE_SET modules TYPE CXX_MODULES FILES + nihil.posix.ccm + argv.ccm + ensure_dir.ccm + exec.ccm + fd.ccm + open.ccm + process.ccm + read_file.ccm + rename.ccm + spawn.ccm + write_file.ccm + + PRIVATE + argv.cc + ensure_dir.cc + exec.cc + getenv.cc + fd.cc + find_in_path.cc + open.cc + process.cc + rename.cc +) + +if(NIHIL_TESTS) + enable_testing() + + add_executable(nihil.posix.test + test_fd.cc + test_getenv.cc + test_spawn.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 +#include +#include +#include + +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(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 +#include +#include +#include + +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 + explicit argv(std::initializer_list &&args) + : argv(std::from_range, std::forward(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 because we need an array of + // char pointers to pass to exec. + std::vector 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 +#include +#include +#include + +module nihil.posix; + +import nihil.error; + +namespace nihil { + +auto ensure_dir(std::filesystem::path const &dir) -> std::expected +{ + 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 +#include + +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; + +} // 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 +#include +#include +#include +#include + +#include +#include +#include + +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 +{ + ::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 +{ + 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 +{ + 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 +{ + 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 +#include + +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 +concept executor = + requires (T e) { + std::same_as::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; + + // 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; + +/* + * 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; + +/* + * 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 +{ + 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 +{ + 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; + +} // 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 +#include + +#include +#include +#include +#include +#include + +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 +{ + 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 +{ + 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 +{ + 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 +{ + 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 +{ + 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 +{ + 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 +{ + 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 +{ + auto flags = co_await getflags(self); + + flags |= newflags; + co_await replaceflags(self, flags); + + co_return flags; +} + +auto clearflags(fd &self, int clrflags) -> std::expected +{ + auto flags = co_await getflags(self); + + flags &= ~clrflags; + co_await replaceflags(self, flags); + + co_return flags; +} + +auto getfdflags(fd const &self) -> std::expected +{ + 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 +{ + 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 +{ + auto flags = co_await getfdflags(self); + + flags |= newflags; + co_await replacefdflags(self, flags); + + co_return flags; +} + +auto clearfdflags(fd &self, int clrflags) -> std::expected +{ + auto flags = co_await getfdflags(self); + + flags &= ~clrflags; + co_await replacefdflags(self, flags); + + co_return flags; +} + +auto pipe() -> std::expected, error> +{ + auto fds = std::array{}; + + 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 buffer) + -> std::expected +{ + 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 buffer) + -> std::expected, 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 +#include +#include +#include +#include +#include + +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; + + // 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::expected; + + // 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::expected, 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; + +// 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; + +// Create a copy of this fd by calling dup(). +export [[nodiscard]] auto raw_dup(fd const &self) + -> std::expected; + +// Create a copy of this fd by calling dup2(). +export [[nodiscard]] auto raw_dup(fd const &self, int newfd) + -> std::expected; + +// Return the fnctl flags for this fd. +export [[nodiscard]] auto getflags(fd const &self) + -> std::expected; + +// Replace the fnctl flags for this fd. +export [[nodiscard]] auto replaceflags(fd &self, int newflags) + -> std::expected; + +// Add bits to the fcntl flags for this fd. Returns the new flags. +export [[nodiscard]] auto setflags(fd &self, int newflags) + -> std::expected; + +// Remove bits from the fcntl flags for this fd. Returns the new flags. +export [[nodiscard]] auto clearflags(fd &self, int clrflags) + -> std::expected; + +// Return the fd flags for this fd. +export [[nodiscard]] auto getfdflags(fd const &self) + -> std::expected; + +// Replace the fd flags for this fd. +export [[nodiscard]] auto replacefdflags(fd &self, int newflags) + -> std::expected; + +// Add bits to the fd flags for this fd. Returns the new flags. +export [[nodiscard]] auto setfdflags(fd &self, int newflags) + -> std::expected; + +// Remove bits from the fd flags for this fd. Returns the new flags. +export [[nodiscard]] auto clearfdflags(fd &self, int clrflags) + -> std::expected; + +// Create two fds by calling pipe() and return them. +export [[nodiscard]] auto pipe() -> std::expected, 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 +requires(sizeof(std::ranges::range_value_t) == 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>, + error> +requires(sizeof(std::ranges::range_value_t) == 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/find_in_path.cc b/nihil.posix/find_in_path.cc new file mode 100644 index 0000000..6be963c --- /dev/null +++ b/nihil.posix/find_in_path.cc @@ -0,0 +1,52 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include +#include +#include + +#include +#include + +module nihil.posix; + +namespace nihil { + +auto find_in_path(std::filesystem::path const &file) + -> std::optional +{ + using namespace std::literals; + + auto try_open = + [](std::filesystem::path const &file) -> std::optional + { + 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/getenv.cc b/nihil.posix/getenv.cc new file mode 100644 index 0000000..36df950 --- /dev/null +++ b/nihil.posix/getenv.cc @@ -0,0 +1,45 @@ + +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include +#include +#include +#include + +#include + +module nihil.posix; + +import nihil.error; + +namespace nihil { + +auto getenv(std::string_view varname) -> std::expected +{ + // Start with a buffer of this size, and double it every iteration. + constexpr auto bufinc = std::size_t{1024}; + + auto cvarname = std::string(varname); + auto buf = std::vector(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))); + } +} + +} // namespace nihil diff --git a/nihil.posix/nihil.posix.ccm b/nihil.posix/nihil.posix.ccm new file mode 100644 index 0000000..9baecf8 --- /dev/null +++ b/nihil.posix/nihil.posix.ccm @@ -0,0 +1,45 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include +#include +#include + +export module nihil.posix; + +import nihil.error; + +export import :argv; +export import :ensure_dir; +export import :exec; +export import :fd; +export import :open; +export import :process; +export import :read_file; +export import :rename; +export import :spawn; +export import :write_file; + +export namespace nihil { + +/* + * Find a variable by the given name in the environment by calling getenv_r(). + */ + +[[nodiscard]] auto getenv(std::string_view varname) + -> std::expected; + + +/* + * Find an executable in $PATH, open it with O_EXEC and return the fd. + * If $PATH is not set, uses _PATH_DEFPATH. If the file can't be found + * or opened, returns std::nullopt. + */ +[[nodiscard]] auto find_in_path(std::filesystem::path const &file) + -> std::optional; + +} // 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 +#include +#include + +#include +#include + +module nihil.posix; + +import nihil.error; +import :fd; + +namespace nihil { + +auto open(std::filesystem::path const &filename, int flags, int mode) + -> std::expected +{ + 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 +#include + +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; + +} // namespace nihil 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 +#include +#include +#include +#include +#include +#include + +#include +#include + +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 +{ + if (WIFEXITED(self._status)) + return WEXITSTATUS(self._status); + return {}; +} + +auto wait_result::signal(this wait_result const &self) -> std::optional +{ + 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 +{ + 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 +#include +#include +#include + +#include + +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; + + // Return the exit signal, if any. + [[nodiscard]] auto signal(this wait_result const &self) + -> std::optional; + +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; + + /* + * 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..c950f67 --- /dev/null +++ b/nihil.posix/read_file.ccm @@ -0,0 +1,48 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include +#include +#include +#include +#include + +#include +#include + +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 auto &&iter) + -> std::expected +{ + auto file = co_await open(filename, O_RDONLY); + + auto constexpr bufsize = std::size_t{1024}; + auto buffer = std::array{}; + + 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 +#include + +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 +{ + 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 +#include + +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; + +} // namespace nihil diff --git a/nihil.posix/spawn.ccm b/nihil.posix/spawn.ccm new file mode 100644 index 0000000..5812716 --- /dev/null +++ b/nihil.posix/spawn.ccm @@ -0,0 +1,249 @@ +/* + * This source code is released into the public domain. + */ + +module; + +/* + * spawn(): fork and execute a child process. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +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 +{ + 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 +{ + 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 +{ + return make_fd_file(stdin_fileno, "/dev/null", O_RDONLY); +} + +export [[nodiscard]] inline auto +stdout_devnull() -> std::expected +{ + return make_fd_file(stdout_fileno, "/dev/null", O_WRONLY); +} + +export [[nodiscard]] inline auto +stderr_devnull() -> std::expected +{ + 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 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(); + 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 auto &&it) + -> std::expected, error> +{ + auto pipe = co_await make_fd_pipe(fdno); + co_return fd_capture(std::move(pipe), + std::forward(it)); +} + +export [[nodiscard]] auto +make_capture(int fdno, std::string &str) + -> std::expected, 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 +{ + 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. Release the process so we don't + // try to wait for ourselves, then run child handlers and + // exec the process. + + std::ignore = std::move(proc).release(); + (actions.run_in_child(proc), ...); + + 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/test_fd.cc b/nihil.posix/test_fd.cc new file mode 100644 index 0000000..8dff323 --- /dev/null +++ b/nihil.posix/test_fd.cc @@ -0,0 +1,199 @@ +/* + * This source code is released into the public domain. + */ + +#include +#include + +#include +#include + +#include + +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); + 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); + 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); + 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{}; + 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..fdb5277 --- /dev/null +++ b/nihil.posix/test_getenv.cc @@ -0,0 +1,49 @@ +/* + * This source code is released into the public domain. + */ + +#include +#include +#include + +#include + +#include + +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 + +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/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 +#include +#include +#include +#include +#include + +#include +#include + +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 +{ + 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 +requires(!std::ranges::contiguous_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 ".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 +{ + 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 -- cgit v1.2.3