/* * Copyright (c) Atmosphère-NX * * This program is free software; you can redistribute it and/or modify it * under the terms and conditions of the GNU General Public License, * version 2, as published by the Free Software Foundation. * * This program is distributed in the hope it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include "sprofile_srv_profile_manager.hpp" #include "sprofile_srv_fs_utils.hpp" namespace ams::sprofile::srv { namespace { constexpr const char PrimaryDirectoryName[] = "primary"; constexpr const char TemporaryDirectoryName[] = "temp"; Result CreateSaveData(const ProfileManager::SaveDataInfo &save_data_info) { R_TRY_CATCH(fs::CreateSystemSaveData(save_data_info.id, save_data_info.size, save_data_info.journal_size, save_data_info.flags)) { R_CATCH(fs::ResultPathAlreadyExists) { /* Nintendo accepts already-existing savedata here. */ } } R_END_TRY_CATCH; R_SUCCEED(); } void SafePrint(char *dst, size_t dst_size, const char *fmt, ...) __attribute__((format(printf, 3, 4))); void SafePrint(char *dst, size_t dst_size, const char *fmt, ...) { std::va_list vl; va_start(vl, fmt); const size_t len = util::TVSNPrintf(dst, dst_size, fmt, vl); va_end(vl); AMS_ABORT_UNLESS(len < dst_size); } void CreateMetadataPathImpl(char *dst, size_t dst_size, const char *mount, const char *dir) { SafePrint(dst, dst_size, "%s:/%s/metadata", mount, dir); } void CreatePrimaryMetadataPath(char *dst, size_t dst_size, const char *mount) { CreateMetadataPathImpl(dst, dst_size, mount, PrimaryDirectoryName); } void CreateTemporaryMetadataPath(char *dst, size_t dst_size, const char *mount) { CreateMetadataPathImpl(dst, dst_size, mount, TemporaryDirectoryName); } void CreateProfilePathImpl(char *dst, size_t dst_size, const char *mount, const char *dir, const Identifier &id) { SafePrint(dst, dst_size, "%s:/%s/profiles/%02x%02x%02x%02x%02x%02x%02x", mount, dir, id.data[0], id.data[1], id.data[2], id.data[3], id.data[4], id.data[5], id.data[6]); } void CreatePrimaryProfilePath(char *dst, size_t dst_size, const char *mount, const Identifier &id) { CreateProfilePathImpl(dst, dst_size, mount, PrimaryDirectoryName, id); } void CreateTemporaryProfilePath(char *dst, size_t dst_size, const char *mount, const Identifier &id) { CreateProfilePathImpl(dst, dst_size, mount, TemporaryDirectoryName, id); } void CreateDirectoryPathImpl(char *dst, size_t dst_size, const char *mount, const char *dir) { SafePrint(dst, dst_size, "%s:/%s", mount, dir); } void CreatePrimaryDirectoryPath(char *dst, size_t dst_size, const char *mount) { CreateDirectoryPathImpl(dst, dst_size, mount, PrimaryDirectoryName); } void CreateTemporaryDirectoryPath(char *dst, size_t dst_size, const char *mount) { CreateDirectoryPathImpl(dst, dst_size, mount, TemporaryDirectoryName); } void CreateProfileDirectoryPathImpl(char *dst, size_t dst_size, const char *mount, const char *dir) { SafePrint(dst, dst_size, "%s:/%s/profiles", mount, dir); } void CreatePrimaryProfileDirectoryPath(char *dst, size_t dst_size, const char *mount) { CreateProfileDirectoryPathImpl(dst, dst_size, mount, PrimaryDirectoryName); } void CreateTemporaryProfileDirectoryPath(char *dst, size_t dst_size, const char *mount) { CreateProfileDirectoryPathImpl(dst, dst_size, mount, TemporaryDirectoryName); } } ProfileManager::ProfileManager(const SaveDataInfo &save_data_info) : m_save_data_info(save_data_info), m_save_file_mounted(false), m_profile_importer(util::nullopt), m_profile_metadata(util::nullopt), m_service_profile(util::nullopt), m_update_observer_manager() { /* ... */ } void ProfileManager::InitializeSaveData() { /* Acquire locks. */ std::scoped_lock lk1(m_general_mutex); std::scoped_lock lk2(m_fs_mutex); /* Ensure the savedata exists. */ if (R_SUCCEEDED(CreateSaveData(m_save_data_info))) { m_save_file_mounted = R_SUCCEEDED(fs::MountSystemSaveData(m_save_data_info.mount_name, m_save_data_info.id)); } } Result ProfileManager::ResetSaveData() { /* Acquire locks. */ std::scoped_lock lk1(m_service_profile_mutex); std::scoped_lock lk2(m_profile_metadata_mutex); std::scoped_lock lk3(m_general_mutex); std::scoped_lock lk4(m_fs_mutex); /* Unmount save file. */ fs::Unmount(m_save_data_info.mount_name); m_save_file_mounted = false; /* Delete save file. */ R_TRY(fs::DeleteSystemSaveData(fs::SaveDataSpaceId::System, m_save_data_info.id, fs::InvalidUserId)); /* Unload profile. */ m_profile_metadata = util::nullopt; m_service_profile = util::nullopt; /* Create the save data. */ R_TRY(CreateSaveData(m_save_data_info)); /* Try to mount the save file. */ const auto result = fs::MountSystemSaveData(m_save_data_info.mount_name, m_save_data_info.id); m_save_file_mounted = R_SUCCEEDED(result); R_RETURN(result); } Result ProfileManager::OpenProfileImporter() { /* Acquire locks. */ std::scoped_lock lk1(m_profile_metadata_mutex); std::scoped_lock lk2(m_profile_importer_mutex); std::scoped_lock lk3(m_general_mutex); /* Check that we don't already have an importer. */ R_UNLESS(!m_profile_importer.has_value(), sprofile::ResultInvalidState()); /* Try to load profile metadata. NOTE: result is not checked, it is okay if this fails. */ this->LoadPrimaryMetadataImpl(); /* Create importer. */ m_profile_importer.emplace(m_profile_metadata); R_SUCCEED(); } void ProfileManager::CloseProfileImporterImpl() { /* Check pre-conditions. */ AMS_ASSERT(m_profile_importer_mutex.IsLockedByCurrentThread()); AMS_ASSERT(m_general_mutex.IsLockedByCurrentThread()); AMS_ASSERT(m_fs_mutex.IsLockedByCurrentThread()); if (m_profile_importer.has_value()) { /* Unmount save file. */ fs::Unmount(m_save_data_info.mount_name); m_save_file_mounted = false; /* Re-mount save file. */ R_ABORT_UNLESS(fs::MountSystemSaveData(m_save_data_info.mount_name, m_save_data_info.id)); m_save_file_mounted = true; /* Reset our importer. */ m_profile_importer = util::nullopt; } } void ProfileManager::CloseProfileImporter() { /* Acquire locks. */ std::scoped_lock lk1(m_profile_importer_mutex); std::scoped_lock lk2(m_general_mutex); std::scoped_lock lk3(m_fs_mutex); /* Close our importer. */ this->CloseProfileImporterImpl(); } Result ProfileManager::ImportProfile(const sprofile::srv::ProfileDataForImportData &import) { /* Acquire locks. */ std::scoped_lock lk1(m_profile_importer_mutex); std::scoped_lock lk2(m_fs_mutex); /* Check that we have an importer. */ R_UNLESS(m_profile_importer.has_value(), sprofile::ResultInvalidState()); /* Check that the metadata we're importing is a valid version. */ R_UNLESS(IsValidProfileFormatVersion(import.header.version), sprofile::ResultInvalidDataVersion()); /* Check that the metadata we're importing has a valid hash. */ { crypto::Md5Generator md5; md5.Initialize(); md5.Update(std::addressof(import.header), sizeof(import.header)); md5.Update(std::addressof(import.data), sizeof(import.data) - sizeof(import.data.entries[0]) * (util::size(import.data.entries) - std::min(import.data.num_entries, util::size(import.data.entries)))); u8 hash[crypto::Md5Generator::HashSize]; md5.GetHash(hash, sizeof(hash)); R_UNLESS(crypto::IsSameBytes(hash, import.hash, sizeof(hash)), sprofile::ResultInvalidDataHash()); } /* Succeed if we already have the profile. */ R_SUCCEED_IF(m_profile_importer->HasProfile(import.header.identifier_0, import.header.identifier_1)); /* Check that we're importing the profile. */ R_UNLESS(m_profile_importer->CanImportProfile(import.header.identifier_0), sprofile::ResultInvalidState()); /* Create temporary directories. */ R_TRY(this->EnsureTemporaryDirectories()); /* Create profile. */ char path[0x30]; CreateTemporaryProfilePath(path, sizeof(path), m_save_data_info.mount_name, import.header.identifier_0); R_TRY(WriteFile(path, std::addressof(import.data), sizeof(import.data))); /* Set profile imported. */ m_profile_importer->OnImportProfile(import.header.identifier_0); R_SUCCEED(); } Result ProfileManager::Commit() { /* Acquire locks. */ std::scoped_lock lk1(m_service_profile_mutex); std::scoped_lock lk2(m_profile_metadata_mutex); std::scoped_lock lk3(m_profile_importer_mutex); std::scoped_lock lk4(m_general_mutex); std::scoped_lock lk5(m_fs_mutex); /* Check that we have an importer. */ R_UNLESS(m_profile_importer.has_value(), sprofile::ResultInvalidState()); /* Commit, and if we fail remount our save. */ { /* If we fail, close our importer. */ ON_RESULT_FAILURE { this->CloseProfileImporterImpl(); }; /* Check that we can commit the importer. */ R_UNLESS(m_profile_importer->CanCommit(), sprofile::ResultInvalidState()); /* Commit newly imported profiles. */ R_TRY(this->CommitImportedProfiles()); /* Cleanup orphaned profiles. */ R_TRY(this->CleanupOrphanedProfiles()); /* Commit the save file. */ R_TRY(fs::CommitSaveData(m_save_data_info.mount_name)); } /* NOTE: Here nintendo generates an "sprofile_update_profile" sreport with the new and old revision keys. */ /* Handle tasks for when we've committed (including notifying update observers). */ this->OnCommitted(); R_SUCCEED(); } Result ProfileManager::ImportMetadata(const sprofile::srv::ProfileMetadataForImportMetadata &import) { /* Acquire locks. */ std::scoped_lock lk1(m_profile_importer_mutex); std::scoped_lock lk2(m_fs_mutex); /* Check that we can import metadata. */ R_UNLESS(m_profile_importer.has_value(), sprofile::ResultInvalidState()); R_UNLESS(m_profile_importer->CanImportMetadata(), sprofile::ResultInvalidState()); /* Check that the metadata we're importing is a valid version. */ R_UNLESS(IsValidProfileFormatVersion(import.header.version), sprofile::ResultInvalidMetadataVersion()); /* Check that the metadata we're importing has a valid hash. */ { crypto::Md5Generator md5; md5.Initialize(); md5.Update(std::addressof(import.header), sizeof(import.header)); md5.Update(std::addressof(import.metadata), sizeof(import.metadata)); md5.Update(std::addressof(import.profile_urls), sizeof(import.profile_urls[0]) * std::min(import.metadata.num_entries, util::size(import.metadata.entries))); u8 hash[crypto::Md5Generator::HashSize]; md5.GetHash(hash, sizeof(hash)); R_UNLESS(crypto::IsSameBytes(hash, import.hash, sizeof(hash)), sprofile::ResultInvalidMetadataHash()); } /* Create temporary directories. */ R_TRY(this->EnsureTemporaryDirectories()); /* Create metadata. */ char path[0x30]; CreateTemporaryMetadataPath(path, sizeof(path), m_save_data_info.mount_name); R_TRY(WriteFile(path, std::addressof(import.metadata), sizeof(import.metadata))); /* Import the metadata. */ m_profile_importer->ImportMetadata(import.metadata); R_SUCCEED(); } Result ProfileManager::LoadPrimaryMetadataImpl() { /* Check pre-conditions. */ AMS_ASSERT(m_profile_metadata_mutex.IsLockedByCurrentThread()); AMS_ASSERT(m_general_mutex.IsLockedByCurrentThread()); /* If we don't have metadata, load it. */ if (!m_profile_metadata.has_value()) { /* Emplace our metadata. */ m_profile_metadata.emplace(); ON_RESULT_FAILURE { m_profile_metadata = util::nullopt; }; /* Read profile metadata. */ char path[0x30]; CreatePrimaryMetadataPath(path, sizeof(path), m_save_data_info.mount_name); R_TRY(ReadFile(path, std::addressof(*m_profile_metadata), sizeof(*m_profile_metadata), 0)); } /* We now have loaded metadata. */ R_SUCCEED(); } Result ProfileManager::LoadPrimaryMetadata(ProfileMetadata *out) { /* Acquire locks. */ std::scoped_lock lk1(m_profile_metadata_mutex); std::scoped_lock lk2(m_general_mutex); /* Load our metadata. */ R_TRY(this->LoadPrimaryMetadataImpl()); /* Set the output. */ *out = *m_profile_metadata; R_SUCCEED(); } Result ProfileManager::LoadProfile(Identifier profile) { /* Check if we already have the profile. */ if (m_service_profile.has_value()) { R_SUCCEED_IF(m_service_profile->name == profile); } /* If we fail past this point, we want to have no profile. */ auto prof_guard = SCOPE_GUARD { m_service_profile = util::nullopt; }; /* Create profile path. */ char path[0x30]; CreatePrimaryProfilePath(path, sizeof(path), m_save_data_info.mount_name, profile); /* Load the profile. */ m_service_profile = {}; R_TRY(ReadFile(path, std::addressof(m_service_profile->data), sizeof(m_service_profile->data), 0)); /* We succeeded. */ prof_guard.Cancel(); R_SUCCEED(); } Result ProfileManager::GetDataEntry(ProfileDataEntry *out, Identifier profile, Identifier key) { /* Acquire locks. */ std::scoped_lock lk1(m_service_profile_mutex); std::scoped_lock lk2(m_general_mutex); /* Load the desired profile. */ if (R_SUCCEEDED(this->LoadProfile(profile))) { /* Find the specified key. */ for (auto i = 0u; i < std::min(m_service_profile->data.num_entries, util::size(m_service_profile->data.entries)); ++i) { if (m_service_profile->data.entries[i].key == key) { *out = m_service_profile->data.entries[i]; R_SUCCEED(); } } } R_THROW(sprofile::ResultKeyNotFound()); } Result ProfileManager::GetSigned64(s64 *out, Identifier profile, Identifier key) { /* Get the data entry. */ ProfileDataEntry entry; R_TRY(this->GetDataEntry(std::addressof(entry), profile, key)); /* Check the type. */ R_UNLESS(entry.type == ValueType_S64, sprofile::ResultInvalidDataType()); /* Set the output value. */ *out = entry.value_s64; R_SUCCEED(); } Result ProfileManager::GetUnsigned64(u64 *out, Identifier profile, Identifier key) { /* Get the data entry. */ ProfileDataEntry entry; R_TRY(this->GetDataEntry(std::addressof(entry), profile, key)); /* Check the type. */ R_UNLESS(entry.type == ValueType_U64, sprofile::ResultInvalidDataType()); /* Set the output value. */ *out = entry.value_u64; R_SUCCEED(); } Result ProfileManager::GetSigned32(s32 *out, Identifier profile, Identifier key) { /* Get the data entry. */ ProfileDataEntry entry; R_TRY(this->GetDataEntry(std::addressof(entry), profile, key)); /* Check the type. */ R_UNLESS(entry.type == ValueType_S32, sprofile::ResultInvalidDataType()); /* Set the output value. */ *out = entry.value_s32; R_SUCCEED(); } Result ProfileManager::GetUnsigned32(u32 *out, Identifier profile, Identifier key) { /* Get the data entry. */ ProfileDataEntry entry; R_TRY(this->GetDataEntry(std::addressof(entry), profile, key)); /* Check the type. */ R_UNLESS(entry.type == ValueType_U32, sprofile::ResultInvalidDataType()); /* Set the output value. */ *out = entry.value_u32; R_SUCCEED(); } Result ProfileManager::GetByte(u8 *out, Identifier profile, Identifier key) { /* Get the data entry. */ ProfileDataEntry entry; R_TRY(this->GetDataEntry(std::addressof(entry), profile, key)); /* Check the type. */ R_UNLESS(entry.type == ValueType_Byte, sprofile::ResultInvalidDataType()); /* Set the output value. */ *out = entry.value_u8; R_SUCCEED(); } Result ProfileManager::GetRaw(u8 *out_type, u64 *out_value, Identifier profile, Identifier key) { /* Get the data entry. */ ProfileDataEntry entry; R_TRY(this->GetDataEntry(std::addressof(entry), profile, key)); /* Set the output type and value. */ *out_type = entry.type; *out_value = entry.value_u64; R_SUCCEED(); } Result ProfileManager::CommitImportedProfiles() { /* Ensure primary directories. */ R_TRY(this->EnsurePrimaryDirectories()); /* Declare re-usable paths. */ char tmp_path[0x30]; char pri_path[0x30]; /* Move the metadata. */ { CreateTemporaryMetadataPath(tmp_path, sizeof(tmp_path), m_save_data_info.mount_name); CreatePrimaryMetadataPath(pri_path, sizeof(pri_path), m_save_data_info.mount_name); R_TRY(MoveFile(tmp_path, pri_path)); } /* Move all newly imported profiles. */ for (auto i = 0; i < m_profile_importer->GetImportingCount(); ++i) { const auto &profile = m_profile_importer->GetImportingProfile(i); if (profile.is_new_import) { CreateTemporaryProfilePath(tmp_path, sizeof(tmp_path), m_save_data_info.mount_name, profile.identifier_0); CreatePrimaryProfilePath(pri_path, sizeof(pri_path), m_save_data_info.mount_name, profile.identifier_0); R_TRY(MoveFile(tmp_path, pri_path)); } } R_SUCCEED(); } Result ProfileManager::CleanupOrphanedProfiles() { /* Check pre-conditions. */ AMS_ASSERT(m_profile_importer.has_value()); /* Declare re-usable path. */ char pri_path[0x30]; /* Cleanup the profiles. */ R_RETURN(m_profile_importer->CleanupOrphanedProfiles([&](Identifier profile) ALWAYS_INLINE_LAMBDA -> Result { CreatePrimaryProfilePath(pri_path, sizeof(pri_path), m_save_data_info.mount_name, profile); R_RETURN(DeleteFile(pri_path)); })); } void ProfileManager::OnCommitted() { /* TODO: Here, Nintendo sets the erpt ServiceProfileRevisionKey to the current revision key. */ /* If we need to, invalidate the loaded service profile. */ if (m_service_profile.has_value()) { for (auto i = 0; i < m_profile_importer->GetImportingCount(); ++i) { if (m_service_profile->name == m_profile_importer->GetImportingProfile(i).identifier_0) { m_service_profile = util::nullopt; break; } } } /* Reset profile metadata. */ m_profile_metadata = util::nullopt; /* Invoke any listeners. */ for (auto i = 0; i < m_profile_importer->GetImportingCount(); ++i) { const auto &profile = m_profile_importer->GetImportingProfile(i); if (profile.is_new_import) { m_update_observer_manager.OnUpdate(profile.identifier_0); } } /* Reset profile importer. */ m_profile_importer = util::nullopt; } Result ProfileManager::EnsurePrimaryDirectories() { /* Ensure the primary directories. */ char path[0x30]; CreatePrimaryDirectoryPath(path, sizeof(path), m_save_data_info.mount_name); R_TRY(EnsureDirectory(path)); CreatePrimaryProfileDirectoryPath(path, sizeof(path), m_save_data_info.mount_name); R_TRY(EnsureDirectory(path)); R_SUCCEED(); } Result ProfileManager::EnsureTemporaryDirectories() { /* Ensure the temporary directories. */ char path[0x30]; CreateTemporaryDirectoryPath(path, sizeof(path), m_save_data_info.mount_name); R_TRY(EnsureDirectory(path)); CreateTemporaryProfileDirectoryPath(path, sizeof(path), m_save_data_info.mount_name); R_TRY(EnsureDirectory(path)); R_SUCCEED(); } }