diff --git a/Source/Core/Core/IOS/ES/NandUtils.cpp b/Source/Core/Core/IOS/ES/NandUtils.cpp index 5e98ed618b..ab25f06c76 100644 --- a/Source/Core/Core/IOS/ES/NandUtils.cpp +++ b/Source/Core/Core/IOS/ES/NandUtils.cpp @@ -109,12 +109,6 @@ static std::vector 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; } diff --git a/Source/Core/Core/IOS/FS/FileSystem.cpp b/Source/Core/Core/IOS/FS/FileSystem.cpp index 3fa652fb22..66bd7659ae 100644 --- a/Source/Core/Core/IOS/FS/FileSystem.cpp +++ b/Source/Core/Core/IOS/FS/FileSystem.cpp @@ -11,6 +11,24 @@ 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(1, last_separator))), + std::string(path.substr(last_separator + 1))}; +} + std::unique_ptr MakeFileSystem(Location location) { const std::string nand_root = @@ -66,12 +84,6 @@ Result FileHandle::GetStatus() const 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 FileSystem::CreateAndOpenFile(Uid uid, Gid gid, const std::string& path, Modes modes) { diff --git a/Source/Core/Core/IOS/FS/FileSystem.h b/Source/Core/Core/IOS/FS/FileSystem.h index 6266ba6979..7d53b6ca0f 100644 --- a/Source/Core/Core/IOS/FS/FileSystem.h +++ b/Source/Core/Core/IOS/FS/FileSystem.h @@ -7,6 +7,7 @@ #include #include #include +#include #include #ifdef _WIN32 @@ -76,6 +77,16 @@ struct Modes { 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 { @@ -111,6 +122,38 @@ struct FileStatus 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 FileHandle final { @@ -196,9 +239,6 @@ public: virtual Result GetNandStats() = 0; /// Get usage information about a directory (used cluster and inode counts). virtual Result GetDirectoryStats(const std::string& path) = 0; - -protected: - void Init(); }; template diff --git a/Source/Core/Core/IOS/FS/FileSystemProxy.cpp b/Source/Core/Core/IOS/FS/FileSystemProxy.cpp index 6a4bcd929d..181a739748 100644 --- a/Source/Core/Core/IOS/FS/FileSystemProxy.cpp +++ b/Source/Core/Core/IOS/FS/FileSystemProxy.cpp @@ -16,6 +16,7 @@ #include "Core/HW/Memmap.h" #include "Core/HW/SystemTimers.h" #include "Core/IOS/FS/FileSystem.h" +#include "Core/IOS/Uids.h" 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) { + 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) diff --git a/Source/Core/Core/IOS/FS/HostBackend/FS.cpp b/Source/Core/Core/IOS/FS/HostBackend/FS.cpp index 345800f5a5..fea5d16261 100644 --- a/Source/Core/Core/IOS/FS/HostBackend/FS.cpp +++ b/Source/Core/Core/IOS/FS/HostBackend/FS.cpp @@ -3,23 +3,26 @@ // Refer to the license.txt file included. #include +#include +#include +#include +#include + +#include #include "Common/Assert.h" #include "Common/ChunkFile.h" #include "Common/FileUtil.h" #include "Common/Logging/Log.h" #include "Common/NandPaths.h" +#include "Common/StringUtil.h" +#include "Common/Swap.h" #include "Core/IOS/ES/ES.h" #include "Core/IOS/FS/HostBackend/FS.h" #include "Core/IOS/IOS.h" 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 { if (wii_path.compare(0, 1, "/") == 0) @@ -44,13 +47,186 @@ static u64 ComputeTotalFileSize(const File::FSTEntry& parent_entry) 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 name{}; + /// File owner user ID + Common::BigEndianValue uid{}; + /// File owner group ID + Common::BigEndianValue 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 x3{}; + /// Number of children + Common::BigEndianValue num_children{}; +}; +static_assert(std::is_standard_layout()); +static_assert(sizeof(SerializedFstEntry) == 0x20); + +template +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} { - Init(); + File::CreateFullPath(m_root_path + "/"); + ResetFst(); + LoadFst(); } 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 { + 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 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) { // 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) { + if (uid != 0) + return ResultCode::AccessDenied; + if (m_root_path.empty()) + return ResultCode::AccessDenied; const std::string root = BuildFilename("/"); if (!File::DeleteDirRecursively(root) || !File::CreateDir(root)) return ResultCode::UnknownError; + ResetFst(); + SaveFst(); + // Reset and close all handles. + m_handles = {}; 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)); - // check if the file already exist - if (File::Exists(file_name)) + if (!IsValidNonRootPath(path) || !std::all_of(path.begin(), path.end(), IsPrintableCharacter)) + return ResultCode::Invalid; + + 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; - // create the file - File::CreateFullPath(file_name); // just to be sure - if (!File::CreateEmptyFile(file_name)) + const bool ok = is_file ? File::CreateEmptyFile(host_path) : File::CreateDir(host_path); + if (!ok) { - ERROR_LOG(IOS_FS, "couldn't create new file"); - return ResultCode::Invalid; + ERROR_LOG(IOS_FS, "Failed to create file or directory: %s", host_path.c_str()); + 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; } -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 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; + return CreateFileOrDirectory(uid, gid, path, attr, modes, true); } -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; - const std::string file_name = BuildFilename(path); - if (File::Delete(file_name)) - INFO_LOG(IOS_FS, "DeleteFile %s", file_name.c_str()); - else if (File::DeleteDirRecursively(file_name)) - INFO_LOG(IOS_FS, "DeleteDir %s", file_name.c_str()); + const std::string host_path = BuildFilename(path); + const auto split_path = SplitPathAndBasename(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::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 - 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; } -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) { - if (!IsValidWiiPath(old_path)) + if (!IsValidNonRootPath(old_path) || !IsValidNonRootPath(new_path)) 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; - const std::string new_name = BuildFilename(new_path); - // try to make the basis directory - File::CreateFullPath(new_name); + if ((!entry->data.is_file && IsDirectoryInUse(old_path)) || + (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 (File::Exists(new_name)) + if (File::Exists(host_new_path)) { - const bool old_is_file = File::IsFile(old_name); - const bool new_is_file = File::IsFile(new_name); + const bool old_is_file = File::IsFile(host_old_path); + const bool new_is_file = File::IsFile(host_new_path); if (old_is_file && new_is_file) - File::Delete(new_name); + File::Delete(host_new_path); else if (!old_is_file && !new_is_file) - File::DeleteDirRecursively(new_name); + File::DeleteDirRecursively(host_new_path); else return ResultCode::Invalid; } - // finally try to rename the file - if (!File::Rename(old_name, new_name)) + if (!File::Rename(host_old_path, host_new_path)) { - 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; } + // 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; } -Result> HostFileSystem::ReadDirectory(Uid, Gid, const std::string& path) +Result> HostFileSystem::ReadDirectory(Uid uid, Gid gid, + const std::string& path) { - if (!IsValidWiiPath(path)) + if (!IsValidPath(path)) return ResultCode::Invalid; - // the Wii uses this function to define the type (dir or file) - const std::string dir_name(BuildFilename(path)); - - const File::FileInfo file_info(dir_name); - - if (!file_info.Exists()) - { - WARN_LOG(IOS_FS, "Search not found: %s", dir_name.c_str()); + const FstEntry* entry = GetFstEntryForPath(path); + if (!entry) return ResultCode::NotFound; - } - if (!file_info.IsDirectory()) - { - // It's not a directory, so error. + if (!entry->CheckPermission(uid, gid, Mode::Read)) + return ResultCode::AccessDenied; + + if (entry->data.is_file) return ResultCode::Invalid; - } - File::FSTEntry entry = File::ScanDirectoryTree(dir_name, false); - - for (File::FSTEntry& child : entry.children) + const std::string host_path = BuildFilename(path); + File::FSTEntry host_entry = File::ScanDirectoryTree(host_path, false); + for (File::FSTEntry& child : host_entry.children) { // Decode escaped invalid file system characters so that games (such as // Harry Potter and the Half-Blood Prince) can find what they expect. child.virtualName = Common::UnescapeFileName(child.virtualName); } - // NOTE(leoetlino): this is absolutely wrong, but there is no way to fix this properly - // if we use the host filesystem. - std::sort(entry.children.begin(), entry.children.end(), - [](const File::FSTEntry& one, const File::FSTEntry& two) { - return one.virtualName < two.virtualName; + // Sort files according to their order in the FST tree (issue 10234). + // The result should look like this: + // [FilesNotInFst, ..., OldestFileInFst, ..., NewestFileInFst] + std::unordered_map sort_keys; + 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 output; - for (File::FSTEntry& child : entry.children) + for (const File::FSTEntry& child : host_entry.children) output.emplace_back(child.virtualName); return output; } -Result HostFileSystem::GetMetadata(Uid, Gid, const std::string& path) +Result HostFileSystem::GetMetadata(Uid uid, Gid gid, const std::string& path) { - Metadata metadata; - metadata.uid = 0; - metadata.gid = 0x3031; // this is also known as makercd, 01 (0x3031) for nintendo and 08 - // (0x3038) for MH3 etc - - if (!IsValidWiiPath(path)) - return ResultCode::Invalid; - - std::string file_name = BuildFilename(path); - metadata.modes = {Mode::ReadWrite, Mode::ReadWrite, Mode::ReadWrite}; - metadata.attribute = 0x00; // no attributes - - // Hack: if the path that is being accessed is within an installed title directory, get the - // UID/GID from the installed title TMD. - Kernel* ios = GetIOS(); - u64 title_id; - if (ios && IsTitlePath(file_name, Common::FROM_SESSION_ROOT, &title_id)) + const FstEntry* entry = nullptr; + if (path == "/") { - IOS::ES::TMDReader tmd = ios->GetES()->FindInstalledTMD(title_id); - if (tmd.IsValid()) - metadata.gid = tmd.GetGroupId(); + entry = &m_root_entry; + } + else + { + if (!IsValidNonRootPath(path)) + return ResultCode::Invalid; + + const auto split_path = SplitPathAndBasename(path); + const FstEntry* parent = GetFstEntryForPath(split_path.parent); + if (!parent) + return ResultCode::NotFound; + if (!parent->CheckPermission(uid, gid, Mode::Read)) + return ResultCode::AccessDenied; + entry = GetFstEntryForPath(path); } - const File::FileInfo info{file_name}; - metadata.is_file = info.IsFile(); - metadata.size = info.GetSize(); - if (!info.Exists()) + if (!entry) return ResultCode::NotFound; + + Metadata metadata = entry->data; + metadata.size = File::GetSize(BuildFilename(path)); return metadata; } 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; + + 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; } @@ -354,7 +656,7 @@ Result HostFileSystem::GetNandStats() Result HostFileSystem::GetDirectoryStats(const std::string& wii_path) { - if (!IsValidWiiPath(wii_path)) + if (!IsValidPath(wii_path)) return ResultCode::Invalid; DirectoryStats stats{}; diff --git a/Source/Core/Core/IOS/FS/HostBackend/FS.h b/Source/Core/Core/IOS/FS/HostBackend/FS.h index 8e3a683887..beb4d56148 100644 --- a/Source/Core/Core/IOS/FS/HostBackend/FS.h +++ b/Source/Core/Core/IOS/FS/HostBackend/FS.h @@ -58,6 +58,20 @@ public: Result GetDirectoryStats(const std::string& path) override; 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 children; + }; + struct Handle { bool opened = false; @@ -73,6 +87,29 @@ private: std::string BuildFilename(const std::string& wii_path) const; std::shared_ptr 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::map> m_open_files; std::array m_handles{}; diff --git a/Source/Core/Core/NetPlayClient.cpp b/Source/Core/Core/NetPlayClient.cpp index c64a2769a4..e769fc599b 100644 --- a/Source/Core/Core/NetPlayClient.cpp +++ b/Source/Core/Core/NetPlayClient.cpp @@ -904,8 +904,8 @@ unsigned int NetPlayClient::OnData(sf::Packet& packet) { auto buffer = DecompressPacketIntoBuffer(packet); - temp_fs->CreateDirectory(IOS::PID_KERNEL, IOS::PID_KERNEL, "/shared2/menu/FaceLib", 0, - fs_modes); + temp_fs->CreateFullPath(IOS::PID_KERNEL, IOS::PID_KERNEL, "/shared2/menu/FaceLib/", 0, + fs_modes); auto file = temp_fs->CreateAndOpenFile(IOS::PID_KERNEL, IOS::PID_KERNEL, Common::GetMiiDatabasePath(), fs_modes); @@ -924,8 +924,8 @@ unsigned int NetPlayClient::OnData(sf::Packet& packet) { u64 title_id = Common::PacketReadU64(packet); titles.push_back(title_id); - temp_fs->CreateDirectory(IOS::PID_KERNEL, IOS::PID_KERNEL, - Common::GetTitleDataPath(title_id), 0, fs_modes); + temp_fs->CreateFullPath(IOS::PID_KERNEL, IOS::PID_KERNEL, + Common::GetTitleDataPath(title_id) + '/', 0, fs_modes); auto save = WiiSave::MakeNandStorage(temp_fs.get(), title_id); bool exists; diff --git a/Source/Core/Core/WiiRoot.cpp b/Source/Core/Core/WiiRoot.cpp index 06be93900a..538cc394fb 100644 --- a/Source/Core/Core/WiiRoot.cpp +++ b/Source/Core/Core/WiiRoot.cpp @@ -34,9 +34,8 @@ static std::string s_temp_wii_root; 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, - {IOS::HLE::FS::Mode::ReadWrite, IOS::HLE::FS::Mode::ReadWrite, - IOS::HLE::FS::Mode::ReadWrite}); + dest->CreateFullPath(IOS::PID_KERNEL, IOS::PID_KERNEL, Common::GetTitleDataPath(title_id) + '/', + 0, {FS::Mode::ReadWrite, FS::Mode::ReadWrite, FS::Mode::ReadWrite}); const auto source_save = WiiSave::MakeNandStorage(source, title_id); const auto dest_save = WiiSave::MakeNandStorage(dest, title_id); 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) { const std::string dir = dest_file.substr(0, last_slash); - dest_fs->CreateDirectory(IOS::PID_KERNEL, IOS::PID_KERNEL, dir, 0, - {IOS::HLE::FS::Mode::ReadWrite, IOS::HLE::FS::Mode::ReadWrite, - IOS::HLE::FS::Mode::ReadWrite}); + dest_fs->CreateFullPath(IOS::PID_KERNEL, IOS::PID_KERNEL, dir + '/', 0, + {FS::Mode::ReadWrite, FS::Mode::ReadWrite, FS::Mode::ReadWrite}); } auto source_handle = @@ -190,7 +188,7 @@ static bool CopySysmenuFilesToFS(FS::FileSystem* fs, const std::string& host_sou 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)) return false; } @@ -259,12 +257,8 @@ void CleanUpWiiFileSystemContents() // FS won't write the save if the directory doesn't exist const std::string title_path = Common::GetTitleDataPath(title_id); - if (!configured_fs->GetMetadata(IOS::PID_KERNEL, IOS::PID_KERNEL, title_path)) - { - 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}); - } + configured_fs->CreateFullPath(IOS::PID_KERNEL, IOS::PID_KERNEL, title_path + '/', 0, + {FS::Mode::ReadWrite, FS::Mode::ReadWrite, FS::Mode::ReadWrite}); const auto user_save = WiiSave::MakeNandStorage(configured_fs.get(), title_id); diff --git a/Source/UnitTests/Core/IOS/FS/FileSystemTest.cpp b/Source/UnitTests/Core/IOS/FS/FileSystemTest.cpp index 3a4f443139..79f853f441 100644 --- a/Source/UnitTests/Core/IOS/FS/FileSystemTest.cpp +++ b/Source/UnitTests/Core/IOS/FS/FileSystemTest.cpp @@ -2,8 +2,10 @@ // Licensed under GPLv2+ // Refer to the license.txt file included. +#include #include #include +#include #include #include @@ -39,6 +41,41 @@ private: 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) { for (const std::string& path : @@ -52,41 +89,59 @@ TEST_F(FileSystemTest, CreateFile) { 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 stats = m_fs->GetMetadata(Uid{0}, Gid{0}, PATH); ASSERT_TRUE(stats.Succeeded()); EXPECT_TRUE(stats->is_file); EXPECT_EQ(stats->size, 0u); - // TODO: After we start saving metadata correctly, check the UID, GID, permissions - // as well (issue 10234). + EXPECT_EQ(stats->uid, 0); + 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); const Result> tmp_files = m_fs->ReadDirectory(Uid{0}, Gid{0}, "/tmp"); ASSERT_TRUE(tmp_files.Succeeded()); 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) { 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 stats = m_fs->GetMetadata(Uid{0}, Gid{0}, PATH); ASSERT_TRUE(stats.Succeeded()); EXPECT_FALSE(stats->is_file); - // TODO: After we start saving metadata correctly, check the UID, GID, permissions - // as well (issue 10234). + EXPECT_EQ(stats->uid, 0); + EXPECT_EQ(stats->gid, 0); + EXPECT_EQ(stats->modes, modes); + EXPECT_EQ(stats->attribute, ArbitraryAttribute); const Result> children = m_fs->ReadDirectory(Uid{0}, Gid{0}, PATH); ASSERT_TRUE(children.Succeeded()); 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, Mode::Read, Mode::None, Mode::None), - // ResultCode::AlreadyExists); + EXPECT_EQ(m_fs->CreateDirectory(Uid{0}, Gid{0}, PATH, 0, modes), 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) @@ -94,6 +149,25 @@ TEST_F(FileSystemTest, Delete) 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->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) @@ -104,6 +178,14 @@ TEST_F(FileSystemTest, Rename) EXPECT_EQ(m_fs->ReadDirectory(Uid{0}, Gid{0}, "/tmp").Error(), ResultCode::NotFound); 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) @@ -124,26 +206,29 @@ TEST_F(FileSystemTest, RenameWithExistingTargetDirectory) 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). - 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 TEST_DATA{{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}}; std::vector read_buffer(TEST_DATA.size()); { - const Result file = m_fs->OpenFile(Uid{0}, Gid{0}, "/tmp/f1", Mode::ReadWrite); + const Result file = m_fs->OpenFile(Uid{0}, Gid{0}, source_path, Mode::ReadWrite); ASSERT_TRUE(file.Succeeded()); ASSERT_TRUE(file->Write(TEST_DATA.data(), TEST_DATA.size()).Succeeded()); } // 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. - EXPECT_EQ(m_fs->Rename(Uid{0}, Gid{0}, "/tmp/f1", "/tmp/f2"), ResultCode::Success); + // Rename /sys/f2 to /tmp/f2 and check that f1 replaced f2. + 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()); - EXPECT_EQ(m_fs->GetMetadata(Uid{0}, Gid{0}, "/tmp/f1").Error(), ResultCode::NotFound); + ASSERT_FALSE(m_fs->GetMetadata(Uid{0}, Gid{0}, source_path).Succeeded()); + EXPECT_EQ(m_fs->GetMetadata(Uid{0}, Gid{0}, source_path).Error(), ResultCode::NotFound); - const Result metadata = m_fs->GetMetadata(Uid{0}, Gid{0}, "/tmp/f2"); + const Result metadata = m_fs->GetMetadata(Uid{0}, Gid{0}, dest_path); ASSERT_TRUE(metadata.Succeeded()); EXPECT_TRUE(metadata->is_file); EXPECT_EQ(metadata->size, TEST_DATA.size()); @@ -325,3 +410,27 @@ TEST_F(FileSystemTest, ReadDirectoryOnFile) ASSERT_FALSE(result.Succeeded()); 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 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> 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())); +} diff --git a/Tools/print-fs-fst.py b/Tools/print-fs-fst.py new file mode 100644 index 0000000000..8b88155e4a --- /dev/null +++ b/Tools/print-fs-fst.py @@ -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()