#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) { const auto table = fxm::get_always(); 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 = fxm::get_always(); 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) { 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 '<': { 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 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"!"[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) { #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", fs::get_parent_dir(path), 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); }