duckstation

duckstation, but archived from the revision just before upstream changed it to a proprietary software project, this version is the libre one
git clone https://git.neptards.moe/u3shit/duckstation.git
Log | Files | Refs | README | LICENSE

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