diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 62dab070e3..6f8b4d8392 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -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 diff --git a/src/core/file_sys/directory_save_data_filesystem.cpp b/src/core/file_sys/directory_save_data_filesystem.cpp new file mode 100644 index 0000000000..402025d561 --- /dev/null +++ b/src/core/file_sys/directory_save_data_filesystem.cpp @@ -0,0 +1,231 @@ +// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#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 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 diff --git a/src/core/file_sys/directory_save_data_filesystem.h b/src/core/file_sys/directory_save_data_filesystem.h new file mode 100644 index 0000000000..eedcddc6e2 --- /dev/null +++ b/src/core/file_sys/directory_save_data_filesystem.h @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#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 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 diff --git a/src/core/file_sys/errors.h b/src/core/file_sys/errors.h index b22767bf5b..d7330f3571 100644 --- a/src/core/file_sys/errors.h +++ b/src/core/file_sys/errors.h @@ -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 diff --git a/src/core/file_sys/fs_path_normalizer.cpp b/src/core/file_sys/fs_path_normalizer.cpp new file mode 100644 index 0000000000..65cfc42367 --- /dev/null +++ b/src/core/file_sys/fs_path_normalizer.cpp @@ -0,0 +1,224 @@ +// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#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 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 diff --git a/src/core/file_sys/fs_path_normalizer.h b/src/core/file_sys/fs_path_normalizer.h new file mode 100644 index 0000000000..18b91fb38b --- /dev/null +++ b/src/core/file_sys/fs_path_normalizer.h @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#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 diff --git a/src/core/file_sys/fsa/fs_i_file.h b/src/core/file_sys/fsa/fs_i_file.h index 8b70185522..7974ce3219 100644 --- a/src/core/file_sys/fsa/fs_i_file.h +++ b/src/core/file_sys/fsa/fs_i_file.h @@ -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((std::min)(file_size - offset, static_cast(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(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(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(); } diff --git a/src/core/file_sys/fsa/fs_i_filesystem.h b/src/core/file_sys/fsa/fs_i_filesystem.h index 8172190f49..1679a7c9af 100644 --- a/src/core/file_sys/fsa/fs_i_filesystem.h +++ b/src/core/file_sys/fsa/fs_i_filesystem.h @@ -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) { diff --git a/src/core/file_sys/savedata_extra_data_accessor.cpp b/src/core/file_sys/savedata_extra_data_accessor.cpp new file mode 100644 index 0000000000..ead1372ddd --- /dev/null +++ b/src/core/file_sys/savedata_extra_data_accessor.cpp @@ -0,0 +1,286 @@ +// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#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 diff --git a/src/core/file_sys/savedata_extra_data_accessor.h b/src/core/file_sys/savedata_extra_data_accessor.h new file mode 100644 index 0000000000..aa9175286d --- /dev/null +++ b/src/core/file_sys/savedata_extra_data_accessor.h @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#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 diff --git a/src/core/file_sys/savedata_factory.cpp b/src/core/file_sys/savedata_factory.cpp index dda8d526d3..89e9651e59 100644 --- a/src/core/file_sys/savedata_factory.cpp +++ b/src/core/file_sys/savedata_factory.cpp @@ -4,11 +4,14 @@ // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include #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(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(&extra_data); + const u8* mask_bytes = reinterpret_cast(&mask); + u8* current_data_bytes = reinterpret_cast(¤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 diff --git a/src/core/file_sys/savedata_factory.h b/src/core/file_sys/savedata_factory.h index 15dd4ec7de..9d3e96f3bf 100644 --- a/src/core/file_sys/savedata_factory.h +++ b/src/core/file_sys/savedata_factory.h @@ -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: diff --git a/src/core/hle/service/filesystem/filesystem.cpp b/src/core/hle/service/filesystem/filesystem.cpp index 9d7de4242e..1ff5a427fa 100644 --- a/src/core/hle/service/filesystem/filesystem.cpp +++ b/src/core/hle/service/filesystem/filesystem.cpp @@ -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; } diff --git a/src/core/hle/service/filesystem/fsp/fs_i_filesystem.cpp b/src/core/hle/service/filesystem/fsp/fs_i_filesystem.cpp index 352b8f77b0..012041333f 100644 --- a/src/core/hle/service/filesystem/fsp/fs_i_filesystem.cpp +++ b/src/core/hle/service/filesystem/fsp/fs_i_filesystem.cpp @@ -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( diff --git a/src/core/hle/service/filesystem/fsp/fsp_srv.cpp b/src/core/hle/service/filesystem/fsp/fsp_srv.cpp index 98634c6f60..07302d2f93 100644 --- a/src/core/hle/service/filesystem/fsp/fsp_srv.cpp +++ b/src/core/hle/service/filesystem/fsp/fsp_srv.cpp @@ -339,70 +339,137 @@ Result FSP_SRV::FindSaveDataWithFilter(Out out_count, Result FSP_SRV::WriteSaveDataFileSystemExtraData(InBuffer 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 buffer, InBuffer 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 mask_buffer, OutBuffer 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(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(&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 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 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 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 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 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 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(system, std::move(patched_romfs)); diff --git a/src/core/hle/service/filesystem/save_data_controller.cpp b/src/core/hle/service/filesystem/save_data_controller.cpp index 8720681d17..9b1f0b9414 100644 --- a/src/core/hle/service/filesystem/save_data_controller.cpp +++ b/src/core/hle/service/filesystem/save_data_controller.cpp @@ -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 diff --git a/src/core/hle/service/filesystem/save_data_controller.h b/src/core/hle/service/filesystem/save_data_controller.h index dc9d713dfb..2117f9adb4 100644 --- a/src/core/hle/service/filesystem/save_data_controller.h +++ b/src/core/hle/service/filesystem/save_data_controller.h @@ -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: