From c86a46287b52d2cbc047f6fb694fe179607b6e4b Mon Sep 17 00:00:00 2001 From: Joel Rosdahl Date: Mon, 31 Jul 2023 19:12:30 +0200 Subject: [PATCH] refactor: Rename and improve Stat class to util::DirEntry --- src/ArgsInfo.hpp | 4 +- src/CMakeLists.txt | 1 - src/Config.cpp | 15 +- src/InodeCache.cpp | 22 +- src/Stat.hpp | 276 ----------- src/Util.cpp | 13 +- src/argprocessing.cpp | 45 +- src/ccache.cpp | 127 +++-- src/core/FileRecompressor.cpp | 31 +- src/core/FileRecompressor.hpp | 10 +- src/core/Manifest.cpp | 12 +- src/core/Result.cpp | 14 +- src/core/ResultExtractor.cpp | 16 +- src/core/ResultRetriever.cpp | 21 +- src/core/common.cpp | 2 +- src/core/mainoptions.cpp | 16 +- src/execute.cpp | 4 +- src/hashutil.cpp | 8 +- src/storage/local/LocalStorage.cpp | 106 +++-- src/storage/local/LocalStorage.hpp | 5 +- src/storage/local/util.cpp | 10 +- src/storage/local/util.hpp | 6 +- src/storage/remote/FileStorage.cpp | 9 +- src/util/CMakeLists.txt | 1 + src/{Stat.cpp => util/DirEntry.cpp} | 105 +++-- src/util/DirEntry.hpp | 278 +++++++++++ src/util/LockFile.cpp | 6 +- src/util/file.cpp | 31 +- src/util/filesystem.cpp | 9 +- src/util/path.cpp | 8 +- src/util/path.hpp | 15 + unittest/CMakeLists.txt | 4 +- unittest/test_AtomicFile.cpp | 4 +- unittest/test_InodeCache.cpp | 4 +- unittest/test_Stat.cpp | 704 ---------------------------- unittest/test_Util.cpp | 1 - unittest/test_core_common.cpp | 5 +- unittest/test_util_DirEntry.cpp | 671 ++++++++++++++++++++++++++ unittest/test_util_LockFile.cpp | 42 +- unittest/test_util_file.cpp | 9 +- unittest/test_util_path.cpp | 10 + 41 files changed, 1350 insertions(+), 1330 deletions(-) delete mode 100644 src/Stat.hpp rename src/{Stat.cpp => util/DirEntry.cpp} (80%) create mode 100644 src/util/DirEntry.hpp delete mode 100644 unittest/test_Stat.cpp create mode 100644 unittest/test_util_DirEntry.cpp diff --git a/src/ArgsInfo.hpp b/src/ArgsInfo.hpp index 60cadce20..e577929a9 100644 --- a/src/ArgsInfo.hpp +++ b/src/ArgsInfo.hpp @@ -1,4 +1,4 @@ -// 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. // @@ -64,7 +64,7 @@ struct ArgsInfo // 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). diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 55cfaa411..a2dc35d60 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -9,7 +9,6 @@ set( Hash.cpp Logging.cpp ProgressBar.cpp - Stat.cpp TemporaryFile.cpp ThreadPool.cpp Util.cpp diff --git a/src/Config.cpp b/src/Config.cpp index a1769f58b..16dfbd5f3 100644 --- a/src/Config.cpp +++ b/src/Config.cpp @@ -23,11 +23,11 @@ #include "Util.hpp" #include "assertions.hpp" -#include #include #include #include #include +#include #include #include #include @@ -67,6 +67,8 @@ const char k_sysconfdir[4096 + 1] = SYSCONFDIR; namespace fs = util::filesystem; +using util::DirEntry; + namespace { enum class ConfigItem { @@ -551,7 +553,7 @@ Config::read(const std::vector& cmdline_config_settings) 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"); @@ -595,11 +597,11 @@ Config::read(const std::vector& cmdline_config_settings) 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) { @@ -914,8 +916,7 @@ Config::set_value_in_file(const std::string& path, 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( util::write_file(resolved_path, ""), @@ -1197,7 +1198,7 @@ Config::default_temporary_dir() const 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; diff --git a/src/InodeCache.cpp b/src/InodeCache.cpp index d0c0856cb..aabc583e8 100644 --- a/src/InodeCache.cpp +++ b/src/InodeCache.cpp @@ -22,11 +22,11 @@ #include "Finalizer.hpp" #include "Hash.hpp" #include "Logging.hpp" -#include "Stat.hpp" #include "TemporaryFile.hpp" #include "Util.hpp" #include "fmtmacros.hpp" +#include #include #include @@ -280,15 +280,15 @@ InodeCache::hash_inode(const std::string& path, 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; } @@ -296,12 +296,12 @@ InodeCache::hash_inode(const std::string& path, 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(reinterpret_cast(&key), diff --git a/src/Stat.hpp b/src/Stat.hpp deleted file mode 100644 index 3a4342e99..000000000 --- a/src/Stat.hpp +++ /dev/null @@ -1,276 +0,0 @@ -// 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 -#include -#include - -#include -#include - -#include -#include -#include - -#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 diff --git a/src/Util.cpp b/src/Util.cpp index b93983811..bbebbc621 100644 --- a/src/Util.cpp +++ b/src/Util.cpp @@ -27,9 +27,9 @@ #include #include -#include #include #include +#include #include #include #include @@ -45,6 +45,8 @@ namespace fs = util::filesystem; +using util::DirEntry; + namespace Util { std::string_view @@ -242,9 +244,10 @@ make_relative_path(const std::string& base_dir, std::vector 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); @@ -271,7 +274,7 @@ make_relative_path(const std::string& base_dir, 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; } } @@ -370,7 +373,7 @@ normalize_concrete_absolute_path(const std::string& path) { 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; } diff --git a/src/argprocessing.cpp b/src/argprocessing.cpp index 9f7dfc6cd..a095b7920 100644 --- a/src/argprocessing.cpp +++ b/src/argprocessing.cpp @@ -27,6 +27,7 @@ #include #include +#include #include #include @@ -37,6 +38,7 @@ #include using core::Statistic; +using util::DirEntry; namespace { @@ -142,7 +144,7 @@ detect_pch(const std::string& option, 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; } @@ -152,7 +154,7 @@ detect_pch(const std::string& option, 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); @@ -165,14 +167,15 @@ detect_pch(const std::string& option, // 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; } @@ -1079,7 +1082,7 @@ process_option_arg(const Context& ctx, 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]); @@ -1113,9 +1116,8 @@ process_arg(const Context& ctx, // // 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; @@ -1209,12 +1211,9 @@ process_args(Context& ctx) 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; } } @@ -1352,7 +1351,7 @@ process_args(Context& ctx) } 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; @@ -1362,18 +1361,20 @@ process_args(Context& ctx) } } - // 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; } diff --git a/src/ccache.cpp b/src/ccache.cpp index f66e382d5..904dca388 100644 --- a/src/ccache.cpp +++ b/src/ccache.cpp @@ -85,6 +85,7 @@ 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 @@ -284,20 +285,20 @@ guess_compiler(std::string_view path) 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; } @@ -352,15 +353,15 @@ do_remember_include_file(Context& ctx, } #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; @@ -373,7 +374,7 @@ do_remember_include_file(Context& ctx, } 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. @@ -402,7 +403,7 @@ do_remember_include_file(Context& ctx, // 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); @@ -860,14 +861,14 @@ update_manifest(Context& ctx, 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) { @@ -899,11 +900,11 @@ find_coverage_file(const Context& ctx) 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"); @@ -923,7 +924,6 @@ find_coverage_file(const Context& ctx) [[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) { @@ -937,7 +937,7 @@ write_result(Context& ctx, 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); @@ -978,7 +978,7 @@ write_result(Context& ctx, 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); @@ -1064,7 +1064,8 @@ to_cache(Context& ctx, 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 // . @@ -1171,24 +1172,20 @@ to_cache(Context& ctx, 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); } @@ -1196,7 +1193,7 @@ to_cache(Context& ctx, 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"); @@ -1300,7 +1297,7 @@ get_result_key_from_cpp(Context& ctx, Args& args, Hash& hash) static tl::expected hash_compiler(const Context& ctx, Hash& hash, - const Stat& st, + const DirEntry& dir_entry, const std::string& path, bool allow_command) { @@ -1308,8 +1305,8 @@ hash_compiler(const Context& ctx, // 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]); @@ -1335,7 +1332,7 @@ hash_compiler(const Context& ctx, static tl::expected hash_nvcc_host_compiler(const Context& ctx, Hash& hash, - const Stat* ccbin_st = nullptr, + const DirEntry* ccbin_st = nullptr, const std::string& ccbin = {}) { // From : @@ -1361,15 +1358,15 @@ hash_nvcc_host_compiler(const Context& ctx, 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)); } } } @@ -1405,13 +1402,13 @@ hash_common_info(const Context& ctx, 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. @@ -1744,31 +1741,31 @@ hash_argument(const Context& ctx, } 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 {}; } @@ -1776,11 +1773,11 @@ hash_argument(const Context& ctx, 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 {}; } @@ -1870,8 +1867,7 @@ hash_profile_data_file(const Context& ctx, Hash& hash) 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)) { @@ -1994,13 +1990,6 @@ calculate_result_and_manifest_key(Context& ctx, 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)); } @@ -2460,14 +2449,12 @@ do_cache_compilation(Context& ctx) 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()) { diff --git a/src/core/FileRecompressor.cpp b/src/core/FileRecompressor.cpp index 11e1f4799..4e2f563a6 100644 --- a/src/core/FileRecompressor.cpp +++ b/src/core/FileRecompressor.cpp @@ -25,25 +25,27 @@ #include #include +using util::DirEntry; + namespace core { -Stat -FileRecompressor::recompress(const Stat& stat, +DirEntry +FileRecompressor::recompress(const DirEntry& dir_entry, std::optional 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 new_stat; + std::optional new_dir_entry; if (header.compression_level != wanted_level) { const auto cache_file_data = util::value_or_throw( - util::read_file(stat.path()), - FMT("Failed to read {}: ", stat.path())); + util::read_file(dir_entry.path().string()), + FMT("Failed to read {}: ", dir_entry.path().string())); core::CacheEntry cache_entry(cache_file_data); cache_entry.verify_checksum(); @@ -52,23 +54,26 @@ FileRecompressor::recompress(const Stat& stat, 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 diff --git a/src/core/FileRecompressor.hpp b/src/core/FileRecompressor.hpp index 84ce28246..0c341a094 100644 --- a/src/core/FileRecompressor.hpp +++ b/src/core/FileRecompressor.hpp @@ -1,4 +1,4 @@ -// 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. // @@ -18,7 +18,7 @@ #pragma once -#include +#include #include #include @@ -36,9 +36,9 @@ public: FileRecompressor() = default; // Returns stat after recompression. - Stat recompress(const Stat& stat, - std::optional level, - KeepAtime keep_atime); + util::DirEntry recompress(const util::DirEntry& dir_entry, + std::optional level, + KeepAtime keep_atime); uint64_t content_size() const; uint64_t old_size() const; diff --git a/src/core/Manifest.cpp b/src/core/Manifest.cpp index 784075665..e9cfadd80 100644 --- a/src/core/Manifest.cpp +++ b/src/core/Manifest.cpp @@ -354,17 +354,17 @@ Manifest::result_matches( 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; diff --git a/src/core/Result.cpp b/src/core/Result.cpp index 8d16808b0..31b3513c8 100644 --- a/src/core/Result.cpp +++ b/src/core/Result.cpp @@ -1,4 +1,4 @@ -// 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. // @@ -23,7 +23,6 @@ #include "Fd.hpp" #include "File.hpp" #include "Logging.hpp" -#include "Stat.hpp" #include "Util.hpp" #include @@ -33,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -68,6 +68,8 @@ // ::= 1 (uint8_t) // ::= uint64_t +using util::DirEntry; + namespace { // File data stored inside the result file. @@ -247,11 +249,11 @@ Serializer::add_file(const FileType file_type, const std::string& path) { 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; @@ -285,7 +287,7 @@ Serializer::serialize(util::Bytes& output) is_file_entry && should_store_raw_file(m_config, entry.file_type); const uint64_t file_size = is_file_entry - ? Stat::stat(std::get(entry.data), Stat::LogOnError::yes) + ? DirEntry(std::get(entry.data), DirEntry::LogOnError::yes) .size() : std::get>(entry.data).size(); diff --git a/src/core/ResultExtractor.cpp b/src/core/ResultExtractor.cpp index 42c5403a9..38347959e 100644 --- a/src/core/ResultExtractor.cpp +++ b/src/core/ResultExtractor.cpp @@ -21,10 +21,10 @@ #include "Util.hpp" #include "fmtmacros.hpp" -#include #include #include #include +#include #include #include #include @@ -35,6 +35,8 @@ #include +using util::DirEntry; + namespace core { ResultExtractor::ResultExtractor( @@ -73,15 +75,15 @@ ResultExtractor::on_raw_file(uint8_t file_number, 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)); } diff --git a/src/core/ResultRetriever.cpp b/src/core/ResultRetriever.cpp index 47e22e7f5..83993331b 100644 --- a/src/core/ResultRetriever.cpp +++ b/src/core/ResultRetriever.cpp @@ -23,14 +23,15 @@ #include "Logging.hpp" #include -#include #include #include #include #include #include +#include #include #include +#include #include #include @@ -42,6 +43,8 @@ # include #endif +using util::DirEntry; + namespace core { using Result::FileType; @@ -74,8 +77,8 @@ ResultRetriever::on_embedded_file(uint8_t file_number, 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) { @@ -104,16 +107,16 @@ ResultRetriever::on_raw_file(uint8_t file_number, } 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)); } @@ -178,7 +181,7 @@ ResultRetriever::get_dest_path(FileType file_type) const 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; diff --git a/src/core/common.cpp b/src/core/common.cpp index 5fd0ba59a..27a2fbbc6 100644 --- a/src/core/common.cpp +++ b/src/core/common.cpp @@ -96,7 +96,7 @@ rewrite_stderr_to_absolute_paths(std::string_view text) 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()); diff --git a/src/core/mainoptions.cpp b/src/core/mainoptions.cpp index 0c28a7acd..2233962b0 100644 --- a/src/core/mainoptions.cpp +++ b/src/core/mainoptions.cpp @@ -73,6 +73,8 @@ extern "C" { } #endif +using util::DirEntry; + namespace core { constexpr const char VERSION_TEXT[] = @@ -297,7 +299,7 @@ trim_dir(const std::string& dir, std::optional> recompress_level, uint32_t recompress_threads) { - std::vector files; + std::vector files; uint64_t initial_size = 0; util::throw_on_error(util::traverse_directory( @@ -305,18 +307,18 @@ trim_dir(const std::string& dir, 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) { @@ -365,7 +367,7 @@ trim_dir(const std::string& dir, 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(); } @@ -719,7 +721,7 @@ process_main_options(int argc, const char* const* argv) } 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)); diff --git a/src/execute.cpp b/src/execute.cpp index 36743e28c..1c7164692 100644 --- a/src/execute.cpp +++ b/src/execute.cpp @@ -24,7 +24,6 @@ #include "Fd.hpp" #include "Logging.hpp" #include "SignalHandler.hpp" -#include "Stat.hpp" #include "TemporaryFile.hpp" #include "Util.hpp" #include "Win32Util.hpp" @@ -32,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -393,7 +393,7 @@ find_executable_in_path(const std::string& name, // 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 diff --git a/src/hashutil.cpp b/src/hashutil.cpp index e75b27576..0cd4265c7 100644 --- a/src/hashutil.cpp +++ b/src/hashutil.cpp @@ -22,13 +22,13 @@ #include "Config.hpp" #include "Context.hpp" #include "Logging.hpp" -#include "Stat.hpp" #include "Win32Util.hpp" #include "execute.hpp" #include "macroskip.hpp" #include #include +#include #include #include #include @@ -291,13 +291,13 @@ hash_source_code_file(const Context& ctx, 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; diff --git a/src/storage/local/LocalStorage.cpp b/src/storage/local/LocalStorage.cpp index 12a4052eb..e97ac996a 100644 --- a/src/storage/local/LocalStorage.cpp +++ b/src/storage/local/LocalStorage.cpp @@ -89,6 +89,7 @@ namespace fs = util::filesystem; using core::Statistic; using core::StatisticsCounters; +using util::DirEntry; namespace storage::local { @@ -161,12 +162,12 @@ struct Level1Counters } // 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(new_stat.size_on_disk()) - - static_cast(old_stat.size_on_disk())) + return (static_cast(new_dir_entry.size_on_disk()) + - static_cast(old_dir_entry.size_on_disk())) / 1024; } @@ -215,21 +216,21 @@ calculate_wanted_cache_level(const uint64_t files_in_level_1) } 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; } } @@ -324,22 +325,23 @@ clean_dir( ++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(); @@ -375,7 +377,7 @@ clean_dir( if (namespace_) { try { - core::CacheEntry::Header header(file.path()); + core::CacheEntry::Header header(file.path().string()); if (header.namespace_ != *namespace_) { continue; } @@ -387,19 +389,16 @@ clean_dir( // 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; } @@ -416,13 +415,14 @@ clean_dir( } 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; @@ -464,7 +464,7 @@ LocalStorage::get(const Hash::Digest& key, const core::CacheEntryType type) std::optional 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(cache_file.path); if (value) { LOG("Retrieved {} from local storage ({})", @@ -500,7 +500,7 @@ LocalStorage::put(const Hash::Digest& key, 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; @@ -533,13 +533,14 @@ LocalStorage::put(const Hash::Digest& key, 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); @@ -564,7 +565,7 @@ LocalStorage::remove(const Hash::Digest& key, const core::CacheEntryType type) 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; } @@ -583,7 +584,7 @@ LocalStorage::remove(const Hash::Digest& key, const core::CacheEntryType type) util::format_digest(key), cache_file.path); increment_level_2_counters( - key, -1, -static_cast(cache_file.stat.size_on_disk() / 1024)); + key, -1, -static_cast(cache_file.dir_entry.size_on_disk() / 1024)); } std::string @@ -620,7 +621,8 @@ LocalStorage::put_raw_files( 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); @@ -631,11 +633,11 @@ LocalStorage::put_raw_files( 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)); } } @@ -741,7 +743,7 @@ LocalStorage::get_all_statistics() const 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); @@ -786,7 +788,7 @@ LocalStorage::wipe_all(const ProgressReceiver& progress_receiver) 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()); } @@ -818,7 +820,7 @@ LocalStorage::get_compression_statistics( 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&) { @@ -872,10 +874,10 @@ LocalStorage::recompress(const std::optional level, 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, @@ -891,7 +893,7 @@ LocalStorage::recompress(const std::optional level, 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(); } @@ -1014,15 +1016,15 @@ LocalStorage::look_up_cache_file(const Hash::Digest& key, 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 @@ -1409,9 +1411,9 @@ LocalStorage::clean_internal_tempdir() 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; } @@ -1424,8 +1426,8 @@ LocalStorage::clean_internal_tempdir() 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); } }) diff --git a/src/storage/local/LocalStorage.hpp b/src/storage/local/LocalStorage.hpp index 8b529b916..0fe89e725 100644 --- a/src/storage/local/LocalStorage.hpp +++ b/src/storage/local/LocalStorage.hpp @@ -32,6 +32,7 @@ #include #include +#include #include #include #include @@ -54,7 +55,7 @@ struct CompressionStatistics 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 { @@ -138,7 +139,7 @@ private: struct LookUpCacheFileResult { std::string path; - Stat stat; + util::DirEntry dir_entry; uint8_t level; }; diff --git a/src/storage/local/util.cpp b/src/storage/local/util.cpp index 6649b1f11..871f8361e 100644 --- a/src/storage/local/util.cpp +++ b/src/storage/local/util.cpp @@ -25,6 +25,8 @@ #include #include +using util::DirEntry; + namespace storage::local { void @@ -62,12 +64,12 @@ for_each_level_1_and_2_stats_file( } } -std::vector +std::vector get_cache_dir_files(const std::string& dir) { - std::vector files; + std::vector files; - if (!Stat::stat(dir)) { + if (!DirEntry(dir).is_directory()) { return files; } util::throw_on_error( @@ -79,7 +81,7 @@ get_cache_dir_files(const std::string& dir) } if (!is_dir) { - files.emplace_back(Stat::lstat(path)); + files.emplace_back(path); } })); diff --git a/src/storage/local/util.hpp b/src/storage/local/util.hpp index ed2ada401..d48af32f8 100644 --- a/src/storage/local/util.hpp +++ b/src/storage/local/util.hpp @@ -1,4 +1,4 @@ -// 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. // @@ -18,7 +18,7 @@ #pragma once -#include +#include #include #include @@ -50,6 +50,6 @@ void for_each_level_1_and_2_stats_file( // - CACHEDIR.TAG // - stats // - .nfs* (temporary NFS files that may be left for open but deleted files). -std::vector get_cache_dir_files(const std::string& dir); +std::vector get_cache_dir_files(const std::string& dir); } // namespace storage::local diff --git a/src/storage/remote/FileStorage.cpp b/src/storage/remote/FileStorage.cpp index da68921b5..840c63001 100644 --- a/src/storage/remote/FileStorage.cpp +++ b/src/storage/remote/FileStorage.cpp @@ -20,12 +20,12 @@ #include #include -#include #include #include #include #include #include +#include #include #include #include @@ -38,6 +38,8 @@ namespace fs = util::filesystem; +using util::DirEntry; + namespace storage::remote { namespace { @@ -115,9 +117,8 @@ tl::expected, RemoteStorage::Backend::Failure> 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; } @@ -142,7 +143,7 @@ FileStorageBackend::put(const Hash::Digest& key, { 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; } diff --git a/src/util/CMakeLists.txt b/src/util/CMakeLists.txt index 38e0b290f..e9d4042fe 100644 --- a/src/util/CMakeLists.txt +++ b/src/util/CMakeLists.txt @@ -1,6 +1,7 @@ set( sources Bytes.cpp + DirEntry.cpp LockFile.cpp LongLivedLockFileManager.cpp TextTable.cpp diff --git a/src/Stat.cpp b/src/util/DirEntry.cpp similarity index 80% rename from src/Stat.cpp rename to src/util/DirEntry.cpp index 429302f31..80ed955ef 100644 --- a/src/Stat.cpp +++ b/src/util/DirEntry.cpp @@ -16,12 +16,11 @@ // 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 +#include +#include #include #include #include @@ -68,7 +67,7 @@ void 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(file_info.nFileIndexHigh) << 32) @@ -108,7 +107,9 @@ win32_file_information_to_stat(const BY_HANDLE_FILE_INFORMATION& file_info, } 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 = {}; @@ -178,9 +179,9 @@ win32_stat_impl(const char* path, bool traverse_links, Stat::stat_t* 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; } @@ -189,9 +190,9 @@ win32_stat(const char* path, Stat::stat_t* st) } 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; } @@ -199,52 +200,58 @@ win32_lstat(const char* path, Stat::stat_t* st) 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 diff --git a/src/util/DirEntry.hpp b/src/util/DirEntry.hpp new file mode 100644 index 000000000..9b4b81c0b --- /dev/null +++ b/src/util/DirEntry.hpp @@ -0,0 +1,278 @@ +// 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 +#include +#include + +#include +#include + +#include +#include +#include + +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 diff --git a/src/util/LockFile.cpp b/src/util/LockFile.cpp index e6832974c..f08340cdc 100644 --- a/src/util/LockFile.cpp +++ b/src/util/LockFile.cpp @@ -23,9 +23,9 @@ #include "Win32Util.hpp" #include "fmtmacros.hpp" -#include #include #include +#include #include #include #include @@ -346,8 +346,8 @@ LockFile::do_acquire(const bool blocking) std::optional 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; } diff --git a/src/util/file.cpp b/src/util/file.cpp index 604ec28a1..107181c59 100644 --- a/src/util/file.cpp +++ b/src/util/file.cpp @@ -21,11 +21,11 @@ #include #include #include -#include #include #include #include #include +#include #include #include #include @@ -124,8 +124,7 @@ create_cachedir_tag(const std::string& dir) "#\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); @@ -233,11 +232,11 @@ tl::expected 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 @@ -499,15 +498,17 @@ traverse_directory(const std::string& directory, } 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); @@ -529,12 +530,12 @@ traverse_directory(const std::string& directory, // 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 { diff --git a/src/util/filesystem.cpp b/src/util/filesystem.cpp index f5cd1ec59..755dc318f 100644 --- a/src/util/filesystem.cpp +++ b/src/util/filesystem.cpp @@ -20,6 +20,10 @@ #include +#ifdef _WIN32 +# include +#endif + namespace util::filesystem { tl::expected @@ -37,9 +41,8 @@ rename(const std::filesystem::path& old_p, const std::filesystem::path& new_p) 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 {}; diff --git a/src/util/path.cpp b/src/util/path.cpp index ccb0bfbb8..019d39656 100644 --- a/src/util/path.cpp +++ b/src/util/path.cpp @@ -18,9 +18,9 @@ #include "path.hpp" -#include #include #include +#include #include #include @@ -61,9 +61,9 @@ apparent_cwd(const std::string& actual_cwd) 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 diff --git a/src/util/path.hpp b/src/util/path.hpp index 1dc242980..ccd92a2c0 100644 --- a/src/util/path.hpp +++ b/src/util/path.hpp @@ -18,6 +18,8 @@ #pragma once +#include + #include #include #include @@ -41,6 +43,9 @@ const char* get_dev_null_path(); // 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); @@ -64,6 +69,16 @@ std::string to_absolute_path_no_drive(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) { diff --git a/unittest/CMakeLists.txt b/unittest/CMakeLists.txt index ddf574a5d..d455ee8ba 100644 --- a/unittest/CMakeLists.txt +++ b/unittest/CMakeLists.txt @@ -7,7 +7,6 @@ set( test_Config.cpp test_Depfile.cpp test_Hash.cpp - test_Stat.cpp test_Util.cpp test_argprocessing.cpp test_ccache.cpp @@ -23,6 +22,7 @@ set( 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 @@ -54,7 +54,7 @@ add_executable(unittest ${source_files}) 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( diff --git a/unittest/test_AtomicFile.cpp b/unittest/test_AtomicFile.cpp index bd0c9cc33..25c0f6a20 100644 --- a/unittest/test_AtomicFile.cpp +++ b/unittest/test_AtomicFile.cpp @@ -19,7 +19,7 @@ #include "../src/AtomicFile.hpp" #include "TestUtil.hpp" -#include +#include #include #include "third_party/doctest.h" @@ -51,7 +51,7 @@ TEST_CASE("Not committing") AtomicFile atomic_file("test", AtomicFile::Mode::text); atomic_file.write("hello"); } - CHECK(!Stat::stat("test")); + CHECK(!util::DirEntry("test")); } TEST_SUITE_END(); diff --git a/unittest/test_InodeCache.cpp b/unittest/test_InodeCache.cpp index 5c2fc3ecc..c5bbf796e 100644 --- a/unittest/test_InodeCache.cpp +++ b/unittest/test_InodeCache.cpp @@ -162,9 +162,9 @@ TEST_CASE("Drop file") 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()); } diff --git a/unittest/test_Stat.cpp b/unittest/test_Stat.cpp deleted file mode 100644 index 18b04799f..000000000 --- a/unittest/test_Stat.cpp +++ /dev/null @@ -1,704 +0,0 @@ -// 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 -#include -#include -#include - -#include "third_party/doctest.h" - -#ifdef HAVE_UNISTD_H -# include -#endif - -#ifdef _WIN32 -# include -#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(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("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(); diff --git a/unittest/test_Util.cpp b/unittest/test_Util.cpp index b254c918c..52f182ad0 100644 --- a/unittest/test_Util.cpp +++ b/unittest/test_Util.cpp @@ -22,7 +22,6 @@ #include "../src/fmtmacros.hpp" #include "TestUtil.hpp" -#include #include #include #include diff --git a/unittest/test_core_common.cpp b/unittest/test_core_common.cpp index 9c8e27ec2..7a972d24b 100644 --- a/unittest/test_core_common.cpp +++ b/unittest/test_core_common.cpp @@ -18,13 +18,14 @@ #include "TestUtil.hpp" -#include #include +#include #include #include using TestUtil::TestContext; +using util::DirEntry; TEST_SUITE_BEGIN("core"); @@ -35,7 +36,7 @@ TEST_CASE("core::ensure_dir_exists") 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( diff --git a/unittest/test_util_DirEntry.cpp b/unittest/test_util_DirEntry.cpp new file mode 100644 index 000000000..b5bb5dd94 --- /dev/null +++ b/unittest/test_util_DirEntry.cpp @@ -0,0 +1,671 @@ +// 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 +#include +#include +#include +#include + +#include + +#ifdef HAVE_UNISTD_H +# include +#endif + +#ifdef _WIN32 +# include +#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(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("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(); diff --git a/unittest/test_util_LockFile.cpp b/unittest/test_util_LockFile.cpp index c57aa5d62..d93fc5d87 100644 --- a/unittest/test_util_LockFile.cpp +++ b/unittest/test_util_LockFile.cpp @@ -16,10 +16,10 @@ // 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 +#include #include #include #include @@ -32,6 +32,8 @@ using namespace std::chrono_literals; +using util::DirEntry; + TEST_SUITE_BEGIN("LockFile"); using TestUtil::TestContext; @@ -43,26 +45,26 @@ TEST_CASE("Acquire and release short-lived lock file") 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") @@ -74,28 +76,28 @@ 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") @@ -106,7 +108,7 @@ 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 diff --git a/unittest/test_util_file.cpp b/unittest/test_util_file.cpp index 9e85441c7..798ac88f1 100644 --- a/unittest/test_util_file.cpp +++ b/unittest/test_util_file.cpp @@ -19,9 +19,9 @@ #include "TestUtil.hpp" #include -#include #include #include +#include #include #include #include @@ -38,6 +38,7 @@ namespace fs = util::filesystem; using TestUtil::TestContext; +using util::DirEntry; namespace { @@ -60,15 +61,15 @@ TEST_CASE("util::fallocate") 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") diff --git a/unittest/test_util_path.cpp b/unittest/test_util_path.cpp index 307a03db3..e7643019b 100644 --- a/unittest/test_util_path.cpp +++ b/unittest/test_util_path.cpp @@ -54,6 +54,16 @@ TEST_CASE("util::is_absolute_path") #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()); -- 2.47.2