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