mirror of
https://github.com/RPCS3/rpcs3.git
synced 2025-07-12 09:48:37 +12:00
Possibly fixes sys_fs_rmdir and other cases of directory removal. Make sure the directory with deleted files always becomes empty. For this purpose, temp files are moved to the root of the device.
597 lines
10 KiB
C++
597 lines
10 KiB
C++
#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<std::pair<std::string, vfs_directory>> 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<vfs_manager>())
|
||
{
|
||
// Init (TODO)
|
||
g_fxo->init<vfs_manager>();
|
||
}
|
||
|
||
const auto table = g_fxo->get<vfs_manager>();
|
||
|
||
// 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<vfs_directory*> 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<std::string>* out_dir)
|
||
{
|
||
const auto table = g_fxo->get<vfs_manager>();
|
||
|
||
reader_lock lock(table->mutex);
|
||
|
||
// Resulting path fragments: decoded ones
|
||
std::vector<std::string_view> 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<const vfs_directory*> 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 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 char{u8"!"[0]}:
|
||
{
|
||
// Escape full-width characters 0xFF01..0xFF5e with ! (0xFF01)
|
||
switch (path[i + 1])
|
||
{
|
||
case char{u8"!"[1]}:
|
||
{
|
||
const uchar c3 = reinterpret_cast<const uchar&>(path[i + 2]);
|
||
|
||
if (c3 >= 0x81 && c3 <= 0xbf)
|
||
{
|
||
result += u8"!";
|
||
}
|
||
|
||
break;
|
||
}
|
||
case char{u8"`"[1]}:
|
||
{
|
||
const uchar c3 = reinterpret_cast<const uchar&>(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<const uchar&>(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<const uchar&>(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<const u16&>(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<std::string>()(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);
|
||
}
|