Compare commits

...
Sign in to create a new pull request.

1 commit

Author SHA1 Message Date
unknown
77ce42a2ed fs changes from citron, test 2025-10-22 22:35:27 +02:00
17 changed files with 1346 additions and 82 deletions

View file

@ -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

View 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

View 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

View file

@ -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

View 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

View 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

View file

@ -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();
}

View file

@ -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) {

View 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

View 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

View file

@ -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(&current_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*>(&current_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

View file

@ -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:

View file

@ -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;
}

View file

@ -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(

View file

@ -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));

View file

@ -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

View file

@ -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: