diff --git a/src/core/file_sys/savedata_factory.cpp b/src/core/file_sys/savedata_factory.cpp index 3ca2e0a6d..8b689e9fc 100644 --- a/src/core/file_sys/savedata_factory.cpp +++ b/src/core/file_sys/savedata_factory.cpp @@ -3,7 +3,9 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include +#include #include +#include #include "common/assert.h" #include "common/common_types.h" #include "common/logging/log.h" @@ -15,56 +17,70 @@ #include "core/file_sys/savedata_extra_data_accessor.h" #include "core/file_sys/savedata_factory.h" #include "core/file_sys/vfs/vfs.h" +#include "core/file_sys/vfs/vfs_real.h" +#include "core/hle/service/acc/profile_manager.h" namespace FileSys { namespace { +// Using a leaked raw pointer for the RealVfsFilesystem singleton. +// This prevents SIGSEGV during shutdown by ensuring the VFS bridge +// outlives all threads that might still be flushing save data. +RealVfsFilesystem* GetPersistentVfs() { + static RealVfsFilesystem* instance = new RealVfsFilesystem(); + return instance; +} + bool ShouldSaveDataBeAutomaticallyCreated(SaveDataSpaceId space, const SaveDataAttribute& attr) { return attr.type == SaveDataType::Cache || attr.type == SaveDataType::Temporary || - (space == SaveDataSpaceId::User && ///< Normal Save Data -- Current Title & User + (space == SaveDataSpaceId::User && (attr.type == SaveDataType::Account || attr.type == SaveDataType::Device) && attr.program_id == 0 && attr.system_save_data_id == 0); } std::string GetFutureSaveDataPath(SaveDataSpaceId space_id, SaveDataType type, u64 title_id, u128 user_id) { - // Only detect nand user saves. - const auto space_id_path = [space_id]() -> std::string_view { - switch (space_id) { - case SaveDataSpaceId::User: - return "/user/save"; - default: - return ""; - } - }(); - - if (space_id_path.empty()) { + if (space_id != SaveDataSpaceId::User) { return ""; } Common::UUID uuid; std::memcpy(uuid.uuid.data(), user_id.data(), sizeof(Common::UUID)); - // Only detect account/device saves from the future location. switch (type) { case SaveDataType::Account: - return fmt::format("{}/account/{}/{:016X}/0", space_id_path, uuid.RawString(), title_id); + return fmt::format("/user/save/account/{}/{:016X}/0", uuid.RawString(), title_id); case SaveDataType::Device: - return fmt::format("{}/device/{:016X}/0", space_id_path, title_id); + return fmt::format("/user/save/device/{:016X}/0", title_id); default: return ""; } } +void BufferedVfsCopy(VirtualFile source, VirtualFile dest) { + if (!source || !dest) return; + try { + std::vector buffer(0x100000); // 1MB buffer + dest->Resize(0); + size_t offset = 0; + while (offset < source->GetSize()) { + const size_t to_read = std::min(buffer.size(), source->GetSize() - offset); + source->Read(buffer.data(), to_read, offset); + dest->Write(buffer.data(), to_read, offset); + offset += to_read; + } + } catch (...) { + LOG_ERROR(Service_FS, "Critical error during VFS mirror operation."); + } +} + } // Anonymous namespace SaveDataFactory::SaveDataFactory(Core::System& system_, ProgramId program_id_, VirtualDir save_directory_, VirtualDir backup_directory_) : system{system_}, program_id{program_id_}, dir{std::move(save_directory_)}, backup_dir{std::move(backup_directory_)} { - // Delete all temporary storages - // On hardware, it is expected that temporary storage be empty at first use. dir->DeleteSubdirectoryRecursive("temp"); } @@ -79,19 +95,14 @@ VirtualDir SaveDataFactory::Create(SaveDataSpaceId space, const SaveDataAttribut return nullptr; } - // 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 + if (accessor.Initialize(true) == ResultSuccess) { 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.available_size = 0; initial_data.journal_size = 0; initial_data.commit_id = 1; @@ -122,6 +133,8 @@ VirtualDir SaveDataFactory::GetSaveDataSpaceDirectory(SaveDataSpaceId space) con std::string SaveDataFactory::GetSaveDataSpaceIdPath(SaveDataSpaceId space) { switch (space) { case SaveDataSpaceId::System: + case SaveDataSpaceId::ProperSystem: + case SaveDataSpaceId::SafeMode: return "/system/"; case SaveDataSpaceId::User: return "/user/"; @@ -130,54 +143,37 @@ std::string SaveDataFactory::GetSaveDataSpaceIdPath(SaveDataSpaceId space) { case SaveDataSpaceId::SdSystem: case SaveDataSpaceId::SdUser: return "/sd/"; - case SaveDataSpaceId::ProperSystem: - return "/system/"; - case SaveDataSpaceId::SafeMode: - return "/system/"; default: - ASSERT_MSG(false, "Unrecognized SaveDataSpaceId: {:02X}", static_cast(space)); - return "/unrecognized/"; ///< To prevent corruption when ignoring asserts. + return "/unrecognized/"; } } std::string SaveDataFactory::GetFullPath(ProgramId program_id, VirtualDir dir, SaveDataSpaceId space, SaveDataType type, u64 title_id, u128 user_id, u64 save_id) { - // According to switchbrew, if a save is of type SaveData and the title id field is 0, it should - // be interpreted as the title id of the current process. - if (type == SaveDataType::Account || type == SaveDataType::Device) { - if (title_id == 0) { - title_id = program_id; - } + if ((type == SaveDataType::Account || type == SaveDataType::Device) && title_id == 0) { + title_id = program_id; } - // For compat with a future impl. - if (std::string future_path = - GetFutureSaveDataPath(space, type, title_id & ~(0xFFULL), user_id); + if (std::string future_path = GetFutureSaveDataPath(space, type, title_id & ~(0xFFULL), user_id); !future_path.empty()) { - // Check if this location exists, and prefer it over the old. - if (const auto future_dir = dir->GetDirectoryRelative(future_path); future_dir != nullptr) { - LOG_INFO(Service_FS, "Using save at new location: {}", future_path); + if (dir->GetDirectoryRelative(future_path) != nullptr) { return future_path; } } std::string out = GetSaveDataSpaceIdPath(space); - switch (type) { case SaveDataType::System: return fmt::format("{}save/{:016X}/{:016X}{:016X}", out, save_id, user_id[1], user_id[0]); case SaveDataType::Account: case SaveDataType::Device: - return fmt::format("{}save/{:016X}/{:016X}{:016X}/{:016X}", out, 0, user_id[1], user_id[0], - title_id); + return fmt::format("{}save/{:016X}/{:016X}{:016X}/{:016X}", out, 0, user_id[1], user_id[0], title_id); case SaveDataType::Temporary: - return fmt::format("{}{:016X}/{:016X}{:016X}/{:016X}", out, 0, user_id[1], user_id[0], - title_id); + return fmt::format("{}{:016X}/{:016X}{:016X}/{:016X}", out, 0, user_id[1], user_id[0], title_id); case SaveDataType::Cache: return fmt::format("{}save/cache/{:016X}", out, title_id); default: - ASSERT_MSG(false, "Unrecognized SaveDataType: {:02X}", static_cast(type)); return fmt::format("{}save/unknown_{:X}/{:016X}", out, static_cast(type), title_id); } } @@ -191,36 +187,21 @@ std::string SaveDataFactory::GetUserGameSaveDataRoot(u128 user_id, bool future) return fmt::format("/user/save/{:016X}/{:016X}{:016X}", 0, user_id[1], user_id[0]); } -SaveDataSize SaveDataFactory::ReadSaveDataSize(SaveDataType type, u64 title_id, - u128 user_id) const { - const auto path = - GetFullPath(program_id, dir, SaveDataSpaceId::User, type, title_id, user_id, 0); +SaveDataSize SaveDataFactory::ReadSaveDataSize(SaveDataType type, u64 title_id, u128 user_id) const { + const auto path = GetFullPath(program_id, dir, SaveDataSpaceId::User, type, title_id, user_id, 0); const auto relative_dir = GetOrCreateDirectoryRelative(dir, path); - const auto size_file = relative_dir->GetFile(GetSaveDataSizeFileName()); - if (size_file == nullptr || size_file->GetSize() < sizeof(SaveDataSize)) { - return {0, 0}; - } - + if (size_file == nullptr || size_file->GetSize() < sizeof(SaveDataSize)) return {0, 0}; SaveDataSize out; - if (size_file->ReadObject(&out) != sizeof(SaveDataSize)) { - return {0, 0}; - } - + if (size_file->ReadObject(&out) != sizeof(SaveDataSize)) return {0, 0}; return out; } -void SaveDataFactory::WriteSaveDataSize(SaveDataType type, u64 title_id, u128 user_id, - SaveDataSize new_value) const { - const auto path = - GetFullPath(program_id, dir, SaveDataSpaceId::User, type, title_id, user_id, 0); +void SaveDataFactory::WriteSaveDataSize(SaveDataType type, u64 title_id, u128 user_id, SaveDataSize new_value) const { + const auto path = GetFullPath(program_id, dir, SaveDataSpaceId::User, type, title_id, user_id, 0); const auto relative_dir = GetOrCreateDirectoryRelative(dir, path); - const auto size_file = relative_dir->CreateFile(GetSaveDataSizeFileName()); - if (size_file == nullptr) { - return; - } - + if (size_file == nullptr) return; size_file->Resize(sizeof(SaveDataSize)); size_file->WriteObject(new_value); } @@ -229,137 +210,173 @@ 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); - +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; - } - + 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 - *out_extra_data = {}; // Or: *out_extra_data = SaveDataExtraData{}; + if (accessor.Initialize(false) != ResultSuccess) { + *out_extra_data = {}; 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); - +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; - } - + 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; + return accessor.CommitExtraData(); } -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); - +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; - } - + 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]; + if (mask_bytes[i] != 0) current_data_bytes[i] = extra_data_bytes[i]; + } + R_TRY(accessor.WriteExtraData(current_data)); + return accessor.CommitExtraData(); +} + +// --- MIRRORING TOOLS --- + +VirtualDir SaveDataFactory::GetMirrorDirectory(u64 title_id) const { + auto it = Settings::values.mirrored_save_paths.find(title_id); + if (it == Settings::values.mirrored_save_paths.end() || it->second.empty()) return nullptr; + + std::filesystem::path host_path(it->second); + if (!std::filesystem::exists(host_path)) return nullptr; + + // Get the persistent VFS bridge + auto* vfs = GetPersistentVfs(); + return vfs->OpenDirectory(it->second, OpenMode::ReadWrite); +} + +void SaveDataFactory::SmartSyncFromSource(VirtualDir source, VirtualDir dest) const { + // Citron: Shutdown and null safety + if (!source || !dest || system.IsShuttingDown()) { + return; + } + + // Sync files from Source to Destination + for (const auto& s_file : source->GetFiles()) { + if (!s_file) continue; + std::string name = s_file->GetName(); + + // Skip metadata and lock files + if (name == ".lock" || name == ".citron_save_size" || name.find("mirror_backup") != std::string::npos) { + continue; + } + + auto d_file = dest->CreateFile(name); + if (d_file) { + BufferedVfsCopy(s_file, d_file); } } - // Write back the masked data - R_TRY(accessor.WriteExtraData(current_data)); + // Recurse into subdirectories + for (const auto& s_subdir : source->GetSubdirectories()) { + if (!s_subdir) continue; - // Commit the changes - R_TRY(accessor.CommitExtraData()); + // Prevent recursion into title-id-named folders to avoid infinite loops + if (s_subdir->GetName().find("0100") != std::string::npos) continue; - return ResultSuccess; + auto d_subdir = dest->GetDirectoryRelative(s_subdir->GetName()); + if (!d_subdir) { + d_subdir = dest->CreateDirectoryRelative(s_subdir->GetName()); + } + + if (d_subdir) { + SmartSyncFromSource(s_subdir, d_subdir); + } + } +} + +void SaveDataFactory::PerformStartupMirrorSync() const { + // If settings are empty or system is shutting down/uninitialized + if (Settings::values.mirrored_save_paths.empty() || system.IsShuttingDown()) { + return; + } + + // Ensure our NAND directory is actually valid + if (!dir) { + LOG_ERROR(Service_FS, "Mirroring: Startup Sync aborted. NAND directory is null."); + return; + } + + // Attempt to locate the save root with null checks at every step + VirtualDir user_save_root = nullptr; + try { + user_save_root = dir->GetDirectoryRelative("user/save/0000000000000000"); + if (!user_save_root) { + user_save_root = dir->GetDirectoryRelative("user/save"); + } + } catch (...) { + LOG_ERROR(Service_FS, "Mirroring: Critical failure accessing VFS. Filesystem may be stale."); + return; + } + + if (!user_save_root) { + LOG_WARNING(Service_FS, "Mirroring: Could not find user save root in NAND."); + return; + } + + LOG_INFO(Service_FS, "Mirroring: Startup Sync initiated."); + + for (const auto& [title_id, host_path] : Settings::values.mirrored_save_paths) { + if (host_path.empty()) continue; + + auto mirror_source = GetMirrorDirectory(title_id); + if (!mirror_source) continue; + + std::string title_id_str = fmt::format("{:016X}", title_id); + + for (const auto& profile_dir : user_save_root->GetSubdirectories()) { + if (!profile_dir) continue; + + auto nand_dest = profile_dir->GetDirectoryRelative(title_id_str); + + if (!nand_dest) { + for (const auto& sub : profile_dir->GetSubdirectories()) { + if (!sub) continue; + nand_dest = sub->GetDirectoryRelative(title_id_str); + if (nand_dest) break; + } + } + + if (nand_dest) { + LOG_INFO(Service_FS, "Mirroring: Pulling external data for {}", title_id_str); + SmartSyncFromSource(mirror_source, nand_dest); + } + } + } } void SaveDataFactory::DoNandBackup(SaveDataSpaceId space, const SaveDataAttribute& meta, VirtualDir custom_dir) const { - LOG_INFO(Common, "Dual-Save: Backup process initiated for Program ID: {:016X}", program_id); + u64 title_id = (meta.program_id != 0 ? meta.program_id : static_cast(program_id)); + if (Settings::values.mirrored_save_paths.count(title_id)) return; - if (!Settings::values.backup_saves_to_nand.GetValue()) { - LOG_INFO(Common, "Dual-Save: Backup skipped (Setting is OFF)"); - return; - } - - if (backup_dir == nullptr) { - LOG_ERROR(Common, "Dual-Save: Backup failed (NAND directory is NULL)"); - return; - } - - if (custom_dir == nullptr) { - LOG_ERROR(Common, "Dual-Save: Backup failed (Source Custom directory is NULL)"); - return; - } - - const auto nand_path = GetFullPath(program_id, backup_dir, space, meta.type, meta.program_id, - meta.user_id, meta.system_save_data_id); + if (!Settings::values.backup_saves_to_nand.GetValue() || backup_dir == nullptr || custom_dir == nullptr) return; + const auto nand_path = GetFullPath(program_id, backup_dir, space, meta.type, meta.program_id, meta.user_id, meta.system_save_data_id); auto nand_out = backup_dir->CreateDirectoryRelative(nand_path); - if (nand_out != nullptr) { - LOG_INFO(Common, "Dual-Save: Mirroring files to NAND: {}", nand_path); - // Clear the old backup + if (nand_out) { nand_out->CleanSubdirectoryRecursive("."); - - // Perform the copy VfsRawCopyD(custom_dir, nand_out); - - LOG_INFO(Common, "Dual-Save: NAND Backup successful."); - } else { - LOG_ERROR(Common, "Dual-Save: Could not create/access NAND backup path!"); } }