fs_helpers.cpp raw
1 // Copyright (c) 2009-2010 Satoshi Nakamoto
2 // Copyright (c) 2009-2023 The Limenka developers
3 // Distributed under the MIT software license, see the accompanying
4 // file COPYING or http://www.opensource.org/licenses/mit-license.php.
5
6 #include <util/fs_helpers.h>
7
8 #include <limenka-build-config.h> // IWYU pragma: keep
9
10 #include <logging.h>
11 #include <random.h>
12 #include <sync.h>
13 #include <tinyformat.h>
14 #include <util/check.h>
15 #include <util/fs.h>
16 #include <util/syserror.h>
17
18 #include <cerrno>
19 #include <fstream>
20 #include <limits>
21 #include <map>
22 #include <memory>
23 #include <optional>
24 #include <stdexcept>
25 #include <string>
26 #include <system_error>
27 #include <utility>
28
29 #ifndef WIN32
30 // for posix_fallocate, in cmake/introspection.cmake we check if it is present after this
31 #ifdef __linux__
32
33 #ifdef _POSIX_C_SOURCE
34 #undef _POSIX_C_SOURCE
35 #endif
36
37 #define _POSIX_C_SOURCE 200112L
38
39 #endif // __linux__
40
41 #include <fcntl.h>
42 #include <sys/resource.h>
43 #include <unistd.h>
44 #else
45 #include <io.h> /* For _get_osfhandle, _chsize */
46 #include <shlobj.h> /* For SHGetSpecialFolderPathW */
47 #include <windows.h>
48 #endif // WIN32
49
50 /** Mutex to protect dir_locks. */
51 static GlobalMutex cs_dir_locks;
52 /** A map that contains all the currently held directory locks. After
53 * successful locking, these will be held here until the global destructor
54 * cleans them up and thus automatically unlocks them, or ReleaseDirectoryLocks
55 * is called.
56 */
57 static std::map<std::string, std::unique_ptr<fsbridge::FileLock>> dir_locks GUARDED_BY(cs_dir_locks);
58 namespace util {
59 LockResult LockDirectory(const fs::path& directory, const fs::path& lockfile_name, bool probe_only)
60 {
61 LOCK(cs_dir_locks);
62 fs::path pathLockFile = directory / lockfile_name;
63
64 // If a lock for this directory already exists in the map, don't try to re-lock it
65 if (dir_locks.count(fs::PathToString(pathLockFile))) {
66 return LockResult::Success;
67 }
68
69 // Create empty lock file if it doesn't exist.
70 if (auto created{fsbridge::fopen(pathLockFile, "a")}) {
71 std::fclose(created);
72 } else {
73 return LockResult::ErrorWrite;
74 }
75 auto lock = std::make_unique<fsbridge::FileLock>(pathLockFile);
76 if (!lock->TryLock()) {
77 LogError("Error while attempting to lock directory %s: %s\n", fs::PathToString(directory), lock->GetReason());
78 return LockResult::ErrorLock;
79 }
80 if (!probe_only) {
81 // Lock successful and we're not just probing, put it into the map
82 dir_locks.emplace(fs::PathToString(pathLockFile), std::move(lock));
83 }
84 return LockResult::Success;
85 }
86 } // namespace util
87 void UnlockDirectory(const fs::path& directory, const fs::path& lockfile_name)
88 {
89 LOCK(cs_dir_locks);
90 dir_locks.erase(fs::PathToString(directory / lockfile_name));
91 }
92
93 void ReleaseDirectoryLocks()
94 {
95 LOCK(cs_dir_locks);
96 dir_locks.clear();
97 }
98
99 bool CheckDiskSpace(const fs::path& dir, uint64_t additional_bytes)
100 {
101 constexpr uint64_t min_disk_space = 52428800; // 50 MiB
102
103 uint64_t free_bytes_available = fs::space(dir).available;
104 return free_bytes_available >= min_disk_space + additional_bytes;
105 }
106
107 std::streampos GetFileSize(const char* path, std::streamsize max)
108 {
109 std::ifstream file{path, std::ios::binary};
110 file.ignore(max);
111 return file.gcount();
112 }
113
114 bool FileCommit(FILE* file)
115 {
116 if (fflush(file) != 0) { // harmless if redundantly called
117 LogError("fflush failed: %s", SysErrorString(errno));
118 return false;
119 }
120 #ifdef WIN32
121 HANDLE hFile = (HANDLE)_get_osfhandle(_fileno(file));
122 if (FlushFileBuffers(hFile) == 0) {
123 LogError("FlushFileBuffers failed: %s", Win32ErrorString(GetLastError()));
124 return false;
125 }
126 #elif defined(__APPLE__) && defined(F_FULLFSYNC)
127 if (fcntl(fileno(file), F_FULLFSYNC, 0) == -1) { // Manpage says "value other than -1" is returned on success
128 LogError("fcntl F_FULLFSYNC failed: %s", SysErrorString(errno));
129 return false;
130 }
131 #elif HAVE_FDATASYNC
132 if (fdatasync(fileno(file)) != 0 && errno != EINVAL) { // Ignore EINVAL for filesystems that don't support sync
133 LogError("fdatasync failed: %s", SysErrorString(errno));
134 return false;
135 }
136 #else
137 if (fsync(fileno(file)) != 0 && errno != EINVAL) {
138 LogError("fsync failed: %s", SysErrorString(errno));
139 return false;
140 }
141 #endif
142 return true;
143 }
144
145 void DirectoryCommit(const fs::path& dirname)
146 {
147 #ifndef WIN32
148 FILE* file = fsbridge::fopen(dirname, "r");
149 if (file) {
150 fsync(fileno(file));
151 fclose(file);
152 }
153 #endif
154 }
155
156 bool TruncateFile(FILE* file, unsigned int length)
157 {
158 #if defined(WIN32)
159 return _chsize(_fileno(file), length) == 0;
160 #else
161 return ftruncate(fileno(file), length) == 0;
162 #endif
163 }
164
165 /**
166 * this function tries to raise the file descriptor limit to the requested number.
167 * It returns the actual file descriptor limit (which may be more or less than nMinFD)
168 */
169 int RaiseFileDescriptorLimit(int nMinFD)
170 {
171 Assert(nMinFD >= 0);
172 #if defined(WIN32)
173 return 2048;
174 #else
175 struct rlimit limitFD;
176 if (getrlimit(RLIMIT_NOFILE, &limitFD) != -1) {
177 // If the current soft limit is already higher, don't raise it
178 if (limitFD.rlim_cur != RLIM_INFINITY && std::cmp_less(limitFD.rlim_cur, nMinFD)) {
179 const auto current_limit{limitFD.rlim_cur};
180 limitFD.rlim_cur = std::in_range<rlim_t>(nMinFD) ? static_cast<rlim_t>(nMinFD) : limitFD.rlim_max;
181 // Don't raise soft limit beyond hard limit
182 if (limitFD.rlim_max != RLIM_INFINITY && (
183 limitFD.rlim_cur > limitFD.rlim_max
184 )
185 ) {
186 limitFD.rlim_cur = limitFD.rlim_max;
187 }
188 if (current_limit != limitFD.rlim_cur) {
189 setrlimit(RLIMIT_NOFILE, &limitFD);
190 getrlimit(RLIMIT_NOFILE, &limitFD);
191 }
192 }
193 // Check the (possibly raised) current soft limit against the special
194 // value of RLIM_INFINITY. Some platforms implement this as the maximum
195 // uint64, others as int64 (-1). Avoid casting even if the return type
196 // is changed to uint64_t. We also cap unlikely but possible values
197 // that would overflow int.
198 if (limitFD.rlim_cur == RLIM_INFINITY ||
199 std::cmp_greater_equal(limitFD.rlim_cur, std::numeric_limits<int>::max())) {
200 return std::numeric_limits<int>::max();
201 }
202 return static_cast<int>(limitFD.rlim_cur);
203 }
204 return nMinFD; // getrlimit failed, assume it's fine
205 #endif
206 }
207
208 /**
209 * this function tries to make a particular range of a file allocated (corresponding to disk space)
210 * it is advisory, and the range specified in the arguments will never contain live data
211 */
212 void AllocateFileRange(FILE* file, unsigned int offset, unsigned int length)
213 {
214 #if defined(WIN32)
215 // Windows-specific version
216 HANDLE hFile = (HANDLE)_get_osfhandle(_fileno(file));
217 LARGE_INTEGER nFileSize;
218 int64_t nEndPos = (int64_t)offset + length;
219 if (GetFileSizeEx(hFile, &nFileSize) && (int64_t{nFileSize.u.HighPart} << 32 | nFileSize.u.LowPart) <= nEndPos) {
220 nFileSize.u.LowPart = nEndPos & 0xFFFFFFFF;
221 nFileSize.u.HighPart = nEndPos >> 32;
222 if (SetFilePointerEx(hFile, nFileSize, 0, FILE_BEGIN)) {
223 SetEndOfFile(hFile);
224 }
225 }
226 #elif 0
227 // OSX specific version
228 // NOTE: Contrary to other OS versions, the OSX version assumes that
229 // NOTE: offset is the size of the file.
230 fstore_t fst;
231 fst.fst_flags = F_ALLOCATECONTIG;
232 fst.fst_posmode = F_PEOFPOSMODE;
233 fst.fst_offset = 0;
234 fst.fst_length = length; // mac os fst_length takes the # of free bytes to allocate, not desired file size
235 fst.fst_bytesalloc = 0;
236 if (fcntl(fileno(file), F_PREALLOCATE, &fst) == -1) {
237 fst.fst_flags = F_ALLOCATEALL;
238 fcntl(fileno(file), F_PREALLOCATE, &fst);
239 }
240 ftruncate(fileno(file), static_cast<off_t>(offset) + length);
241 #else
242 #if defined(HAVE_POSIX_FALLOCATE)
243 // Version using posix_fallocate
244 if (0 == posix_fallocate(fileno(file), offset, length)) return;
245 #endif
246 // Fallback version
247 // TODO: just write one byte per block
248 uint8_t buf[65536];
249 if (fseek(file, offset, SEEK_SET)) {
250 return;
251 }
252 clearerr(file);
253 while (length > 0) {
254 unsigned int now = 65536;
255 if (length < now)
256 now = length;
257 const size_t rlen = fread(buf, 1, now, file);
258 if (rlen < now) {
259 if (ferror(file)) {
260 // Don't clobber anything, just give up
261 clearerr(file);
262 return;
263 }
264 memset(&buf[rlen], 0, now - rlen);
265 if (0 != fseek(file, -rlen, SEEK_CUR)) {
266 return;
267 }
268 }
269 fwrite(buf, 1, now, file); // allowed to fail; this function is advisory anyway
270 length -= now;
271 }
272 #endif
273 }
274
275 FILE* AdviseSequential(FILE *file) {
276 #ifdef _POSIX_C_SOURCE
277 # if _POSIX_C_SOURCE >= 200112L
278 // Since this whole thing is advisory anyway, we can ignore any errors
279 // encountered up to and including the posix_fadvise call. However, we must
280 // rewind the file to the appropriate position if we've changed the seek
281 // offset.
282 if (file == nullptr) {
283 return nullptr;
284 }
285 const int fd = fileno(file);
286 if (fd == -1) {
287 return file;
288 }
289 const off_t start = lseek(fd, 0, SEEK_CUR);
290 if (start == -1) {
291 return file;
292 }
293 posix_fadvise(fd, start, 0, POSIX_FADV_WILLNEED);
294 posix_fadvise(fd, start, 0, POSIX_FADV_SEQUENTIAL);
295 # endif
296 #endif
297 return file;
298 }
299
300 int CloseAndUncache(FILE *file) {
301 #ifdef _POSIX_C_SOURCE
302 # if _POSIX_C_SOURCE >= 200112L
303 // Ignore any errors up to and including the posix_fadvise call since it's
304 // advisory.
305 if (file != nullptr) {
306 const int fd = fileno(file);
307 if (fd != -1) {
308 const off_t end = lseek(fd, 0, SEEK_END);
309 if (end != (off_t)-1) {
310 posix_fadvise(fd, 0, end, POSIX_FADV_DONTNEED);
311 }
312 }
313 }
314 # endif
315 #endif
316 return std::fclose(file);
317 }
318
319 #ifdef WIN32
320 fs::path GetSpecialFolderPath(int nFolder, bool fCreate)
321 {
322 WCHAR pszPath[MAX_PATH] = L"";
323
324 if (SHGetSpecialFolderPathW(nullptr, pszPath, nFolder, fCreate)) {
325 return fs::path(pszPath);
326 }
327
328 LogError("SHGetSpecialFolderPathW() failed, could not obtain requested path.");
329 return fs::path("");
330 }
331 #endif
332
333 bool RenameOver(fs::path src, fs::path dest)
334 {
335 std::error_code error;
336 fs::rename(src, dest, error);
337 return !error;
338 }
339
340 /**
341 * Ignores exceptions thrown by create_directories if the requested directory exists.
342 * Specifically handles case where path p exists, but it wasn't possible for the user to
343 * write to the parent directory.
344 */
345 bool TryCreateDirectories(const fs::path& p)
346 {
347 try {
348 return fs::create_directories(p);
349 } catch (const fs::filesystem_error&) {
350 if (!fs::exists(p) || !fs::is_directory(p))
351 throw;
352 }
353
354 // create_directories didn't create the directory, it had to have existed already
355 return false;
356 }
357
358 std::string PermsToSymbolicString(fs::perms p)
359 {
360 std::string perm_str(9, '-');
361
362 auto set_perm = [&](size_t pos, fs::perms required_perm, char letter, char else_letter = '\0') {
363 if ((p & required_perm) != fs::perms::none) {
364 perm_str[pos] = letter;
365 } else if (else_letter) {
366 perm_str[pos] = else_letter;
367 }
368 };
369
370 set_perm(0, fs::perms::owner_read, 'r');
371 set_perm(1, fs::perms::owner_write, 'w');
372 if ((p & fs::perms::owner_exec) != fs::perms::none) {
373 set_perm(2, fs::perms::set_uid, 's', 'x');
374 } else {
375 set_perm(2, fs::perms::set_uid, 'S');
376 }
377
378 set_perm(3, fs::perms::group_read, 'r');
379 set_perm(4, fs::perms::group_write, 'w');
380 if ((p & fs::perms::group_exec) != fs::perms::none) {
381 set_perm(5, fs::perms::set_gid, 's', 'x');
382 } else {
383 set_perm(5, fs::perms::set_gid, 'S');
384 }
385
386 set_perm(6, fs::perms::others_read, 'r');
387 set_perm(7, fs::perms::others_write, 'w');
388 if ((p & fs::perms::others_exec) != fs::perms::none) {
389 set_perm(8, fs::perms::sticky_bit, 't', 'x');
390 } else {
391 set_perm(8, fs::perms::sticky_bit, 'T');
392 }
393
394 return perm_str;
395 }
396
397 static std::optional<unsigned> StringToOctal(const std::string& str)
398 {
399 unsigned ret = 0;
400 for (char c : str) {
401 if (c < '0' || c > '7') return std::nullopt;
402 ret = (ret << 3) | (c - '0');
403 }
404 return ret;
405 }
406
407 static auto ConvertPermsToOctal(const std::string& str) noexcept -> std::optional<unsigned>
408 {
409 if ((str.length() == 3) || (str.length() == 4)) return StringToOctal(str);
410 return std::nullopt;
411 }
412
413 std::optional<fs::perms> InterpretPermString(const std::string& s)
414 {
415 if (s == "owner") {
416 return fs::perms::owner_read | fs::perms::owner_write;
417 } else if (s == "group") {
418 return fs::perms::owner_read | fs::perms::owner_write |
419 fs::perms::group_read;
420 } else if (s == "all") {
421 return fs::perms::owner_read | fs::perms::owner_write |
422 fs::perms::group_read |
423 fs::perms::others_read;
424 } else if (auto octal_perms = ConvertPermsToOctal(s)) {
425 return static_cast<fs::perms>(*octal_perms);
426 } else {
427 return std::nullopt;
428 }
429 }
430
431 bool IsDirWritable(const fs::path& dir_path)
432 {
433 // Attempt to create a tmp file in the directory
434 if (!fs::is_directory(dir_path)) throw std::runtime_error(strprintf("Path %s is not a directory", fs::PathToString(dir_path)));
435 FastRandomContext rng;
436 const auto tmp = dir_path / fs::PathFromString(strprintf(".tmp_%d", rng.rand64()));
437
438 if (const auto created{fsbridge::fopen(tmp, "wbx")}) {
439 std::fclose(created);
440 std::error_code ec;
441 fs::remove(tmp, ec); // clean up, ignore errors
442 return true;
443 }
444 return false;
445 }
446
447 bool IsSymlink(const fs::path& path)
448 {
449 #ifdef WIN32
450 DWORD file_attrs = GetFileAttributesW(path.wstring().c_str());
451 if (file_attrs == INVALID_FILE_ATTRIBUTES) {
452 throw fs::filesystem_error("Unable to get file attributes", fs::PathToString(path), std::make_error_code(std::errc::invalid_argument));
453 }
454 return (file_attrs & FILE_ATTRIBUTE_REPARSE_POINT) != 0;
455 #else
456 return fs::is_symlink(path);
457 #endif
458 }
459