From c54ff48ac3abb62a40eb1a438da8e3e7ef139797 Mon Sep 17 00:00:00 2001 From: Lexi Winter Date: Sat, 28 Jun 2025 20:40:25 +0100 Subject: posix: add tempfile() --- nihil.posix/CMakeLists.txt | 54 +++++---- 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/posix.argv.cc | 65 ++++++++++ nihil.posix/posix.argv.ccm | 78 ++++++++++++ nihil.posix/posix.ccm | 39 ++++++ nihil.posix/posix.ensure_dir.cc | 30 +++++ nihil.posix/posix.ensure_dir.ccm | 23 ++++ nihil.posix/posix.exec.cc | 71 +++++++++++ nihil.posix/posix.exec.ccm | 105 ++++++++++++++++ nihil.posix/posix.fd.cc | 220 +++++++++++++++++++++++++++++++++ nihil.posix/posix.fd.ccm | 157 ++++++++++++++++++++++++ nihil.posix/posix.find_in_path.cc | 52 ++++++++ nihil.posix/posix.getenv.cc | 45 +++++++ nihil.posix/posix.getenv.ccm | 23 ++++ nihil.posix/posix.open.cc | 31 +++++ nihil.posix/posix.open.ccm | 24 ++++ nihil.posix/posix.process.cc | 102 ++++++++++++++++ nihil.posix/posix.process.ccm | 91 ++++++++++++++ nihil.posix/posix.read_file.ccm | 48 ++++++++ nihil.posix/posix.rename.cc | 34 ++++++ nihil.posix/posix.rename.ccm | 23 ++++ nihil.posix/posix.spawn.ccm | 249 ++++++++++++++++++++++++++++++++++++++ nihil.posix/posix.tempfile.cc | 142 ++++++++++++++++++++++ nihil.posix/posix.tempfile.ccm | 85 +++++++++++++ nihil.posix/posix.write_file.ccm | 82 +++++++++++++ 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/test.tempfile.cc | 90 ++++++++++++++ nihil.posix/test_fd.cc | 199 ------------------------------ nihil.posix/test_getenv.cc | 49 -------- nihil.posix/test_spawn.cc | 117 ------------------ nihil.posix/write_file.ccm | 82 ------------- 51 files changed, 2304 insertions(+), 1964 deletions(-) delete mode 100644 nihil.posix/argv.cc delete mode 100644 nihil.posix/argv.ccm delete mode 100644 nihil.posix/ensure_dir.cc delete mode 100644 nihil.posix/ensure_dir.ccm delete mode 100644 nihil.posix/exec.cc delete mode 100644 nihil.posix/exec.ccm delete mode 100644 nihil.posix/fd.cc delete mode 100644 nihil.posix/fd.ccm delete mode 100644 nihil.posix/find_in_path.cc delete mode 100644 nihil.posix/getenv.cc delete mode 100644 nihil.posix/nihil.posix.ccm delete mode 100644 nihil.posix/open.cc delete mode 100644 nihil.posix/open.ccm create mode 100644 nihil.posix/posix.argv.cc create mode 100644 nihil.posix/posix.argv.ccm create mode 100644 nihil.posix/posix.ccm create mode 100644 nihil.posix/posix.ensure_dir.cc create mode 100644 nihil.posix/posix.ensure_dir.ccm create mode 100644 nihil.posix/posix.exec.cc create mode 100644 nihil.posix/posix.exec.ccm create mode 100644 nihil.posix/posix.fd.cc create mode 100644 nihil.posix/posix.fd.ccm create mode 100644 nihil.posix/posix.find_in_path.cc create mode 100644 nihil.posix/posix.getenv.cc create mode 100644 nihil.posix/posix.getenv.ccm create mode 100644 nihil.posix/posix.open.cc create mode 100644 nihil.posix/posix.open.ccm create mode 100644 nihil.posix/posix.process.cc create mode 100644 nihil.posix/posix.process.ccm create mode 100644 nihil.posix/posix.read_file.ccm create mode 100644 nihil.posix/posix.rename.cc create mode 100644 nihil.posix/posix.rename.ccm create mode 100644 nihil.posix/posix.spawn.ccm create mode 100644 nihil.posix/posix.tempfile.cc create mode 100644 nihil.posix/posix.tempfile.ccm create mode 100644 nihil.posix/posix.write_file.ccm delete mode 100644 nihil.posix/process.cc delete mode 100644 nihil.posix/process.ccm delete mode 100644 nihil.posix/read_file.ccm delete mode 100644 nihil.posix/rename.cc delete mode 100644 nihil.posix/rename.ccm delete 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/test.tempfile.cc delete mode 100644 nihil.posix/test_fd.cc delete mode 100644 nihil.posix/test_getenv.cc delete mode 100644 nihil.posix/test_spawn.cc delete mode 100644 nihil.posix/write_file.ccm (limited to 'nihil.posix') diff --git a/nihil.posix/CMakeLists.txt b/nihil.posix/CMakeLists.txt index db5e5aa..6f410ce 100644 --- a/nihil.posix/CMakeLists.txt +++ b/nihil.posix/CMakeLists.txt @@ -1,41 +1,47 @@ # 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_link_libraries(nihil.posix PRIVATE + nihil.error nihil.flagset 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 + posix.ccm + + posix.argv.ccm + posix.ensure_dir.ccm + posix.exec.ccm + posix.fd.ccm + posix.getenv.ccm + posix.open.ccm + posix.process.ccm + posix.read_file.ccm + posix.rename.ccm + posix.spawn.ccm + posix.tempfile.ccm + posix.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 + posix.argv.cc + posix.ensure_dir.cc + posix.exec.cc + posix.getenv.cc + posix.fd.cc + posix.find_in_path.cc + posix.open.cc + posix.process.cc + posix.rename.cc + posix.tempfile.cc ) if(NIHIL_TESTS) enable_testing() add_executable(nihil.posix.test - test_fd.cc - test_getenv.cc - test_spawn.cc + test.fd.cc + test.getenv.cc + test.spawn.cc + test.tempfile.cc ) target_link_libraries(nihil.posix.test PRIVATE diff --git a/nihil.posix/argv.cc b/nihil.posix/argv.cc deleted file mode 100644 index e6b1389..0000000 --- a/nihil.posix/argv.cc +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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 deleted file mode 100644 index 6f60f4b..0000000 --- a/nihil.posix/argv.ccm +++ /dev/null @@ -1,78 +0,0 @@ -/* - * 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 deleted file mode 100644 index 88e8898..0000000 --- a/nihil.posix/ensure_dir.cc +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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 deleted file mode 100644 index fa92a90..0000000 --- a/nihil.posix/ensure_dir.ccm +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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 deleted file mode 100644 index 5bdcbf7..0000000 --- a/nihil.posix/exec.cc +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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 deleted file mode 100644 index 6098318..0000000 --- a/nihil.posix/exec.ccm +++ /dev/null @@ -1,105 +0,0 @@ -/* - * 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 deleted file mode 100644 index 6d5e54f..0000000 --- a/nihil.posix/fd.cc +++ /dev/null @@ -1,220 +0,0 @@ -/* - * 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 deleted file mode 100644 index b937f46..0000000 --- a/nihil.posix/fd.ccm +++ /dev/null @@ -1,157 +0,0 @@ -/* - * 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 deleted file mode 100644 index 6be963c..0000000 --- a/nihil.posix/find_in_path.cc +++ /dev/null @@ -1,52 +0,0 @@ -/* - * 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 deleted file mode 100644 index 36df950..0000000 --- a/nihil.posix/getenv.cc +++ /dev/null @@ -1,45 +0,0 @@ - -/* - * 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 deleted file mode 100644 index 9baecf8..0000000 --- a/nihil.posix/nihil.posix.ccm +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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 deleted file mode 100644 index 9ef6538..0000000 --- a/nihil.posix/open.cc +++ /dev/null @@ -1,31 +0,0 @@ -/* - * 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 deleted file mode 100644 index eaedacd..0000000 --- a/nihil.posix/open.ccm +++ /dev/null @@ -1,24 +0,0 @@ -/* - * 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/posix.argv.cc b/nihil.posix/posix.argv.cc new file mode 100644 index 0000000..e6b1389 --- /dev/null +++ b/nihil.posix/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/posix.argv.ccm b/nihil.posix/posix.argv.ccm new file mode 100644 index 0000000..6f60f4b --- /dev/null +++ b/nihil.posix/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/posix.ccm b/nihil.posix/posix.ccm new file mode 100644 index 0000000..e63ad6b --- /dev/null +++ b/nihil.posix/posix.ccm @@ -0,0 +1,39 @@ +/* + * 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 :getenv; +export import :open; +export import :process; +export import :read_file; +export import :rename; +export import :spawn; +export import :tempfile; +export import :write_file; + +export namespace nihil { + +/* + * 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/posix.ensure_dir.cc b/nihil.posix/posix.ensure_dir.cc new file mode 100644 index 0000000..88e8898 --- /dev/null +++ b/nihil.posix/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/posix.ensure_dir.ccm b/nihil.posix/posix.ensure_dir.ccm new file mode 100644 index 0000000..fa92a90 --- /dev/null +++ b/nihil.posix/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/posix.exec.cc b/nihil.posix/posix.exec.cc new file mode 100644 index 0000000..5bdcbf7 --- /dev/null +++ b/nihil.posix/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/posix.exec.ccm b/nihil.posix/posix.exec.ccm new file mode 100644 index 0000000..6098318 --- /dev/null +++ b/nihil.posix/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/posix.fd.cc b/nihil.posix/posix.fd.cc new file mode 100644 index 0000000..6d5e54f --- /dev/null +++ b/nihil.posix/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/posix.fd.ccm b/nihil.posix/posix.fd.ccm new file mode 100644 index 0000000..b937f46 --- /dev/null +++ b/nihil.posix/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/posix.find_in_path.cc b/nihil.posix/posix.find_in_path.cc new file mode 100644 index 0000000..6be963c --- /dev/null +++ b/nihil.posix/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/posix.getenv.cc b/nihil.posix/posix.getenv.cc new file mode 100644 index 0000000..36df950 --- /dev/null +++ b/nihil.posix/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/posix.getenv.ccm b/nihil.posix/posix.getenv.ccm new file mode 100644 index 0000000..465f7e7 --- /dev/null +++ b/nihil.posix/posix.getenv.ccm @@ -0,0 +1,23 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include + +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; + +} // namespace nihil diff --git a/nihil.posix/posix.open.cc b/nihil.posix/posix.open.cc new file mode 100644 index 0000000..9ef6538 --- /dev/null +++ b/nihil.posix/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/posix.open.ccm b/nihil.posix/posix.open.ccm new file mode 100644 index 0000000..eaedacd --- /dev/null +++ b/nihil.posix/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/posix.process.cc b/nihil.posix/posix.process.cc new file mode 100644 index 0000000..70e84b7 --- /dev/null +++ b/nihil.posix/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/posix.process.ccm b/nihil.posix/posix.process.ccm new file mode 100644 index 0000000..425deac --- /dev/null +++ b/nihil.posix/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/posix.read_file.ccm b/nihil.posix/posix.read_file.ccm new file mode 100644 index 0000000..c950f67 --- /dev/null +++ b/nihil.posix/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/posix.rename.cc b/nihil.posix/posix.rename.cc new file mode 100644 index 0000000..9203d08 --- /dev/null +++ b/nihil.posix/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/posix.rename.ccm b/nihil.posix/posix.rename.ccm new file mode 100644 index 0000000..796ec5b --- /dev/null +++ b/nihil.posix/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/posix.spawn.ccm b/nihil.posix/posix.spawn.ccm new file mode 100644 index 0000000..5812716 --- /dev/null +++ b/nihil.posix/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/posix.tempfile.cc b/nihil.posix/posix.tempfile.cc new file mode 100644 index 0000000..942594c --- /dev/null +++ b/nihil.posix/posix.tempfile.cc @@ -0,0 +1,142 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include +#include +#include +#include +#include + +#include +#include + +module nihil.posix; + +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)) +{ +} + +auto temporary_file::operator=(this temporary_file &self, + temporary_file &&other) + noexcept -> temporary_file & +{ + if (&self != &other) { + if (self.m_fd) + self.release(); + + self.m_fd = std::move(other.m_fd); + self.m_path = std::move(other.m_path); + } + + return self; +} + +temporary_file::~temporary_file() +{ + 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 +{ + 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/posix.tempfile.ccm b/nihil.posix/posix.tempfile.ccm new file mode 100644 index 0000000..20378b5 --- /dev/null +++ b/nihil.posix/posix.tempfile.ccm @@ -0,0 +1,85 @@ +/* + * This source code is released into the public domain. + */ + +module; + +/* + * tempfile: create a temporary file. + */ + +#include +#include +#include +#include + +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; + +// 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; + auto operator=(this temporary_file &, temporary_file const &) + -> temporary_file & = delete; + + // Movable. + temporary_file(temporary_file &&other) noexcept; + auto operator=(this temporary_file &, temporary_file &&) noexcept + -> temporary_file &; + +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; +}; + +/* + * Create a temporary file and return it. + */ +export [[nodiscard]] auto tempfile(tempfile_flags_t flags = tempfile_none) + -> std::expected; + +} // namespace nihil diff --git a/nihil.posix/posix.write_file.ccm b/nihil.posix/posix.write_file.ccm new file mode 100644 index 0000000..867e0db --- /dev/null +++ b/nihil.posix/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 diff --git a/nihil.posix/process.cc b/nihil.posix/process.cc deleted file mode 100644 index 70e84b7..0000000 --- a/nihil.posix/process.cc +++ /dev/null @@ -1,102 +0,0 @@ -/* - * 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 deleted file mode 100644 index 425deac..0000000 --- a/nihil.posix/process.ccm +++ /dev/null @@ -1,91 +0,0 @@ -/* - * 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 deleted file mode 100644 index c950f67..0000000 --- a/nihil.posix/read_file.ccm +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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 deleted file mode 100644 index 9203d08..0000000 --- a/nihil.posix/rename.cc +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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 deleted file mode 100644 index 796ec5b..0000000 --- a/nihil.posix/rename.ccm +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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 deleted file mode 100644 index 5812716..0000000 --- a/nihil.posix/spawn.ccm +++ /dev/null @@ -1,249 +0,0 @@ -/* - * 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/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 + +#include + +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/test_fd.cc b/nihil.posix/test_fd.cc deleted file mode 100644 index 8dff323..0000000 --- a/nihil.posix/test_fd.cc +++ /dev/null @@ -1,199 +0,0 @@ -/* - * 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 deleted file mode 100644 index fdb5277..0000000 --- a/nihil.posix/test_getenv.cc +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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 deleted file mode 100644 index da321ff..0000000 --- a/nihil.posix/test_spawn.cc +++ /dev/null @@ -1,117 +0,0 @@ -/* - * 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 deleted file mode 100644 index 867e0db..0000000 --- a/nihil.posix/write_file.ccm +++ /dev/null @@ -1,82 +0,0 @@ -/* - * 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