Merge pull request #8539 from leoetlino/fs-accuracy
IOS/FS: Reimplement many functions in a more accurate way
This commit is contained in:
commit
73aea8af6b
|
@ -109,12 +109,6 @@ static std::vector<u64> GetTitlesInTitleOrImport(FS::FileSystem* fs, const std::
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// On a real Wii, the title list is not in any particular order. However, because of how
|
|
||||||
// the flash filesystem works, titles such as 1-2 are *never* in the first position.
|
|
||||||
// We must keep this behaviour, or some versions of the System Menu may break.
|
|
||||||
|
|
||||||
std::sort(title_ids.begin(), title_ids.end(), std::greater<>());
|
|
||||||
|
|
||||||
return title_ids;
|
return title_ids;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,24 @@
|
||||||
|
|
||||||
namespace IOS::HLE::FS
|
namespace IOS::HLE::FS
|
||||||
{
|
{
|
||||||
|
bool IsValidPath(std::string_view path)
|
||||||
|
{
|
||||||
|
return path == "/" || IsValidNonRootPath(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsValidNonRootPath(std::string_view path)
|
||||||
|
{
|
||||||
|
return path.length() > 1 && path.length() <= MaxPathLength && path[0] == '/' &&
|
||||||
|
path.back() != '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
SplitPathResult SplitPathAndBasename(std::string_view path)
|
||||||
|
{
|
||||||
|
const auto last_separator = path.rfind('/');
|
||||||
|
return {std::string(path.substr(0, std::max<size_t>(1, last_separator))),
|
||||||
|
std::string(path.substr(last_separator + 1))};
|
||||||
|
}
|
||||||
|
|
||||||
std::unique_ptr<FileSystem> MakeFileSystem(Location location)
|
std::unique_ptr<FileSystem> MakeFileSystem(Location location)
|
||||||
{
|
{
|
||||||
const std::string nand_root =
|
const std::string nand_root =
|
||||||
|
@ -66,12 +84,6 @@ Result<FileStatus> FileHandle::GetStatus() const
|
||||||
return m_fs->GetFileStatus(*m_fd);
|
return m_fs->GetFileStatus(*m_fd);
|
||||||
}
|
}
|
||||||
|
|
||||||
void FileSystem::Init()
|
|
||||||
{
|
|
||||||
if (Delete(0, 0, "/tmp") == ResultCode::Success)
|
|
||||||
CreateDirectory(0, 0, "/tmp", 0, {Mode::ReadWrite, Mode::ReadWrite, Mode::ReadWrite});
|
|
||||||
}
|
|
||||||
|
|
||||||
Result<FileHandle> FileSystem::CreateAndOpenFile(Uid uid, Gid gid, const std::string& path,
|
Result<FileHandle> FileSystem::CreateAndOpenFile(Uid uid, Gid gid, const std::string& path,
|
||||||
Modes modes)
|
Modes modes)
|
||||||
{
|
{
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
|
@ -76,6 +77,16 @@ struct Modes
|
||||||
{
|
{
|
||||||
Mode owner, group, other;
|
Mode owner, group, other;
|
||||||
};
|
};
|
||||||
|
inline bool operator==(const Modes& lhs, const Modes& rhs)
|
||||||
|
{
|
||||||
|
const auto fields = [](const Modes& obj) { return std::tie(obj.owner, obj.group, obj.other); };
|
||||||
|
return fields(lhs) == fields(rhs);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool operator!=(const Modes& lhs, const Modes& rhs)
|
||||||
|
{
|
||||||
|
return !(lhs == rhs);
|
||||||
|
}
|
||||||
|
|
||||||
struct Metadata
|
struct Metadata
|
||||||
{
|
{
|
||||||
|
@ -111,6 +122,38 @@ struct FileStatus
|
||||||
u32 size;
|
u32 size;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// The maximum number of components a path can have.
|
||||||
|
constexpr size_t MaxPathDepth = 8;
|
||||||
|
/// The maximum number of characters a path can have.
|
||||||
|
constexpr size_t MaxPathLength = 64;
|
||||||
|
|
||||||
|
/// Returns whether a Wii path is valid.
|
||||||
|
bool IsValidPath(std::string_view path);
|
||||||
|
bool IsValidNonRootPath(std::string_view path);
|
||||||
|
|
||||||
|
struct SplitPathResult
|
||||||
|
{
|
||||||
|
std::string parent;
|
||||||
|
std::string file_name;
|
||||||
|
};
|
||||||
|
inline bool operator==(const SplitPathResult& lhs, const SplitPathResult& rhs)
|
||||||
|
{
|
||||||
|
const auto fields = [](const SplitPathResult& obj) {
|
||||||
|
return std::tie(obj.parent, obj.file_name);
|
||||||
|
};
|
||||||
|
return fields(lhs) == fields(rhs);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool operator!=(const SplitPathResult& lhs, const SplitPathResult& rhs)
|
||||||
|
{
|
||||||
|
return !(lhs == rhs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Split a path into a parent path and the file name. Takes a *valid non-root* path.
|
||||||
|
///
|
||||||
|
/// Example: /shared2/sys/SYSCONF => {/shared2/sys, SYSCONF}
|
||||||
|
SplitPathResult SplitPathAndBasename(std::string_view path);
|
||||||
|
|
||||||
class FileSystem;
|
class FileSystem;
|
||||||
class FileHandle final
|
class FileHandle final
|
||||||
{
|
{
|
||||||
|
@ -196,9 +239,6 @@ public:
|
||||||
virtual Result<NandStats> GetNandStats() = 0;
|
virtual Result<NandStats> GetNandStats() = 0;
|
||||||
/// Get usage information about a directory (used cluster and inode counts).
|
/// Get usage information about a directory (used cluster and inode counts).
|
||||||
virtual Result<DirectoryStats> GetDirectoryStats(const std::string& path) = 0;
|
virtual Result<DirectoryStats> GetDirectoryStats(const std::string& path) = 0;
|
||||||
|
|
||||||
protected:
|
|
||||||
void Init();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
template <typename T>
|
template <typename T>
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
#include "Core/HW/Memmap.h"
|
#include "Core/HW/Memmap.h"
|
||||||
#include "Core/HW/SystemTimers.h"
|
#include "Core/HW/SystemTimers.h"
|
||||||
#include "Core/IOS/FS/FileSystem.h"
|
#include "Core/IOS/FS/FileSystem.h"
|
||||||
|
#include "Core/IOS/Uids.h"
|
||||||
|
|
||||||
namespace IOS::HLE::Device
|
namespace IOS::HLE::Device
|
||||||
{
|
{
|
||||||
|
@ -37,6 +38,11 @@ constexpr size_t CLUSTER_DATA_SIZE = 0x4000;
|
||||||
|
|
||||||
FS::FS(Kernel& ios, const std::string& device_name) : Device(ios, device_name)
|
FS::FS(Kernel& ios, const std::string& device_name) : Device(ios, device_name)
|
||||||
{
|
{
|
||||||
|
if (ios.GetFS()->Delete(PID_KERNEL, PID_KERNEL, "/tmp") == ResultCode::Success)
|
||||||
|
{
|
||||||
|
ios.GetFS()->CreateDirectory(PID_KERNEL, PID_KERNEL, "/tmp", 0,
|
||||||
|
{Mode::ReadWrite, Mode::ReadWrite, Mode::ReadWrite});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void FS::DoState(PointerWrap& p)
|
void FS::DoState(PointerWrap& p)
|
||||||
|
|
|
@ -3,23 +3,26 @@
|
||||||
// Refer to the license.txt file included.
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <optional>
|
||||||
|
#include <string_view>
|
||||||
|
#include <type_traits>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include <fmt/format.h>
|
||||||
|
|
||||||
#include "Common/Assert.h"
|
#include "Common/Assert.h"
|
||||||
#include "Common/ChunkFile.h"
|
#include "Common/ChunkFile.h"
|
||||||
#include "Common/FileUtil.h"
|
#include "Common/FileUtil.h"
|
||||||
#include "Common/Logging/Log.h"
|
#include "Common/Logging/Log.h"
|
||||||
#include "Common/NandPaths.h"
|
#include "Common/NandPaths.h"
|
||||||
|
#include "Common/StringUtil.h"
|
||||||
|
#include "Common/Swap.h"
|
||||||
#include "Core/IOS/ES/ES.h"
|
#include "Core/IOS/ES/ES.h"
|
||||||
#include "Core/IOS/FS/HostBackend/FS.h"
|
#include "Core/IOS/FS/HostBackend/FS.h"
|
||||||
#include "Core/IOS/IOS.h"
|
#include "Core/IOS/IOS.h"
|
||||||
|
|
||||||
namespace IOS::HLE::FS
|
namespace IOS::HLE::FS
|
||||||
{
|
{
|
||||||
static bool IsValidWiiPath(const std::string& path)
|
|
||||||
{
|
|
||||||
return path.compare(0, 1, "/") == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string HostFileSystem::BuildFilename(const std::string& wii_path) const
|
std::string HostFileSystem::BuildFilename(const std::string& wii_path) const
|
||||||
{
|
{
|
||||||
if (wii_path.compare(0, 1, "/") == 0)
|
if (wii_path.compare(0, 1, "/") == 0)
|
||||||
|
@ -44,13 +47,186 @@ static u64 ComputeTotalFileSize(const File::FSTEntry& parent_entry)
|
||||||
return sizeOfFiles;
|
return sizeOfFiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
struct SerializedFstEntry
|
||||||
|
{
|
||||||
|
std::string_view GetName() const { return {name.data(), strnlen(name.data(), name.size())}; }
|
||||||
|
void SetName(std::string_view new_name)
|
||||||
|
{
|
||||||
|
std::memcpy(name.data(), new_name.data(), std::min(name.size(), new_name.length()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// File name
|
||||||
|
std::array<char, 12> name{};
|
||||||
|
/// File owner user ID
|
||||||
|
Common::BigEndianValue<Uid> uid{};
|
||||||
|
/// File owner group ID
|
||||||
|
Common::BigEndianValue<Gid> gid{};
|
||||||
|
/// Is this a file or a directory?
|
||||||
|
bool is_file = false;
|
||||||
|
/// File access modes
|
||||||
|
Modes modes{};
|
||||||
|
/// File attribute
|
||||||
|
FileAttribute attribute{};
|
||||||
|
/// Unknown property
|
||||||
|
Common::BigEndianValue<u32> x3{};
|
||||||
|
/// Number of children
|
||||||
|
Common::BigEndianValue<u32> num_children{};
|
||||||
|
};
|
||||||
|
static_assert(std::is_standard_layout<SerializedFstEntry>());
|
||||||
|
static_assert(sizeof(SerializedFstEntry) == 0x20);
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
auto GetMetadataFields(T& obj)
|
||||||
|
{
|
||||||
|
return std::tie(obj.uid, obj.gid, obj.is_file, obj.modes, obj.attribute);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto GetNamePredicate(const std::string& name)
|
||||||
|
{
|
||||||
|
return [&name](const auto& entry) { return entry.name == name; };
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool HostFileSystem::FstEntry::CheckPermission(Uid caller_uid, Gid caller_gid,
|
||||||
|
Mode requested_mode) const
|
||||||
|
{
|
||||||
|
if (caller_uid == 0)
|
||||||
|
return true;
|
||||||
|
Mode file_mode = data.modes.other;
|
||||||
|
if (data.uid == caller_uid)
|
||||||
|
file_mode = data.modes.owner;
|
||||||
|
else if (data.gid == caller_gid)
|
||||||
|
file_mode = data.modes.group;
|
||||||
|
return (u8(requested_mode) & u8(file_mode)) == u8(requested_mode);
|
||||||
|
}
|
||||||
|
|
||||||
HostFileSystem::HostFileSystem(const std::string& root_path) : m_root_path{root_path}
|
HostFileSystem::HostFileSystem(const std::string& root_path) : m_root_path{root_path}
|
||||||
{
|
{
|
||||||
Init();
|
File::CreateFullPath(m_root_path + "/");
|
||||||
|
ResetFst();
|
||||||
|
LoadFst();
|
||||||
}
|
}
|
||||||
|
|
||||||
HostFileSystem::~HostFileSystem() = default;
|
HostFileSystem::~HostFileSystem() = default;
|
||||||
|
|
||||||
|
std::string HostFileSystem::GetFstFilePath() const
|
||||||
|
{
|
||||||
|
return fmt::format("{}/fst.bin", m_root_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HostFileSystem::ResetFst()
|
||||||
|
{
|
||||||
|
m_root_entry = {};
|
||||||
|
m_root_entry.name = "/";
|
||||||
|
// Mode 0x16 (Directory | Owner_None | Group_Read | Other_Read) in the FS sysmodule
|
||||||
|
m_root_entry.data.modes = {Mode::None, Mode::Read, Mode::Read};
|
||||||
|
}
|
||||||
|
|
||||||
|
void HostFileSystem::LoadFst()
|
||||||
|
{
|
||||||
|
File::IOFile file{GetFstFilePath(), "rb"};
|
||||||
|
// Existing filesystems will not have a FST. This is not a problem,
|
||||||
|
// as the rest of HostFileSystem will use sane defaults.
|
||||||
|
if (!file)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const auto parse_entry = [&file](const auto& parse, size_t depth) -> std::optional<FstEntry> {
|
||||||
|
if (depth > MaxPathDepth)
|
||||||
|
return std::nullopt;
|
||||||
|
|
||||||
|
SerializedFstEntry entry;
|
||||||
|
if (!file.ReadArray(&entry, 1))
|
||||||
|
return std::nullopt;
|
||||||
|
|
||||||
|
FstEntry result;
|
||||||
|
result.name = entry.GetName();
|
||||||
|
GetMetadataFields(result.data) = GetMetadataFields(entry);
|
||||||
|
for (size_t i = 0; i < entry.num_children; ++i)
|
||||||
|
{
|
||||||
|
const auto maybe_child = parse(parse, depth + 1);
|
||||||
|
if (!maybe_child.has_value())
|
||||||
|
return std::nullopt;
|
||||||
|
result.children.push_back(*maybe_child);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto root_entry = parse_entry(parse_entry, 0);
|
||||||
|
if (!root_entry.has_value())
|
||||||
|
{
|
||||||
|
ERROR_LOG(IOS_FS, "Failed to parse FST: at least one of the entries was invalid");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_root_entry = *root_entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HostFileSystem::SaveFst()
|
||||||
|
{
|
||||||
|
std::vector<SerializedFstEntry> to_write;
|
||||||
|
auto collect_entries = [&to_write](const auto& collect, const FstEntry& entry) -> void {
|
||||||
|
SerializedFstEntry& serialized = to_write.emplace_back();
|
||||||
|
serialized.SetName(entry.name);
|
||||||
|
GetMetadataFields(serialized) = GetMetadataFields(entry.data);
|
||||||
|
serialized.num_children = u32(entry.children.size());
|
||||||
|
for (const FstEntry& child : entry.children)
|
||||||
|
collect(collect, child);
|
||||||
|
};
|
||||||
|
collect_entries(collect_entries, m_root_entry);
|
||||||
|
|
||||||
|
const std::string dest_path = GetFstFilePath();
|
||||||
|
const std::string temp_path = File::GetTempFilenameForAtomicWrite(dest_path);
|
||||||
|
File::IOFile file{temp_path, "wb"};
|
||||||
|
if (!file.WriteArray(to_write.data(), to_write.size()) || !File::Rename(temp_path, dest_path))
|
||||||
|
ERROR_LOG(IOS_FS, "Failed to write new FST");
|
||||||
|
}
|
||||||
|
|
||||||
|
HostFileSystem::FstEntry* HostFileSystem::GetFstEntryForPath(const std::string& path)
|
||||||
|
{
|
||||||
|
if (path == "/")
|
||||||
|
return &m_root_entry;
|
||||||
|
|
||||||
|
if (!IsValidNonRootPath(path))
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
const File::FileInfo host_file_info{BuildFilename(path)};
|
||||||
|
if (!host_file_info.Exists())
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
FstEntry* entry = &m_root_entry;
|
||||||
|
std::string complete_path = "";
|
||||||
|
for (const std::string& component : SplitString(std::string(path.substr(1)), '/'))
|
||||||
|
{
|
||||||
|
complete_path += '/' + component;
|
||||||
|
const auto next =
|
||||||
|
std::find_if(entry->children.begin(), entry->children.end(), GetNamePredicate(component));
|
||||||
|
if (next != entry->children.end())
|
||||||
|
{
|
||||||
|
entry = &*next;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Fall back to dummy data to avoid breaking existing filesystems.
|
||||||
|
// This code path is also reached when creating a new file or directory;
|
||||||
|
// proper metadata is filled in later.
|
||||||
|
INFO_LOG(IOS_FS, "Creating a default entry for %s", complete_path.c_str());
|
||||||
|
entry = &entry->children.emplace_back();
|
||||||
|
entry->name = component;
|
||||||
|
entry->data.modes = {Mode::ReadWrite, Mode::ReadWrite, Mode::ReadWrite};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entry->data.is_file = host_file_info.IsFile();
|
||||||
|
if (entry->data.is_file && !entry->children.empty())
|
||||||
|
{
|
||||||
|
WARN_LOG(IOS_FS, "%s is a file but also has children; clearing children", path.c_str());
|
||||||
|
entry->children.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
void HostFileSystem::DoState(PointerWrap& p)
|
void HostFileSystem::DoState(PointerWrap& p)
|
||||||
{
|
{
|
||||||
// Temporarily close the file, to prevent any issues with the savestating of /tmp
|
// Temporarily close the file, to prevent any issues with the savestating of /tmp
|
||||||
|
@ -159,179 +335,305 @@ void HostFileSystem::DoState(PointerWrap& p)
|
||||||
|
|
||||||
ResultCode HostFileSystem::Format(Uid uid)
|
ResultCode HostFileSystem::Format(Uid uid)
|
||||||
{
|
{
|
||||||
|
if (uid != 0)
|
||||||
|
return ResultCode::AccessDenied;
|
||||||
|
if (m_root_path.empty())
|
||||||
|
return ResultCode::AccessDenied;
|
||||||
const std::string root = BuildFilename("/");
|
const std::string root = BuildFilename("/");
|
||||||
if (!File::DeleteDirRecursively(root) || !File::CreateDir(root))
|
if (!File::DeleteDirRecursively(root) || !File::CreateDir(root))
|
||||||
return ResultCode::UnknownError;
|
return ResultCode::UnknownError;
|
||||||
|
ResetFst();
|
||||||
|
SaveFst();
|
||||||
|
// Reset and close all handles.
|
||||||
|
m_handles = {};
|
||||||
return ResultCode::Success;
|
return ResultCode::Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
ResultCode HostFileSystem::CreateFile(Uid, Gid, const std::string& path, FileAttribute, Modes)
|
ResultCode HostFileSystem::CreateFileOrDirectory(Uid uid, Gid gid, const std::string& path,
|
||||||
|
FileAttribute attr, Modes modes, bool is_file)
|
||||||
{
|
{
|
||||||
std::string file_name(BuildFilename(path));
|
if (!IsValidNonRootPath(path) || !std::all_of(path.begin(), path.end(), IsPrintableCharacter))
|
||||||
// check if the file already exist
|
return ResultCode::Invalid;
|
||||||
if (File::Exists(file_name))
|
|
||||||
|
if (!is_file && std::count(path.begin(), path.end(), '/') > int(MaxPathDepth))
|
||||||
|
return ResultCode::TooManyPathComponents;
|
||||||
|
|
||||||
|
const auto split_path = SplitPathAndBasename(path);
|
||||||
|
const std::string host_path = BuildFilename(path);
|
||||||
|
|
||||||
|
FstEntry* parent = GetFstEntryForPath(split_path.parent);
|
||||||
|
if (!parent)
|
||||||
|
return ResultCode::NotFound;
|
||||||
|
|
||||||
|
if (!parent->CheckPermission(uid, gid, Mode::Write))
|
||||||
|
return ResultCode::AccessDenied;
|
||||||
|
|
||||||
|
if (File::Exists(host_path))
|
||||||
return ResultCode::AlreadyExists;
|
return ResultCode::AlreadyExists;
|
||||||
|
|
||||||
// create the file
|
const bool ok = is_file ? File::CreateEmptyFile(host_path) : File::CreateDir(host_path);
|
||||||
File::CreateFullPath(file_name); // just to be sure
|
if (!ok)
|
||||||
if (!File::CreateEmptyFile(file_name))
|
|
||||||
{
|
{
|
||||||
ERROR_LOG(IOS_FS, "couldn't create new file");
|
ERROR_LOG(IOS_FS, "Failed to create file or directory: %s", host_path.c_str());
|
||||||
return ResultCode::Invalid;
|
return ResultCode::UnknownError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FstEntry* child = GetFstEntryForPath(path);
|
||||||
|
*child = {};
|
||||||
|
child->name = split_path.file_name;
|
||||||
|
child->data.is_file = is_file;
|
||||||
|
child->data.modes = modes;
|
||||||
|
child->data.uid = uid;
|
||||||
|
child->data.gid = gid;
|
||||||
|
child->data.attribute = attr;
|
||||||
|
SaveFst();
|
||||||
return ResultCode::Success;
|
return ResultCode::Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
ResultCode HostFileSystem::CreateDirectory(Uid, Gid, const std::string& path, FileAttribute, Modes)
|
ResultCode HostFileSystem::CreateFile(Uid uid, Gid gid, const std::string& path, FileAttribute attr,
|
||||||
|
Modes modes)
|
||||||
{
|
{
|
||||||
if (!IsValidWiiPath(path))
|
return CreateFileOrDirectory(uid, gid, path, attr, modes, true);
|
||||||
return ResultCode::Invalid;
|
|
||||||
|
|
||||||
std::string name(BuildFilename(path));
|
|
||||||
|
|
||||||
name += "/";
|
|
||||||
File::CreateFullPath(name);
|
|
||||||
DEBUG_ASSERT_MSG(IOS_FS, File::IsDirectory(name), "CREATE_DIR %s failed", name.c_str());
|
|
||||||
|
|
||||||
return ResultCode::Success;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ResultCode HostFileSystem::Delete(Uid, Gid, const std::string& path)
|
ResultCode HostFileSystem::CreateDirectory(Uid uid, Gid gid, const std::string& path,
|
||||||
|
FileAttribute attr, Modes modes)
|
||||||
{
|
{
|
||||||
if (!IsValidWiiPath(path))
|
return CreateFileOrDirectory(uid, gid, path, attr, modes, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HostFileSystem::IsFileOpened(const std::string& path) const
|
||||||
|
{
|
||||||
|
return std::any_of(m_handles.begin(), m_handles.end(), [&path](const Handle& handle) {
|
||||||
|
return handle.opened && handle.wii_path == path;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HostFileSystem::IsDirectoryInUse(const std::string& path) const
|
||||||
|
{
|
||||||
|
return std::any_of(m_handles.begin(), m_handles.end(), [&path](const Handle& handle) {
|
||||||
|
return handle.opened && StringBeginsWith(handle.wii_path, path);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ResultCode HostFileSystem::Delete(Uid uid, Gid gid, const std::string& path)
|
||||||
|
{
|
||||||
|
if (!IsValidNonRootPath(path))
|
||||||
return ResultCode::Invalid;
|
return ResultCode::Invalid;
|
||||||
|
|
||||||
const std::string file_name = BuildFilename(path);
|
const std::string host_path = BuildFilename(path);
|
||||||
if (File::Delete(file_name))
|
const auto split_path = SplitPathAndBasename(path);
|
||||||
INFO_LOG(IOS_FS, "DeleteFile %s", file_name.c_str());
|
|
||||||
else if (File::DeleteDirRecursively(file_name))
|
FstEntry* parent = GetFstEntryForPath(split_path.parent);
|
||||||
INFO_LOG(IOS_FS, "DeleteDir %s", file_name.c_str());
|
if (!parent)
|
||||||
|
return ResultCode::NotFound;
|
||||||
|
|
||||||
|
if (!parent->CheckPermission(uid, gid, Mode::Write))
|
||||||
|
return ResultCode::AccessDenied;
|
||||||
|
|
||||||
|
if (!File::Exists(host_path))
|
||||||
|
return ResultCode::NotFound;
|
||||||
|
|
||||||
|
if (File::IsFile(host_path) && !IsFileOpened(path))
|
||||||
|
File::Delete(host_path);
|
||||||
|
else if (File::IsDirectory(host_path) && !IsDirectoryInUse(path))
|
||||||
|
File::DeleteDirRecursively(host_path);
|
||||||
else
|
else
|
||||||
WARN_LOG(IOS_FS, "DeleteFile %s - failed!!!", file_name.c_str());
|
return ResultCode::InUse;
|
||||||
|
|
||||||
|
const auto it = std::find_if(parent->children.begin(), parent->children.end(),
|
||||||
|
GetNamePredicate(split_path.file_name));
|
||||||
|
if (it != parent->children.end())
|
||||||
|
parent->children.erase(it);
|
||||||
|
SaveFst();
|
||||||
|
|
||||||
return ResultCode::Success;
|
return ResultCode::Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
ResultCode HostFileSystem::Rename(Uid, Gid, const std::string& old_path,
|
ResultCode HostFileSystem::Rename(Uid uid, Gid gid, const std::string& old_path,
|
||||||
const std::string& new_path)
|
const std::string& new_path)
|
||||||
{
|
{
|
||||||
if (!IsValidWiiPath(old_path))
|
if (!IsValidNonRootPath(old_path) || !IsValidNonRootPath(new_path))
|
||||||
return ResultCode::Invalid;
|
return ResultCode::Invalid;
|
||||||
const std::string old_name = BuildFilename(old_path);
|
|
||||||
|
|
||||||
if (!IsValidWiiPath(new_path))
|
const auto split_old_path = SplitPathAndBasename(old_path);
|
||||||
|
const auto split_new_path = SplitPathAndBasename(new_path);
|
||||||
|
|
||||||
|
FstEntry* old_parent = GetFstEntryForPath(split_old_path.parent);
|
||||||
|
FstEntry* new_parent = GetFstEntryForPath(split_new_path.parent);
|
||||||
|
if (!old_parent || !new_parent)
|
||||||
|
return ResultCode::NotFound;
|
||||||
|
|
||||||
|
if (!old_parent->CheckPermission(uid, gid, Mode::Write) ||
|
||||||
|
!new_parent->CheckPermission(uid, gid, Mode::Write))
|
||||||
|
{
|
||||||
|
return ResultCode::AccessDenied;
|
||||||
|
}
|
||||||
|
|
||||||
|
FstEntry* entry = GetFstEntryForPath(old_path);
|
||||||
|
if (!entry)
|
||||||
|
return ResultCode::NotFound;
|
||||||
|
|
||||||
|
// For files, the file name is not allowed to change.
|
||||||
|
if (entry->data.is_file && split_old_path.file_name != split_new_path.file_name)
|
||||||
return ResultCode::Invalid;
|
return ResultCode::Invalid;
|
||||||
const std::string new_name = BuildFilename(new_path);
|
|
||||||
|
|
||||||
// try to make the basis directory
|
if ((!entry->data.is_file && IsDirectoryInUse(old_path)) ||
|
||||||
File::CreateFullPath(new_name);
|
(entry->data.is_file && IsFileOpened(old_path)))
|
||||||
|
{
|
||||||
|
return ResultCode::InUse;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string host_old_path = BuildFilename(old_path);
|
||||||
|
const std::string host_new_path = BuildFilename(new_path);
|
||||||
|
|
||||||
// If there is already something of the same type at the new path, delete it.
|
// If there is already something of the same type at the new path, delete it.
|
||||||
if (File::Exists(new_name))
|
if (File::Exists(host_new_path))
|
||||||
{
|
{
|
||||||
const bool old_is_file = File::IsFile(old_name);
|
const bool old_is_file = File::IsFile(host_old_path);
|
||||||
const bool new_is_file = File::IsFile(new_name);
|
const bool new_is_file = File::IsFile(host_new_path);
|
||||||
if (old_is_file && new_is_file)
|
if (old_is_file && new_is_file)
|
||||||
File::Delete(new_name);
|
File::Delete(host_new_path);
|
||||||
else if (!old_is_file && !new_is_file)
|
else if (!old_is_file && !new_is_file)
|
||||||
File::DeleteDirRecursively(new_name);
|
File::DeleteDirRecursively(host_new_path);
|
||||||
else
|
else
|
||||||
return ResultCode::Invalid;
|
return ResultCode::Invalid;
|
||||||
}
|
}
|
||||||
|
|
||||||
// finally try to rename the file
|
if (!File::Rename(host_old_path, host_new_path))
|
||||||
if (!File::Rename(old_name, new_name))
|
|
||||||
{
|
{
|
||||||
ERROR_LOG(IOS_FS, "Rename %s to %s - failed", old_name.c_str(), new_name.c_str());
|
ERROR_LOG(IOS_FS, "Rename %s to %s - failed", host_old_path.c_str(), host_new_path.c_str());
|
||||||
return ResultCode::NotFound;
|
return ResultCode::NotFound;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Finally, remove the child from the old parent and move it to the new parent.
|
||||||
|
const auto it = std::find_if(old_parent->children.begin(), old_parent->children.end(),
|
||||||
|
GetNamePredicate(split_old_path.file_name));
|
||||||
|
FstEntry* new_entry = GetFstEntryForPath(new_path);
|
||||||
|
if (it != old_parent->children.end())
|
||||||
|
{
|
||||||
|
*new_entry = *it;
|
||||||
|
old_parent->children.erase(it);
|
||||||
|
}
|
||||||
|
new_entry->name = split_new_path.file_name;
|
||||||
|
SaveFst();
|
||||||
|
|
||||||
return ResultCode::Success;
|
return ResultCode::Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
Result<std::vector<std::string>> HostFileSystem::ReadDirectory(Uid, Gid, const std::string& path)
|
Result<std::vector<std::string>> HostFileSystem::ReadDirectory(Uid uid, Gid gid,
|
||||||
|
const std::string& path)
|
||||||
{
|
{
|
||||||
if (!IsValidWiiPath(path))
|
if (!IsValidPath(path))
|
||||||
return ResultCode::Invalid;
|
return ResultCode::Invalid;
|
||||||
|
|
||||||
// the Wii uses this function to define the type (dir or file)
|
const FstEntry* entry = GetFstEntryForPath(path);
|
||||||
const std::string dir_name(BuildFilename(path));
|
if (!entry)
|
||||||
|
|
||||||
const File::FileInfo file_info(dir_name);
|
|
||||||
|
|
||||||
if (!file_info.Exists())
|
|
||||||
{
|
|
||||||
WARN_LOG(IOS_FS, "Search not found: %s", dir_name.c_str());
|
|
||||||
return ResultCode::NotFound;
|
return ResultCode::NotFound;
|
||||||
}
|
|
||||||
|
|
||||||
if (!file_info.IsDirectory())
|
if (!entry->CheckPermission(uid, gid, Mode::Read))
|
||||||
{
|
return ResultCode::AccessDenied;
|
||||||
// It's not a directory, so error.
|
|
||||||
|
if (entry->data.is_file)
|
||||||
return ResultCode::Invalid;
|
return ResultCode::Invalid;
|
||||||
}
|
|
||||||
|
|
||||||
File::FSTEntry entry = File::ScanDirectoryTree(dir_name, false);
|
const std::string host_path = BuildFilename(path);
|
||||||
|
File::FSTEntry host_entry = File::ScanDirectoryTree(host_path, false);
|
||||||
for (File::FSTEntry& child : entry.children)
|
for (File::FSTEntry& child : host_entry.children)
|
||||||
{
|
{
|
||||||
// Decode escaped invalid file system characters so that games (such as
|
// Decode escaped invalid file system characters so that games (such as
|
||||||
// Harry Potter and the Half-Blood Prince) can find what they expect.
|
// Harry Potter and the Half-Blood Prince) can find what they expect.
|
||||||
child.virtualName = Common::UnescapeFileName(child.virtualName);
|
child.virtualName = Common::UnescapeFileName(child.virtualName);
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE(leoetlino): this is absolutely wrong, but there is no way to fix this properly
|
// Sort files according to their order in the FST tree (issue 10234).
|
||||||
// if we use the host filesystem.
|
// The result should look like this:
|
||||||
std::sort(entry.children.begin(), entry.children.end(),
|
// [FilesNotInFst, ..., OldestFileInFst, ..., NewestFileInFst]
|
||||||
[](const File::FSTEntry& one, const File::FSTEntry& two) {
|
std::unordered_map<std::string_view, int> sort_keys;
|
||||||
return one.virtualName < two.virtualName;
|
sort_keys.reserve(entry->children.size());
|
||||||
|
for (size_t i = 0; i < entry->children.size(); ++i)
|
||||||
|
sort_keys.emplace(entry->children[i].name, int(i));
|
||||||
|
|
||||||
|
const auto get_key = [&sort_keys](std::string_view key) {
|
||||||
|
const auto it = sort_keys.find(key);
|
||||||
|
// As a fallback, files that are not in the FST are put at the beginning.
|
||||||
|
return it != sort_keys.end() ? it->second : -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Now sort in reverse order because Nintendo traverses a linked list
|
||||||
|
// in which new elements are inserted at the front.
|
||||||
|
std::sort(host_entry.children.begin(), host_entry.children.end(),
|
||||||
|
[&get_key](const File::FSTEntry& one, const File::FSTEntry& two) {
|
||||||
|
const int key1 = get_key(one.virtualName);
|
||||||
|
const int key2 = get_key(two.virtualName);
|
||||||
|
if (key1 != key2)
|
||||||
|
return key1 > key2;
|
||||||
|
|
||||||
|
// For files that are not in the FST, sort lexicographically to ensure that
|
||||||
|
// results are consistent no matter what the underlying filesystem is.
|
||||||
|
return one.virtualName > two.virtualName;
|
||||||
});
|
});
|
||||||
|
|
||||||
std::vector<std::string> output;
|
std::vector<std::string> output;
|
||||||
for (File::FSTEntry& child : entry.children)
|
for (const File::FSTEntry& child : host_entry.children)
|
||||||
output.emplace_back(child.virtualName);
|
output.emplace_back(child.virtualName);
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
Result<Metadata> HostFileSystem::GetMetadata(Uid, Gid, const std::string& path)
|
Result<Metadata> HostFileSystem::GetMetadata(Uid uid, Gid gid, const std::string& path)
|
||||||
{
|
{
|
||||||
Metadata metadata;
|
const FstEntry* entry = nullptr;
|
||||||
metadata.uid = 0;
|
if (path == "/")
|
||||||
metadata.gid = 0x3031; // this is also known as makercd, 01 (0x3031) for nintendo and 08
|
{
|
||||||
// (0x3038) for MH3 etc
|
entry = &m_root_entry;
|
||||||
|
}
|
||||||
if (!IsValidWiiPath(path))
|
else
|
||||||
|
{
|
||||||
|
if (!IsValidNonRootPath(path))
|
||||||
return ResultCode::Invalid;
|
return ResultCode::Invalid;
|
||||||
|
|
||||||
std::string file_name = BuildFilename(path);
|
const auto split_path = SplitPathAndBasename(path);
|
||||||
metadata.modes = {Mode::ReadWrite, Mode::ReadWrite, Mode::ReadWrite};
|
const FstEntry* parent = GetFstEntryForPath(split_path.parent);
|
||||||
metadata.attribute = 0x00; // no attributes
|
if (!parent)
|
||||||
|
return ResultCode::NotFound;
|
||||||
// Hack: if the path that is being accessed is within an installed title directory, get the
|
if (!parent->CheckPermission(uid, gid, Mode::Read))
|
||||||
// UID/GID from the installed title TMD.
|
return ResultCode::AccessDenied;
|
||||||
Kernel* ios = GetIOS();
|
entry = GetFstEntryForPath(path);
|
||||||
u64 title_id;
|
|
||||||
if (ios && IsTitlePath(file_name, Common::FROM_SESSION_ROOT, &title_id))
|
|
||||||
{
|
|
||||||
IOS::ES::TMDReader tmd = ios->GetES()->FindInstalledTMD(title_id);
|
|
||||||
if (tmd.IsValid())
|
|
||||||
metadata.gid = tmd.GetGroupId();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const File::FileInfo info{file_name};
|
if (!entry)
|
||||||
metadata.is_file = info.IsFile();
|
|
||||||
metadata.size = info.GetSize();
|
|
||||||
if (!info.Exists())
|
|
||||||
return ResultCode::NotFound;
|
return ResultCode::NotFound;
|
||||||
|
|
||||||
|
Metadata metadata = entry->data;
|
||||||
|
metadata.size = File::GetSize(BuildFilename(path));
|
||||||
return metadata;
|
return metadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
ResultCode HostFileSystem::SetMetadata(Uid caller_uid, const std::string& path, Uid uid, Gid gid,
|
ResultCode HostFileSystem::SetMetadata(Uid caller_uid, const std::string& path, Uid uid, Gid gid,
|
||||||
FileAttribute, Modes)
|
FileAttribute attr, Modes modes)
|
||||||
{
|
{
|
||||||
if (!IsValidWiiPath(path))
|
if (!IsValidPath(path))
|
||||||
return ResultCode::Invalid;
|
return ResultCode::Invalid;
|
||||||
|
|
||||||
|
FstEntry* entry = GetFstEntryForPath(path);
|
||||||
|
if (!entry)
|
||||||
|
return ResultCode::NotFound;
|
||||||
|
|
||||||
|
if (caller_uid != 0 && caller_uid != entry->data.uid)
|
||||||
|
return ResultCode::AccessDenied;
|
||||||
|
if (caller_uid != 0 && uid != entry->data.uid)
|
||||||
|
return ResultCode::AccessDenied;
|
||||||
|
|
||||||
|
const bool is_empty = File::GetSize(BuildFilename(path)) == 0;
|
||||||
|
if (entry->data.uid != uid && entry->data.is_file && !is_empty)
|
||||||
|
return ResultCode::FileNotEmpty;
|
||||||
|
|
||||||
|
entry->data.gid = gid;
|
||||||
|
entry->data.uid = uid;
|
||||||
|
entry->data.attribute = attr;
|
||||||
|
entry->data.modes = modes;
|
||||||
|
SaveFst();
|
||||||
|
|
||||||
return ResultCode::Success;
|
return ResultCode::Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -354,7 +656,7 @@ Result<NandStats> HostFileSystem::GetNandStats()
|
||||||
|
|
||||||
Result<DirectoryStats> HostFileSystem::GetDirectoryStats(const std::string& wii_path)
|
Result<DirectoryStats> HostFileSystem::GetDirectoryStats(const std::string& wii_path)
|
||||||
{
|
{
|
||||||
if (!IsValidWiiPath(wii_path))
|
if (!IsValidPath(wii_path))
|
||||||
return ResultCode::Invalid;
|
return ResultCode::Invalid;
|
||||||
|
|
||||||
DirectoryStats stats{};
|
DirectoryStats stats{};
|
||||||
|
|
|
@ -58,6 +58,20 @@ public:
|
||||||
Result<DirectoryStats> GetDirectoryStats(const std::string& path) override;
|
Result<DirectoryStats> GetDirectoryStats(const std::string& path) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
struct FstEntry
|
||||||
|
{
|
||||||
|
bool CheckPermission(Uid uid, Gid gid, Mode requested_mode) const;
|
||||||
|
|
||||||
|
std::string name;
|
||||||
|
Metadata data{};
|
||||||
|
/// Children of this FST entry. Only valid for directories.
|
||||||
|
///
|
||||||
|
/// We use a vector rather than a list here because iterating over children
|
||||||
|
/// happens a lot more often than removals.
|
||||||
|
/// Newly created entries are added at the end.
|
||||||
|
std::vector<FstEntry> children;
|
||||||
|
};
|
||||||
|
|
||||||
struct Handle
|
struct Handle
|
||||||
{
|
{
|
||||||
bool opened = false;
|
bool opened = false;
|
||||||
|
@ -73,6 +87,29 @@ private:
|
||||||
std::string BuildFilename(const std::string& wii_path) const;
|
std::string BuildFilename(const std::string& wii_path) const;
|
||||||
std::shared_ptr<File::IOFile> OpenHostFile(const std::string& host_path);
|
std::shared_ptr<File::IOFile> OpenHostFile(const std::string& host_path);
|
||||||
|
|
||||||
|
ResultCode CreateFileOrDirectory(Uid uid, Gid gid, const std::string& path,
|
||||||
|
FileAttribute attribute, Modes modes, bool is_file);
|
||||||
|
bool IsFileOpened(const std::string& path) const;
|
||||||
|
bool IsDirectoryInUse(const std::string& path) const;
|
||||||
|
|
||||||
|
std::string GetFstFilePath() const;
|
||||||
|
void ResetFst();
|
||||||
|
void LoadFst();
|
||||||
|
void SaveFst();
|
||||||
|
/// Get the FST entry for a file (or directory).
|
||||||
|
/// Automatically creates fallback entries for parents if they do not exist.
|
||||||
|
/// Returns nullptr if the path is invalid or the file does not exist.
|
||||||
|
FstEntry* GetFstEntryForPath(const std::string& path);
|
||||||
|
|
||||||
|
/// FST entry for the filesystem root.
|
||||||
|
///
|
||||||
|
/// Note that unlike a real Wii's FST, ours is the single source of truth only for
|
||||||
|
/// filesystem metadata and ordering. File existence must be checked by querying
|
||||||
|
/// the host filesystem.
|
||||||
|
/// The reasons for this design are twofold: existing users do not have a FST
|
||||||
|
/// and we do not want FS to break if the user adds or removes files in their
|
||||||
|
/// filesystem root manually.
|
||||||
|
FstEntry m_root_entry{};
|
||||||
std::string m_root_path;
|
std::string m_root_path;
|
||||||
std::map<std::string, std::weak_ptr<File::IOFile>> m_open_files;
|
std::map<std::string, std::weak_ptr<File::IOFile>> m_open_files;
|
||||||
std::array<Handle, 16> m_handles{};
|
std::array<Handle, 16> m_handles{};
|
||||||
|
|
|
@ -904,7 +904,7 @@ unsigned int NetPlayClient::OnData(sf::Packet& packet)
|
||||||
{
|
{
|
||||||
auto buffer = DecompressPacketIntoBuffer(packet);
|
auto buffer = DecompressPacketIntoBuffer(packet);
|
||||||
|
|
||||||
temp_fs->CreateDirectory(IOS::PID_KERNEL, IOS::PID_KERNEL, "/shared2/menu/FaceLib", 0,
|
temp_fs->CreateFullPath(IOS::PID_KERNEL, IOS::PID_KERNEL, "/shared2/menu/FaceLib/", 0,
|
||||||
fs_modes);
|
fs_modes);
|
||||||
auto file = temp_fs->CreateAndOpenFile(IOS::PID_KERNEL, IOS::PID_KERNEL,
|
auto file = temp_fs->CreateAndOpenFile(IOS::PID_KERNEL, IOS::PID_KERNEL,
|
||||||
Common::GetMiiDatabasePath(), fs_modes);
|
Common::GetMiiDatabasePath(), fs_modes);
|
||||||
|
@ -924,8 +924,8 @@ unsigned int NetPlayClient::OnData(sf::Packet& packet)
|
||||||
{
|
{
|
||||||
u64 title_id = Common::PacketReadU64(packet);
|
u64 title_id = Common::PacketReadU64(packet);
|
||||||
titles.push_back(title_id);
|
titles.push_back(title_id);
|
||||||
temp_fs->CreateDirectory(IOS::PID_KERNEL, IOS::PID_KERNEL,
|
temp_fs->CreateFullPath(IOS::PID_KERNEL, IOS::PID_KERNEL,
|
||||||
Common::GetTitleDataPath(title_id), 0, fs_modes);
|
Common::GetTitleDataPath(title_id) + '/', 0, fs_modes);
|
||||||
auto save = WiiSave::MakeNandStorage(temp_fs.get(), title_id);
|
auto save = WiiSave::MakeNandStorage(temp_fs.get(), title_id);
|
||||||
|
|
||||||
bool exists;
|
bool exists;
|
||||||
|
|
|
@ -34,9 +34,8 @@ static std::string s_temp_wii_root;
|
||||||
|
|
||||||
static void CopySave(FS::FileSystem* source, FS::FileSystem* dest, const u64 title_id)
|
static void CopySave(FS::FileSystem* source, FS::FileSystem* dest, const u64 title_id)
|
||||||
{
|
{
|
||||||
dest->CreateDirectory(IOS::PID_KERNEL, IOS::PID_KERNEL, Common::GetTitleDataPath(title_id), 0,
|
dest->CreateFullPath(IOS::PID_KERNEL, IOS::PID_KERNEL, Common::GetTitleDataPath(title_id) + '/',
|
||||||
{IOS::HLE::FS::Mode::ReadWrite, IOS::HLE::FS::Mode::ReadWrite,
|
0, {FS::Mode::ReadWrite, FS::Mode::ReadWrite, FS::Mode::ReadWrite});
|
||||||
IOS::HLE::FS::Mode::ReadWrite});
|
|
||||||
const auto source_save = WiiSave::MakeNandStorage(source, title_id);
|
const auto source_save = WiiSave::MakeNandStorage(source, title_id);
|
||||||
const auto dest_save = WiiSave::MakeNandStorage(dest, title_id);
|
const auto dest_save = WiiSave::MakeNandStorage(dest, title_id);
|
||||||
WiiSave::Copy(source_save.get(), dest_save.get());
|
WiiSave::Copy(source_save.get(), dest_save.get());
|
||||||
|
@ -49,9 +48,8 @@ static bool CopyNandFile(FS::FileSystem* source_fs, const std::string& source_fi
|
||||||
if (last_slash != std::string::npos && last_slash > 0)
|
if (last_slash != std::string::npos && last_slash > 0)
|
||||||
{
|
{
|
||||||
const std::string dir = dest_file.substr(0, last_slash);
|
const std::string dir = dest_file.substr(0, last_slash);
|
||||||
dest_fs->CreateDirectory(IOS::PID_KERNEL, IOS::PID_KERNEL, dir, 0,
|
dest_fs->CreateFullPath(IOS::PID_KERNEL, IOS::PID_KERNEL, dir + '/', 0,
|
||||||
{IOS::HLE::FS::Mode::ReadWrite, IOS::HLE::FS::Mode::ReadWrite,
|
{FS::Mode::ReadWrite, FS::Mode::ReadWrite, FS::Mode::ReadWrite});
|
||||||
IOS::HLE::FS::Mode::ReadWrite});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto source_handle =
|
auto source_handle =
|
||||||
|
@ -190,7 +188,7 @@ static bool CopySysmenuFilesToFS(FS::FileSystem* fs, const std::string& host_sou
|
||||||
|
|
||||||
if (entry.isDirectory)
|
if (entry.isDirectory)
|
||||||
{
|
{
|
||||||
fs->CreateDirectory(IOS::SYSMENU_UID, IOS::SYSMENU_GID, nand_path, 0, public_modes);
|
fs->CreateFullPath(IOS::SYSMENU_UID, IOS::SYSMENU_GID, nand_path + '/', 0, public_modes);
|
||||||
if (!CopySysmenuFilesToFS(fs, host_path, nand_path))
|
if (!CopySysmenuFilesToFS(fs, host_path, nand_path))
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -259,12 +257,8 @@ void CleanUpWiiFileSystemContents()
|
||||||
|
|
||||||
// FS won't write the save if the directory doesn't exist
|
// FS won't write the save if the directory doesn't exist
|
||||||
const std::string title_path = Common::GetTitleDataPath(title_id);
|
const std::string title_path = Common::GetTitleDataPath(title_id);
|
||||||
if (!configured_fs->GetMetadata(IOS::PID_KERNEL, IOS::PID_KERNEL, title_path))
|
configured_fs->CreateFullPath(IOS::PID_KERNEL, IOS::PID_KERNEL, title_path + '/', 0,
|
||||||
{
|
{FS::Mode::ReadWrite, FS::Mode::ReadWrite, FS::Mode::ReadWrite});
|
||||||
configured_fs->CreateDirectory(IOS::PID_KERNEL, IOS::PID_KERNEL, title_path, 0,
|
|
||||||
{IOS::HLE::FS::Mode::ReadWrite, IOS::HLE::FS::Mode::ReadWrite,
|
|
||||||
IOS::HLE::FS::Mode::ReadWrite});
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto user_save = WiiSave::MakeNandStorage(configured_fs.get(), title_id);
|
const auto user_save = WiiSave::MakeNandStorage(configured_fs.get(), title_id);
|
||||||
|
|
||||||
|
|
|
@ -2,8 +2,10 @@
|
||||||
// Licensed under GPLv2+
|
// Licensed under GPLv2+
|
||||||
// Refer to the license.txt file included.
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
|
@ -39,6 +41,41 @@ private:
|
||||||
std::string m_profile_path;
|
std::string m_profile_path;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
TEST(FileSystem, BasicPathValidity)
|
||||||
|
{
|
||||||
|
EXPECT_TRUE(IsValidPath("/"));
|
||||||
|
EXPECT_FALSE(IsValidNonRootPath("/"));
|
||||||
|
|
||||||
|
EXPECT_TRUE(IsValidNonRootPath("/shared2/sys/SYSCONF"));
|
||||||
|
EXPECT_TRUE(IsValidNonRootPath("/shared2/sys"));
|
||||||
|
EXPECT_TRUE(IsValidNonRootPath("/shared2"));
|
||||||
|
|
||||||
|
// Paths must start with /.
|
||||||
|
EXPECT_FALSE(IsValidNonRootPath("\\test"));
|
||||||
|
// Paths must not end with /.
|
||||||
|
EXPECT_FALSE(IsValidNonRootPath("/shared2/sys/"));
|
||||||
|
// Paths must not be longer than 64 characters.
|
||||||
|
EXPECT_FALSE(IsValidPath(
|
||||||
|
"/abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(FileSystem, PathSplitting)
|
||||||
|
{
|
||||||
|
SplitPathResult result;
|
||||||
|
|
||||||
|
result = {"/shared1", "00000042.app"};
|
||||||
|
EXPECT_EQ(SplitPathAndBasename("/shared1/00000042.app"), result);
|
||||||
|
|
||||||
|
result = {"/shared2/sys", "SYSCONF"};
|
||||||
|
EXPECT_EQ(SplitPathAndBasename("/shared2/sys/SYSCONF"), result);
|
||||||
|
|
||||||
|
result = {"/shared2", "sys"};
|
||||||
|
EXPECT_EQ(SplitPathAndBasename("/shared2/sys"), result);
|
||||||
|
|
||||||
|
result = {"/", "shared2"};
|
||||||
|
EXPECT_EQ(SplitPathAndBasename("/shared2"), result);
|
||||||
|
}
|
||||||
|
|
||||||
TEST_F(FileSystemTest, EssentialDirectories)
|
TEST_F(FileSystemTest, EssentialDirectories)
|
||||||
{
|
{
|
||||||
for (const std::string& path :
|
for (const std::string& path :
|
||||||
|
@ -52,41 +89,59 @@ TEST_F(FileSystemTest, CreateFile)
|
||||||
{
|
{
|
||||||
const std::string PATH = "/tmp/f";
|
const std::string PATH = "/tmp/f";
|
||||||
|
|
||||||
ASSERT_EQ(m_fs->CreateFile(Uid{0}, Gid{0}, PATH, 0, modes), ResultCode::Success);
|
constexpr u8 ArbitraryAttribute = 0xE1;
|
||||||
|
|
||||||
|
ASSERT_EQ(m_fs->CreateFile(Uid{0}, Gid{0}, PATH, ArbitraryAttribute, modes), ResultCode::Success);
|
||||||
|
|
||||||
const Result<Metadata> stats = m_fs->GetMetadata(Uid{0}, Gid{0}, PATH);
|
const Result<Metadata> stats = m_fs->GetMetadata(Uid{0}, Gid{0}, PATH);
|
||||||
ASSERT_TRUE(stats.Succeeded());
|
ASSERT_TRUE(stats.Succeeded());
|
||||||
EXPECT_TRUE(stats->is_file);
|
EXPECT_TRUE(stats->is_file);
|
||||||
EXPECT_EQ(stats->size, 0u);
|
EXPECT_EQ(stats->size, 0u);
|
||||||
// TODO: After we start saving metadata correctly, check the UID, GID, permissions
|
EXPECT_EQ(stats->uid, 0);
|
||||||
// as well (issue 10234).
|
EXPECT_EQ(stats->gid, 0);
|
||||||
|
EXPECT_EQ(stats->modes, modes);
|
||||||
|
EXPECT_EQ(stats->attribute, ArbitraryAttribute);
|
||||||
|
|
||||||
ASSERT_EQ(m_fs->CreateFile(Uid{0}, Gid{0}, PATH, 0, modes), ResultCode::AlreadyExists);
|
ASSERT_EQ(m_fs->CreateFile(Uid{0}, Gid{0}, PATH, 0, modes), ResultCode::AlreadyExists);
|
||||||
|
|
||||||
const Result<std::vector<std::string>> tmp_files = m_fs->ReadDirectory(Uid{0}, Gid{0}, "/tmp");
|
const Result<std::vector<std::string>> tmp_files = m_fs->ReadDirectory(Uid{0}, Gid{0}, "/tmp");
|
||||||
ASSERT_TRUE(tmp_files.Succeeded());
|
ASSERT_TRUE(tmp_files.Succeeded());
|
||||||
EXPECT_EQ(std::count(tmp_files->begin(), tmp_files->end(), "f"), 1u);
|
EXPECT_EQ(std::count(tmp_files->begin(), tmp_files->end(), "f"), 1u);
|
||||||
|
|
||||||
|
// Test invalid paths
|
||||||
|
// Unprintable characters
|
||||||
|
EXPECT_EQ(m_fs->CreateFile(Uid{0}, Gid{0}, "/tmp/tes\1t", 0, modes), ResultCode::Invalid);
|
||||||
|
EXPECT_EQ(m_fs->CreateFile(Uid{0}, Gid{0}, "/tmp/te\x7fst", 0, modes), ResultCode::Invalid);
|
||||||
|
// Paths with too many components are not rejected for files.
|
||||||
|
EXPECT_EQ(m_fs->CreateFile(Uid{0}, Gid{0}, "/1/2/3/4/5/6/7/8/9", 0, modes), ResultCode::NotFound);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(FileSystemTest, CreateDirectory)
|
TEST_F(FileSystemTest, CreateDirectory)
|
||||||
{
|
{
|
||||||
const std::string PATH = "/tmp/d";
|
const std::string PATH = "/tmp/d";
|
||||||
|
|
||||||
ASSERT_EQ(m_fs->CreateDirectory(Uid{0}, Gid{0}, PATH, 0, modes), ResultCode::Success);
|
constexpr u8 ArbitraryAttribute = 0x20;
|
||||||
|
|
||||||
|
ASSERT_EQ(m_fs->CreateDirectory(Uid{0}, Gid{0}, PATH, ArbitraryAttribute, modes),
|
||||||
|
ResultCode::Success);
|
||||||
|
|
||||||
const Result<Metadata> stats = m_fs->GetMetadata(Uid{0}, Gid{0}, PATH);
|
const Result<Metadata> stats = m_fs->GetMetadata(Uid{0}, Gid{0}, PATH);
|
||||||
ASSERT_TRUE(stats.Succeeded());
|
ASSERT_TRUE(stats.Succeeded());
|
||||||
EXPECT_FALSE(stats->is_file);
|
EXPECT_FALSE(stats->is_file);
|
||||||
// TODO: After we start saving metadata correctly, check the UID, GID, permissions
|
EXPECT_EQ(stats->uid, 0);
|
||||||
// as well (issue 10234).
|
EXPECT_EQ(stats->gid, 0);
|
||||||
|
EXPECT_EQ(stats->modes, modes);
|
||||||
|
EXPECT_EQ(stats->attribute, ArbitraryAttribute);
|
||||||
|
|
||||||
const Result<std::vector<std::string>> children = m_fs->ReadDirectory(Uid{0}, Gid{0}, PATH);
|
const Result<std::vector<std::string>> children = m_fs->ReadDirectory(Uid{0}, Gid{0}, PATH);
|
||||||
ASSERT_TRUE(children.Succeeded());
|
ASSERT_TRUE(children.Succeeded());
|
||||||
EXPECT_TRUE(children->empty());
|
EXPECT_TRUE(children->empty());
|
||||||
|
|
||||||
// TODO: uncomment this after the FS code is fixed to return AlreadyExists.
|
EXPECT_EQ(m_fs->CreateDirectory(Uid{0}, Gid{0}, PATH, 0, modes), ResultCode::AlreadyExists);
|
||||||
// EXPECT_EQ(m_fs->CreateDirectory(Uid{0}, Gid{0}, PATH, 0, Mode::Read, Mode::None, Mode::None),
|
|
||||||
// ResultCode::AlreadyExists);
|
// Paths with too many components should be rejected.
|
||||||
|
EXPECT_EQ(m_fs->CreateDirectory(Uid{0}, Gid{0}, "/1/2/3/4/5/6/7/8/9", 0, modes),
|
||||||
|
ResultCode::TooManyPathComponents);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(FileSystemTest, Delete)
|
TEST_F(FileSystemTest, Delete)
|
||||||
|
@ -94,6 +149,25 @@ TEST_F(FileSystemTest, Delete)
|
||||||
EXPECT_TRUE(m_fs->ReadDirectory(Uid{0}, Gid{0}, "/tmp").Succeeded());
|
EXPECT_TRUE(m_fs->ReadDirectory(Uid{0}, Gid{0}, "/tmp").Succeeded());
|
||||||
EXPECT_EQ(m_fs->Delete(Uid{0}, Gid{0}, "/tmp"), ResultCode::Success);
|
EXPECT_EQ(m_fs->Delete(Uid{0}, Gid{0}, "/tmp"), ResultCode::Success);
|
||||||
EXPECT_EQ(m_fs->ReadDirectory(Uid{0}, Gid{0}, "/tmp").Error(), ResultCode::NotFound);
|
EXPECT_EQ(m_fs->ReadDirectory(Uid{0}, Gid{0}, "/tmp").Error(), ResultCode::NotFound);
|
||||||
|
|
||||||
|
// Test recursive directory deletion.
|
||||||
|
ASSERT_EQ(m_fs->CreateDirectory(Uid{0}, Gid{0}, "/sys/1", 0, modes), ResultCode::Success);
|
||||||
|
ASSERT_EQ(m_fs->CreateDirectory(Uid{0}, Gid{0}, "/sys/1/2", 0, modes), ResultCode::Success);
|
||||||
|
ASSERT_EQ(m_fs->CreateFile(Uid{0}, Gid{0}, "/sys/1/2/3", 0, modes), ResultCode::Success);
|
||||||
|
ASSERT_EQ(m_fs->CreateFile(Uid{0}, Gid{0}, "/sys/1/2/4", 0, modes), ResultCode::Success);
|
||||||
|
|
||||||
|
// Leave a file open. Deletion should fail while the file is in use.
|
||||||
|
auto handle = std::make_optional(m_fs->OpenFile(Uid{0}, Gid{0}, "/sys/1/2/3", Mode::Read));
|
||||||
|
ASSERT_TRUE(handle->Succeeded());
|
||||||
|
EXPECT_EQ(m_fs->Delete(Uid{0}, Gid{0}, "/sys/1/2/3"), ResultCode::InUse);
|
||||||
|
// A directory that contains a file that is in use is considered to be in use,
|
||||||
|
// so this should fail too.
|
||||||
|
EXPECT_EQ(m_fs->Delete(Uid{0}, Gid{0}, "/sys/1"), ResultCode::InUse);
|
||||||
|
|
||||||
|
// With the handle closed, both of these should work:
|
||||||
|
handle.reset();
|
||||||
|
EXPECT_EQ(m_fs->Delete(Uid{0}, Gid{0}, "/sys/1/2/3"), ResultCode::Success);
|
||||||
|
EXPECT_EQ(m_fs->Delete(Uid{0}, Gid{0}, "/sys/1"), ResultCode::Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(FileSystemTest, Rename)
|
TEST_F(FileSystemTest, Rename)
|
||||||
|
@ -104,6 +178,14 @@ TEST_F(FileSystemTest, Rename)
|
||||||
|
|
||||||
EXPECT_EQ(m_fs->ReadDirectory(Uid{0}, Gid{0}, "/tmp").Error(), ResultCode::NotFound);
|
EXPECT_EQ(m_fs->ReadDirectory(Uid{0}, Gid{0}, "/tmp").Error(), ResultCode::NotFound);
|
||||||
EXPECT_TRUE(m_fs->ReadDirectory(Uid{0}, Gid{0}, "/test").Succeeded());
|
EXPECT_TRUE(m_fs->ReadDirectory(Uid{0}, Gid{0}, "/test").Succeeded());
|
||||||
|
|
||||||
|
// Rename /test back to /tmp.
|
||||||
|
EXPECT_EQ(m_fs->Rename(Uid{0}, Gid{0}, "/test", "/tmp"), ResultCode::Success);
|
||||||
|
|
||||||
|
// Create a file called /tmp/f1, and rename it to /tmp/f2.
|
||||||
|
// This should not work; file name changes are not allowed for files.
|
||||||
|
ASSERT_EQ(m_fs->CreateFile(Uid{0}, Gid{0}, "/tmp/f1", 0, modes), ResultCode::Success);
|
||||||
|
EXPECT_EQ(m_fs->Rename(Uid{0}, Gid{0}, "/tmp/f1", "/tmp/f2"), ResultCode::Invalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(FileSystemTest, RenameWithExistingTargetDirectory)
|
TEST_F(FileSystemTest, RenameWithExistingTargetDirectory)
|
||||||
|
@ -124,26 +206,29 @@ TEST_F(FileSystemTest, RenameWithExistingTargetDirectory)
|
||||||
|
|
||||||
TEST_F(FileSystemTest, RenameWithExistingTargetFile)
|
TEST_F(FileSystemTest, RenameWithExistingTargetFile)
|
||||||
{
|
{
|
||||||
|
const std::string source_path = "/sys/f2";
|
||||||
|
const std::string dest_path = "/tmp/f2";
|
||||||
|
|
||||||
// Create the test source file and write some data (so that we can check its size later on).
|
// Create the test source file and write some data (so that we can check its size later on).
|
||||||
ASSERT_EQ(m_fs->CreateFile(Uid{0}, Gid{0}, "/tmp/f1", 0, modes), ResultCode::Success);
|
ASSERT_EQ(m_fs->CreateFile(Uid{0}, Gid{0}, source_path, 0, modes), ResultCode::Success);
|
||||||
const std::vector<u8> TEST_DATA{{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}};
|
const std::vector<u8> TEST_DATA{{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}};
|
||||||
std::vector<u8> read_buffer(TEST_DATA.size());
|
std::vector<u8> read_buffer(TEST_DATA.size());
|
||||||
{
|
{
|
||||||
const Result<FileHandle> file = m_fs->OpenFile(Uid{0}, Gid{0}, "/tmp/f1", Mode::ReadWrite);
|
const Result<FileHandle> file = m_fs->OpenFile(Uid{0}, Gid{0}, source_path, Mode::ReadWrite);
|
||||||
ASSERT_TRUE(file.Succeeded());
|
ASSERT_TRUE(file.Succeeded());
|
||||||
ASSERT_TRUE(file->Write(TEST_DATA.data(), TEST_DATA.size()).Succeeded());
|
ASSERT_TRUE(file->Write(TEST_DATA.data(), TEST_DATA.size()).Succeeded());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the test target file and leave it empty.
|
// Create the test target file and leave it empty.
|
||||||
ASSERT_EQ(m_fs->CreateFile(Uid{0}, Gid{0}, "/tmp/f2", 0, modes), ResultCode::Success);
|
ASSERT_EQ(m_fs->CreateFile(Uid{0}, Gid{0}, dest_path, 0, modes), ResultCode::Success);
|
||||||
|
|
||||||
// Rename f1 to f2 and check that f1 replaced f2.
|
// Rename /sys/f2 to /tmp/f2 and check that f1 replaced f2.
|
||||||
EXPECT_EQ(m_fs->Rename(Uid{0}, Gid{0}, "/tmp/f1", "/tmp/f2"), ResultCode::Success);
|
EXPECT_EQ(m_fs->Rename(Uid{0}, Gid{0}, source_path, dest_path), ResultCode::Success);
|
||||||
|
|
||||||
ASSERT_FALSE(m_fs->GetMetadata(Uid{0}, Gid{0}, "/tmp/f1").Succeeded());
|
ASSERT_FALSE(m_fs->GetMetadata(Uid{0}, Gid{0}, source_path).Succeeded());
|
||||||
EXPECT_EQ(m_fs->GetMetadata(Uid{0}, Gid{0}, "/tmp/f1").Error(), ResultCode::NotFound);
|
EXPECT_EQ(m_fs->GetMetadata(Uid{0}, Gid{0}, source_path).Error(), ResultCode::NotFound);
|
||||||
|
|
||||||
const Result<Metadata> metadata = m_fs->GetMetadata(Uid{0}, Gid{0}, "/tmp/f2");
|
const Result<Metadata> metadata = m_fs->GetMetadata(Uid{0}, Gid{0}, dest_path);
|
||||||
ASSERT_TRUE(metadata.Succeeded());
|
ASSERT_TRUE(metadata.Succeeded());
|
||||||
EXPECT_TRUE(metadata->is_file);
|
EXPECT_TRUE(metadata->is_file);
|
||||||
EXPECT_EQ(metadata->size, TEST_DATA.size());
|
EXPECT_EQ(metadata->size, TEST_DATA.size());
|
||||||
|
@ -325,3 +410,27 @@ TEST_F(FileSystemTest, ReadDirectoryOnFile)
|
||||||
ASSERT_FALSE(result.Succeeded());
|
ASSERT_FALSE(result.Succeeded());
|
||||||
EXPECT_EQ(result.Error(), ResultCode::Invalid);
|
EXPECT_EQ(result.Error(), ResultCode::Invalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_F(FileSystemTest, ReadDirectoryOrdering)
|
||||||
|
{
|
||||||
|
ASSERT_EQ(m_fs->CreateDirectory(Uid{0}, Gid{0}, "/tmp/o", 0, modes), ResultCode::Success);
|
||||||
|
|
||||||
|
// Randomly generated file names in no particular order.
|
||||||
|
const std::array<std::string, 5> file_names{{
|
||||||
|
"Rkj62lGwHp",
|
||||||
|
"XGDQTDJMea",
|
||||||
|
"1z5M43WeFw",
|
||||||
|
"YAY39VuMRd",
|
||||||
|
"hxJ86nkoBX",
|
||||||
|
}};
|
||||||
|
// Create the files.
|
||||||
|
for (const auto& name : file_names)
|
||||||
|
ASSERT_EQ(m_fs->CreateFile(Uid{0}, Gid{0}, "/tmp/o/" + name, 0, modes), ResultCode::Success);
|
||||||
|
|
||||||
|
// Verify that ReadDirectory returns a file list that is ordered by descending creation date
|
||||||
|
// (issue 10234).
|
||||||
|
const Result<std::vector<std::string>> result = m_fs->ReadDirectory(Uid{0}, Gid{0}, "/tmp/o");
|
||||||
|
ASSERT_TRUE(result.Succeeded());
|
||||||
|
ASSERT_EQ(result->size(), file_names.size());
|
||||||
|
EXPECT_TRUE(std::equal(result->begin(), result->end(), file_names.rbegin()));
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
import argparse
|
||||||
|
import struct
|
||||||
|
|
||||||
|
def read_entry(f) -> dict:
|
||||||
|
name = struct.unpack_from("12s", f.read(12))[0]
|
||||||
|
uid = struct.unpack_from(">I", f.read(4))[0]
|
||||||
|
gid = struct.unpack_from(">H", f.read(2))[0]
|
||||||
|
is_file = struct.unpack_from("?", f.read(1))[0]
|
||||||
|
modes = struct.unpack_from("BBB", f.read(3))
|
||||||
|
attr = struct.unpack_from("B", f.read(2))[0]
|
||||||
|
x3 = struct.unpack_from(">I", f.read(4))[0]
|
||||||
|
num_children = struct.unpack_from(">I", f.read(4))[0]
|
||||||
|
|
||||||
|
children = []
|
||||||
|
for i in range(num_children):
|
||||||
|
children.append(read_entry(f))
|
||||||
|
|
||||||
|
return {
|
||||||
|
"name": name,
|
||||||
|
"uid": uid,
|
||||||
|
"gid": gid,
|
||||||
|
"is_file": is_file,
|
||||||
|
"modes": modes,
|
||||||
|
"attr": attr,
|
||||||
|
"x3": x3,
|
||||||
|
"children": children,
|
||||||
|
}
|
||||||
|
|
||||||
|
COLOR_RESET = "\x1b[0;00m"
|
||||||
|
BOLD = "\x1b[0;37m"
|
||||||
|
COLOR_BLUE = "\x1b[1;34m"
|
||||||
|
COLOR_GREEN = "\x1b[0;32m"
|
||||||
|
|
||||||
|
def print_entry(entry, indent) -> None:
|
||||||
|
mode_str = {0: "--", 1: "r-", 2: "-w", 3: "rw"}
|
||||||
|
|
||||||
|
sp = ' ' * indent
|
||||||
|
color = BOLD if entry["is_file"] else COLOR_BLUE
|
||||||
|
|
||||||
|
owner = f"{COLOR_GREEN}{entry['uid']:04x}{COLOR_RESET}:{entry['gid']:04x}"
|
||||||
|
attrs = f"{''.join(mode_str[mode] for mode in entry['modes'])}"
|
||||||
|
other_attrs = f"{entry['attr']} {entry['x3']}"
|
||||||
|
|
||||||
|
print(f"{sp}{color}{entry['name'].decode()}{COLOR_RESET} [{owner} {attrs} {other_attrs}]")
|
||||||
|
for child in entry["children"]:
|
||||||
|
print_entry(child, indent + 2)
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description="Prints a FST in a tree-like format.")
|
||||||
|
parser.add_argument("file")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
with open(args.file, "rb") as f:
|
||||||
|
root = read_entry(f)
|
||||||
|
|
||||||
|
print_entry(root, 0)
|
||||||
|
|
||||||
|
main()
|
Loading…
Reference in New Issue