file_system.cpp (72384B)
1 // SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com> 2 // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) 3 4 #include "file_system.h" 5 #include "assert.h" 6 #include "error.h" 7 #include "log.h" 8 #include "path.h" 9 #include "string_util.h" 10 #include "timer.h" 11 12 #include <algorithm> 13 #include <cstdlib> 14 #include <cstring> 15 #include <limits> 16 #include <numeric> 17 18 #ifdef __APPLE__ 19 #include <mach-o/dyld.h> 20 #include <stdlib.h> 21 #include <sys/param.h> 22 #endif 23 24 #ifdef __FreeBSD__ 25 #include <sys/sysctl.h> 26 #endif 27 28 #if defined(_WIN32) 29 #include "windows_headers.h" 30 #include <io.h> 31 #include <malloc.h> 32 #include <pathcch.h> 33 #include <share.h> 34 #include <shlobj.h> 35 #include <winioctl.h> 36 #else 37 #include <dirent.h> 38 #include <errno.h> 39 #include <fcntl.h> 40 #include <limits.h> 41 #include <sys/stat.h> 42 #include <sys/types.h> 43 #include <unistd.h> 44 #endif 45 46 Log_SetChannel(FileSystem); 47 48 #ifndef __ANDROID__ 49 50 #ifdef _WIN32 51 static std::time_t ConvertFileTimeToUnixTime(const FILETIME& ft) 52 { 53 // based off https://stackoverflow.com/a/6161842 54 static constexpr s64 WINDOWS_TICK = 10000000; 55 static constexpr s64 SEC_TO_UNIX_EPOCH = 11644473600LL; 56 57 const s64 full = static_cast<s64>((static_cast<u64>(ft.dwHighDateTime) << 32) | static_cast<u64>(ft.dwLowDateTime)); 58 return static_cast<std::time_t>(full / WINDOWS_TICK - SEC_TO_UNIX_EPOCH); 59 } 60 template<class T> 61 static bool IsUNCPath(const T& path) 62 { 63 return (path.length() >= 3 && path[0] == '\\' && path[1] == '\\'); 64 } 65 #endif 66 67 static inline bool FileSystemCharacterIsSane(char32_t c, bool strip_slashes) 68 { 69 #ifdef _WIN32 70 // https://docs.microsoft.com/en-gb/windows/win32/fileio/naming-a-file?redirectedfrom=MSDN#naming-conventions 71 if ((c == U'/' || c == U'\\') && strip_slashes) 72 return false; 73 74 if (c == U'<' || c == U'>' || c == U':' || c == U'"' || c == U'|' || c == U'?' || c == U'*' || c == 0 || 75 c <= static_cast<char32_t>(31)) 76 { 77 return false; 78 } 79 #else 80 if (c == '/' && strip_slashes) 81 return false; 82 83 // drop asterisks too, they make globbing annoying 84 if (c == '*') 85 return false; 86 87 // macos doesn't allow colons, apparently 88 #ifdef __APPLE__ 89 if (c == U':') 90 return false; 91 #endif 92 #endif 93 94 return true; 95 } 96 97 template<typename T> 98 static inline void PathAppendString(std::string& dst, const T& src) 99 { 100 if (dst.capacity() < (dst.length() + src.length())) 101 dst.reserve(dst.length() + src.length()); 102 103 bool last_separator = (!dst.empty() && dst.back() == FS_OSPATH_SEPARATOR_CHARACTER); 104 105 size_t index = 0; 106 107 #ifdef _WIN32 108 // special case for UNC paths here 109 if (dst.empty() && IsUNCPath(src)) 110 { 111 dst.append("\\\\"); 112 index = 2; 113 } 114 #endif 115 116 for (; index < src.length(); index++) 117 { 118 const char ch = src[index]; 119 120 #ifdef _WIN32 121 // convert forward slashes to backslashes 122 if (ch == '\\' || ch == '/') 123 #else 124 if (ch == '/') 125 #endif 126 { 127 if (last_separator) 128 continue; 129 last_separator = true; 130 dst.push_back(FS_OSPATH_SEPARATOR_CHARACTER); 131 } 132 else 133 { 134 last_separator = false; 135 dst.push_back(ch); 136 } 137 } 138 } 139 140 std::string Path::SanitizeFileName(std::string_view str, bool strip_slashes /* = true */) 141 { 142 std::string ret; 143 ret.reserve(str.length()); 144 145 size_t pos = 0; 146 while (pos < str.length()) 147 { 148 char32_t ch; 149 pos += StringUtil::DecodeUTF8(str, pos, &ch); 150 ch = FileSystemCharacterIsSane(ch, strip_slashes) ? ch : U'_'; 151 StringUtil::EncodeAndAppendUTF8(ret, ch); 152 } 153 154 #ifdef _WIN32 155 // Windows: Can't end filename with a period. 156 if (ret.length() > 0 && ret.back() == '.') 157 ret.back() = '_'; 158 #endif 159 160 return ret; 161 } 162 163 void Path::SanitizeFileName(std::string* str, bool strip_slashes /* = true */) 164 { 165 const size_t len = str->length(); 166 167 char small_buf[128]; 168 std::unique_ptr<char[]> large_buf; 169 char* str_copy = small_buf; 170 if (len >= std::size(small_buf)) 171 { 172 large_buf = std::make_unique<char[]>(len + 1); 173 str_copy = large_buf.get(); 174 } 175 std::memcpy(str_copy, str->c_str(), sizeof(char) * (len + 1)); 176 str->clear(); 177 178 size_t pos = 0; 179 while (pos < len) 180 { 181 char32_t ch; 182 pos += StringUtil::DecodeUTF8(str_copy + pos, pos - len, &ch); 183 ch = FileSystemCharacterIsSane(ch, strip_slashes) ? ch : U'_'; 184 StringUtil::EncodeAndAppendUTF8(*str, ch); 185 } 186 187 #ifdef _WIN32 188 // Windows: Can't end filename with a period. 189 if (str->length() > 0 && str->back() == '.') 190 str->back() = '_'; 191 #endif 192 } 193 194 std::string Path::RemoveLengthLimits(std::string_view str) 195 { 196 std::string ret; 197 #ifdef _WIN32 198 ret.reserve(str.length() + (IsUNCPath(str) ? 4 : 6)); 199 #endif 200 ret.append(str); 201 RemoveLengthLimits(&ret); 202 return ret; 203 } 204 205 void Path::RemoveLengthLimits(std::string* path) 206 { 207 DebugAssert(IsAbsolute(*path)); 208 #ifdef _WIN32 209 // Any forward slashes should be backslashes. 210 for (char& ch : *path) 211 ch = (ch == '/') ? '\\' : ch; 212 213 if (IsUNCPath(*path)) 214 { 215 // \\server\path => \\?\UNC\server\path 216 DebugAssert((*path)[0] == '\\' && (*path)[1] == '\\'); 217 path->erase(0, 2); 218 path->insert(0, "\\\\?\\UNC\\"); 219 } 220 else 221 { 222 // C:\file => \\?\C:\file 223 path->insert(0, "\\\\?\\"); 224 } 225 #endif 226 } 227 228 #ifdef _WIN32 229 230 bool FileSystem::GetWin32Path(std::wstring* dest, std::string_view str) 231 { 232 // Just convert to wide if it's a relative path, MAX_PATH still applies. 233 if (!Path::IsAbsolute(str)) 234 return StringUtil::UTF8StringToWideString(*dest, str); 235 236 // PathCchCanonicalizeEx() thankfully takes care of everything. 237 // But need to widen the string first, avoid the stack allocation. 238 int wlen = MultiByteToWideChar(CP_UTF8, 0, str.data(), static_cast<int>(str.length()), nullptr, 0); 239 if (wlen <= 0) [[unlikely]] 240 return false; 241 242 // So copy it to a temp wide buffer first. 243 wchar_t* wstr_buf = static_cast<wchar_t*>(_malloca(sizeof(wchar_t) * (static_cast<size_t>(wlen) + 1))); 244 wlen = MultiByteToWideChar(CP_UTF8, 0, str.data(), static_cast<int>(str.length()), wstr_buf, wlen); 245 if (wlen <= 0) [[unlikely]] 246 { 247 _freea(wstr_buf); 248 return false; 249 } 250 251 // And use PathCchCanonicalizeEx() to fix up any non-direct elements. 252 wstr_buf[wlen] = '\0'; 253 dest->resize(std::max<size_t>(static_cast<size_t>(wlen) + (IsUNCPath(str) ? 9 : 5), 16)); 254 for (;;) 255 { 256 const HRESULT hr = 257 PathCchCanonicalizeEx(dest->data(), dest->size(), wstr_buf, PATHCCH_ENSURE_IS_EXTENDED_LENGTH_PATH); 258 if (SUCCEEDED(hr)) 259 { 260 dest->resize(std::wcslen(dest->data())); 261 _freea(wstr_buf); 262 return true; 263 } 264 else if (hr == HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER)) 265 { 266 dest->resize(dest->size() * 2); 267 continue; 268 } 269 else [[unlikely]] 270 { 271 ERROR_LOG("PathCchCanonicalizeEx() returned {:08X}", static_cast<unsigned>(hr)); 272 _freea(wstr_buf); 273 return false; 274 } 275 } 276 } 277 278 std::wstring FileSystem::GetWin32Path(std::string_view str) 279 { 280 std::wstring ret; 281 if (!GetWin32Path(&ret, str)) 282 ret.clear(); 283 return ret; 284 } 285 286 #endif 287 288 bool Path::IsAbsolute(std::string_view path) 289 { 290 #ifdef _WIN32 291 return (path.length() >= 3 && ((path[0] >= 'A' && path[0] <= 'Z') || (path[0] >= 'a' && path[0] <= 'z')) && 292 path[1] == ':' && (path[2] == '/' || path[2] == '\\')) || 293 IsUNCPath(path); 294 #else 295 return (path.length() >= 1 && path[0] == '/'); 296 #endif 297 } 298 299 std::string Path::RealPath(std::string_view path) 300 { 301 // Resolve non-absolute paths first. 302 std::vector<std::string_view> components; 303 if (!IsAbsolute(path)) 304 components = Path::SplitNativePath(Path::Combine(FileSystem::GetWorkingDirectory(), path)); 305 else 306 components = Path::SplitNativePath(path); 307 308 std::string realpath; 309 if (components.empty()) 310 return realpath; 311 312 // Different to path because relative. 313 realpath.reserve(std::accumulate(components.begin(), components.end(), static_cast<size_t>(0), 314 [](size_t l, const std::string_view& s) { return l + s.length(); }) + 315 components.size() + 1); 316 317 #ifdef _WIN32 318 std::wstring wrealpath; 319 std::vector<WCHAR> symlink_buf; 320 wrealpath.reserve(realpath.size()); 321 symlink_buf.resize(path.size() + 1); 322 323 // Check for any symbolic links throughout the path while adding components. 324 const bool skip_first = IsUNCPath(path); 325 bool test_symlink = true; 326 for (const std::string_view& comp : components) 327 { 328 if (!realpath.empty()) 329 { 330 realpath.push_back(FS_OSPATH_SEPARATOR_CHARACTER); 331 realpath.append(comp); 332 } 333 else if (skip_first) 334 { 335 realpath.append(comp); 336 continue; 337 } 338 else 339 { 340 realpath.append(comp); 341 } 342 if (test_symlink) 343 { 344 DWORD attribs; 345 if (FileSystem::GetWin32Path(&wrealpath, realpath) && 346 (attribs = GetFileAttributesW(wrealpath.c_str())) != INVALID_FILE_ATTRIBUTES) 347 { 348 // if not a link, go to the next component 349 if (attribs & FILE_ATTRIBUTE_REPARSE_POINT) 350 { 351 const HANDLE hFile = 352 CreateFileW(wrealpath.c_str(), FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, 353 nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); 354 if (hFile != INVALID_HANDLE_VALUE) 355 { 356 // is a link! resolve it. 357 DWORD ret = GetFinalPathNameByHandleW(hFile, symlink_buf.data(), static_cast<DWORD>(symlink_buf.size()), 358 FILE_NAME_NORMALIZED); 359 if (ret > symlink_buf.size()) 360 { 361 symlink_buf.resize(ret); 362 ret = GetFinalPathNameByHandleW(hFile, symlink_buf.data(), static_cast<DWORD>(symlink_buf.size()), 363 FILE_NAME_NORMALIZED); 364 } 365 if (ret != 0) 366 StringUtil::WideStringToUTF8String(realpath, std::wstring_view(symlink_buf.data(), ret)); 367 else 368 test_symlink = false; 369 370 CloseHandle(hFile); 371 } 372 } 373 } 374 else 375 { 376 // not a file or link 377 test_symlink = false; 378 } 379 } 380 } 381 382 // GetFinalPathNameByHandleW() adds a \\?\ prefix, so remove it. 383 if (realpath.starts_with("\\\\?\\") && IsAbsolute(std::string_view(realpath.data() + 4, realpath.size() - 4))) 384 { 385 realpath.erase(0, 4); 386 } 387 else if (realpath.starts_with("\\\\?\\UNC\\")) 388 { 389 realpath.erase(0, 7); 390 realpath.insert(realpath.begin(), '\\'); 391 } 392 393 #else 394 // Why this monstrosity instead of calling realpath()? realpath() only works on files that exist. 395 std::string basepath; 396 std::string symlink; 397 398 basepath.reserve(realpath.capacity()); 399 symlink.resize(realpath.capacity()); 400 401 // Check for any symbolic links throughout the path while adding components. 402 bool test_symlink = true; 403 for (const std::string_view& comp : components) 404 { 405 if (!test_symlink) 406 { 407 realpath.push_back(FS_OSPATH_SEPARATOR_CHARACTER); 408 realpath.append(comp); 409 continue; 410 } 411 412 basepath = realpath; 413 if (realpath.empty() || realpath.back() != FS_OSPATH_SEPARATOR_CHARACTER) 414 realpath.push_back(FS_OSPATH_SEPARATOR_CHARACTER); 415 realpath.append(comp); 416 417 // Check if the last component added is a symlink 418 struct stat sb; 419 if (lstat(realpath.c_str(), &sb) != 0) 420 { 421 // Don't bother checking any further components once we error out. 422 test_symlink = false; 423 continue; 424 } 425 else if (!S_ISLNK(sb.st_mode)) 426 { 427 // Nope, keep going. 428 continue; 429 } 430 431 for (;;) 432 { 433 ssize_t sz = readlink(realpath.c_str(), symlink.data(), symlink.size()); 434 if (sz < 0) 435 { 436 // shouldn't happen, due to the S_ISLNK check above. 437 test_symlink = false; 438 break; 439 } 440 else if (static_cast<size_t>(sz) == symlink.size()) 441 { 442 // need a larger buffer 443 symlink.resize(symlink.size() * 2); 444 continue; 445 } 446 else 447 { 448 // is a link, and we resolved it. gotta check if the symlink itself is relative :( 449 symlink.resize(static_cast<size_t>(sz)); 450 if (!Path::IsAbsolute(symlink)) 451 { 452 // symlink is relative to the directory of the symlink 453 realpath = basepath; 454 if (realpath.empty() || realpath.back() != FS_OSPATH_SEPARATOR_CHARACTER) 455 realpath.push_back(FS_OSPATH_SEPARATOR_CHARACTER); 456 realpath.append(symlink); 457 } 458 else 459 { 460 // Use the new, symlinked path. 461 realpath = symlink; 462 } 463 464 break; 465 } 466 } 467 } 468 #endif 469 470 // Get rid of any current/parent directory components before returning. 471 // This should be fine on Linux, since any symbolic links have already replaced the leading portion. 472 Path::Canonicalize(&realpath); 473 474 return realpath; 475 } 476 477 std::string Path::ToNativePath(std::string_view path) 478 { 479 std::string ret; 480 PathAppendString(ret, path); 481 482 // remove trailing slashes 483 if (ret.length() > 1) 484 { 485 while (ret.back() == FS_OSPATH_SEPARATOR_CHARACTER) 486 ret.pop_back(); 487 } 488 489 return ret; 490 } 491 492 void Path::ToNativePath(std::string* path) 493 { 494 *path = Path::ToNativePath(*path); 495 } 496 497 std::string Path::Canonicalize(std::string_view path) 498 { 499 std::vector<std::string_view> components = Path::SplitNativePath(path); 500 std::vector<std::string_view> new_components; 501 new_components.reserve(components.size()); 502 for (const std::string_view& component : components) 503 { 504 if (component == ".") 505 { 506 // current directory, so it can be skipped, unless it's the only component 507 if (components.size() == 1) 508 new_components.push_back(component); 509 } 510 else if (component == "..") 511 { 512 // parent directory, pop one off if we're not at the beginning, otherwise preserve. 513 if (!new_components.empty()) 514 new_components.pop_back(); 515 else 516 new_components.push_back(component); 517 } 518 else 519 { 520 // anything else, preserve 521 new_components.push_back(component); 522 } 523 } 524 525 return Path::JoinNativePath(new_components); 526 } 527 528 void Path::Canonicalize(std::string* path) 529 { 530 *path = Canonicalize(*path); 531 } 532 533 std::string Path::MakeRelative(std::string_view path, std::string_view relative_to) 534 { 535 // simple algorithm, we just work on the components. could probably be better, but it'll do for now. 536 std::vector<std::string_view> path_components(SplitNativePath(path)); 537 std::vector<std::string_view> relative_components(SplitNativePath(relative_to)); 538 std::vector<std::string_view> new_components; 539 540 // both must be absolute paths 541 if (Path::IsAbsolute(path) && Path::IsAbsolute(relative_to)) 542 { 543 // find the number of same components 544 size_t num_same = 0; 545 for (size_t i = 0; i < path_components.size() && i < relative_components.size(); i++) 546 { 547 if (path_components[i] == relative_components[i]) 548 num_same++; 549 else 550 break; 551 } 552 553 // we need at least one same component 554 if (num_same > 0) 555 { 556 // from the relative_to directory, back up to the start of the common components 557 const size_t num_ups = relative_components.size() - num_same; 558 for (size_t i = 0; i < num_ups; i++) 559 new_components.emplace_back(".."); 560 561 // and add the remainder of the path components 562 for (size_t i = num_same; i < path_components.size(); i++) 563 new_components.push_back(std::move(path_components[i])); 564 } 565 else 566 { 567 // no similarity 568 new_components = std::move(path_components); 569 } 570 } 571 else 572 { 573 // not absolute 574 new_components = std::move(path_components); 575 } 576 577 return JoinNativePath(new_components); 578 } 579 580 std::string_view Path::GetExtension(std::string_view path) 581 { 582 const std::string_view::size_type pos = path.rfind('.'); 583 if (pos == std::string_view::npos) 584 return std::string_view(); 585 else 586 return path.substr(pos + 1); 587 } 588 589 std::string_view Path::StripExtension(std::string_view path) 590 { 591 const std::string_view::size_type pos = path.rfind('.'); 592 if (pos == std::string_view::npos) 593 return path; 594 595 return path.substr(0, pos); 596 } 597 598 std::string Path::ReplaceExtension(std::string_view path, std::string_view new_extension) 599 { 600 const std::string_view::size_type pos = path.rfind('.'); 601 if (pos == std::string_view::npos) 602 return std::string(path); 603 604 std::string ret(path, 0, pos + 1); 605 ret.append(new_extension); 606 return ret; 607 } 608 609 static std::string_view::size_type GetLastSeperatorPosition(std::string_view filename, bool include_separator) 610 { 611 std::string_view::size_type last_separator = filename.rfind('/'); 612 if (include_separator && last_separator != std::string_view::npos) 613 last_separator++; 614 615 #if defined(_WIN32) 616 std::string_view::size_type other_last_separator = filename.rfind('\\'); 617 if (other_last_separator != std::string_view::npos) 618 { 619 if (include_separator) 620 other_last_separator++; 621 if (last_separator == std::string_view::npos || other_last_separator > last_separator) 622 last_separator = other_last_separator; 623 } 624 #endif 625 626 return last_separator; 627 } 628 629 std::string FileSystem::GetDisplayNameFromPath(std::string_view path) 630 { 631 return std::string(Path::GetFileName(path)); 632 } 633 634 std::string_view Path::GetDirectory(std::string_view path) 635 { 636 const std::string::size_type pos = GetLastSeperatorPosition(path, false); 637 if (pos == std::string_view::npos) 638 return {}; 639 640 return path.substr(0, pos); 641 } 642 643 std::string_view Path::GetFileName(std::string_view path) 644 { 645 const std::string_view::size_type pos = GetLastSeperatorPosition(path, true); 646 if (pos == std::string_view::npos) 647 return path; 648 649 return path.substr(pos); 650 } 651 652 std::string_view Path::GetFileTitle(std::string_view path) 653 { 654 const std::string_view filename(GetFileName(path)); 655 const std::string::size_type pos = filename.rfind('.'); 656 if (pos == std::string_view::npos) 657 return filename; 658 659 return filename.substr(0, pos); 660 } 661 662 std::string Path::ChangeFileName(std::string_view path, std::string_view new_filename) 663 { 664 std::string ret; 665 PathAppendString(ret, path); 666 667 const std::string_view::size_type pos = GetLastSeperatorPosition(ret, true); 668 if (pos == std::string_view::npos) 669 { 670 ret.clear(); 671 PathAppendString(ret, new_filename); 672 } 673 else 674 { 675 if (!new_filename.empty()) 676 { 677 ret.erase(pos); 678 PathAppendString(ret, new_filename); 679 } 680 else 681 { 682 ret.erase(pos - 1); 683 } 684 } 685 686 return ret; 687 } 688 689 void Path::ChangeFileName(std::string* path, std::string_view new_filename) 690 { 691 *path = ChangeFileName(*path, new_filename); 692 } 693 694 std::string Path::AppendDirectory(std::string_view path, std::string_view new_dir) 695 { 696 std::string ret; 697 if (!new_dir.empty()) 698 { 699 const std::string_view::size_type pos = GetLastSeperatorPosition(path, true); 700 701 ret.reserve(path.length() + new_dir.length() + 1); 702 if (pos != std::string_view::npos) 703 PathAppendString(ret, path.substr(0, pos)); 704 705 while (!ret.empty() && ret.back() == FS_OSPATH_SEPARATOR_CHARACTER) 706 ret.pop_back(); 707 708 if (!ret.empty()) 709 ret += FS_OSPATH_SEPARATOR_CHARACTER; 710 711 PathAppendString(ret, new_dir); 712 713 if (pos != std::string_view::npos) 714 { 715 const std::string_view filepart(path.substr(pos)); 716 if (!filepart.empty()) 717 { 718 ret += FS_OSPATH_SEPARATOR_CHARACTER; 719 PathAppendString(ret, filepart); 720 } 721 } 722 else if (!path.empty()) 723 { 724 ret += FS_OSPATH_SEPARATOR_CHARACTER; 725 PathAppendString(ret, path); 726 } 727 } 728 else 729 { 730 PathAppendString(ret, path); 731 } 732 733 return ret; 734 } 735 736 void Path::AppendDirectory(std::string* path, std::string_view new_dir) 737 { 738 *path = AppendDirectory(*path, new_dir); 739 } 740 741 std::vector<std::string_view> Path::SplitWindowsPath(std::string_view path) 742 { 743 std::vector<std::string_view> parts; 744 745 std::string::size_type start = 0; 746 std::string::size_type pos = 0; 747 748 // preserve unc paths 749 if (path.size() > 2 && path[0] == '\\' && path[1] == '\\') 750 pos = 2; 751 752 while (pos < path.size()) 753 { 754 if (path[pos] != '/' && path[pos] != '\\') 755 { 756 pos++; 757 continue; 758 } 759 760 // skip consecutive separators 761 if (pos != start) 762 parts.push_back(path.substr(start, pos - start)); 763 764 pos++; 765 start = pos; 766 } 767 768 if (start != pos) 769 parts.push_back(path.substr(start)); 770 771 return parts; 772 } 773 774 std::string Path::JoinWindowsPath(const std::vector<std::string_view>& components) 775 { 776 return StringUtil::JoinString(components.begin(), components.end(), '\\'); 777 } 778 779 std::vector<std::string_view> Path::SplitNativePath(std::string_view path) 780 { 781 #ifdef _WIN32 782 return SplitWindowsPath(path); 783 #else 784 std::vector<std::string_view> parts; 785 786 std::string::size_type start = 0; 787 std::string::size_type pos = 0; 788 while (pos < path.size()) 789 { 790 if (path[pos] != '/') 791 { 792 pos++; 793 continue; 794 } 795 796 // skip consecutive separators 797 // for unix, we create an empty element at the beginning when it's an absolute path 798 // that way, when it's re-joined later, we preserve the starting slash. 799 if (pos != start || pos == 0) 800 parts.push_back(path.substr(start, pos - start)); 801 802 pos++; 803 start = pos; 804 } 805 806 if (start != pos) 807 parts.push_back(path.substr(start)); 808 809 return parts; 810 #endif 811 } 812 813 std::string Path::JoinNativePath(const std::vector<std::string_view>& components) 814 { 815 return StringUtil::JoinString(components.begin(), components.end(), FS_OSPATH_SEPARATOR_CHARACTER); 816 } 817 818 std::vector<std::string> FileSystem::GetRootDirectoryList() 819 { 820 std::vector<std::string> results; 821 822 #if defined(_WIN32) 823 char buf[256]; 824 const DWORD size = GetLogicalDriveStringsA(sizeof(buf), buf); 825 if (size != 0 && size < (sizeof(buf) - 1)) 826 { 827 const char* ptr = buf; 828 while (*ptr != '\0') 829 { 830 const std::size_t len = std::strlen(ptr); 831 results.emplace_back(ptr, len); 832 ptr += len + 1u; 833 } 834 } 835 #else 836 const char* home_path = std::getenv("HOME"); 837 if (home_path) 838 results.push_back(home_path); 839 840 results.push_back("/"); 841 #endif 842 843 return results; 844 } 845 846 std::string Path::BuildRelativePath(std::string_view filename, std::string_view new_filename) 847 { 848 std::string new_string; 849 850 std::string_view::size_type pos = GetLastSeperatorPosition(filename, true); 851 if (pos != std::string_view::npos) 852 new_string.assign(filename, 0, pos); 853 new_string.append(new_filename); 854 return new_string; 855 } 856 857 std::string Path::Combine(std::string_view base, std::string_view next) 858 { 859 std::string ret; 860 ret.reserve(base.length() + next.length() + 1); 861 862 PathAppendString(ret, base); 863 while (!ret.empty() && ret.back() == FS_OSPATH_SEPARATOR_CHARACTER) 864 ret.pop_back(); 865 866 ret += FS_OSPATH_SEPARATOR_CHARACTER; 867 PathAppendString(ret, next); 868 while (!ret.empty() && ret.back() == FS_OSPATH_SEPARATOR_CHARACTER) 869 ret.pop_back(); 870 871 return ret; 872 } 873 874 std::string Path::URLEncode(std::string_view str) 875 { 876 std::string ret; 877 ret.reserve(str.length() + ((str.length() + 3) / 4) * 3); 878 879 for (size_t i = 0, l = str.size(); i < l; i++) 880 { 881 const char c = str[i]; 882 if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '-' || c == '_' || 883 c == '.' || c == '!' || c == '~' || c == '*' || c == '\'' || c == '(' || c == ')') 884 { 885 ret.push_back(c); 886 } 887 else 888 { 889 ret.push_back('%'); 890 891 const unsigned char n1 = static_cast<unsigned char>(c) >> 4; 892 const unsigned char n2 = static_cast<unsigned char>(c) & 0x0F; 893 ret.push_back((n1 >= 10) ? ('a' + (n1 - 10)) : ('0' + n1)); 894 ret.push_back((n2 >= 10) ? ('a' + (n2 - 10)) : ('0' + n2)); 895 } 896 } 897 898 return ret; 899 } 900 901 std::string Path::URLDecode(std::string_view str) 902 { 903 std::string ret; 904 ret.reserve(str.length()); 905 906 for (size_t i = 0, l = str.size(); i < l; i++) 907 { 908 const char c = str[i]; 909 if (c == '+') 910 { 911 ret.push_back(c); 912 } 913 else if (c == '%') 914 { 915 if ((i + 2) >= str.length()) 916 break; 917 918 const char clower = str[i + 1]; 919 const char cupper = str[i + 2]; 920 const unsigned char lower = 921 (clower >= '0' && clower <= '9') ? 922 static_cast<unsigned char>(clower - '0') : 923 ((clower >= 'a' && clower <= 'f') ? 924 static_cast<unsigned char>(clower - 'a') : 925 ((clower >= 'A' && clower <= 'F') ? static_cast<unsigned char>(clower - 'A') : 0)); 926 const unsigned char upper = 927 (cupper >= '0' && cupper <= '9') ? 928 static_cast<unsigned char>(cupper - '0') : 929 ((cupper >= 'a' && cupper <= 'f') ? 930 static_cast<unsigned char>(cupper - 'a') : 931 ((cupper >= 'A' && cupper <= 'F') ? static_cast<unsigned char>(cupper - 'A') : 0)); 932 const char dch = static_cast<char>(lower | (upper << 4)); 933 ret.push_back(dch); 934 } 935 else 936 { 937 ret.push_back(c); 938 } 939 } 940 941 return std::string(str); 942 } 943 944 std::string Path::CreateFileURL(std::string_view path) 945 { 946 DebugAssert(IsAbsolute(path)); 947 948 std::string ret; 949 ret.reserve(path.length() + 10); 950 ret.append("file://"); 951 952 const std::vector<std::string_view> components = SplitNativePath(path); 953 Assert(!components.empty()); 954 955 const std::string_view& first = components.front(); 956 #ifdef _WIN32 957 // Windows doesn't urlencode the drive letter. 958 // UNC paths should be omit the leading slash. 959 if (first.starts_with("\\\\")) 960 { 961 // file://hostname/... 962 ret.append(first.substr(2)); 963 } 964 else 965 { 966 // file:///c:/... 967 fmt::format_to(std::back_inserter(ret), "/{}", first); 968 } 969 #else 970 // Don't append a leading slash for the first component. 971 ret.append(first); 972 #endif 973 974 for (size_t comp = 1; comp < components.size(); comp++) 975 { 976 fmt::format_to(std::back_inserter(ret), "/{}", URLEncode(components[comp])); 977 } 978 979 return ret; 980 } 981 982 std::FILE* FileSystem::OpenCFile(const char* filename, const char* mode, Error* error) 983 { 984 #ifdef _WIN32 985 const std::wstring wfilename = GetWin32Path(filename); 986 const std::wstring wmode = StringUtil::UTF8StringToWideString(mode); 987 if (!wfilename.empty() && !wmode.empty()) 988 { 989 std::FILE* fp; 990 const errno_t err = _wfopen_s(&fp, wfilename.c_str(), wmode.c_str()); 991 if (err != 0) 992 { 993 Error::SetErrno(error, err); 994 return nullptr; 995 } 996 997 return fp; 998 } 999 1000 std::FILE* fp; 1001 const errno_t err = fopen_s(&fp, filename, mode); 1002 if (err != 0) 1003 { 1004 Error::SetErrno(error, err); 1005 return nullptr; 1006 } 1007 1008 return fp; 1009 #else 1010 std::FILE* fp = std::fopen(filename, mode); 1011 if (!fp) 1012 Error::SetErrno(error, errno); 1013 return fp; 1014 #endif 1015 } 1016 1017 std::FILE* FileSystem::OpenExistingOrCreateCFile(const char* filename, s32 retry_ms, Error* error /*= nullptr*/) 1018 { 1019 #ifdef _WIN32 1020 const std::wstring wfilename = GetWin32Path(filename); 1021 if (wfilename.empty()) 1022 { 1023 Error::SetStringView(error, "Invalid path."); 1024 return nullptr; 1025 } 1026 1027 HANDLE file = CreateFileW(wfilename.c_str(), GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, NULL); 1028 1029 // if there's a sharing violation, keep retrying 1030 if (file == INVALID_HANDLE_VALUE && GetLastError() == ERROR_SHARING_VIOLATION && retry_ms >= 0) 1031 { 1032 Common::Timer timer; 1033 while (retry_ms == 0 || timer.GetTimeMilliseconds() <= retry_ms) 1034 { 1035 Sleep(1); 1036 file = CreateFileW(wfilename.c_str(), GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, NULL); 1037 if (file != INVALID_HANDLE_VALUE || GetLastError() != ERROR_SHARING_VIOLATION) 1038 break; 1039 } 1040 } 1041 1042 if (file == INVALID_HANDLE_VALUE && GetLastError() == ERROR_FILE_NOT_FOUND) 1043 { 1044 // try creating it 1045 file = CreateFileW(wfilename.c_str(), GENERIC_READ | GENERIC_WRITE, 0, nullptr, CREATE_NEW, 0, NULL); 1046 if (file == INVALID_HANDLE_VALUE && GetLastError() == ERROR_FILE_EXISTS) 1047 { 1048 // someone else beat us in the race, try again with existing. 1049 file = CreateFileW(wfilename.c_str(), GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, NULL); 1050 } 1051 } 1052 1053 // done? 1054 if (file == INVALID_HANDLE_VALUE) 1055 { 1056 Error::SetWin32(error, "CreateFile() failed: ", GetLastError()); 1057 return nullptr; 1058 } 1059 1060 // convert to C FILE 1061 const int fd = _open_osfhandle(reinterpret_cast<intptr_t>(file), 0); 1062 if (fd < 0) 1063 { 1064 Error::SetErrno(error, "_open_osfhandle() failed: ", errno); 1065 CloseHandle(file); 1066 return nullptr; 1067 } 1068 1069 // convert to a stream 1070 std::FILE* cfile = _fdopen(fd, "r+b"); 1071 if (!cfile) 1072 { 1073 Error::SetErrno(error, "_fdopen() failed: ", errno); 1074 _close(fd); 1075 } 1076 1077 return cfile; 1078 #else 1079 std::FILE* fp = std::fopen(filename, "r+b"); 1080 if (fp) 1081 return fp; 1082 1083 // don't try creating for any error other than "not exist" 1084 if (errno != ENOENT) 1085 { 1086 Error::SetErrno(error, errno); 1087 return nullptr; 1088 } 1089 1090 // try again, but create the file. mode "x" exists on all platforms. 1091 fp = std::fopen(filename, "w+bx"); 1092 if (fp) 1093 return fp; 1094 1095 // if it already exists, someone else beat us in the race. try again with existing. 1096 if (errno == EEXIST) 1097 fp = std::fopen(filename, "r+b"); 1098 if (!fp) 1099 { 1100 Error::SetErrno(error, errno); 1101 return nullptr; 1102 } 1103 1104 return fp; 1105 #endif 1106 } 1107 1108 int FileSystem::OpenFDFile(const char* filename, int flags, int mode, Error* error) 1109 { 1110 #ifdef _WIN32 1111 const std::wstring wfilename(GetWin32Path(filename)); 1112 if (!wfilename.empty()) 1113 return _wopen(wfilename.c_str(), flags, mode); 1114 1115 return -1; 1116 #else 1117 const int fd = open(filename, flags, mode); 1118 if (fd < 0) 1119 Error::SetErrno(error, errno); 1120 return fd; 1121 #endif 1122 } 1123 1124 std::FILE* FileSystem::OpenSharedCFile(const char* filename, const char* mode, FileShareMode share_mode, Error* error) 1125 { 1126 #ifdef _WIN32 1127 const std::wstring wfilename = GetWin32Path(filename); 1128 const std::wstring wmode = StringUtil::UTF8StringToWideString(mode); 1129 if (wfilename.empty() || wmode.empty()) 1130 return nullptr; 1131 1132 int share_flags = 0; 1133 switch (share_mode) 1134 { 1135 case FileShareMode::DenyNone: 1136 share_flags = _SH_DENYNO; 1137 break; 1138 case FileShareMode::DenyRead: 1139 share_flags = _SH_DENYRD; 1140 break; 1141 case FileShareMode::DenyWrite: 1142 share_flags = _SH_DENYWR; 1143 break; 1144 case FileShareMode::DenyReadWrite: 1145 default: 1146 share_flags = _SH_DENYRW; 1147 break; 1148 } 1149 1150 std::FILE* fp = _wfsopen(wfilename.c_str(), wmode.c_str(), share_flags); 1151 if (fp) 1152 return fp; 1153 1154 Error::SetErrno(error, errno); 1155 return nullptr; 1156 #else 1157 std::FILE* fp = std::fopen(filename, mode); 1158 if (!fp) 1159 Error::SetErrno(error, errno); 1160 return fp; 1161 #endif 1162 } 1163 1164 FileSystem::AtomicRenamedFileDeleter::AtomicRenamedFileDeleter(std::string temp_filename, std::string final_filename) 1165 : m_temp_filename(std::move(temp_filename)), m_final_filename(std::move(final_filename)) 1166 { 1167 } 1168 1169 FileSystem::AtomicRenamedFileDeleter::~AtomicRenamedFileDeleter() = default; 1170 1171 void FileSystem::AtomicRenamedFileDeleter::operator()(std::FILE* fp) 1172 { 1173 if (!fp) 1174 return; 1175 1176 Error error; 1177 if (std::fclose(fp) != 0) 1178 { 1179 error.SetErrno(errno); 1180 ERROR_LOG("Failed to close temporary file '{}', discarding.", Path::GetFileName(m_temp_filename)); 1181 m_final_filename.clear(); 1182 } 1183 1184 // final filename empty => discarded. 1185 if (m_final_filename.empty()) 1186 { 1187 if (!DeleteFile(m_temp_filename.c_str(), &error)) 1188 ERROR_LOG("Failed to delete temporary file '{}': {}", Path::GetFileName(m_temp_filename), error.GetDescription()); 1189 } 1190 else 1191 { 1192 if (!RenamePath(m_temp_filename.c_str(), m_final_filename.c_str(), &error)) 1193 ERROR_LOG("Failed to rename temporary file '{}': {}", Path::GetFileName(m_temp_filename), error.GetDescription()); 1194 } 1195 } 1196 1197 void FileSystem::AtomicRenamedFileDeleter::discard() 1198 { 1199 m_final_filename = {}; 1200 } 1201 1202 FileSystem::AtomicRenamedFile FileSystem::CreateAtomicRenamedFile(std::string filename, const char* mode, 1203 Error* error /*= nullptr*/) 1204 { 1205 std::string temp_filename; 1206 std::FILE* fp = nullptr; 1207 if (!filename.empty()) 1208 { 1209 // this is disgusting, but we need null termination, and std::string::data() does not guarantee it. 1210 const size_t filename_length = filename.length(); 1211 const size_t name_buf_size = filename_length + 8; 1212 std::unique_ptr<char[]> name_buf = std::make_unique<char[]>(name_buf_size); 1213 std::memcpy(name_buf.get(), filename.c_str(), filename_length); 1214 StringUtil::Strlcpy(name_buf.get() + filename_length, ".XXXXXX", name_buf_size); 1215 1216 #ifdef _WIN32 1217 _mktemp_s(name_buf.get(), name_buf_size); 1218 #elif defined(__linux__) || defined(__ANDROID__) || defined(__APPLE__) 1219 mkstemp(name_buf.get()); 1220 #else 1221 mktemp(name_buf.get()); 1222 #endif 1223 1224 fp = OpenCFile(name_buf.get(), mode, error); 1225 if (fp) 1226 temp_filename.assign(name_buf.get(), name_buf_size - 1); 1227 else 1228 filename.clear(); 1229 } 1230 1231 return AtomicRenamedFile(fp, AtomicRenamedFileDeleter(std::move(temp_filename), std::move(filename))); 1232 } 1233 1234 bool FileSystem::WriteAtomicRenamedFile(std::string filename, const void* data, size_t data_length, 1235 Error* error /*= nullptr*/) 1236 { 1237 AtomicRenamedFile fp = CreateAtomicRenamedFile(std::move(filename), "wb", error); 1238 if (!fp) 1239 return false; 1240 1241 if (data_length > 0 && std::fwrite(data, 1u, data_length, fp.get()) != data_length) [[unlikely]] 1242 { 1243 Error::SetErrno(error, "fwrite() failed: ", errno); 1244 DiscardAtomicRenamedFile(fp); 1245 return false; 1246 } 1247 1248 return true; 1249 } 1250 1251 void FileSystem::DiscardAtomicRenamedFile(AtomicRenamedFile& file) 1252 { 1253 file.get_deleter().discard(); 1254 } 1255 1256 #endif 1257 1258 FileSystem::ManagedCFilePtr FileSystem::OpenManagedCFile(const char* filename, const char* mode, Error* error) 1259 { 1260 return ManagedCFilePtr(OpenCFile(filename, mode, error)); 1261 } 1262 1263 FileSystem::ManagedCFilePtr FileSystem::OpenExistingOrCreateManagedCFile(const char* filename, s32 retry_ms, 1264 Error* error) 1265 { 1266 return ManagedCFilePtr(OpenExistingOrCreateCFile(filename, retry_ms, error)); 1267 } 1268 1269 FileSystem::ManagedCFilePtr FileSystem::OpenManagedSharedCFile(const char* filename, const char* mode, 1270 FileShareMode share_mode, Error* error) 1271 { 1272 return ManagedCFilePtr(OpenSharedCFile(filename, mode, share_mode, error)); 1273 } 1274 1275 int FileSystem::FSeek64(std::FILE* fp, s64 offset, int whence) 1276 { 1277 #ifdef _WIN32 1278 return _fseeki64(fp, offset, whence); 1279 #else 1280 // Prevent truncation on platforms which don't have a 64-bit off_t. 1281 if constexpr (sizeof(off_t) != sizeof(s64)) 1282 { 1283 if (offset < std::numeric_limits<off_t>::min() || offset > std::numeric_limits<off_t>::max()) 1284 return -1; 1285 } 1286 1287 return fseeko(fp, static_cast<off_t>(offset), whence); 1288 #endif 1289 } 1290 1291 bool FileSystem::FSeek64(std::FILE* fp, s64 offset, int whence, Error* error) 1292 { 1293 #ifdef _WIN32 1294 const int res = _fseeki64(fp, offset, whence); 1295 #else 1296 // Prevent truncation on platforms which don't have a 64-bit off_t. 1297 if constexpr (sizeof(off_t) != sizeof(s64)) 1298 { 1299 if (offset < std::numeric_limits<off_t>::min() || offset > std::numeric_limits<off_t>::max()) 1300 { 1301 Error::SetStringView(error, "Invalid offset."); 1302 return false; 1303 } 1304 } 1305 1306 const int res = fseeko(fp, static_cast<off_t>(offset), whence); 1307 #endif 1308 1309 if (res == 0) 1310 return true; 1311 1312 Error::SetErrno(error, errno); 1313 return false; 1314 } 1315 1316 s64 FileSystem::FTell64(std::FILE* fp) 1317 { 1318 #ifdef _WIN32 1319 return static_cast<s64>(_ftelli64(fp)); 1320 #else 1321 return static_cast<s64>(ftello(fp)); 1322 #endif 1323 } 1324 1325 s64 FileSystem::FSize64(std::FILE* fp, Error* error) 1326 { 1327 const s64 pos = FTell64(fp); 1328 if (pos < 0) [[unlikely]] 1329 { 1330 Error::SetErrno(error, "FTell64() failed: ", errno); 1331 return -1; 1332 } 1333 1334 if (FSeek64(fp, 0, SEEK_END) != 0) [[unlikely]] 1335 { 1336 Error::SetErrno(error, "FSeek64() to end failed: ", errno); 1337 return -1; 1338 } 1339 1340 const s64 size = FTell64(fp); 1341 if (size < 0) [[unlikely]] 1342 { 1343 Error::SetErrno(error, "FTell64() failed: ", errno); 1344 return -1; 1345 } 1346 1347 if (FSeek64(fp, pos, SEEK_SET) != 0) 1348 { 1349 Error::SetErrno(error, "FSeek64() to original position failed: ", errno); 1350 return -1; 1351 } 1352 1353 return size; 1354 } 1355 1356 bool FileSystem::FTruncate64(std::FILE* fp, s64 size, Error* error) 1357 { 1358 const int fd = fileno(fp); 1359 if (fd < 0) 1360 { 1361 Error::SetErrno(error, "fileno() failed: ", errno); 1362 return false; 1363 } 1364 1365 #ifdef _WIN32 1366 const errno_t err = _chsize_s(fd, size); 1367 if (err != 0) 1368 { 1369 Error::SetErrno(error, "_chsize_s() failed: ", err); 1370 return false; 1371 } 1372 1373 return true; 1374 #else 1375 // Prevent truncation on platforms which don't have a 64-bit off_t. 1376 if constexpr (sizeof(off_t) != sizeof(s64)) 1377 { 1378 if (size < std::numeric_limits<off_t>::min() || size > std::numeric_limits<off_t>::max()) 1379 { 1380 Error::SetStringView(error, "File size is too large."); 1381 return false; 1382 } 1383 } 1384 1385 if (ftruncate(fd, static_cast<off_t>(size)) < 0) 1386 { 1387 Error::SetErrno(error, "ftruncate() failed: ", errno); 1388 return false; 1389 } 1390 1391 return true; 1392 #endif 1393 } 1394 1395 s64 FileSystem::GetPathFileSize(const char* Path) 1396 { 1397 FILESYSTEM_STAT_DATA sd; 1398 if (!StatFile(Path, &sd)) 1399 return -1; 1400 1401 return sd.Size; 1402 } 1403 1404 std::optional<DynamicHeapArray<u8>> FileSystem::ReadBinaryFile(const char* filename, Error* error) 1405 { 1406 std::optional<DynamicHeapArray<u8>> ret; 1407 1408 ManagedCFilePtr fp = OpenManagedCFile(filename, "rb", error); 1409 if (!fp) 1410 return ret; 1411 1412 ret = ReadBinaryFile(fp.get(), error); 1413 return ret; 1414 } 1415 1416 std::optional<DynamicHeapArray<u8>> FileSystem::ReadBinaryFile(std::FILE* fp, Error* error) 1417 { 1418 std::optional<DynamicHeapArray<u8>> ret; 1419 1420 if (FSeek64(fp, 0, SEEK_END) != 0) [[unlikely]] 1421 { 1422 Error::SetErrno(error, "FSeek64() to end failed: ", errno); 1423 return ret; 1424 } 1425 1426 const s64 size = FTell64(fp); 1427 if (size < 0) [[unlikely]] 1428 { 1429 Error::SetErrno(error, "FTell64() for length failed: ", errno); 1430 return ret; 1431 } 1432 1433 if constexpr (sizeof(s64) != sizeof(size_t)) 1434 { 1435 if (size > static_cast<s64>(std::numeric_limits<long>::max())) [[unlikely]] 1436 { 1437 Error::SetStringFmt(error, "File size of {} is too large to read on this platform.", size); 1438 return ret; 1439 } 1440 } 1441 1442 if (FSeek64(fp, 0, SEEK_SET) != 0) [[unlikely]] 1443 { 1444 Error::SetErrno(error, "FSeek64() to start failed: ", errno); 1445 return ret; 1446 } 1447 1448 ret = DynamicHeapArray<u8>(static_cast<size_t>(size)); 1449 if (size > 0 && std::fread(ret->data(), 1u, static_cast<size_t>(size), fp) != static_cast<size_t>(size)) [[unlikely]] 1450 { 1451 Error::SetErrno(error, "fread() failed: ", errno); 1452 ret.reset(); 1453 } 1454 1455 return ret; 1456 } 1457 1458 std::optional<std::string> FileSystem::ReadFileToString(const char* filename, Error* error) 1459 { 1460 std::optional<std::string> ret; 1461 1462 ManagedCFilePtr fp = OpenManagedCFile(filename, "rb", error); 1463 if (!fp) 1464 return ret; 1465 1466 ret = ReadFileToString(fp.get()); 1467 return ret; 1468 } 1469 1470 std::optional<std::string> FileSystem::ReadFileToString(std::FILE* fp, Error* error) 1471 { 1472 std::optional<std::string> ret; 1473 1474 if (FSeek64(fp, 0, SEEK_END) != 0) [[unlikely]] 1475 { 1476 Error::SetErrno(error, "FSeek64() to end failed: ", errno); 1477 return ret; 1478 } 1479 1480 const s64 size = FTell64(fp); 1481 if (size < 0) [[unlikely]] 1482 { 1483 Error::SetErrno(error, "FTell64() for length failed: ", errno); 1484 return ret; 1485 } 1486 1487 if constexpr (sizeof(s64) != sizeof(size_t)) 1488 { 1489 if (size > static_cast<s64>(std::numeric_limits<long>::max())) [[unlikely]] 1490 { 1491 Error::SetStringFmt(error, "File size of {} is too large to read on this platform.", size); 1492 return ret; 1493 } 1494 } 1495 1496 if (FSeek64(fp, 0, SEEK_SET) != 0) [[unlikely]] 1497 { 1498 Error::SetErrno(error, "FSeek64() to start failed: ", errno); 1499 return ret; 1500 } 1501 1502 ret = std::string(); 1503 ret->resize(static_cast<size_t>(size)); 1504 // NOTE - assumes mode 'rb', for example, this will fail over missing Windows carriage return bytes 1505 if (size > 0 && std::fread(ret->data(), 1u, static_cast<size_t>(size), fp) != static_cast<size_t>(size)) 1506 { 1507 Error::SetErrno(error, "fread() failed: ", errno); 1508 ret.reset(); 1509 } 1510 1511 return ret; 1512 } 1513 1514 bool FileSystem::WriteBinaryFile(const char* filename, const void* data, size_t data_length, Error* error) 1515 { 1516 ManagedCFilePtr fp = OpenManagedCFile(filename, "wb", error); 1517 if (!fp) 1518 return false; 1519 1520 if (data_length > 0 && std::fwrite(data, 1u, data_length, fp.get()) != data_length) 1521 { 1522 Error::SetErrno(error, "fwrite() failed: ", errno); 1523 return false; 1524 } 1525 1526 return true; 1527 } 1528 1529 bool FileSystem::WriteStringToFile(const char* filename, std::string_view sv, Error* error) 1530 { 1531 ManagedCFilePtr fp = OpenManagedCFile(filename, "wb", error); 1532 if (!fp) 1533 return false; 1534 1535 if (sv.length() > 0 && std::fwrite(sv.data(), 1u, sv.length(), fp.get()) != sv.length()) 1536 { 1537 Error::SetErrno(error, "fwrite() failed: ", errno); 1538 return false; 1539 } 1540 1541 return true; 1542 } 1543 1544 bool FileSystem::EnsureDirectoryExists(const char* path, bool recursive, Error* error) 1545 { 1546 if (FileSystem::DirectoryExists(path)) 1547 return true; 1548 1549 // if it fails to create, we're not going to be able to use it anyway 1550 return FileSystem::CreateDirectory(path, recursive, error); 1551 } 1552 1553 bool FileSystem::RecursiveDeleteDirectory(const char* path) 1554 { 1555 FindResultsArray results; 1556 if (FindFiles(path, "*", FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_FOLDERS | FILESYSTEM_FIND_HIDDEN_FILES, &results)) 1557 { 1558 for (const FILESYSTEM_FIND_DATA& fd : results) 1559 { 1560 if (fd.Attributes & FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY) 1561 { 1562 if (!RecursiveDeleteDirectory(fd.FileName.c_str())) 1563 return false; 1564 } 1565 else 1566 { 1567 if (!DeleteFile(fd.FileName.c_str())) 1568 return false; 1569 } 1570 } 1571 } 1572 1573 return DeleteDirectory(path); 1574 } 1575 1576 bool FileSystem::CopyFilePath(const char* source, const char* destination, bool replace) 1577 { 1578 #ifndef _WIN32 1579 // TODO: There's technically a race here between checking and opening the file.. 1580 // But fopen doesn't specify any way to say "don't create if it exists"... 1581 if (!replace && FileExists(destination)) 1582 return false; 1583 1584 auto in_fp = OpenManagedCFile(source, "rb"); 1585 if (!in_fp) 1586 return false; 1587 1588 auto out_fp = OpenManagedCFile(destination, "wb"); 1589 if (!out_fp) 1590 return false; 1591 1592 u8 buf[4096]; 1593 while (!std::feof(in_fp.get())) 1594 { 1595 size_t bytes_in = std::fread(buf, 1, sizeof(buf), in_fp.get()); 1596 if ((bytes_in == 0 && !std::feof(in_fp.get())) || 1597 (bytes_in > 0 && std::fwrite(buf, 1, bytes_in, out_fp.get()) != bytes_in)) 1598 { 1599 out_fp.reset(); 1600 DeleteFile(destination); 1601 return false; 1602 } 1603 } 1604 1605 if (std::fflush(out_fp.get()) != 0) 1606 { 1607 out_fp.reset(); 1608 DeleteFile(destination); 1609 return false; 1610 } 1611 1612 return true; 1613 #else 1614 return CopyFileW(GetWin32Path(source).c_str(), GetWin32Path(destination).c_str(), !replace); 1615 #endif 1616 } 1617 1618 #ifdef _WIN32 1619 1620 static u32 TranslateWin32Attributes(u32 w32attrs) 1621 { 1622 return ((w32attrs & FILE_ATTRIBUTE_DIRECTORY) ? FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY : 0) | 1623 ((w32attrs & FILE_ATTRIBUTE_READONLY) ? FILESYSTEM_FILE_ATTRIBUTE_READ_ONLY : 0) | 1624 ((w32attrs & FILE_ATTRIBUTE_COMPRESSED) ? FILESYSTEM_FILE_ATTRIBUTE_COMPRESSED : 0) | 1625 ((w32attrs & FILE_ATTRIBUTE_REPARSE_POINT) ? FILESYSTEM_FILE_ATTRIBUTE_LINK : 0); 1626 } 1627 1628 static u32 RecursiveFindFiles(const char* origin_path, const char* parent_path, const char* path, const char* pattern, 1629 u32 flags, FileSystem::FindResultsArray* results, std::vector<std::string>& visited) 1630 { 1631 std::string search_dir; 1632 if (path) 1633 { 1634 if (parent_path) 1635 search_dir = fmt::format("{}\\{}\\{}\\*", origin_path, parent_path, path); 1636 else 1637 search_dir = fmt::format("{}\\{}\\*", origin_path, path); 1638 } 1639 else 1640 { 1641 search_dir = fmt::format("{}\\*", origin_path); 1642 } 1643 1644 // holder for utf-8 conversion 1645 WIN32_FIND_DATAW wfd; 1646 std::string utf8_filename; 1647 utf8_filename.reserve((sizeof(wfd.cFileName) / sizeof(wfd.cFileName[0])) * 2); 1648 1649 const HANDLE hFind = FindFirstFileW(FileSystem::GetWin32Path(search_dir).c_str(), &wfd); 1650 if (hFind == INVALID_HANDLE_VALUE) 1651 return 0; 1652 1653 // small speed optimization for '*' case 1654 bool hasWildCards = false; 1655 bool wildCardMatchAll = false; 1656 u32 nFiles = 0; 1657 if (std::strpbrk(pattern, "*?")) 1658 { 1659 hasWildCards = true; 1660 wildCardMatchAll = !(std::strcmp(pattern, "*")); 1661 } 1662 1663 // iterate results 1664 do 1665 { 1666 if (wfd.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN && !(flags & FILESYSTEM_FIND_HIDDEN_FILES)) 1667 continue; 1668 1669 if (wfd.cFileName[0] == L'.') 1670 { 1671 if (wfd.cFileName[1] == L'\0' || (wfd.cFileName[1] == L'.' && wfd.cFileName[2] == L'\0')) 1672 continue; 1673 } 1674 1675 if (!StringUtil::WideStringToUTF8String(utf8_filename, wfd.cFileName)) 1676 continue; 1677 1678 FILESYSTEM_FIND_DATA outData; 1679 outData.Attributes = TranslateWin32Attributes(wfd.dwFileAttributes); 1680 1681 if (wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) 1682 { 1683 if (flags & FILESYSTEM_FIND_RECURSIVE) 1684 { 1685 // check that we're not following an infinite symbolic link loop 1686 std::string real_recurse_dir; 1687 if (parent_path) 1688 real_recurse_dir = 1689 Path::RealPath(fmt::format("{}\\{}\\{}\\{}", origin_path, parent_path, path, utf8_filename)); 1690 else if (path) 1691 real_recurse_dir = Path::RealPath(fmt::format("{}\\{}\\{}", origin_path, path, utf8_filename)); 1692 else 1693 real_recurse_dir = Path::RealPath(fmt::format("{}\\{}", origin_path, utf8_filename)); 1694 if (real_recurse_dir.empty() || std::find(visited.begin(), visited.end(), real_recurse_dir) == visited.end()) 1695 { 1696 if (!real_recurse_dir.empty()) 1697 visited.push_back(std::move(real_recurse_dir)); 1698 1699 // recurse into this directory 1700 if (parent_path) 1701 { 1702 const std::string recurse_dir = fmt::format("{}\\{}", parent_path, path); 1703 nFiles += RecursiveFindFiles(origin_path, recurse_dir.c_str(), utf8_filename.c_str(), pattern, flags, 1704 results, visited); 1705 } 1706 else 1707 { 1708 nFiles += RecursiveFindFiles(origin_path, path, utf8_filename.c_str(), pattern, flags, results, visited); 1709 } 1710 } 1711 } 1712 1713 if (!(flags & FILESYSTEM_FIND_FOLDERS)) 1714 continue; 1715 } 1716 else 1717 { 1718 if (!(flags & FILESYSTEM_FIND_FILES)) 1719 continue; 1720 } 1721 1722 // match the filename 1723 if (hasWildCards) 1724 { 1725 if (!wildCardMatchAll && !StringUtil::WildcardMatch(utf8_filename.c_str(), pattern)) 1726 continue; 1727 } 1728 else 1729 { 1730 if (std::strcmp(utf8_filename.c_str(), pattern) != 0) 1731 continue; 1732 } 1733 1734 // add file to list 1735 if (!(flags & FILESYSTEM_FIND_RELATIVE_PATHS)) 1736 { 1737 if (parent_path) 1738 outData.FileName = fmt::format("{}\\{}\\{}\\{}", origin_path, parent_path, path, utf8_filename); 1739 else if (path) 1740 outData.FileName = fmt::format("{}\\{}\\{}", origin_path, path, utf8_filename); 1741 else 1742 outData.FileName = fmt::format("{}\\{}", origin_path, utf8_filename); 1743 } 1744 else 1745 { 1746 if (parent_path) 1747 outData.FileName = fmt::format("{}\\{}\\{}", parent_path, path, utf8_filename); 1748 else if (path) 1749 outData.FileName = fmt::format("{}\\{}", path, utf8_filename); 1750 else 1751 outData.FileName = utf8_filename; 1752 } 1753 1754 outData.CreationTime = ConvertFileTimeToUnixTime(wfd.ftCreationTime); 1755 outData.ModificationTime = ConvertFileTimeToUnixTime(wfd.ftLastWriteTime); 1756 outData.Size = (static_cast<u64>(wfd.nFileSizeHigh) << 32) | static_cast<u64>(wfd.nFileSizeLow); 1757 1758 nFiles++; 1759 results->push_back(std::move(outData)); 1760 } while (FindNextFileW(hFind, &wfd) == TRUE); 1761 FindClose(hFind); 1762 1763 return nFiles; 1764 } 1765 1766 bool FileSystem::FindFiles(const char* path, const char* pattern, u32 flags, FindResultsArray* results) 1767 { 1768 // clear result array 1769 if (!(flags & FILESYSTEM_FIND_KEEP_ARRAY)) 1770 results->clear(); 1771 1772 // add self if recursive, we don't want to visit it twice 1773 std::vector<std::string> visited; 1774 if (flags & FILESYSTEM_FIND_RECURSIVE) 1775 { 1776 std::string real_path = Path::RealPath(path); 1777 if (!real_path.empty()) 1778 visited.push_back(std::move(real_path)); 1779 } 1780 1781 // enter the recursive function 1782 if (RecursiveFindFiles(path, nullptr, nullptr, pattern, flags, results, visited) == 0) 1783 return false; 1784 1785 if (flags & FILESYSTEM_FIND_SORT_BY_NAME) 1786 { 1787 std::sort(results->begin(), results->end(), [](const FILESYSTEM_FIND_DATA& lhs, const FILESYSTEM_FIND_DATA& rhs) { 1788 // directories first 1789 if ((lhs.Attributes & FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY) != 1790 (rhs.Attributes & FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY)) 1791 { 1792 return ((lhs.Attributes & FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY) != 0); 1793 } 1794 1795 return (StringUtil::Strcasecmp(lhs.FileName.c_str(), rhs.FileName.c_str()) < 0); 1796 }); 1797 } 1798 1799 return true; 1800 } 1801 1802 static void TranslateStat64(struct stat* st, const struct _stat64& st64) 1803 { 1804 static constexpr __int64 MAX_SIZE = static_cast<__int64>(std::numeric_limits<decltype(st->st_size)>::max()); 1805 st->st_dev = st64.st_dev; 1806 st->st_ino = st64.st_ino; 1807 st->st_mode = st64.st_mode; 1808 st->st_nlink = st64.st_nlink; 1809 st->st_uid = st64.st_uid; 1810 st->st_rdev = st64.st_rdev; 1811 st->st_size = static_cast<decltype(st->st_size)>((st64.st_size > MAX_SIZE) ? MAX_SIZE : st64.st_size); 1812 st->st_atime = static_cast<time_t>(st64.st_atime); 1813 st->st_mtime = static_cast<time_t>(st64.st_mtime); 1814 st->st_ctime = static_cast<time_t>(st64.st_ctime); 1815 } 1816 1817 bool FileSystem::StatFile(const char* path, struct stat* st) 1818 { 1819 // convert to wide string 1820 const std::wstring wpath = GetWin32Path(path); 1821 if (wpath.empty()) 1822 return false; 1823 1824 struct _stat64 st64; 1825 if (_wstati64(wpath.c_str(), &st64) != 0) 1826 return false; 1827 1828 TranslateStat64(st, st64); 1829 return true; 1830 } 1831 1832 bool FileSystem::StatFile(std::FILE* fp, struct stat* st) 1833 { 1834 const int fd = _fileno(fp); 1835 if (fd < 0) 1836 return false; 1837 1838 struct _stat64 st64; 1839 if (_fstati64(fd, &st64) != 0) 1840 return false; 1841 1842 TranslateStat64(st, st64); 1843 return true; 1844 } 1845 1846 bool FileSystem::StatFile(const char* path, FILESYSTEM_STAT_DATA* sd) 1847 { 1848 // convert to wide string 1849 const std::wstring wpath = GetWin32Path(path); 1850 if (wpath.empty()) 1851 return false; 1852 1853 // determine attributes for the path. if it's a directory, things have to be handled differently.. 1854 DWORD fileAttributes = GetFileAttributesW(wpath.c_str()); 1855 if (fileAttributes == INVALID_FILE_ATTRIBUTES) 1856 return false; 1857 1858 // test if it is a directory 1859 HANDLE hFile; 1860 if (fileAttributes & FILE_ATTRIBUTE_DIRECTORY) 1861 { 1862 hFile = CreateFileW(wpath.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, 1863 OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); 1864 } 1865 else 1866 { 1867 hFile = CreateFileW(wpath.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, 1868 OPEN_EXISTING, 0, nullptr); 1869 } 1870 1871 // createfile succeded? 1872 if (hFile == INVALID_HANDLE_VALUE) 1873 return false; 1874 1875 // use GetFileInformationByHandle 1876 BY_HANDLE_FILE_INFORMATION bhfi; 1877 if (GetFileInformationByHandle(hFile, &bhfi) == FALSE) 1878 { 1879 CloseHandle(hFile); 1880 return false; 1881 } 1882 1883 // close handle 1884 CloseHandle(hFile); 1885 1886 // fill in the stat data 1887 sd->Attributes = TranslateWin32Attributes(bhfi.dwFileAttributes); 1888 sd->CreationTime = ConvertFileTimeToUnixTime(bhfi.ftCreationTime); 1889 sd->ModificationTime = ConvertFileTimeToUnixTime(bhfi.ftLastWriteTime); 1890 sd->Size = static_cast<s64>(((u64)bhfi.nFileSizeHigh) << 32 | (u64)bhfi.nFileSizeLow); 1891 return true; 1892 } 1893 1894 bool FileSystem::StatFile(std::FILE* fp, FILESYSTEM_STAT_DATA* sd) 1895 { 1896 const int fd = _fileno(fp); 1897 if (fd < 0) 1898 return false; 1899 1900 struct _stat64 st; 1901 if (_fstati64(fd, &st) != 0) 1902 return false; 1903 1904 // parse attributes 1905 sd->CreationTime = st.st_ctime; 1906 sd->ModificationTime = st.st_mtime; 1907 sd->Attributes = 0; 1908 if ((st.st_mode & _S_IFMT) == _S_IFDIR) 1909 sd->Attributes |= FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY; 1910 1911 // parse size 1912 if ((st.st_mode & _S_IFMT) == _S_IFREG) 1913 sd->Size = st.st_size; 1914 else 1915 sd->Size = 0; 1916 1917 return true; 1918 } 1919 1920 bool FileSystem::FileExists(const char* path) 1921 { 1922 // convert to wide string 1923 const std::wstring wpath = GetWin32Path(path); 1924 if (wpath.empty()) 1925 return false; 1926 1927 // determine attributes for the path. if it's a directory, things have to be handled differently.. 1928 const DWORD fileAttributes = GetFileAttributesW(wpath.c_str()); 1929 if (fileAttributes == INVALID_FILE_ATTRIBUTES) 1930 return false; 1931 1932 return ((fileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0); 1933 } 1934 1935 bool FileSystem::DirectoryExists(const char* path) 1936 { 1937 // convert to wide string 1938 const std::wstring wpath = GetWin32Path(path); 1939 if (wpath.empty()) 1940 return false; 1941 1942 // determine attributes for the path. if it's a directory, things have to be handled differently.. 1943 const DWORD fileAttributes = GetFileAttributesW(wpath.c_str()); 1944 if (fileAttributes == INVALID_FILE_ATTRIBUTES) 1945 return false; 1946 1947 return ((fileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0); 1948 } 1949 1950 bool FileSystem::IsRealDirectory(const char* path) 1951 { 1952 // convert to wide string 1953 const std::wstring wpath = GetWin32Path(path); 1954 if (wpath.empty()) 1955 return false; 1956 1957 // determine attributes for the path. if it's a directory, things have to be handled differently.. 1958 const DWORD fileAttributes = GetFileAttributesW(wpath.c_str()); 1959 if (fileAttributes == INVALID_FILE_ATTRIBUTES) 1960 return false; 1961 1962 return ((fileAttributes & (FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_REPARSE_POINT)) != FILE_ATTRIBUTE_DIRECTORY); 1963 } 1964 1965 bool FileSystem::IsDirectoryEmpty(const char* path) 1966 { 1967 std::wstring wpath = GetWin32Path(path); 1968 wpath += L"\\*"; 1969 1970 WIN32_FIND_DATAW wfd; 1971 HANDLE hFind = FindFirstFileW(wpath.c_str(), &wfd); 1972 1973 if (hFind == INVALID_HANDLE_VALUE) 1974 return true; 1975 1976 do 1977 { 1978 if (wfd.cFileName[0] == L'.') 1979 { 1980 if (wfd.cFileName[1] == L'\0' || (wfd.cFileName[1] == L'.' && wfd.cFileName[2] == L'\0')) 1981 continue; 1982 } 1983 1984 FindClose(hFind); 1985 return false; 1986 } while (FindNextFileW(hFind, &wfd)); 1987 1988 FindClose(hFind); 1989 return true; 1990 } 1991 1992 bool FileSystem::CreateDirectory(const char* Path, bool Recursive, Error* error) 1993 { 1994 const std::wstring win32_path = GetWin32Path(Path); 1995 if (win32_path.empty()) [[unlikely]] 1996 { 1997 Error::SetStringView(error, "Path is empty."); 1998 return false; 1999 } 2000 2001 // try just flat-out, might work if there's no other segments that have to be made 2002 if (CreateDirectoryW(win32_path.c_str(), nullptr)) 2003 return true; 2004 2005 DWORD lastError = GetLastError(); 2006 if (lastError == ERROR_ALREADY_EXISTS) 2007 { 2008 // check the attributes 2009 const u32 Attributes = GetFileAttributesW(win32_path.c_str()); 2010 if (Attributes != INVALID_FILE_ATTRIBUTES && Attributes & FILE_ATTRIBUTE_DIRECTORY) 2011 return true; 2012 } 2013 2014 if (!Recursive) 2015 { 2016 Error::SetWin32(error, "CreateDirectoryW() failed: ", lastError); 2017 return false; 2018 } 2019 2020 // check error 2021 if (lastError == ERROR_PATH_NOT_FOUND) 2022 { 2023 // part of the path does not exist, so we'll create the parent folders, then 2024 // the full path again. 2025 const size_t pathLength = std::strlen(Path); 2026 for (size_t i = 0; i < pathLength; i++) 2027 { 2028 if (Path[i] == '\\' || Path[i] == '/') 2029 { 2030 const std::string_view ppath(Path, i); 2031 const BOOL result = CreateDirectoryW(GetWin32Path(ppath).c_str(), nullptr); 2032 if (!result) 2033 { 2034 lastError = GetLastError(); 2035 if (lastError != ERROR_ALREADY_EXISTS) // fine, continue to next path segment 2036 { 2037 Error::SetWin32(error, "CreateDirectoryW() failed: ", lastError); 2038 return false; 2039 } 2040 } 2041 } 2042 } 2043 2044 // re-create the end if it's not a separator, check / as well because windows can interpret them 2045 if (Path[pathLength - 1] != '\\' && Path[pathLength - 1] != '/') 2046 { 2047 const BOOL result = CreateDirectoryW(win32_path.c_str(), nullptr); 2048 if (!result) 2049 { 2050 lastError = GetLastError(); 2051 if (lastError != ERROR_ALREADY_EXISTS) 2052 { 2053 Error::SetWin32(error, "CreateDirectoryW() failed: ", lastError); 2054 return false; 2055 } 2056 } 2057 } 2058 2059 // ok 2060 return true; 2061 } 2062 else 2063 { 2064 // unhandled error 2065 Error::SetWin32(error, "CreateDirectoryW() failed: ", lastError); 2066 return false; 2067 } 2068 } 2069 2070 bool FileSystem::DeleteFile(const char* path, Error* error) 2071 { 2072 const std::wstring wpath = GetWin32Path(path); 2073 const DWORD fileAttributes = GetFileAttributesW(wpath.c_str()); 2074 if (fileAttributes == INVALID_FILE_ATTRIBUTES || fileAttributes & FILE_ATTRIBUTE_DIRECTORY) 2075 { 2076 Error::SetStringView(error, "File does not exist."); 2077 return false; 2078 } 2079 2080 if (!DeleteFileW(wpath.c_str())) 2081 { 2082 Error::SetWin32(error, "DeleteFileW() failed: ", GetLastError()); 2083 return false; 2084 } 2085 2086 return true; 2087 } 2088 2089 bool FileSystem::RenamePath(const char* old_path, const char* new_path, Error* error) 2090 { 2091 const std::wstring old_wpath = GetWin32Path(old_path); 2092 const std::wstring new_wpath = GetWin32Path(new_path); 2093 2094 if (!MoveFileExW(old_wpath.c_str(), new_wpath.c_str(), MOVEFILE_REPLACE_EXISTING)) [[unlikely]] 2095 { 2096 Error::SetWin32(error, "MoveFileExW() failed: ", GetLastError()); 2097 return false; 2098 } 2099 2100 return true; 2101 } 2102 2103 bool FileSystem::DeleteDirectory(const char* path) 2104 { 2105 const std::wstring wpath = GetWin32Path(path); 2106 return RemoveDirectoryW(wpath.c_str()); 2107 } 2108 2109 std::string FileSystem::GetProgramPath() 2110 { 2111 std::wstring buffer; 2112 buffer.resize(MAX_PATH); 2113 2114 // Fall back to the main module if this fails. 2115 HMODULE module = nullptr; 2116 GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, 2117 reinterpret_cast<LPCWSTR>(&GetProgramPath), &module); 2118 2119 for (;;) 2120 { 2121 DWORD nChars = GetModuleFileNameW(module, buffer.data(), static_cast<DWORD>(buffer.size())); 2122 if (nChars == static_cast<DWORD>(buffer.size()) && GetLastError() == ERROR_INSUFFICIENT_BUFFER) 2123 { 2124 buffer.resize(buffer.size() * 2); 2125 continue; 2126 } 2127 2128 buffer.resize(nChars); 2129 break; 2130 } 2131 2132 // Windows symlinks don't behave silly like Linux, so no need to RealPath() it. 2133 return StringUtil::WideStringToUTF8String(buffer); 2134 } 2135 2136 std::string FileSystem::GetWorkingDirectory() 2137 { 2138 DWORD required_size = GetCurrentDirectoryW(0, nullptr); 2139 if (!required_size) 2140 return {}; 2141 2142 std::wstring buffer; 2143 buffer.resize(required_size - 1); 2144 2145 if (!GetCurrentDirectoryW(static_cast<DWORD>(buffer.size() + 1), buffer.data())) 2146 return {}; 2147 2148 return StringUtil::WideStringToUTF8String(buffer); 2149 } 2150 2151 bool FileSystem::SetWorkingDirectory(const char* path) 2152 { 2153 const std::wstring wpath = GetWin32Path(path); 2154 return (SetCurrentDirectoryW(wpath.c_str()) == TRUE); 2155 } 2156 2157 bool FileSystem::SetPathCompression(const char* path, bool enable) 2158 { 2159 const std::wstring wpath = GetWin32Path(path); 2160 const DWORD attrs = GetFileAttributesW(wpath.c_str()); 2161 if (attrs == INVALID_FILE_ATTRIBUTES) 2162 return false; 2163 2164 const bool isCompressed = (attrs & FILE_ATTRIBUTE_COMPRESSED) != 0; 2165 if (enable == isCompressed) 2166 { 2167 // already compressed/not compressed 2168 return true; 2169 } 2170 2171 const bool isFile = !(attrs & FILE_ATTRIBUTE_DIRECTORY); 2172 const DWORD flags = isFile ? FILE_ATTRIBUTE_NORMAL : (FILE_FLAG_BACKUP_SEMANTICS | FILE_ATTRIBUTE_DIRECTORY); 2173 2174 const HANDLE handle = CreateFileW(wpath.c_str(), FILE_GENERIC_WRITE | FILE_GENERIC_READ, 2175 FILE_SHARE_READ | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, flags, nullptr); 2176 if (handle == INVALID_HANDLE_VALUE) 2177 return false; 2178 2179 DWORD bytesReturned = 0; 2180 DWORD compressMode = enable ? COMPRESSION_FORMAT_DEFAULT : COMPRESSION_FORMAT_NONE; 2181 2182 bool result = DeviceIoControl(handle, FSCTL_SET_COMPRESSION, &compressMode, 2, nullptr, 0, &bytesReturned, nullptr); 2183 2184 CloseHandle(handle); 2185 return result; 2186 } 2187 2188 #elif !defined(__ANDROID__) 2189 2190 static u32 TranslateStatAttributes(struct stat& st) 2191 { 2192 return (S_ISDIR(st.st_mode) ? FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY : 0) | 2193 (S_ISLNK(st.st_mode) ? FILESYSTEM_FILE_ATTRIBUTE_LINK : 0); 2194 } 2195 2196 static u32 RecursiveFindFiles(const char* OriginPath, const char* ParentPath, const char* Path, const char* Pattern, 2197 u32 Flags, FileSystem::FindResultsArray* pResults, std::vector<std::string>& visited) 2198 { 2199 std::string tempStr; 2200 if (Path) 2201 { 2202 if (ParentPath) 2203 tempStr = fmt::format("{}/{}/{}", OriginPath, ParentPath, Path); 2204 else 2205 tempStr = fmt::format("{}/{}", OriginPath, Path); 2206 } 2207 else 2208 { 2209 tempStr = fmt::format("{}", OriginPath); 2210 } 2211 2212 DIR* pDir = opendir(tempStr.c_str()); 2213 if (!pDir) 2214 return 0; 2215 2216 // small speed optimization for '*' case 2217 bool hasWildCards = false; 2218 bool wildCardMatchAll = false; 2219 u32 nFiles = 0; 2220 if (std::strpbrk(Pattern, "*?")) 2221 { 2222 hasWildCards = true; 2223 wildCardMatchAll = (std::strcmp(Pattern, "*") == 0); 2224 } 2225 2226 // iterate results 2227 struct dirent* pDirEnt; 2228 while ((pDirEnt = readdir(pDir)) != nullptr) 2229 { 2230 if (pDirEnt->d_name[0] == '.') 2231 { 2232 if (pDirEnt->d_name[1] == '\0' || (pDirEnt->d_name[1] == '.' && pDirEnt->d_name[2] == '\0')) 2233 continue; 2234 2235 if (!(Flags & FILESYSTEM_FIND_HIDDEN_FILES)) 2236 continue; 2237 } 2238 2239 std::string full_path; 2240 if (ParentPath) 2241 full_path = fmt::format("{}/{}/{}/{}", OriginPath, ParentPath, Path, pDirEnt->d_name); 2242 else if (Path) 2243 full_path = fmt::format("{}/{}/{}", OriginPath, Path, pDirEnt->d_name); 2244 else 2245 full_path = fmt::format("{}/{}", OriginPath, pDirEnt->d_name); 2246 2247 struct stat sDir; 2248 if (stat(full_path.c_str(), &sDir) < 0) 2249 continue; 2250 2251 FILESYSTEM_FIND_DATA outData; 2252 outData.Attributes = TranslateStatAttributes(sDir); 2253 2254 if (S_ISDIR(sDir.st_mode)) 2255 { 2256 if (Flags & FILESYSTEM_FIND_RECURSIVE) 2257 { 2258 // check that we're not following an infinite symbolic link loop 2259 if (std::string real_recurse_dir = Path::RealPath(full_path); 2260 real_recurse_dir.empty() || std::find(visited.begin(), visited.end(), real_recurse_dir) == visited.end()) 2261 { 2262 if (!real_recurse_dir.empty()) 2263 visited.push_back(std::move(real_recurse_dir)); 2264 2265 // recurse into this directory 2266 if (ParentPath) 2267 { 2268 const std::string recursive_dir = fmt::format("{}/{}", ParentPath, Path); 2269 nFiles += 2270 RecursiveFindFiles(OriginPath, recursive_dir.c_str(), pDirEnt->d_name, Pattern, Flags, pResults, visited); 2271 } 2272 else 2273 { 2274 nFiles += RecursiveFindFiles(OriginPath, Path, pDirEnt->d_name, Pattern, Flags, pResults, visited); 2275 } 2276 } 2277 } 2278 2279 if (!(Flags & FILESYSTEM_FIND_FOLDERS)) 2280 continue; 2281 } 2282 else 2283 { 2284 if (!(Flags & FILESYSTEM_FIND_FILES)) 2285 continue; 2286 } 2287 2288 outData.Size = static_cast<u64>(sDir.st_size); 2289 outData.CreationTime = sDir.st_ctime; 2290 outData.ModificationTime = sDir.st_mtime; 2291 2292 // match the filename 2293 if (hasWildCards) 2294 { 2295 if (!wildCardMatchAll && !StringUtil::WildcardMatch(pDirEnt->d_name, Pattern)) 2296 continue; 2297 } 2298 else 2299 { 2300 if (std::strcmp(pDirEnt->d_name, Pattern) != 0) 2301 continue; 2302 } 2303 2304 // add file to list 2305 if (!(Flags & FILESYSTEM_FIND_RELATIVE_PATHS)) 2306 { 2307 outData.FileName = std::move(full_path); 2308 } 2309 else 2310 { 2311 if (ParentPath) 2312 outData.FileName = fmt::format("{}/{}/{}", ParentPath, Path, pDirEnt->d_name); 2313 else if (Path) 2314 outData.FileName = fmt::format("{}/{}", Path, pDirEnt->d_name); 2315 else 2316 outData.FileName = pDirEnt->d_name; 2317 } 2318 2319 nFiles++; 2320 pResults->push_back(std::move(outData)); 2321 } 2322 2323 closedir(pDir); 2324 return nFiles; 2325 } 2326 2327 bool FileSystem::FindFiles(const char* path, const char* pattern, u32 flags, FindResultsArray* results) 2328 { 2329 // clear result array 2330 if (!(flags & FILESYSTEM_FIND_KEEP_ARRAY)) 2331 results->clear(); 2332 2333 // add self if recursive, we don't want to visit it twice 2334 std::vector<std::string> visited; 2335 if (flags & FILESYSTEM_FIND_RECURSIVE) 2336 { 2337 std::string real_path = Path::RealPath(path); 2338 if (!real_path.empty()) 2339 visited.push_back(std::move(real_path)); 2340 } 2341 2342 // enter the recursive function 2343 if (RecursiveFindFiles(path, nullptr, nullptr, pattern, flags, results, visited) == 0) 2344 return false; 2345 2346 if (flags & FILESYSTEM_FIND_SORT_BY_NAME) 2347 { 2348 std::sort(results->begin(), results->end(), [](const FILESYSTEM_FIND_DATA& lhs, const FILESYSTEM_FIND_DATA& rhs) { 2349 // directories first 2350 if ((lhs.Attributes & FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY) != 2351 (rhs.Attributes & FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY)) 2352 { 2353 return ((lhs.Attributes & FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY) != 0); 2354 } 2355 2356 return (StringUtil::Strcasecmp(lhs.FileName.c_str(), rhs.FileName.c_str()) < 0); 2357 }); 2358 } 2359 2360 return true; 2361 } 2362 2363 bool FileSystem::StatFile(const char* path, struct stat* st) 2364 { 2365 return stat(path, st) == 0; 2366 } 2367 2368 bool FileSystem::StatFile(std::FILE* fp, struct stat* st) 2369 { 2370 const int fd = fileno(fp); 2371 if (fd < 0) 2372 return false; 2373 2374 return fstat(fd, st) == 0; 2375 } 2376 2377 bool FileSystem::StatFile(const char* path, FILESYSTEM_STAT_DATA* sd) 2378 { 2379 // stat file 2380 struct stat sysStatData; 2381 if (stat(path, &sysStatData) < 0) 2382 return false; 2383 2384 // parse attributes 2385 sd->CreationTime = sysStatData.st_ctime; 2386 sd->ModificationTime = sysStatData.st_mtime; 2387 sd->Attributes = TranslateStatAttributes(sysStatData); 2388 sd->Size = S_ISREG(sysStatData.st_mode) ? sysStatData.st_size : 0; 2389 2390 // ok 2391 return true; 2392 } 2393 2394 bool FileSystem::StatFile(std::FILE* fp, FILESYSTEM_STAT_DATA* sd) 2395 { 2396 const int fd = fileno(fp); 2397 if (fd < 0) 2398 return false; 2399 2400 // stat file 2401 struct stat sysStatData; 2402 if (fstat(fd, &sysStatData) < 0) 2403 return false; 2404 2405 // parse attributes 2406 sd->CreationTime = sysStatData.st_ctime; 2407 sd->ModificationTime = sysStatData.st_mtime; 2408 sd->Attributes = TranslateStatAttributes(sysStatData); 2409 sd->Size = S_ISREG(sysStatData.st_mode) ? sysStatData.st_size : 0; 2410 2411 return true; 2412 } 2413 2414 bool FileSystem::FileExists(const char* path) 2415 { 2416 struct stat sysStatData; 2417 if (stat(path, &sysStatData) < 0) 2418 return false; 2419 2420 if (S_ISDIR(sysStatData.st_mode)) 2421 return false; 2422 else 2423 return true; 2424 } 2425 2426 bool FileSystem::DirectoryExists(const char* path) 2427 { 2428 struct stat sysStatData; 2429 if (stat(path, &sysStatData) < 0) 2430 return false; 2431 2432 return S_ISDIR(sysStatData.st_mode); 2433 } 2434 2435 bool FileSystem::IsRealDirectory(const char* path) 2436 { 2437 struct stat sysStatData; 2438 if (lstat(path, &sysStatData) < 0) 2439 return false; 2440 2441 return (S_ISDIR(sysStatData.st_mode) && !S_ISLNK(sysStatData.st_mode)); 2442 } 2443 2444 bool FileSystem::IsDirectoryEmpty(const char* path) 2445 { 2446 DIR* pDir = opendir(path); 2447 if (pDir == nullptr) 2448 return true; 2449 2450 // iterate results 2451 struct dirent* pDirEnt; 2452 while ((pDirEnt = readdir(pDir)) != nullptr) 2453 { 2454 if (pDirEnt->d_name[0] == '.') 2455 { 2456 if (pDirEnt->d_name[1] == '\0' || (pDirEnt->d_name[1] == '.' && pDirEnt->d_name[2] == '\0')) 2457 continue; 2458 } 2459 2460 closedir(pDir); 2461 return false; 2462 } 2463 2464 closedir(pDir); 2465 return true; 2466 } 2467 2468 bool FileSystem::CreateDirectory(const char* path, bool recursive, Error* error) 2469 { 2470 // has a path 2471 const size_t pathLength = std::strlen(path); 2472 if (pathLength == 0) 2473 return false; 2474 2475 // try just flat-out, might work if there's no other segments that have to be made 2476 if (mkdir(path, 0777) == 0) 2477 return true; 2478 2479 // check error 2480 int lastError = errno; 2481 if (lastError == EEXIST) 2482 { 2483 // check the attributes 2484 struct stat sysStatData; 2485 if (stat(path, &sysStatData) == 0 && S_ISDIR(sysStatData.st_mode)) 2486 return true; 2487 } 2488 2489 if (!recursive) 2490 { 2491 Error::SetErrno(error, "mkdir() failed: ", lastError); 2492 return false; 2493 } 2494 2495 else if (lastError == ENOENT) 2496 { 2497 // part of the path does not exist, so we'll create the parent folders, then 2498 // the full path again. 2499 std::string tempPath; 2500 tempPath.reserve(pathLength); 2501 2502 // create directories along the path 2503 for (size_t i = 0; i < pathLength; i++) 2504 { 2505 if (i > 0 && path[i] == '/') 2506 { 2507 if (mkdir(tempPath.c_str(), 0777) < 0) 2508 { 2509 lastError = errno; 2510 if (lastError != EEXIST) // fine, continue to next path segment 2511 { 2512 Error::SetErrno(error, "mkdir() failed: ", lastError); 2513 return false; 2514 } 2515 } 2516 } 2517 2518 tempPath.push_back(path[i]); 2519 } 2520 2521 // re-create the end if it's not a separator, check / as well because windows can interpret them 2522 if (path[pathLength - 1] != '/') 2523 { 2524 if (mkdir(path, 0777) < 0) 2525 { 2526 lastError = errno; 2527 if (lastError != EEXIST) 2528 { 2529 Error::SetErrno(error, "mkdir() failed: ", lastError); 2530 return false; 2531 } 2532 } 2533 } 2534 2535 // ok 2536 return true; 2537 } 2538 else 2539 { 2540 // unhandled error 2541 Error::SetErrno(error, "mkdir() failed: ", lastError); 2542 return false; 2543 } 2544 } 2545 2546 bool FileSystem::DeleteFile(const char* path, Error* error) 2547 { 2548 struct stat sysStatData; 2549 if (stat(path, &sysStatData) != 0 || S_ISDIR(sysStatData.st_mode)) 2550 { 2551 Error::SetStringView(error, "File does not exist."); 2552 return false; 2553 } 2554 2555 if (unlink(path) != 0) 2556 { 2557 Error::SetErrno(error, "unlink() failed: ", errno); 2558 return false; 2559 } 2560 2561 return true; 2562 } 2563 2564 bool FileSystem::RenamePath(const char* old_path, const char* new_path, Error* error) 2565 { 2566 if (rename(old_path, new_path) != 0) 2567 { 2568 const int err = errno; 2569 Error::SetErrno(error, "rename() failed: ", err); 2570 return false; 2571 } 2572 2573 return true; 2574 } 2575 2576 bool FileSystem::DeleteDirectory(const char* path) 2577 { 2578 struct stat sysStatData; 2579 if (stat(path, &sysStatData) != 0 || !S_ISDIR(sysStatData.st_mode)) 2580 return false; 2581 2582 return (rmdir(path) == 0); 2583 } 2584 2585 std::string FileSystem::GetProgramPath() 2586 { 2587 #if defined(__linux__) 2588 static const char* exeFileName = "/proc/self/exe"; 2589 2590 int curSize = PATH_MAX; 2591 char* buffer = static_cast<char*>(std::realloc(nullptr, curSize)); 2592 for (;;) 2593 { 2594 int len = readlink(exeFileName, buffer, curSize); 2595 if (len < 0) 2596 { 2597 std::free(buffer); 2598 return {}; 2599 } 2600 else if (len < curSize) 2601 { 2602 buffer[len] = '\0'; 2603 std::string ret(buffer, len); 2604 std::free(buffer); 2605 return ret; 2606 } 2607 2608 curSize *= 2; 2609 buffer = static_cast<char*>(std::realloc(buffer, curSize)); 2610 } 2611 2612 #elif defined(__APPLE__) 2613 2614 int curSize = PATH_MAX; 2615 char* buffer = static_cast<char*>(std::realloc(nullptr, curSize)); 2616 for (;;) 2617 { 2618 u32 nChars = curSize - 1; 2619 int res = _NSGetExecutablePath(buffer, &nChars); 2620 if (res == 0) 2621 { 2622 buffer[nChars] = 0; 2623 2624 char* resolvedBuffer = realpath(buffer, nullptr); 2625 if (resolvedBuffer == nullptr) 2626 { 2627 std::free(buffer); 2628 return {}; 2629 } 2630 2631 std::string ret(buffer); 2632 std::free(buffer); 2633 return ret; 2634 } 2635 2636 curSize *= 2; 2637 buffer = static_cast<char*>(std::realloc(buffer, curSize + 1)); 2638 } 2639 2640 #elif defined(__FreeBSD__) 2641 int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, -1}; 2642 char buffer[PATH_MAX]; 2643 size_t cb = sizeof(buffer) - 1; 2644 int res = sysctl(mib, std::size(mib), buffer, &cb, nullptr, 0); 2645 if (res != 0) 2646 return {}; 2647 2648 buffer[cb] = '\0'; 2649 return buffer; 2650 #else 2651 return {}; 2652 #endif 2653 } 2654 2655 std::string FileSystem::GetWorkingDirectory() 2656 { 2657 std::string buffer; 2658 buffer.resize(PATH_MAX); 2659 while (!getcwd(buffer.data(), buffer.size())) 2660 { 2661 if (errno != ERANGE) 2662 return {}; 2663 2664 buffer.resize(buffer.size() * 2); 2665 } 2666 2667 buffer.resize(std::strlen(buffer.c_str())); // Remove excess nulls 2668 return buffer; 2669 } 2670 2671 bool FileSystem::SetWorkingDirectory(const char* path) 2672 { 2673 return (chdir(path) == 0); 2674 } 2675 2676 bool FileSystem::SetPathCompression(const char* path, bool enable) 2677 { 2678 return false; 2679 } 2680 2681 static bool SetLock(int fd, bool lock) 2682 { 2683 // We want to lock the whole file. 2684 const off_t offs = lseek(fd, 0, SEEK_CUR); 2685 if (offs < 0) 2686 { 2687 ERROR_LOG("lseek({}) failed: {}", fd, errno); 2688 return false; 2689 } 2690 2691 if (offs != 0 && lseek(fd, 0, SEEK_SET) < 0) 2692 { 2693 ERROR_LOG("lseek({}, 0) failed: {}", fd, errno); 2694 return false; 2695 } 2696 2697 // bloody signals... 2698 bool res; 2699 for (;;) 2700 { 2701 res = (lockf(fd, lock ? F_LOCK : F_ULOCK, 0) == 0); 2702 if (!res && errno == EINTR) 2703 continue; 2704 else 2705 break; 2706 } 2707 2708 if (lseek(fd, offs, SEEK_SET) < 0) 2709 Panic("Repositioning file descriptor after lock failed."); 2710 2711 if (!res) 2712 ERROR_LOG("lockf() for {} failed: {}", lock ? "lock" : "unlock", errno); 2713 2714 return res; 2715 } 2716 2717 FileSystem::POSIXLock::POSIXLock(int fd) : m_fd(fd) 2718 { 2719 if (!SetLock(m_fd, true)) 2720 m_fd = -1; 2721 } 2722 2723 FileSystem::POSIXLock::POSIXLock(std::FILE* fp) : m_fd(fileno(fp)) 2724 { 2725 if (!SetLock(m_fd, true)) 2726 m_fd = -1; 2727 } 2728 2729 FileSystem::POSIXLock::~POSIXLock() 2730 { 2731 if (m_fd >= 0) 2732 SetLock(m_fd, false); 2733 } 2734 2735 #endif