fs.h raw

   1  // Copyright (c) 2017-present The Limenka developers
   2  // Distributed under the MIT software license, see the accompanying
   3  // file COPYING or http://www.opensource.org/licenses/mit-license.php.
   4  
   5  #ifndef LIMENKA_UTIL_FS_H
   6  #define LIMENKA_UTIL_FS_H
   7  
   8  #include <tinyformat.h>
   9  
  10  #include <cstdio>
  11  #include <filesystem> // IWYU pragma: export
  12  #include <functional>
  13  #include <iomanip>
  14  #include <ios>
  15  #include <ostream>
  16  #include <string>
  17  #include <system_error>
  18  #include <type_traits>
  19  #include <utility>
  20  
  21  /** Filesystem operations and types */
  22  namespace fs {
  23  
  24  using namespace std::filesystem;
  25  
  26  /**
  27   * Path class wrapper to block calls to the fs::path(std::string) implicit
  28   * constructor and the fs::path::string() method, which have unsafe and
  29   * unpredictable behavior on Windows (see implementation note in
  30   * \ref PathToString for details)
  31   */
  32  class path : public std::filesystem::path
  33  {
  34  public:
  35      using std::filesystem::path::path;
  36  
  37      // Allow path objects arguments for compatibility.
  38      path(std::filesystem::path path) : std::filesystem::path::path(std::move(path)) {}
  39      path& operator=(std::filesystem::path path) { std::filesystem::path::operator=(std::move(path)); return *this; }
  40      path& operator/=(const std::filesystem::path& path) { std::filesystem::path::operator/=(path); return *this; }
  41  
  42      // Allow literal string arguments, which are safe as long as the literals are ASCII.
  43      path(const char* c) : std::filesystem::path(c) {}
  44      path& operator=(const char* c) { std::filesystem::path::operator=(c); return *this; }
  45      path& operator/=(const char* c) { std::filesystem::path::operator/=(c); return *this; }
  46      path& append(const char* c) { std::filesystem::path::append(c); return *this; }
  47  
  48      // Disallow std::string arguments to avoid locale-dependent decoding on windows.
  49      path(std::string) = delete;
  50      path& operator=(std::string) = delete;
  51      path& operator/=(std::string) = delete;
  52      path& append(std::string) = delete;
  53  
  54      // Disallow std::string conversion method to avoid locale-dependent encoding on windows.
  55      std::string string() const = delete;
  56  
  57      /**
  58       * Return a UTF-8 representation of the path as a std::string, for
  59       * compatibility with code using std::string. For code using the newer
  60       * std::u8string type, it is more efficient to call the inherited
  61       * std::filesystem::path::u8string method instead.
  62       */
  63      std::string utf8string() const
  64      {
  65          const std::u8string& utf8_str{std::filesystem::path::u8string()};
  66          return std::string{utf8_str.begin(), utf8_str.end()};
  67      }
  68  
  69      // Required for path overloads in <fstream>.
  70      // See https://gcc.gnu.org/git/?p=gcc.git;a=commit;h=96e0367ead5d8dcac3bec2865582e76e2fbab190
  71      path& make_preferred() { std::filesystem::path::make_preferred(); return *this; }
  72      path filename() const { return std::filesystem::path::filename(); }
  73  };
  74  
  75  static inline path u8path(const std::string& utf8_str)
  76  {
  77      return std::filesystem::path(std::u8string{utf8_str.begin(), utf8_str.end()});
  78  }
  79  
  80  // Disallow implicit std::string conversion for absolute to avoid
  81  // locale-dependent encoding on windows.
  82  static inline path absolute(const path& p)
  83  {
  84      return std::filesystem::absolute(p);
  85  }
  86  
  87  // Disallow implicit std::string conversion for exists to avoid
  88  // locale-dependent encoding on windows.
  89  static inline bool exists(const path& p)
  90  {
  91      return std::filesystem::exists(p);
  92  }
  93  static inline bool exists(const std::filesystem::file_status& s)
  94  {
  95      return std::filesystem::exists(s);
  96  }
  97  
  98  // Allow explicit quoted stream I/O.
  99  static inline auto quoted(const std::string& s)
 100  {
 101      return std::quoted(s, '"', '&');
 102  }
 103  
 104  // Allow safe path append operations.
 105  static inline path operator/(path p1, const path& p2)
 106  {
 107      p1 /= p2;
 108      return p1;
 109  }
 110  static inline path operator/(path p1, const char* p2)
 111  {
 112      p1 /= p2;
 113      return p1;
 114  }
 115  static inline path operator+(path p1, const char* p2)
 116  {
 117      p1 += p2;
 118      return p1;
 119  }
 120  static inline path operator+(path p1, path::value_type p2)
 121  {
 122      p1 += p2;
 123      return p1;
 124  }
 125  
 126  // Disallow unsafe path append operations.
 127  template<typename T> static inline path operator/(path p1, T p2) = delete;
 128  template<typename T> static inline path operator+(path p1, T p2) = delete;
 129  
 130  // Disallow implicit std::string conversion for copy_file
 131  // to avoid locale-dependent encoding on Windows.
 132  static inline bool copy_file(const path& from, const path& to, copy_options options)
 133  {
 134      return std::filesystem::copy_file(from, to, options);
 135  }
 136  
 137  /**
 138   * Convert path object to a byte string. On POSIX, paths natively are byte
 139   * strings, so this is trivial. On Windows, paths natively are Unicode, so an
 140   * encoding step is necessary. The inverse of \ref PathToString is \ref
 141   * PathFromString. The strings returned and parsed by these functions can be
 142   * used to call POSIX APIs, and for roundtrip conversion, logging, and
 143   * debugging.
 144   *
 145   * Because \ref PathToString and \ref PathFromString functions don't specify an
 146   * encoding, they are meant to be used internally, not externally. They are not
 147   * appropriate to use in applications requiring UTF-8, where
 148   * fs::path::u8string() / fs::path::utf8string() and fs::u8path() methods should be used instead. Other
 149   * applications could require still different encodings. For example, JSON, XML,
 150   * or URI applications might prefer to use higher-level escapes (\uXXXX or
 151   * &XXXX; or %XX) instead of multibyte encoding. Rust, Python, Java applications
 152   * may require encoding paths with their respective UTF-8 derivatives WTF-8,
 153   * PEP-383, and CESU-8 (see https://en.wikipedia.org/wiki/UTF-8#Derivatives).
 154   */
 155  static inline std::string PathToString(const path& path)
 156  {
 157      // Implementation note: On Windows, the std::filesystem::path(string)
 158      // constructor and std::filesystem::path::string() method are not safe to
 159      // use here, because these methods encode the path using C++'s narrow
 160      // multibyte encoding, which on Windows corresponds to the current "code
 161      // page", which is unpredictable and typically not able to represent all
 162      // valid paths. So fs::path::utf8string() and
 163      // fs::u8path() functions are used instead on Windows. On
 164      // POSIX, u8string/utf8string/u8path functions are not safe to use because paths are
 165      // not always valid UTF-8, so plain string methods which do not transform
 166      // the path there are used.
 167  #ifdef WIN32
 168      return path.utf8string();
 169  #else
 170      static_assert(std::is_same<path::string_type, std::string>::value, "PathToString not implemented on this platform");
 171      return path.std::filesystem::path::string();
 172  #endif
 173  }
 174  
 175  /**
 176   * Convert byte string to path object. Inverse of \ref PathToString.
 177   */
 178  static inline path PathFromString(const std::string& string)
 179  {
 180  #ifdef WIN32
 181      return u8path(string);
 182  #else
 183      return std::filesystem::path(string);
 184  #endif
 185  }
 186  
 187  /**
 188   * Create directory (and if necessary its parents), unless the leaf directory
 189   * already exists or is a symlink to an existing directory.
 190   * This is a temporary workaround for an issue in libstdc++ that has been fixed
 191   * upstream [PR101510].
 192   * https://gcc.gnu.org/bugzilla/show_bug.cgi?id=101510
 193   */
 194  static inline bool create_directories(const std::filesystem::path& p)
 195  {
 196      if (std::filesystem::is_symlink(p) && std::filesystem::is_directory(p)) {
 197          return false;
 198      }
 199      return std::filesystem::create_directories(p);
 200  }
 201  
 202  /**
 203   * This variant is not used. Delete it to prevent it from accidentally working
 204   * around the workaround. If it is needed, add a workaround in the same pattern
 205   * as above.
 206   */
 207  bool create_directories(const std::filesystem::path& p, std::error_code& ec) = delete;
 208  
 209  } // namespace fs
 210  
 211  /** Bridge operations to C stdio */
 212  namespace fsbridge {
 213      using FopenFn = std::function<FILE*(const fs::path&, const char*)>;
 214      FILE *fopen(const fs::path& p, const char *mode);
 215  
 216      /**
 217       * Helper function for joining two paths
 218       *
 219       * @param[in] base  Base path
 220       * @param[in] path  Path to combine with base
 221       * @returns path unchanged if it is an absolute path, otherwise returns base joined with path. Returns base unchanged if path is empty.
 222       * @pre  Base path must be absolute
 223       * @post Returned path will always be absolute
 224       */
 225      fs::path AbsPathJoin(const fs::path& base, const fs::path& path);
 226  
 227      class FileLock
 228      {
 229      public:
 230          FileLock() = delete;
 231          FileLock(const FileLock&) = delete;
 232          FileLock(FileLock&&) = delete;
 233          explicit FileLock(const fs::path& file);
 234          ~FileLock();
 235          bool TryLock();
 236          std::string GetReason() { return reason; }
 237  
 238      private:
 239          std::string reason;
 240  #ifndef WIN32
 241          int fd = -1;
 242  #else
 243          void* hFile = (void*)-1; // INVALID_HANDLE_VALUE
 244  #endif
 245      };
 246  
 247      std::string get_filesystem_error_message(const fs::filesystem_error& e);
 248  };
 249  
 250  // Disallow path operator<< formatting in tinyformat to avoid locale-dependent
 251  // encoding on windows.
 252  namespace tinyformat {
 253  template<> inline void formatValue(std::ostream&, const char*, const char*, int, const std::filesystem::path&) = delete;
 254  template<> inline void formatValue(std::ostream&, const char*, const char*, int, const fs::path&) = delete;
 255  } // namespace tinyformat
 256  
 257  #endif // LIMENKA_UTIL_FS_H
 258