-// Copyright (C) 2020-2022 Joel Rosdahl and other contributors
+// Copyright (C) 2020-2023 Joel Rosdahl and other contributors
//
// See doc/AUTHORS.adoc for a complete list of contributors.
//
// Assembler listing file.
std::string output_al;
- // The .gch/.pch/.pth file used for compilation.
+ // The .gch/.pch/.pth file or directory used for compilation.
std::string included_pch_file;
// Language to use for the compilation target (see language.c).
Hash.cpp
Logging.cpp
ProgressBar.cpp
- Stat.cpp
TemporaryFile.cpp
ThreadPool.cpp
Util.cpp
#include "Util.hpp"
#include "assertions.hpp"
-#include <Stat.hpp>
#include <core/common.hpp>
#include <core/exceptions.hpp>
#include <core/types.hpp>
#include <fmtmacros.hpp>
+#include <util/DirEntry.hpp>
#include <util/Tokenizer.hpp>
#include <util/UmaskScope.hpp>
#include <util/environment.hpp>
namespace fs = util::filesystem;
+using util::DirEntry;
+
namespace {
enum class ConfigItem {
const std::string home_dir = home_directory();
const std::string legacy_ccache_dir = Util::make_path(home_dir, ".ccache");
const bool legacy_ccache_dir_exists =
- Stat::stat(legacy_ccache_dir).is_directory();
+ DirEntry(legacy_ccache_dir).is_directory();
#ifdef _WIN32
const char* const env_appdata = getenv("APPDATA");
const char* const env_local_appdata = getenv("LOCALAPPDATA");
config_dir = legacy_ccache_dir;
#ifdef _WIN32
} else if (env_local_appdata
- && Stat::stat(
+ && DirEntry(
Util::make_path(env_local_appdata, "ccache", "ccache.conf"))) {
config_dir = Util::make_path(env_local_appdata, "ccache");
} else if (env_appdata
- && Stat::stat(
+ && DirEntry(
Util::make_path(env_appdata, "ccache", "ccache.conf"))) {
config_dir = Util::make_path(env_appdata, "ccache");
} else if (env_local_appdata) {
dummy_config.set_item(key, value, std::nullopt, false, "");
const auto resolved_path = util::real_path(path);
- const auto st = Stat::stat(resolved_path);
- if (!st) {
+ if (!DirEntry(resolved_path).is_regular_file()) {
core::ensure_dir_exists(Util::dir_name(resolved_path));
util::throw_on_error<core::Error>(
util::write_file(resolved_path, ""),
static const std::string run_user_tmp_dir = [] {
#ifndef _WIN32
const char* const xdg_runtime_dir = getenv("XDG_RUNTIME_DIR");
- if (xdg_runtime_dir && Stat::stat(xdg_runtime_dir).is_directory()) {
+ if (xdg_runtime_dir && DirEntry(xdg_runtime_dir).is_directory()) {
auto dir = FMT("{}/ccache-tmp", xdg_runtime_dir);
if (fs::create_directories(dir) && access(dir.c_str(), W_OK) == 0) {
return dir;
#include "Finalizer.hpp"
#include "Hash.hpp"
#include "Logging.hpp"
-#include "Stat.hpp"
#include "TemporaryFile.hpp"
#include "Util.hpp"
#include "fmtmacros.hpp"
+#include <util/DirEntry.hpp>
#include <util/conversion.hpp>
#include <util/file.hpp>
ContentType type,
Hash::Digest& digest)
{
- Stat stat = Stat::stat(path);
- if (!stat) {
- LOG("Could not stat {}: {}", path, strerror(stat.error_number()));
+ util::DirEntry de(path);
+ if (!de.exists()) {
+ LOG("Could not stat {}: {}", path, strerror(de.error_number()));
return false;
}
// See comment for InodeCache::InodeCache why this check is done.
auto now = util::TimePoint::now();
- if (now - stat.ctime() < m_min_age || now - stat.mtime() < m_min_age) {
+ if (now - de.ctime() < m_min_age || now - de.mtime() < m_min_age) {
LOG("Too new ctime or mtime of {}, not considering for inode cache", path);
return false;
}
Key key;
memset(&key, 0, sizeof(Key));
key.type = type;
- key.st_dev = stat.device();
- key.st_ino = stat.inode();
- key.st_mode = stat.mode();
- key.st_mtim = stat.mtime().to_timespec();
- key.st_ctim = stat.ctime().to_timespec();
- key.st_size = stat.size();
+ key.st_dev = de.device();
+ key.st_ino = de.inode();
+ key.st_mode = de.mode();
+ key.st_mtim = de.mtime().to_timespec();
+ key.st_ctim = de.ctime().to_timespec();
+ key.st_size = de.size();
Hash hash;
hash.hash(nonstd::span<const uint8_t>(reinterpret_cast<const uint8_t*>(&key),
+++ /dev/null
-// Copyright (C) 2019-2023 Joel Rosdahl and other contributors
-//
-// See doc/AUTHORS.adoc for a complete list of contributors.
-//
-// This program is free software; you can redistribute it and/or modify it
-// under the terms of the GNU General Public License as published by the Free
-// Software Foundation; either version 3 of the License, or (at your option)
-// any later version.
-//
-// This program is distributed in the hope that it will be useful, but WITHOUT
-// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-// more details.
-//
-// You should have received a copy of the GNU General Public License along with
-// this program; if not, write to the Free Software Foundation, Inc., 51
-// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-
-#pragma once
-
-#include <util/TimePoint.hpp>
-#include <util/file.hpp>
-#include <util/wincompat.hpp>
-
-#include <sys/stat.h>
-#include <sys/types.h>
-
-#include <cstdint>
-#include <ctime>
-#include <string>
-
-#ifdef _WIN32
-# ifndef S_IFIFO
-# define S_IFIFO 0x1000
-# endif
-# ifndef S_IFBLK
-# define S_IFBLK 0x6000
-# endif
-# ifndef S_IFLNK
-# define S_IFLNK 0xA000
-# endif
-# ifndef S_ISREG
-# define S_ISREG(m) (((m)&S_IFMT) == S_IFREG)
-# endif
-# ifndef S_ISDIR
-# define S_ISDIR(m) (((m)&S_IFMT) == S_IFDIR)
-# endif
-# ifndef S_ISFIFO
-# define S_ISFIFO(m) (((m)&S_IFMT) == S_IFIFO)
-# endif
-# ifndef S_ISCHR
-# define S_ISCHR(m) (((m)&S_IFMT) == S_IFCHR)
-# endif
-# ifndef S_ISLNK
-# define S_ISLNK(m) (((m)&S_IFMT) == S_IFLNK)
-# endif
-# ifndef S_ISBLK
-# define S_ISBLK(m) (((m)&S_IFMT) == S_IFBLK)
-# endif
-#endif
-
-class Stat
-{
-public:
- enum class LogOnError { no, yes };
-
-#if defined(_WIN32)
- struct stat_t
- {
- uint64_t st_dev;
- uint64_t st_ino;
- uint16_t st_mode;
- uint16_t st_nlink;
- uint64_t st_size;
- struct timespec st_atim;
- struct timespec st_mtim;
- struct timespec st_ctim;
- uint32_t st_file_attributes;
- uint32_t st_reparse_tag;
- };
-#else
- using stat_t = struct stat;
-#endif
-
- using dev_t = decltype(stat_t{}.st_dev);
- using ino_t = decltype(stat_t{}.st_ino);
-
- // Create an empty stat result. operator bool() will return false,
- // error_number() will return -1 and other accessors will return false or 0.
- Stat();
-
- // Run stat(2) on `path`.
- static Stat stat(const std::string& path,
- LogOnError log_on_error = LogOnError::no);
-
- // Run lstat(2) on `path` if available, otherwise stat(2).
- static Stat lstat(const std::string& path,
- LogOnError log_on_error = LogOnError::no);
-
- // Return true if the file could be (l)stat-ed (i.e., the file exists),
- // otherwise false.
- operator bool() const;
-
- // Return the path that this stat result refers to.
- const std::string& path() const;
-
- // Return whether this object refers to the same device and i-node as `other`
- // does.
- bool same_inode_as(const Stat& other) const;
-
- // Return errno from the (l)stat call (0 if successful).
- int error_number() const;
-
- dev_t device() const;
- ino_t inode() const;
- mode_t mode() const;
- util::TimePoint atime() const;
- util::TimePoint ctime() const;
- util::TimePoint mtime() const;
- uint64_t size() const;
-
- uint64_t size_on_disk() const;
-
- bool is_directory() const;
- bool is_regular() const;
- bool is_symlink() const;
-
-#ifdef _WIN32
- uint32_t file_attributes() const;
- uint32_t reparse_tag() const;
-#endif
-
-protected:
- using StatFunction = int (*)(const char*, stat_t*);
-
- Stat(StatFunction stat_function,
- const std::string& path,
- LogOnError log_on_error);
-
-private:
- std::string m_path;
- stat_t m_stat;
- int m_errno;
-
- bool operator==(const Stat&) const;
- bool operator!=(const Stat&) const;
-};
-
-inline Stat::Stat() : m_stat{}, m_errno(-1)
-{
-}
-
-inline Stat::operator bool() const
-{
- return m_errno == 0;
-}
-
-inline bool
-Stat::same_inode_as(const Stat& other) const
-{
- return m_errno == 0 && device() == other.device() && inode() == other.inode();
-}
-
-inline const std::string&
-Stat::path() const
-{
- return m_path;
-}
-
-inline int
-Stat::error_number() const
-{
- return m_errno;
-}
-
-inline Stat::dev_t
-Stat::device() const
-{
- return m_stat.st_dev;
-}
-
-inline Stat::ino_t
-Stat::inode() const
-{
- return m_stat.st_ino;
-}
-
-inline mode_t
-Stat::mode() const
-{
- return m_stat.st_mode;
-}
-
-inline util::TimePoint
-Stat::atime() const
-{
-#if defined(_WIN32) || defined(HAVE_STRUCT_STAT_ST_ATIM)
- return util::TimePoint(m_stat.st_atim);
-#elif defined(HAVE_STRUCT_STAT_ST_ATIMESPEC)
- return util::TimePoint(m_stat.st_atimespec);
-#else
- return util::TimePoint(m_stat.st_atime, 0);
-#endif
-}
-
-inline util::TimePoint
-Stat::ctime() const
-{
-#if defined(_WIN32) || defined(HAVE_STRUCT_STAT_ST_CTIM)
- return util::TimePoint(m_stat.st_ctim);
-#elif defined(HAVE_STRUCT_STAT_ST_CTIMESPEC)
- return util::TimePoint(m_stat.st_ctimespec);
-#else
- return util::TimePoint(m_stat.st_ctime, 0);
-#endif
-}
-
-inline util::TimePoint
-Stat::mtime() const
-{
-#if defined(_WIN32) || defined(HAVE_STRUCT_STAT_ST_MTIM)
- return util::TimePoint(m_stat.st_mtim);
-#elif defined(HAVE_STRUCT_STAT_ST_CTIMESPEC)
- return util::TimePoint(m_stat.st_mtimespec);
-#else
- return util::TimePoint(m_stat.st_mtime, 0);
-#endif
-}
-
-inline uint64_t
-Stat::size() const
-{
- return m_stat.st_size;
-}
-
-inline uint64_t
-Stat::size_on_disk() const
-{
-#ifdef _WIN32
- return util::likely_size_on_disk(size());
-#else
- return m_stat.st_blocks * 512;
-#endif
-}
-
-inline bool
-Stat::is_directory() const
-{
- return S_ISDIR(mode());
-}
-
-inline bool
-Stat::is_symlink() const
-{
- return S_ISLNK(mode());
-}
-
-inline bool
-Stat::is_regular() const
-{
- return S_ISREG(mode());
-}
-
-#ifdef _WIN32
-inline uint32_t
-Stat::file_attributes() const
-{
- return m_stat.st_file_attributes;
-}
-
-inline uint32_t
-Stat::reparse_tag() const
-{
- return m_stat.st_reparse_tag;
-}
-#endif
#include <Config.hpp>
#include <Finalizer.hpp>
-#include <Stat.hpp>
#include <core/exceptions.hpp>
#include <fmtmacros.hpp>
+#include <util/DirEntry.hpp>
#include <util/expected.hpp>
#include <util/file.hpp>
#include <util/filesystem.hpp>
namespace fs = util::filesystem;
+using util::DirEntry;
+
namespace Util {
std::string_view
std::vector<std::string> relpath_candidates;
const auto original_path = path;
- Stat path_stat;
- while (!(path_stat = Stat::stat(std::string(path)))) {
+ DirEntry dir_entry(path);
+ while (!dir_entry.exists()) {
path = Util::dir_name(path);
+ dir_entry = DirEntry(path);
}
const auto path_suffix = std::string(original_path.substr(path.length()));
const auto real_path = util::real_path(path);
return path1.length() < path2.length();
});
for (const auto& relpath : relpath_candidates) {
- if (Stat::stat(relpath).same_inode_as(path_stat)) {
+ if (DirEntry(relpath).same_inode_as(dir_entry)) {
return relpath + path_suffix;
}
}
{
const auto normalized_path = normalize_abstract_absolute_path(path);
return (normalized_path == path
- || Stat::stat(normalized_path).same_inode_as(Stat::stat(path)))
+ || DirEntry(normalized_path).same_inode_as(DirEntry(path)))
? normalized_path
: path;
}
#include <Depfile.hpp>
#include <Util.hpp>
+#include <util/path.hpp>
#include <util/string.hpp>
#include <util/wincompat.hpp>
#include <cassert>
using core::Statistic;
+using util::DirEntry;
namespace {
included_pch_file.clear(); // reset pch file set from /Fp
} else {
std::string file = Util::change_extension(arg, ".pch");
- if (Stat::stat(file)) {
+ if (DirEntry(file).is_regular_file()) {
LOG("Detected use of precompiled header: {}", file);
pch_file = file;
}
if (Util::get_extension(file).empty()) {
file += ".pch";
}
- if (Stat::stat(file)) {
+ if (DirEntry(file).is_regular_file()) {
state.found_valid_Fp = true;
if (!state.found_Yu) {
LOG("Precompiled header file specified: {}", file);
// continue and set as if the file was passed to -Yu
}
} else if (option == "-include-pch" || option == "-include-pth") {
- if (Stat::stat(arg)) {
+ if (DirEntry(arg).is_regular_file()) {
LOG("Detected use of precompiled header: {}", arg);
pch_file = arg;
}
} else if (!is_cc1_option) {
for (const auto& extension : {".gch", ".pch", ".pth"}) {
std::string path = arg + extension;
- if (Stat::stat(path)) {
+ DirEntry de(path);
+ if (de.is_regular_file() || de.is_directory()) {
LOG("Detected use of precompiled header: {}", path);
pch_file = path;
}
return Statistic::none;
} else if (ctx.config.is_compiler_group_msvc()
&& args[i][0] == '/' // Intentionally not checking arg here
- && Stat::stat(args[i])) {
+ && DirEntry(args[i]).is_regular_file()) {
// Likely the input file, which is handled in process_arg later.
} else {
state.common_args.push_back(args[i]);
//
// Note that "/dev/null" is an exception that is sometimes used as an input
// file when code is testing compiler flags.
- if (args[i] != "/dev/null") {
- auto st = Stat::stat(args[i]);
- if (!st || !st.is_regular()) {
+ if (!util::is_dev_null_path(args[i])) {
+ if (!DirEntry(args[i]).is_regular_file()) {
LOG("{} is not a regular file, not considering as input file", args[i]);
state.common_args.push_back(args[i]);
return Statistic::none;
if (!output_obj_by_source && ctx.config.is_compiler_group_msvc()) {
if (*args_info.output_obj.rbegin() == '\\') {
output_obj_by_source = true;
- } else {
- auto st = Stat::stat(args_info.output_obj);
- if (st && st.is_directory()) {
- args_info.output_obj.append("\\");
- output_obj_by_source = true;
- }
+ } else if (DirEntry(args_info.output_obj).is_directory()) {
+ args_info.output_obj.append("\\");
+ output_obj_by_source = true;
}
}
}
if (args_info.seen_split_dwarf) {
- if (args_info.output_obj == "/dev/null") {
+ if (util::is_dev_null_path(args_info.output_obj)) {
// Outputting to /dev/null -> compiler won't write a .dwo, so just pretend
// we haven't seen the -gsplit-dwarf option.
args_info.seen_split_dwarf = false;
}
}
- // Cope with -o /dev/null.
- if (args_info.output_obj != "/dev/null") {
- auto st = Stat::stat(args_info.output_obj);
- if (st && !st.is_regular()) {
+ if (!util::is_dev_null_path(args_info.output_obj)) {
+ DirEntry entry(args_info.output_obj);
+ if (entry.exists() && !entry.is_regular_file()) {
LOG("Not a regular file: {}", args_info.output_obj);
return Statistic::bad_output_file;
}
}
- auto output_dir = std::string(Util::dir_name(args_info.output_obj));
- auto st = Stat::stat(output_dir);
- if (!st || !st.is_directory()) {
+ if (util::is_dev_null_path(args_info.output_dep)) {
+ args_info.generating_dependencies = false;
+ }
+
+ auto output_dir = Util::dir_name(args_info.output_obj);
+ if (!DirEntry(output_dir).is_directory()) {
LOG("Directory does not exist: {}", output_dir);
return Statistic::bad_output_file;
}
namespace fs = util::filesystem;
using core::Statistic;
+using util::DirEntry;
// This is a string that identifies the current "version" of the hash sum
// computed by ccache. If, for any reason, we want to force the hash sum to be
static bool
include_file_too_new(const Context& ctx,
const std::string& path,
- const Stat& path_stat)
+ const DirEntry& dir_entry)
{
// The comparison using >= is intentional, due to a possible race between
// starting compilation and writing the include file. See also the notes under
// "Performance" in doc/MANUAL.adoc.
if (!(ctx.config.sloppiness().contains(core::Sloppy::include_file_mtime))
- && path_stat.mtime() >= ctx.time_of_compilation) {
+ && dir_entry.mtime() >= ctx.time_of_compilation) {
LOG("Include file {} too new", path);
return true;
}
// The same >= logic as above applies to the change time of the file.
if (!(ctx.config.sloppiness().contains(core::Sloppy::include_file_ctime))
- && path_stat.ctime() >= ctx.time_of_compilation) {
+ && dir_entry.ctime() >= ctx.time_of_compilation) {
LOG("Include file {} ctime too new", path);
return true;
}
}
#endif
- auto st = Stat::stat(path, Stat::LogOnError::yes);
- if (!st) {
+ DirEntry dir_entry(path, DirEntry::LogOnError::yes);
+ if (!dir_entry.exists()) {
return false;
}
- if (st.is_directory()) {
+ if (dir_entry.is_directory()) {
// Ignore directory, typically $PWD.
return true;
}
- if (!st.is_regular()) {
+ if (!dir_entry.is_regular_file()) {
// Device, pipe, socket or other strange creature.
LOG("Non-regular include file {}", path);
return false;
}
const bool is_pch = is_precompiled_header(path);
- const bool too_new = include_file_too_new(ctx, path, st);
+ const bool too_new = include_file_too_new(ctx, path, dir_entry);
if (too_new) {
// Opt out of direct mode because of a race condition.
// hash pch.sum instead of pch when it exists
// to prevent hashing a very large .pch file every time
std::string pch_sum_path = FMT("{}.sum", path);
- if (Stat::stat(pch_sum_path, Stat::LogOnError::yes)) {
+ if (DirEntry(pch_sum_path, DirEntry::LogOnError::yes).is_regular_file()) {
path = std::move(pch_sum_path);
using_pch_sum = true;
LOG("Using pch.sum file {}", path);
const bool added = ctx.manifest.add_result(
result_key, ctx.included_files, [&](const std::string& path) {
- auto stat = Stat::stat(path, Stat::LogOnError::yes);
+ DirEntry de(path, DirEntry::LogOnError::yes);
bool cache_time =
save_timestamp
- && ctx.time_of_compilation > std::max(stat.mtime(), stat.ctime());
+ && ctx.time_of_compilation > std::max(de.mtime(), de.ctime());
return core::Manifest::FileStats{
- stat.size(),
- stat && cache_time ? stat.mtime() : util::TimePoint(),
- stat && cache_time ? stat.ctime() : util::TimePoint(),
+ de.size(),
+ de.is_regular_file() && cache_time ? de.mtime() : util::TimePoint(),
+ de.is_regular_file() && cache_time ? de.ctime() : util::TimePoint(),
};
});
if (added) {
std::string mangled_form = core::Result::gcno_file_in_mangled_form(ctx);
std::string unmangled_form = core::Result::gcno_file_in_unmangled_form(ctx);
std::string found_file;
- if (Stat::stat(mangled_form)) {
+ if (DirEntry(mangled_form).is_regular_file()) {
LOG("Found coverage file {}", mangled_form);
found_file = mangled_form;
}
- if (Stat::stat(unmangled_form)) {
+ if (DirEntry(unmangled_form).is_regular_file()) {
LOG("Found coverage file {}", unmangled_form);
if (!found_file.empty()) {
LOG_RAW("Found two coverage files, cannot continue");
[[nodiscard]] static bool
write_result(Context& ctx,
const Hash::Digest& result_key,
- const Stat& obj_stat,
const util::Bytes& stdout_data,
const util::Bytes& stderr_data)
{
if (!stdout_data.empty()) {
serializer.add_data(core::Result::FileType::stdout_output, stdout_data);
}
- if (obj_stat
+ if (ctx.args_info.expect_output_obj
&& !serializer.add_file(core::Result::FileType::object,
ctx.args_info.output_obj)) {
LOG("Object file {} missing", ctx.args_info.output_obj);
if (ctx.args_info.seen_split_dwarf
// Only store .dwo file if it was created by the compiler (GCC and Clang
// behave differently e.g. for "-gsplit-dwarf -g1").
- && Stat::stat(ctx.args_info.output_dwo)
+ && DirEntry(ctx.args_info.output_dwo).is_regular_file()
&& !serializer.add_file(core::Result::FileType::dwarf_object,
ctx.args_info.output_dwo)) {
LOG("Split dwarf file {} missing", ctx.args_info.output_dwo);
args.push_back(ctx.args_info.output_obj);
}
- if (ctx.config.hard_link() && ctx.args_info.output_obj != "/dev/null") {
+ if (ctx.config.hard_link()
+ && !util::is_dev_null_path(ctx.args_info.output_obj)) {
// Workaround for Clang bug where it overwrites an existing object file
// when it's compiling an assembler file, see
// <https://bugs.llvm.org/show_bug.cgi?id=39782>.
ASSERT(result_key);
- bool produce_dep_file = ctx.args_info.generating_dependencies
- && ctx.args_info.output_dep != "/dev/null";
-
- if (produce_dep_file) {
+ if (ctx.args_info.generating_dependencies) {
Depfile::make_paths_relative_in_output_dep(ctx);
}
- Stat obj_stat;
if (!ctx.args_info.expect_output_obj) {
// Don't probe for object file when we don't expect one since we otherwise
// will be fooled by an already existing object file.
LOG_RAW("Compiler not expected to produce an object file");
} else {
- obj_stat = Stat::stat(ctx.args_info.output_obj);
- if (!obj_stat) {
+ DirEntry dir_entry(ctx.args_info.output_obj);
+ if (!dir_entry.is_regular_file()) {
LOG_RAW("Compiler didn't produce an object file");
return tl::unexpected(Statistic::compiler_produced_no_output);
- } else if (obj_stat.size() == 0) {
+ } else if (dir_entry.size() == 0) {
LOG_RAW("Compiler produced an empty object file");
return tl::unexpected(Statistic::compiler_produced_empty_output);
}
MTR_BEGIN("result", "result_put");
if (!write_result(
- ctx, *result_key, obj_stat, result->stdout_data, result->stderr_data)) {
+ ctx, *result_key, result->stdout_data, result->stderr_data)) {
return tl::unexpected(Statistic::compiler_produced_no_output);
}
MTR_END("result", "result_put");
static tl::expected<void, Failure>
hash_compiler(const Context& ctx,
Hash& hash,
- const Stat& st,
+ const DirEntry& dir_entry,
const std::string& path,
bool allow_command)
{
// Do nothing.
} else if (ctx.config.compiler_check() == "mtime") {
hash.hash_delimiter("cc_mtime");
- hash.hash(st.size());
- hash.hash(st.mtime().nsec());
+ hash.hash(dir_entry.size());
+ hash.hash(dir_entry.mtime().nsec());
} else if (util::starts_with(ctx.config.compiler_check(), "string:")) {
hash.hash_delimiter("cc_hash");
hash.hash(&ctx.config.compiler_check()[7]);
static tl::expected<void, Failure>
hash_nvcc_host_compiler(const Context& ctx,
Hash& hash,
- const Stat* ccbin_st = nullptr,
+ const DirEntry* ccbin_st = nullptr,
const std::string& ccbin = {})
{
// From <http://docs.nvidia.com/cuda/cuda-compiler-driver-nvcc/index.html>:
for (const char* compiler : compilers) {
if (!ccbin.empty()) {
std::string path = FMT("{}/{}", ccbin, compiler);
- auto st = Stat::stat(path);
- if (st) {
- TRY(hash_compiler(ctx, hash, st, path, false));
+ DirEntry de(path);
+ if (de.is_regular_file()) {
+ TRY(hash_compiler(ctx, hash, de, path, false));
}
} else {
std::string path = find_executable(ctx, compiler, ctx.orig_args[0]);
if (!path.empty()) {
- auto st = Stat::stat(path, Stat::LogOnError::yes);
- TRY(hash_compiler(ctx, hash, st, ccbin, false));
+ DirEntry de(path, DirEntry::LogOnError::yes);
+ TRY(hash_compiler(ctx, hash, de, ccbin, false));
}
}
}
const std::string compiler_path = args[0];
#endif
- auto st = Stat::stat(compiler_path, Stat::LogOnError::yes);
- if (!st) {
+ DirEntry dir_entry(compiler_path, DirEntry::LogOnError::yes);
+ if (!dir_entry.is_regular_file()) {
return tl::unexpected(Statistic::could_not_find_compiler);
}
// Hash information about the compiler.
- TRY(hash_compiler(ctx, hash, st, compiler_path, true));
+ TRY(hash_compiler(ctx, hash, dir_entry, compiler_path, true));
// Also hash the compiler name as some compilers use hard links and behave
// differently depending on the real name.
} else {
path = args[i].substr(eq_pos + 1);
}
- auto st = Stat::stat(path, Stat::LogOnError::yes);
- if (st) {
+ DirEntry dir_entry(path, DirEntry::LogOnError::yes);
+ if (dir_entry.is_regular_file()) {
// If given an explicit specs file, then hash that file, but don't
// include the path to it in the hash.
hash.hash_delimiter("specs");
- TRY(hash_compiler(ctx, hash, st, path, false));
+ TRY(hash_compiler(ctx, hash, dir_entry, path, false));
return {};
}
}
if (util::starts_with(args[i], "-fplugin=")) {
- auto st = Stat::stat(&args[i][9], Stat::LogOnError::yes);
- if (st) {
+ DirEntry dir_entry(&args[i][9], DirEntry::LogOnError::yes);
+ if (dir_entry.is_regular_file()) {
hash.hash_delimiter("plugin");
- TRY(hash_compiler(ctx, hash, st, &args[i][9], false));
+ TRY(hash_compiler(ctx, hash, dir_entry, &args[i][9], false));
return {};
}
}
if (args[i] == "-Xclang" && i + 3 < args.size() && args[i + 1] == "-load"
&& args[i + 2] == "-Xclang") {
- auto st = Stat::stat(args[i + 3], Stat::LogOnError::yes);
- if (st) {
+ DirEntry dir_entry(args[i + 3], DirEntry::LogOnError::yes);
+ if (dir_entry.is_regular_file()) {
hash.hash_delimiter("plugin");
- TRY(hash_compiler(ctx, hash, st, args[i + 3], false));
+ TRY(hash_compiler(ctx, hash, dir_entry, args[i + 3], false));
i += 3;
return {};
}
if ((args[i] == "-ccbin" || args[i] == "--compiler-bindir")
&& i + 1 < args.size()) {
- auto st = Stat::stat(args[i + 1]);
- if (st) {
+ DirEntry dir_entry(args[i + 1]);
+ if (dir_entry.exists()) {
found_ccbin = true;
hash.hash_delimiter("ccbin");
- TRY(hash_nvcc_host_compiler(ctx, hash, &st, args[i + 1]));
+ TRY(hash_nvcc_host_compiler(ctx, hash, &dir_entry, args[i + 1]));
i++;
return {};
}
bool found = false;
for (const std::string& p : paths_to_try) {
LOG("Checking for profile data file {}", p);
- auto st = Stat::stat(p);
- if (st && !st.is_directory()) {
+ if (DirEntry(p).is_regular_file()) {
LOG("Adding profile data {} to the hash", p);
hash.hash_delimiter("-fprofile-use");
if (hash_binary_file(ctx, hash, p)) {
TRY(hash_argument(ctx, args, i, hash, is_clang, direct_mode, found_ccbin));
}
- // Make results with dependency file /dev/null different from those without
- // it.
- if (ctx.args_info.generating_dependencies
- && ctx.args_info.output_dep == "/dev/null") {
- hash.hash_delimiter("/dev/null dependency file");
- }
-
if (!found_ccbin && ctx.args_info.actual_language == "cu") {
TRY(hash_nvcc_host_compiler(ctx, hash));
}
ctx.config.set_run_second_cpp(true);
}
- if (ctx.config.depend_mode()) {
- const bool deps = ctx.args_info.generating_dependencies
- && ctx.args_info.output_dep != "/dev/null";
- const bool includes = ctx.args_info.generating_includes;
- if (!ctx.config.run_second_cpp() || (!deps && !includes)) {
- LOG_RAW("Disabling depend mode");
- ctx.config.set_depend_mode(false);
- }
+ if (ctx.config.depend_mode()
+ && !(ctx.config.run_second_cpp()
+ && (ctx.args_info.generating_dependencies
+ || ctx.args_info.generating_includes))) {
+ LOG_RAW("Disabling depend mode");
+ ctx.config.set_depend_mode(false);
}
if (ctx.storage.has_remote_storage()) {
#include <util/expected.hpp>
#include <util/file.hpp>
+using util::DirEntry;
+
namespace core {
-Stat
-FileRecompressor::recompress(const Stat& stat,
+DirEntry
+FileRecompressor::recompress(const DirEntry& dir_entry,
std::optional<int8_t> level,
KeepAtime keep_atime)
{
- core::CacheEntry::Header header(stat.path());
+ core::CacheEntry::Header header(dir_entry.path().string());
const int8_t wanted_level =
level ? (*level == 0 ? core::CacheEntry::default_compression_level : *level)
: 0;
- std::optional<Stat> new_stat;
+ std::optional<DirEntry> new_dir_entry;
if (header.compression_level != wanted_level) {
const auto cache_file_data = util::value_or_throw<core::Error>(
- util::read_file<util::Bytes>(stat.path()),
- FMT("Failed to read {}: ", stat.path()));
+ util::read_file<util::Bytes>(dir_entry.path().string()),
+ FMT("Failed to read {}: ", dir_entry.path().string()));
core::CacheEntry cache_entry(cache_file_data);
cache_entry.verify_checksum();
level ? core::CompressionType::zstd : core::CompressionType::none;
header.compression_level = wanted_level;
- AtomicFile new_cache_file(stat.path(), AtomicFile::Mode::binary);
+ AtomicFile new_cache_file(dir_entry.path().string(),
+ AtomicFile::Mode::binary);
new_cache_file.write(
core::CacheEntry::serialize(header, cache_entry.payload()));
new_cache_file.commit();
- new_stat = Stat::lstat(stat.path(), Stat::LogOnError::yes);
+ new_dir_entry =
+ DirEntry(dir_entry.path().string(), DirEntry::LogOnError::yes);
}
// Restore mtime/atime to keep cache LRU cleanup working as expected:
- if (keep_atime == KeepAtime::yes || new_stat) {
- util::set_timestamps(stat.path(), stat.mtime(), stat.atime());
+ if (keep_atime == KeepAtime::yes || new_dir_entry) {
+ util::set_timestamps(
+ dir_entry.path().string(), dir_entry.mtime(), dir_entry.atime());
}
m_content_size += util::likely_size_on_disk(header.entry_size);
- m_old_size += stat.size_on_disk();
- m_new_size += (new_stat ? *new_stat : stat).size_on_disk();
+ m_old_size += dir_entry.size_on_disk();
+ m_new_size += new_dir_entry.value_or(dir_entry).size_on_disk();
- return new_stat ? *new_stat : stat;
+ return new_dir_entry.value_or(dir_entry);
}
uint64_t
-// Copyright (C) 2022 Joel Rosdahl and other contributors
+// Copyright (C) 2022-2023 Joel Rosdahl and other contributors
//
// See doc/AUTHORS.adoc for a complete list of contributors.
//
#pragma once
-#include <Stat.hpp>
+#include <util/DirEntry.hpp>
#include <atomic>
#include <cstdint>
FileRecompressor() = default;
// Returns stat after recompression.
- Stat recompress(const Stat& stat,
- std::optional<int8_t> level,
- KeepAtime keep_atime);
+ util::DirEntry recompress(const util::DirEntry& dir_entry,
+ std::optional<int8_t> level,
+ KeepAtime keep_atime);
uint64_t content_size() const;
uint64_t old_size() const;
auto stated_files_iter = stated_files.find(path);
if (stated_files_iter == stated_files.end()) {
- const auto file_stat = Stat::stat(path);
- if (!file_stat) {
+ util::DirEntry entry(path);
+ if (!entry) {
LOG("Info: {} is mentioned in a manifest entry but can't be read ({})",
path,
- strerror(file_stat.error_number()));
+ strerror(entry.error_number()));
return false;
}
FileStats st;
- st.size = file_stat.size();
- st.mtime = file_stat.mtime();
- st.ctime = file_stat.ctime();
+ st.size = entry.size();
+ st.mtime = entry.mtime();
+ st.ctime = entry.ctime();
stated_files_iter = stated_files.emplace(path, st).first;
}
const FileStats& fs = stated_files_iter->second;
-// Copyright (C) 2019-2022 Joel Rosdahl and other contributors
+// Copyright (C) 2019-2023 Joel Rosdahl and other contributors
//
// See doc/AUTHORS.adoc for a complete list of contributors.
//
#include "Fd.hpp"
#include "File.hpp"
#include "Logging.hpp"
-#include "Stat.hpp"
#include "Util.hpp"
#include <ccache.hpp>
#include <core/exceptions.hpp>
#include <fmtmacros.hpp>
#include <util/Bytes.hpp>
+#include <util/DirEntry.hpp>
#include <util/expected.hpp>
#include <util/file.hpp>
#include <util/path.hpp>
// <raw_file_marker> ::= 1 (uint8_t)
// <file_size> ::= uint64_t
+using util::DirEntry;
+
namespace {
// File data stored inside the result file.
{
m_serialized_size += 1 + 1 + 8; // marker + file_type + file_size
if (!should_store_raw_file(m_config, file_type)) {
- auto st = Stat::stat(path);
- if (!st) {
+ DirEntry entry(path);
+ if (!entry.is_regular_file()) {
return false;
}
- m_serialized_size += st.size();
+ m_serialized_size += entry.size();
}
m_file_entries.push_back(FileEntry{file_type, path});
return true;
is_file_entry && should_store_raw_file(m_config, entry.file_type);
const uint64_t file_size =
is_file_entry
- ? Stat::stat(std::get<std::string>(entry.data), Stat::LogOnError::yes)
+ ? DirEntry(std::get<std::string>(entry.data), DirEntry::LogOnError::yes)
.size()
: std::get<nonstd::span<const uint8_t>>(entry.data).size();
#include "Util.hpp"
#include "fmtmacros.hpp"
-#include <Stat.hpp>
#include <core/exceptions.hpp>
#include <fmtmacros.hpp>
#include <util/Bytes.hpp>
+#include <util/DirEntry.hpp>
#include <util/expected.hpp>
#include <util/file.hpp>
#include <util/wincompat.hpp>
#include <vector>
+using util::DirEntry;
+
namespace core {
ResultExtractor::ResultExtractor(
throw Error("Raw entry for non-local result");
}
const auto raw_file_path = (*m_get_raw_file_path)(file_number);
- const auto st = Stat::stat(raw_file_path, Stat::LogOnError::yes);
- if (!st) {
- throw Error(
- FMT("Failed to stat {}: {}", raw_file_path, strerror(st.error_number())));
+ DirEntry entry(raw_file_path, DirEntry::LogOnError::yes);
+ if (!entry) {
+ throw Error(FMT(
+ "Failed to stat {}: {}", raw_file_path, strerror(entry.error_number())));
}
- if (st.size() != file_size) {
+ if (entry.size() != file_size) {
throw Error(FMT("Bad file size of {} (actual {} bytes, expected {} bytes)",
raw_file_path,
- st.size(),
+ entry.size(),
file_size));
}
#include "Logging.hpp"
#include <Context.hpp>
-#include <Stat.hpp>
#include <Util.hpp>
#include <core/MsvcShowIncludesOutput.hpp>
#include <core/common.hpp>
#include <core/exceptions.hpp>
#include <fmtmacros.hpp>
+#include <util/DirEntry.hpp>
#include <util/expected.hpp>
#include <util/file.hpp>
+#include <util/path.hpp>
#include <util/string.hpp>
#include <util/wincompat.hpp>
# include <unistd.h>
#endif
+using util::DirEntry;
+
namespace core {
using Result::FileType;
const auto dest_path = get_dest_path(file_type);
if (dest_path.empty()) {
LOG_RAW("Not writing");
- } else if (dest_path == "/dev/null") {
- LOG_RAW("Not writing to /dev/null");
+ } else if (util::is_dev_null_path(dest_path)) {
+ LOG("Not writing to {}", dest_path);
} else {
LOG("Writing to {}", dest_path);
if (file_type == FileType::dependency) {
}
const auto raw_file_path =
m_ctx.storage.local.get_raw_file_path(*m_result_key, file_number);
- const auto st = Stat::stat(raw_file_path, Stat::LogOnError::yes);
- if (!st) {
+ DirEntry de(raw_file_path, DirEntry::LogOnError::yes);
+ if (!de) {
throw Error(
- FMT("Failed to stat {}: {}", raw_file_path, strerror(st.error_number())));
+ FMT("Failed to stat {}: {}", raw_file_path, strerror(de.error_number())));
}
- if (st.size() != file_size) {
+ if (de.size() != file_size) {
throw core::Error(
FMT("Bad file size of {} (actual {} bytes, expected {} bytes)",
raw_file_path,
- st.size(),
+ de.size(),
file_size));
}
case FileType::dwarf_object:
if (m_ctx.args_info.seen_split_dwarf
- && m_ctx.args_info.output_obj != "/dev/null") {
+ && !util::is_dev_null_path(m_ctx.args_info.output_obj)) {
return m_ctx.args_info.output_dwo;
}
break;
result.append(line.data(), line.length());
} else {
std::string path(line.substr(0, path_end));
- if (Stat::stat(path)) {
+ if (util::DirEntry(path)) {
result += util::real_path(path);
auto tail = line.substr(path_end);
result.append(tail.data(), tail.length());
}
#endif
+using util::DirEntry;
+
namespace core {
constexpr const char VERSION_TEXT[] =
std::optional<std::optional<int8_t>> recompress_level,
uint32_t recompress_threads)
{
- std::vector<Stat> files;
+ std::vector<DirEntry> files;
uint64_t initial_size = 0;
util::throw_on_error<core::Error>(util::traverse_directory(
if (is_dir || TemporaryFile::is_tmp_file(path)) {
return;
}
- auto stat = Stat::lstat(path);
- if (!stat) {
+ DirEntry entry(path);
+ if (!entry) {
// Probably some race, ignore.
return;
}
- initial_size += stat.size_on_disk();
+ initial_size += entry.size_on_disk();
const auto name = Util::base_name(path);
if (name == "ccache.conf" || name == "stats") {
throw Fatal(
FMT("this looks like a local cache directory (found {})", path));
}
- files.emplace_back(std::move(stat));
+ files.emplace_back(std::move(entry));
}));
std::sort(files.begin(), files.end(), [&](const auto& f1, const auto& f2) {
if (final_size <= trim_max_size) {
break;
}
- if (util::remove(file.path())) {
+ if (util::remove(file.path().string())) {
++removed_files;
final_size -= file.size_on_disk();
}
}
Statistics statistics(StatsLog(config.stats_log()).read());
const auto timestamp =
- Stat::stat(config.stats_log(), Stat::LogOnError::yes).mtime();
+ DirEntry(config.stats_log(), DirEntry::LogOnError::yes).mtime();
PRINT_RAW(
stdout,
statistics.format_human_readable(config, timestamp, verbosity, true));
#include "Fd.hpp"
#include "Logging.hpp"
#include "SignalHandler.hpp"
-#include "Stat.hpp"
#include "TemporaryFile.hpp"
#include "Util.hpp"
#include "Win32Util.hpp"
#include <ccache.hpp>
#include <core/exceptions.hpp>
#include <fmtmacros.hpp>
+#include <util/DirEntry.hpp>
#include <util/file.hpp>
#include <util/path.hpp>
#include <util/string.hpp>
// symlink to another ccache executable.
const bool candidate_exists =
#ifdef _WIN32
- Stat::stat(candidate);
+ util::DirEntry(candidate).is_regular_file();
#else
access(candidate.c_str(), X_OK) == 0;
#endif
#include "Config.hpp"
#include "Context.hpp"
#include "Logging.hpp"
-#include "Stat.hpp"
#include "Win32Util.hpp"
#include "execute.hpp"
#include "macroskip.hpp"
#include <core/exceptions.hpp>
#include <fmtmacros.hpp>
+#include <util/DirEntry.hpp>
#include <util/file.hpp>
#include <util/string.hpp>
#include <util/time.hpp>
if (result.contains(HashSourceCode::found_timestamp)) {
LOG("Found __TIMESTAMP__ in {}", path);
- const auto stat = Stat::stat(path);
- if (!stat) {
+ util::DirEntry dir_entry(path);
+ if (!dir_entry.is_regular_file()) {
result.insert(HashSourceCode::error);
return result;
}
- auto modified_time = util::localtime(stat.mtime());
+ auto modified_time = util::localtime(dir_entry.mtime());
if (!modified_time) {
result.insert(HashSourceCode::error);
return result;
using core::Statistic;
using core::StatisticsCounters;
+using util::DirEntry;
namespace storage::local {
} // namespace
-// Return size change in KiB between `old_stat` and `new_stat`.
+// Return size change in KiB between `old_dir_entry` and `new_dir_entry`.
static int64_t
-kibibyte_size_diff(const Stat& old_stat, const Stat& new_stat)
+kibibyte_size_diff(const DirEntry& old_dir_entry, const DirEntry& new_dir_entry)
{
- return (static_cast<int64_t>(new_stat.size_on_disk())
- - static_cast<int64_t>(old_stat.size_on_disk()))
+ return (static_cast<int64_t>(new_dir_entry.size_on_disk())
+ - static_cast<int64_t>(old_dir_entry.size_on_disk()))
/ 1024;
}
}
static void
-delete_file(const std::string& path,
- const uint64_t size,
+delete_file(const DirEntry& dir_entry,
uint64_t& cache_size,
uint64_t& files_in_cache)
{
- const auto result = util::remove_nfs_safe(path, util::LogFailure::no);
+ const auto result =
+ util::remove_nfs_safe(dir_entry.path().string(), util::LogFailure::no);
if (!result && result.error().value() != ENOENT
&& result.error().value() != ESTALE) {
- LOG("Failed to unlink {} ({})", path, strerror(errno));
+ LOG("Failed to unlink {} ({})", dir_entry.path().string(), strerror(errno));
} else {
// The counters are intentionally subtracted even if there was no file to
// delete since the final cache size calculation will be incorrect if they
// aren't. (This can happen when there are several parallel ongoing
// cleanups of the same directory.)
- cache_size -= size;
+ cache_size -= dir_entry.size_on_disk();
--files_in_cache;
}
}
++i, progress_receiver(1.0 / 3 + 1.0 * i / files.size() / 3)) {
const auto& file = files[i];
- if (!file.is_regular()) {
+ if (!file.is_regular_file()) {
// Not a file or missing file.
continue;
}
// Delete any tmp files older than 1 hour right away.
if (file.mtime() + util::Duration(3600) < current_time
- && TemporaryFile::is_tmp_file(file.path())) {
- util::remove(file.path());
+ && TemporaryFile::is_tmp_file(file.path().string())) {
+ util::remove(file.path().string());
continue;
}
if (namespace_ && file_type_from_path(file.path()) == FileType::raw) {
const auto result_filename =
- FMT("{}R", file.path().substr(0, file.path().length() - 2));
- raw_files_map[result_filename].push_back(file.path());
+ FMT("{}R",
+ file.path().string().substr(0, file.path().string().length() - 2));
+ raw_files_map[result_filename].push_back(file.path().string());
}
cache_size += file.size_on_disk();
if (namespace_) {
try {
- core::CacheEntry::Header header(file.path());
+ core::CacheEntry::Header header(file.path().string());
if (header.namespace_ != *namespace_) {
continue;
}
// For namespace eviction we need to remove raw files based on result
// filename since they don't have a header.
if (file_type_from_path(file.path()) == FileType::result) {
- const auto entry = raw_files_map.find(file.path());
+ const auto entry = raw_files_map.find(file.path().string());
if (entry != raw_files_map.end()) {
for (const auto& raw_file : entry->second) {
- delete_file(raw_file,
- Stat::lstat(raw_file).size_on_disk(),
- cache_size,
- files_in_cache);
+ delete_file(DirEntry(raw_file), cache_size, files_in_cache);
}
}
}
}
- delete_file(file.path(), file.size_on_disk(), cache_size, files_in_cache);
+ delete_file(file, cache_size, files_in_cache);
cleaned = true;
}
}
FileType
-file_type_from_path(std::string_view path)
+file_type_from_path(const fs::path& path)
{
- if (util::ends_with(path, "M")) {
+ auto filename = path.filename().string();
+ if (util::ends_with(filename, "M")) {
return FileType::manifest;
- } else if (util::ends_with(path, "R")) {
+ } else if (util::ends_with(filename, "R")) {
return FileType::result;
- } else if (util::ends_with(path, "W")) {
+ } else if (util::ends_with(filename, "W")) {
return FileType::raw;
} else {
return FileType::unknown;
std::optional<util::Bytes> return_value;
const auto cache_file = look_up_cache_file(key, type);
- if (cache_file.stat) {
+ if (cache_file.dir_entry.is_regular_file()) {
const auto value = util::read_file<util::Bytes>(cache_file.path);
if (value) {
LOG("Retrieved {} from local storage ({})",
MTR_SCOPE("local_storage", "put");
const auto cache_file = look_up_cache_file(key, type);
- if (only_if_missing && cache_file.stat) {
+ if (only_if_missing && cache_file.dir_entry.exists()) {
LOG("Not storing {} in local storage since it already exists",
cache_file.path);
return;
increment_statistic(Statistic::local_storage_write);
- const auto new_stat = Stat::stat(cache_file.path, Stat::LogOnError::yes);
- if (!new_stat) {
+ DirEntry new_dir_entry(cache_file.path, DirEntry::LogOnError::yes);
+ if (!new_dir_entry.exists()) {
return;
}
- int64_t files_change = cache_file.stat ? 0 : 1;
- int64_t size_change_kibibyte = kibibyte_size_diff(cache_file.stat, new_stat);
+ int64_t files_change = cache_file.dir_entry.exists() ? 0 : 1;
+ int64_t size_change_kibibyte =
+ kibibyte_size_diff(cache_file.dir_entry, new_dir_entry);
auto counters =
increment_level_2_counters(key, files_change, size_change_kibibyte);
MTR_SCOPE("local_storage", "remove");
const auto cache_file = look_up_cache_file(key, type);
- if (!cache_file.stat) {
+ if (!cache_file.dir_entry) {
LOG("No {} to remove from local storage", util::format_digest(key));
return;
}
util::format_digest(key),
cache_file.path);
increment_level_2_counters(
- key, -1, -static_cast<int64_t>(cache_file.stat.size_on_disk() / 1024));
+ key, -1, -static_cast<int64_t>(cache_file.dir_entry.size_on_disk() / 1024));
}
std::string
for (auto [file_number, source_path] : raw_files) {
const auto dest_path = get_raw_file_path(cache_file.path, file_number);
- const auto old_stat = Stat::stat(dest_path);
+ DirEntry old_dir_entry(dest_path);
+ old_dir_entry.refresh();
try {
clone_hard_link_or_copy_file(source_path, dest_path, true);
m_added_raw_files.push_back(dest_path);
e.what());
throw;
}
- const auto new_stat = Stat::stat(dest_path);
+ DirEntry new_dir_entry(dest_path);
increment_statistic(Statistic::cache_size_kibibyte,
- kibibyte_size_diff(old_stat, new_stat));
+ kibibyte_size_diff(old_dir_entry, new_dir_entry));
increment_statistic(Statistic::files_in_cache,
- (new_stat ? 1 : 0) - (old_stat ? 1 : 0));
+ (new_dir_entry ? 1 : 0) - (old_dir_entry ? 1 : 0));
}
}
counters.increment(StatsFile(path).read());
zero_timestamp = std::max(counters.get(Statistic::stats_zeroed_timestamp),
zero_timestamp);
- last_updated = std::max(last_updated, Stat::stat(path).mtime());
+ last_updated = std::max(last_updated, DirEntry(path).mtime());
});
counters.set(Statistic::stats_zeroed_timestamp, zero_timestamp);
l2_progress_receiver(0.5);
for (size_t i = 0; i < files.size(); ++i) {
- util::remove_nfs_safe(files[i].path());
+ util::remove_nfs_safe(files[i].path().string());
l2_progress_receiver(0.5 + 0.5 * i / files.size());
}
for (size_t i = 0; i < files.size(); ++i) {
const auto& cache_file = files[i];
try {
- core::CacheEntry::Header header(cache_file.path());
+ core::CacheEntry::Header header(cache_file.path().string());
cs.actual_size += cache_file.size_on_disk();
cs.content_size += util::likely_size_on_disk(header.entry_size);
} catch (core::Error&) {
if (file_type_from_path(file.path()) != FileType::unknown) {
thread_pool.enqueue([=, &recompressor, &incompressible_size] {
try {
- Stat new_stat = recompressor.recompress(
+ DirEntry new_dir_entry = recompressor.recompress(
file, level, core::FileRecompressor::KeepAtime::no);
auto size_change_kibibyte =
- kibibyte_size_diff(file, new_stat);
+ kibibyte_size_diff(file, new_dir_entry);
if (size_change_kibibyte != 0) {
StatsFile(stats_file).update([=](auto& cs) {
cs.increment(Statistic::cache_size_kibibyte,
incompressible_size += file.size_on_disk();
}
});
- } else if (!TemporaryFile::is_tmp_file(file.path())) {
+ } else if (!TemporaryFile::is_tmp_file(file.path().string())) {
incompressible_size += file.size_on_disk();
}
for (uint8_t level = k_min_cache_levels; level <= k_max_cache_levels;
++level) {
const auto path = get_path_in_cache(level, key_string);
- const auto stat = Stat::stat(path);
- if (stat) {
- return {path, stat, level};
+ DirEntry dir_entry(path);
+ if (dir_entry.is_regular_file()) {
+ return {path, dir_entry, level};
}
}
const auto shallowest_path =
get_path_in_cache(k_min_cache_levels, key_string);
- return {shallowest_path, Stat(), k_min_cache_levels};
+ return {shallowest_path, DirEntry(), k_min_cache_levels};
}
StatsFile
const auto now = util::TimePoint::now();
const auto cleaned_stamp = FMT("{}/.cleaned", m_config.temporary_dir());
- const auto cleaned_stat = Stat::stat(cleaned_stamp);
- if (cleaned_stat
- && cleaned_stat.mtime() + k_tempdir_cleanup_interval >= now) {
+ DirEntry cleaned_dir_entry(cleaned_stamp);
+ if (cleaned_dir_entry.is_regular_file()
+ && cleaned_dir_entry.mtime() + k_tempdir_cleanup_interval >= now) {
// No cleanup needed.
return;
}
if (is_dir) {
return;
}
- const auto st = Stat::lstat(path, Stat::LogOnError::yes);
- if (st && st.mtime() + k_tempdir_cleanup_interval < now) {
+ DirEntry dir_entry(path, DirEntry::LogOnError::yes);
+ if (dir_entry && dir_entry.mtime() + k_tempdir_cleanup_interval < now) {
util::remove(path);
}
})
#include <third_party/nonstd/span.hpp>
#include <cstdint>
+#include <filesystem>
#include <optional>
#include <string_view>
#include <vector>
enum class FileType { result, manifest, raw, unknown };
-FileType file_type_from_path(std::string_view path);
+FileType file_type_from_path(const std::filesystem::path& path);
class LocalStorage
{
struct LookUpCacheFileResult
{
std::string path;
- Stat stat;
+ util::DirEntry dir_entry;
uint8_t level;
};
#include <util/file.hpp>
#include <util/string.hpp>
+using util::DirEntry;
+
namespace storage::local {
void
}
}
-std::vector<Stat>
+std::vector<DirEntry>
get_cache_dir_files(const std::string& dir)
{
- std::vector<Stat> files;
+ std::vector<DirEntry> files;
- if (!Stat::stat(dir)) {
+ if (!DirEntry(dir).is_directory()) {
return files;
}
util::throw_on_error<core::Error>(
}
if (!is_dir) {
- files.emplace_back(Stat::lstat(path));
+ files.emplace_back(path);
}
}));
-// Copyright (C) 2021-2022 Joel Rosdahl and other contributors
+// Copyright (C) 2021-2023 Joel Rosdahl and other contributors
//
// See doc/AUTHORS.adoc for a complete list of contributors.
//
#pragma once
-#include <Stat.hpp>
+#include <util/DirEntry.hpp>
#include <functional>
#include <string>
// - CACHEDIR.TAG
// - stats
// - .nfs* (temporary NFS files that may be left for open but deleted files).
-std::vector<Stat> get_cache_dir_files(const std::string& dir);
+std::vector<util::DirEntry> get_cache_dir_files(const std::string& dir);
} // namespace storage::local
#include <AtomicFile.hpp>
#include <Logging.hpp>
-#include <Stat.hpp>
#include <Util.hpp>
#include <assertions.hpp>
#include <core/exceptions.hpp>
#include <fmtmacros.hpp>
#include <util/Bytes.hpp>
+#include <util/DirEntry.hpp>
#include <util/UmaskScope.hpp>
#include <util/expected.hpp>
#include <util/file.hpp>
namespace fs = util::filesystem;
+using util::DirEntry;
+
namespace storage::remote {
namespace {
FileStorageBackend::get(const Hash::Digest& key)
{
const auto path = get_entry_path(key);
- const bool exists = Stat::stat(path);
- if (!exists) {
+ if (!DirEntry(path).exists()) {
// Don't log failure if the entry doesn't exist.
return std::nullopt;
}
{
const auto path = get_entry_path(key);
- if (only_if_missing && Stat::stat(path)) {
+ if (only_if_missing && DirEntry(path).exists()) {
LOG("{} already in cache", path);
return false;
}
set(
sources
Bytes.cpp
+ DirEntry.cpp
LockFile.cpp
LongLivedLockFileManager.cpp
TextTable.cpp
// this program; if not, write to the Free Software Foundation, Inc., 51
// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-#include "Stat.hpp"
-
-#include "Finalizer.hpp"
-#include "Logging.hpp"
-#include "Win32Util.hpp"
+#include "DirEntry.hpp"
+#include <Finalizer.hpp>
+#include <Logging.hpp>
+#include <Win32Util.hpp>
#include <core/exceptions.hpp>
#include <fmtmacros.hpp>
#include <util/wincompat.hpp>
win32_file_information_to_stat(const BY_HANDLE_FILE_INFORMATION& file_info,
const FILE_ATTRIBUTE_TAG_INFO& reparse_info,
const char* path,
- Stat::stat_t* st)
+ util::DirEntry::stat_t* st)
{
st->st_dev = file_info.dwVolumeSerialNumber;
st->st_ino = (static_cast<uint64_t>(file_info.nFileIndexHigh) << 32)
}
bool
-win32_stat_impl(const char* path, bool traverse_links, Stat::stat_t* st)
+win32_stat_impl(const char* path,
+ bool traverse_links,
+ util::DirEntry::stat_t* st)
{
*st = {};
}
int
-win32_stat(const char* path, Stat::stat_t* st)
+lstat_func(const char* path, util::DirEntry::stat_t* st)
{
- bool ok = win32_stat_impl(path, true, st);
+ bool ok = win32_stat_impl(path, false, st);
if (ok) {
return 0;
}
}
int
-win32_lstat(const char* path, Stat::stat_t* st)
+stat_func(const char* path, util::DirEntry::stat_t* st)
{
- bool ok = win32_stat_impl(path, false, st);
+ bool ok = win32_stat_impl(path, true, st);
if (ok) {
return 0;
}
return -1;
}
+#else
+
+auto lstat_func = ::lstat;
+auto stat_func = ::stat;
+
#endif // _WIN32
} // namespace
-Stat::Stat(StatFunction stat_function,
- const std::string& path,
- Stat::LogOnError log_on_error)
- : m_path(path)
+namespace util {
+
+const DirEntry::stat_t&
+DirEntry::do_stat() const
{
- int result = stat_function(path.c_str(), &m_stat);
- if (result == 0) {
- m_errno = 0;
- } else {
- m_errno = errno;
- if (log_on_error == LogOnError::yes) {
- LOG("Failed to stat {}: {}", path, strerror(errno));
+ if (!m_initialized) {
+ m_exists = false;
+ m_is_symlink = false;
+
+ int result = lstat_func(m_path.string().c_str(), &m_stat);
+ if (result == 0) {
+ m_errno = 0;
+ if (S_ISLNK(m_stat.st_mode)
+#ifdef _WIN32
+ || (m_stat.st_file_attributes & FILE_ATTRIBUTE_REPARSE_POINT)
+#endif
+ ) {
+ m_is_symlink = true;
+ stat_t st;
+ if (stat_func(m_path.string().c_str(), &st) == 0) {
+ m_stat = st;
+ m_exists = true;
+ }
+ } else {
+ m_exists = true;
+ }
+ } else {
+ m_errno = errno;
+ if (m_log_on_error == LogOnError::yes) {
+ LOG("Failed to lstat {}: {}", m_path.string(), strerror(m_errno));
+ }
+ }
+
+ if (!m_exists) {
+ // The file is missing, so just zero fill the stat structure. This will
+ // make e.g. the is_*() methods return false and mtime() will be 0, etc.
+ memset(&m_stat, '\0', sizeof(m_stat));
}
- // The file is missing, so just zero fill the stat structure. This will
- // make e.g. the is_*() methods return false and mtime() will be 0, etc.
- memset(&m_stat, '\0', sizeof(m_stat));
+ m_initialized = true;
}
-}
-Stat
-Stat::stat(const std::string& path, LogOnError log_on_error)
-{
- return Stat(
-#ifdef _WIN32
- win32_stat,
-#else
- ::stat,
-#endif
- path,
- log_on_error);
+ return m_stat;
}
-Stat
-Stat::lstat(const std::string& path, LogOnError log_on_error)
-{
- return Stat(
-#ifdef _WIN32
- win32_lstat,
-#else
- ::lstat,
-#endif
- path,
- log_on_error);
-}
+} // namespace util
--- /dev/null
+// Copyright (C) 2019-2023 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#pragma once
+
+#include <util/TimePoint.hpp>
+#include <util/file.hpp>
+#include <util/wincompat.hpp>
+
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include <cstdint>
+#include <ctime>
+#include <filesystem>
+
+namespace util {
+
+// This class is similar to std::filesystem::directory_entry with a couple of
+// extra features, for example:
+//
+// - operator bool tells whether the directory entry exists (not following
+// symlinks, in contrast to the exists() method).
+// - Supports access to atime and ctime fields.
+// - Supports logging on error.
+class DirEntry
+{
+public:
+ enum class LogOnError : bool { no, yes };
+
+#if defined(_WIN32)
+ struct stat_t
+ {
+ uint64_t st_dev;
+ uint64_t st_ino;
+ uint16_t st_mode;
+ uint16_t st_nlink;
+ uint64_t st_size;
+ struct timespec st_atim;
+ struct timespec st_mtim;
+ struct timespec st_ctim;
+ uint32_t st_file_attributes;
+ uint32_t st_reparse_tag;
+ };
+#else
+ using stat_t = struct stat;
+#endif
+
+ using dev_t = decltype(stat_t{}.st_dev);
+ using ino_t = decltype(stat_t{}.st_ino);
+
+ // Create an empty directory entry. operator bool() will return false,
+ // error_number() will return ENOENT and other accessors will return false or
+ // 0.
+ DirEntry() = default;
+
+ // The underlying (l)stat(2) call will not be made by the constructor but
+ // on-demand when calling the first query function. That (l)stat result is
+ // then cached. See also the refresh method.
+ DirEntry(const std::filesystem::path& path,
+ LogOnError log_on_error = LogOnError::no);
+
+ // Return true if the file could be lstat(2)-ed (i.e., the directory entry
+ // exists without following symlinks), otherwise false.
+ operator bool() const;
+
+ // Return true if the file could be stat(2)-ed (i.e., the directory entry
+ // exists when following symlinks), otherwise false.
+ bool exists() const;
+
+ // Return the path that this entry refers to.
+ const std::filesystem::path& path() const;
+
+ // Return whether the entry refers to the same device and i-node as `other`.
+ bool same_inode_as(const DirEntry& other) const;
+
+ // Return errno from the lstat(2) call (0 if successful).
+ int error_number() const;
+
+ dev_t device() const;
+ ino_t inode() const;
+ mode_t mode() const;
+ util::TimePoint atime() const;
+ util::TimePoint ctime() const;
+ util::TimePoint mtime() const;
+ uint64_t size() const;
+
+ uint64_t size_on_disk() const;
+
+ bool is_directory() const;
+ bool is_regular_file() const;
+ bool is_symlink() const;
+
+ // Update the cached (l)stat(2) result.
+ void refresh();
+
+#ifdef _WIN32
+ uint32_t file_attributes() const;
+ uint32_t reparse_tag() const;
+#endif
+
+private:
+ std::filesystem::path m_path;
+ LogOnError m_log_on_error = LogOnError::no;
+ mutable stat_t m_stat;
+ mutable int m_errno = -1;
+ mutable bool m_initialized = false;
+ mutable bool m_exists = false;
+ mutable bool m_is_symlink = false;
+
+ const stat_t& do_stat() const;
+};
+
+inline DirEntry::DirEntry(const std::filesystem::path& path,
+ LogOnError log_on_error)
+ : m_path(path),
+ m_log_on_error(log_on_error)
+{
+}
+
+inline DirEntry::operator bool() const
+{
+ do_stat();
+ return m_errno == 0;
+}
+
+inline bool
+DirEntry::exists() const
+{
+ do_stat();
+ return m_exists;
+}
+
+inline bool
+DirEntry::same_inode_as(const DirEntry& other) const
+{
+ do_stat();
+ return m_errno == 0 && device() == other.device() && inode() == other.inode();
+}
+
+inline const std::filesystem::path&
+DirEntry::path() const
+{
+ return m_path;
+}
+
+inline int
+DirEntry::error_number() const
+{
+ do_stat();
+ return m_errno;
+}
+
+inline DirEntry::dev_t
+DirEntry::device() const
+{
+ return do_stat().st_dev;
+}
+
+inline DirEntry::ino_t
+DirEntry::inode() const
+{
+ return do_stat().st_ino;
+}
+
+inline mode_t
+DirEntry::mode() const
+{
+ return do_stat().st_mode;
+}
+
+inline util::TimePoint
+DirEntry::atime() const
+{
+#if defined(_WIN32) || defined(HAVE_STRUCT_STAT_ST_ATIM)
+ return util::TimePoint(do_stat().st_atim);
+#elif defined(HAVE_STRUCT_STAT_ST_ATIMESPEC)
+ return util::TimePoint(do_stat().st_atimespec);
+#else
+ return util::TimePoint(do_stat().st_atime, 0);
+#endif
+}
+
+inline util::TimePoint
+DirEntry::ctime() const
+{
+#if defined(_WIN32) || defined(HAVE_STRUCT_STAT_ST_CTIM)
+ return util::TimePoint(do_stat().st_ctim);
+#elif defined(HAVE_STRUCT_STAT_ST_CTIMESPEC)
+ return util::TimePoint(do_stat().st_ctimespec);
+#else
+ return util::TimePoint(do_stat().st_ctime, 0);
+#endif
+}
+
+inline util::TimePoint
+DirEntry::mtime() const
+{
+#if defined(_WIN32) || defined(HAVE_STRUCT_STAT_ST_MTIM)
+ return util::TimePoint(do_stat().st_mtim);
+#elif defined(HAVE_STRUCT_STAT_ST_CTIMESPEC)
+ return util::TimePoint(do_stat().st_mtimespec);
+#else
+ return util::TimePoint(do_stat().st_mtime, 0);
+#endif
+}
+
+inline uint64_t
+DirEntry::size() const
+{
+ return do_stat().st_size;
+}
+
+inline uint64_t
+DirEntry::size_on_disk() const
+{
+#ifdef _WIN32
+ return util::likely_size_on_disk(size());
+#else
+ return do_stat().st_blocks * 512;
+#endif
+}
+
+inline bool
+DirEntry::is_directory() const
+{
+ return S_ISDIR(mode());
+}
+
+inline bool
+DirEntry::is_symlink() const
+{
+ return m_is_symlink;
+}
+
+inline void
+DirEntry::refresh()
+{
+ m_initialized = false;
+ do_stat();
+}
+
+inline bool
+DirEntry::is_regular_file() const
+{
+ return S_ISREG(mode());
+}
+
+#ifdef _WIN32
+inline uint32_t
+DirEntry::file_attributes() const
+{
+ return do_stat().st_file_attributes;
+}
+
+inline uint32_t
+DirEntry::reparse_tag() const
+{
+ return do_stat().st_reparse_tag;
+}
+#endif
+
+} // namespace util
#include "Win32Util.hpp"
#include "fmtmacros.hpp"
-#include <Stat.hpp>
#include <assertions.hpp>
#include <core/exceptions.hpp>
+#include <util/DirEntry.hpp>
#include <util/file.hpp>
#include <util/filesystem.hpp>
#include <util/process.hpp>
std::optional<TimePoint>
LockFile::get_last_lock_update()
{
- if (const auto stat = Stat::stat(m_alive_file); stat) {
- return stat.mtime();
+ if (DirEntry entry(m_alive_file); entry) {
+ return entry.mtime();
} else {
return std::nullopt;
}
#include <Fd.hpp>
#include <Finalizer.hpp>
#include <Logging.hpp>
-#include <Stat.hpp>
#include <TemporaryFile.hpp>
#include <Win32Util.hpp>
#include <fmtmacros.hpp>
#include <util/Bytes.hpp>
+#include <util/DirEntry.hpp>
#include <util/expected.hpp>
#include <util/file.hpp>
#include <util/filesystem.hpp>
"#\thttp://www.brynosaurus.com/cachedir/\n";
const std::string path = FMT("{}/CACHEDIR.TAG", dir);
- const auto stat = Stat::stat(path);
- if (stat) {
+ if (DirEntry(path).exists()) {
return;
}
const auto result = write_file(path, cachedir_tag);
read_file(const std::string& path, size_t size_hint)
{
if (size_hint == 0) {
- const auto stat = Stat::stat(path);
- if (!stat) {
+ DirEntry de(path);
+ if (!de) {
return tl::unexpected(strerror(errno));
}
- size_hint = stat.size();
+ size_hint = de.size();
}
// +1 to be able to detect EOF in the first read call
} else
# endif
{
- auto stat = Stat::lstat(entry_path);
- if (!stat) {
- if (stat.error_number() == ENOENT || stat.error_number() == ESTALE) {
+ DirEntry dir_entry(entry_path);
+ if (!dir_entry) {
+ if (dir_entry.error_number() == ENOENT
+ || dir_entry.error_number() == ESTALE) {
continue;
}
- return tl::unexpected(FMT(
- "Failed to lstat {}: {}", entry_path, strerror(stat.error_number())));
+ return tl::unexpected(FMT("Failed to lstat {}: {}",
+ entry_path,
+ strerror(dir_entry.error_number())));
}
- is_dir = stat.is_directory();
+ is_dir = dir_entry.is_directory();
}
if (is_dir) {
traverse_directory(entry_path, visitor);
// Note: Intentionally not using std::filesystem::recursive_directory_iterator
// since it visits directories in preorder.
- auto stat = Stat::lstat(directory);
- if (!stat.is_directory()) {
+ DirEntry dir_entry(directory);
+ if (!dir_entry.is_directory()) {
return tl::unexpected(
FMT("Failed to traverse {}: {}",
directory,
- stat ? "Not a directory" : "No such file or directory"));
+ dir_entry ? "Not a directory" : "No such file or directory"));
}
try {
#include <util/wincompat.hpp>
+#ifdef _WIN32
+# include <third_party/win32/winerror_to_errno.h>
+#endif
+
namespace util::filesystem {
tl::expected<void, std::error_code>
if (!MoveFileExA(old_p.string().c_str(),
new_p.string().c_str(),
MOVEFILE_REPLACE_EXISTING)) {
- DWORD error = GetLastError();
- // TODO: How should the Win32 error be mapped to std::error_code?
- return tl::unexpected(std::error_code(error, std::system_category()));
+ return tl::unexpected(std::error_code(winerror_to_errno(GetLastError()),
+ std::system_category()));
}
#endif
return {};
#include "path.hpp"
-#include <Stat.hpp>
#include <Util.hpp>
#include <fmtmacros.hpp>
+#include <util/DirEntry.hpp>
#include <util/filesystem.hpp>
#include <util/string.hpp>
return actual_cwd;
}
- auto pwd_stat = Stat::stat(pwd);
- auto cwd_stat = Stat::stat(actual_cwd);
- return !pwd_stat || !cwd_stat || !pwd_stat.same_inode_as(cwd_stat)
+ DirEntry pwd_de(pwd);
+ DirEntry cwd_de(actual_cwd);
+ return !pwd_de || !cwd_de || !pwd_de.same_inode_as(cwd_de)
? actual_cwd
: Util::normalize_concrete_absolute_path(pwd);
#endif
#pragma once
+#include <util/string.hpp>
+
#include <string>
#include <string_view>
#include <vector>
// Return whether `path` is absolute.
bool is_absolute_path(std::string_view path);
+// Return whether `path` is /dev/null or (on Windows) NUL.
+bool is_dev_null_path(std::string_view path);
+
// Return whether `path` includes at least one directory separator.
bool is_full_path(std::string_view path);
// --- Inline implementations ---
+inline bool
+is_dev_null_path(const std::string_view path)
+{
+ return path == "/dev/null"
+#ifdef _WIN32
+ || util::to_lowercase(path) == "nul"
+#endif
+ ;
+}
+
inline bool
is_full_path(const std::string_view path)
{
test_Config.cpp
test_Depfile.cpp
test_Hash.cpp
- test_Stat.cpp
test_Util.cpp
test_argprocessing.cpp
test_ccache.cpp
test_storage_local_util.cpp
test_util_BitSet.cpp
test_util_Bytes.cpp
+ test_util_DirEntry.cpp
test_util_Duration.cpp
test_util_LockFile.cpp
test_util_TextTable.cpp
if(MSVC AND NOT CMAKE_CXX_COMPILER_ID MATCHES "^Clang$")
# Turn off /Zc:preprocessor for this test because it triggers a bug in some older Windows 10 SDK headers.
- set_source_files_properties(test_Stat.cpp PROPERTIES COMPILE_FLAGS /Zc:preprocessor-)
+ set_source_files_properties(test_util_DirEntry.cpp PROPERTIES COMPILE_FLAGS /Zc:preprocessor-)
endif()
target_link_libraries(
#include "../src/AtomicFile.hpp"
#include "TestUtil.hpp"
-#include <Stat.hpp>
+#include <util/DirEntry.hpp>
#include <util/file.hpp>
#include "third_party/doctest.h"
AtomicFile atomic_file("test", AtomicFile::Mode::text);
atomic_file.write("hello");
}
- CHECK(!Stat::stat("test"));
+ CHECK(!util::DirEntry("test"));
}
TEST_SUITE_END();
InodeCache inode_cache(config, util::Duration(0));
inode_cache.get("a", InodeCache::ContentType::raw);
- CHECK(Stat::stat(inode_cache.get_file()));
+ CHECK(util::DirEntry(inode_cache.get_file()));
CHECK(inode_cache.drop());
- CHECK(!Stat::stat(inode_cache.get_file()));
+ CHECK(!util::DirEntry(inode_cache.get_file()));
CHECK(inode_cache.drop());
}
+++ /dev/null
-// Copyright (C) 2019-2023 Joel Rosdahl and other contributors
-//
-// See doc/AUTHORS.adoc for a complete list of contributors.
-//
-// This program is free software; you can redistribute it and/or modify it
-// under the terms of the GNU General Public License as published by the Free
-// Software Foundation; either version 3 of the License, or (at your option)
-// any later version.
-//
-// This program is distributed in the hope that it will be useful, but WITHOUT
-// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-// more details.
-//
-// You should have received a copy of the GNU General Public License along with
-// this program; if not, write to the Free Software Foundation, Inc., 51
-// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-
-#include "../src/Finalizer.hpp"
-#include "../src/Stat.hpp"
-#include "../src/Util.hpp"
-#include "TestUtil.hpp"
-
-#include <core/exceptions.hpp>
-#include <util/environment.hpp>
-#include <util/file.hpp>
-#include <util/wincompat.hpp>
-
-#include "third_party/doctest.h"
-
-#ifdef HAVE_UNISTD_H
-# include <unistd.h>
-#endif
-
-#ifdef _WIN32
-# include <shlobj.h>
-#endif
-
-using TestUtil::TestContext;
-
-namespace {
-
-bool
-running_under_wine()
-{
-#ifdef _WIN32
- static bool is_wine =
- GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "wine_get_version")
- != nullptr;
- return is_wine;
-#else
- return false;
-#endif
-}
-
-bool
-symlinks_supported()
-{
-#ifdef _WIN32
- // Windows only supports symlinks if the user has the required privilege (e.g.
- // they're an admin) or if developer mode is enabled.
-
- // See: https://stackoverflow.com/a/41232108/192102
- const char* dev_mode_key =
- "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\AppModelUnlock";
- const char* dev_mode_value = "AllowDevelopmentWithoutDevLicense";
-
- DWORD dev_mode_enabled = 0;
- DWORD buf_size = sizeof(dev_mode_enabled);
-
- return !running_under_wine()
- && (IsUserAnAdmin()
- || (RegGetValueA(HKEY_LOCAL_MACHINE,
- dev_mode_key,
- dev_mode_value,
- RRF_RT_DWORD,
- nullptr,
- &dev_mode_enabled,
- &buf_size)
- == ERROR_SUCCESS
- && dev_mode_enabled));
-#else
- return true;
-#endif
-}
-
-#ifdef _WIN32
-bool
-win32_is_junction(const std::string& path)
-{
- HANDLE handle =
- CreateFileA(path.c_str(),
- FILE_READ_ATTRIBUTES,
- FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
- nullptr,
- OPEN_EXISTING,
- FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
- nullptr);
- if (handle == INVALID_HANDLE_VALUE) {
- return false;
- }
- FILE_ATTRIBUTE_TAG_INFO reparse_info = {};
- bool is_junction =
- (GetFileType(handle) == FILE_TYPE_DISK)
- && GetFileInformationByHandleEx(
- handle, FileAttributeTagInfo, &reparse_info, sizeof(reparse_info))
- && (reparse_info.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)
- && (reparse_info.ReparseTag == IO_REPARSE_TAG_MOUNT_POINT);
- CloseHandle(handle);
- return is_junction;
-}
-
-bool
-win32_get_file_info(const std::string& path, BY_HANDLE_FILE_INFORMATION* info)
-{
- HANDLE handle =
- CreateFileA(path.c_str(),
- FILE_READ_ATTRIBUTES,
- FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
- nullptr,
- OPEN_EXISTING,
- FILE_FLAG_BACKUP_SEMANTICS,
- nullptr);
- if (handle == INVALID_HANDLE_VALUE) {
- return false;
- }
- BOOL ret = GetFileInformationByHandle(handle, info);
- CloseHandle(handle);
- return ret;
-}
-
-struct timespec
-win32_filetime_to_timespec(FILETIME ft)
-{
- static const int64_t SECS_BETWEEN_EPOCHS = 11644473600;
- uint64_t v =
- (static_cast<uint64_t>(ft.dwHighDateTime) << 32) | ft.dwLowDateTime;
-
- struct timespec ts = {};
- ts.tv_sec = (v / 10000000) - SECS_BETWEEN_EPOCHS;
- ts.tv_nsec = (v % 10000000) * 100;
- return ts;
-}
-#endif
-
-} // namespace
-
-TEST_SUITE_BEGIN("Stat");
-
-TEST_CASE("Default constructor")
-{
- Stat stat;
- CHECK(!stat);
- CHECK(stat.error_number() == -1);
- CHECK(stat.device() == 0);
- CHECK(stat.inode() == 0);
- CHECK(stat.mode() == 0);
- CHECK(stat.ctime().sec() == 0);
- CHECK(stat.ctime().nsec() == 0);
- CHECK(stat.mtime().sec() == 0);
- CHECK(stat.mtime().nsec() == 0);
- CHECK(stat.size() == 0);
- CHECK(stat.size_on_disk() == 0);
- CHECK(!stat.is_directory());
- CHECK(!stat.is_regular());
- CHECK(!stat.is_symlink());
-
-#ifdef _WIN32
- CHECK(stat.file_attributes() == 0);
- CHECK(stat.reparse_tag() == 0);
-#endif
-}
-
-TEST_CASE("Named constructors")
-{
- CHECK(!Stat::stat("does_not_exist"));
- CHECK(!Stat::stat("does_not_exist", Stat::LogOnError::no));
- CHECK(!Stat::stat("does_not_exist", Stat::LogOnError::yes));
-}
-
-TEST_CASE("Same i-node as")
-{
- TestContext test_context;
-
- util::write_file("a", "");
- util::write_file("b", "");
- auto a_stat = Stat::stat("a");
- auto b_stat = Stat::stat("b");
-
- CHECK(a_stat.same_inode_as(a_stat));
- CHECK(!a_stat.same_inode_as(b_stat));
-
- util::write_file("a", "change size", util::InPlace::yes);
- auto new_a_stat = Stat::stat("a");
- CHECK(new_a_stat.same_inode_as(a_stat));
-
- CHECK(!Stat::stat("nonexistent").same_inode_as(Stat::stat("nonexistent")));
-}
-
-TEST_CASE("Get path")
-{
- TestContext test_context;
-
- util::write_file("a", "");
- CHECK(Stat::stat("a").path() == "a");
- CHECK(Stat::stat("does_not_exist").path() == "does_not_exist");
-}
-
-TEST_CASE("Return values when file is missing")
-{
- auto stat = Stat::stat("does_not_exist");
- CHECK(!stat);
- CHECK(stat.error_number() == ENOENT);
- CHECK(stat.device() == 0);
- CHECK(stat.inode() == 0);
- CHECK(stat.mode() == 0);
- CHECK(stat.ctime().sec() == 0);
- CHECK(stat.ctime().nsec() == 0);
- CHECK(stat.mtime().sec() == 0);
- CHECK(stat.mtime().nsec() == 0);
- CHECK(stat.size() == 0);
- CHECK(stat.size_on_disk() == 0);
- CHECK(!stat.is_directory());
- CHECK(!stat.is_regular());
- CHECK(!stat.is_symlink());
-
-#ifdef _WIN32
- CHECK(stat.file_attributes() == 0);
- CHECK(stat.reparse_tag() == 0);
-#endif
-}
-
-TEST_CASE("Return values when file exists")
-{
- TestContext test_context;
-
- util::write_file("file", "1234567");
-
- auto stat = Stat::stat("file");
- CHECK(stat);
- CHECK(stat.error_number() == 0);
- CHECK(!stat.is_directory());
- CHECK(stat.is_regular());
- CHECK(!stat.is_symlink());
- CHECK(stat.size() == 7);
-
-#ifdef _WIN32
- BY_HANDLE_FILE_INFORMATION info = {};
- CHECK(win32_get_file_info("file", &info));
-
- CHECK(stat.device() == info.dwVolumeSerialNumber);
- CHECK((stat.inode() >> 32) == info.nFileIndexHigh);
- CHECK((stat.inode() & ((1ULL << 32) - 1)) == info.nFileIndexLow);
- CHECK(S_ISREG(stat.mode()));
- CHECK((stat.mode() & ~S_IFMT) == 0666);
-
- struct timespec creation_time =
- win32_filetime_to_timespec(info.ftCreationTime);
- struct timespec last_write_time =
- win32_filetime_to_timespec(info.ftLastWriteTime);
-
- CHECK(stat.ctime().sec() == creation_time.tv_sec);
- CHECK(stat.ctime().nsec_decimal_part() == creation_time.tv_nsec);
- CHECK(stat.mtime().sec() == last_write_time.tv_sec);
- CHECK(stat.mtime().nsec_decimal_part() == last_write_time.tv_nsec);
-
- CHECK(stat.size_on_disk() == ((stat.size() + 4095) & ~4095));
- CHECK(stat.file_attributes() == info.dwFileAttributes);
- CHECK(stat.reparse_tag() == 0);
-
-#else
- struct stat st;
- CHECK(::stat("file", &st) == 0);
-
- CHECK(stat.device() == st.st_dev);
- CHECK(stat.inode() == st.st_ino);
- CHECK(stat.mode() == st.st_mode);
- CHECK(stat.size_on_disk() == st.st_blocks * 512);
-
-# ifdef HAVE_STRUCT_STAT_ST_CTIM
- CHECK(stat.ctime().sec() == st.st_ctim.tv_sec);
- CHECK(stat.ctime().nsec_decimal_part() == st.st_ctim.tv_nsec);
-# elif defined(HAVE_STRUCT_STAT_ST_CTIMESPEC)
- CHECK(stat.ctime().sec() == st.st_ctimespec.tv_sec);
- CHECK(stat.ctime().nsec_decimal_part() == st.st_ctimespec.tv_nsec);
-# else
- CHECK(stat.ctime().sec() == st.st_ctime);
- CHECK(stat.ctime().nsec_decimal_part() == 0);
-# endif
-
-# ifdef HAVE_STRUCT_STAT_ST_MTIM
- CHECK(stat.mtime().sec() == st.st_mtim.tv_sec);
- CHECK(stat.mtime().nsec_decimal_part() == st.st_mtim.tv_nsec);
-# elif defined(HAVE_STRUCT_STAT_ST_MTIMESPEC)
- CHECK(stat.mtime().sec() == st.st_mtimespec.tv_sec);
- CHECK(stat.mtime().nsec_decimal_part() == st.st_mtimespec.tv_nsec);
-# else
- CHECK(stat.mtime().sec() == st.st_mtime);
- CHECK(stat.mtime().nsec_decimal_part() == 0);
-# endif
-#endif
-}
-
-TEST_CASE("Directory")
-{
- TestContext test_context;
-
- REQUIRE(mkdir("directory", 0456) == 0);
- auto stat = Stat::stat("directory");
-
- CHECK(stat);
- CHECK(stat.error_number() == 0);
- CHECK(stat.is_directory());
- CHECK(!stat.is_regular());
- CHECK(!stat.is_symlink());
- CHECK(S_ISDIR(stat.mode()));
-#ifdef _WIN32
- CHECK((stat.mode() & ~S_IFMT) == 0777);
- CHECK((stat.file_attributes() & FILE_ATTRIBUTE_DIRECTORY));
- CHECK(!(stat.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT));
- CHECK(stat.reparse_tag() == 0);
-#endif
-}
-
-TEST_CASE("Symlinks" * doctest::skip(!symlinks_supported()))
-{
- TestContext test_context;
-
- util::write_file("file", "1234567");
-
-#ifdef _WIN32
- REQUIRE(CreateSymbolicLinkA(
- "symlink", "file", 0x2 /*SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE*/));
-#else
- REQUIRE(symlink("file", "symlink") == 0);
-#endif
-
- SUBCASE("file lstat")
- {
- auto stat = Stat::lstat("file", Stat::LogOnError::no);
- CHECK(stat);
- CHECK(stat.error_number() == 0);
- CHECK(!stat.is_directory());
- CHECK(stat.is_regular());
- CHECK(!stat.is_symlink());
- CHECK(S_ISREG(stat.mode()));
- CHECK(stat.size() == 7);
-#ifdef _WIN32
- CHECK(!(stat.file_attributes() & FILE_ATTRIBUTE_DIRECTORY));
- CHECK(!(stat.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT));
- CHECK(stat.reparse_tag() == 0);
-#endif
- }
-
- SUBCASE("file stat")
- {
- auto stat = Stat::stat("file", Stat::LogOnError::no);
- CHECK(stat);
- CHECK(stat.error_number() == 0);
- CHECK(!stat.is_directory());
- CHECK(stat.is_regular());
- CHECK(!stat.is_symlink());
- CHECK(S_ISREG(stat.mode()));
- CHECK(stat.size() == 7);
-#ifdef _WIN32
- CHECK(!(stat.file_attributes() & FILE_ATTRIBUTE_DIRECTORY));
- CHECK(!(stat.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT));
- CHECK(stat.reparse_tag() == 0);
-#endif
- }
-
- SUBCASE("symlink lstat")
- {
- auto stat = Stat::lstat("symlink", Stat::LogOnError::no);
- CHECK(stat);
- CHECK(stat.error_number() == 0);
- CHECK(!stat.is_directory());
- CHECK(!stat.is_regular());
- CHECK(stat.is_symlink());
- CHECK(S_ISLNK(stat.mode()));
-#ifdef _WIN32
- CHECK(!(stat.file_attributes() & FILE_ATTRIBUTE_DIRECTORY));
- CHECK((stat.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT));
- CHECK(stat.reparse_tag() == IO_REPARSE_TAG_SYMLINK);
-#else
- CHECK(stat.size() == 4);
-#endif
- }
-
- SUBCASE("symlink stat")
- {
- auto stat = Stat::stat("symlink", Stat::LogOnError::no);
- CHECK(stat);
- CHECK(stat.error_number() == 0);
- CHECK(!stat.is_directory());
- CHECK(stat.is_regular());
- CHECK(!stat.is_symlink());
- CHECK(S_ISREG(stat.mode()));
- CHECK(stat.size() == 7);
-#ifdef _WIN32
- CHECK(!(stat.file_attributes() & FILE_ATTRIBUTE_DIRECTORY));
- CHECK(!(stat.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT));
- CHECK(stat.reparse_tag() == 0);
-#endif
- }
-}
-
-TEST_CASE("Hard links")
-{
- TestContext test_context;
-
- util::write_file("a", "");
-
-#ifdef _WIN32
- REQUIRE(CreateHardLinkA("b", "a", nullptr));
-#else
- REQUIRE(link("a", "b") == 0);
-#endif
-
- auto stat_a = Stat::stat("a");
- CHECK(stat_a);
- CHECK(stat_a.error_number() == 0);
- CHECK(!stat_a.is_directory());
- CHECK(stat_a.is_regular());
- CHECK(!stat_a.is_symlink());
- CHECK(stat_a.size() == 0);
-
- auto stat_b = Stat::stat("b");
- CHECK(stat_b);
- CHECK(stat_b.error_number() == 0);
- CHECK(!stat_b.is_directory());
- CHECK(stat_b.is_regular());
- CHECK(!stat_b.is_symlink());
- CHECK(stat_b.size() == 0);
-
- CHECK(stat_a.device() == stat_b.device());
- CHECK(stat_a.inode() == stat_b.inode());
- CHECK(stat_a.same_inode_as(stat_b));
-
- util::write_file("a", "1234567", util::InPlace::yes);
- stat_a = Stat::stat("a");
- stat_b = Stat::stat("b");
-
- CHECK(stat_a.size() == 7);
- CHECK(stat_b.size() == 7);
-}
-
-TEST_CASE("Special" * doctest::skip(running_under_wine()))
-{
- SUBCASE("tty")
- {
-#ifdef _WIN32
- auto stat = Stat::stat("\\\\.\\CON");
-#else
- auto stat = Stat::stat("/dev/tty");
-#endif
- CHECK(stat);
- CHECK(stat.error_number() == 0);
- CHECK(!stat.is_directory());
- CHECK(!stat.is_regular());
- CHECK(!stat.is_symlink());
- CHECK(S_ISCHR(stat.mode()));
-#ifdef _WIN32
- CHECK(stat.file_attributes() == 0);
- CHECK(stat.reparse_tag() == 0);
-#endif
- }
-
- SUBCASE("null")
- {
-#ifdef _WIN32
- auto stat = Stat::stat("\\\\.\\NUL");
-#else
- auto stat = Stat::stat("/dev/null");
-#endif
- CHECK(stat);
- CHECK(stat.error_number() == 0);
- CHECK(!stat.is_directory());
- CHECK(!stat.is_regular());
- CHECK(!stat.is_symlink());
- CHECK(S_ISCHR(stat.mode()));
-#ifdef _WIN32
- CHECK(stat.file_attributes() == 0);
- CHECK(stat.reparse_tag() == 0);
-#endif
- }
-
- SUBCASE("pipe")
- {
-#ifdef _WIN32
- const char* pipe_path = "\\\\.\\pipe\\InitShutdown"; // Well-known pipe name
-#else
- const char* pipe_path = "my_pipe";
- REQUIRE(mkfifo(pipe_path, 0600) == 0);
-#endif
-
- auto stat = Stat::stat(pipe_path);
- CHECK(stat);
- CHECK(stat.error_number() == 0);
- CHECK(!stat.is_directory());
- CHECK(!stat.is_regular());
- CHECK(!stat.is_symlink());
- CHECK(S_ISFIFO(stat.mode()));
-#ifdef _WIN32
- CHECK(stat.file_attributes() == 0);
- CHECK(stat.reparse_tag() == 0);
-#endif
- }
-
- SUBCASE("block device")
- {
-#ifdef _WIN32
- auto stat = Stat::stat("\\\\.\\C:");
- CHECK(stat);
- CHECK(stat.error_number() == 0);
- CHECK(!stat.is_directory());
- CHECK(!stat.is_regular());
- CHECK(!stat.is_symlink());
- CHECK(S_ISBLK(stat.mode()));
- CHECK(stat.file_attributes() == 0);
- CHECK(stat.reparse_tag() == 0);
-#endif
- }
-}
-
-#ifdef _WIN32
-TEST_CASE("Win32 Readonly File")
-{
- TestContext test_context;
-
- util::write_file("file", "");
-
- DWORD prev_attrs = GetFileAttributesA("file");
- REQUIRE(prev_attrs != INVALID_FILE_ATTRIBUTES);
- REQUIRE(SetFileAttributesA("file", prev_attrs | FILE_ATTRIBUTE_READONLY));
-
- auto stat = Stat::stat("file");
- REQUIRE(SetFileAttributesA("file", prev_attrs));
-
- CHECK(stat);
- CHECK(stat.error_number() == 0);
- CHECK(S_ISREG(stat.mode()));
- CHECK((stat.mode() & ~S_IFMT) == 0444);
- CHECK((stat.file_attributes() & FILE_ATTRIBUTE_READONLY));
- CHECK(!(stat.file_attributes() & FILE_ATTRIBUTE_DIRECTORY));
- CHECK(!(stat.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT));
- CHECK(stat.reparse_tag() == 0);
-}
-
-TEST_CASE("Win32 Executable File")
-{
- TestContext test_context;
-
- const char* comspec = getenv("COMSPEC");
- REQUIRE(comspec != nullptr);
-
- auto stat = Stat::stat(comspec);
- CHECK(stat);
- CHECK(stat.error_number() == 0);
- CHECK(!stat.is_directory());
- CHECK(stat.is_regular());
- CHECK(!stat.is_symlink());
- CHECK(S_ISREG(stat.mode()));
- CHECK((stat.mode() & ~S_IFMT) == 0777);
- CHECK(!(stat.file_attributes() & FILE_ATTRIBUTE_DIRECTORY));
- CHECK(!(stat.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT));
- CHECK(stat.reparse_tag() == 0);
-}
-
-TEST_CASE("Win32 Pending Delete" * doctest::skip(running_under_wine()))
-{
- TestContext test_context;
-
- HANDLE handle =
- CreateFileA("file",
- GENERIC_READ | GENERIC_WRITE | DELETE,
- FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
- nullptr,
- CREATE_NEW,
- FILE_ATTRIBUTE_NORMAL,
- nullptr);
- REQUIRE_MESSAGE(handle != INVALID_HANDLE_VALUE, "err=" << GetLastError());
- Finalizer cleanup([&] { CloseHandle(handle); });
-
- // Mark file as deleted. This puts it into a "pending delete" state that
- // will persist until the handle is closed. Until the file is closed, new
- // handles cannot be created to the file; attempts to do so fail with
- // ERROR_ACCESS_DENIED/STATUS_DELETE_PENDING. Our stat implementation maps
- // these to ENOENT.
- FILE_DISPOSITION_INFO info{};
- info.DeleteFile = TRUE;
- REQUIRE_MESSAGE(SetFileInformationByHandle(
- handle, FileDispositionInfo, &info, sizeof(info)),
- "err=" << GetLastError());
-
- SUBCASE("stat file pending delete")
- {
- auto st = Stat::stat("file");
- CHECK(!st);
- CHECK(st.error_number() == ENOENT);
- }
-
- SUBCASE("lstat file pending delete")
- {
- auto st = Stat::lstat("file");
- CHECK(!st);
- CHECK(st.error_number() == ENOENT);
- }
-}
-
-// Our Win32 Stat implementation should open files using FILE_READ_ATTRIBUTES,
-// which bypasses sharing restrictions.
-TEST_CASE("Win32 No Sharing")
-{
- TestContext test_context;
-
- HANDLE handle = CreateFileA("file",
- GENERIC_READ | GENERIC_WRITE,
- 0 /* no sharing */,
- nullptr,
- CREATE_NEW,
- FILE_ATTRIBUTE_NORMAL,
- nullptr);
- REQUIRE_MESSAGE(handle != INVALID_HANDLE_VALUE, "err=" << GetLastError());
- Finalizer cleanup([&] { CloseHandle(handle); });
-
- // Sanity check we can't open the file for read/write access.
- REQUIRE(!util::read_file<std::string>("file"));
-
- SUBCASE("stat file no sharing")
- {
- auto stat = Stat::stat("file");
- CHECK(stat);
- CHECK(stat.error_number() == 0);
- CHECK(!stat.is_directory());
- CHECK(stat.is_regular());
- CHECK(!stat.is_symlink());
- CHECK(S_ISREG(stat.mode()));
- CHECK(stat.size() == 0);
- CHECK(!(stat.file_attributes() & FILE_ATTRIBUTE_DIRECTORY));
- CHECK(!(stat.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT));
- CHECK(stat.reparse_tag() == 0);
- }
-
- SUBCASE("lstat file no sharing")
- {
- auto stat = Stat::lstat("file");
- CHECK(stat);
- CHECK(stat.error_number() == 0);
- CHECK(!stat.is_directory());
- CHECK(stat.is_regular());
- CHECK(!stat.is_symlink());
- CHECK(S_ISREG(stat.mode()));
- CHECK(stat.size() == 0);
- CHECK(!(stat.file_attributes() & FILE_ATTRIBUTE_DIRECTORY));
- CHECK(!(stat.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT));
- CHECK(stat.reparse_tag() == 0);
- }
-}
-
-// Creating a directory junction for test purposes is tricky on Windows.
-// Instead, test a well-known junction that has existed in all Windows versions
-// since Vista. (Not present on Wine.)
-TEST_CASE("Win32 Directory Junction"
- * doctest::skip(!win32_is_junction(util::expand_environment_variables(
- "${ALLUSERSPROFILE}\\Application Data"))))
-{
- TestContext test_context;
-
- SUBCASE("junction stat")
- {
- auto stat = Stat::stat(util::expand_environment_variables(
- "${ALLUSERSPROFILE}\\Application Data"));
- CHECK(stat);
- CHECK(stat.error_number() == 0);
- CHECK(stat.is_directory());
- CHECK(!stat.is_regular());
- CHECK(!stat.is_symlink());
- CHECK(S_ISDIR(stat.mode()));
- CHECK((stat.mode() & ~S_IFMT) == 0777);
- CHECK((stat.file_attributes() & FILE_ATTRIBUTE_DIRECTORY));
- CHECK(!(stat.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT));
- CHECK(stat.reparse_tag() == 0);
- }
-
- SUBCASE("junction lstat")
- {
- auto stat = Stat::lstat(util::expand_environment_variables(
- "${ALLUSERSPROFILE}\\Application Data"));
- CHECK(stat);
- CHECK(stat.error_number() == 0);
- CHECK(!stat.is_directory());
- CHECK(!stat.is_regular());
- CHECK(!stat.is_symlink()); // Should only be true for bona fide symlinks
- CHECK((stat.mode() & S_IFMT) == 0); // Not a symlink/file/directory
- CHECK((stat.mode() & ~S_IFMT) == 0777);
- CHECK((stat.file_attributes() & FILE_ATTRIBUTE_DIRECTORY));
- CHECK((stat.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT));
- CHECK(stat.reparse_tag() == IO_REPARSE_TAG_MOUNT_POINT);
- }
-}
-#endif
-
-TEST_SUITE_END();
#include "../src/fmtmacros.hpp"
#include "TestUtil.hpp"
-#include <Stat.hpp>
#include <core/exceptions.hpp>
#include <util/environment.hpp>
#include <util/file.hpp>
#include "TestUtil.hpp"
-#include <Stat.hpp>
#include <core/common.hpp>
+#include <util/DirEntry.hpp>
#include <util/file.hpp>
#include <third_party/doctest.h>
using TestUtil::TestContext;
+using util::DirEntry;
TEST_SUITE_BEGIN("core");
CHECK_NOTHROW(core::ensure_dir_exists("/"));
CHECK_NOTHROW(core::ensure_dir_exists("create/dir"));
- CHECK(Stat::stat("create/dir").is_directory());
+ CHECK(DirEntry("create/dir").is_directory());
util::write_file("create/dir/file", "");
CHECK_THROWS_WITH(
--- /dev/null
+// Copyright (C) 2019-2023 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#include "TestUtil.hpp"
+
+#include <Finalizer.hpp>
+#include <util/DirEntry.hpp>
+#include <util/environment.hpp>
+#include <util/file.hpp>
+#include <util/wincompat.hpp>
+
+#include <third_party/doctest.h>
+
+#ifdef HAVE_UNISTD_H
+# include <unistd.h>
+#endif
+
+#ifdef _WIN32
+# include <shlobj.h>
+#endif
+
+using TestUtil::TestContext;
+using util::DirEntry;
+
+namespace {
+
+bool
+running_under_wine()
+{
+#ifdef _WIN32
+ static bool is_wine =
+ GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "wine_get_version")
+ != nullptr;
+ return is_wine;
+#else
+ return false;
+#endif
+}
+
+bool
+symlinks_supported()
+{
+#ifdef _WIN32
+ // Windows only supports symlinks if the user has the required privilege (e.g.
+ // they're an admin) or if developer mode is enabled.
+
+ // See: https://stackoverflow.com/a/41232108/192102
+ const char* dev_mode_key =
+ "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\AppModelUnlock";
+ const char* dev_mode_value = "AllowDevelopmentWithoutDevLicense";
+
+ DWORD dev_mode_enabled = 0;
+ DWORD buf_size = sizeof(dev_mode_enabled);
+
+ return !running_under_wine()
+ && (IsUserAnAdmin()
+ || (RegGetValueA(HKEY_LOCAL_MACHINE,
+ dev_mode_key,
+ dev_mode_value,
+ RRF_RT_DWORD,
+ nullptr,
+ &dev_mode_enabled,
+ &buf_size)
+ == ERROR_SUCCESS
+ && dev_mode_enabled));
+#else
+ return true;
+#endif
+}
+
+#ifdef _WIN32
+bool
+win32_is_junction(const std::string& path)
+{
+ HANDLE handle =
+ CreateFileA(path.c_str(),
+ FILE_READ_ATTRIBUTES,
+ FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
+ nullptr,
+ OPEN_EXISTING,
+ FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
+ nullptr);
+ if (handle == INVALID_HANDLE_VALUE) {
+ return false;
+ }
+ FILE_ATTRIBUTE_TAG_INFO reparse_info = {};
+ bool is_junction =
+ (GetFileType(handle) == FILE_TYPE_DISK)
+ && GetFileInformationByHandleEx(
+ handle, FileAttributeTagInfo, &reparse_info, sizeof(reparse_info))
+ && (reparse_info.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)
+ && (reparse_info.ReparseTag == IO_REPARSE_TAG_MOUNT_POINT);
+ CloseHandle(handle);
+ return is_junction;
+}
+
+bool
+win32_get_file_info(const std::string& path, BY_HANDLE_FILE_INFORMATION* info)
+{
+ HANDLE handle =
+ CreateFileA(path.c_str(),
+ FILE_READ_ATTRIBUTES,
+ FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
+ nullptr,
+ OPEN_EXISTING,
+ FILE_FLAG_BACKUP_SEMANTICS,
+ nullptr);
+ if (handle == INVALID_HANDLE_VALUE) {
+ return false;
+ }
+ BOOL ret = GetFileInformationByHandle(handle, info);
+ CloseHandle(handle);
+ return ret;
+}
+
+struct timespec
+win32_filetime_to_timespec(FILETIME ft)
+{
+ static const int64_t SECS_BETWEEN_EPOCHS = 11644473600;
+ uint64_t v =
+ (static_cast<uint64_t>(ft.dwHighDateTime) << 32) | ft.dwLowDateTime;
+
+ struct timespec ts = {};
+ ts.tv_sec = (v / 10000000) - SECS_BETWEEN_EPOCHS;
+ ts.tv_nsec = (v % 10000000) * 100;
+ return ts;
+}
+#endif
+
+} // namespace
+
+TEST_SUITE_BEGIN("util::DirEntry");
+
+TEST_CASE("Default constructor")
+{
+ DirEntry entry;
+ CHECK(!entry);
+ CHECK(!entry.exists());
+ CHECK(entry.error_number() == ENOENT);
+ CHECK(entry.path() == "");
+ CHECK(entry.device() == 0);
+ CHECK(entry.inode() == 0);
+ CHECK(entry.mode() == 0);
+ CHECK(entry.ctime().sec() == 0);
+ CHECK(entry.ctime().nsec() == 0);
+ CHECK(entry.mtime().sec() == 0);
+ CHECK(entry.mtime().nsec() == 0);
+ CHECK(entry.size() == 0);
+ CHECK(entry.size_on_disk() == 0);
+ CHECK(!entry.is_directory());
+ CHECK(!entry.is_regular_file());
+ CHECK(!entry.is_symlink());
+
+#ifdef _WIN32
+ CHECK(entry.file_attributes() == 0);
+ CHECK(entry.reparse_tag() == 0);
+#endif
+}
+
+TEST_CASE("Construction for missing entry")
+{
+ DirEntry entry("does_not_exist");
+ CHECK(!entry);
+ CHECK(!entry.exists());
+ CHECK(entry.error_number() == ENOENT);
+ CHECK(entry.path() == "does_not_exist");
+ CHECK(entry.device() == 0);
+ CHECK(entry.inode() == 0);
+ CHECK(entry.mode() == 0);
+ CHECK(entry.ctime().sec() == 0);
+ CHECK(entry.ctime().nsec() == 0);
+ CHECK(entry.mtime().sec() == 0);
+ CHECK(entry.mtime().nsec() == 0);
+ CHECK(entry.size() == 0);
+ CHECK(entry.size_on_disk() == 0);
+ CHECK(!entry.is_directory());
+ CHECK(!entry.is_regular_file());
+ CHECK(!entry.is_symlink());
+
+#ifdef _WIN32
+ CHECK(entry.file_attributes() == 0);
+ CHECK(entry.reparse_tag() == 0);
+#endif
+}
+
+TEST_CASE("Caching and refresh")
+{
+ TestContext test_context;
+
+ util::write_file("a", "");
+
+ DirEntry entry("a");
+ CHECK(entry.size() == 0);
+
+ util::write_file("a", "123", util::InPlace::yes);
+ CHECK(entry.size() == 0);
+ entry.refresh();
+ CHECK(entry.size() == 3);
+}
+
+TEST_CASE("Same i-node as")
+{
+ TestContext test_context;
+
+ util::write_file("a", "");
+ util::write_file("b", "");
+ DirEntry entry_a("a");
+ DirEntry entry_b("b");
+
+ CHECK(entry_a.same_inode_as(entry_a));
+ CHECK(!entry_a.same_inode_as(entry_b));
+
+ util::write_file("a", "change size", util::InPlace::yes);
+ CHECK(DirEntry("a").same_inode_as(entry_a));
+
+ CHECK(!DirEntry("nonexistent").same_inode_as(DirEntry("nonexistent")));
+}
+
+TEST_CASE("Get path")
+{
+ TestContext test_context;
+
+ util::write_file("a", "");
+ CHECK(DirEntry("a").path() == "a");
+ CHECK(DirEntry("does_not_exist").path() == "does_not_exist");
+}
+
+TEST_CASE("Return values when file exists")
+{
+ TestContext test_context;
+
+ util::write_file("file", "1234567");
+
+ DirEntry de("file");
+ CHECK(de);
+ CHECK(de.exists());
+ CHECK(de.error_number() == 0);
+ CHECK(de.path() == "file");
+ CHECK(!de.is_directory());
+ CHECK(de.is_regular_file());
+ CHECK(!de.is_symlink());
+ CHECK(de.size() == 7);
+
+#ifdef _WIN32
+ BY_HANDLE_FILE_INFORMATION info = {};
+ CHECK(win32_get_file_info("file", &info));
+
+ CHECK(de.device() == info.dwVolumeSerialNumber);
+ CHECK((de.inode() >> 32) == info.nFileIndexHigh);
+ CHECK((de.inode() & ((1ULL << 32) - 1)) == info.nFileIndexLow);
+ CHECK(S_ISREG(de.mode()));
+ CHECK((de.mode() & ~S_IFMT) == 0666);
+
+ struct timespec creation_time =
+ win32_filetime_to_timespec(info.ftCreationTime);
+ struct timespec last_write_time =
+ win32_filetime_to_timespec(info.ftLastWriteTime);
+
+ CHECK(de.ctime().sec() == creation_time.tv_sec);
+ CHECK(de.ctime().nsec_decimal_part() == creation_time.tv_nsec);
+ CHECK(de.mtime().sec() == last_write_time.tv_sec);
+ CHECK(de.mtime().nsec_decimal_part() == last_write_time.tv_nsec);
+
+ CHECK(de.size_on_disk() == ((de.size() + 4095) & ~4095));
+ CHECK(de.file_attributes() == info.dwFileAttributes);
+ CHECK(de.reparse_tag() == 0);
+
+#else
+ struct stat st;
+ CHECK(::stat("file", &st) == 0);
+
+ CHECK(de.device() == st.st_dev);
+ CHECK(de.inode() == st.st_ino);
+ CHECK(de.mode() == st.st_mode);
+ CHECK(de.size_on_disk() == st.st_blocks * 512);
+
+# ifdef HAVE_STRUCT_STAT_ST_CTIM
+ CHECK(de.ctime().sec() == st.st_ctim.tv_sec);
+ CHECK(de.ctime().nsec_decimal_part() == st.st_ctim.tv_nsec);
+# elif defined(HAVE_STRUCT_STAT_ST_CTIMESPEC)
+ CHECK(de.ctime().sec() == st.st_ctimespec.tv_sec);
+ CHECK(de.ctime().nsec_decimal_part() == st.st_ctimespec.tv_nsec);
+# else
+ CHECK(de.ctime().sec() == st.st_ctime);
+ CHECK(de.ctime().nsec_decimal_part() == 0);
+# endif
+
+# ifdef HAVE_STRUCT_STAT_ST_MTIM
+ CHECK(de.mtime().sec() == st.st_mtim.tv_sec);
+ CHECK(de.mtime().nsec_decimal_part() == st.st_mtim.tv_nsec);
+# elif defined(HAVE_STRUCT_STAT_ST_MTIMESPEC)
+ CHECK(de.mtime().sec() == st.st_mtimespec.tv_sec);
+ CHECK(de.mtime().nsec_decimal_part() == st.st_mtimespec.tv_nsec);
+# else
+ CHECK(de.mtime().sec() == st.st_mtime);
+ CHECK(de.mtime().nsec_decimal_part() == 0);
+# endif
+#endif
+}
+
+TEST_CASE("Directory")
+{
+ TestContext test_context;
+
+ REQUIRE(mkdir("directory", 0456) == 0);
+ DirEntry entry("directory");
+
+ CHECK(entry);
+ CHECK(entry.exists());
+ CHECK(entry.error_number() == 0);
+ CHECK(entry.is_directory());
+ CHECK(!entry.is_regular_file());
+ CHECK(!entry.is_symlink());
+ CHECK(S_ISDIR(entry.mode()));
+#ifdef _WIN32
+ CHECK((entry.mode() & ~S_IFMT) == 0777);
+ CHECK((entry.file_attributes() & FILE_ATTRIBUTE_DIRECTORY));
+ CHECK(!(entry.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT));
+ CHECK(entry.reparse_tag() == 0);
+#endif
+}
+
+TEST_CASE("Symlinks" * doctest::skip(!symlinks_supported()))
+{
+ TestContext test_context;
+
+ util::write_file("file", "1234567");
+
+#ifdef _WIN32
+ // SYMBOLIC_LINK_FLAG_DIRECTORY: 0x1
+ // SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE: 0x2
+ REQUIRE(CreateSymbolicLinkA("symlink_to_dir", ".", 0x1 | 0x2));
+ REQUIRE(CreateSymbolicLinkA("symlink_to_file", "file", 0x2));
+ REQUIRE(CreateSymbolicLinkA("symlink_to_none", "does_not_exist", 0x2));
+#else
+ REQUIRE(symlink(".", "symlink_to_dir") == 0);
+ REQUIRE(symlink("file", "symlink_to_file") == 0);
+ REQUIRE(symlink("does_not_exist", "symlink_to_none") == 0);
+#endif
+
+ SUBCASE("symlink to dir")
+ {
+ DirEntry entry("symlink_to_dir");
+ CHECK(entry);
+ CHECK(entry.path() == "symlink_to_dir");
+ CHECK(entry.exists());
+ CHECK(entry.error_number() == 0);
+ CHECK(entry.is_directory());
+ CHECK(!entry.is_regular_file());
+ CHECK(entry.is_symlink());
+ CHECK(S_ISDIR(entry.mode()));
+#ifdef _WIN32
+ CHECK((entry.file_attributes() & FILE_ATTRIBUTE_DIRECTORY));
+ CHECK(!(entry.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT));
+ CHECK(entry.reparse_tag() == 0);
+#endif
+ }
+
+ SUBCASE("symlink to file")
+ {
+ DirEntry entry("symlink_to_file");
+ CHECK(entry);
+ CHECK(entry.exists());
+ CHECK(entry.error_number() == 0);
+ CHECK(entry.path() == "symlink_to_file");
+ CHECK(!entry.is_directory());
+ CHECK(entry.is_regular_file());
+ CHECK(entry.is_symlink());
+ CHECK(S_ISREG(entry.mode()));
+ CHECK(entry.size() == 7);
+#ifdef _WIN32
+ CHECK(!(entry.file_attributes() & FILE_ATTRIBUTE_DIRECTORY));
+ CHECK(!(entry.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT));
+ CHECK(entry.reparse_tag() == 0);
+#endif
+ }
+
+ SUBCASE("symlink to none")
+ {
+ DirEntry entry("symlink_to_none");
+ CHECK(entry);
+ CHECK(!entry.exists());
+ CHECK(entry.error_number() == 0);
+ CHECK(entry.path() == "symlink_to_none");
+ CHECK(!entry.is_directory());
+ CHECK(!entry.is_regular_file());
+ CHECK(entry.is_symlink());
+ CHECK(entry.mode() == 0);
+ CHECK(entry.size() == 0);
+#ifdef _WIN32
+ CHECK(!(entry.file_attributes() & FILE_ATTRIBUTE_DIRECTORY));
+ CHECK(!(entry.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT));
+ CHECK(entry.reparse_tag() == 0);
+#endif
+ }
+}
+
+TEST_CASE("Hard links")
+{
+ TestContext test_context;
+
+ util::write_file("a", "");
+
+#ifdef _WIN32
+ REQUIRE(CreateHardLinkA("b", "a", nullptr));
+#else
+ REQUIRE(link("a", "b") == 0);
+#endif
+
+ DirEntry entry_a("a");
+ CHECK(entry_a);
+ CHECK(entry_a.exists());
+ CHECK(entry_a.error_number() == 0);
+ CHECK(!entry_a.is_directory());
+ CHECK(entry_a.is_regular_file());
+ CHECK(!entry_a.is_symlink());
+ CHECK(entry_a.size() == 0);
+
+ DirEntry entry_b("b");
+ CHECK(entry_b.exists());
+ CHECK(entry_b);
+ CHECK(entry_b.error_number() == 0);
+ CHECK(!entry_b.is_directory());
+ CHECK(entry_b.is_regular_file());
+ CHECK(!entry_b.is_symlink());
+ CHECK(entry_b.size() == 0);
+
+ CHECK(entry_a.device() == entry_b.device());
+ CHECK(entry_a.inode() == entry_b.inode());
+ CHECK(entry_a.same_inode_as(entry_b));
+
+ util::write_file("a", "1234567", util::InPlace::yes);
+ entry_b.refresh();
+ CHECK(entry_b.size() == 7);
+}
+
+TEST_CASE("Special" * doctest::skip(running_under_wine()))
+{
+ SUBCASE("tty")
+ {
+#ifdef _WIN32
+ DirEntry entry("\\\\.\\CON");
+#else
+ DirEntry entry("/dev/tty");
+#endif
+ CHECK(entry);
+ CHECK(entry.exists());
+ CHECK(entry.error_number() == 0);
+ CHECK(!entry.is_directory());
+ CHECK(!entry.is_regular_file());
+ CHECK(!entry.is_symlink());
+ CHECK(S_ISCHR(entry.mode()));
+#ifdef _WIN32
+ CHECK(entry.file_attributes() == 0);
+ CHECK(entry.reparse_tag() == 0);
+#endif
+ }
+
+ SUBCASE("null")
+ {
+#ifdef _WIN32
+ DirEntry entry("\\\\.\\NUL");
+#else
+ DirEntry entry("/dev/null");
+#endif
+ CHECK(entry);
+ CHECK(entry.exists());
+ CHECK(entry.error_number() == 0);
+ CHECK(!entry.is_directory());
+ CHECK(!entry.is_regular_file());
+ CHECK(!entry.is_symlink());
+ CHECK(S_ISCHR(entry.mode()));
+#ifdef _WIN32
+ CHECK(entry.file_attributes() == 0);
+ CHECK(entry.reparse_tag() == 0);
+#endif
+ }
+
+ SUBCASE("pipe")
+ {
+#ifdef _WIN32
+ const char pipe_path[] = "\\\\.\\pipe\\InitShutdown"; // Well-known pipe
+#else
+ const char pipe_path[] = "my_pipe";
+ REQUIRE(mkfifo(pipe_path, 0600) == 0);
+#endif
+
+ DirEntry entry(pipe_path);
+ CHECK(entry);
+ CHECK(entry.exists());
+ CHECK(entry.error_number() == 0);
+ CHECK(!entry.is_directory());
+ CHECK(!entry.is_regular_file());
+ CHECK(!entry.is_symlink());
+ CHECK(S_ISFIFO(entry.mode()));
+#ifdef _WIN32
+ CHECK(entry.file_attributes() == 0);
+ CHECK(entry.reparse_tag() == 0);
+#endif
+ }
+
+ SUBCASE("block device")
+ {
+#ifdef _WIN32
+ DirEntry entry("\\\\.\\C:");
+ CHECK(entry);
+ CHECK(entry.exists());
+ CHECK(entry.error_number() == 0);
+ CHECK(!entry.is_directory());
+ CHECK(!entry.is_regular_file());
+ CHECK(!entry.is_symlink());
+ CHECK(S_ISBLK(entry.mode()));
+ CHECK(entry.file_attributes() == 0);
+ CHECK(entry.reparse_tag() == 0);
+#endif
+ }
+}
+
+#ifdef _WIN32
+TEST_CASE("Win32 Readonly File")
+{
+ TestContext test_context;
+
+ util::write_file("file", "");
+
+ DWORD prev_attrs = GetFileAttributesA("file");
+ REQUIRE(prev_attrs != INVALID_FILE_ATTRIBUTES);
+ REQUIRE(SetFileAttributesA("file", prev_attrs | FILE_ATTRIBUTE_READONLY));
+
+ DirEntry entry("file");
+ entry.refresh();
+ REQUIRE(SetFileAttributesA("file", prev_attrs));
+
+ CHECK(entry);
+ CHECK(entry.exists());
+ CHECK(entry.error_number() == 0);
+ CHECK(S_ISREG(entry.mode()));
+ CHECK((entry.mode() & ~S_IFMT) == 0444);
+ CHECK((entry.file_attributes() & FILE_ATTRIBUTE_READONLY));
+ CHECK(!(entry.file_attributes() & FILE_ATTRIBUTE_DIRECTORY));
+ CHECK(!(entry.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT));
+ CHECK(entry.reparse_tag() == 0);
+}
+
+TEST_CASE("Win32 Executable File")
+{
+ TestContext test_context;
+
+ const char* comspec = getenv("COMSPEC");
+ REQUIRE(comspec != nullptr);
+
+ DirEntry entry(comspec);
+ CHECK(entry);
+ CHECK(entry.exists());
+ CHECK(entry.error_number() == 0);
+ CHECK(!entry.is_directory());
+ CHECK(entry.is_regular_file());
+ CHECK(!entry.is_symlink());
+ CHECK(S_ISREG(entry.mode()));
+ CHECK((entry.mode() & ~S_IFMT) == 0777);
+ CHECK(!(entry.file_attributes() & FILE_ATTRIBUTE_DIRECTORY));
+ CHECK(!(entry.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT));
+ CHECK(entry.reparse_tag() == 0);
+}
+
+TEST_CASE("Win32 Pending Delete" * doctest::skip(running_under_wine()))
+{
+ TestContext test_context;
+
+ HANDLE handle =
+ CreateFileA("file",
+ GENERIC_READ | GENERIC_WRITE | DELETE,
+ FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
+ nullptr,
+ CREATE_NEW,
+ FILE_ATTRIBUTE_NORMAL,
+ nullptr);
+ REQUIRE_MESSAGE(handle != INVALID_HANDLE_VALUE, "err=" << GetLastError());
+ Finalizer cleanup([&] { CloseHandle(handle); });
+
+ // Mark file as deleted. This puts it into a "pending delete" state that
+ // will persist until the handle is closed. Until the file is closed, new
+ // handles cannot be created to the file; attempts to do so fail with
+ // ERROR_ACCESS_DENIED/STATUS_DELETE_PENDING. Our stat implementation maps
+ // these to ENOENT.
+ FILE_DISPOSITION_INFO info{};
+ info.DeleteFile = TRUE;
+ REQUIRE_MESSAGE(SetFileInformationByHandle(
+ handle, FileDispositionInfo, &info, sizeof(info)),
+ "err=" << GetLastError());
+
+ DirEntry entry("file");
+ CHECK(!entry);
+ CHECK(!entry.exists());
+ CHECK(entry.error_number() == ENOENT);
+}
+
+// Our Win32 Stat implementation should open files using FILE_READ_ATTRIBUTES,
+// which bypasses sharing restrictions.
+TEST_CASE("Win32 No Sharing")
+{
+ TestContext test_context;
+
+ HANDLE handle = CreateFileA("file",
+ GENERIC_READ | GENERIC_WRITE,
+ 0 /* no sharing */,
+ nullptr,
+ CREATE_NEW,
+ FILE_ATTRIBUTE_NORMAL,
+ nullptr);
+ REQUIRE_MESSAGE(handle != INVALID_HANDLE_VALUE, "err=" << GetLastError());
+ Finalizer cleanup([&] { CloseHandle(handle); });
+
+ // Sanity check we can't open the file for read/write access.
+ REQUIRE(!util::read_file<std::string>("file"));
+
+ DirEntry entry("file");
+ CHECK(entry);
+ CHECK(entry.exists());
+ CHECK(entry.error_number() == 0);
+ CHECK(!entry.is_directory());
+ CHECK(entry.is_regular_file());
+ CHECK(!entry.is_symlink());
+ CHECK(S_ISREG(entry.mode()));
+ CHECK(entry.size() == 0);
+ CHECK(!(entry.file_attributes() & FILE_ATTRIBUTE_DIRECTORY));
+ CHECK(!(entry.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT));
+ CHECK(entry.reparse_tag() == 0);
+}
+
+// Creating a directory junction for test purposes is tricky on Windows.
+// Instead, test a well-known junction that has existed in all Windows versions
+// since Vista. (Not present on Wine.)
+TEST_CASE("Win32 Directory Junction"
+ * doctest::skip(!win32_is_junction(util::expand_environment_variables(
+ "${ALLUSERSPROFILE}\\Application Data"))))
+{
+ TestContext test_context;
+
+ DirEntry entry(
+ util::expand_environment_variables("${ALLUSERSPROFILE}\\Application Data"));
+ CHECK(entry);
+ CHECK(entry.exists());
+ CHECK(entry.error_number() == 0);
+ CHECK(entry.is_directory());
+ CHECK(!entry.is_regular_file());
+ CHECK(entry.is_symlink());
+ CHECK(S_ISDIR(entry.mode()));
+ CHECK((entry.mode() & ~S_IFMT) == 0777);
+ CHECK((entry.file_attributes() & FILE_ATTRIBUTE_DIRECTORY));
+ CHECK(!(entry.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT));
+ CHECK(entry.reparse_tag() == 0);
+}
+#endif
+
+TEST_SUITE_END();
// this program; if not, write to the Free Software Foundation, Inc., 51
// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-#include "../src/Stat.hpp"
#include "TestUtil.hpp"
#include <Util.hpp>
+#include <util/DirEntry.hpp>
#include <util/LockFile.hpp>
#include <util/file.hpp>
#include <util/wincompat.hpp>
using namespace std::chrono_literals;
+using util::DirEntry;
+
TEST_SUITE_BEGIN("LockFile");
using TestUtil::TestContext;
util::LockFile lock("test");
{
CHECK(!lock.acquired());
- CHECK(!Stat::lstat("test.lock"));
- CHECK(!Stat::lstat("test.alive"));
+ CHECK(!DirEntry("test.lock"));
+ CHECK(!DirEntry("test.alive"));
CHECK(lock.acquire());
CHECK(lock.acquired());
- const auto st = Stat::lstat("test.lock");
- CHECK(st);
+ DirEntry entry("test.lock");
+ CHECK(entry);
#ifndef _WIN32
- CHECK(Stat::lstat("test.alive"));
- CHECK(st.is_symlink());
+ CHECK(DirEntry("test.alive"));
+ CHECK(entry.is_symlink());
#else
- CHECK(st.is_regular());
+ CHECK(entry.is_regular_file());
#endif
}
lock.release();
lock.release();
CHECK(!lock.acquired());
- CHECK(!Stat::lstat("test.lock"));
- CHECK(!Stat::lstat("test.alive"));
+ CHECK(!DirEntry("test.lock"));
+ CHECK(!DirEntry("test.alive"));
}
TEST_CASE("Acquire and release long-lived lock file")
lock.make_long_lived(lock_manager);
{
CHECK(!lock.acquired());
- CHECK(!Stat::lstat("test.lock"));
- CHECK(!Stat::lstat("test.alive"));
+ CHECK(!DirEntry("test.lock"));
+ CHECK(!DirEntry("test.alive"));
CHECK(lock.acquire());
CHECK(lock.acquired());
#ifndef _WIN32
- CHECK(Stat::lstat("test.alive"));
+ CHECK(DirEntry("test.alive"));
#endif
- const auto st = Stat::lstat("test.lock");
- CHECK(st);
+ DirEntry entry("test.lock");
+ CHECK(entry);
#ifndef _WIN32
- CHECK(st.is_symlink());
+ CHECK(entry.is_symlink());
#else
- CHECK(st.is_regular());
+ CHECK(entry.is_regular_file());
#endif
}
lock.release();
lock.release();
CHECK(!lock.acquired());
- CHECK(!Stat::lstat("test.lock"));
- CHECK(!Stat::lstat("test.alive"));
+ CHECK(!DirEntry("test.lock"));
+ CHECK(!DirEntry("test.alive"));
}
TEST_CASE("LockFile creates missing directories")
util::LockFile lock("a/b/c/test");
lock.make_long_lived(lock_manager);
CHECK(lock.acquire());
- CHECK(Stat::lstat("a/b/c/test.lock"));
+ CHECK(DirEntry("a/b/c/test.lock"));
}
#ifndef _WIN32
#include "TestUtil.hpp"
#include <Fd.hpp>
-#include <Stat.hpp>
#include <fmtmacros.hpp>
#include <util/Bytes.hpp>
+#include <util/DirEntry.hpp>
#include <util/file.hpp>
#include <util/filesystem.hpp>
#include <util/string.hpp>
namespace fs = util::filesystem;
using TestUtil::TestContext;
+using util::DirEntry;
namespace {
const char filename[] = "test-file";
CHECK(util::fallocate(Fd(creat(filename, S_IRUSR | S_IWUSR)).get(), 10000));
- CHECK(Stat::stat(filename).size() == 10000);
+ CHECK(DirEntry(filename).size() == 10000);
CHECK(
util::fallocate(Fd(open(filename, O_RDWR, S_IRUSR | S_IWUSR)).get(), 5000));
- CHECK(Stat::stat(filename).size() == 10000);
+ CHECK(DirEntry(filename).size() == 10000);
CHECK(util::fallocate(Fd(open(filename, O_RDWR, S_IRUSR | S_IWUSR)).get(),
20000));
- CHECK(Stat::stat(filename).size() == 20000);
+ CHECK(DirEntry(filename).size() == 20000);
}
TEST_CASE("util::likely_size_on_disk")
#endif
}
+TEST_CASE("util::is_dev_null_path")
+{
+ CHECK(!util::is_dev_null_path("dev/null"));
+ CHECK(util::is_dev_null_path("/dev/null"));
+#ifdef _WIN32
+ CHECK(util::is_dev_null_path("nul"));
+ CHECK(util::is_dev_null_path("NUL"));
+#endif
+}
+
TEST_CASE("util::split_path_list")
{
CHECK(util::split_path_list("").empty());