blob: 374f4fca195b6274aac62a9563ad1b72f3f2a707 [file] [log] [blame]
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: Apache-2.0
// Copyright Apache Software Foundation 2019
/** @file
Minimalist version of std::filesystem.
*/
#include <variant>
#include <fcntl.h>
#include <unistd.h>
#include <dirent.h>
#include "swoc/swoc_file.h"
#include "swoc/bwf_base.h"
using namespace swoc::literals;
namespace swoc { inline namespace SWOC_VERSION_NS {
namespace file {
path
path::parent_path() const {
TextView parent{_path};
parent.split_suffix_at(SEPARATOR);
return parent ? parent : "/"_tv;
}
path
path::relative_path() const {
if (!_path.empty() && _path.front() == SEPARATOR) {
return _path.substr(1);
}
return *this;
}
auto
path::filename() const -> self_type {
auto const idx = _path.find_last_of(SEPARATOR);
return idx == std::string::npos ? self_type(_path) : _path.substr(idx + 1);
}
path &
path::operator/=(std::string_view that) {
if (!that.empty()) { // don't waste time appending nothing.
if (that.front() == SEPARATOR || _path.empty()) {
_path.assign(that);
} else {
if (_path.back() == SEPARATOR) {
_path.reserve(_path.size() + that.size());
} else {
_path.reserve(_path.size() + that.size() + 1);
_path.push_back(SEPARATOR);
}
_path.append(that);
}
}
return *this;
}
void
file_status::init() {
switch (_stat.st_mode & S_IFMT) {
case S_IFREG:
_type = file_type::regular;
break;
case S_IFDIR:
_type = file_type::directory;
break;
case S_IFLNK:
_type = file_type::symlink;
break;
case S_IFBLK:
_type = file_type::block;
break;
case S_IFCHR:
_type = file_type::character;
break;
case S_IFIFO:
_type = file_type::fifo;
break;
case S_IFSOCK:
_type = file_type::socket;
break;
default:
_type = file_type::unknown;
break;
}
}
file_status
status(path const &file, std::error_code &ec) noexcept {
file_status zret;
if (::stat(file.c_str(), &zret._stat) >= 0) {
ec.clear();
zret.init();
} else {
ec = std::error_code(errno, std::system_category());
if (errno == ENOENT) {
zret._type = file_type::not_found;
}
}
return zret;
}
int
file_type(const file_status &fs) {
return fs._stat.st_mode & S_IFMT;
}
uintmax_t
file_size(const file_status &fs) {
return fs._stat.st_size;
}
bool
exists(const path &p) {
std::error_code ec;
auto fs = status(p, ec);
return exists(fs);
}
path
absolute(path const &src, std::error_code &ec) {
char buff[4096];
ec.clear();
if (src.is_absolute()) {
return src;
}
auto s = realpath(src.c_str(), buff);
if (s == nullptr) {
if (errno == ENAMETOOLONG) {
s = realpath(src.c_str(), nullptr);
if (s != nullptr) {
path zret{s};
free(s);
return zret;
}
}
ec = std::error_code(errno, std::system_category());
return {};
}
return path{s};
}
namespace {
inline file_time_type
chrono_cast(timespec const &ts) {
using namespace std::chrono;
return system_clock::time_point{duration_cast<system_clock::duration>(seconds{ts.tv_sec} + nanoseconds{ts.tv_nsec})};
}
// Apple has different names for these members, need to have accessors that account for that.
// Under -O2 these are completely elided.
template <typename S>
auto
a_time(S const &s, meta::CaseTag<0>) -> decltype(S::st_atim) {
return s.st_atim;
}
template <typename S>
auto
a_time(S const &s, meta::CaseTag<1>) -> decltype(S::st_atimespec) {
return s.st_atimespec;
}
template <typename S>
auto
m_time(S const &s, meta::CaseTag<0>) -> decltype(S::st_mtim) {
return s.st_mtim;
}
template <typename S>
auto
m_time(S const &s, meta::CaseTag<1>) -> decltype(S::st_mtimespec) {
return s.st_mtimespec;
}
template <typename S>
auto
c_time(S const &s, meta::CaseTag<0>) -> decltype(S::st_ctim) {
return s.st_ctim;
}
template <typename S>
auto
c_time(S const &s, meta::CaseTag<1>) -> decltype(S::st_ctimespec) {
return s.st_ctimespec;
}
} // namespace
file_time_type
last_write_time(file_status const &fs) {
return chrono_cast(m_time(fs._stat, meta::CaseArg));
}
file_time_type
access_time(file_status const &fs) {
return chrono_cast(a_time(fs._stat, meta::CaseArg));
}
file_time_type
status_time(file_status const &fs) {
return chrono_cast(c_time(fs._stat, meta::CaseArg));
}
file_time_type
last_write_time(path const &p, std::error_code &ec) {
auto fs = status(p, ec);
if (ec) {
return file_time_type::min();
}
return last_write_time(fs);
}
bool
is_readable(const path &p) {
return 0 == access(p.c_str(), R_OK);
}
path
temp_directory_path() {
/* ISO/IEC 9945 (POSIX): The path supplied by the first environment variable found in the list TMPDIR, TMP, TEMP, TEMPDIR.
* If none of these are found, "/tmp"
* */
for (char const *tp : {"TMPDIR", "TMP", "TEMPDIR"}) {
if (auto v = ::getenv(tp); v) {
return path(v);
}
}
return path("/tmp");
}
path
current_path() {
char buff[PATH_MAX + 1];
if (auto p = ::getcwd(buff, sizeof(buff)); p) {
return path{buff};
#if !__FreeBSD__ && !__APPLE__ // Freakin' Apple and FreeBSD.
} else if (ERANGE == errno) {
swoc::unique_malloc<char> raw{::get_current_dir_name()};
return path{raw.get()};
#endif
}
return {};
}
path
canonical(const path &p, std::error_code &ec) {
if (p.empty()) {
ec = std::error_code(EINVAL, std::system_category());
return {};
}
char buf[PATH_MAX + 1];
if (auto rp = ::realpath(p.c_str(), buf); rp) {
return path{rp};
}
if (auto rp = ::realpath(p.c_str(), nullptr); rp) {
return path{rp};
}
ec = std::error_code(errno, std::system_category());
return {};
}
bool
create_directory(const path &path, std::error_code &ec, mode_t mode) noexcept {
if (path.empty()) {
ec = std::error_code(EINVAL, std::system_category());
return false;
}
ec.clear();
if (::mkdir(path.c_str(), mode) != 0) {
if (EEXIST == errno) {
std::error_code local_ec;
auto fs = status(path, local_ec);
if (!local_ec && is_dir(fs)) {
return true;
}
}
ec = std::error_code(errno, std::system_category());
return false;
}
return true;
}
bool
create_directories(const path &p, std::error_code &ec, mode_t mode) noexcept {
TextView text(p.string());
if (text.empty()) {
ec = std::error_code(EINVAL, std::system_category());
return false;
}
path path;
if (text.front() == path::SEPARATOR) {
path = text.prefix(1); // copy leading separator.
++text;
if (!text) {
ec.clear();
return true; // Tried to create root directory, it's already tehre.
}
}
path.reserve(p.string().size());
while (text) {
auto elt = text.take_prefix_at(path::SEPARATOR);
path /= elt;
if (!create_directory(path, ec, mode)) {
return false;
}
}
return true;
}
bool
copy(const path &from, const path &to, std::error_code &ec) {
static constexpr size_t BUF_SIZE = 65536;
std::error_code local_ec;
char buf[BUF_SIZE];
swoc::MemSpan span{buf};
if (from.empty() || to.empty()) {
ec = std::error_code(EINVAL, std::system_category());
return false;
}
ec.clear();
unique_fd src_fd{::open(from.c_str(), O_RDONLY)};
if (NO_FD == src_fd) {
ec = std::error_code(errno, std::system_category());
return false;
}
auto src_fs = file::status(from, local_ec);
path final_to;
if (auto fs = file::status(to, local_ec); !(local_ec && ENOENT == local_ec.value()) && is_dir(fs)) {
final_to = to / from.filename();
} else {
final_to = to;
}
unique_fd dst_fd{::open(final_to.c_str(), O_WRONLY | O_CREAT, src_fs.mode())};
if (NO_FD == dst_fd) {
ec = std::error_code(errno, std::system_category());
return false;
}
while (true) {
if (auto n = read(src_fd, span.data(), span.size()); n > 0) {
if (::write(dst_fd, span.data(), n) < n) {
ec = std::error_code(errno, std::system_category());
break;
}
} else {
break;
}
}
return true;
}
uintmax_t
remove_all(const path &p, std::error_code &ec) {
// coverity TOCTOU - issue is doing stat before doing operation. Stupid complaint, ignore.
DIR *dir = nullptr;
struct dirent *entry = nullptr;
std::error_code err;
uintmax_t zret = 0;
struct ::stat s {};
if (p.empty()) {
ec = std::error_code(EINVAL, std::system_category());
return zret;
} else if (::stat(p.c_str(), &s) < 0) {
ec = std::error_code(errno, std::system_category());
return zret;
} else if (S_ISREG(s.st_mode)) { // regular file, try to remove it!
// coverity[toctou : SUPPRESS]
if (unlink(p.c_str()) != 0) {
ec = std::error_code(errno, std::system_category());
} else {
++zret;
}
return zret;
} else if (!S_ISDIR(s.st_mode)) { // not a directory
ec = std::error_code(ENOTDIR, std::system_category());
return zret;
}
// Invariant - @a p is a directory.
// recursively remove nested files and directories
// coverity[toctou : SUPPRESS]
if (nullptr == (dir = opendir(p.c_str()))) {
ec = std::error_code(errno, std::system_category());
return zret;
}
auto child = p; // Minimize string allocations / re-allocations.
while (nullptr != (entry = readdir(dir))) {
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) {
continue;
}
child = p;
child /= entry->d_name;
zret += remove_all(child, ec);
}
if (0 != rmdir(p.c_str())) {
ec = std::error_code(errno, std::system_category());
}
++zret;
closedir(dir);
return zret;
}
bool
remove(path const &p, std::error_code &ec) {
// coverity TOCTOU - issue is doing stat before doing operation. Stupid complaint, ignore.
struct ::stat fs {};
if (p.empty()) {
ec = std::error_code(EINVAL, std::system_category());
} else if (::stat(p.c_str(), &fs) < 0) {
ec = std::error_code(errno, std::system_category());
} else if (S_ISREG(fs.st_mode)) { // regular file, try to remove it!
// coverity[toctou : SUPPRESS]
if (unlink(p.c_str()) != 0) {
ec = std::error_code(errno, std::system_category());
}
} else if (S_ISDIR(fs.st_mode)) { // not a directory
// coverity[toctou : SUPPRESS]
if (rmdir(p.c_str()) != 0) {
ec = std::error_code(errno, std::system_category());
}
} else {
ec = std::error_code(EINVAL, std::system_category());
}
return !ec;
}
std::string
load(const path &p, std::error_code &ec) {
std::string zret;
ec.clear();
if (unique_fd fd(::open(p.c_str(), O_RDONLY)); fd < 0) {
ec = std::error_code(errno, std::system_category());
} else {
struct stat info {};
if (0 != ::fstat(fd, &info)) {
ec = std::error_code(errno, std::system_category());
} else {
auto n = info.st_size;
zret.resize(n);
auto read_len = ::read(fd, zret.data(), n);
if (read_len < n) {
ec = std::error_code(errno, std::system_category());
}
}
}
return zret;
}
} // namespace file
BufferWriter &
bwformat(BufferWriter &w, bwf::Spec const &spec, file::path const &p) {
return bwformat(w, spec, p.string());
}
}} // namespace swoc::SWOC_VERSION_NS