diff options
| author | Lexi Winter <lexi@le-fay.org> | 2025-06-28 20:40:25 +0100 |
|---|---|---|
| committer | Lexi Winter <lexi@le-fay.org> | 2025-06-28 20:40:25 +0100 |
| commit | c54ff48ac3abb62a40eb1a438da8e3e7ef139797 (patch) | |
| tree | 4a78c556c7cbc6d3d7e364ca0c52c57ac0f5094b /nihil.posix | |
| parent | a2d7181700ac64b8e7a4472ec26dfa253b38f188 (diff) | |
| download | nihil-c54ff48ac3abb62a40eb1a438da8e3e7ef139797.tar.gz nihil-c54ff48ac3abb62a40eb1a438da8e3e7ef139797.tar.bz2 | |
posix: add tempfile()
Diffstat (limited to 'nihil.posix')
| -rw-r--r-- | nihil.posix/CMakeLists.txt | 54 | ||||
| -rw-r--r-- | nihil.posix/posix.argv.cc (renamed from nihil.posix/argv.cc) | 0 | ||||
| -rw-r--r-- | nihil.posix/posix.argv.ccm (renamed from nihil.posix/argv.ccm) | 0 | ||||
| -rw-r--r-- | nihil.posix/posix.ccm (renamed from nihil.posix/nihil.posix.ccm) | 10 | ||||
| -rw-r--r-- | nihil.posix/posix.ensure_dir.cc (renamed from nihil.posix/ensure_dir.cc) | 0 | ||||
| -rw-r--r-- | nihil.posix/posix.ensure_dir.ccm (renamed from nihil.posix/ensure_dir.ccm) | 0 | ||||
| -rw-r--r-- | nihil.posix/posix.exec.cc (renamed from nihil.posix/exec.cc) | 0 | ||||
| -rw-r--r-- | nihil.posix/posix.exec.ccm (renamed from nihil.posix/exec.ccm) | 0 | ||||
| -rw-r--r-- | nihil.posix/posix.fd.cc (renamed from nihil.posix/fd.cc) | 0 | ||||
| -rw-r--r-- | nihil.posix/posix.fd.ccm (renamed from nihil.posix/fd.ccm) | 0 | ||||
| -rw-r--r-- | nihil.posix/posix.find_in_path.cc (renamed from nihil.posix/find_in_path.cc) | 0 | ||||
| -rw-r--r-- | nihil.posix/posix.getenv.cc (renamed from nihil.posix/getenv.cc) | 0 | ||||
| -rw-r--r-- | nihil.posix/posix.getenv.ccm | 23 | ||||
| -rw-r--r-- | nihil.posix/posix.open.cc (renamed from nihil.posix/open.cc) | 0 | ||||
| -rw-r--r-- | nihil.posix/posix.open.ccm (renamed from nihil.posix/open.ccm) | 0 | ||||
| -rw-r--r-- | nihil.posix/posix.process.cc (renamed from nihil.posix/process.cc) | 0 | ||||
| -rw-r--r-- | nihil.posix/posix.process.ccm (renamed from nihil.posix/process.ccm) | 0 | ||||
| -rw-r--r-- | nihil.posix/posix.read_file.ccm (renamed from nihil.posix/read_file.ccm) | 0 | ||||
| -rw-r--r-- | nihil.posix/posix.rename.cc (renamed from nihil.posix/rename.cc) | 0 | ||||
| -rw-r--r-- | nihil.posix/posix.rename.ccm (renamed from nihil.posix/rename.ccm) | 0 | ||||
| -rw-r--r-- | nihil.posix/posix.spawn.ccm (renamed from nihil.posix/spawn.ccm) | 0 | ||||
| -rw-r--r-- | nihil.posix/posix.tempfile.cc | 142 | ||||
| -rw-r--r-- | nihil.posix/posix.tempfile.ccm | 85 | ||||
| -rw-r--r-- | nihil.posix/posix.write_file.ccm (renamed from nihil.posix/write_file.ccm) | 0 | ||||
| -rw-r--r-- | nihil.posix/test.fd.cc (renamed from nihil.posix/test_fd.cc) | 0 | ||||
| -rw-r--r-- | nihil.posix/test.getenv.cc (renamed from nihil.posix/test_getenv.cc) | 0 | ||||
| -rw-r--r-- | nihil.posix/test.spawn.cc (renamed from nihil.posix/test_spawn.cc) | 0 | ||||
| -rw-r--r-- | nihil.posix/test.tempfile.cc | 90 |
28 files changed, 372 insertions, 32 deletions
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/posix.argv.cc index e6b1389..e6b1389 100644 --- a/nihil.posix/argv.cc +++ b/nihil.posix/posix.argv.cc diff --git a/nihil.posix/argv.ccm b/nihil.posix/posix.argv.ccm index 6f60f4b..6f60f4b 100644 --- a/nihil.posix/argv.ccm +++ b/nihil.posix/posix.argv.ccm diff --git a/nihil.posix/nihil.posix.ccm b/nihil.posix/posix.ccm index 9baecf8..e63ad6b 100644 --- a/nihil.posix/nihil.posix.ccm +++ b/nihil.posix/posix.ccm @@ -17,24 +17,18 @@ 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 a variable by the given name in the environment by calling getenv_r(). - */ - -[[nodiscard]] auto getenv(std::string_view varname) - -> std::expected<std::string, error>; - - -/* * 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. diff --git a/nihil.posix/ensure_dir.cc b/nihil.posix/posix.ensure_dir.cc index 88e8898..88e8898 100644 --- a/nihil.posix/ensure_dir.cc +++ b/nihil.posix/posix.ensure_dir.cc diff --git a/nihil.posix/ensure_dir.ccm b/nihil.posix/posix.ensure_dir.ccm index fa92a90..fa92a90 100644 --- a/nihil.posix/ensure_dir.ccm +++ b/nihil.posix/posix.ensure_dir.ccm diff --git a/nihil.posix/exec.cc b/nihil.posix/posix.exec.cc index 5bdcbf7..5bdcbf7 100644 --- a/nihil.posix/exec.cc +++ b/nihil.posix/posix.exec.cc diff --git a/nihil.posix/exec.ccm b/nihil.posix/posix.exec.ccm index 6098318..6098318 100644 --- a/nihil.posix/exec.ccm +++ b/nihil.posix/posix.exec.ccm diff --git a/nihil.posix/fd.cc b/nihil.posix/posix.fd.cc index 6d5e54f..6d5e54f 100644 --- a/nihil.posix/fd.cc +++ b/nihil.posix/posix.fd.cc diff --git a/nihil.posix/fd.ccm b/nihil.posix/posix.fd.ccm index b937f46..b937f46 100644 --- a/nihil.posix/fd.ccm +++ b/nihil.posix/posix.fd.ccm diff --git a/nihil.posix/find_in_path.cc b/nihil.posix/posix.find_in_path.cc index 6be963c..6be963c 100644 --- a/nihil.posix/find_in_path.cc +++ b/nihil.posix/posix.find_in_path.cc diff --git a/nihil.posix/getenv.cc b/nihil.posix/posix.getenv.cc index 36df950..36df950 100644 --- a/nihil.posix/getenv.cc +++ b/nihil.posix/posix.getenv.cc 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 <expected> +#include <string> + +export module nihil.posix:getenv; + +import nihil.error; + +namespace nihil { + +/* + * Find a variable by the given name in the environment by calling getenv_r(). + */ + +export [[nodiscard]] auto getenv(std::string_view varname) + -> std::expected<std::string, error>; + +} // namespace nihil diff --git a/nihil.posix/open.cc b/nihil.posix/posix.open.cc index 9ef6538..9ef6538 100644 --- a/nihil.posix/open.cc +++ b/nihil.posix/posix.open.cc diff --git a/nihil.posix/open.ccm b/nihil.posix/posix.open.ccm index eaedacd..eaedacd 100644 --- a/nihil.posix/open.ccm +++ b/nihil.posix/posix.open.ccm diff --git a/nihil.posix/process.cc b/nihil.posix/posix.process.cc index 70e84b7..70e84b7 100644 --- a/nihil.posix/process.cc +++ b/nihil.posix/posix.process.cc diff --git a/nihil.posix/process.ccm b/nihil.posix/posix.process.ccm index 425deac..425deac 100644 --- a/nihil.posix/process.ccm +++ b/nihil.posix/posix.process.ccm diff --git a/nihil.posix/read_file.ccm b/nihil.posix/posix.read_file.ccm index c950f67..c950f67 100644 --- a/nihil.posix/read_file.ccm +++ b/nihil.posix/posix.read_file.ccm diff --git a/nihil.posix/rename.cc b/nihil.posix/posix.rename.cc index 9203d08..9203d08 100644 --- a/nihil.posix/rename.cc +++ b/nihil.posix/posix.rename.cc diff --git a/nihil.posix/rename.ccm b/nihil.posix/posix.rename.ccm index 796ec5b..796ec5b 100644 --- a/nihil.posix/rename.ccm +++ b/nihil.posix/posix.rename.ccm diff --git a/nihil.posix/spawn.ccm b/nihil.posix/posix.spawn.ccm index 5812716..5812716 100644 --- a/nihil.posix/spawn.ccm +++ b/nihil.posix/posix.spawn.ccm 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 <algorithm> +#include <coroutine> +#include <expected> +#include <filesystem> +#include <random> +#include <string> + +#include <fcntl.h> +#include <unistd.h> + +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<temporary_file, error> +{ + auto rng = std::default_random_engine(std::random_device{}()); + + auto random_name = [&] -> std::string { + auto constexpr length = std::size_t{12}; + auto constexpr randchars = std::string_view( + "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "0123456789"); + + auto dist = std::uniform_int_distribution<>( + 0, randchars.size() - 1); + auto ret = std::string(length, 0); + std::ranges::generate_n(ret.begin(), length, + [&] -> char { + return randchars[dist(rng)]; + }); + return ret; + }; + + auto dir = std::filesystem::path(getenv("TMPDIR").value_or("/tmp")); + + // Keep trying until we don't get EEXIST. + for (;;) { + auto filename = dir / (random_name() + ".tmp"); + auto fd = nihil::open(filename, O_RDWR | O_CREAT | O_EXCL, + 0600); + if (!fd) { + if (fd.error() == std::errc::file_exists) + continue; + return std::unexpected(fd.error()); + } + + if (flags & tempfile_unlink) { + auto ec = std::error_code(); + remove(filename, ec); + return temporary_file(std::move(*fd)); + } else { + return temporary_file(std::move(*fd), + std::move(filename)); + } + } +} + +} // namespace nihil diff --git a/nihil.posix/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 <cstdint> +#include <expected> +#include <filesystem> +#include <string> + +export module nihil.posix:tempfile; + +import nihil.error; +import nihil.flagset; +import :fd; + +namespace nihil { + +struct tempfile_flags_tag {}; +export using tempfile_flags_t = flagset<std::uint8_t, tempfile_flags_tag>; + +// No flags. +export inline constexpr auto tempfile_none = tempfile_flags_t(); + +// Unlink the tempfile immediately after creating it +export inline constexpr auto tempfile_unlink = tempfile_flags_t::bit<0>(); + +export struct temporary_file final { + /* + * Fetch the file's fd. + */ + [[nodiscard]] auto fd(this temporary_file &) -> nihil::fd &; + + /* + * Fetch the name of this file. If tempfile_unlink was specified, + * throws std::logic_error. + */ + [[nodiscard]] auto path(this temporary_file const &) + -> std::filesystem::path const &; + + /* + * Release this temporary file, causing it to be deleted immediately. + * Throws std::logic_error if the file has already been released. + */ + auto release(this temporary_file &) -> void; + + /* + * Destructor; unlink the file if we didn't already. + */ + ~temporary_file(); + + // Not copyable. + temporary_file(temporary_file const &) = delete; + 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<temporary_file, error>; +}; + +/* + * Create a temporary file and return it. + */ +export [[nodiscard]] auto tempfile(tempfile_flags_t flags = tempfile_none) + -> std::expected<temporary_file, error>; + +} // namespace nihil diff --git a/nihil.posix/write_file.ccm b/nihil.posix/posix.write_file.ccm index 867e0db..867e0db 100644 --- a/nihil.posix/write_file.ccm +++ b/nihil.posix/posix.write_file.ccm diff --git a/nihil.posix/test_fd.cc b/nihil.posix/test.fd.cc index 8dff323..8dff323 100644 --- a/nihil.posix/test_fd.cc +++ b/nihil.posix/test.fd.cc diff --git a/nihil.posix/test_getenv.cc b/nihil.posix/test.getenv.cc index fdb5277..fdb5277 100644 --- a/nihil.posix/test_getenv.cc +++ b/nihil.posix/test.getenv.cc diff --git a/nihil.posix/test_spawn.cc b/nihil.posix/test.spawn.cc index da321ff..da321ff 100644 --- a/nihil.posix/test_spawn.cc +++ b/nihil.posix/test.spawn.cc diff --git a/nihil.posix/test.tempfile.cc b/nihil.posix/test.tempfile.cc new file mode 100644 index 0000000..b1c7604 --- /dev/null +++ b/nihil.posix/test.tempfile.cc @@ -0,0 +1,90 @@ +/* + * This source code is released into the public domain. + */ + +#include <filesystem> + +#include <catch2/catch_test_macros.hpp> + +import nihil.posix; + +TEST_CASE("posix.tempfile: create", "[nihil][nihil.posix]") +{ + auto file = nihil::tempfile(); + REQUIRE(file); + REQUIRE(file->fd()); + + auto path = file->path(); + REQUIRE(exists(path) == true); +} + +TEST_CASE("posix.tempfile: create and release", "[nihil][nihil.posix]") +{ + auto file = nihil::tempfile(); + REQUIRE(file); + REQUIRE(file->fd()); + + auto path = file->path(); + REQUIRE(exists(path) == true); + + file->release(); + REQUIRE(exists(path) == false); + + REQUIRE_THROWS_AS(file->fd(), std::logic_error); + REQUIRE_THROWS_AS(file->path(), std::logic_error); +} + +TEST_CASE("posix.tempfile: create and double release", "[nihil][nihil.posix]") +{ + auto file = nihil::tempfile(); + REQUIRE(file->fd()); + + auto path = file->path(); + REQUIRE(exists(path) == true); + + file->release(); + REQUIRE(exists(path) == false); + + REQUIRE_THROWS_AS(file->fd(), std::logic_error); + REQUIRE_THROWS_AS(file->release(), std::logic_error); + REQUIRE_THROWS_AS(file->path(), std::logic_error); +} + +TEST_CASE("posix.tempfile: create unlinked", "[nihil][nihil.posix]") +{ + auto file = nihil::tempfile(nihil::tempfile_unlink); + REQUIRE(file); + REQUIRE(file->fd()); + + REQUIRE_THROWS_AS(file->path(), std::logic_error); +} + +TEST_CASE("posix.tempfile: create unlinked and release", + "[nihil][nihil.posix]") +{ + auto file = nihil::tempfile(nihil::tempfile_unlink); + REQUIRE(file); + REQUIRE(file->fd()); + + REQUIRE_THROWS_AS(file->path(), std::logic_error); + + file->release(); + + REQUIRE_THROWS_AS(file->fd(), std::logic_error); + REQUIRE_THROWS_AS(file->path(), std::logic_error); +} + +TEST_CASE("posix.tempfile: create unlinked and double release", + "[nihil][nihil.posix]") +{ + auto file = nihil::tempfile(nihil::tempfile_unlink); + REQUIRE(file->fd()); + + REQUIRE_THROWS_AS(file->path(), std::logic_error); + + file->release(); + + REQUIRE_THROWS_AS(file->fd(), std::logic_error); + REQUIRE_THROWS_AS(file->release(), std::logic_error); + REQUIRE_THROWS_AS(file->path(), std::logic_error); +} |
