aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLexi Winter <lexi@le-fay.org>2025-06-28 20:40:25 +0100
committerLexi Winter <lexi@le-fay.org>2025-06-28 20:40:25 +0100
commitc54ff48ac3abb62a40eb1a438da8e3e7ef139797 (patch)
tree4a78c556c7cbc6d3d7e364ca0c52c57ac0f5094b
parenta2d7181700ac64b8e7a4472ec26dfa253b38f188 (diff)
downloadnihil-c54ff48ac3abb62a40eb1a438da8e3e7ef139797.tar.gz
nihil-c54ff48ac3abb62a40eb1a438da8e3e7ef139797.tar.bz2
posix: add tempfile()
-rw-r--r--nihil.posix/CMakeLists.txt54
-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.ccm23
-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.cc142
-rw-r--r--nihil.posix/posix.tempfile.ccm85
-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.cc90
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);
+}