#include "stdafx.h" #include "IdManager.h" #include "System.h" #include "VFS.h" #include "Utilities/mutex.h" #include "Utilities/StrUtil.h" struct vfs_directory { // Real path (empty if root or not exists) std::string path; // Virtual subdirectories (vector because only vector allows incomplete types) std::vector> dirs; }; struct vfs_manager { shared_mutex mutex; // VFS root vfs_directory root; }; bool vfs::mount(std::string_view vpath, std::string_view path) { if (!g_fxo->get()) { // Init (TODO) g_fxo->init(); } const auto table = g_fxo->get(); // TODO: scan roots of mounted devices for undeleted vfs::host::unlink remnants, and try to delete them (_WIN32 only) std::lock_guard lock(table->mutex); if (vpath.empty()) { // Empty relative path, should set relative path base; unsupported return false; } for (std::vector list{&table->root};;) { // Skip one or more '/' const auto pos = vpath.find_first_not_of('/'); if (pos == 0) { // Mounting relative path is not supported return false; } if (pos == -1) { // Mounting completed list.back()->path = path; return true; } // Get fragment name const auto name = vpath.substr(pos, vpath.find_first_of('/', pos) - pos); vpath.remove_prefix(name.size() + pos); if (name == ".") { // Keep current continue; } if (name == "..") { // Root parent is root if (list.size() == 1) { continue; } // Go back one level list.resize(list.size() - 1); continue; } // Find or add const auto last = list.back(); for (auto& dir : last->dirs) { if (dir.first == name) { list.push_back(&dir.second); break; } } if (last == list.back()) { // Add new entry list.push_back(&last->dirs.emplace_back(name, vfs_directory{}).second); } } } std::string vfs::get(std::string_view vpath, std::vector* out_dir) { const auto table = g_fxo->get(); reader_lock lock(table->mutex); // Resulting path fragments: decoded ones std::vector result; result.reserve(vpath.size() / 2); // Mounted path std::string_view result_base; if (vpath.empty()) { // Empty relative path (reuse further return) vpath = "."; } for (std::vector list{&table->root};;) { // Skip one or more '/' const auto pos = vpath.find_first_not_of('/'); if (pos == 0) { // Relative path: point to non-existent location return fs::get_config_dir() + "delete_this_dir.../delete_this..."; } if (pos == -1) { // Absolute path: finalize for (auto it = list.rbegin(), rend = list.rend(); it != rend; it++) { if (auto* dir = *it; dir && (!dir->path.empty() || list.size() == 1)) { // Save latest valid mount path result_base = dir->path; // Erase unnecessary path fragments result.erase(result.begin(), result.begin() + (std::distance(it, rend) - 1)); // Extract mounted subdirectories (TODO) if (out_dir) { for (auto& pair : dir->dirs) { if (!pair.second.path.empty()) { out_dir->emplace_back(pair.first); } } } break; } } if (!vpath.empty()) { // Finalize path with '/' result.emplace_back(""); } break; } // Get fragment name const auto name = vpath.substr(pos, vpath.find_first_of('/', pos) - pos); vpath.remove_prefix(name.size() + pos); // Process special directories if (name == ".") { // Keep current continue; } if (name == "..") { // Root parent is root if (list.size() == 1) { continue; } // Go back one level list.resize(list.size() - 1); result.resize(result.size() - 1); continue; } const auto last = list.back(); list.push_back(nullptr); result.push_back(name); if (!last) { continue; } for (auto& dir : last->dirs) { if (dir.first == name) { list.back() = &dir.second; if (dir.second.path == "/"sv) { if (vpath.empty()) { return {}; } // Handle /host_root (not escaped, not processed) return std::string{vpath.substr(1)}; } break; } } } if (result_base.empty()) { // Not mounted return {}; } // Escape and merge path fragments return std::string{result_base} + vfs::escape(fmt::merge(result, "/")); } std::string vfs::escape(std::string_view path, bool escape_slash) { std::string result; result.reserve(path.size()); for (std::size_t i = 0, s = path.size(); i < s; i++) { switch (char c = path[i]) { case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7: case 8: case 9: { result += u8"0"; result.back() += c; break; } case 10: case 11: case 12: case 13: case 14: case 15: case 16: case 17: case 18: case 19: case 20: case 21: case 22: case 23: case 24: case 25: case 26: case 27: case 28: case 29: case 30: case 31: { result += u8"A"; result.back() += c; result.back() -= 10; break; } case '<': { result += u8"<"; break; } case '>': { result += u8">"; break; } case ':': { result += u8":"; break; } case '"': { result += u8"""; break; } case '\\': { result += u8"\"; break; } case '|': { result += u8"|"; break; } case '?': { result += u8"?"; break; } case '*': { result += u8"*"; break; } case '/': { if (escape_slash) { result += u8"/"; break; } result += c; break; } case char{u8"!"[0]}: { // Escape full-width characters 0xFF01..0xFF5e with ! (0xFF01) switch (path[i + 1]) { case char{u8"!"[1]}: { const uchar c3 = reinterpret_cast(path[i + 2]); if (c3 >= 0x81 && c3 <= 0xbf) { result += u8"!"; } break; } case char{u8"`"[1]}: { const uchar c3 = reinterpret_cast(path[i + 2]); if (c3 >= 0x80 && c3 <= 0x9e) { result += u8"!"; } break; } } result += c; break; } default: { result += c; break; } } } return result; } std::string vfs::unescape(std::string_view path) { std::string result; result.reserve(path.size()); for (std::size_t i = 0, s = path.size(); i < s; i++) { switch (char c = path[i]) { case char{u8"!"[0]}: { switch (path[i + 1]) { case char{u8"!"[1]}: { const uchar c3 = reinterpret_cast(path[i + 2]); if (c3 >= 0x81 && c3 <= 0xbf) { switch (path[i + 2]) { case char{u8"0"[2]}: case char{u8"1"[2]}: case char{u8"2"[2]}: case char{u8"3"[2]}: case char{u8"4"[2]}: case char{u8"5"[2]}: case char{u8"6"[2]}: case char{u8"7"[2]}: case char{u8"8"[2]}: case char{u8"9"[2]}: { result += path[i + 2]; result.back() -= u8"0"[2]; continue; } case char{u8"A"[2]}: case char{u8"B"[2]}: case char{u8"C"[2]}: case char{u8"D"[2]}: case char{u8"E"[2]}: case char{u8"F"[2]}: case char{u8"G"[2]}: case char{u8"H"[2]}: case char{u8"I"[2]}: case char{u8"J"[2]}: case char{u8"K"[2]}: case char{u8"L"[2]}: case char{u8"M"[2]}: case char{u8"N"[2]}: case char{u8"O"[2]}: case char{u8"P"[2]}: case char{u8"Q"[2]}: case char{u8"R"[2]}: case char{u8"S"[2]}: case char{u8"T"[2]}: case char{u8"U"[2]}: case char{u8"V"[2]}: { result += path[i + 2]; result.back() -= u8"A"[2]; result.back() += 10; continue; } case char{u8"!"[2]}: { i += 3; result += c; continue; } case char{u8"<"[2]}: { result += '<'; break; } case char{u8">"[2]}: { result += '>'; break; } case char{u8":"[2]}: { result += ':'; break; } case char{u8"""[2]}: { result += '"'; break; } case char{u8"\"[2]}: { result += '\\'; break; } case char{u8"?"[2]}: { result += '?'; break; } case char{u8"*"[2]}: { result += '*'; break; } case char{u8"$"[2]}: { if (i == 0) { // Special case: filename starts with full-width $ likely created by vfs::host::unlink result.resize(1, '.'); return result; } [[fallthrough]]; } default: { // Unrecognized character (ignored) break; } } i += 2; } else { result += c; } break; } case char{u8"`"[1]}: { const uchar c3 = reinterpret_cast(path[i + 2]); if (c3 >= 0x80 && c3 <= 0x9e) { switch (path[i + 2]) { case char{u8"|"[2]}: { result += '|'; break; } default: { // Unrecognized character (ignored) break; } } i += 2; } else { result += c; } break; } default: { result += c; break; } } break; } default: { result += c; break; } } } return result; } bool vfs::host::rename(const std::string& from, const std::string& to, bool overwrite) { while (!fs::rename(from, to, overwrite)) { // Try to ignore access error in order to prevent spurious failure if (Emu.IsStopped() || fs::g_tls_error != fs::error::acces) { return false; } } return true; } bool vfs::host::unlink(const std::string& path, const std::string& dev_root) { #ifdef _WIN32 if (path.size() < 2 || reinterpret_cast(path.front()) != "//"_u16) { // Rename to special dummy name which will be ignored by VFS (but opened file handles can still read or write it) const std::string dummy = fmt::format(u8"%s/$%s%s", dev_root, fmt::base57(std::hash()(path)), fmt::base57(__rdtsc())); if (!fs::rename(path, dummy, true)) { return false; } if (fs::file f{dummy, fs::read + fs::write}) { // Set to delete on close on last handle f.set_delete(); return true; } // TODO: what could cause this and how to handle it return true; } #endif return fs::remove_file(path); } bool vfs::host::remove_all(const std::string& path, const std::string& dev_root, bool remove_root) { if (remove_root) { // Rename to special dummy folder which will be ignored by VFS (but opened file handles can still read or write it) const std::string dummy = fmt::format(u8"%s/$%s%s", dev_root, fmt::base57(std::hash()(path)), fmt::base57(__rdtsc())); if (!vfs::host::rename(path, dummy, false)) { return false; } if (!fs::remove_all(dummy)) { return false; } } else { const auto root_dir = fs::dir(path); if (!root_dir) { return false; } for (const auto& entry : root_dir) { if (entry.name == "." || entry.name == "..") { continue; } if (!entry.is_directory) { if (!vfs::host::unlink(path + '/' + entry.name, dev_root)) { return false; } } else { if (!vfs::host::remove_all(path + '/' + entry.name, dev_root)) { return false; } } } } return true; }