summaryrefslogtreecommitdiffstats
path: root/src/libutil/cxx/file_util.cxx
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/libutil/cxx/file_util.cxx457
1 files changed, 457 insertions, 0 deletions
diff --git a/src/libutil/cxx/file_util.cxx b/src/libutil/cxx/file_util.cxx
new file mode 100644
index 0000000..2f031f0
--- /dev/null
+++ b/src/libutil/cxx/file_util.cxx
@@ -0,0 +1,457 @@
+/*
+ * Copyright 2023 Vsevolod Stakhov
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include "file_util.hxx"
+#include <fmt/core.h>
+#include "libutil/util.h"
+#include "libutil/unix-std.h"
+
+#define DOCTEST_CONFIG_IMPLEMENTATION_IN_DLL
+
+#include "doctest/doctest.h"
+
+namespace rspamd::util {
+
+auto raii_file::open(const char *fname, int flags) -> tl::expected<raii_file, error>
+{
+ int oflags = flags;
+#ifdef O_CLOEXEC
+ oflags |= O_CLOEXEC;
+#endif
+
+ if (fname == nullptr) {
+ return tl::make_unexpected(error{"cannot open file; filename is nullptr", EINVAL, error_category::CRITICAL});
+ }
+
+ auto fd = ::open(fname, oflags);
+
+ if (fd == -1) {
+ return tl::make_unexpected(error{fmt::format("cannot open file {}: {}", fname, ::strerror(errno)), errno});
+ }
+
+ auto ret = raii_file{fname, fd, false};
+
+ if (fstat(ret.fd, &ret.st) == -1) {
+ return tl::make_unexpected(error{fmt::format("cannot stat file {}: {}", fname, ::strerror(errno)), errno});
+ }
+
+ return ret;
+}
+
+auto raii_file::create(const char *fname, int flags, int perms) -> tl::expected<raii_file, error>
+{
+ int oflags = flags | O_CREAT;
+#ifdef O_CLOEXEC
+ oflags |= O_CLOEXEC;
+#endif
+
+ if (fname == nullptr) {
+ return tl::make_unexpected(error{"cannot create file; filename is nullptr", EINVAL, error_category::CRITICAL});
+ }
+
+ auto fd = ::open(fname, oflags, perms);
+
+ if (fd == -1) {
+ return tl::make_unexpected(error{fmt::format("cannot create file {}: {}", fname, ::strerror(errno)), errno});
+ }
+
+ auto ret = raii_file{fname, fd, false};
+
+ if (fstat(ret.fd, &ret.st) == -1) {
+ return tl::make_unexpected(error{fmt::format("cannot stat file {}: {}", fname, ::strerror(errno)), errno});
+ }
+
+ return ret;
+}
+
+auto raii_file::create_temp(const char *fname, int flags, int perms) -> tl::expected<raii_file, error>
+{
+ int oflags = flags;
+#ifdef O_CLOEXEC
+ oflags |= O_CLOEXEC | O_CREAT | O_EXCL;
+#endif
+ if (fname == nullptr) {
+ return tl::make_unexpected(error{"cannot open file; filename is nullptr", EINVAL, error_category::CRITICAL});
+ }
+
+ auto fd = ::open(fname, oflags, perms);
+
+ if (fd == -1) {
+ return tl::make_unexpected(error{fmt::format("cannot create file {}: {}", fname, ::strerror(errno)), errno});
+ }
+
+ auto ret = raii_file{fname, fd, true};
+
+ if (fstat(ret.fd, &ret.st) == -1) {
+ return tl::make_unexpected(error{fmt::format("cannot stat file {}: {}", fname, ::strerror(errno)), errno});
+ }
+
+ return ret;
+}
+
+auto raii_file::mkstemp(const char *pattern, int flags, int perms) -> tl::expected<raii_file, error>
+{
+ int oflags = flags;
+#ifdef O_CLOEXEC
+ oflags |= O_CLOEXEC | O_CREAT | O_EXCL;
+#endif
+ if (pattern == nullptr) {
+ return tl::make_unexpected(error{"cannot open file; pattern is nullptr", EINVAL, error_category::CRITICAL});
+ }
+
+ std::string mutable_pattern = pattern;
+
+ auto fd = g_mkstemp_full(mutable_pattern.data(), oflags, perms);
+
+ if (fd == -1) {
+ return tl::make_unexpected(error{fmt::format("cannot create file {}: {}", pattern, ::strerror(errno)), errno});
+ }
+
+ auto ret = raii_file{mutable_pattern.c_str(), fd, true};
+
+ if (fstat(ret.fd, &ret.st) == -1) {
+ return tl::make_unexpected(error{fmt::format("cannot stat file {}: {}",
+ mutable_pattern, ::strerror(errno)),
+ errno});
+ }
+
+ return ret;
+}
+
+raii_file::~raii_file() noexcept
+{
+ if (fd != -1) {
+ if (temp) {
+ (void) unlink(fname.c_str());
+ }
+ close(fd);
+ }
+}
+
+auto raii_file::update_stat() noexcept -> bool
+{
+ return fstat(fd, &st) != -1;
+}
+
+raii_file::raii_file(const char *fname, int fd, bool temp)
+ : fd(fd), temp(temp)
+{
+ std::size_t nsz;
+
+ /* Normalize path */
+ this->fname = fname;
+ rspamd_normalize_path_inplace(this->fname.data(), this->fname.size(), &nsz);
+ this->fname.resize(nsz);
+}
+
+
+raii_locked_file::~raii_locked_file() noexcept
+{
+ if (fd != -1) {
+ (void) rspamd_file_unlock(fd, FALSE);
+ }
+}
+
+auto raii_locked_file::lock_raii_file(raii_file &&unlocked) -> tl::expected<raii_locked_file, error>
+{
+ if (!rspamd_file_lock(unlocked.get_fd(), TRUE)) {
+ return tl::make_unexpected(
+ error{fmt::format("cannot lock file {}: {}", unlocked.get_name(), ::strerror(errno)), errno});
+ }
+
+ return raii_locked_file{std::move(unlocked)};
+}
+
+auto raii_locked_file::unlock() -> raii_file
+{
+ if (fd != -1) {
+ (void) rspamd_file_unlock(fd, FALSE);
+ }
+
+ return raii_file{static_cast<raii_file &&>(std::move(*this))};
+}
+
+raii_mmaped_file::raii_mmaped_file(raii_file &&file, void *map, std::size_t sz)
+ : file(std::move(file)), map(map), map_size(sz)
+{
+}
+
+auto raii_mmaped_file::mmap_shared(raii_file &&file,
+ int flags, std::int64_t offset) -> tl::expected<raii_mmaped_file, error>
+{
+ void *map;
+
+ if (file.get_stat().st_size < offset || offset < 0) {
+ return tl::make_unexpected(error{
+ fmt::format("cannot mmap file {} due to incorrect offset; offset={}, size={}",
+ file.get_name(), offset, file.get_size()),
+ EINVAL});
+ }
+ /* Update stat on file to ensure it is up-to-date */
+ file.update_stat();
+ map = mmap(nullptr, (std::size_t)(file.get_size() - offset), flags, MAP_SHARED, file.get_fd(), offset);
+
+ if (map == MAP_FAILED) {
+ return tl::make_unexpected(error{fmt::format("cannot mmap file {}: {}",
+ file.get_name(), ::strerror(errno)),
+ errno});
+ }
+
+ return raii_mmaped_file{std::move(file), map, (std::size_t)(file.get_size() - offset)};
+}
+
+auto raii_mmaped_file::mmap_shared(const char *fname, int open_flags,
+ int mmap_flags, std::int64_t offset) -> tl::expected<raii_mmaped_file, error>
+{
+ auto file = raii_file::open(fname, open_flags);
+
+ if (!file.has_value()) {
+ return tl::make_unexpected(file.error());
+ }
+
+ return raii_mmaped_file::mmap_shared(std::move(file.value()), mmap_flags, offset);
+}
+
+raii_mmaped_file::~raii_mmaped_file()
+{
+ if (map != nullptr) {
+ munmap(map, map_size);
+ }
+}
+
+raii_mmaped_file::raii_mmaped_file(raii_mmaped_file &&other) noexcept
+ : file(std::move(other.file))
+{
+ std::swap(map, other.map);
+ std::swap(map_size, other.map_size);
+}
+
+auto raii_file_sink::create(const char *fname, int flags, int perms,
+ const char *suffix) -> tl::expected<raii_file_sink, error>
+{
+ if (!fname || !suffix) {
+ return tl::make_unexpected(error{"cannot create file; filename is nullptr", EINVAL, error_category::CRITICAL});
+ }
+
+ auto tmp_fname = fmt::format("{}.{}", fname, suffix);
+ auto file = raii_locked_file::create(tmp_fname.c_str(), flags, perms);
+
+ if (!file.has_value()) {
+ return tl::make_unexpected(file.error());
+ }
+
+ return raii_file_sink{std::move(file.value()), fname, std::move(tmp_fname)};
+}
+
+auto raii_file_sink::write_output() -> bool
+{
+ if (success) {
+ /* We cannot write output twice */
+ return false;
+ }
+
+ if (rename(tmp_fname.c_str(), output_fname.c_str()) == -1) {
+ return false;
+ }
+
+ success = true;
+
+ return true;
+}
+
+raii_file_sink::~raii_file_sink()
+{
+ if (!success) {
+ /* Unlink sink */
+ unlink(tmp_fname.c_str());
+ }
+}
+
+raii_file_sink::raii_file_sink(raii_locked_file &&_file, const char *_output, std::string &&_tmp_fname)
+ : file(std::move(_file)), output_fname(_output), tmp_fname(std::move(_tmp_fname)), success(false)
+{
+}
+
+raii_file_sink::raii_file_sink(raii_file_sink &&other) noexcept
+ : file(std::move(other.file)),
+ output_fname(std::move(other.output_fname)),
+ tmp_fname(std::move(other.tmp_fname)),
+ success(other.success)
+{
+}
+
+namespace tests {
+template<class T>
+static auto test_read_file(const T &f)
+{
+ auto fd = f.get_fd();
+ (void) ::lseek(fd, 0, SEEK_SET);
+ std::string buf('\0', (std::size_t) f.get_size());
+ ::read(fd, buf.data(), buf.size());
+ return buf;
+}
+template<class T>
+static auto test_write_file(const T &f, const std::string_view &buf)
+{
+ auto fd = f.get_fd();
+ (void) ::lseek(fd, 0, SEEK_SET);
+ return ::write(fd, buf.data(), buf.size());
+}
+auto random_fname(std::string_view extension)
+{
+ const auto *tmpdir = getenv("TMPDIR");
+ if (tmpdir == nullptr) {
+ tmpdir = G_DIR_SEPARATOR_S "tmp";
+ }
+
+ std::string out_fname{tmpdir};
+ out_fname += G_DIR_SEPARATOR_S;
+
+ char hexbuf[32];
+ rspamd_random_hex(hexbuf, sizeof(hexbuf));
+ out_fname.append((const char *) hexbuf, sizeof(hexbuf));
+ if (!extension.empty()) {
+ out_fname.append(".");
+ out_fname.append(extension);
+ }
+
+ return out_fname;
+}
+TEST_SUITE("loked files utils")
+{
+
+ TEST_CASE("create and delete file")
+ {
+ auto fname = random_fname("tmp");
+ {
+ auto raii_locked_file = raii_locked_file::create_temp(fname.c_str(), O_RDONLY, 00600);
+ CHECK(raii_locked_file.has_value());
+ CHECK(raii_locked_file.value().get_extension() == "tmp");
+ CHECK(::access(fname.c_str(), R_OK) == 0);
+ }
+ // File must be deleted after this call
+ auto ret = ::access(fname.c_str(), R_OK);
+ auto serrno = errno;
+ CHECK(ret == -1);
+ CHECK(serrno == ENOENT);
+ // Create one more time
+ {
+ auto raii_locked_file = raii_locked_file::create_temp(fname.c_str(), O_RDONLY, 00600);
+ CHECK(raii_locked_file.has_value());
+ CHECK(::access(fname.c_str(), R_OK) == 0);
+ }
+ ret = ::access(fname.c_str(), R_OK);
+ serrno = errno;
+ CHECK(ret == -1);
+ CHECK(serrno == ENOENT);
+ }
+
+ TEST_CASE("check lock")
+ {
+ auto fname = random_fname("");
+ {
+ auto raii_locked_file = raii_locked_file::create_temp(fname.c_str(), O_RDONLY, 00600);
+ CHECK(raii_locked_file.has_value());
+ CHECK(raii_locked_file.value().get_extension() == "");
+ CHECK(::access(fname.c_str(), R_OK) == 0);
+ auto raii_locked_file2 = raii_locked_file::open(fname.c_str(), O_RDONLY);
+ CHECK(!raii_locked_file2.has_value());
+ CHECK(::access(fname.c_str(), R_OK) == 0);
+ }
+ // File must be deleted after this call
+ auto ret = ::access(fname.c_str(), R_OK);
+ auto serrno = errno;
+ CHECK(ret == -1);
+ CHECK(serrno == ENOENT);
+ }
+
+ auto get_tmpdir()->std::string
+ {
+ const auto *tmpdir = getenv("TMPDIR");
+ if (tmpdir == nullptr) {
+ tmpdir = G_DIR_SEPARATOR_S "tmp";
+ }
+
+ std::size_t sz;
+ std::string mut_fname = tmpdir;
+ rspamd_normalize_path_inplace(mut_fname.data(), mut_fname.size(), &sz);
+ mut_fname.resize(sz);
+
+ if (!mut_fname.ends_with(G_DIR_SEPARATOR)) {
+ mut_fname += G_DIR_SEPARATOR;
+ }
+
+ return mut_fname;
+ }
+
+ TEST_CASE("tempfile")
+ {
+ std::string tmpname;
+ const std::string tmpdir{get_tmpdir()};
+ {
+ auto raii_locked_file = raii_locked_file::mkstemp(std::string(tmpdir + G_DIR_SEPARATOR_S + "doctest-XXXXXXXX").c_str(),
+ O_RDONLY, 00600);
+ CHECK(raii_locked_file.has_value());
+ CHECK(raii_locked_file.value().get_dir() == tmpdir);
+ CHECK(access(raii_locked_file.value().get_name().data(), R_OK) == 0);
+ auto raii_locked_file2 = raii_locked_file::open(raii_locked_file.value().get_name().data(), O_RDONLY);
+ CHECK(!raii_locked_file2.has_value());
+ CHECK(access(raii_locked_file.value().get_name().data(), R_OK) == 0);
+ tmpname = raii_locked_file.value().get_name();
+ }
+ // File must be deleted after this call
+ auto ret = ::access(tmpname.c_str(), R_OK);
+ auto serrno = errno;
+ CHECK(ret == -1);
+ CHECK(serrno == ENOENT);
+ }
+
+ TEST_CASE("mmap")
+ {
+ std::string tmpname;
+ const std::string tmpdir{get_tmpdir()};
+ {
+ auto raii_file = raii_file::mkstemp(std::string(tmpdir + G_DIR_SEPARATOR_S + "doctest-XXXXXXXX").c_str(),
+ O_RDWR | O_CREAT | O_EXCL, 00600);
+ CHECK(raii_file.has_value());
+ CHECK(raii_file->get_dir() == tmpdir);
+ CHECK(access(raii_file->get_name().data(), R_OK) == 0);
+ tmpname = std::string{raii_file->get_name()};
+ char payload[] = {'1', '2', '3'};
+ CHECK(write(raii_file->get_fd(), payload, sizeof(payload)) == sizeof(payload));
+ auto mmapped_file1 = raii_mmaped_file::mmap_shared(std::move(raii_file.value()), PROT_READ | PROT_WRITE);
+ CHECK(mmapped_file1.has_value());
+ CHECK(!raii_file->is_valid());
+ CHECK(mmapped_file1->get_size() == sizeof(payload));
+ CHECK(memcmp(mmapped_file1->get_map(), payload, sizeof(payload)) == 0);
+ *(char *) mmapped_file1->get_map() = '2';
+ auto mmapped_file2 = raii_mmaped_file::mmap_shared(tmpname.c_str(), O_RDONLY, PROT_READ);
+ CHECK(mmapped_file2.has_value());
+ CHECK(mmapped_file2->get_size() == sizeof(payload));
+ CHECK(memcmp(mmapped_file2->get_map(), payload, sizeof(payload)) != 0);
+ CHECK(memcmp(mmapped_file2->get_map(), mmapped_file1->get_map(), sizeof(payload)) == 0);
+ }
+ // File must be deleted after this call
+ auto ret = ::access(tmpname.c_str(), R_OK);
+ auto serrno = errno;
+ CHECK(ret == -1);
+ CHECK(serrno == ENOENT);
+ }
+
+}// TEST_SUITE
+
+}// namespace tests
+
+}// namespace rspamd::util