]> git.ipfire.org Git - thirdparty/ccache.git/commitdiff
refactor: Rename and improve Stat class to util::DirEntry
authorJoel Rosdahl <joel@rosdahl.net>
Mon, 31 Jul 2023 17:12:30 +0000 (19:12 +0200)
committerJoel Rosdahl <joel@rosdahl.net>
Wed, 2 Aug 2023 14:01:03 +0000 (16:01 +0200)
41 files changed:
src/ArgsInfo.hpp
src/CMakeLists.txt
src/Config.cpp
src/InodeCache.cpp
src/Stat.hpp [deleted file]
src/Util.cpp
src/argprocessing.cpp
src/ccache.cpp
src/core/FileRecompressor.cpp
src/core/FileRecompressor.hpp
src/core/Manifest.cpp
src/core/Result.cpp
src/core/ResultExtractor.cpp
src/core/ResultRetriever.cpp
src/core/common.cpp
src/core/mainoptions.cpp
src/execute.cpp
src/hashutil.cpp
src/storage/local/LocalStorage.cpp
src/storage/local/LocalStorage.hpp
src/storage/local/util.cpp
src/storage/local/util.hpp
src/storage/remote/FileStorage.cpp
src/util/CMakeLists.txt
src/util/DirEntry.cpp [moved from src/Stat.cpp with 80% similarity]
src/util/DirEntry.hpp [new file with mode: 0644]
src/util/LockFile.cpp
src/util/file.cpp
src/util/filesystem.cpp
src/util/path.cpp
src/util/path.hpp
unittest/CMakeLists.txt
unittest/test_AtomicFile.cpp
unittest/test_InodeCache.cpp
unittest/test_Stat.cpp [deleted file]
unittest/test_Util.cpp
unittest/test_core_common.cpp
unittest/test_util_DirEntry.cpp [new file with mode: 0644]
unittest/test_util_LockFile.cpp
unittest/test_util_file.cpp
unittest/test_util_path.cpp

index 60cadce201aaae987cd93cb7547e54e2ca78c325..e577929a9622842f47b3f5925944830a6c4eca2b 100644 (file)
@@ -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).
index 55cfaa4113cfffdcb03138789d2d61d031ab660d..a2dc35d60631c8fc5875c25114c29f3ccb1f79ce 100644 (file)
@@ -9,7 +9,6 @@ set(
   Hash.cpp
   Logging.cpp
   ProgressBar.cpp
-  Stat.cpp
   TemporaryFile.cpp
   ThreadPool.cpp
   Util.cpp
index a1769f58b49eff5df9a4c3906145b017aaace583..16dfbd5f3c8944934ded5e1c5596b4f32a1c831e 100644 (file)
 #include "Util.hpp"
 #include "assertions.hpp"
 
-#include <Stat.hpp>
 #include <core/common.hpp>
 #include <core/exceptions.hpp>
 #include <core/types.hpp>
 #include <fmtmacros.hpp>
+#include <util/DirEntry.hpp>
 #include <util/Tokenizer.hpp>
 #include <util/UmaskScope.hpp>
 #include <util/environment.hpp>
@@ -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<std::string>& 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<std::string>& 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<core::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;
index d0c0856cb00803bf028e11105dc798ad5b3066c2..aabc583e86cfae5b16ffe776a753057c3e256611 100644 (file)
 #include "Finalizer.hpp"
 #include "Hash.hpp"
 #include "Logging.hpp"
-#include "Stat.hpp"
 #include "TemporaryFile.hpp"
 #include "Util.hpp"
 #include "fmtmacros.hpp"
 
+#include <util/DirEntry.hpp>
 #include <util/conversion.hpp>
 #include <util/file.hpp>
 
@@ -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<const uint8_t>(reinterpret_cast<const uint8_t*>(&key),
diff --git a/src/Stat.hpp b/src/Stat.hpp
deleted file mode 100644 (file)
index 3a4342e..0000000
+++ /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 <util/TimePoint.hpp>
-#include <util/file.hpp>
-#include <util/wincompat.hpp>
-
-#include <sys/stat.h>
-#include <sys/types.h>
-
-#include <cstdint>
-#include <ctime>
-#include <string>
-
-#ifdef _WIN32
-#  ifndef S_IFIFO
-#    define S_IFIFO 0x1000
-#  endif
-#  ifndef S_IFBLK
-#    define S_IFBLK 0x6000
-#  endif
-#  ifndef S_IFLNK
-#    define S_IFLNK 0xA000
-#  endif
-#  ifndef S_ISREG
-#    define S_ISREG(m) (((m)&S_IFMT) == S_IFREG)
-#  endif
-#  ifndef S_ISDIR
-#    define S_ISDIR(m) (((m)&S_IFMT) == S_IFDIR)
-#  endif
-#  ifndef S_ISFIFO
-#    define S_ISFIFO(m) (((m)&S_IFMT) == S_IFIFO)
-#  endif
-#  ifndef S_ISCHR
-#    define S_ISCHR(m) (((m)&S_IFMT) == S_IFCHR)
-#  endif
-#  ifndef S_ISLNK
-#    define S_ISLNK(m) (((m)&S_IFMT) == S_IFLNK)
-#  endif
-#  ifndef S_ISBLK
-#    define S_ISBLK(m) (((m)&S_IFMT) == S_IFBLK)
-#  endif
-#endif
-
-class Stat
-{
-public:
-  enum class LogOnError { no, yes };
-
-#if defined(_WIN32)
-  struct stat_t
-  {
-    uint64_t st_dev;
-    uint64_t st_ino;
-    uint16_t st_mode;
-    uint16_t st_nlink;
-    uint64_t st_size;
-    struct timespec st_atim;
-    struct timespec st_mtim;
-    struct timespec st_ctim;
-    uint32_t st_file_attributes;
-    uint32_t st_reparse_tag;
-  };
-#else
-  using stat_t = struct stat;
-#endif
-
-  using dev_t = decltype(stat_t{}.st_dev);
-  using ino_t = decltype(stat_t{}.st_ino);
-
-  // Create an empty stat result. operator bool() will return false,
-  // error_number() will return -1 and other accessors will return false or 0.
-  Stat();
-
-  // Run stat(2) on `path`.
-  static Stat stat(const std::string& path,
-                   LogOnError log_on_error = LogOnError::no);
-
-  // Run lstat(2) on `path` if available, otherwise stat(2).
-  static Stat lstat(const std::string& path,
-                    LogOnError log_on_error = LogOnError::no);
-
-  // Return true if the file could be (l)stat-ed (i.e., the file exists),
-  // otherwise false.
-  operator bool() const;
-
-  // Return the path that this stat result refers to.
-  const std::string& path() const;
-
-  // Return whether this object refers to the same device and i-node as `other`
-  // does.
-  bool same_inode_as(const Stat& other) const;
-
-  // Return errno from the (l)stat call (0 if successful).
-  int error_number() const;
-
-  dev_t device() const;
-  ino_t inode() const;
-  mode_t mode() const;
-  util::TimePoint atime() const;
-  util::TimePoint ctime() const;
-  util::TimePoint mtime() const;
-  uint64_t size() const;
-
-  uint64_t size_on_disk() const;
-
-  bool is_directory() const;
-  bool is_regular() const;
-  bool is_symlink() const;
-
-#ifdef _WIN32
-  uint32_t file_attributes() const;
-  uint32_t reparse_tag() const;
-#endif
-
-protected:
-  using StatFunction = int (*)(const char*, stat_t*);
-
-  Stat(StatFunction stat_function,
-       const std::string& path,
-       LogOnError log_on_error);
-
-private:
-  std::string m_path;
-  stat_t m_stat;
-  int m_errno;
-
-  bool operator==(const Stat&) const;
-  bool operator!=(const Stat&) const;
-};
-
-inline Stat::Stat() : m_stat{}, m_errno(-1)
-{
-}
-
-inline Stat::operator bool() const
-{
-  return m_errno == 0;
-}
-
-inline bool
-Stat::same_inode_as(const Stat& other) const
-{
-  return m_errno == 0 && device() == other.device() && inode() == other.inode();
-}
-
-inline const std::string&
-Stat::path() const
-{
-  return m_path;
-}
-
-inline int
-Stat::error_number() const
-{
-  return m_errno;
-}
-
-inline Stat::dev_t
-Stat::device() const
-{
-  return m_stat.st_dev;
-}
-
-inline Stat::ino_t
-Stat::inode() const
-{
-  return m_stat.st_ino;
-}
-
-inline mode_t
-Stat::mode() const
-{
-  return m_stat.st_mode;
-}
-
-inline util::TimePoint
-Stat::atime() const
-{
-#if defined(_WIN32) || defined(HAVE_STRUCT_STAT_ST_ATIM)
-  return util::TimePoint(m_stat.st_atim);
-#elif defined(HAVE_STRUCT_STAT_ST_ATIMESPEC)
-  return util::TimePoint(m_stat.st_atimespec);
-#else
-  return util::TimePoint(m_stat.st_atime, 0);
-#endif
-}
-
-inline util::TimePoint
-Stat::ctime() const
-{
-#if defined(_WIN32) || defined(HAVE_STRUCT_STAT_ST_CTIM)
-  return util::TimePoint(m_stat.st_ctim);
-#elif defined(HAVE_STRUCT_STAT_ST_CTIMESPEC)
-  return util::TimePoint(m_stat.st_ctimespec);
-#else
-  return util::TimePoint(m_stat.st_ctime, 0);
-#endif
-}
-
-inline util::TimePoint
-Stat::mtime() const
-{
-#if defined(_WIN32) || defined(HAVE_STRUCT_STAT_ST_MTIM)
-  return util::TimePoint(m_stat.st_mtim);
-#elif defined(HAVE_STRUCT_STAT_ST_CTIMESPEC)
-  return util::TimePoint(m_stat.st_mtimespec);
-#else
-  return util::TimePoint(m_stat.st_mtime, 0);
-#endif
-}
-
-inline uint64_t
-Stat::size() const
-{
-  return m_stat.st_size;
-}
-
-inline uint64_t
-Stat::size_on_disk() const
-{
-#ifdef _WIN32
-  return util::likely_size_on_disk(size());
-#else
-  return m_stat.st_blocks * 512;
-#endif
-}
-
-inline bool
-Stat::is_directory() const
-{
-  return S_ISDIR(mode());
-}
-
-inline bool
-Stat::is_symlink() const
-{
-  return S_ISLNK(mode());
-}
-
-inline bool
-Stat::is_regular() const
-{
-  return S_ISREG(mode());
-}
-
-#ifdef _WIN32
-inline uint32_t
-Stat::file_attributes() const
-{
-  return m_stat.st_file_attributes;
-}
-
-inline uint32_t
-Stat::reparse_tag() const
-{
-  return m_stat.st_reparse_tag;
-}
-#endif
index b93983811e728daf389d9ab8536a18f6eb9dc7c6..bbebbc621d62b593a414d3cc3c7ce48892b1fd4d 100644 (file)
@@ -27,9 +27,9 @@
 
 #include <Config.hpp>
 #include <Finalizer.hpp>
-#include <Stat.hpp>
 #include <core/exceptions.hpp>
 #include <fmtmacros.hpp>
+#include <util/DirEntry.hpp>
 #include <util/expected.hpp>
 #include <util/file.hpp>
 #include <util/filesystem.hpp>
@@ -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<std::string> relpath_candidates;
   const auto original_path = path;
-  Stat path_stat;
-  while (!(path_stat = Stat::stat(std::string(path)))) {
+  DirEntry dir_entry(path);
+  while (!dir_entry.exists()) {
     path = Util::dir_name(path);
+    dir_entry = DirEntry(path);
   }
   const auto path_suffix = std::string(original_path.substr(path.length()));
   const auto real_path = util::real_path(path);
@@ -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;
 }
index 9f7dfc6cd2744c4384503e95a6f4951cf6f71fc9..a095b7920c60dbac7bcd7c168477ac2941a2ba9b 100644 (file)
@@ -27,6 +27,7 @@
 
 #include <Depfile.hpp>
 #include <Util.hpp>
+#include <util/path.hpp>
 #include <util/string.hpp>
 #include <util/wincompat.hpp>
 
@@ -37,6 +38,7 @@
 #include <cassert>
 
 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;
   }
index f66e382d55063a4acf974500b119bd6f5fa7ed26..904dca3883bbd44886e7d7ee89a827404e771a4f 100644 (file)
@@ -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
     // <https://bugs.llvm.org/show_bug.cgi?id=39782>.
@@ -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<void, Failure>
 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<void, Failure>
 hash_nvcc_host_compiler(const Context& ctx,
                         Hash& hash,
-                        const Stat* ccbin_st = nullptr,
+                        const DirEntry* ccbin_st = nullptr,
                         const std::string& ccbin = {})
 {
   // From <http://docs.nvidia.com/cuda/cuda-compiler-driver-nvcc/index.html>:
@@ -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()) {
index 11e1f479906cf7a58f56f506733209975c6759f8..4e2f563a6e65f554d1f1b185245666cd9e450471 100644 (file)
 #include <util/expected.hpp>
 #include <util/file.hpp>
 
+using util::DirEntry;
+
 namespace core {
 
-Stat
-FileRecompressor::recompress(const Stat& stat,
+DirEntry
+FileRecompressor::recompress(const DirEntry& dir_entry,
                              std::optional<int8_t> level,
                              KeepAtime keep_atime)
 {
-  core::CacheEntry::Header header(stat.path());
+  core::CacheEntry::Header header(dir_entry.path().string());
 
   const int8_t wanted_level =
     level ? (*level == 0 ? core::CacheEntry::default_compression_level : *level)
           : 0;
 
-  std::optional<Stat> new_stat;
+  std::optional<DirEntry> new_dir_entry;
 
   if (header.compression_level != wanted_level) {
     const auto cache_file_data = util::value_or_throw<core::Error>(
-      util::read_file<util::Bytes>(stat.path()),
-      FMT("Failed to read {}: ", stat.path()));
+      util::read_file<util::Bytes>(dir_entry.path().string()),
+      FMT("Failed to read {}: ", dir_entry.path().string()));
     core::CacheEntry cache_entry(cache_file_data);
     cache_entry.verify_checksum();
 
@@ -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
index 84ce2824617ef82f4226b026649671ad151508b1..0c341a0942f9297a9e5fac8c1232c0b24bf9206e 100644 (file)
@@ -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 <Stat.hpp>
+#include <util/DirEntry.hpp>
 
 #include <atomic>
 #include <cstdint>
@@ -36,9 +36,9 @@ public:
   FileRecompressor() = default;
 
   // Returns stat after recompression.
-  Stat recompress(const Stat& stat,
-                  std::optional<int8_t> level,
-                  KeepAtime keep_atime);
+  util::DirEntry recompress(const util::DirEntry& dir_entry,
+                            std::optional<int8_t> level,
+                            KeepAtime keep_atime);
 
   uint64_t content_size() const;
   uint64_t old_size() const;
index 784075665763abd7641ab3baee97b0bcd9abc813..e9cfadd8097bc6952b0a45f8dcd87dd29345bfec 100644 (file)
@@ -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;
index 8d16808b05bb72f594106b59198c344e97922853..31b3513c86b623abae3bdf07a33385e9ec88fd01 100644 (file)
@@ -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 <ccache.hpp>
@@ -33,6 +32,7 @@
 #include <core/exceptions.hpp>
 #include <fmtmacros.hpp>
 #include <util/Bytes.hpp>
+#include <util/DirEntry.hpp>
 #include <util/expected.hpp>
 #include <util/file.hpp>
 #include <util/path.hpp>
@@ -68,6 +68,8 @@
 // <raw_file_marker>      ::= 1 (uint8_t)
 // <file_size>            ::= 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<std::string>(entry.data), Stat::LogOnError::yes)
+        ? DirEntry(std::get<std::string>(entry.data), DirEntry::LogOnError::yes)
             .size()
         : std::get<nonstd::span<const uint8_t>>(entry.data).size();
 
index 42c5403a94ccd15c457fdf3bdf177c4079cd58d2..38347959e1ffa1f70cfa1d583e8c6c44a80d6f66 100644 (file)
 #include "Util.hpp"
 #include "fmtmacros.hpp"
 
-#include <Stat.hpp>
 #include <core/exceptions.hpp>
 #include <fmtmacros.hpp>
 #include <util/Bytes.hpp>
+#include <util/DirEntry.hpp>
 #include <util/expected.hpp>
 #include <util/file.hpp>
 #include <util/wincompat.hpp>
@@ -35,6 +35,8 @@
 
 #include <vector>
 
+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));
   }
 
index 47e22e7f5de026f64ef4531b5564f9219503b7a5..83993331bfd84e20b4971f8b18e5a9a95411200c 100644 (file)
 #include "Logging.hpp"
 
 #include <Context.hpp>
-#include <Stat.hpp>
 #include <Util.hpp>
 #include <core/MsvcShowIncludesOutput.hpp>
 #include <core/common.hpp>
 #include <core/exceptions.hpp>
 #include <fmtmacros.hpp>
+#include <util/DirEntry.hpp>
 #include <util/expected.hpp>
 #include <util/file.hpp>
+#include <util/path.hpp>
 #include <util/string.hpp>
 #include <util/wincompat.hpp>
 
@@ -42,6 +43,8 @@
 #  include <unistd.h>
 #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;
index 5fd0ba59a6fc10b340a891288406748a66eace08..27a2fbbc6bb6a053e2081248b06d9552e88e4c8f 100644 (file)
@@ -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());
index 0c28a7acdedb77d5b306595b7e0553fc55bef292..2233962b08f04669d28199d7dfb2afd56f5489ff 100644 (file)
@@ -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<std::optional<int8_t>> recompress_level,
          uint32_t recompress_threads)
 {
-  std::vector<Stat> files;
+  std::vector<DirEntry> files;
   uint64_t initial_size = 0;
 
   util::throw_on_error<core::Error>(util::traverse_directory(
@@ -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));
index 36743e28c96b7aa1c3a5aaf10041a44c62b8d4f3..1c716469200cd4393c63ffc4ff16ab7ef33dcd9b 100644 (file)
@@ -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 <ccache.hpp>
 #include <core/exceptions.hpp>
 #include <fmtmacros.hpp>
+#include <util/DirEntry.hpp>
 #include <util/file.hpp>
 #include <util/path.hpp>
 #include <util/string.hpp>
@@ -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
index e75b2757603e8d13e83b1555ce9517c9d9018067..0cd4265c7c70bd87f4e8a0a3c99832771df3c7ae 100644 (file)
 #include "Config.hpp"
 #include "Context.hpp"
 #include "Logging.hpp"
-#include "Stat.hpp"
 #include "Win32Util.hpp"
 #include "execute.hpp"
 #include "macroskip.hpp"
 
 #include <core/exceptions.hpp>
 #include <fmtmacros.hpp>
+#include <util/DirEntry.hpp>
 #include <util/file.hpp>
 #include <util/string.hpp>
 #include <util/time.hpp>
@@ -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;
index 12a4052eb7f6d889c952fcb1cc31306c2960b570..e97ac996adfcf9c41ff6118a68846b838f08c5fd 100644 (file)
@@ -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<int64_t>(new_stat.size_on_disk())
-          - static_cast<int64_t>(old_stat.size_on_disk()))
+  return (static_cast<int64_t>(new_dir_entry.size_on_disk())
+          - static_cast<int64_t>(old_dir_entry.size_on_disk()))
          / 1024;
 }
 
@@ -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<util::Bytes> return_value;
 
   const auto cache_file = look_up_cache_file(key, type);
-  if (cache_file.stat) {
+  if (cache_file.dir_entry.is_regular_file()) {
     const auto value = util::read_file<util::Bytes>(cache_file.path);
     if (value) {
       LOG("Retrieved {} from local storage ({})",
@@ -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<int64_t>(cache_file.stat.size_on_disk() / 1024));
+    key, -1, -static_cast<int64_t>(cache_file.dir_entry.size_on_disk() / 1024));
 }
 
 std::string
@@ -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<int8_t> 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<int8_t> 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);
       }
     })
index 8b529b916fbd400feb588eac378b214a12627ff1..0fe89e725368b58a53a84d0fff0c911a734d78e2 100644 (file)
@@ -32,6 +32,7 @@
 #include <third_party/nonstd/span.hpp>
 
 #include <cstdint>
+#include <filesystem>
 #include <optional>
 #include <string_view>
 #include <vector>
@@ -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;
   };
 
index 6649b1f11a747efd13435fc6d43fc8d66b2283d4..871f8361e822fe6c4cf9dcfeb84b69a433aa1169 100644 (file)
@@ -25,6 +25,8 @@
 #include <util/file.hpp>
 #include <util/string.hpp>
 
+using util::DirEntry;
+
 namespace storage::local {
 
 void
@@ -62,12 +64,12 @@ for_each_level_1_and_2_stats_file(
   }
 }
 
-std::vector<Stat>
+std::vector<DirEntry>
 get_cache_dir_files(const std::string& dir)
 {
-  std::vector<Stat> files;
+  std::vector<DirEntry> files;
 
-  if (!Stat::stat(dir)) {
+  if (!DirEntry(dir).is_directory()) {
     return files;
   }
   util::throw_on_error<core::Error>(
@@ -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);
       }
     }));
 
index ed2ada401f0e347216f09ff926a5a61161b097af..d48af32f8b7f393fa500ab0bef406208073f03fd 100644 (file)
@@ -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 <Stat.hpp>
+#include <util/DirEntry.hpp>
 
 #include <functional>
 #include <string>
@@ -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<Stat> get_cache_dir_files(const std::string& dir);
+std::vector<util::DirEntry> get_cache_dir_files(const std::string& dir);
 
 } // namespace storage::local
index da68921b56a836f78636ce4d19495888df4e6961..840c63001026aea13f32cdacb58eb58051d60f1a 100644 (file)
 
 #include <AtomicFile.hpp>
 #include <Logging.hpp>
-#include <Stat.hpp>
 #include <Util.hpp>
 #include <assertions.hpp>
 #include <core/exceptions.hpp>
 #include <fmtmacros.hpp>
 #include <util/Bytes.hpp>
+#include <util/DirEntry.hpp>
 #include <util/UmaskScope.hpp>
 #include <util/expected.hpp>
 #include <util/file.hpp>
@@ -38,6 +38,8 @@
 
 namespace fs = util::filesystem;
 
+using util::DirEntry;
+
 namespace storage::remote {
 
 namespace {
@@ -115,9 +117,8 @@ tl::expected<std::optional<util::Bytes>, 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;
   }
index 38e0b290f9b8e88ca22a827c26680c84f61e461d..e9d4042fe9d7896b2ee1c75eb5710b1b8203a059 100644 (file)
@@ -1,6 +1,7 @@
 set(
   sources
   Bytes.cpp
+  DirEntry.cpp
   LockFile.cpp
   LongLivedLockFileManager.cpp
   TextTable.cpp
similarity index 80%
rename from src/Stat.cpp
rename to src/util/DirEntry.cpp
index 429302f313b1d4c56d14a361542ff3eac10b60c9..80ed955ef345c16f77d82045908c2d8a17a27538 100644 (file)
 // this program; if not, write to the Free Software Foundation, Inc., 51
 // Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
-#include "Stat.hpp"
-
-#include "Finalizer.hpp"
-#include "Logging.hpp"
-#include "Win32Util.hpp"
+#include "DirEntry.hpp"
 
+#include <Finalizer.hpp>
+#include <Logging.hpp>
+#include <Win32Util.hpp>
 #include <core/exceptions.hpp>
 #include <fmtmacros.hpp>
 #include <util/wincompat.hpp>
@@ -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<uint64_t>(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 (file)
index 0000000..9b4b81c
--- /dev/null
@@ -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 <util/TimePoint.hpp>
+#include <util/file.hpp>
+#include <util/wincompat.hpp>
+
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include <cstdint>
+#include <ctime>
+#include <filesystem>
+
+namespace util {
+
+// This class is similar to std::filesystem::directory_entry with a couple of
+// extra features, for example:
+//
+// - operator bool tells whether the directory entry exists (not following
+//   symlinks, in contrast to the exists() method).
+// - Supports access to atime and ctime fields.
+// - Supports logging on error.
+class DirEntry
+{
+public:
+  enum class LogOnError : bool { no, yes };
+
+#if defined(_WIN32)
+  struct stat_t
+  {
+    uint64_t st_dev;
+    uint64_t st_ino;
+    uint16_t st_mode;
+    uint16_t st_nlink;
+    uint64_t st_size;
+    struct timespec st_atim;
+    struct timespec st_mtim;
+    struct timespec st_ctim;
+    uint32_t st_file_attributes;
+    uint32_t st_reparse_tag;
+  };
+#else
+  using stat_t = struct stat;
+#endif
+
+  using dev_t = decltype(stat_t{}.st_dev);
+  using ino_t = decltype(stat_t{}.st_ino);
+
+  // Create an empty directory entry. operator bool() will return false,
+  // error_number() will return ENOENT and other accessors will return false or
+  // 0.
+  DirEntry() = default;
+
+  // The underlying (l)stat(2) call will not be made by the constructor but
+  // on-demand when calling the first query function. That (l)stat result is
+  // then cached. See also the refresh method.
+  DirEntry(const std::filesystem::path& path,
+           LogOnError log_on_error = LogOnError::no);
+
+  // Return true if the file could be lstat(2)-ed (i.e., the directory entry
+  // exists without following symlinks), otherwise false.
+  operator bool() const;
+
+  // Return true if the file could be stat(2)-ed (i.e., the directory entry
+  // exists when following symlinks), otherwise false.
+  bool exists() const;
+
+  // Return the path that this entry refers to.
+  const std::filesystem::path& path() const;
+
+  // Return whether the entry refers to the same device and i-node as `other`.
+  bool same_inode_as(const DirEntry& other) const;
+
+  // Return errno from the lstat(2) call (0 if successful).
+  int error_number() const;
+
+  dev_t device() const;
+  ino_t inode() const;
+  mode_t mode() const;
+  util::TimePoint atime() const;
+  util::TimePoint ctime() const;
+  util::TimePoint mtime() const;
+  uint64_t size() const;
+
+  uint64_t size_on_disk() const;
+
+  bool is_directory() const;
+  bool is_regular_file() const;
+  bool is_symlink() const;
+
+  // Update the cached (l)stat(2) result.
+  void refresh();
+
+#ifdef _WIN32
+  uint32_t file_attributes() const;
+  uint32_t reparse_tag() const;
+#endif
+
+private:
+  std::filesystem::path m_path;
+  LogOnError m_log_on_error = LogOnError::no;
+  mutable stat_t m_stat;
+  mutable int m_errno = -1;
+  mutable bool m_initialized = false;
+  mutable bool m_exists = false;
+  mutable bool m_is_symlink = false;
+
+  const stat_t& do_stat() const;
+};
+
+inline DirEntry::DirEntry(const std::filesystem::path& path,
+                          LogOnError log_on_error)
+  : m_path(path),
+    m_log_on_error(log_on_error)
+{
+}
+
+inline DirEntry::operator bool() const
+{
+  do_stat();
+  return m_errno == 0;
+}
+
+inline bool
+DirEntry::exists() const
+{
+  do_stat();
+  return m_exists;
+}
+
+inline bool
+DirEntry::same_inode_as(const DirEntry& other) const
+{
+  do_stat();
+  return m_errno == 0 && device() == other.device() && inode() == other.inode();
+}
+
+inline const std::filesystem::path&
+DirEntry::path() const
+{
+  return m_path;
+}
+
+inline int
+DirEntry::error_number() const
+{
+  do_stat();
+  return m_errno;
+}
+
+inline DirEntry::dev_t
+DirEntry::device() const
+{
+  return do_stat().st_dev;
+}
+
+inline DirEntry::ino_t
+DirEntry::inode() const
+{
+  return do_stat().st_ino;
+}
+
+inline mode_t
+DirEntry::mode() const
+{
+  return do_stat().st_mode;
+}
+
+inline util::TimePoint
+DirEntry::atime() const
+{
+#if defined(_WIN32) || defined(HAVE_STRUCT_STAT_ST_ATIM)
+  return util::TimePoint(do_stat().st_atim);
+#elif defined(HAVE_STRUCT_STAT_ST_ATIMESPEC)
+  return util::TimePoint(do_stat().st_atimespec);
+#else
+  return util::TimePoint(do_stat().st_atime, 0);
+#endif
+}
+
+inline util::TimePoint
+DirEntry::ctime() const
+{
+#if defined(_WIN32) || defined(HAVE_STRUCT_STAT_ST_CTIM)
+  return util::TimePoint(do_stat().st_ctim);
+#elif defined(HAVE_STRUCT_STAT_ST_CTIMESPEC)
+  return util::TimePoint(do_stat().st_ctimespec);
+#else
+  return util::TimePoint(do_stat().st_ctime, 0);
+#endif
+}
+
+inline util::TimePoint
+DirEntry::mtime() const
+{
+#if defined(_WIN32) || defined(HAVE_STRUCT_STAT_ST_MTIM)
+  return util::TimePoint(do_stat().st_mtim);
+#elif defined(HAVE_STRUCT_STAT_ST_CTIMESPEC)
+  return util::TimePoint(do_stat().st_mtimespec);
+#else
+  return util::TimePoint(do_stat().st_mtime, 0);
+#endif
+}
+
+inline uint64_t
+DirEntry::size() const
+{
+  return do_stat().st_size;
+}
+
+inline uint64_t
+DirEntry::size_on_disk() const
+{
+#ifdef _WIN32
+  return util::likely_size_on_disk(size());
+#else
+  return do_stat().st_blocks * 512;
+#endif
+}
+
+inline bool
+DirEntry::is_directory() const
+{
+  return S_ISDIR(mode());
+}
+
+inline bool
+DirEntry::is_symlink() const
+{
+  return m_is_symlink;
+}
+
+inline void
+DirEntry::refresh()
+{
+  m_initialized = false;
+  do_stat();
+}
+
+inline bool
+DirEntry::is_regular_file() const
+{
+  return S_ISREG(mode());
+}
+
+#ifdef _WIN32
+inline uint32_t
+DirEntry::file_attributes() const
+{
+  return do_stat().st_file_attributes;
+}
+
+inline uint32_t
+DirEntry::reparse_tag() const
+{
+  return do_stat().st_reparse_tag;
+}
+#endif
+
+} // namespace util
index e6832974cd9170c476c2130646f4974ab1ea0a3e..f08340cdcfcc4cddb9f618aca73f2a01ce0a5ec6 100644 (file)
@@ -23,9 +23,9 @@
 #include "Win32Util.hpp"
 #include "fmtmacros.hpp"
 
-#include <Stat.hpp>
 #include <assertions.hpp>
 #include <core/exceptions.hpp>
+#include <util/DirEntry.hpp>
 #include <util/file.hpp>
 #include <util/filesystem.hpp>
 #include <util/process.hpp>
@@ -346,8 +346,8 @@ LockFile::do_acquire(const bool blocking)
 std::optional<TimePoint>
 LockFile::get_last_lock_update()
 {
-  if (const auto stat = Stat::stat(m_alive_file); stat) {
-    return stat.mtime();
+  if (DirEntry entry(m_alive_file); entry) {
+    return entry.mtime();
   } else {
     return std::nullopt;
   }
index 604ec28a1095063c86792dd645165998fd745c4c..107181c59f7946b95538726e2d51892cad2f7bcd 100644 (file)
 #include <Fd.hpp>
 #include <Finalizer.hpp>
 #include <Logging.hpp>
-#include <Stat.hpp>
 #include <TemporaryFile.hpp>
 #include <Win32Util.hpp>
 #include <fmtmacros.hpp>
 #include <util/Bytes.hpp>
+#include <util/DirEntry.hpp>
 #include <util/expected.hpp>
 #include <util/file.hpp>
 #include <util/filesystem.hpp>
@@ -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<T, std::string>
 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 {
index f5cd1ec5910409bcf4dc4b3de7057b3edabe48de..755dc318f51f3a568d264b90fd6cf3c166bb5cfe 100644 (file)
 
 #include <util/wincompat.hpp>
 
+#ifdef _WIN32
+#  include <third_party/win32/winerror_to_errno.h>
+#endif
+
 namespace util::filesystem {
 
 tl::expected<void, std::error_code>
@@ -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 {};
index ccb0bfbb865df29f6b2b47e7352d839645570bdf..019d3965660f2c1e6c9d5a6382cf8dac16dbf5d2 100644 (file)
@@ -18,9 +18,9 @@
 
 #include "path.hpp"
 
-#include <Stat.hpp>
 #include <Util.hpp>
 #include <fmtmacros.hpp>
+#include <util/DirEntry.hpp>
 #include <util/filesystem.hpp>
 #include <util/string.hpp>
 
@@ -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
index 1dc2429805aa8138fa5aa8c69d0e6743c1e8b5d5..ccd92a2c038e725c8a6207163a875f0b5c901dba 100644 (file)
@@ -18,6 +18,8 @@
 
 #pragma once
 
+#include <util/string.hpp>
+
 #include <string>
 #include <string_view>
 #include <vector>
@@ -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)
 {
index ddf574a5d30da81e79d531ef4de3b7045a703c4f..d455ee8babade6dff17ec98efbabce47fbdefc41 100644 (file)
@@ -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(
index bd0c9cc3387b750d15af74b1caa4a56bfe68e00d..25c0f6a20cc0c11b55999380c44e5d55a023fdcd 100644 (file)
@@ -19,7 +19,7 @@
 #include "../src/AtomicFile.hpp"
 #include "TestUtil.hpp"
 
-#include <Stat.hpp>
+#include <util/DirEntry.hpp>
 #include <util/file.hpp>
 
 #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();
index 5c2fc3ecc56ad080714b06304cdc3a631e50877e..c5bbf796ec91375ba179c972b4bcf1a772473723 100644 (file)
@@ -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 (file)
index 18b0479..0000000
+++ /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 <core/exceptions.hpp>
-#include <util/environment.hpp>
-#include <util/file.hpp>
-#include <util/wincompat.hpp>
-
-#include "third_party/doctest.h"
-
-#ifdef HAVE_UNISTD_H
-#  include <unistd.h>
-#endif
-
-#ifdef _WIN32
-#  include <shlobj.h>
-#endif
-
-using TestUtil::TestContext;
-
-namespace {
-
-bool
-running_under_wine()
-{
-#ifdef _WIN32
-  static bool is_wine =
-    GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "wine_get_version")
-    != nullptr;
-  return is_wine;
-#else
-  return false;
-#endif
-}
-
-bool
-symlinks_supported()
-{
-#ifdef _WIN32
-  // Windows only supports symlinks if the user has the required privilege (e.g.
-  // they're an admin) or if developer mode is enabled.
-
-  // See: https://stackoverflow.com/a/41232108/192102
-  const char* dev_mode_key =
-    "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\AppModelUnlock";
-  const char* dev_mode_value = "AllowDevelopmentWithoutDevLicense";
-
-  DWORD dev_mode_enabled = 0;
-  DWORD buf_size = sizeof(dev_mode_enabled);
-
-  return !running_under_wine()
-         && (IsUserAnAdmin()
-             || (RegGetValueA(HKEY_LOCAL_MACHINE,
-                              dev_mode_key,
-                              dev_mode_value,
-                              RRF_RT_DWORD,
-                              nullptr,
-                              &dev_mode_enabled,
-                              &buf_size)
-                   == ERROR_SUCCESS
-                 && dev_mode_enabled));
-#else
-  return true;
-#endif
-}
-
-#ifdef _WIN32
-bool
-win32_is_junction(const std::string& path)
-{
-  HANDLE handle =
-    CreateFileA(path.c_str(),
-                FILE_READ_ATTRIBUTES,
-                FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
-                nullptr,
-                OPEN_EXISTING,
-                FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
-                nullptr);
-  if (handle == INVALID_HANDLE_VALUE) {
-    return false;
-  }
-  FILE_ATTRIBUTE_TAG_INFO reparse_info = {};
-  bool is_junction =
-    (GetFileType(handle) == FILE_TYPE_DISK)
-    && GetFileInformationByHandleEx(
-      handle, FileAttributeTagInfo, &reparse_info, sizeof(reparse_info))
-    && (reparse_info.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)
-    && (reparse_info.ReparseTag == IO_REPARSE_TAG_MOUNT_POINT);
-  CloseHandle(handle);
-  return is_junction;
-}
-
-bool
-win32_get_file_info(const std::string& path, BY_HANDLE_FILE_INFORMATION* info)
-{
-  HANDLE handle =
-    CreateFileA(path.c_str(),
-                FILE_READ_ATTRIBUTES,
-                FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
-                nullptr,
-                OPEN_EXISTING,
-                FILE_FLAG_BACKUP_SEMANTICS,
-                nullptr);
-  if (handle == INVALID_HANDLE_VALUE) {
-    return false;
-  }
-  BOOL ret = GetFileInformationByHandle(handle, info);
-  CloseHandle(handle);
-  return ret;
-}
-
-struct timespec
-win32_filetime_to_timespec(FILETIME ft)
-{
-  static const int64_t SECS_BETWEEN_EPOCHS = 11644473600;
-  uint64_t v =
-    (static_cast<uint64_t>(ft.dwHighDateTime) << 32) | ft.dwLowDateTime;
-
-  struct timespec ts = {};
-  ts.tv_sec = (v / 10000000) - SECS_BETWEEN_EPOCHS;
-  ts.tv_nsec = (v % 10000000) * 100;
-  return ts;
-}
-#endif
-
-} // namespace
-
-TEST_SUITE_BEGIN("Stat");
-
-TEST_CASE("Default constructor")
-{
-  Stat stat;
-  CHECK(!stat);
-  CHECK(stat.error_number() == -1);
-  CHECK(stat.device() == 0);
-  CHECK(stat.inode() == 0);
-  CHECK(stat.mode() == 0);
-  CHECK(stat.ctime().sec() == 0);
-  CHECK(stat.ctime().nsec() == 0);
-  CHECK(stat.mtime().sec() == 0);
-  CHECK(stat.mtime().nsec() == 0);
-  CHECK(stat.size() == 0);
-  CHECK(stat.size_on_disk() == 0);
-  CHECK(!stat.is_directory());
-  CHECK(!stat.is_regular());
-  CHECK(!stat.is_symlink());
-
-#ifdef _WIN32
-  CHECK(stat.file_attributes() == 0);
-  CHECK(stat.reparse_tag() == 0);
-#endif
-}
-
-TEST_CASE("Named constructors")
-{
-  CHECK(!Stat::stat("does_not_exist"));
-  CHECK(!Stat::stat("does_not_exist", Stat::LogOnError::no));
-  CHECK(!Stat::stat("does_not_exist", Stat::LogOnError::yes));
-}
-
-TEST_CASE("Same i-node as")
-{
-  TestContext test_context;
-
-  util::write_file("a", "");
-  util::write_file("b", "");
-  auto a_stat = Stat::stat("a");
-  auto b_stat = Stat::stat("b");
-
-  CHECK(a_stat.same_inode_as(a_stat));
-  CHECK(!a_stat.same_inode_as(b_stat));
-
-  util::write_file("a", "change size", util::InPlace::yes);
-  auto new_a_stat = Stat::stat("a");
-  CHECK(new_a_stat.same_inode_as(a_stat));
-
-  CHECK(!Stat::stat("nonexistent").same_inode_as(Stat::stat("nonexistent")));
-}
-
-TEST_CASE("Get path")
-{
-  TestContext test_context;
-
-  util::write_file("a", "");
-  CHECK(Stat::stat("a").path() == "a");
-  CHECK(Stat::stat("does_not_exist").path() == "does_not_exist");
-}
-
-TEST_CASE("Return values when file is missing")
-{
-  auto stat = Stat::stat("does_not_exist");
-  CHECK(!stat);
-  CHECK(stat.error_number() == ENOENT);
-  CHECK(stat.device() == 0);
-  CHECK(stat.inode() == 0);
-  CHECK(stat.mode() == 0);
-  CHECK(stat.ctime().sec() == 0);
-  CHECK(stat.ctime().nsec() == 0);
-  CHECK(stat.mtime().sec() == 0);
-  CHECK(stat.mtime().nsec() == 0);
-  CHECK(stat.size() == 0);
-  CHECK(stat.size_on_disk() == 0);
-  CHECK(!stat.is_directory());
-  CHECK(!stat.is_regular());
-  CHECK(!stat.is_symlink());
-
-#ifdef _WIN32
-  CHECK(stat.file_attributes() == 0);
-  CHECK(stat.reparse_tag() == 0);
-#endif
-}
-
-TEST_CASE("Return values when file exists")
-{
-  TestContext test_context;
-
-  util::write_file("file", "1234567");
-
-  auto stat = Stat::stat("file");
-  CHECK(stat);
-  CHECK(stat.error_number() == 0);
-  CHECK(!stat.is_directory());
-  CHECK(stat.is_regular());
-  CHECK(!stat.is_symlink());
-  CHECK(stat.size() == 7);
-
-#ifdef _WIN32
-  BY_HANDLE_FILE_INFORMATION info = {};
-  CHECK(win32_get_file_info("file", &info));
-
-  CHECK(stat.device() == info.dwVolumeSerialNumber);
-  CHECK((stat.inode() >> 32) == info.nFileIndexHigh);
-  CHECK((stat.inode() & ((1ULL << 32) - 1)) == info.nFileIndexLow);
-  CHECK(S_ISREG(stat.mode()));
-  CHECK((stat.mode() & ~S_IFMT) == 0666);
-
-  struct timespec creation_time =
-    win32_filetime_to_timespec(info.ftCreationTime);
-  struct timespec last_write_time =
-    win32_filetime_to_timespec(info.ftLastWriteTime);
-
-  CHECK(stat.ctime().sec() == creation_time.tv_sec);
-  CHECK(stat.ctime().nsec_decimal_part() == creation_time.tv_nsec);
-  CHECK(stat.mtime().sec() == last_write_time.tv_sec);
-  CHECK(stat.mtime().nsec_decimal_part() == last_write_time.tv_nsec);
-
-  CHECK(stat.size_on_disk() == ((stat.size() + 4095) & ~4095));
-  CHECK(stat.file_attributes() == info.dwFileAttributes);
-  CHECK(stat.reparse_tag() == 0);
-
-#else
-  struct stat st;
-  CHECK(::stat("file", &st) == 0);
-
-  CHECK(stat.device() == st.st_dev);
-  CHECK(stat.inode() == st.st_ino);
-  CHECK(stat.mode() == st.st_mode);
-  CHECK(stat.size_on_disk() == st.st_blocks * 512);
-
-#  ifdef HAVE_STRUCT_STAT_ST_CTIM
-  CHECK(stat.ctime().sec() == st.st_ctim.tv_sec);
-  CHECK(stat.ctime().nsec_decimal_part() == st.st_ctim.tv_nsec);
-#  elif defined(HAVE_STRUCT_STAT_ST_CTIMESPEC)
-  CHECK(stat.ctime().sec() == st.st_ctimespec.tv_sec);
-  CHECK(stat.ctime().nsec_decimal_part() == st.st_ctimespec.tv_nsec);
-#  else
-  CHECK(stat.ctime().sec() == st.st_ctime);
-  CHECK(stat.ctime().nsec_decimal_part() == 0);
-#  endif
-
-#  ifdef HAVE_STRUCT_STAT_ST_MTIM
-  CHECK(stat.mtime().sec() == st.st_mtim.tv_sec);
-  CHECK(stat.mtime().nsec_decimal_part() == st.st_mtim.tv_nsec);
-#  elif defined(HAVE_STRUCT_STAT_ST_MTIMESPEC)
-  CHECK(stat.mtime().sec() == st.st_mtimespec.tv_sec);
-  CHECK(stat.mtime().nsec_decimal_part() == st.st_mtimespec.tv_nsec);
-#  else
-  CHECK(stat.mtime().sec() == st.st_mtime);
-  CHECK(stat.mtime().nsec_decimal_part() == 0);
-#  endif
-#endif
-}
-
-TEST_CASE("Directory")
-{
-  TestContext test_context;
-
-  REQUIRE(mkdir("directory", 0456) == 0);
-  auto stat = Stat::stat("directory");
-
-  CHECK(stat);
-  CHECK(stat.error_number() == 0);
-  CHECK(stat.is_directory());
-  CHECK(!stat.is_regular());
-  CHECK(!stat.is_symlink());
-  CHECK(S_ISDIR(stat.mode()));
-#ifdef _WIN32
-  CHECK((stat.mode() & ~S_IFMT) == 0777);
-  CHECK((stat.file_attributes() & FILE_ATTRIBUTE_DIRECTORY));
-  CHECK(!(stat.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT));
-  CHECK(stat.reparse_tag() == 0);
-#endif
-}
-
-TEST_CASE("Symlinks" * doctest::skip(!symlinks_supported()))
-{
-  TestContext test_context;
-
-  util::write_file("file", "1234567");
-
-#ifdef _WIN32
-  REQUIRE(CreateSymbolicLinkA(
-    "symlink", "file", 0x2 /*SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE*/));
-#else
-  REQUIRE(symlink("file", "symlink") == 0);
-#endif
-
-  SUBCASE("file lstat")
-  {
-    auto stat = Stat::lstat("file", Stat::LogOnError::no);
-    CHECK(stat);
-    CHECK(stat.error_number() == 0);
-    CHECK(!stat.is_directory());
-    CHECK(stat.is_regular());
-    CHECK(!stat.is_symlink());
-    CHECK(S_ISREG(stat.mode()));
-    CHECK(stat.size() == 7);
-#ifdef _WIN32
-    CHECK(!(stat.file_attributes() & FILE_ATTRIBUTE_DIRECTORY));
-    CHECK(!(stat.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT));
-    CHECK(stat.reparse_tag() == 0);
-#endif
-  }
-
-  SUBCASE("file stat")
-  {
-    auto stat = Stat::stat("file", Stat::LogOnError::no);
-    CHECK(stat);
-    CHECK(stat.error_number() == 0);
-    CHECK(!stat.is_directory());
-    CHECK(stat.is_regular());
-    CHECK(!stat.is_symlink());
-    CHECK(S_ISREG(stat.mode()));
-    CHECK(stat.size() == 7);
-#ifdef _WIN32
-    CHECK(!(stat.file_attributes() & FILE_ATTRIBUTE_DIRECTORY));
-    CHECK(!(stat.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT));
-    CHECK(stat.reparse_tag() == 0);
-#endif
-  }
-
-  SUBCASE("symlink lstat")
-  {
-    auto stat = Stat::lstat("symlink", Stat::LogOnError::no);
-    CHECK(stat);
-    CHECK(stat.error_number() == 0);
-    CHECK(!stat.is_directory());
-    CHECK(!stat.is_regular());
-    CHECK(stat.is_symlink());
-    CHECK(S_ISLNK(stat.mode()));
-#ifdef _WIN32
-    CHECK(!(stat.file_attributes() & FILE_ATTRIBUTE_DIRECTORY));
-    CHECK((stat.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT));
-    CHECK(stat.reparse_tag() == IO_REPARSE_TAG_SYMLINK);
-#else
-    CHECK(stat.size() == 4);
-#endif
-  }
-
-  SUBCASE("symlink stat")
-  {
-    auto stat = Stat::stat("symlink", Stat::LogOnError::no);
-    CHECK(stat);
-    CHECK(stat.error_number() == 0);
-    CHECK(!stat.is_directory());
-    CHECK(stat.is_regular());
-    CHECK(!stat.is_symlink());
-    CHECK(S_ISREG(stat.mode()));
-    CHECK(stat.size() == 7);
-#ifdef _WIN32
-    CHECK(!(stat.file_attributes() & FILE_ATTRIBUTE_DIRECTORY));
-    CHECK(!(stat.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT));
-    CHECK(stat.reparse_tag() == 0);
-#endif
-  }
-}
-
-TEST_CASE("Hard links")
-{
-  TestContext test_context;
-
-  util::write_file("a", "");
-
-#ifdef _WIN32
-  REQUIRE(CreateHardLinkA("b", "a", nullptr));
-#else
-  REQUIRE(link("a", "b") == 0);
-#endif
-
-  auto stat_a = Stat::stat("a");
-  CHECK(stat_a);
-  CHECK(stat_a.error_number() == 0);
-  CHECK(!stat_a.is_directory());
-  CHECK(stat_a.is_regular());
-  CHECK(!stat_a.is_symlink());
-  CHECK(stat_a.size() == 0);
-
-  auto stat_b = Stat::stat("b");
-  CHECK(stat_b);
-  CHECK(stat_b.error_number() == 0);
-  CHECK(!stat_b.is_directory());
-  CHECK(stat_b.is_regular());
-  CHECK(!stat_b.is_symlink());
-  CHECK(stat_b.size() == 0);
-
-  CHECK(stat_a.device() == stat_b.device());
-  CHECK(stat_a.inode() == stat_b.inode());
-  CHECK(stat_a.same_inode_as(stat_b));
-
-  util::write_file("a", "1234567", util::InPlace::yes);
-  stat_a = Stat::stat("a");
-  stat_b = Stat::stat("b");
-
-  CHECK(stat_a.size() == 7);
-  CHECK(stat_b.size() == 7);
-}
-
-TEST_CASE("Special" * doctest::skip(running_under_wine()))
-{
-  SUBCASE("tty")
-  {
-#ifdef _WIN32
-    auto stat = Stat::stat("\\\\.\\CON");
-#else
-    auto stat = Stat::stat("/dev/tty");
-#endif
-    CHECK(stat);
-    CHECK(stat.error_number() == 0);
-    CHECK(!stat.is_directory());
-    CHECK(!stat.is_regular());
-    CHECK(!stat.is_symlink());
-    CHECK(S_ISCHR(stat.mode()));
-#ifdef _WIN32
-    CHECK(stat.file_attributes() == 0);
-    CHECK(stat.reparse_tag() == 0);
-#endif
-  }
-
-  SUBCASE("null")
-  {
-#ifdef _WIN32
-    auto stat = Stat::stat("\\\\.\\NUL");
-#else
-    auto stat = Stat::stat("/dev/null");
-#endif
-    CHECK(stat);
-    CHECK(stat.error_number() == 0);
-    CHECK(!stat.is_directory());
-    CHECK(!stat.is_regular());
-    CHECK(!stat.is_symlink());
-    CHECK(S_ISCHR(stat.mode()));
-#ifdef _WIN32
-    CHECK(stat.file_attributes() == 0);
-    CHECK(stat.reparse_tag() == 0);
-#endif
-  }
-
-  SUBCASE("pipe")
-  {
-#ifdef _WIN32
-    const char* pipe_path = "\\\\.\\pipe\\InitShutdown"; // Well-known pipe name
-#else
-    const char* pipe_path = "my_pipe";
-    REQUIRE(mkfifo(pipe_path, 0600) == 0);
-#endif
-
-    auto stat = Stat::stat(pipe_path);
-    CHECK(stat);
-    CHECK(stat.error_number() == 0);
-    CHECK(!stat.is_directory());
-    CHECK(!stat.is_regular());
-    CHECK(!stat.is_symlink());
-    CHECK(S_ISFIFO(stat.mode()));
-#ifdef _WIN32
-    CHECK(stat.file_attributes() == 0);
-    CHECK(stat.reparse_tag() == 0);
-#endif
-  }
-
-  SUBCASE("block device")
-  {
-#ifdef _WIN32
-    auto stat = Stat::stat("\\\\.\\C:");
-    CHECK(stat);
-    CHECK(stat.error_number() == 0);
-    CHECK(!stat.is_directory());
-    CHECK(!stat.is_regular());
-    CHECK(!stat.is_symlink());
-    CHECK(S_ISBLK(stat.mode()));
-    CHECK(stat.file_attributes() == 0);
-    CHECK(stat.reparse_tag() == 0);
-#endif
-  }
-}
-
-#ifdef _WIN32
-TEST_CASE("Win32 Readonly File")
-{
-  TestContext test_context;
-
-  util::write_file("file", "");
-
-  DWORD prev_attrs = GetFileAttributesA("file");
-  REQUIRE(prev_attrs != INVALID_FILE_ATTRIBUTES);
-  REQUIRE(SetFileAttributesA("file", prev_attrs | FILE_ATTRIBUTE_READONLY));
-
-  auto stat = Stat::stat("file");
-  REQUIRE(SetFileAttributesA("file", prev_attrs));
-
-  CHECK(stat);
-  CHECK(stat.error_number() == 0);
-  CHECK(S_ISREG(stat.mode()));
-  CHECK((stat.mode() & ~S_IFMT) == 0444);
-  CHECK((stat.file_attributes() & FILE_ATTRIBUTE_READONLY));
-  CHECK(!(stat.file_attributes() & FILE_ATTRIBUTE_DIRECTORY));
-  CHECK(!(stat.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT));
-  CHECK(stat.reparse_tag() == 0);
-}
-
-TEST_CASE("Win32 Executable File")
-{
-  TestContext test_context;
-
-  const char* comspec = getenv("COMSPEC");
-  REQUIRE(comspec != nullptr);
-
-  auto stat = Stat::stat(comspec);
-  CHECK(stat);
-  CHECK(stat.error_number() == 0);
-  CHECK(!stat.is_directory());
-  CHECK(stat.is_regular());
-  CHECK(!stat.is_symlink());
-  CHECK(S_ISREG(stat.mode()));
-  CHECK((stat.mode() & ~S_IFMT) == 0777);
-  CHECK(!(stat.file_attributes() & FILE_ATTRIBUTE_DIRECTORY));
-  CHECK(!(stat.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT));
-  CHECK(stat.reparse_tag() == 0);
-}
-
-TEST_CASE("Win32 Pending Delete" * doctest::skip(running_under_wine()))
-{
-  TestContext test_context;
-
-  HANDLE handle =
-    CreateFileA("file",
-                GENERIC_READ | GENERIC_WRITE | DELETE,
-                FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
-                nullptr,
-                CREATE_NEW,
-                FILE_ATTRIBUTE_NORMAL,
-                nullptr);
-  REQUIRE_MESSAGE(handle != INVALID_HANDLE_VALUE, "err=" << GetLastError());
-  Finalizer cleanup([&] { CloseHandle(handle); });
-
-  // Mark file as deleted. This puts it into a "pending delete" state that
-  // will persist until the handle is closed. Until the file is closed, new
-  // handles cannot be created to the file; attempts to do so fail with
-  // ERROR_ACCESS_DENIED/STATUS_DELETE_PENDING. Our stat implementation maps
-  // these to ENOENT.
-  FILE_DISPOSITION_INFO info{};
-  info.DeleteFile = TRUE;
-  REQUIRE_MESSAGE(SetFileInformationByHandle(
-                    handle, FileDispositionInfo, &info, sizeof(info)),
-                  "err=" << GetLastError());
-
-  SUBCASE("stat file pending delete")
-  {
-    auto st = Stat::stat("file");
-    CHECK(!st);
-    CHECK(st.error_number() == ENOENT);
-  }
-
-  SUBCASE("lstat file pending delete")
-  {
-    auto st = Stat::lstat("file");
-    CHECK(!st);
-    CHECK(st.error_number() == ENOENT);
-  }
-}
-
-// Our Win32 Stat implementation should open files using FILE_READ_ATTRIBUTES,
-// which bypasses sharing restrictions.
-TEST_CASE("Win32 No Sharing")
-{
-  TestContext test_context;
-
-  HANDLE handle = CreateFileA("file",
-                              GENERIC_READ | GENERIC_WRITE,
-                              0 /* no sharing */,
-                              nullptr,
-                              CREATE_NEW,
-                              FILE_ATTRIBUTE_NORMAL,
-                              nullptr);
-  REQUIRE_MESSAGE(handle != INVALID_HANDLE_VALUE, "err=" << GetLastError());
-  Finalizer cleanup([&] { CloseHandle(handle); });
-
-  // Sanity check we can't open the file for read/write access.
-  REQUIRE(!util::read_file<std::string>("file"));
-
-  SUBCASE("stat file no sharing")
-  {
-    auto stat = Stat::stat("file");
-    CHECK(stat);
-    CHECK(stat.error_number() == 0);
-    CHECK(!stat.is_directory());
-    CHECK(stat.is_regular());
-    CHECK(!stat.is_symlink());
-    CHECK(S_ISREG(stat.mode()));
-    CHECK(stat.size() == 0);
-    CHECK(!(stat.file_attributes() & FILE_ATTRIBUTE_DIRECTORY));
-    CHECK(!(stat.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT));
-    CHECK(stat.reparse_tag() == 0);
-  }
-
-  SUBCASE("lstat file no sharing")
-  {
-    auto stat = Stat::lstat("file");
-    CHECK(stat);
-    CHECK(stat.error_number() == 0);
-    CHECK(!stat.is_directory());
-    CHECK(stat.is_regular());
-    CHECK(!stat.is_symlink());
-    CHECK(S_ISREG(stat.mode()));
-    CHECK(stat.size() == 0);
-    CHECK(!(stat.file_attributes() & FILE_ATTRIBUTE_DIRECTORY));
-    CHECK(!(stat.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT));
-    CHECK(stat.reparse_tag() == 0);
-  }
-}
-
-// Creating a directory junction for test purposes is tricky on Windows.
-// Instead, test a well-known junction that has existed in all Windows versions
-// since Vista. (Not present on Wine.)
-TEST_CASE("Win32 Directory Junction"
-          * doctest::skip(!win32_is_junction(util::expand_environment_variables(
-            "${ALLUSERSPROFILE}\\Application Data"))))
-{
-  TestContext test_context;
-
-  SUBCASE("junction stat")
-  {
-    auto stat = Stat::stat(util::expand_environment_variables(
-      "${ALLUSERSPROFILE}\\Application Data"));
-    CHECK(stat);
-    CHECK(stat.error_number() == 0);
-    CHECK(stat.is_directory());
-    CHECK(!stat.is_regular());
-    CHECK(!stat.is_symlink());
-    CHECK(S_ISDIR(stat.mode()));
-    CHECK((stat.mode() & ~S_IFMT) == 0777);
-    CHECK((stat.file_attributes() & FILE_ATTRIBUTE_DIRECTORY));
-    CHECK(!(stat.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT));
-    CHECK(stat.reparse_tag() == 0);
-  }
-
-  SUBCASE("junction lstat")
-  {
-    auto stat = Stat::lstat(util::expand_environment_variables(
-      "${ALLUSERSPROFILE}\\Application Data"));
-    CHECK(stat);
-    CHECK(stat.error_number() == 0);
-    CHECK(!stat.is_directory());
-    CHECK(!stat.is_regular());
-    CHECK(!stat.is_symlink()); // Should only be true for bona fide symlinks
-    CHECK((stat.mode() & S_IFMT) == 0); // Not a symlink/file/directory
-    CHECK((stat.mode() & ~S_IFMT) == 0777);
-    CHECK((stat.file_attributes() & FILE_ATTRIBUTE_DIRECTORY));
-    CHECK((stat.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT));
-    CHECK(stat.reparse_tag() == IO_REPARSE_TAG_MOUNT_POINT);
-  }
-}
-#endif
-
-TEST_SUITE_END();
index b254c918cc9b6a8877b78ed4f5267dd3b4933758..52f182ad0ff9675ffee588fb39960fcf1e22a57d 100644 (file)
@@ -22,7 +22,6 @@
 #include "../src/fmtmacros.hpp"
 #include "TestUtil.hpp"
 
-#include <Stat.hpp>
 #include <core/exceptions.hpp>
 #include <util/environment.hpp>
 #include <util/file.hpp>
index 9c8e27ec27f238019b1996f25d926c2e22be32ab..7a972d24b79f031bf044314c25d0657be2489c58 100644 (file)
 
 #include "TestUtil.hpp"
 
-#include <Stat.hpp>
 #include <core/common.hpp>
+#include <util/DirEntry.hpp>
 #include <util/file.hpp>
 
 #include <third_party/doctest.h>
 
 using TestUtil::TestContext;
+using util::DirEntry;
 
 TEST_SUITE_BEGIN("core");
 
@@ -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 (file)
index 0000000..b5bb5dd
--- /dev/null
@@ -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 <Finalizer.hpp>
+#include <util/DirEntry.hpp>
+#include <util/environment.hpp>
+#include <util/file.hpp>
+#include <util/wincompat.hpp>
+
+#include <third_party/doctest.h>
+
+#ifdef HAVE_UNISTD_H
+#  include <unistd.h>
+#endif
+
+#ifdef _WIN32
+#  include <shlobj.h>
+#endif
+
+using TestUtil::TestContext;
+using util::DirEntry;
+
+namespace {
+
+bool
+running_under_wine()
+{
+#ifdef _WIN32
+  static bool is_wine =
+    GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "wine_get_version")
+    != nullptr;
+  return is_wine;
+#else
+  return false;
+#endif
+}
+
+bool
+symlinks_supported()
+{
+#ifdef _WIN32
+  // Windows only supports symlinks if the user has the required privilege (e.g.
+  // they're an admin) or if developer mode is enabled.
+
+  // See: https://stackoverflow.com/a/41232108/192102
+  const char* dev_mode_key =
+    "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\AppModelUnlock";
+  const char* dev_mode_value = "AllowDevelopmentWithoutDevLicense";
+
+  DWORD dev_mode_enabled = 0;
+  DWORD buf_size = sizeof(dev_mode_enabled);
+
+  return !running_under_wine()
+         && (IsUserAnAdmin()
+             || (RegGetValueA(HKEY_LOCAL_MACHINE,
+                              dev_mode_key,
+                              dev_mode_value,
+                              RRF_RT_DWORD,
+                              nullptr,
+                              &dev_mode_enabled,
+                              &buf_size)
+                   == ERROR_SUCCESS
+                 && dev_mode_enabled));
+#else
+  return true;
+#endif
+}
+
+#ifdef _WIN32
+bool
+win32_is_junction(const std::string& path)
+{
+  HANDLE handle =
+    CreateFileA(path.c_str(),
+                FILE_READ_ATTRIBUTES,
+                FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
+                nullptr,
+                OPEN_EXISTING,
+                FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
+                nullptr);
+  if (handle == INVALID_HANDLE_VALUE) {
+    return false;
+  }
+  FILE_ATTRIBUTE_TAG_INFO reparse_info = {};
+  bool is_junction =
+    (GetFileType(handle) == FILE_TYPE_DISK)
+    && GetFileInformationByHandleEx(
+      handle, FileAttributeTagInfo, &reparse_info, sizeof(reparse_info))
+    && (reparse_info.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)
+    && (reparse_info.ReparseTag == IO_REPARSE_TAG_MOUNT_POINT);
+  CloseHandle(handle);
+  return is_junction;
+}
+
+bool
+win32_get_file_info(const std::string& path, BY_HANDLE_FILE_INFORMATION* info)
+{
+  HANDLE handle =
+    CreateFileA(path.c_str(),
+                FILE_READ_ATTRIBUTES,
+                FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
+                nullptr,
+                OPEN_EXISTING,
+                FILE_FLAG_BACKUP_SEMANTICS,
+                nullptr);
+  if (handle == INVALID_HANDLE_VALUE) {
+    return false;
+  }
+  BOOL ret = GetFileInformationByHandle(handle, info);
+  CloseHandle(handle);
+  return ret;
+}
+
+struct timespec
+win32_filetime_to_timespec(FILETIME ft)
+{
+  static const int64_t SECS_BETWEEN_EPOCHS = 11644473600;
+  uint64_t v =
+    (static_cast<uint64_t>(ft.dwHighDateTime) << 32) | ft.dwLowDateTime;
+
+  struct timespec ts = {};
+  ts.tv_sec = (v / 10000000) - SECS_BETWEEN_EPOCHS;
+  ts.tv_nsec = (v % 10000000) * 100;
+  return ts;
+}
+#endif
+
+} // namespace
+
+TEST_SUITE_BEGIN("util::DirEntry");
+
+TEST_CASE("Default constructor")
+{
+  DirEntry entry;
+  CHECK(!entry);
+  CHECK(!entry.exists());
+  CHECK(entry.error_number() == ENOENT);
+  CHECK(entry.path() == "");
+  CHECK(entry.device() == 0);
+  CHECK(entry.inode() == 0);
+  CHECK(entry.mode() == 0);
+  CHECK(entry.ctime().sec() == 0);
+  CHECK(entry.ctime().nsec() == 0);
+  CHECK(entry.mtime().sec() == 0);
+  CHECK(entry.mtime().nsec() == 0);
+  CHECK(entry.size() == 0);
+  CHECK(entry.size_on_disk() == 0);
+  CHECK(!entry.is_directory());
+  CHECK(!entry.is_regular_file());
+  CHECK(!entry.is_symlink());
+
+#ifdef _WIN32
+  CHECK(entry.file_attributes() == 0);
+  CHECK(entry.reparse_tag() == 0);
+#endif
+}
+
+TEST_CASE("Construction for missing entry")
+{
+  DirEntry entry("does_not_exist");
+  CHECK(!entry);
+  CHECK(!entry.exists());
+  CHECK(entry.error_number() == ENOENT);
+  CHECK(entry.path() == "does_not_exist");
+  CHECK(entry.device() == 0);
+  CHECK(entry.inode() == 0);
+  CHECK(entry.mode() == 0);
+  CHECK(entry.ctime().sec() == 0);
+  CHECK(entry.ctime().nsec() == 0);
+  CHECK(entry.mtime().sec() == 0);
+  CHECK(entry.mtime().nsec() == 0);
+  CHECK(entry.size() == 0);
+  CHECK(entry.size_on_disk() == 0);
+  CHECK(!entry.is_directory());
+  CHECK(!entry.is_regular_file());
+  CHECK(!entry.is_symlink());
+
+#ifdef _WIN32
+  CHECK(entry.file_attributes() == 0);
+  CHECK(entry.reparse_tag() == 0);
+#endif
+}
+
+TEST_CASE("Caching and refresh")
+{
+  TestContext test_context;
+
+  util::write_file("a", "");
+
+  DirEntry entry("a");
+  CHECK(entry.size() == 0);
+
+  util::write_file("a", "123", util::InPlace::yes);
+  CHECK(entry.size() == 0);
+  entry.refresh();
+  CHECK(entry.size() == 3);
+}
+
+TEST_CASE("Same i-node as")
+{
+  TestContext test_context;
+
+  util::write_file("a", "");
+  util::write_file("b", "");
+  DirEntry entry_a("a");
+  DirEntry entry_b("b");
+
+  CHECK(entry_a.same_inode_as(entry_a));
+  CHECK(!entry_a.same_inode_as(entry_b));
+
+  util::write_file("a", "change size", util::InPlace::yes);
+  CHECK(DirEntry("a").same_inode_as(entry_a));
+
+  CHECK(!DirEntry("nonexistent").same_inode_as(DirEntry("nonexistent")));
+}
+
+TEST_CASE("Get path")
+{
+  TestContext test_context;
+
+  util::write_file("a", "");
+  CHECK(DirEntry("a").path() == "a");
+  CHECK(DirEntry("does_not_exist").path() == "does_not_exist");
+}
+
+TEST_CASE("Return values when file exists")
+{
+  TestContext test_context;
+
+  util::write_file("file", "1234567");
+
+  DirEntry de("file");
+  CHECK(de);
+  CHECK(de.exists());
+  CHECK(de.error_number() == 0);
+  CHECK(de.path() == "file");
+  CHECK(!de.is_directory());
+  CHECK(de.is_regular_file());
+  CHECK(!de.is_symlink());
+  CHECK(de.size() == 7);
+
+#ifdef _WIN32
+  BY_HANDLE_FILE_INFORMATION info = {};
+  CHECK(win32_get_file_info("file", &info));
+
+  CHECK(de.device() == info.dwVolumeSerialNumber);
+  CHECK((de.inode() >> 32) == info.nFileIndexHigh);
+  CHECK((de.inode() & ((1ULL << 32) - 1)) == info.nFileIndexLow);
+  CHECK(S_ISREG(de.mode()));
+  CHECK((de.mode() & ~S_IFMT) == 0666);
+
+  struct timespec creation_time =
+    win32_filetime_to_timespec(info.ftCreationTime);
+  struct timespec last_write_time =
+    win32_filetime_to_timespec(info.ftLastWriteTime);
+
+  CHECK(de.ctime().sec() == creation_time.tv_sec);
+  CHECK(de.ctime().nsec_decimal_part() == creation_time.tv_nsec);
+  CHECK(de.mtime().sec() == last_write_time.tv_sec);
+  CHECK(de.mtime().nsec_decimal_part() == last_write_time.tv_nsec);
+
+  CHECK(de.size_on_disk() == ((de.size() + 4095) & ~4095));
+  CHECK(de.file_attributes() == info.dwFileAttributes);
+  CHECK(de.reparse_tag() == 0);
+
+#else
+  struct stat st;
+  CHECK(::stat("file", &st) == 0);
+
+  CHECK(de.device() == st.st_dev);
+  CHECK(de.inode() == st.st_ino);
+  CHECK(de.mode() == st.st_mode);
+  CHECK(de.size_on_disk() == st.st_blocks * 512);
+
+#  ifdef HAVE_STRUCT_STAT_ST_CTIM
+  CHECK(de.ctime().sec() == st.st_ctim.tv_sec);
+  CHECK(de.ctime().nsec_decimal_part() == st.st_ctim.tv_nsec);
+#  elif defined(HAVE_STRUCT_STAT_ST_CTIMESPEC)
+  CHECK(de.ctime().sec() == st.st_ctimespec.tv_sec);
+  CHECK(de.ctime().nsec_decimal_part() == st.st_ctimespec.tv_nsec);
+#  else
+  CHECK(de.ctime().sec() == st.st_ctime);
+  CHECK(de.ctime().nsec_decimal_part() == 0);
+#  endif
+
+#  ifdef HAVE_STRUCT_STAT_ST_MTIM
+  CHECK(de.mtime().sec() == st.st_mtim.tv_sec);
+  CHECK(de.mtime().nsec_decimal_part() == st.st_mtim.tv_nsec);
+#  elif defined(HAVE_STRUCT_STAT_ST_MTIMESPEC)
+  CHECK(de.mtime().sec() == st.st_mtimespec.tv_sec);
+  CHECK(de.mtime().nsec_decimal_part() == st.st_mtimespec.tv_nsec);
+#  else
+  CHECK(de.mtime().sec() == st.st_mtime);
+  CHECK(de.mtime().nsec_decimal_part() == 0);
+#  endif
+#endif
+}
+
+TEST_CASE("Directory")
+{
+  TestContext test_context;
+
+  REQUIRE(mkdir("directory", 0456) == 0);
+  DirEntry entry("directory");
+
+  CHECK(entry);
+  CHECK(entry.exists());
+  CHECK(entry.error_number() == 0);
+  CHECK(entry.is_directory());
+  CHECK(!entry.is_regular_file());
+  CHECK(!entry.is_symlink());
+  CHECK(S_ISDIR(entry.mode()));
+#ifdef _WIN32
+  CHECK((entry.mode() & ~S_IFMT) == 0777);
+  CHECK((entry.file_attributes() & FILE_ATTRIBUTE_DIRECTORY));
+  CHECK(!(entry.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT));
+  CHECK(entry.reparse_tag() == 0);
+#endif
+}
+
+TEST_CASE("Symlinks" * doctest::skip(!symlinks_supported()))
+{
+  TestContext test_context;
+
+  util::write_file("file", "1234567");
+
+#ifdef _WIN32
+  // SYMBOLIC_LINK_FLAG_DIRECTORY: 0x1
+  // SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE: 0x2
+  REQUIRE(CreateSymbolicLinkA("symlink_to_dir", ".", 0x1 | 0x2));
+  REQUIRE(CreateSymbolicLinkA("symlink_to_file", "file", 0x2));
+  REQUIRE(CreateSymbolicLinkA("symlink_to_none", "does_not_exist", 0x2));
+#else
+  REQUIRE(symlink(".", "symlink_to_dir") == 0);
+  REQUIRE(symlink("file", "symlink_to_file") == 0);
+  REQUIRE(symlink("does_not_exist", "symlink_to_none") == 0);
+#endif
+
+  SUBCASE("symlink to dir")
+  {
+    DirEntry entry("symlink_to_dir");
+    CHECK(entry);
+    CHECK(entry.path() == "symlink_to_dir");
+    CHECK(entry.exists());
+    CHECK(entry.error_number() == 0);
+    CHECK(entry.is_directory());
+    CHECK(!entry.is_regular_file());
+    CHECK(entry.is_symlink());
+    CHECK(S_ISDIR(entry.mode()));
+#ifdef _WIN32
+    CHECK((entry.file_attributes() & FILE_ATTRIBUTE_DIRECTORY));
+    CHECK(!(entry.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT));
+    CHECK(entry.reparse_tag() == 0);
+#endif
+  }
+
+  SUBCASE("symlink to file")
+  {
+    DirEntry entry("symlink_to_file");
+    CHECK(entry);
+    CHECK(entry.exists());
+    CHECK(entry.error_number() == 0);
+    CHECK(entry.path() == "symlink_to_file");
+    CHECK(!entry.is_directory());
+    CHECK(entry.is_regular_file());
+    CHECK(entry.is_symlink());
+    CHECK(S_ISREG(entry.mode()));
+    CHECK(entry.size() == 7);
+#ifdef _WIN32
+    CHECK(!(entry.file_attributes() & FILE_ATTRIBUTE_DIRECTORY));
+    CHECK(!(entry.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT));
+    CHECK(entry.reparse_tag() == 0);
+#endif
+  }
+
+  SUBCASE("symlink to none")
+  {
+    DirEntry entry("symlink_to_none");
+    CHECK(entry);
+    CHECK(!entry.exists());
+    CHECK(entry.error_number() == 0);
+    CHECK(entry.path() == "symlink_to_none");
+    CHECK(!entry.is_directory());
+    CHECK(!entry.is_regular_file());
+    CHECK(entry.is_symlink());
+    CHECK(entry.mode() == 0);
+    CHECK(entry.size() == 0);
+#ifdef _WIN32
+    CHECK(!(entry.file_attributes() & FILE_ATTRIBUTE_DIRECTORY));
+    CHECK(!(entry.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT));
+    CHECK(entry.reparse_tag() == 0);
+#endif
+  }
+}
+
+TEST_CASE("Hard links")
+{
+  TestContext test_context;
+
+  util::write_file("a", "");
+
+#ifdef _WIN32
+  REQUIRE(CreateHardLinkA("b", "a", nullptr));
+#else
+  REQUIRE(link("a", "b") == 0);
+#endif
+
+  DirEntry entry_a("a");
+  CHECK(entry_a);
+  CHECK(entry_a.exists());
+  CHECK(entry_a.error_number() == 0);
+  CHECK(!entry_a.is_directory());
+  CHECK(entry_a.is_regular_file());
+  CHECK(!entry_a.is_symlink());
+  CHECK(entry_a.size() == 0);
+
+  DirEntry entry_b("b");
+  CHECK(entry_b.exists());
+  CHECK(entry_b);
+  CHECK(entry_b.error_number() == 0);
+  CHECK(!entry_b.is_directory());
+  CHECK(entry_b.is_regular_file());
+  CHECK(!entry_b.is_symlink());
+  CHECK(entry_b.size() == 0);
+
+  CHECK(entry_a.device() == entry_b.device());
+  CHECK(entry_a.inode() == entry_b.inode());
+  CHECK(entry_a.same_inode_as(entry_b));
+
+  util::write_file("a", "1234567", util::InPlace::yes);
+  entry_b.refresh();
+  CHECK(entry_b.size() == 7);
+}
+
+TEST_CASE("Special" * doctest::skip(running_under_wine()))
+{
+  SUBCASE("tty")
+  {
+#ifdef _WIN32
+    DirEntry entry("\\\\.\\CON");
+#else
+    DirEntry entry("/dev/tty");
+#endif
+    CHECK(entry);
+    CHECK(entry.exists());
+    CHECK(entry.error_number() == 0);
+    CHECK(!entry.is_directory());
+    CHECK(!entry.is_regular_file());
+    CHECK(!entry.is_symlink());
+    CHECK(S_ISCHR(entry.mode()));
+#ifdef _WIN32
+    CHECK(entry.file_attributes() == 0);
+    CHECK(entry.reparse_tag() == 0);
+#endif
+  }
+
+  SUBCASE("null")
+  {
+#ifdef _WIN32
+    DirEntry entry("\\\\.\\NUL");
+#else
+    DirEntry entry("/dev/null");
+#endif
+    CHECK(entry);
+    CHECK(entry.exists());
+    CHECK(entry.error_number() == 0);
+    CHECK(!entry.is_directory());
+    CHECK(!entry.is_regular_file());
+    CHECK(!entry.is_symlink());
+    CHECK(S_ISCHR(entry.mode()));
+#ifdef _WIN32
+    CHECK(entry.file_attributes() == 0);
+    CHECK(entry.reparse_tag() == 0);
+#endif
+  }
+
+  SUBCASE("pipe")
+  {
+#ifdef _WIN32
+    const char pipe_path[] = "\\\\.\\pipe\\InitShutdown"; // Well-known pipe
+#else
+    const char pipe_path[] = "my_pipe";
+    REQUIRE(mkfifo(pipe_path, 0600) == 0);
+#endif
+
+    DirEntry entry(pipe_path);
+    CHECK(entry);
+    CHECK(entry.exists());
+    CHECK(entry.error_number() == 0);
+    CHECK(!entry.is_directory());
+    CHECK(!entry.is_regular_file());
+    CHECK(!entry.is_symlink());
+    CHECK(S_ISFIFO(entry.mode()));
+#ifdef _WIN32
+    CHECK(entry.file_attributes() == 0);
+    CHECK(entry.reparse_tag() == 0);
+#endif
+  }
+
+  SUBCASE("block device")
+  {
+#ifdef _WIN32
+    DirEntry entry("\\\\.\\C:");
+    CHECK(entry);
+    CHECK(entry.exists());
+    CHECK(entry.error_number() == 0);
+    CHECK(!entry.is_directory());
+    CHECK(!entry.is_regular_file());
+    CHECK(!entry.is_symlink());
+    CHECK(S_ISBLK(entry.mode()));
+    CHECK(entry.file_attributes() == 0);
+    CHECK(entry.reparse_tag() == 0);
+#endif
+  }
+}
+
+#ifdef _WIN32
+TEST_CASE("Win32 Readonly File")
+{
+  TestContext test_context;
+
+  util::write_file("file", "");
+
+  DWORD prev_attrs = GetFileAttributesA("file");
+  REQUIRE(prev_attrs != INVALID_FILE_ATTRIBUTES);
+  REQUIRE(SetFileAttributesA("file", prev_attrs | FILE_ATTRIBUTE_READONLY));
+
+  DirEntry entry("file");
+  entry.refresh();
+  REQUIRE(SetFileAttributesA("file", prev_attrs));
+
+  CHECK(entry);
+  CHECK(entry.exists());
+  CHECK(entry.error_number() == 0);
+  CHECK(S_ISREG(entry.mode()));
+  CHECK((entry.mode() & ~S_IFMT) == 0444);
+  CHECK((entry.file_attributes() & FILE_ATTRIBUTE_READONLY));
+  CHECK(!(entry.file_attributes() & FILE_ATTRIBUTE_DIRECTORY));
+  CHECK(!(entry.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT));
+  CHECK(entry.reparse_tag() == 0);
+}
+
+TEST_CASE("Win32 Executable File")
+{
+  TestContext test_context;
+
+  const char* comspec = getenv("COMSPEC");
+  REQUIRE(comspec != nullptr);
+
+  DirEntry entry(comspec);
+  CHECK(entry);
+  CHECK(entry.exists());
+  CHECK(entry.error_number() == 0);
+  CHECK(!entry.is_directory());
+  CHECK(entry.is_regular_file());
+  CHECK(!entry.is_symlink());
+  CHECK(S_ISREG(entry.mode()));
+  CHECK((entry.mode() & ~S_IFMT) == 0777);
+  CHECK(!(entry.file_attributes() & FILE_ATTRIBUTE_DIRECTORY));
+  CHECK(!(entry.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT));
+  CHECK(entry.reparse_tag() == 0);
+}
+
+TEST_CASE("Win32 Pending Delete" * doctest::skip(running_under_wine()))
+{
+  TestContext test_context;
+
+  HANDLE handle =
+    CreateFileA("file",
+                GENERIC_READ | GENERIC_WRITE | DELETE,
+                FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
+                nullptr,
+                CREATE_NEW,
+                FILE_ATTRIBUTE_NORMAL,
+                nullptr);
+  REQUIRE_MESSAGE(handle != INVALID_HANDLE_VALUE, "err=" << GetLastError());
+  Finalizer cleanup([&] { CloseHandle(handle); });
+
+  // Mark file as deleted. This puts it into a "pending delete" state that
+  // will persist until the handle is closed. Until the file is closed, new
+  // handles cannot be created to the file; attempts to do so fail with
+  // ERROR_ACCESS_DENIED/STATUS_DELETE_PENDING. Our stat implementation maps
+  // these to ENOENT.
+  FILE_DISPOSITION_INFO info{};
+  info.DeleteFile = TRUE;
+  REQUIRE_MESSAGE(SetFileInformationByHandle(
+                    handle, FileDispositionInfo, &info, sizeof(info)),
+                  "err=" << GetLastError());
+
+  DirEntry entry("file");
+  CHECK(!entry);
+  CHECK(!entry.exists());
+  CHECK(entry.error_number() == ENOENT);
+}
+
+// Our Win32 Stat implementation should open files using FILE_READ_ATTRIBUTES,
+// which bypasses sharing restrictions.
+TEST_CASE("Win32 No Sharing")
+{
+  TestContext test_context;
+
+  HANDLE handle = CreateFileA("file",
+                              GENERIC_READ | GENERIC_WRITE,
+                              0 /* no sharing */,
+                              nullptr,
+                              CREATE_NEW,
+                              FILE_ATTRIBUTE_NORMAL,
+                              nullptr);
+  REQUIRE_MESSAGE(handle != INVALID_HANDLE_VALUE, "err=" << GetLastError());
+  Finalizer cleanup([&] { CloseHandle(handle); });
+
+  // Sanity check we can't open the file for read/write access.
+  REQUIRE(!util::read_file<std::string>("file"));
+
+  DirEntry entry("file");
+  CHECK(entry);
+  CHECK(entry.exists());
+  CHECK(entry.error_number() == 0);
+  CHECK(!entry.is_directory());
+  CHECK(entry.is_regular_file());
+  CHECK(!entry.is_symlink());
+  CHECK(S_ISREG(entry.mode()));
+  CHECK(entry.size() == 0);
+  CHECK(!(entry.file_attributes() & FILE_ATTRIBUTE_DIRECTORY));
+  CHECK(!(entry.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT));
+  CHECK(entry.reparse_tag() == 0);
+}
+
+// Creating a directory junction for test purposes is tricky on Windows.
+// Instead, test a well-known junction that has existed in all Windows versions
+// since Vista. (Not present on Wine.)
+TEST_CASE("Win32 Directory Junction"
+          * doctest::skip(!win32_is_junction(util::expand_environment_variables(
+            "${ALLUSERSPROFILE}\\Application Data"))))
+{
+  TestContext test_context;
+
+  DirEntry entry(
+    util::expand_environment_variables("${ALLUSERSPROFILE}\\Application Data"));
+  CHECK(entry);
+  CHECK(entry.exists());
+  CHECK(entry.error_number() == 0);
+  CHECK(entry.is_directory());
+  CHECK(!entry.is_regular_file());
+  CHECK(entry.is_symlink());
+  CHECK(S_ISDIR(entry.mode()));
+  CHECK((entry.mode() & ~S_IFMT) == 0777);
+  CHECK((entry.file_attributes() & FILE_ATTRIBUTE_DIRECTORY));
+  CHECK(!(entry.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT));
+  CHECK(entry.reparse_tag() == 0);
+}
+#endif
+
+TEST_SUITE_END();
index c57aa5d6249d5717864f1d2c60938c09dfef9628..d93fc5d87c9d8e8ba7df899f088ca46a6f0a77db 100644 (file)
 // this program; if not, write to the Free Software Foundation, Inc., 51
 // Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
-#include "../src/Stat.hpp"
 #include "TestUtil.hpp"
 
 #include <Util.hpp>
+#include <util/DirEntry.hpp>
 #include <util/LockFile.hpp>
 #include <util/file.hpp>
 #include <util/wincompat.hpp>
@@ -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
index 9e85441c7098cb8175ae4ac06f3497311c5b147c..798ac88f1980fefe621044c93935f3e37eec6bee 100644 (file)
@@ -19,9 +19,9 @@
 #include "TestUtil.hpp"
 
 #include <Fd.hpp>
-#include <Stat.hpp>
 #include <fmtmacros.hpp>
 #include <util/Bytes.hpp>
+#include <util/DirEntry.hpp>
 #include <util/file.hpp>
 #include <util/filesystem.hpp>
 #include <util/string.hpp>
@@ -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")
index 307a03db328c53f31696b91957bf7a688a9ab85d..e7643019b7135273b9bc78d71dd74ddddb183e06 100644 (file)
@@ -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());