Compare commits
1 commit
master
...
citron_fs_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77ce42a2ed |
17 changed files with 1346 additions and 82 deletions
|
|
@ -130,6 +130,12 @@ add_library(core STATIC
|
|||
file_sys/romfs.h
|
||||
file_sys/romfs_factory.cpp
|
||||
file_sys/romfs_factory.h
|
||||
file_sys/directory_save_data_filesystem.cpp
|
||||
file_sys/directory_save_data_filesystem.h
|
||||
file_sys/fs_path_normalizer.cpp
|
||||
file_sys/fs_path_normalizer.h
|
||||
file_sys/savedata_extra_data_accessor.cpp
|
||||
file_sys/savedata_extra_data_accessor.h
|
||||
file_sys/savedata_factory.cpp
|
||||
file_sys/savedata_factory.h
|
||||
file_sys/sdmc_factory.cpp
|
||||
|
|
|
|||
231
src/core/file_sys/directory_save_data_filesystem.cpp
Normal file
231
src/core/file_sys/directory_save_data_filesystem.cpp
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
#include "common/logging/log.h"
|
||||
#include "core/file_sys/errors.h"
|
||||
#include "core/file_sys/directory_save_data_filesystem.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int MaxRetryCount = 10;
|
||||
constexpr int RetryWaitTimeMs = 100;
|
||||
|
||||
} // Anonymous namespace
|
||||
|
||||
DirectorySaveDataFileSystem::DirectorySaveDataFileSystem(VirtualDir base_filesystem)
|
||||
: base_fs(std::move(base_filesystem)), extra_data_accessor(base_fs), journaling_enabled(true),
|
||||
open_writable_files(0) {}
|
||||
|
||||
DirectorySaveDataFileSystem::~DirectorySaveDataFileSystem() = default;
|
||||
|
||||
Result DirectorySaveDataFileSystem::Initialize(bool enable_journaling) {
|
||||
std::scoped_lock lk{mutex};
|
||||
|
||||
journaling_enabled = enable_journaling;
|
||||
|
||||
// Initialize extra data
|
||||
R_TRY(extra_data_accessor.Initialize(true));
|
||||
|
||||
// Get or create the working directory (always needed)
|
||||
working_dir = base_fs->GetSubdirectory(ModifiedDirectoryName);
|
||||
if (working_dir == nullptr) {
|
||||
working_dir = base_fs->CreateSubdirectory(ModifiedDirectoryName);
|
||||
if (working_dir == nullptr) {
|
||||
return ResultPermissionDenied;
|
||||
}
|
||||
}
|
||||
|
||||
if (!journaling_enabled) {
|
||||
// Non-journaling mode: working directory is all we need
|
||||
return ResultSuccess;
|
||||
}
|
||||
|
||||
// Get or create the committed directory
|
||||
committed_dir = base_fs->GetSubdirectory(CommittedDirectoryName);
|
||||
|
||||
if (committed_dir == nullptr) {
|
||||
// Check for synchronizing directory (interrupted commit)
|
||||
auto sync_dir = base_fs->GetSubdirectory(SynchronizingDirectoryName);
|
||||
if (sync_dir != nullptr) {
|
||||
// Finish the interrupted commit
|
||||
if (!sync_dir->Rename(CommittedDirectoryName)) {
|
||||
return ResultPermissionDenied;
|
||||
}
|
||||
committed_dir = base_fs->GetSubdirectory(CommittedDirectoryName);
|
||||
} else {
|
||||
// Create committed directory and sync from working
|
||||
committed_dir = base_fs->CreateSubdirectory(CommittedDirectoryName);
|
||||
if (committed_dir == nullptr) {
|
||||
return ResultPermissionDenied;
|
||||
}
|
||||
|
||||
// Initial commit: copy working → committed
|
||||
R_TRY(SynchronizeDirectory(CommittedDirectoryName, ModifiedDirectoryName));
|
||||
}
|
||||
} else {
|
||||
// Committed exists - restore working from it (previous run may have crashed)
|
||||
R_TRY(SynchronizeDirectory(ModifiedDirectoryName, CommittedDirectoryName));
|
||||
}
|
||||
|
||||
return ResultSuccess;
|
||||
}
|
||||
|
||||
VirtualDir DirectorySaveDataFileSystem::GetWorkingDirectory() {
|
||||
return working_dir;
|
||||
}
|
||||
|
||||
VirtualDir DirectorySaveDataFileSystem::GetCommittedDirectory() {
|
||||
return committed_dir;
|
||||
}
|
||||
|
||||
Result DirectorySaveDataFileSystem::Commit() {
|
||||
std::scoped_lock lk{mutex};
|
||||
|
||||
if (!journaling_enabled) {
|
||||
// Non-journaling: just commit extra data
|
||||
return extra_data_accessor.CommitExtraDataWithTimeStamp(
|
||||
std::chrono::system_clock::now().time_since_epoch().count());
|
||||
}
|
||||
|
||||
// Check that all writable files are closed
|
||||
if (open_writable_files > 0) {
|
||||
LOG_ERROR(Service_FS, "Cannot commit: {} writable files still open", open_writable_files);
|
||||
return ResultWriteModeFileNotClosed;
|
||||
}
|
||||
|
||||
// Atomic commit process (based on LibHac lines 572-622)
|
||||
// 1. Rename committed → synchronizing (backup old version)
|
||||
auto committed = base_fs->GetSubdirectory(CommittedDirectoryName);
|
||||
if (committed != nullptr) {
|
||||
if (!committed->Rename(SynchronizingDirectoryName)) {
|
||||
return ResultPermissionDenied;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Copy working → synchronizing (prepare new commit)
|
||||
R_TRY(SynchronizeDirectory(SynchronizingDirectoryName, ModifiedDirectoryName));
|
||||
|
||||
// 3. Commit extra data with updated timestamp
|
||||
R_TRY(extra_data_accessor.CommitExtraDataWithTimeStamp(
|
||||
std::chrono::system_clock::now().time_since_epoch().count()));
|
||||
|
||||
// 4. Rename synchronizing → committed (make it permanent)
|
||||
auto sync_dir = base_fs->GetSubdirectory(SynchronizingDirectoryName);
|
||||
if (sync_dir == nullptr) {
|
||||
return ResultPathNotFound;
|
||||
}
|
||||
|
||||
if (!sync_dir->Rename(CommittedDirectoryName)) {
|
||||
return ResultPermissionDenied;
|
||||
}
|
||||
|
||||
// Update cached committed_dir reference
|
||||
committed_dir = base_fs->GetSubdirectory(CommittedDirectoryName);
|
||||
|
||||
LOG_INFO(Service_FS, "Save data committed successfully");
|
||||
return ResultSuccess;
|
||||
}
|
||||
|
||||
Result DirectorySaveDataFileSystem::Rollback() {
|
||||
std::scoped_lock lk{mutex};
|
||||
|
||||
if (!journaling_enabled) {
|
||||
// Can't rollback without journaling
|
||||
return ResultSuccess;
|
||||
}
|
||||
|
||||
// Restore working directory from committed
|
||||
R_TRY(SynchronizeDirectory(ModifiedDirectoryName, CommittedDirectoryName));
|
||||
|
||||
LOG_INFO(Service_FS, "Save data rolled back to last commit");
|
||||
return ResultSuccess;
|
||||
}
|
||||
|
||||
bool DirectorySaveDataFileSystem::HasUncommittedChanges() const {
|
||||
// For now, assume any write means uncommitted changes
|
||||
// A full implementation would compare directory contents
|
||||
return open_writable_files > 0;
|
||||
}
|
||||
|
||||
Result DirectorySaveDataFileSystem::SynchronizeDirectory(const char* dest_name,
|
||||
const char* source_name) {
|
||||
auto source_dir = base_fs->GetSubdirectory(source_name);
|
||||
if (source_dir == nullptr) {
|
||||
return ResultPathNotFound;
|
||||
}
|
||||
|
||||
// Delete destination if it exists
|
||||
auto dest_dir = base_fs->GetSubdirectory(dest_name);
|
||||
if (dest_dir != nullptr) {
|
||||
if (!base_fs->DeleteSubdirectoryRecursive(dest_name)) {
|
||||
return ResultPermissionDenied;
|
||||
}
|
||||
}
|
||||
|
||||
// Create new destination
|
||||
dest_dir = base_fs->CreateSubdirectory(dest_name);
|
||||
if (dest_dir == nullptr) {
|
||||
return ResultPermissionDenied;
|
||||
}
|
||||
|
||||
// Copy contents recursively
|
||||
return CopyDirectoryRecursively(dest_dir, source_dir);
|
||||
}
|
||||
|
||||
Result DirectorySaveDataFileSystem::CopyDirectoryRecursively(VirtualDir dest, VirtualDir source) {
|
||||
// Copy all files
|
||||
for (const auto& file : source->GetFiles()) {
|
||||
auto new_file = dest->CreateFile(file->GetName());
|
||||
if (new_file == nullptr) {
|
||||
return ResultUsableSpaceNotEnough;
|
||||
}
|
||||
|
||||
auto data = file->ReadAllBytes();
|
||||
if (new_file->WriteBytes(data) != data.size()) {
|
||||
return ResultUsableSpaceNotEnough;
|
||||
}
|
||||
}
|
||||
|
||||
// Copy all subdirectories recursively
|
||||
for (const auto& subdir : source->GetSubdirectories()) {
|
||||
auto new_subdir = dest->CreateSubdirectory(subdir->GetName());
|
||||
if (new_subdir == nullptr) {
|
||||
return ResultPermissionDenied;
|
||||
}
|
||||
|
||||
R_TRY(CopyDirectoryRecursively(new_subdir, subdir));
|
||||
}
|
||||
|
||||
return ResultSuccess;
|
||||
}
|
||||
|
||||
Result DirectorySaveDataFileSystem::RetryFinitelyForTargetLocked(
|
||||
std::function<Result()> operation) {
|
||||
int remaining_retries = MaxRetryCount;
|
||||
|
||||
while (true) {
|
||||
Result result = operation();
|
||||
|
||||
if (result == ResultSuccess) {
|
||||
return ResultSuccess;
|
||||
}
|
||||
|
||||
// Only retry on TargetLocked error
|
||||
if (result != ResultTargetLocked) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (remaining_retries <= 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
remaining_retries--;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(RetryWaitTimeMs));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace FileSys
|
||||
65
src/core/file_sys/directory_save_data_filesystem.h
Normal file
65
src/core/file_sys/directory_save_data_filesystem.h
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include "core/file_sys/fs_save_data_types.h"
|
||||
#include "core/file_sys/savedata_extra_data_accessor.h"
|
||||
#include "core/file_sys/vfs/vfs.h"
|
||||
#include "core/hle/result.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
/// A filesystem wrapper that provides transactional commit semantics for save data
|
||||
/// Based on LibHac's DirectorySaveDataFileSystem implementation
|
||||
/// Uses /0 (committed) and /1 (working) directories for journaling
|
||||
class DirectorySaveDataFileSystem {
|
||||
public:
|
||||
explicit DirectorySaveDataFileSystem(VirtualDir base_filesystem);
|
||||
~DirectorySaveDataFileSystem();
|
||||
|
||||
/// Initialize the journaling filesystem
|
||||
Result Initialize(bool enable_journaling);
|
||||
|
||||
/// Get the working directory (where changes are made)
|
||||
VirtualDir GetWorkingDirectory();
|
||||
|
||||
/// Get the committed directory (stable version)
|
||||
VirtualDir GetCommittedDirectory();
|
||||
|
||||
/// Commit all changes (makes working directory the new committed version)
|
||||
Result Commit();
|
||||
|
||||
/// Rollback changes (restore working directory from committed)
|
||||
Result Rollback();
|
||||
|
||||
/// Check if there are uncommitted changes
|
||||
bool HasUncommittedChanges() const;
|
||||
|
||||
/// Get the extra data accessor for this save
|
||||
SaveDataExtraDataAccessor& GetExtraDataAccessor() {
|
||||
return extra_data_accessor;
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr const char* CommittedDirectoryName = "0";
|
||||
static constexpr const char* ModifiedDirectoryName = "1";
|
||||
static constexpr const char* SynchronizingDirectoryName = "_";
|
||||
|
||||
Result SynchronizeDirectory(const char* dest_name, const char* source_name);
|
||||
Result CopyDirectoryRecursively(VirtualDir dest, VirtualDir source);
|
||||
Result RetryFinitelyForTargetLocked(std::function<Result()> operation);
|
||||
|
||||
VirtualDir base_fs;
|
||||
VirtualDir working_dir;
|
||||
VirtualDir committed_dir;
|
||||
SaveDataExtraDataAccessor extra_data_accessor;
|
||||
std::mutex mutex;
|
||||
bool journaling_enabled;
|
||||
int open_writable_files;
|
||||
};
|
||||
|
||||
} // namespace FileSys
|
||||
|
|
@ -13,6 +13,10 @@ constexpr Result ResultUnsupportedSdkVersion{ErrorModule::FS, 50};
|
|||
constexpr Result ResultPartitionNotFound{ErrorModule::FS, 1001};
|
||||
constexpr Result ResultTargetNotFound{ErrorModule::FS, 1002};
|
||||
constexpr Result ResultPortSdCardNoDevice{ErrorModule::FS, 2001};
|
||||
constexpr Result ResultTargetLocked{ErrorModule::FS, 7};
|
||||
constexpr Result ResultDirectoryNotEmpty{ErrorModule::FS, 8};
|
||||
constexpr Result ResultDirectoryStatusChanged{ErrorModule::FS, 13};
|
||||
constexpr Result ResultUsableSpaceNotEnough{ErrorModule::FS, 39};
|
||||
constexpr Result ResultNotImplemented{ErrorModule::FS, 3001};
|
||||
constexpr Result ResultUnsupportedVersion{ErrorModule::FS, 3002};
|
||||
constexpr Result ResultOutOfRange{ErrorModule::FS, 3005};
|
||||
|
|
@ -93,5 +97,9 @@ constexpr Result ResultUnsupportedWriteForCompressedStorage{ErrorModule::FS, 638
|
|||
constexpr Result ResultUnsupportedOperateRangeForCompressedStorage{ErrorModule::FS, 6388};
|
||||
constexpr Result ResultPermissionDenied{ErrorModule::FS, 6400};
|
||||
constexpr Result ResultBufferAllocationFailed{ErrorModule::FS, 6705};
|
||||
constexpr Result ResultMappingTableFull{ErrorModule::FS, 6706};
|
||||
constexpr Result ResultOpenCountLimit{ErrorModule::FS, 6709};
|
||||
constexpr Result ResultWriteModeFileNotClosed{ErrorModule::FS, 6710};
|
||||
constexpr Result ResultDataCorrupted{ErrorModule::FS, 4001};
|
||||
|
||||
} // namespace FileSys
|
||||
|
|
|
|||
224
src/core/file_sys/fs_path_normalizer.cpp
Normal file
224
src/core/file_sys/fs_path_normalizer.cpp
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
#include "common/string_util.h"
|
||||
#include "core/file_sys/errors.h"
|
||||
#include "core/file_sys/fs_path_normalizer.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr char DirectorySeparator = '/';
|
||||
constexpr std::string_view CurrentDirectory = ".";
|
||||
constexpr std::string_view ParentDirectory = "..";
|
||||
|
||||
// Invalid characters for Nintendo Switch paths
|
||||
bool IsInvalidCharacter(char c) {
|
||||
// Control characters
|
||||
if (c < 0x20) {
|
||||
return true;
|
||||
}
|
||||
// Reserved characters
|
||||
switch (c) {
|
||||
case '<':
|
||||
case '>':
|
||||
case '"':
|
||||
case '\\':
|
||||
case '|':
|
||||
case '?':
|
||||
case '*':
|
||||
case ':': // Colon is invalid except for drive letters (which we don't use)
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
} // Anonymous namespace
|
||||
|
||||
bool PathNormalizer::IsValidCharacter(char c) {
|
||||
return !IsInvalidCharacter(c);
|
||||
}
|
||||
|
||||
Result PathNormalizer::ValidateCharacters(std::string_view path) {
|
||||
for (char c : path) {
|
||||
if (IsInvalidCharacter(c)) {
|
||||
return ResultInvalidCharacter;
|
||||
}
|
||||
}
|
||||
return ResultSuccess;
|
||||
}
|
||||
|
||||
Result PathNormalizer::ValidatePath(std::string_view path) {
|
||||
// Check path length
|
||||
if (path.length() >= MaxPathLength) {
|
||||
return ResultTooLongPath;
|
||||
}
|
||||
|
||||
// Check for invalid characters
|
||||
R_TRY(ValidateCharacters(path));
|
||||
|
||||
// Empty path is valid (represents current directory)
|
||||
if (path.empty()) {
|
||||
return ResultSuccess;
|
||||
}
|
||||
|
||||
return ResultSuccess;
|
||||
}
|
||||
|
||||
Result PathNormalizer::Normalize(std::string* out_path, std::string_view path) {
|
||||
// Validate input
|
||||
R_TRY(ValidatePath(path));
|
||||
|
||||
// Empty or root path
|
||||
if (path.empty() || path == "/") {
|
||||
*out_path = "/";
|
||||
return ResultSuccess;
|
||||
}
|
||||
|
||||
return NormalizeImpl(out_path, path);
|
||||
}
|
||||
|
||||
Result PathNormalizer::NormalizeImpl(std::string* out_path, std::string_view path) {
|
||||
std::vector<std::string> components;
|
||||
std::string current_component;
|
||||
|
||||
// Split path into components and resolve "." and ".."
|
||||
for (size_t i = 0; i < path.length(); ++i) {
|
||||
char c = path[i];
|
||||
|
||||
if (c == DirectorySeparator) {
|
||||
if (!current_component.empty()) {
|
||||
if (current_component == CurrentDirectory) {
|
||||
// Skip "." components
|
||||
} else if (current_component == ParentDirectory) {
|
||||
// Go up one directory
|
||||
if (components.empty()) {
|
||||
// Can't go above root
|
||||
return ResultInvalidPath;
|
||||
}
|
||||
components.pop_back();
|
||||
} else {
|
||||
components.push_back(current_component);
|
||||
}
|
||||
current_component.clear();
|
||||
}
|
||||
// Skip redundant slashes
|
||||
} else {
|
||||
current_component += c;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle last component
|
||||
if (!current_component.empty()) {
|
||||
if (current_component == CurrentDirectory) {
|
||||
// Skip
|
||||
} else if (current_component == ParentDirectory) {
|
||||
if (components.empty()) {
|
||||
return ResultInvalidPath;
|
||||
}
|
||||
components.pop_back();
|
||||
} else {
|
||||
components.push_back(current_component);
|
||||
}
|
||||
}
|
||||
|
||||
// Build normalized path
|
||||
if (components.empty()) {
|
||||
*out_path = "/";
|
||||
return ResultSuccess;
|
||||
}
|
||||
|
||||
std::string normalized = "";
|
||||
for (const auto& component : components) {
|
||||
normalized += DirectorySeparator;
|
||||
normalized += component;
|
||||
}
|
||||
|
||||
// Check normalized path length
|
||||
if (normalized.length() >= MaxPathLength) {
|
||||
return ResultTooLongPath;
|
||||
}
|
||||
|
||||
*out_path = std::move(normalized);
|
||||
return ResultSuccess;
|
||||
}
|
||||
|
||||
bool PathNormalizer::IsNormalized(std::string_view path) {
|
||||
// Empty path is normalized
|
||||
if (path.empty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for invalid characters
|
||||
if (ValidateCharacters(path) != ResultSuccess) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for "." or ".." components
|
||||
if (path.find("/.") != std::string_view::npos ||
|
||||
path.find("./") != std::string_view::npos ||
|
||||
path == "." || path == "..") {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for redundant slashes
|
||||
if (path.find("//") != std::string_view::npos) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for trailing slashes (except for root)
|
||||
if (path.length() > 1 && path.back() == DirectorySeparator) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
namespace PathUtility {
|
||||
|
||||
bool IsRootPath(std::string_view path) {
|
||||
return path == "/" || path.empty();
|
||||
}
|
||||
|
||||
bool IsAbsolutePath(std::string_view path) {
|
||||
return !path.empty() && path[0] == DirectorySeparator;
|
||||
}
|
||||
|
||||
std::string RemoveTrailingSlashes(std::string_view path) {
|
||||
if (path.empty() || path == "/") {
|
||||
return std::string(path);
|
||||
}
|
||||
|
||||
size_t end = path.length();
|
||||
while (end > 1 && path[end - 1] == DirectorySeparator) {
|
||||
--end;
|
||||
}
|
||||
|
||||
return std::string(path.substr(0, end));
|
||||
}
|
||||
|
||||
std::string CombinePaths(std::string_view base, std::string_view relative) {
|
||||
if (relative.empty()) {
|
||||
return std::string(base);
|
||||
}
|
||||
|
||||
if (IsAbsolutePath(relative)) {
|
||||
return std::string(relative);
|
||||
}
|
||||
|
||||
std::string result(base);
|
||||
if (!result.empty() && result.back() != DirectorySeparator) {
|
||||
result += DirectorySeparator;
|
||||
}
|
||||
result += relative;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace PathUtility
|
||||
|
||||
} // namespace FileSys
|
||||
57
src/core/file_sys/fs_path_normalizer.h
Normal file
57
src/core/file_sys/fs_path_normalizer.h
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include "core/hle/result.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
/// Path normalization and validation utilities
|
||||
/// Based on LibHac's PathNormalizer and PathUtility
|
||||
class PathNormalizer {
|
||||
public:
|
||||
/// Maximum path length for Nintendo Switch filesystem
|
||||
static constexpr size_t MaxPathLength = 0x300; // 768 bytes
|
||||
|
||||
/// Normalize a path by resolving ".", "..", removing redundant slashes, etc.
|
||||
/// Returns the normalized path and Result
|
||||
static Result Normalize(std::string* out_path, std::string_view path);
|
||||
|
||||
/// Validate that a path contains only valid characters
|
||||
static Result ValidateCharacters(std::string_view path);
|
||||
|
||||
/// Check if a path is normalized
|
||||
static bool IsNormalized(std::string_view path);
|
||||
|
||||
/// Check if path is valid for Nintendo Switch filesystem
|
||||
static Result ValidatePath(std::string_view path);
|
||||
|
||||
private:
|
||||
/// Check if a character is valid in a path
|
||||
static bool IsValidCharacter(char c);
|
||||
|
||||
/// Remove redundant slashes and resolve "." and ".."
|
||||
static Result NormalizeImpl(std::string* out_path, std::string_view path);
|
||||
};
|
||||
|
||||
/// Helper functions for path manipulation
|
||||
namespace PathUtility {
|
||||
|
||||
/// Check if path represents root directory
|
||||
bool IsRootPath(std::string_view path);
|
||||
|
||||
/// Check if path starts with "/"
|
||||
bool IsAbsolutePath(std::string_view path);
|
||||
|
||||
/// Remove trailing slashes
|
||||
std::string RemoveTrailingSlashes(std::string_view path);
|
||||
|
||||
/// Combine two paths
|
||||
std::string CombinePaths(std::string_view base, std::string_view relative);
|
||||
|
||||
} // namespace PathUtility
|
||||
|
||||
} // namespace FileSys
|
||||
|
|
@ -93,7 +93,7 @@ protected:
|
|||
|
||||
// Get the file size, and validate our offset
|
||||
s64 file_size = 0;
|
||||
R_TRY(this->DoGetSize(std::addressof(file_size)));
|
||||
R_TRY(this->DoGetSize(&file_size));
|
||||
R_UNLESS(offset <= file_size, ResultOutOfRange);
|
||||
|
||||
*out = static_cast<size_t>((std::min)(file_size - offset, static_cast<s64>(size)));
|
||||
|
|
@ -128,6 +128,11 @@ protected:
|
|||
|
||||
private:
|
||||
Result DoRead(size_t* out, s64 offset, void* buffer, size_t size, const ReadOption& option) {
|
||||
// Validate backend exists
|
||||
if (!backend) {
|
||||
return ResultPathNotFound;
|
||||
}
|
||||
|
||||
const auto read_size = backend->Read(static_cast<u8*>(buffer), size, offset);
|
||||
*out = read_size;
|
||||
|
||||
|
|
@ -135,27 +140,47 @@ private:
|
|||
}
|
||||
|
||||
Result DoGetSize(s64* out) {
|
||||
// Validate backend exists
|
||||
if (!backend) {
|
||||
return ResultPathNotFound;
|
||||
}
|
||||
|
||||
*out = backend->GetSize();
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result DoFlush() {
|
||||
// Exists for SDK compatibiltity -- No need to flush file.
|
||||
// Exists for SDK compatibility -- No need to flush file.
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result DoWrite(s64 offset, const void* buffer, size_t size, const WriteOption& option) {
|
||||
// Validate backend exists
|
||||
if (!backend) {
|
||||
return ResultPathNotFound;
|
||||
}
|
||||
|
||||
const std::size_t written = backend->Write(static_cast<const u8*>(buffer), size, offset);
|
||||
|
||||
ASSERT_MSG(written == size,
|
||||
"Could not write all bytes to file (requested={:016X}, actual={:016X}).", size,
|
||||
written);
|
||||
// Based on LibHac: Check if write was successful
|
||||
if (written != size) {
|
||||
LOG_ERROR(Service_FS, "Write failed: requested={:016X}, actual={:016X}", size, written);
|
||||
return ResultUsableSpaceNotEnough;
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result DoSetSize(s64 size) {
|
||||
backend->Resize(size);
|
||||
// Validate backend exists
|
||||
if (!backend) {
|
||||
return ResultPathNotFound;
|
||||
}
|
||||
|
||||
// Try to resize, check for success
|
||||
if (!backend->Resize(size)) {
|
||||
return ResultUsableSpaceNotEnough;
|
||||
}
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -163,7 +163,9 @@ private:
|
|||
}
|
||||
|
||||
Result DoCommit() {
|
||||
R_THROW(ResultNotImplemented);
|
||||
// For most filesystems (SDMC, RomFS, etc), commit is a no-op
|
||||
// SaveData filesystems would override this if they need journaling
|
||||
return ResultSuccess;
|
||||
}
|
||||
|
||||
Result DoGetFreeSpaceSize(s64* out, const Path& path) {
|
||||
|
|
|
|||
286
src/core/file_sys/savedata_extra_data_accessor.cpp
Normal file
286
src/core/file_sys/savedata_extra_data_accessor.cpp
Normal file
|
|
@ -0,0 +1,286 @@
|
|||
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <cstring>
|
||||
#include "common/logging/log.h"
|
||||
#include "core/file_sys/errors.h"
|
||||
#include "core/file_sys/savedata_extra_data_accessor.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
SaveDataExtraDataAccessor::SaveDataExtraDataAccessor(VirtualDir save_data_directory)
|
||||
: save_directory(std::move(save_data_directory)), is_journaling_enabled(true) {}
|
||||
|
||||
SaveDataExtraDataAccessor::~SaveDataExtraDataAccessor() = default;
|
||||
|
||||
Result SaveDataExtraDataAccessor::Initialize(bool create_if_missing) {
|
||||
std::scoped_lock lk{mutex};
|
||||
|
||||
// Check if modified (working) extra data exists
|
||||
auto modified_file = save_directory->GetFile(ModifiedExtraDataFileName);
|
||||
|
||||
if (modified_file == nullptr) {
|
||||
if (!create_if_missing) {
|
||||
return ResultPathNotFound;
|
||||
}
|
||||
|
||||
// Create the modified extra data file
|
||||
modified_file = save_directory->CreateFile(ModifiedExtraDataFileName);
|
||||
if (modified_file == nullptr) {
|
||||
return ResultPermissionDenied;
|
||||
}
|
||||
|
||||
if (!modified_file->Resize(sizeof(SaveDataExtraData))) {
|
||||
return ResultUsableSpaceNotEnough;
|
||||
}
|
||||
|
||||
// Initialize with zeros
|
||||
SaveDataExtraData initial_data{};
|
||||
modified_file->WriteObject(initial_data);
|
||||
}
|
||||
|
||||
// Ensure modified file is correct size
|
||||
R_TRY(EnsureExtraDataSize(ModifiedExtraDataFileName));
|
||||
|
||||
// Check for committed extra data (for journaling)
|
||||
auto committed_file = save_directory->GetFile(CommittedExtraDataFileName);
|
||||
|
||||
if (committed_file == nullptr) {
|
||||
// Check if synchronizing file exists (interrupted commit)
|
||||
auto sync_file = save_directory->GetFile(SynchronizingExtraDataFileName);
|
||||
|
||||
if (sync_file != nullptr) {
|
||||
// Interrupted commit - finish it
|
||||
if (!sync_file->Rename(CommittedExtraDataFileName)) {
|
||||
return ResultPermissionDenied;
|
||||
}
|
||||
} else if (create_if_missing) {
|
||||
// Create committed file
|
||||
committed_file = save_directory->CreateFile(CommittedExtraDataFileName);
|
||||
if (committed_file == nullptr) {
|
||||
return ResultPermissionDenied;
|
||||
}
|
||||
|
||||
if (!committed_file->Resize(sizeof(SaveDataExtraData))) {
|
||||
return ResultUsableSpaceNotEnough;
|
||||
}
|
||||
|
||||
// Copy from modified to committed
|
||||
R_TRY(SynchronizeExtraData(CommittedExtraDataFileName, ModifiedExtraDataFileName));
|
||||
}
|
||||
} else {
|
||||
// Ensure committed file is correct size
|
||||
R_TRY(EnsureExtraDataSize(CommittedExtraDataFileName));
|
||||
|
||||
// If journaling is enabled, sync committed → modified
|
||||
if (is_journaling_enabled) {
|
||||
R_TRY(SynchronizeExtraData(ModifiedExtraDataFileName, CommittedExtraDataFileName));
|
||||
}
|
||||
}
|
||||
|
||||
return ResultSuccess;
|
||||
}
|
||||
|
||||
Result SaveDataExtraDataAccessor::ReadExtraData(SaveDataExtraData* out_extra_data) {
|
||||
std::scoped_lock lk{mutex};
|
||||
|
||||
// For journaling: read from committed if it exists, otherwise modified
|
||||
// For non-journaling: always read from modified
|
||||
const char* file_to_read =
|
||||
is_journaling_enabled ? CommittedExtraDataFileName : ModifiedExtraDataFileName;
|
||||
|
||||
auto file = save_directory->GetFile(file_to_read);
|
||||
if (file == nullptr) {
|
||||
// Fallback to modified if committed doesn't exist
|
||||
file = save_directory->GetFile(ModifiedExtraDataFileName);
|
||||
if (file == nullptr) {
|
||||
return ResultPathNotFound;
|
||||
}
|
||||
file_to_read = ModifiedExtraDataFileName;
|
||||
}
|
||||
|
||||
Result result = ReadExtraDataImpl(out_extra_data, file_to_read);
|
||||
if (result != ResultSuccess) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Update size information based on current directory contents
|
||||
out_extra_data->available_size = CalculateDirectorySize(save_directory);
|
||||
|
||||
return ResultSuccess;
|
||||
}
|
||||
|
||||
s64 SaveDataExtraDataAccessor::CalculateDirectorySize(VirtualDir directory) const {
|
||||
if (directory == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
s64 total_size = 0;
|
||||
|
||||
// Add file sizes
|
||||
for (const auto& file : directory->GetFiles()) {
|
||||
total_size += file->GetSize();
|
||||
}
|
||||
|
||||
// Add subdirectory sizes recursively (but skip ExtraData files)
|
||||
for (const auto& subdir : directory->GetSubdirectories()) {
|
||||
total_size += CalculateDirectorySize(subdir);
|
||||
}
|
||||
|
||||
return total_size;
|
||||
}
|
||||
|
||||
Result SaveDataExtraDataAccessor::WriteExtraData(const SaveDataExtraData& extra_data) {
|
||||
std::scoped_lock lk{mutex};
|
||||
|
||||
return WriteExtraDataImpl(extra_data, ModifiedExtraDataFileName);
|
||||
}
|
||||
|
||||
Result SaveDataExtraDataAccessor::CommitExtraData() {
|
||||
std::scoped_lock lk{mutex};
|
||||
|
||||
if (!is_journaling_enabled) {
|
||||
// Non-journaling: just write directly to the file
|
||||
return ResultSuccess;
|
||||
}
|
||||
|
||||
// Journaling: Atomic commit process
|
||||
// 1. Rename committed → synchronizing (backup)
|
||||
auto committed_file = save_directory->GetFile(CommittedExtraDataFileName);
|
||||
if (committed_file != nullptr) {
|
||||
if (!committed_file->Rename(SynchronizingExtraDataFileName)) {
|
||||
return ResultPermissionDenied;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Copy modified → synchronizing
|
||||
R_TRY(SynchronizeExtraData(SynchronizingExtraDataFileName, ModifiedExtraDataFileName));
|
||||
|
||||
// 3. Rename synchronizing → committed (make it permanent)
|
||||
auto sync_file = save_directory->GetFile(SynchronizingExtraDataFileName);
|
||||
if (sync_file == nullptr) {
|
||||
return ResultPathNotFound;
|
||||
}
|
||||
|
||||
if (!sync_file->Rename(CommittedExtraDataFileName)) {
|
||||
return ResultPermissionDenied;
|
||||
}
|
||||
|
||||
return ResultSuccess;
|
||||
}
|
||||
|
||||
Result SaveDataExtraDataAccessor::CommitExtraDataWithTimeStamp(s64 timestamp) {
|
||||
std::scoped_lock lk{mutex};
|
||||
|
||||
// Read current extra data
|
||||
SaveDataExtraData extra_data{};
|
||||
R_TRY(ReadExtraDataImpl(&extra_data, ModifiedExtraDataFileName));
|
||||
|
||||
// Update timestamp
|
||||
extra_data.timestamp = timestamp;
|
||||
|
||||
// Generate new commit ID (non-zero, different from previous)
|
||||
if (extra_data.commit_id == 0) {
|
||||
extra_data.commit_id = 1;
|
||||
} else {
|
||||
extra_data.commit_id++;
|
||||
}
|
||||
|
||||
// Write updated data
|
||||
R_TRY(WriteExtraDataImpl(extra_data, ModifiedExtraDataFileName));
|
||||
|
||||
// Unlock mutex for commit
|
||||
mutex.unlock();
|
||||
Result result = CommitExtraData();
|
||||
mutex.lock();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool SaveDataExtraDataAccessor::ExtraDataExists() const {
|
||||
return save_directory->GetFile(ModifiedExtraDataFileName) != nullptr ||
|
||||
save_directory->GetFile(CommittedExtraDataFileName) != nullptr;
|
||||
}
|
||||
|
||||
Result SaveDataExtraDataAccessor::ReadExtraDataImpl(SaveDataExtraData* out_extra_data,
|
||||
const char* file_name) {
|
||||
auto file = save_directory->GetFile(file_name);
|
||||
if (file == nullptr) {
|
||||
return ResultPathNotFound;
|
||||
}
|
||||
|
||||
if (file->GetSize() < sizeof(SaveDataExtraData)) {
|
||||
LOG_ERROR(Service_FS, "ExtraData file {} is too small: {} bytes", file_name,
|
||||
file->GetSize());
|
||||
return ResultDataCorrupted;
|
||||
}
|
||||
|
||||
const auto bytes_read = file->ReadObject(out_extra_data);
|
||||
if (bytes_read != sizeof(SaveDataExtraData)) {
|
||||
LOG_ERROR(Service_FS, "Failed to read ExtraData from {}: read {} bytes", file_name,
|
||||
bytes_read);
|
||||
return ResultDataCorrupted;
|
||||
}
|
||||
|
||||
return ResultSuccess;
|
||||
}
|
||||
|
||||
Result SaveDataExtraDataAccessor::WriteExtraDataImpl(const SaveDataExtraData& extra_data,
|
||||
const char* file_name) {
|
||||
auto file = save_directory->GetFile(file_name);
|
||||
if (file == nullptr) {
|
||||
// Create the file if it doesn't exist
|
||||
file = save_directory->CreateFile(file_name);
|
||||
if (file == nullptr) {
|
||||
return ResultPermissionDenied;
|
||||
}
|
||||
|
||||
if (!file->Resize(sizeof(SaveDataExtraData))) {
|
||||
return ResultUsableSpaceNotEnough;
|
||||
}
|
||||
}
|
||||
|
||||
const auto bytes_written = file->WriteObject(extra_data);
|
||||
if (bytes_written != sizeof(SaveDataExtraData)) {
|
||||
LOG_ERROR(Service_FS, "Failed to write ExtraData to {}: wrote {} bytes", file_name,
|
||||
bytes_written);
|
||||
return ResultUsableSpaceNotEnough;
|
||||
}
|
||||
|
||||
return ResultSuccess;
|
||||
}
|
||||
|
||||
Result SaveDataExtraDataAccessor::SynchronizeExtraData(const char* dest_file,
|
||||
const char* source_file) {
|
||||
// Read from source
|
||||
SaveDataExtraData extra_data{};
|
||||
R_TRY(ReadExtraDataImpl(&extra_data, source_file));
|
||||
|
||||
// Write to destination
|
||||
R_TRY(WriteExtraDataImpl(extra_data, dest_file));
|
||||
|
||||
return ResultSuccess;
|
||||
}
|
||||
|
||||
Result SaveDataExtraDataAccessor::EnsureExtraDataSize(const char* file_name) {
|
||||
auto file = save_directory->GetFile(file_name);
|
||||
if (file == nullptr) {
|
||||
return ResultPathNotFound;
|
||||
}
|
||||
|
||||
const auto current_size = file->GetSize();
|
||||
if (current_size == sizeof(SaveDataExtraData)) {
|
||||
return ResultSuccess;
|
||||
}
|
||||
|
||||
LOG_WARNING(Service_FS, "ExtraData file {} has incorrect size: {} bytes, resizing to {}",
|
||||
file_name, current_size, sizeof(SaveDataExtraData));
|
||||
|
||||
if (!file->Resize(sizeof(SaveDataExtraData))) {
|
||||
return ResultUsableSpaceNotEnough;
|
||||
}
|
||||
|
||||
return ResultSuccess;
|
||||
}
|
||||
|
||||
} // namespace FileSys
|
||||
58
src/core/file_sys/savedata_extra_data_accessor.h
Normal file
58
src/core/file_sys/savedata_extra_data_accessor.h
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include "core/file_sys/fs_save_data_types.h"
|
||||
#include "core/file_sys/vfs/vfs.h"
|
||||
#include "core/hle/result.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
/// Manages reading and writing SaveDataExtraData with transactional semantics
|
||||
/// Based on LibHac's DirectorySaveDataFileSystem extra data implementation
|
||||
class SaveDataExtraDataAccessor {
|
||||
public:
|
||||
explicit SaveDataExtraDataAccessor(VirtualDir save_data_directory);
|
||||
~SaveDataExtraDataAccessor();
|
||||
|
||||
/// Initialize extra data files for this save data
|
||||
Result Initialize(bool create_if_missing);
|
||||
|
||||
/// Read the current extra data
|
||||
Result ReadExtraData(SaveDataExtraData* out_extra_data);
|
||||
|
||||
/// Write extra data (updates working copy only)
|
||||
Result WriteExtraData(const SaveDataExtraData& extra_data);
|
||||
|
||||
/// Commit extra data changes (makes working copy permanent)
|
||||
Result CommitExtraData();
|
||||
|
||||
/// Update timestamp and commit ID, then commit
|
||||
Result CommitExtraDataWithTimeStamp(s64 timestamp);
|
||||
|
||||
/// Check if extra data exists
|
||||
bool ExtraDataExists() const;
|
||||
|
||||
private:
|
||||
static constexpr const char* CommittedExtraDataFileName = "ExtraData0";
|
||||
static constexpr const char* ModifiedExtraDataFileName = "ExtraData1";
|
||||
static constexpr const char* SynchronizingExtraDataFileName = "ExtraData_";
|
||||
|
||||
Result ReadExtraDataImpl(SaveDataExtraData* out_extra_data, const char* file_name);
|
||||
Result WriteExtraDataImpl(const SaveDataExtraData& extra_data, const char* file_name);
|
||||
Result SynchronizeExtraData(const char* dest_file, const char* source_file);
|
||||
Result EnsureExtraDataSize(const char* file_name);
|
||||
|
||||
/// Calculate total size of directory contents
|
||||
s64 CalculateDirectorySize(VirtualDir directory) const;
|
||||
|
||||
VirtualDir save_directory;
|
||||
std::mutex mutex;
|
||||
bool is_journaling_enabled;
|
||||
};
|
||||
|
||||
} // namespace FileSys
|
||||
|
|
@ -4,11 +4,14 @@
|
|||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <chrono>
|
||||
#include "common/assert.h"
|
||||
#include "common/common_types.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/uuid.h"
|
||||
#include "core/core.h"
|
||||
#include "core/file_sys/errors.h"
|
||||
#include "core/file_sys/savedata_extra_data_accessor.h"
|
||||
#include "core/file_sys/savedata_factory.h"
|
||||
#include "core/file_sys/vfs/vfs.h"
|
||||
|
||||
|
|
@ -68,8 +71,33 @@ SaveDataFactory::~SaveDataFactory() = default;
|
|||
VirtualDir SaveDataFactory::Create(SaveDataSpaceId space, const SaveDataAttribute& meta) const {
|
||||
const auto save_directory = GetFullPath(program_id, dir, space, meta.type, meta.program_id,
|
||||
meta.user_id, meta.system_save_data_id);
|
||||
auto save_dir = dir->CreateDirectoryRelative(save_directory);
|
||||
if (save_dir == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return dir->CreateDirectoryRelative(save_directory);
|
||||
// Initialize ExtraData for new save
|
||||
SaveDataExtraDataAccessor accessor(save_dir);
|
||||
if (accessor.Initialize(true) != ResultSuccess) {
|
||||
LOG_WARNING(Service_FS, "Failed to initialize ExtraData for new save at {}",
|
||||
save_directory);
|
||||
// Continue anyway - save is still usable
|
||||
} else {
|
||||
// Write initial extra data
|
||||
SaveDataExtraData initial_data{};
|
||||
initial_data.attr = meta;
|
||||
initial_data.owner_id = meta.program_id;
|
||||
initial_data.timestamp = std::chrono::system_clock::now().time_since_epoch().count();
|
||||
initial_data.flags = static_cast<u32>(SaveDataFlags::None);
|
||||
initial_data.available_size = 0; // Will be updated on commit
|
||||
initial_data.journal_size = 0;
|
||||
initial_data.commit_id = 1;
|
||||
|
||||
accessor.WriteExtraData(initial_data);
|
||||
accessor.CommitExtraData();
|
||||
}
|
||||
|
||||
return save_dir;
|
||||
}
|
||||
|
||||
VirtualDir SaveDataFactory::Open(SaveDataSpaceId space, const SaveDataAttribute& meta) const {
|
||||
|
|
@ -193,4 +221,101 @@ void SaveDataFactory::SetAutoCreate(bool state) {
|
|||
auto_create = state;
|
||||
}
|
||||
|
||||
Result SaveDataFactory::ReadSaveDataExtraData(SaveDataExtraData* out_extra_data,
|
||||
SaveDataSpaceId space,
|
||||
const SaveDataAttribute& attribute) const {
|
||||
const auto save_directory =
|
||||
GetFullPath(program_id, dir, space, attribute.type, attribute.program_id, attribute.user_id,
|
||||
attribute.system_save_data_id);
|
||||
|
||||
auto save_dir = dir->GetDirectoryRelative(save_directory);
|
||||
if (save_dir == nullptr) {
|
||||
return ResultPathNotFound;
|
||||
}
|
||||
|
||||
SaveDataExtraDataAccessor accessor(save_dir);
|
||||
|
||||
// Try to initialize (but don't create if missing)
|
||||
if (Result result = accessor.Initialize(false); result != ResultSuccess) {
|
||||
// ExtraData doesn't exist - return default values
|
||||
LOG_DEBUG(Service_FS, "ExtraData not found for save at {}, returning defaults",
|
||||
save_directory);
|
||||
|
||||
// Return zeroed data
|
||||
std::memset(out_extra_data, 0, sizeof(SaveDataExtraData));
|
||||
out_extra_data->attr = attribute;
|
||||
return ResultSuccess;
|
||||
}
|
||||
|
||||
return accessor.ReadExtraData(out_extra_data);
|
||||
}
|
||||
|
||||
Result SaveDataFactory::WriteSaveDataExtraData(const SaveDataExtraData& extra_data,
|
||||
SaveDataSpaceId space,
|
||||
const SaveDataAttribute& attribute) const {
|
||||
const auto save_directory =
|
||||
GetFullPath(program_id, dir, space, attribute.type, attribute.program_id, attribute.user_id,
|
||||
attribute.system_save_data_id);
|
||||
|
||||
auto save_dir = dir->GetDirectoryRelative(save_directory);
|
||||
if (save_dir == nullptr) {
|
||||
return ResultPathNotFound;
|
||||
}
|
||||
|
||||
SaveDataExtraDataAccessor accessor(save_dir);
|
||||
|
||||
// Initialize and create if missing
|
||||
R_TRY(accessor.Initialize(true));
|
||||
|
||||
// Write the data
|
||||
R_TRY(accessor.WriteExtraData(extra_data));
|
||||
|
||||
// Commit immediately for transactional writes
|
||||
R_TRY(accessor.CommitExtraData());
|
||||
|
||||
return ResultSuccess;
|
||||
}
|
||||
|
||||
Result SaveDataFactory::WriteSaveDataExtraDataWithMask(const SaveDataExtraData& extra_data,
|
||||
const SaveDataExtraData& mask,
|
||||
SaveDataSpaceId space,
|
||||
const SaveDataAttribute& attribute) const {
|
||||
const auto save_directory =
|
||||
GetFullPath(program_id, dir, space, attribute.type, attribute.program_id, attribute.user_id,
|
||||
attribute.system_save_data_id);
|
||||
|
||||
auto save_dir = dir->GetDirectoryRelative(save_directory);
|
||||
if (save_dir == nullptr) {
|
||||
return ResultPathNotFound;
|
||||
}
|
||||
|
||||
SaveDataExtraDataAccessor accessor(save_dir);
|
||||
|
||||
// Initialize and create if missing
|
||||
R_TRY(accessor.Initialize(true));
|
||||
|
||||
// Read existing data
|
||||
SaveDataExtraData current_data{};
|
||||
R_TRY(accessor.ReadExtraData(¤t_data));
|
||||
|
||||
// Apply mask: copy only the bytes where mask is non-zero
|
||||
const u8* extra_data_bytes = reinterpret_cast<const u8*>(&extra_data);
|
||||
const u8* mask_bytes = reinterpret_cast<const u8*>(&mask);
|
||||
u8* current_data_bytes = reinterpret_cast<u8*>(¤t_data);
|
||||
|
||||
for (size_t i = 0; i < sizeof(SaveDataExtraData); ++i) {
|
||||
if (mask_bytes[i] != 0) {
|
||||
current_data_bytes[i] = extra_data_bytes[i];
|
||||
}
|
||||
}
|
||||
|
||||
// Write back the masked data
|
||||
R_TRY(accessor.WriteExtraData(current_data));
|
||||
|
||||
// Commit the changes
|
||||
R_TRY(accessor.CommitExtraData());
|
||||
|
||||
return ResultSuccess;
|
||||
}
|
||||
|
||||
} // namespace FileSys
|
||||
|
|
|
|||
|
|
@ -44,6 +44,15 @@ public:
|
|||
void WriteSaveDataSize(SaveDataType type, u64 title_id, u128 user_id,
|
||||
SaveDataSize new_value) const;
|
||||
|
||||
// ExtraData operations
|
||||
Result ReadSaveDataExtraData(SaveDataExtraData* out_extra_data, SaveDataSpaceId space,
|
||||
const SaveDataAttribute& attribute) const;
|
||||
Result WriteSaveDataExtraData(const SaveDataExtraData& extra_data, SaveDataSpaceId space,
|
||||
const SaveDataAttribute& attribute) const;
|
||||
Result WriteSaveDataExtraDataWithMask(const SaveDataExtraData& extra_data,
|
||||
const SaveDataExtraData& mask, SaveDataSpaceId space,
|
||||
const SaveDataAttribute& attribute) const;
|
||||
|
||||
void SetAutoCreate(bool state);
|
||||
|
||||
private:
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
#include "core/file_sys/card_image.h"
|
||||
#include "core/file_sys/control_metadata.h"
|
||||
#include "core/file_sys/errors.h"
|
||||
#include "core/file_sys/fs_path_normalizer.h"
|
||||
#include "core/file_sys/patch_manager.h"
|
||||
#include "core/file_sys/registered_cache.h"
|
||||
#include "core/file_sys/romfs_factory.h"
|
||||
|
|
@ -62,12 +63,10 @@ Result VfsDirectoryServiceWrapper::CreateFile(const std::string& path_, u64 size
|
|||
|
||||
auto file = dir->CreateFile(Common::FS::GetFilename(path));
|
||||
if (file == nullptr) {
|
||||
// TODO(DarkLordZach): Find a better error code for this
|
||||
return ResultUnknown;
|
||||
return FileSys::ResultPermissionDenied;
|
||||
}
|
||||
if (!file->Resize(size)) {
|
||||
// TODO(DarkLordZach): Find a better error code for this
|
||||
return ResultUnknown;
|
||||
return FileSys::ResultUsableSpaceNotEnough;
|
||||
}
|
||||
return ResultSuccess;
|
||||
}
|
||||
|
|
@ -84,8 +83,7 @@ Result VfsDirectoryServiceWrapper::DeleteFile(const std::string& path_) const {
|
|||
return FileSys::ResultPathNotFound;
|
||||
}
|
||||
if (!dir->DeleteFile(Common::FS::GetFilename(path))) {
|
||||
// TODO(DarkLordZach): Find a better error code for this
|
||||
return ResultUnknown;
|
||||
return FileSys::ResultPermissionDenied;
|
||||
}
|
||||
|
||||
return ResultSuccess;
|
||||
|
|
@ -104,8 +102,7 @@ Result VfsDirectoryServiceWrapper::CreateDirectory(const std::string& path_) con
|
|||
relative_path = Common::FS::SanitizePath(fmt::format("{}/{}", relative_path, component));
|
||||
auto new_dir = backing->CreateSubdirectory(relative_path);
|
||||
if (new_dir == nullptr) {
|
||||
// TODO(DarkLordZach): Find a better error code for this
|
||||
return ResultUnknown;
|
||||
return FileSys::ResultUsableSpaceNotEnough;
|
||||
}
|
||||
}
|
||||
return ResultSuccess;
|
||||
|
|
@ -114,9 +111,22 @@ Result VfsDirectoryServiceWrapper::CreateDirectory(const std::string& path_) con
|
|||
Result VfsDirectoryServiceWrapper::DeleteDirectory(const std::string& path_) const {
|
||||
std::string path(Common::FS::SanitizePath(path_));
|
||||
auto dir = GetDirectoryRelativeWrapped(backing, Common::FS::GetParentPath(path));
|
||||
if (dir == nullptr) {
|
||||
return FileSys::ResultPathNotFound;
|
||||
}
|
||||
|
||||
auto target_dir = dir->GetSubdirectory(Common::FS::GetFilename(path));
|
||||
if (target_dir == nullptr) {
|
||||
return FileSys::ResultPathNotFound;
|
||||
}
|
||||
|
||||
// Check if directory is empty
|
||||
if (!target_dir->GetFiles().empty() || !target_dir->GetSubdirectories().empty()) {
|
||||
return FileSys::ResultDirectoryNotEmpty;
|
||||
}
|
||||
|
||||
if (!dir->DeleteSubdirectory(Common::FS::GetFilename(path))) {
|
||||
// TODO(DarkLordZach): Find a better error code for this
|
||||
return ResultUnknown;
|
||||
return FileSys::ResultPermissionDenied;
|
||||
}
|
||||
return ResultSuccess;
|
||||
}
|
||||
|
|
@ -124,9 +134,11 @@ Result VfsDirectoryServiceWrapper::DeleteDirectory(const std::string& path_) con
|
|||
Result VfsDirectoryServiceWrapper::DeleteDirectoryRecursively(const std::string& path_) const {
|
||||
std::string path(Common::FS::SanitizePath(path_));
|
||||
auto dir = GetDirectoryRelativeWrapped(backing, Common::FS::GetParentPath(path));
|
||||
if (dir == nullptr) {
|
||||
return FileSys::ResultPathNotFound;
|
||||
}
|
||||
if (!dir->DeleteSubdirectoryRecursive(Common::FS::GetFilename(path))) {
|
||||
// TODO(DarkLordZach): Find a better error code for this
|
||||
return ResultUnknown;
|
||||
return FileSys::ResultPermissionDenied;
|
||||
}
|
||||
return ResultSuccess;
|
||||
}
|
||||
|
|
@ -135,9 +147,12 @@ Result VfsDirectoryServiceWrapper::CleanDirectoryRecursively(const std::string&
|
|||
const std::string sanitized_path(Common::FS::SanitizePath(path));
|
||||
auto dir = GetDirectoryRelativeWrapped(backing, Common::FS::GetParentPath(sanitized_path));
|
||||
|
||||
if (dir == nullptr) {
|
||||
return FileSys::ResultPathNotFound;
|
||||
}
|
||||
|
||||
if (!dir->CleanSubdirectoryRecursive(Common::FS::GetFilename(sanitized_path))) {
|
||||
// TODO(DarkLordZach): Find a better error code for this
|
||||
return ResultUnknown;
|
||||
return FileSys::ResultPermissionDenied;
|
||||
}
|
||||
|
||||
return ResultSuccess;
|
||||
|
|
@ -161,8 +176,7 @@ Result VfsDirectoryServiceWrapper::RenameFile(const std::string& src_path_,
|
|||
}
|
||||
|
||||
if (!src->Rename(Common::FS::GetFilename(dest_path))) {
|
||||
// TODO(DarkLordZach): Find a better error code for this
|
||||
return ResultUnknown;
|
||||
return FileSys::ResultPermissionDenied;
|
||||
}
|
||||
return ResultSuccess;
|
||||
}
|
||||
|
|
@ -179,8 +193,7 @@ Result VfsDirectoryServiceWrapper::RenameFile(const std::string& src_path_,
|
|||
"Could not write all of the bytes but everything else has succeeded.");
|
||||
|
||||
if (!src->GetContainingDirectory()->DeleteFile(Common::FS::GetFilename(src_path))) {
|
||||
// TODO(DarkLordZach): Find a better error code for this
|
||||
return ResultUnknown;
|
||||
return FileSys::ResultPermissionDenied;
|
||||
}
|
||||
|
||||
return ResultSuccess;
|
||||
|
|
@ -191,25 +204,76 @@ Result VfsDirectoryServiceWrapper::RenameDirectory(const std::string& src_path_,
|
|||
std::string src_path(Common::FS::SanitizePath(src_path_));
|
||||
std::string dest_path(Common::FS::SanitizePath(dest_path_));
|
||||
auto src = GetDirectoryRelativeWrapped(backing, src_path);
|
||||
|
||||
if (src == nullptr) {
|
||||
return FileSys::ResultPathNotFound;
|
||||
}
|
||||
|
||||
// Check if destination already exists
|
||||
auto dest = GetDirectoryRelativeWrapped(backing, dest_path);
|
||||
if (dest != nullptr) {
|
||||
return FileSys::ResultPathAlreadyExists;
|
||||
}
|
||||
|
||||
if (Common::FS::GetParentPath(src_path) == Common::FS::GetParentPath(dest_path)) {
|
||||
// Use more-optimized vfs implementation rename.
|
||||
if (src == nullptr)
|
||||
return FileSys::ResultPathNotFound;
|
||||
// Use more-optimized vfs implementation rename (same parent directory).
|
||||
if (!src->Rename(Common::FS::GetFilename(dest_path))) {
|
||||
// TODO(DarkLordZach): Find a better error code for this
|
||||
return ResultUnknown;
|
||||
return FileSys::ResultPermissionDenied;
|
||||
}
|
||||
return ResultSuccess;
|
||||
}
|
||||
|
||||
// TODO(DarkLordZach): Implement renaming across the tree (move).
|
||||
ASSERT_MSG(false,
|
||||
"Could not rename directory with path \"{}\" to new path \"{}\" because parent dirs "
|
||||
"don't match -- UNIMPLEMENTED",
|
||||
src_path, dest_path);
|
||||
// Different parent directories - need to move by copying then deleting.
|
||||
// Based on LibHac's approach: create dest, copy contents recursively, delete source.
|
||||
LOG_DEBUG(Service_FS, "Moving directory across tree from \"{}\" to \"{}\"", src_path, dest_path);
|
||||
|
||||
// TODO(DarkLordZach): Find a better error code for this
|
||||
return ResultUnknown;
|
||||
// Create the destination directory
|
||||
auto dest_parent = GetDirectoryRelativeWrapped(backing, Common::FS::GetParentPath(dest_path));
|
||||
if (dest_parent == nullptr) {
|
||||
return FileSys::ResultPathNotFound;
|
||||
}
|
||||
|
||||
auto new_dir = dest_parent->CreateSubdirectory(Common::FS::GetFilename(dest_path));
|
||||
if (new_dir == nullptr) {
|
||||
return FileSys::ResultPermissionDenied;
|
||||
}
|
||||
|
||||
// Recursively copy all contents
|
||||
// Copy files
|
||||
for (const auto& file : src->GetFiles()) {
|
||||
auto new_file = new_dir->CreateFile(file->GetName());
|
||||
if (new_file == nullptr) {
|
||||
return FileSys::ResultUsableSpaceNotEnough;
|
||||
}
|
||||
|
||||
const auto data = file->ReadAllBytes();
|
||||
if (new_file->WriteBytes(data) != data.size()) {
|
||||
return FileSys::ResultUsableSpaceNotEnough;
|
||||
}
|
||||
}
|
||||
|
||||
// Copy subdirectories recursively
|
||||
for (const auto& subdir : src->GetSubdirectories()) {
|
||||
auto src_subdir_path = fmt::format("{}/{}", src_path, subdir->GetName());
|
||||
auto dest_subdir_path = fmt::format("{}/{}", dest_path, subdir->GetName());
|
||||
|
||||
auto result = RenameDirectory(src_subdir_path, dest_subdir_path);
|
||||
if (result != ResultSuccess) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the source directory
|
||||
auto src_parent = GetDirectoryRelativeWrapped(backing, Common::FS::GetParentPath(src_path));
|
||||
if (src_parent == nullptr) {
|
||||
return FileSys::ResultPathNotFound;
|
||||
}
|
||||
|
||||
if (!src_parent->DeleteSubdirectory(Common::FS::GetFilename(src_path))) {
|
||||
return FileSys::ResultPermissionDenied;
|
||||
}
|
||||
|
||||
return ResultSuccess;
|
||||
}
|
||||
|
||||
Result VfsDirectoryServiceWrapper::OpenFile(FileSys::VirtualFile* out_file,
|
||||
|
|
@ -250,14 +314,21 @@ Result VfsDirectoryServiceWrapper::OpenDirectory(FileSys::VirtualDir* out_direct
|
|||
Result VfsDirectoryServiceWrapper::GetEntryType(FileSys::DirectoryEntryType* out_entry_type,
|
||||
const std::string& path_) const {
|
||||
std::string path(Common::FS::SanitizePath(path_));
|
||||
|
||||
// Handle root directory case (based on LibHac behavior)
|
||||
if (FileSys::PathUtility::IsRootPath(path)) {
|
||||
*out_entry_type = FileSys::DirectoryEntryType::Directory;
|
||||
return ResultSuccess;
|
||||
}
|
||||
|
||||
auto dir = GetDirectoryRelativeWrapped(backing, Common::FS::GetParentPath(path));
|
||||
if (dir == nullptr) {
|
||||
return FileSys::ResultPathNotFound;
|
||||
}
|
||||
|
||||
auto filename = Common::FS::GetFilename(path);
|
||||
// TODO(Subv): Some games use the '/' path, find out what this means.
|
||||
if (filename.empty()) {
|
||||
// Empty filename after normalization means the path is a directory
|
||||
*out_entry_type = FileSys::DirectoryEntryType::Directory;
|
||||
return ResultSuccess;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -127,9 +127,11 @@ Result IFileSystem::GetEntryType(
|
|||
}
|
||||
|
||||
Result IFileSystem::Commit() {
|
||||
LOG_WARNING(Service_FS, "(STUBBED) called");
|
||||
LOG_DEBUG(Service_FS, "called");
|
||||
|
||||
R_SUCCEED();
|
||||
// Based on LibHac DirectorySaveDataFileSystem::DoCommit
|
||||
// The backend FSA layer should handle the actual commit logic
|
||||
R_RETURN(backend->Commit());
|
||||
}
|
||||
|
||||
Result IFileSystem::GetFreeSpaceSize(
|
||||
|
|
|
|||
|
|
@ -339,70 +339,137 @@ Result FSP_SRV::FindSaveDataWithFilter(Out<s64> out_count,
|
|||
Result FSP_SRV::WriteSaveDataFileSystemExtraData(InBuffer<BufferAttr_HipcMapAlias> buffer,
|
||||
FileSys::SaveDataSpaceId space_id,
|
||||
u64 save_data_id) {
|
||||
LOG_WARNING(Service_FS, "(STUBBED) called, space_id={}, save_data_id={:016X}", space_id,
|
||||
save_data_id);
|
||||
R_SUCCEED();
|
||||
LOG_DEBUG(Service_FS, "called, space_id={}, save_data_id={:016X}", space_id, save_data_id);
|
||||
|
||||
if (buffer.size() < sizeof(FileSys::SaveDataExtraData)) {
|
||||
return FileSys::ResultInvalidSize;
|
||||
}
|
||||
|
||||
FileSys::SaveDataExtraData extra_data{};
|
||||
std::memcpy(&extra_data, buffer.data(), sizeof(FileSys::SaveDataExtraData));
|
||||
|
||||
R_RETURN(save_data_controller->WriteSaveDataExtraData(extra_data, space_id,
|
||||
extra_data.attr));
|
||||
}
|
||||
|
||||
Result FSP_SRV::WriteSaveDataFileSystemExtraDataWithMaskBySaveDataAttribute(
|
||||
InBuffer<BufferAttr_HipcMapAlias> buffer, InBuffer<BufferAttr_HipcMapAlias> mask_buffer,
|
||||
FileSys::SaveDataSpaceId space_id, FileSys::SaveDataAttribute attribute) {
|
||||
LOG_WARNING(Service_FS,
|
||||
"(STUBBED) called, space_id={}, attribute.program_id={:016X}\n"
|
||||
"attribute.user_id={:016X}{:016X}, attribute.save_id={:016X}\n"
|
||||
"attribute.type={}, attribute.rank={}, attribute.index={}",
|
||||
space_id, attribute.program_id, attribute.user_id[1], attribute.user_id[0],
|
||||
attribute.system_save_data_id, attribute.type, attribute.rank, attribute.index);
|
||||
R_SUCCEED();
|
||||
LOG_DEBUG(Service_FS,
|
||||
"called, space_id={}, attribute.program_id={:016X}\n"
|
||||
"attribute.user_id={:016X}{:016X}, attribute.save_id={:016X}\n"
|
||||
"attribute.type={}, attribute.rank={}, attribute.index={}",
|
||||
space_id, attribute.program_id, attribute.user_id[1], attribute.user_id[0],
|
||||
attribute.system_save_data_id, attribute.type, attribute.rank, attribute.index);
|
||||
|
||||
if (buffer.size() < sizeof(FileSys::SaveDataExtraData) ||
|
||||
mask_buffer.size() < sizeof(FileSys::SaveDataExtraData)) {
|
||||
return FileSys::ResultInvalidSize;
|
||||
}
|
||||
|
||||
FileSys::SaveDataExtraData extra_data{};
|
||||
FileSys::SaveDataExtraData mask{};
|
||||
std::memcpy(&extra_data, buffer.data(), sizeof(FileSys::SaveDataExtraData));
|
||||
std::memcpy(&mask, mask_buffer.data(), sizeof(FileSys::SaveDataExtraData));
|
||||
|
||||
R_RETURN(
|
||||
save_data_controller->WriteSaveDataExtraDataWithMask(extra_data, mask, space_id, attribute));
|
||||
}
|
||||
|
||||
Result FSP_SRV::ReadSaveDataFileSystemExtraDataWithMaskBySaveDataAttribute(
|
||||
FileSys::SaveDataSpaceId space_id, FileSys::SaveDataAttribute attribute,
|
||||
InBuffer<BufferAttr_HipcMapAlias> mask_buffer, OutBuffer<BufferAttr_HipcMapAlias> out_buffer) {
|
||||
// Stub this to None for now, backend needs an impl to read/write the SaveDataExtraData
|
||||
// In an earlier version of the code, this was returned as an out argument, but this is not
|
||||
// correct
|
||||
[[maybe_unused]] constexpr auto flags = static_cast<u32>(FileSys::SaveDataFlags::None);
|
||||
LOG_DEBUG(Service_FS,
|
||||
"called, space_id={}, attribute.program_id={:016X}\n"
|
||||
"attribute.user_id={:016X}{:016X}, attribute.save_id={:016X}\n"
|
||||
"attribute.type={}, attribute.rank={}, attribute.index={}",
|
||||
space_id, attribute.program_id, attribute.user_id[1], attribute.user_id[0],
|
||||
attribute.system_save_data_id, attribute.type, attribute.rank, attribute.index);
|
||||
|
||||
LOG_WARNING(Service_FS,
|
||||
"(STUBBED) called, flags={}, space_id={}, attribute.program_id={:016X}\n"
|
||||
"attribute.user_id={:016X}{:016X}, attribute.save_id={:016X}\n"
|
||||
"attribute.type={}, attribute.rank={}, attribute.index={}",
|
||||
flags, space_id, attribute.program_id, attribute.user_id[1], attribute.user_id[0],
|
||||
attribute.system_save_data_id, attribute.type, attribute.rank, attribute.index);
|
||||
if (out_buffer.size() < sizeof(FileSys::SaveDataExtraData)) {
|
||||
return FileSys::ResultInvalidSize;
|
||||
}
|
||||
|
||||
FileSys::SaveDataExtraData extra_data{};
|
||||
R_TRY(save_data_controller->ReadSaveDataExtraData(&extra_data, space_id, attribute));
|
||||
|
||||
// Apply mask if provided
|
||||
if (mask_buffer.size() >= sizeof(FileSys::SaveDataExtraData)) {
|
||||
const u8* mask_bytes = mask_buffer.data();
|
||||
u8* extra_data_bytes = reinterpret_cast<u8*>(&extra_data);
|
||||
|
||||
for (size_t i = 0; i < sizeof(FileSys::SaveDataExtraData); ++i) {
|
||||
if (mask_bytes[i] == 0) {
|
||||
extra_data_bytes[i] = 0; // Zero out masked bytes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::memcpy(out_buffer.data(), &extra_data, sizeof(FileSys::SaveDataExtraData));
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result FSP_SRV::ReadSaveDataFileSystemExtraData(OutBuffer<BufferAttr_HipcMapAlias> out_buffer,
|
||||
u64 save_data_id) {
|
||||
// Stub, backend needs an impl to read/write the SaveDataExtraData
|
||||
LOG_WARNING(Service_FS, "(STUBBED) called, save_data_id={:016X}", save_data_id);
|
||||
std::memset(out_buffer.data(), 0, out_buffer.size());
|
||||
LOG_DEBUG(Service_FS, "called, save_data_id={:016X}", save_data_id);
|
||||
|
||||
if (out_buffer.size() < sizeof(FileSys::SaveDataExtraData)) {
|
||||
return FileSys::ResultInvalidSize;
|
||||
}
|
||||
|
||||
// For now, use User space and construct attribute from save_data_id
|
||||
// In a full implementation, we'd have a save data index to look this up
|
||||
FileSys::SaveDataAttribute attribute{};
|
||||
attribute.system_save_data_id = save_data_id;
|
||||
attribute.type = FileSys::SaveDataType::System;
|
||||
|
||||
FileSys::SaveDataExtraData extra_data{};
|
||||
R_TRY(save_data_controller->ReadSaveDataExtraData(&extra_data, FileSys::SaveDataSpaceId::User,
|
||||
attribute));
|
||||
|
||||
std::memcpy(out_buffer.data(), &extra_data, sizeof(FileSys::SaveDataExtraData));
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result FSP_SRV::ReadSaveDataFileSystemExtraDataBySaveDataAttribute(
|
||||
OutBuffer<BufferAttr_HipcMapAlias> out_buffer, FileSys::SaveDataSpaceId space_id,
|
||||
FileSys::SaveDataAttribute attribute) {
|
||||
// Stub, backend needs an impl to read/write the SaveDataExtraData
|
||||
LOG_WARNING(Service_FS,
|
||||
"(STUBBED) called, space_id={}, attribute.program_id={:016X}\n"
|
||||
"attribute.user_id={:016X}{:016X}, attribute.save_id={:016X}\n"
|
||||
"attribute.type={}, attribute.rank={}, attribute.index={}",
|
||||
space_id, attribute.program_id, attribute.user_id[1], attribute.user_id[0],
|
||||
attribute.system_save_data_id, attribute.type, attribute.rank, attribute.index);
|
||||
std::memset(out_buffer.data(), 0, out_buffer.size());
|
||||
LOG_DEBUG(Service_FS,
|
||||
"called, space_id={}, attribute.program_id={:016X}\n"
|
||||
"attribute.user_id={:016X}{:016X}, attribute.save_id={:016X}\n"
|
||||
"attribute.type={}, attribute.rank={}, attribute.index={}",
|
||||
space_id, attribute.program_id, attribute.user_id[1], attribute.user_id[0],
|
||||
attribute.system_save_data_id, attribute.type, attribute.rank, attribute.index);
|
||||
|
||||
if (out_buffer.size() < sizeof(FileSys::SaveDataExtraData)) {
|
||||
return FileSys::ResultInvalidSize;
|
||||
}
|
||||
|
||||
FileSys::SaveDataExtraData extra_data{};
|
||||
R_TRY(save_data_controller->ReadSaveDataExtraData(&extra_data, space_id, attribute));
|
||||
|
||||
std::memcpy(out_buffer.data(), &extra_data, sizeof(FileSys::SaveDataExtraData));
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result FSP_SRV::ReadSaveDataFileSystemExtraDataBySaveDataSpaceId(
|
||||
OutBuffer<BufferAttr_HipcMapAlias> out_buffer, FileSys::SaveDataSpaceId space_id,
|
||||
u64 save_data_id) {
|
||||
// Stub, backend needs an impl to read/write the SaveDataExtraData
|
||||
LOG_WARNING(Service_FS, "(STUBBED) called, space_id={}, save_data_id={:016X}", space_id,
|
||||
save_data_id);
|
||||
std::memset(out_buffer.data(), 0, out_buffer.size());
|
||||
LOG_DEBUG(Service_FS, "called, space_id={}, save_data_id={:016X}", space_id, save_data_id);
|
||||
|
||||
if (out_buffer.size() < sizeof(FileSys::SaveDataExtraData)) {
|
||||
return FileSys::ResultInvalidSize;
|
||||
}
|
||||
|
||||
// Construct attribute from save_data_id
|
||||
FileSys::SaveDataAttribute attribute{};
|
||||
attribute.system_save_data_id = save_data_id;
|
||||
attribute.type = FileSys::SaveDataType::System;
|
||||
|
||||
FileSys::SaveDataExtraData extra_data{};
|
||||
R_TRY(save_data_controller->ReadSaveDataExtraData(&extra_data, space_id, attribute));
|
||||
|
||||
std::memcpy(out_buffer.data(), &extra_data, sizeof(FileSys::SaveDataExtraData));
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
|
|
@ -419,9 +486,8 @@ Result FSP_SRV::OpenDataStorageByCurrentProcess(OutInterface<IStorage> out_inter
|
|||
if (!romfs) {
|
||||
auto current_romfs = romfs_controller->OpenRomFSCurrentProcess();
|
||||
if (!current_romfs) {
|
||||
// TODO (bunnei): Find the right error code to use here
|
||||
LOG_CRITICAL(Service_FS, "No file system interface available!");
|
||||
R_RETURN(ResultUnknown);
|
||||
R_RETURN(FileSys::ResultTargetNotFound);
|
||||
}
|
||||
|
||||
romfs = current_romfs;
|
||||
|
|
@ -447,11 +513,10 @@ Result FSP_SRV::OpenDataStorageByDataId(OutInterface<IStorage> out_interface,
|
|||
R_SUCCEED();
|
||||
}
|
||||
|
||||
// TODO(DarkLordZach): Find the right error code to use here
|
||||
LOG_ERROR(Service_FS,
|
||||
"Could not open data storage with title_id={:016X}, storage_id={:02X}", title_id,
|
||||
storage_id);
|
||||
R_RETURN(ResultUnknown);
|
||||
R_RETURN(FileSys::ResultTargetNotFound);
|
||||
}
|
||||
|
||||
const FileSys::PatchManager pm{title_id, fsc, content_provider};
|
||||
|
|
@ -481,9 +546,8 @@ Result FSP_SRV::OpenDataStorageWithProgramIndex(OutInterface<IStorage> out_inter
|
|||
program_id, program_index, FileSys::ContentRecordType::Program);
|
||||
|
||||
if (!patched_romfs) {
|
||||
// TODO: Find the right error code to use here
|
||||
LOG_ERROR(Service_FS, "Could not open storage with program_index={}", program_index);
|
||||
R_RETURN(ResultUnknown);
|
||||
R_RETURN(FileSys::ResultTargetNotFound);
|
||||
}
|
||||
|
||||
*out_interface = std::make_shared<IStorage>(system, std::move(patched_romfs));
|
||||
|
|
|
|||
|
|
@ -99,4 +99,22 @@ void SaveDataController::SetAutoCreate(bool state) {
|
|||
factory->SetAutoCreate(state);
|
||||
}
|
||||
|
||||
Result SaveDataController::ReadSaveDataExtraData(FileSys::SaveDataExtraData* out_extra_data,
|
||||
FileSys::SaveDataSpaceId space,
|
||||
const FileSys::SaveDataAttribute& attribute) {
|
||||
return factory->ReadSaveDataExtraData(out_extra_data, space, attribute);
|
||||
}
|
||||
|
||||
Result SaveDataController::WriteSaveDataExtraData(const FileSys::SaveDataExtraData& extra_data,
|
||||
FileSys::SaveDataSpaceId space,
|
||||
const FileSys::SaveDataAttribute& attribute) {
|
||||
return factory->WriteSaveDataExtraData(extra_data, space, attribute);
|
||||
}
|
||||
|
||||
Result SaveDataController::WriteSaveDataExtraDataWithMask(
|
||||
const FileSys::SaveDataExtraData& extra_data, const FileSys::SaveDataExtraData& mask,
|
||||
FileSys::SaveDataSpaceId space, const FileSys::SaveDataAttribute& attribute) {
|
||||
return factory->WriteSaveDataExtraDataWithMask(extra_data, mask, space, attribute);
|
||||
}
|
||||
|
||||
} // namespace Service::FileSystem
|
||||
|
|
|
|||
|
|
@ -25,6 +25,19 @@ public:
|
|||
FileSys::SaveDataSize ReadSaveDataSize(FileSys::SaveDataType type, u64 title_id, u128 user_id);
|
||||
void WriteSaveDataSize(FileSys::SaveDataType type, u64 title_id, u128 user_id,
|
||||
FileSys::SaveDataSize new_value);
|
||||
|
||||
// ExtraData operations
|
||||
Result ReadSaveDataExtraData(FileSys::SaveDataExtraData* out_extra_data,
|
||||
FileSys::SaveDataSpaceId space,
|
||||
const FileSys::SaveDataAttribute& attribute);
|
||||
Result WriteSaveDataExtraData(const FileSys::SaveDataExtraData& extra_data,
|
||||
FileSys::SaveDataSpaceId space,
|
||||
const FileSys::SaveDataAttribute& attribute);
|
||||
Result WriteSaveDataExtraDataWithMask(const FileSys::SaveDataExtraData& extra_data,
|
||||
const FileSys::SaveDataExtraData& mask,
|
||||
FileSys::SaveDataSpaceId space,
|
||||
const FileSys::SaveDataAttribute& attribute);
|
||||
|
||||
void SetAutoCreate(bool state);
|
||||
|
||||
private:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue