Merge pull request #231 from Splaser/main

Fix filesystem/content lifecycle race causing inconsistent firmware and DLC state on relaunch and update install
This commit is contained in:
cheezwiz7899
2026-06-29 10:04:27 +10:00
committed by GitHub
19 changed files with 793 additions and 243 deletions
@@ -426,7 +426,7 @@ class HomeSettingsFragment : Fragment() {
binding.linearLayoutSettings.updatePadding(bottom = spacingNavigation)
if (ViewCompat.getLayoutDirection(view) == ViewCompat.LAYOUT_DIRECTION_LTR) {
if (view.layoutDirection == View.LAYOUT_DIRECTION_LTR) {
binding.linearLayoutSettings.updatePadding(left = spacingNavigationRail)
} else {
binding.linearLayoutSettings.updatePadding(right = spacingNavigationRail)
@@ -681,8 +681,8 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
}
// Reinitialize relevant data
NativeLibrary.initializeSystem(true)
NativeConfig.initializeGlobalConfig()
NativeLibrary.initializeSystem(true)
gamesViewModel.reloadGames(false)
driverViewModel.reloadDriverData()
@@ -23,9 +23,10 @@ object DirectoryInitialization {
fun start() {
if (!areDirectoriesReady) {
initializeInternalStorage()
NativeLibrary.initializeSystem(false)
NativeConfig.initializeGlobalConfig()
migrateSettings()
NativeLibrary.reloadKeys()
NativeLibrary.initializeSystem(false)
areDirectoriesReady = true
}
}
+84 -7
View File
@@ -209,7 +209,22 @@ void EmulationSession::ConfigureFilesystemProvider(const std::string& filepath)
}
void EmulationSession::InitializeSystem(bool reload) {
std::scoped_lock lock(m_mutex);
if (m_is_running || m_load_result == Core::SystemResultStatus::Success) {
LOG_ERROR(Frontend, "Refusing to initialize system while emulation is active");
return;
}
auto& fs_controller = m_system.GetFileSystemController();
if (fs_controller.GetInitStage() < Service::FileSystem::InitStage::SETTINGS_READY) {
LOG_ERROR(Frontend, "Refusing to initialize system before settings are ready");
return;
}
if (!reload) {
m_system.Initialize();
// Initialize logging system
Common::Log::Initialize();
Common::Log::SetColorConsoleBackendEnabled(true);
@@ -218,6 +233,11 @@ void EmulationSession::InitializeSystem(bool reload) {
m_input_subsystem.Initialize();
}
Core::Crypto::KeyManager::Instance().ReloadKeys();
Core::Crypto::KeyManager::Instance().PopulateTickets();
LOG_INFO(Frontend, "InitializeSystem: keys_loaded_before_content={}",
Core::Crypto::KeyManager::Instance().AreKeysLoaded());
// Initialize filesystem.
m_system.SetFilesystem(m_vfs);
m_system.GetUserChannel().clear();
@@ -225,7 +245,52 @@ void EmulationSession::InitializeSystem(bool reload) {
m_system.SetContentProvider(std::make_unique<FileSys::ContentProviderUnion>());
m_system.RegisterContentProvider(FileSys::ContentProviderUnionSlot::FrontendManual,
m_manual_provider.get());
m_system.GetFileSystemController().CreateFactories(*m_vfs);
fs_controller.SetInitStage(Service::FileSystem::InitStage::FS_READY);
fs_controller.InitializeContentSystem(*m_vfs);
}
void EmulationSession::RefreshContentSystemUnlocked()
{
auto& fs = m_system.GetFileSystemController();
if (auto filesystem = m_system.GetFilesystem()) {
fs.InitializeContentSystem(*filesystem);
}
}
void EmulationSession::RefreshContentSystem() {
std::scoped_lock lock(m_mutex);
RefreshContentSystemUnlocked();
}
bool EmulationSession::RefreshContentIfIdle(bool keys_loaded)
{
if (!keys_loaded) {
return false;
}
std::scoped_lock lock(m_mutex);
if (m_is_running || m_load_result == Core::SystemResultStatus::Success) {
return false;
}
RefreshContentSystemUnlocked();
return true;
}
void EmulationSession::SetFilesystemInitStage(Service::FileSystem::InitStage stage) {
std::scoped_lock lock(m_mutex);
m_system.GetFileSystemController().SetInitStage(stage);
}
void EmulationSession::PromoteFilesystemInitStage(Service::FileSystem::InitStage stage) {
std::scoped_lock lock(m_mutex);
auto& controller = m_system.GetFileSystemController();
if (controller.GetInitStage() < stage) {
controller.SetInitStage(stage);
}
}
void EmulationSession::SetAppletId(int applet_id) {
@@ -561,8 +626,16 @@ jobjectArray Java_org_citron_citron_1emu_utils_GpuDriverHelper_getSystemDriverIn
}
jboolean Java_org_citron_citron_1emu_NativeLibrary_reloadKeys(JNIEnv* env, jclass clazz) {
auto& session = EmulationSession::GetInstance();
Core::Crypto::KeyManager::Instance().ReloadKeys();
return static_cast<jboolean>(Core::Crypto::KeyManager::Instance().AreKeysLoaded());
const bool keys_loaded = Core::Crypto::KeyManager::Instance().AreKeysLoaded();
const bool refreshed_content = session.RefreshContentIfIdle(keys_loaded);
LOG_INFO(Frontend,
"reloadKeys: keys_loaded={}, refreshed_content={}",
keys_loaded, refreshed_content);
return static_cast<jboolean>(keys_loaded);
}
void Java_org_citron_citron_1emu_NativeLibrary_unpauseEmulation(JNIEnv* env, jclass clazz) {
@@ -588,9 +661,6 @@ jboolean Java_org_citron_citron_1emu_NativeLibrary_isPaused(JNIEnv* env, jclass
void Java_org_citron_citron_1emu_NativeLibrary_initializeSystem(JNIEnv* env, jclass clazz,
jboolean reload) {
// Initialize the emulated system.
if (!reload) {
EmulationSession::GetInstance().System().Initialize();
}
EmulationSession::GetInstance().InitializeSystem(reload);
}
@@ -893,9 +963,16 @@ void Java_org_citron_citron_1emu_NativeLibrary_clearFilesystemProvider(JNIEnv* e
}
jboolean Java_org_citron_citron_1emu_NativeLibrary_areKeysPresent(JNIEnv* env, jobject jobj) {
auto& system = EmulationSession::GetInstance().System();
system.GetFileSystemController().CreateFactories(*system.GetFilesystem());
auto& session = EmulationSession::GetInstance();
Core::Crypto::KeyManager::Instance().ReloadKeys();
const bool keys_loaded =
Core::Crypto::KeyManager::Instance().AreKeysLoaded();
const bool refreshed_content = session.RefreshContentIfIdle(keys_loaded);
LOG_INFO(Frontend, "areKeysPresent: refreshed_content={}", refreshed_content);
return ContentManager::AreKeysPresent();
}
+6
View File
@@ -7,6 +7,7 @@
#include "core/core.h"
#include "core/file_sys/registered_cache.h"
#include "core/hle/service/acc/profile_manager.h"
#include "core/hle/service/filesystem/filesystem.h"
#include "core/perf_stats.h"
#include "frontend_common/content_manager.h"
#include "jni/emu_window/emu_window.h"
@@ -46,6 +47,10 @@ public:
const Core::PerfStatsResults& PerfStats();
void ConfigureFilesystemProvider(const std::string& filepath);
void InitializeSystem(bool reload);
void RefreshContentSystem();
bool RefreshContentIfIdle(bool keys_loaded);
void SetFilesystemInitStage(Service::FileSystem::InitStage stage);
void PromoteFilesystemInitStage(Service::FileSystem::InitStage stage);
void SetAppletId(int applet_id);
Core::SystemResultStatus InitializeEmulation(const std::string& filepath,
const std::size_t program_index,
@@ -58,6 +63,7 @@ public:
static u64 GetProgramId(JNIEnv* env, jstring jprogramId);
private:
void RefreshContentSystemUnlocked();
static void LoadDiskCacheProgress(VideoCore::LoadCallbackStage stage, int progress, int max);
static void OnEmulationStopped(Core::SystemResultStatus result);
static void ChangeProgram(std::size_t program_index);
@@ -12,11 +12,24 @@
#include "common/logging.h"
#include "common/settings.h"
#include "frontend_common/config.h"
#include "core/hle/service/filesystem/filesystem.h"
#include "native.h"
std::unique_ptr<AndroidConfig> global_config;
std::unique_ptr<AndroidConfig> per_game_config;
namespace {
void SetFilesystemInitStage(Service::FileSystem::InitStage stage) {
EmulationSession::GetInstance().SetFilesystemInitStage(stage);
}
void PromoteFilesystemInitStage(Service::FileSystem::InitStage stage) {
EmulationSession::GetInstance().PromoteFilesystemInitStage(stage);
}
} // namespace
template <typename T>
Settings::Setting<T>* getSetting(JNIEnv* env, jstring jkey) {
auto key = Common::Android::GetJString(env, jkey);
@@ -36,14 +49,17 @@ extern "C" {
void Java_org_citron_citron_1emu_utils_NativeConfig_initializeGlobalConfig(JNIEnv* env, jobject obj) {
global_config = std::make_unique<AndroidConfig>();
PromoteFilesystemInitStage(Service::FileSystem::InitStage::SETTINGS_READY);
}
void Java_org_citron_citron_1emu_utils_NativeConfig_unloadGlobalConfig(JNIEnv* env, jobject obj) {
global_config.reset();
SetFilesystemInitStage(Service::FileSystem::InitStage::NONE);
}
void Java_org_citron_citron_1emu_utils_NativeConfig_reloadGlobalConfig(JNIEnv* env, jobject obj) {
global_config->AndroidConfig::ReloadAllValues();
PromoteFilesystemInitStage(Service::FileSystem::InitStage::SETTINGS_READY);
}
void Java_org_citron_citron_1emu_utils_NativeConfig_saveGlobalConfig(JNIEnv* env, jobject obj) {
+6 -6
View File
@@ -452,7 +452,7 @@ GMainWindow::GMainWindow(std::unique_ptr<QtConfig> config_, bool has_broken_vulk
// 1. First, create the factories
LOG_INFO(Frontend, "Initializing factories...");
system->GetFileSystemController().CreateFactories(*vfs);
system->GetFileSystemController().InitializeContentSystem(*vfs);
autoloader_provider = std::make_unique<FileSys::ManualContentProvider>();
system->RegisterContentProvider(FileSys::ContentProviderUnionSlot::Autoloader,
@@ -2556,7 +2556,7 @@ void GMainWindow::OnEmulationStopped() {
game_list->CancelPopulation();
// This is necessary to reset the in-memory state for the next launch.
system->GetFileSystemController().CreateFactories(*vfs, true);
system->GetFileSystemController().InitializeContentSystem(*vfs, true);
// Refresh the game list now that the filesystem is valid again.
game_list->ClearLaunchOverlays();
@@ -5130,7 +5130,7 @@ void GMainWindow::OnInstallFirmwareFromZip() {
}
// Re-scan VFS for the newly placed firmware files.
system->GetFileSystemController().CreateFactories(*vfs);
system->GetFileSystemController().InitializeContentSystem(*vfs);
auto VerifyFirmwareCallback = [&](size_t total_size, size_t processed_size) {
progress.setValue(85 + static_cast<int>((processed_size * 15) / total_size));
@@ -5294,7 +5294,7 @@ void GMainWindow::OnInstallFirmware() {
}
// Re-scan VFS for the newly placed firmware files.
system->GetFileSystemController().CreateFactories(*vfs);
system->GetFileSystemController().InitializeContentSystem(*vfs);
auto VerifyFirmwareCallback = [&](size_t total_size, size_t processed_size) {
progress.setValue(90 + static_cast<int>((processed_size * 10) / total_size));
@@ -5391,7 +5391,7 @@ void GMainWindow::OnInstallDecryptionKeys() {
// and re-populate the game list in the UI if the user has already added
// game folders.
Core::Crypto::KeyManager::Instance().ReloadKeys();
system->GetFileSystemController().CreateFactories(*vfs);
system->GetFileSystemController().InitializeContentSystem(*vfs);
game_list->PopulateAsync(UISettings::values.game_dirs);
if (ContentManager::AreKeysPresent()) {
@@ -6066,7 +6066,7 @@ void GMainWindow::OnMouseActivity() {
}
void GMainWindow::OnCheckFirmwareDecryption() {
system->GetFileSystemController().CreateFactories(*vfs);
system->GetFileSystemController().InitializeContentSystem(*vfs);
if (!ContentManager::AreKeysPresent()) {
QMessageBox::warning(this, tr("Derivation Components Missing"),
tr("Encryption keys are missing. "
+1 -1
View File
@@ -360,7 +360,7 @@ int main(int argc, char** argv) {
system.SetContentProvider(std::make_unique<FileSys::ContentProviderUnion>());
system.SetFilesystem(std::make_shared<FileSys::RealVfsFilesystem>());
system.GetFileSystemController().CreateFactories(*system.GetFilesystem());
system.GetFileSystemController().InitializeContentSystem(*system.GetFilesystem());
system.GetUserChannel().clear();
Service::AM::FrontendAppletParameters load_parameters{
+114 -9
View File
@@ -234,18 +234,33 @@ Ticket Ticket::Read(std::span<const u8> raw_data) {
switch (sig_type) {
case SignatureType::RSA_4096_SHA1:
case SignatureType::RSA_4096_SHA256: {
if (raw_data.size() < sizeof(RSA4096Ticket)) {
LOG_WARNING(Crypto, "Attempted to parse RSA4096 ticket buffer with invalid size {}.",
raw_data.size());
return Ticket{std::monostate()};
}
RSA4096Ticket ticket{};
std::memcpy(&ticket, raw_data.data(), sizeof(ticket));
return Ticket{ticket};
}
case SignatureType::RSA_2048_SHA1:
case SignatureType::RSA_2048_SHA256: {
if (raw_data.size() < sizeof(RSA2048Ticket)) {
LOG_WARNING(Crypto, "Attempted to parse RSA2048 ticket buffer with invalid size {}.",
raw_data.size());
return Ticket{std::monostate()};
}
RSA2048Ticket ticket{};
std::memcpy(&ticket, raw_data.data(), sizeof(ticket));
return Ticket{ticket};
}
case SignatureType::ECDSA_SHA1:
case SignatureType::ECDSA_SHA256: {
if (raw_data.size() < sizeof(ECDSATicket)) {
LOG_WARNING(Crypto, "Attempted to parse ECDSA ticket buffer with invalid size {}.",
raw_data.size());
return Ticket{std::monostate()};
}
ECDSATicket ticket{};
std::memcpy(&ticket, raw_data.data(), sizeof(ticket));
return Ticket{ticket};
@@ -479,6 +494,22 @@ Loader::ResultStatus DeriveSDKeys(std::array<Key256, 2>& sd_keys, KeyManager& ke
return Loader::ResultStatus::Success;
}
static std::optional<std::size_t> GetTicketSize(SignatureType sig_type) {
switch (sig_type) {
case SignatureType::RSA_4096_SHA1:
case SignatureType::RSA_4096_SHA256:
return sizeof(RSA4096Ticket);
case SignatureType::RSA_2048_SHA1:
case SignatureType::RSA_2048_SHA256:
return sizeof(RSA2048Ticket);
case SignatureType::ECDSA_SHA1:
case SignatureType::ECDSA_SHA256:
return sizeof(ECDSATicket);
default:
return std::nullopt;
}
}
std::vector<Ticket> GetTicketblob(const Common::FS::IOFile& ticket_save) {
if (!ticket_save.IsOpen()) {
return {};
@@ -490,21 +521,46 @@ std::vector<Ticket> GetTicketblob(const Common::FS::IOFile& ticket_save) {
}
std::vector<Ticket> out;
for (std::size_t offset = 0; offset + 0x4 < buffer.size(); ++offset) {
if (buffer[offset] == 0x4 && buffer[offset + 1] == 0x0 && buffer[offset + 2] == 0x1 &&
buffer[offset + 3] == 0x0) {
// NOTE: Assumes ticket blob will only contain RSA-2048 tickets.
auto ticket = Ticket::Read(std::span{buffer.data() + offset, sizeof(RSA2048Ticket)});
offset += sizeof(RSA2048Ticket);
if (ticket.IsValid()) {
out.push_back(ticket);
}
for (std::size_t offset = 0; offset + sizeof(SignatureType) <= buffer.size(); ++offset) {
SignatureType sig_type;
std::memcpy(&sig_type, buffer.data() + offset, sizeof(sig_type));
const auto ticket_size = GetTicketSize(sig_type);
if (!ticket_size) {
continue;
}
if (offset + *ticket_size > buffer.size()) {
break;
}
auto ticket = Ticket::Read(std::span{buffer.data() + offset, *ticket_size});
offset += *ticket_size - 1;
if (ticket.IsValid()) {
out.push_back(ticket);
}
}
return out;
}
static std::span<const u8> GetTicketBytes(const Ticket& ticket) {
if (const auto* rsa4096 = std::get_if<RSA4096Ticket>(&ticket.data)) {
return {reinterpret_cast<const u8*>(rsa4096), sizeof(RSA4096Ticket)};
}
if (const auto* rsa2048 = std::get_if<RSA2048Ticket>(&ticket.data)) {
return {reinterpret_cast<const u8*>(rsa2048), sizeof(RSA2048Ticket)};
}
if (const auto* ecdsa = std::get_if<ECDSATicket>(&ticket.data)) {
return {reinterpret_cast<const u8*>(ecdsa), sizeof(ECDSATicket)};
}
return {};
}
static bool TicketMatchesRightsId(const Ticket& ticket, const std::array<u8, 0x10>& rights_id) {
return ticket.IsValid() && ticket.GetData().rights_id == rights_id;
}
template <size_t size>
static std::array<u8, size> operator^(const std::array<u8, size>& lhs,
const std::array<u8, size>& rhs) {
@@ -1282,4 +1338,53 @@ bool KeyManager::AddTicket(const Ticket& ticket) {
SetKey(S128KeyType::Titlekey, key.value(), rights_id[1], rights_id[0]);
return true;
}
bool KeyManager::PersistTicket(const Ticket& ticket) {
if (!ticket.IsValid()) {
LOG_WARNING(Crypto, "Attempted to persist invalid ticket.");
return false;
}
const auto ticket_path = Common::FS::GetCitronPath(Common::FS::CitronPath::NANDDir) /
(ticket.GetData().type == TitleKeyType::Common
? "system/save/80000000000000e1"
: "system/save/80000000000000e2");
if (!Common::FS::CreateParentDirs(ticket_path)) {
LOG_ERROR(Crypto, "Failed to create ticket save directory for {}",
Common::FS::PathToUTF8String(ticket_path));
return false;
}
if (Common::FS::Exists(ticket_path)) {
const Common::FS::IOFile existing_file{ticket_path, Common::FS::FileAccessMode::Read,
Common::FS::FileType::BinaryFile};
const auto existing_tickets = GetTicketblob(existing_file);
if (std::any_of(existing_tickets.begin(), existing_tickets.end(),
[&ticket](const Ticket& existing_ticket) {
return TicketMatchesRightsId(existing_ticket,
ticket.GetData().rights_id);
})) {
return true;
}
}
const auto ticket_bytes = GetTicketBytes(ticket);
if (ticket_bytes.empty()) {
LOG_WARNING(Crypto, "Unable to serialize invalid ticket variant.");
return false;
}
const Common::FS::IOFile ticket_save{ticket_path, Common::FS::FileAccessMode::Append,
Common::FS::FileType::BinaryFile};
if (!ticket_save.IsOpen()) {
return false;
}
if (ticket_save.WriteSpan(ticket_bytes) != ticket_bytes.size()) {
LOG_ERROR(Crypto, "Failed to write ticket to {}", Common::FS::PathToUTF8String(ticket_path));
return false;
}
return ticket_save.Flush();
}
} // namespace Core::Crypto
+1
View File
@@ -291,6 +291,7 @@ public:
const std::map<u128, Ticket>& GetPersonalizedTickets() const;
bool AddTicket(const Ticket& ticket);
bool PersistTicket(const Ticket& ticket);
void ReloadKeys();
bool AreKeysLoaded() const;
+141 -134
View File
@@ -46,6 +46,12 @@
namespace FileSys {
namespace {
// patch_manager.cpp only
const ContentProviderUnion* GetUnionProvider(const ContentProvider& p) {
return p.IsUnionProvider()
? static_cast<const ContentProviderUnion*>(&p)
: nullptr;
}
constexpr u32 SINGLE_BYTE_MODULUS = 0x100;
@@ -172,6 +178,8 @@ bool IsDirValidAndNonEmpty(const VirtualDir& dir) {
}
} // Anonymous namespace
PatchManager::PatchManager(u64 title_id_,
const Service::FileSystem::FileSystemController& fs_controller_,
const ContentProvider& content_provider_)
@@ -235,13 +243,10 @@ VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const {
if (found_best) {
const auto update_tid = GetUpdateTitleID(title_id);
if (active_update.is_external) {
const auto* content_provider_union =
static_cast<const ContentProviderUnion*>(&content_provider);
const auto* content_provider_union = GetUnionProvider(content_provider);
if (content_provider_union) {
if (const auto* external_provider = content_provider_union->GetExternalProvider()) {
best_update_raw = external_provider->GetEntryForVersion(
update_tid, ContentRecordType::Program, best_version);
}
best_update_raw = content_provider_union->GetExternalEntryForVersion(
update_tid, ContentRecordType::Program, best_version);
}
} else {
best_update_raw = content_provider.GetEntryRaw(update_tid, ContentRecordType::Program);
@@ -572,19 +577,25 @@ VirtualFile PatchManager::PatchRomFS(const NCA* base_nca, VirtualFile base_romfs
if (found_best) {
const auto update_tid = GetUpdateTitleID(title_id);
if (active_update.is_external) {
const auto* content_provider_union =
static_cast<const ContentProviderUnion*>(&content_provider);
const auto* content_provider_union = GetUnionProvider(content_provider);
if (content_provider_union) {
if (const auto* external_provider = content_provider_union->GetExternalProvider()) {
best_update_raw = external_provider->GetEntryForVersion(
update_tid, type, best_version);
}
best_update_raw =
content_provider_union->GetExternalEntryForVersion(update_tid, type,
best_version);
}
} else {
best_update_raw = content_provider.GetEntryRaw(update_tid, type);
}
}
if (type == ContentRecordType::Program || type == ContentRecordType::Control) {
LOG_INFO(Loader,
"PatchRomFS update lookup: title_id={:016X}, type={:02X}, found_best={}, "
"best_version={}, best_update_raw={}",
title_id, static_cast<u32>(type), found_best, best_version,
best_update_raw != nullptr);
}
// 3. Packed Update (Fallback)
// If we still haven't found a best update, or if the packed one is enabled (named "Update"
// usually? Or "Update (PACKED)"?) The GetPatches logic names it "Update" with version
@@ -708,8 +719,7 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
const auto update_disabled =
std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend();
const auto* content_provider_union =
static_cast<const ContentProviderUnion*>(&content_provider);
const auto* content_provider_union = GetUnionProvider(content_provider);
bool is_nand_control = true;
bool is_nand_program = true;
if (content_provider_union) {
@@ -758,84 +768,79 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
// Next, check for external updates
if (content_provider_union) {
const auto* external_provider = content_provider_union->GetExternalProvider();
if (external_provider) {
const auto updates = external_provider->ListUpdateVersions(update_tid);
for (const auto& update : updates) {
// Canonical name uses the raw version number (via FormatTitleVersion) so that
// the disabled_addons key matches what PatchExeFS/PatchRomFS check at boot.
// The NACP-derived string is only used as a UI display label.
const auto canonical_version_str = FormatTitleVersion(update.version);
const auto canonical_name =
fmt::format("External Files/Update v{}", canonical_version_str);
const auto updates = content_provider_union->ListExternalUpdateVersions(update_tid);
for (const auto& update : updates) {
// Canonical name uses the raw version number (via FormatTitleVersion) so that
// the disabled_addons key matches what PatchExeFS/PatchRomFS check at boot.
// The NACP-derived string is only used as a UI display label.
const auto canonical_version_str = FormatTitleVersion(update.version);
const auto canonical_name =
fmt::format("External Files/Update v{}", canonical_version_str);
// Attempt to resolve a human-readable display version from the Control NCA.
std::string display_version_str;
const auto control_file = external_provider->GetEntryForVersion(
update_tid, ContentRecordType::Control, update.version);
if (control_file) {
NCA control_nca(control_file);
if (control_nca.GetStatus() == Loader::ResultStatus::Success) {
if (auto control_romfs = control_nca.GetRomFS()) {
if (auto extracted = ExtractRomFS(control_romfs)) {
auto nacp_file = extracted->GetFile("control.nacp");
if (!nacp_file) {
nacp_file = extracted->GetFile("Control.nacp");
}
if (nacp_file) {
NACP control_nacp(nacp_file);
display_version_str = CleanNacpVersion(control_nacp.GetVersionString());
}
// Attempt to resolve a human-readable display version from the Control NCA.
std::string display_version_str;
const auto control_file = content_provider_union->GetExternalEntryForVersion(
update_tid, ContentRecordType::Control, update.version);
if (control_file) {
NCA control_nca(control_file);
if (control_nca.GetStatus() == Loader::ResultStatus::Success) {
if (auto control_romfs = control_nca.GetRomFS()) {
if (auto extracted = ExtractRomFS(control_romfs)) {
auto nacp_file = extracted->GetFile("control.nacp");
if (!nacp_file) {
nacp_file = extracted->GetFile("Control.nacp");
}
if (nacp_file) {
NACP control_nacp(nacp_file);
display_version_str =
CleanNacpVersion(control_nacp.GetVersionString());
}
}
}
}
// The name stored in disabled_addons is the canonical one (FormatTitleVersion),
// ensuring stability across reboots regardless of NACP parse success.
// Also check the NACP-derived name for backwards compatibility.
bool patch_disabled =
std::find(disabled.cbegin(), disabled.cend(), canonical_name) !=
disabled.cend();
if (!patch_disabled && !display_version_str.empty() &&
display_version_str != canonical_version_str) {
const auto nacp_name =
fmt::format("External Files/Update v{}", display_version_str);
patch_disabled =
std::find(disabled.cbegin(), disabled.cend(), nacp_name) !=
disabled.cend();
}
// Use the human-readable string for display if available.
const auto display_name = display_version_str.empty()
? canonical_name
: fmt::format("External Files/Update v{}",
display_version_str);
const auto version_str =
display_version_str.empty() ? canonical_version_str : display_version_str;
// Deduplicate against installed system update if versions match
bool exists = false;
for (const auto& existing : out) {
if (existing.type == PatchType::Update &&
existing.version == canonical_version_str) {
exists = true;
break;
}
}
if (exists)
continue;
// Store canonical_name as .name so the UI checkbox writes the right key to
// disabled_addons, matching what PatchExeFS/PatchRomFS check.
Patch update_patch = {.enabled = !patch_disabled,
.name = canonical_name,
.version = version_str,
.type = PatchType::Update,
.program_id = title_id,
.title_id = title_id};
out.push_back(update_patch);
}
// The name stored in disabled_addons is the canonical one (FormatTitleVersion),
// ensuring stability across reboots regardless of NACP parse success.
// Also check the NACP-derived name for backwards compatibility.
bool patch_disabled = std::find(disabled.cbegin(), disabled.cend(), canonical_name) !=
disabled.cend();
if (!patch_disabled && !display_version_str.empty() &&
display_version_str != canonical_version_str) {
const auto nacp_name = fmt::format("External Files/Update v{}", display_version_str);
patch_disabled =
std::find(disabled.cbegin(), disabled.cend(), nacp_name) != disabled.cend();
}
// Use the human-readable string for display if available.
const auto display_name = display_version_str.empty()
? canonical_name
: fmt::format("External Files/Update v{}",
display_version_str);
const auto version_str =
display_version_str.empty() ? canonical_version_str : display_version_str;
// Deduplicate against installed system update if versions match
bool exists = false;
for (const auto& existing : out) {
if (existing.type == PatchType::Update &&
existing.version == canonical_version_str) {
exists = true;
break;
}
}
if (exists)
continue;
// Store canonical_name as .name so the UI checkbox writes the right key to
// disabled_addons, matching what PatchExeFS/PatchRomFS check.
Patch update_patch = {.enabled = !patch_disabled,
.name = canonical_name,
.version = version_str,
.type = PatchType::Update,
.program_id = title_id,
.title_id = title_id};
out.push_back(update_patch);
}
}
@@ -1088,53 +1093,48 @@ PatchManager::ActiveUpdate PatchManager::GetActiveUpdate() const {
bool is_external = false;
// 1. External Updates
const auto* content_provider_union =
static_cast<const ContentProviderUnion*>(&content_provider);
const auto* content_provider_union = GetUnionProvider(content_provider);
if (content_provider_union) {
const auto* external_provider = content_provider_union->GetExternalProvider();
if (external_provider) {
const auto update_tid = GetUpdateTitleID(title_id);
const auto updates = external_provider->ListUpdateVersions(update_tid);
for (const auto& update : updates) {
const auto canonical_version_str = FormatTitleVersion(update.version);
const auto canonical_name =
fmt::format("External Files/Update v{}", canonical_version_str);
const auto update_tid = GetUpdateTitleID(title_id);
const auto updates = content_provider_union->ListExternalUpdateVersions(update_tid);
for (const auto& update : updates) {
const auto canonical_version_str = FormatTitleVersion(update.version);
const auto canonical_name =
fmt::format("External Files/Update v{}", canonical_version_str);
std::string nacp_version_str;
const auto control_file = external_provider->GetEntryForVersion(
update_tid, ContentRecordType::Control, update.version);
if (control_file) {
NCA control_nca(control_file);
if (control_nca.GetStatus() == Loader::ResultStatus::Success) {
if (auto control_romfs = control_nca.GetRomFS()) {
if (auto extracted = ExtractRomFS(control_romfs)) {
auto nacp_file = extracted->GetFile("control.nacp");
if (!nacp_file) {
nacp_file = extracted->GetFile("Control.nacp");
}
if (nacp_file) {
NACP nacp(nacp_file);
nacp_version_str = CleanNacpVersion(nacp.GetVersionString());
}
std::string nacp_version_str;
const auto control_file = content_provider_union->GetExternalEntryForVersion(
update_tid, ContentRecordType::Control, update.version);
if (control_file) {
NCA control_nca(control_file);
if (control_nca.GetStatus() == Loader::ResultStatus::Success) {
if (auto control_romfs = control_nca.GetRomFS()) {
if (auto extracted = ExtractRomFS(control_romfs)) {
auto nacp_file = extracted->GetFile("control.nacp");
if (!nacp_file) {
nacp_file = extracted->GetFile("Control.nacp");
}
if (nacp_file) {
NACP nacp(nacp_file);
nacp_version_str = CleanNacpVersion(nacp.GetVersionString());
}
}
}
}
}
bool patch_disabled = is_disabled(canonical_name);
if (!patch_disabled && !nacp_version_str.empty() &&
nacp_version_str != canonical_version_str) {
const auto nacp_name =
fmt::format("External Files/Update v{}", nacp_version_str);
patch_disabled = is_disabled(nacp_name);
}
bool patch_disabled = is_disabled(canonical_name);
if (!patch_disabled && !nacp_version_str.empty() &&
nacp_version_str != canonical_version_str) {
const auto nacp_name = fmt::format("External Files/Update v{}", nacp_version_str);
patch_disabled = is_disabled(nacp_name);
}
if (!patch_disabled) {
if (!found_best || update.version > best_version) {
best_version = update.version;
found_best = true;
is_external = true;
}
if (!patch_disabled) {
if (!found_best || update.version > best_version) {
best_version = update.version;
found_best = true;
is_external = true;
}
}
}
@@ -1232,31 +1232,38 @@ PatchManager::Metadata PatchManager::GetControlMetadata() const {
}
const auto active_update = GetActiveUpdate();
LOG_INFO(Loader,
"GetControlMetadata: title_id={:016X}, active_update_found={}, version={}, "
"is_external={}",
title_id, active_update.found, active_update.version, active_update.is_external);
if (active_update.found) {
const auto update_tid = GetUpdateTitleID(title_id);
if (active_update.is_external) {
const auto* content_provider_union =
static_cast<const ContentProviderUnion*>(&content_provider);
const auto* content_provider_union = GetUnionProvider(content_provider);
if (content_provider_union) {
if (const auto* external_provider = content_provider_union->GetExternalProvider()) {
auto control_file = external_provider->GetEntryForVersion(
update_tid, ContentRecordType::Control, active_update.version);
if (control_file) {
control_nca = std::make_unique<NCA>(control_file);
}
auto control_file = content_provider_union->GetExternalEntryForVersion(
update_tid, ContentRecordType::Control, active_update.version);
if (control_file) {
control_nca = std::make_unique<NCA>(control_file);
}
}
} else {
control_nca = content_provider.GetEntry(update_tid, ContentRecordType::Control);
}
LOG_INFO(Loader, "GetControlMetadata: update control_nca={}", control_nca != nullptr);
}
if (control_nca == nullptr) {
control_nca = content_provider.GetEntry(title_id, ContentRecordType::Control);
LOG_INFO(Loader, "GetControlMetadata: base control_nca={}", control_nca != nullptr);
}
if (control_nca == nullptr)
if (control_nca == nullptr) {
LOG_WARNING(Loader, "GetControlMetadata: no control NCA for title_id={:016X}", title_id);
return {};
}
LOG_INFO(Loader, "GetControlMetadata: selected control status={}, type={:02X}",
static_cast<u32>(control_nca->GetStatus()), static_cast<u32>(control_nca->GetType()));
return ParseControlNCA(*control_nca);
}
+124 -11
View File
@@ -3,8 +3,10 @@
#include <algorithm>
#include <array>
#include <mutex>
#include <random>
#include <regex>
#include <shared_mutex>
#include <vector>
#include <fmt/format.h>
#include <openssl/evp.h>
@@ -71,7 +73,7 @@ static bool FollowsNcaIdFormat(std::string_view name) {
static std::string GetRelativePathFromNcaID(const std::array<u8, 16>& nca_id, bool second_hex_upper,
bool within_two_digit, bool cnmt_suffix) {
if (!within_two_digit) {
const auto format_str = fmt::runtime(cnmt_suffix ? "{}.cnmt.nca" : "/{}.nca");
const auto format_str = fmt::runtime(cnmt_suffix ? "/{}.cnmt.nca" : "/{}.nca");
return fmt::format(format_str, Common::HexToString(nca_id, second_hex_upper));
}
@@ -424,32 +426,87 @@ std::vector<NcaID> RegisteredCache::AccumulateFiles() const {
return ids;
}
void RegisteredCache::ProcessFiles(const std::vector<NcaID>& ids) {
void RegisteredCache::ProcessFiles(const std::vector<NcaID>& ids, std::map<u64, CNMT>& out_meta,
std::map<u64, NcaID>& out_meta_id) const {
for (const auto& id : ids) {
const auto file = GetFileAtID(id);
if (file == nullptr)
if (file == nullptr) {
LOG_WARNING(Service_FS, "RegisteredCache ProcessFiles: missing file for nca_id={}",
Common::HexToString(id, false));
continue;
}
const auto nca = std::make_shared<NCA>(parser(file, id));
if (nca->GetStatus() != Loader::ResultStatus::Success ||
nca->GetType() != NCAContentType::Meta || nca->GetSubdirectories().empty()) {
if (nca->GetStatus() != Loader::ResultStatus::Success) {
LOG_WARNING(Service_FS,
"RegisteredCache ProcessFiles: failed nca_id={}, status={}",
Common::HexToString(id, false), static_cast<u16>(nca->GetStatus()));
continue;
}
const auto nca_type = nca->GetType();
if (nca_type != NCAContentType::Meta) {
LOG_INFO(Service_FS,
"RegisteredCache ProcessFiles: skipped non-meta nca_id={}, title_id={:016X}, "
"type={:02X}",
Common::HexToString(id, false), nca->GetTitleId(), static_cast<u32>(nca_type));
continue;
}
if (nca->GetSubdirectories().empty()) {
LOG_WARNING(Service_FS,
"RegisteredCache ProcessFiles: meta has no sections nca_id={}, "
"title_id={:016X}",
Common::HexToString(id, false), nca->GetTitleId());
continue;
}
const auto section0 = nca->GetSubdirectories()[0];
bool found_cnmt = false;
for (const auto& section0_file : section0->GetFiles()) {
if (section0_file->GetExtension() != "cnmt")
continue;
meta.insert_or_assign(nca->GetTitleId(), CNMT(section0_file));
meta_id.insert_or_assign(nca->GetTitleId(), id);
CNMT cnmt(section0_file);
const auto title_id = cnmt.GetTitleID();
LOG_INFO(Service_FS,
"RegisteredCache ProcessFiles: parsed meta nca_id={}, nca_title_id={:016X}, "
"cnmt_title_id={:016X}, title_type={:02X}, records={}",
Common::HexToString(id, false), nca->GetTitleId(), title_id,
static_cast<u32>(cnmt.GetType()), cnmt.GetContentRecords().size());
if (cnmt.GetType() == TitleType::AOC || cnmt.GetType() == TitleType::Update) {
const auto& content_records = cnmt.GetContentRecords();
for (std::size_t record_index = 0; record_index < content_records.size();
++record_index) {
const auto& record = content_records[record_index];
LOG_INFO(Service_FS,
"RegisteredCache ProcessFiles: meta record title_id={:016X}, "
"record_index={}, record_type={:02X}, record_nca_id={}, "
"record_file_found={}",
title_id, record_index, static_cast<u32>(record.type),
Common::HexToString(record.nca_id, false),
GetFileAtID(record.nca_id) != nullptr);
}
}
out_meta.insert_or_assign(title_id, std::move(cnmt));
out_meta_id.insert_or_assign(title_id, id);
found_cnmt = true;
break;
}
if (!found_cnmt) {
LOG_WARNING(Service_FS,
"RegisteredCache ProcessFiles: meta section lacks cnmt nca_id={}, "
"title_id={:016X}",
Common::HexToString(id, false), nca->GetTitleId());
}
}
}
void RegisteredCache::AccumulateCitronMeta() {
void RegisteredCache::AccumulateCitronMeta(std::map<u64, CNMT>& out_citron_meta) const {
const auto meta_dir = dir->GetSubdirectory("citron_meta");
if (meta_dir == nullptr) {
return;
@@ -461,7 +518,7 @@ void RegisteredCache::AccumulateCitronMeta() {
}
CNMT cnmt(file);
citron_meta.insert_or_assign(cnmt.GetTitleID(), std::move(cnmt));
out_citron_meta.insert_or_assign(cnmt.GetTitleID(), std::move(cnmt));
}
}
@@ -471,8 +528,20 @@ void RegisteredCache::Refresh() {
}
const auto ids = AccumulateFiles();
ProcessFiles(ids);
AccumulateCitronMeta();
std::map<u64, CNMT> new_meta;
std::map<u64, NcaID> new_meta_id;
std::map<u64, CNMT> new_citron_meta;
ProcessFiles(ids, new_meta, new_meta_id);
AccumulateCitronMeta(new_citron_meta);
meta.swap(new_meta);
meta_id.swap(new_meta_id);
citron_meta.swap(new_citron_meta);
LOG_INFO(Service_FS,
"RegisteredCache refreshed: path={}, nca_ids={}, meta_entries={}, citron_meta={}",
dir->GetFullPath(), ids.size(), meta.size(), citron_meta.size());
}
RegisteredCache::RegisteredCache(VirtualDir dir_, ContentProviderParsingFunction parsing_function)
@@ -846,6 +915,7 @@ bool RegisteredCache::RawInstallCitronMeta(const CNMT& cnmt) {
ContentProviderUnion::~ContentProviderUnion() = default;
const ExternalContentProvider* ContentProviderUnion::GetExternalProvider() const {
std::shared_lock lock{providers_mutex};
auto it = providers.find(ContentProviderUnionSlot::External);
if (it != providers.end()) {
return static_cast<const ExternalContentProvider*>(it->second);
@@ -854,6 +924,7 @@ const ExternalContentProvider* ContentProviderUnion::GetExternalProvider() const
}
const ContentProvider* ContentProviderUnion::GetSlotProvider(ContentProviderUnionSlot slot) const {
std::shared_lock lock{providers_mutex};
auto it = providers.find(slot);
if (it != providers.end()) {
return it->second;
@@ -862,14 +933,25 @@ const ContentProvider* ContentProviderUnion::GetSlotProvider(ContentProviderUnio
}
void ContentProviderUnion::SetSlot(ContentProviderUnionSlot slot, ContentProvider* provider) {
std::unique_lock lock{providers_mutex};
providers[slot] = provider;
}
void ContentProviderUnion::SetSlots(
std::initializer_list<std::pair<ContentProviderUnionSlot, ContentProvider*>> slots) {
std::unique_lock lock{providers_mutex};
for (const auto& [slot, provider] : slots) {
providers[slot] = provider;
}
}
void ContentProviderUnion::ClearSlot(ContentProviderUnionSlot slot) {
std::unique_lock lock{providers_mutex};
providers[slot] = nullptr;
}
void ContentProviderUnion::Refresh() {
std::unique_lock lock{providers_mutex};
for (auto& provider : providers) {
if (provider.second == nullptr)
continue;
@@ -879,6 +961,7 @@ void ContentProviderUnion::Refresh() {
}
bool ContentProviderUnion::HasEntry(u64 title_id, ContentRecordType type) const {
std::shared_lock lock{providers_mutex};
for (const auto& provider : providers) {
if (provider.second == nullptr)
continue;
@@ -891,6 +974,7 @@ bool ContentProviderUnion::HasEntry(u64 title_id, ContentRecordType type) const
}
std::optional<u32> ContentProviderUnion::GetEntryVersion(u64 title_id) const {
std::shared_lock lock{providers_mutex};
for (const auto& provider : providers) {
if (provider.second == nullptr)
continue;
@@ -904,6 +988,7 @@ std::optional<u32> ContentProviderUnion::GetEntryVersion(u64 title_id) const {
}
VirtualFile ContentProviderUnion::GetEntryUnparsed(u64 title_id, ContentRecordType type) const {
std::shared_lock lock{providers_mutex};
for (const auto& provider : providers) {
if (provider.second == nullptr)
continue;
@@ -917,6 +1002,7 @@ VirtualFile ContentProviderUnion::GetEntryUnparsed(u64 title_id, ContentRecordTy
}
VirtualFile ContentProviderUnion::GetEntryRaw(u64 title_id, ContentRecordType type) const {
std::shared_lock lock{providers_mutex};
for (const auto& provider : providers) {
if (provider.second == nullptr)
continue;
@@ -930,6 +1016,7 @@ VirtualFile ContentProviderUnion::GetEntryRaw(u64 title_id, ContentRecordType ty
}
std::unique_ptr<NCA> ContentProviderUnion::GetEntry(u64 title_id, ContentRecordType type) const {
std::shared_lock lock{providers_mutex};
for (const auto& provider : providers) {
if (provider.second == nullptr)
continue;
@@ -947,6 +1034,7 @@ std::vector<ContentProviderEntry> ContentProviderUnion::ListEntriesFilter(
std::optional<u64> title_id) const {
std::vector<ContentProviderEntry> out;
std::shared_lock lock{providers_mutex};
for (const auto& provider : providers) {
if (provider.second == nullptr)
continue;
@@ -967,6 +1055,7 @@ ContentProviderUnion::ListEntriesFilterOrigin(std::optional<ContentProviderUnion
std::optional<u64> title_id) const {
std::vector<std::pair<ContentProviderUnionSlot, ContentProviderEntry>> out;
std::shared_lock lock{providers_mutex};
for (const auto& provider : providers) {
if (provider.second == nullptr)
continue;
@@ -988,6 +1077,7 @@ ContentProviderUnion::ListEntriesFilterOrigin(std::optional<ContentProviderUnion
std::optional<ContentProviderUnionSlot> ContentProviderUnion::GetSlotForEntry(
u64 title_id, ContentRecordType type) const {
std::shared_lock lock{providers_mutex};
const auto iter =
std::find_if(providers.begin(), providers.end(), [title_id, type](const auto& provider) {
return provider.second != nullptr && provider.second->HasEntry(title_id, type);
@@ -1000,6 +1090,29 @@ std::optional<ContentProviderUnionSlot> ContentProviderUnion::GetSlotForEntry(
return iter->first;
}
VirtualFile ContentProviderUnion::GetExternalEntryForVersion(u64 title_id, ContentRecordType type,
u32 version) const {
std::shared_lock lock{providers_mutex};
auto it = providers.find(ContentProviderUnionSlot::External);
if (it == providers.end() || it->second == nullptr) {
return nullptr;
}
return static_cast<const ExternalContentProvider*>(it->second)
->GetEntryForVersion(title_id, type, version);
}
std::vector<ExternalUpdateEntry> ContentProviderUnion::ListExternalUpdateVersions(
u64 title_id) const {
std::shared_lock lock{providers_mutex};
auto it = providers.find(ContentProviderUnionSlot::External);
if (it == providers.end() || it->second == nullptr) {
return {};
}
return static_cast<const ExternalContentProvider*>(it->second)->ListUpdateVersions(title_id);
}
ManualContentProvider::~ManualContentProvider() = default;
void ManualContentProvider::AddEntry(TitleType title_type, ContentRecordType content_type,
+17 -2
View File
@@ -6,11 +6,14 @@
#include <array>
#include <functional>
#include <initializer_list>
#include <map>
#include <memory>
#include <optional>
#include <shared_mutex>
#include <string>
#include <tuple>
#include <utility>
#include <vector>
#include <boost/container/flat_map.hpp>
#include "common/common_types.h"
@@ -77,6 +80,8 @@ class ContentProvider {
public:
virtual ~ContentProvider();
virtual bool IsUnionProvider() const { return false; }
virtual void Refresh() = 0;
virtual bool HasEntry(u64 title_id, ContentRecordType type) const = 0;
@@ -195,8 +200,9 @@ private:
std::function<T(const CNMT&, const ContentRecord&)> proc,
std::function<bool(const CNMT&, const ContentRecord&)> filter) const;
std::vector<NcaID> AccumulateFiles() const;
void ProcessFiles(const std::vector<NcaID>& ids);
void AccumulateCitronMeta();
void ProcessFiles(const std::vector<NcaID>& ids, std::map<u64, CNMT>& out_meta,
std::map<u64, NcaID>& out_meta_id) const;
void AccumulateCitronMeta(std::map<u64, CNMT>& out_citron_meta) const;
std::optional<NcaID> GetNcaIDFromMetadata(u64 title_id, ContentRecordType type) const;
VirtualFile GetFileAtID(NcaID id) const;
VirtualFile OpenFileOrDirectoryConcat(const VirtualDir& open_dir, std::string_view path) const;
@@ -230,7 +236,11 @@ class ContentProviderUnion : public ContentProvider {
public:
~ContentProviderUnion() override;
bool IsUnionProvider() const override { return true; }
void SetSlot(ContentProviderUnionSlot slot, ContentProvider* provider);
void SetSlots(
std::initializer_list<std::pair<ContentProviderUnionSlot, ContentProvider*>> slots);
void ClearSlot(ContentProviderUnionSlot slot);
void Refresh() override;
@@ -251,10 +261,15 @@ public:
std::optional<ContentProviderUnionSlot> GetSlotForEntry(u64 title_id,
ContentRecordType type) const;
VirtualFile GetExternalEntryForVersion(u64 title_id, ContentRecordType type,
u32 version) const;
std::vector<ExternalUpdateEntry> ListExternalUpdateVersions(u64 title_id) const;
const class ExternalContentProvider* GetExternalProvider() const;
const ContentProvider* GetSlotProvider(ContentProviderUnionSlot slot) const;
private:
mutable std::shared_mutex providers_mutex;
std::map<ContentProviderUnionSlot, ContentProvider*> providers;
};
+4
View File
@@ -195,6 +195,10 @@ void NSP::SetTicketKeys(const std::vector<VirtualFile>& files) {
LOG_WARNING(Common_Filesystem, "Could not load NSP ticket {}", ticket_file->GetName());
continue;
}
if (!keys.PersistTicket(ticket)) {
LOG_WARNING(Common_Filesystem, "Could not persist NSP ticket {}",
ticket_file->GetName());
}
}
}
@@ -58,6 +58,22 @@ static std::vector<u64> AccumulateAOCTitleIDs(Core::System& system) {
return add_on_content;
}
static std::vector<u64> GetAOCTitleIDsForBase(const std::vector<u64>& add_on_content, u64 base) {
std::vector<u64> out;
for (const auto title_id : add_on_content) {
if (CheckAOCTitleIDMatchesBase(title_id, base)) {
out.push_back(title_id);
}
}
std::sort(out.begin(), out.end());
return out;
}
static u32 GetGuestAOCIndex(std::size_t ordinal) {
return static_cast<u32>(ordinal + 1);
}
IAddOnContentManager::IAddOnContentManager(Core::System& system_)
: ServiceFramework{system_, "aoc:u"}, add_on_content{AccumulateAOCTitleIDs(system)},
service_context{system_, "aoc:u"} {
@@ -98,8 +114,8 @@ IAddOnContentManager::~IAddOnContentManager() {
}
Result IAddOnContentManager::CountAddOnContent(Out<u32> out_count, ClientProcessId process_id) {
LOG_DEBUG(Service_AOC, "called. process_id={}", process_id.pid);
// LOG_DEBUG(Service_AOC, "called. process_id={}", process_id.pid);
LOG_WARNING(Service_AOC, "CountAddOnContent called. process_id={}", process_id.pid);
const auto current = system.GetApplicationProcessProgramID();
const auto& disabled = Settings::values.disabled_addons[current];
@@ -108,10 +124,10 @@ Result IAddOnContentManager::CountAddOnContent(Out<u32> out_count, ClientProcess
R_SUCCEED();
}
*out_count = static_cast<u32>(
std::count_if(add_on_content.begin(), add_on_content.end(),
[current](u64 tid) { return CheckAOCTitleIDMatchesBase(tid, current); }));
*out_count = static_cast<u32>(GetAOCTitleIDsForBase(add_on_content, current).size());
LOG_WARNING(Service_AOC,
"CountAddOnContent: program_id={:016X}, count={}",
current, *out_count);
R_SUCCEED();
}
@@ -124,20 +140,16 @@ Result IAddOnContentManager::ListAddOnContent(Out<u32> out_count,
const auto current = FileSys::GetBaseTitleID(system.GetApplicationProcessProgramID());
std::vector<u32> out;
const auto matching_aocs = GetAOCTitleIDsForBase(add_on_content, current);
const auto& disabled = Settings::values.disabled_addons[current];
if (std::find(disabled.begin(), disabled.end(), "DLC") == disabled.end()) {
LOG_WARNING(Service_AOC, "Filtering AOCs for base title ID: {:016X}", current);
for (u64 content_id : add_on_content) {
const auto aoc_base = FileSys::GetBaseTitleID(content_id);
if (aoc_base != current) {
// LOG_WARNING(Service_AOC, "Skipping AOC {:016X} (Base: {:016X})", content_id,
// aoc_base);
continue;
}
LOG_WARNING(Service_AOC, "Match! AOC {:016X} belongs to current app. Adding to list.",
content_id);
out.push_back(static_cast<u32>(FileSys::GetAOCID(content_id)));
for (std::size_t i = 0; i < matching_aocs.size(); ++i) {
const auto guest_index = GetGuestAOCIndex(i);
LOG_WARNING(Service_AOC,
"Match! AOC {:016X} belongs to current app. Adding guest_index={}.",
matching_aocs[i], guest_index);
out.push_back(guest_index);
}
} else {
LOG_WARNING(Service_AOC, "DLCs are disabled for this title {:016X}", current);
@@ -146,7 +158,23 @@ Result IAddOnContentManager::ListAddOnContent(Out<u32> out_count,
// TODO(DarkLordZach): Find the correct error code.
R_UNLESS(out.size() >= offset, ResultUnknown);
*out_count = static_cast<u32>(std::min<size_t>(out.size() - offset, count));
const auto buffer_count = out_addons.size() / sizeof(u32);
*out_count = static_cast<u32>(std::min({out.size() - offset, static_cast<size_t>(count),
buffer_count}));
LOG_WARNING(Service_AOC,
"ListAddOnContent result: base_id={:016X}, matched_total={}, offset={}, "
"requested_count={}, returned_count={}, buffer_bytes={}",
current, out.size(), offset, count, *out_count, out_addons.size());
for (u32 i = 0; i < *out_count; ++i) {
const auto addon_index = out[offset + i];
const auto physical_title_id = matching_aocs[addon_index - 1];
LOG_WARNING(Service_AOC,
"ListAddOnContent output[{}]: addon_index={}, physical_title_id={:016X}",
i, addon_index, physical_title_id);
}
std::rotate(out.begin(), out.begin() + offset, out.end());
std::memcpy(out_addons.data(), out.data(), *out_count * sizeof(u32));
@@ -166,9 +194,7 @@ Result IAddOnContentManager::CountAddOnContentByApplicationId(Out<u32> out_count
R_SUCCEED();
}
*out_count = static_cast<u32>(
std::count_if(add_on_content.begin(), add_on_content.end(),
[current](u64 tid) { return CheckAOCTitleIDMatchesBase(tid, current); }));
*out_count = static_cast<u32>(GetAOCTitleIDsForBase(add_on_content, current).size());
R_SUCCEED();
}
@@ -182,18 +208,16 @@ Result IAddOnContentManager::ListAddOnContentByApplicationId(
const auto current = FileSys::GetBaseTitleID(application_id);
std::vector<u32> out;
const auto matching_aocs = GetAOCTitleIDsForBase(add_on_content, current);
const auto& disabled = Settings::values.disabled_addons[current];
if (std::find(disabled.begin(), disabled.end(), "DLC") == disabled.end()) {
LOG_WARNING(Service_AOC, "Filtering AOCs for base title ID: {:016X}", current);
for (u64 content_id : add_on_content) {
const auto aoc_base = FileSys::GetBaseTitleID(content_id);
if (aoc_base != current) {
continue;
}
LOG_WARNING(Service_AOC, "Match! AOC {:016X} belongs to current app. Adding to list.",
content_id);
out.push_back(static_cast<u32>(FileSys::GetAOCID(content_id)));
for (std::size_t i = 0; i < matching_aocs.size(); ++i) {
const auto guest_index = GetGuestAOCIndex(i);
LOG_WARNING(Service_AOC,
"Match! AOC {:016X} belongs to current app. Adding guest_index={}.",
matching_aocs[i], guest_index);
out.push_back(guest_index);
}
} else {
LOG_WARNING(Service_AOC, "DLCs are disabled for this title {:016X}", current);
@@ -202,7 +226,24 @@ Result IAddOnContentManager::ListAddOnContentByApplicationId(
// TODO(DarkLordZach): Find the correct error code.
R_UNLESS(out.size() >= offset, ResultUnknown);
*out_count = static_cast<u32>(std::min<size_t>(out.size() - offset, count));
const auto buffer_count = out_addons.size() / sizeof(u32);
*out_count = static_cast<u32>(std::min({out.size() - offset, static_cast<size_t>(count),
buffer_count}));
LOG_WARNING(Service_AOC,
"ListAddOnContentByApplicationId result: base_id={:016X}, matched_total={}, "
"offset={}, requested_count={}, returned_count={}, buffer_bytes={}",
current, out.size(), offset, count, *out_count, out_addons.size());
for (u32 i = 0; i < *out_count; ++i) {
const auto addon_index = out[offset + i];
const auto physical_title_id = matching_aocs[addon_index - 1];
LOG_WARNING(Service_AOC,
"ListAddOnContentByApplicationId output[{}]: addon_index={}, "
"physical_title_id={:016X}",
i, addon_index, physical_title_id);
}
std::rotate(out.begin(), out.begin() + offset, out.end());
std::memcpy(out_addons.data(), out.data(), *out_count * sizeof(u32));
@@ -212,8 +253,8 @@ Result IAddOnContentManager::ListAddOnContentByApplicationId(
Result IAddOnContentManager::GetAddOnContentBaseId(Out<u64> out_title_id,
ClientProcessId process_id) {
LOG_DEBUG(Service_AOC, "called. process_id={}", process_id.pid);
// LOG_DEBUG(Service_AOC, "called. process_id={}", process_id.pid);
LOG_WARNING(Service_AOC, "GetAddOnContentBaseId called. process_id={}", process_id.pid);
const auto title_id = system.GetApplicationProcessProgramID();
const FileSys::PatchManager pm{title_id, system.GetFileSystemController(),
system.GetContentProvider()};
@@ -221,16 +262,33 @@ Result IAddOnContentManager::GetAddOnContentBaseId(Out<u64> out_title_id,
const auto res = pm.GetControlMetadata();
if (res.first == nullptr) {
*out_title_id = FileSys::GetAOCBaseTitleID(title_id);
LOG_WARNING(Service_AOC,
"GetAddOnContentBaseId fallback: program_id={:016X}, aoc_base={:016X}",
title_id, *out_title_id);
R_SUCCEED();
}
*out_title_id = res.first->GetDLCBaseTitleId();
LOG_WARNING(Service_AOC,
"GetAddOnContentBaseId metadata: program_id={:016X}, aoc_base={:016X}",
title_id, *out_title_id);
R_SUCCEED();
}
Result IAddOnContentManager::PrepareAddOnContent(s32 addon_index, ClientProcessId process_id) {
LOG_WARNING(Service_AOC, "(STUBBED) called with addon_index={}, process_id={}", addon_index,
const auto program_id = system.GetApplicationProcessProgramID();
const auto aoc_base_id = FileSys::GetAOCBaseTitleID(program_id);
const auto matching_aocs = GetAOCTitleIDsForBase(add_on_content, program_id);
u64 physical_title_id = 0;
if (addon_index > 0 && static_cast<std::size_t>(addon_index) <= matching_aocs.size()) {
physical_title_id = matching_aocs[static_cast<std::size_t>(addon_index) - 1];
}
LOG_WARNING(Service_AOC,
"PrepareAddOnContent: program_id={:016X}, aoc_base={:016X}, "
"addon_index={}, physical_title_id={:016X}, process_id={}",
program_id, aoc_base_id, addon_index, physical_title_id,
process_id.pid);
R_SUCCEED();
+88 -27
View File
@@ -803,45 +803,84 @@ FileSys::VirtualDir FileSystemController::GetBCATDirectory(u64 title_id) const {
return bis_factory->GetBCATDirectory(title_id);
}
void FileSystemController::SetInitStage(InitStage stage) {
init_stage = stage;
}
InitStage FileSystemController::GetInitStage() const {
return init_stage;
}
void FileSystemController::InitializeContentSystem(FileSys::VfsFilesystem& vfs, bool overwrite) {
if (init_stage < InitStage::FS_READY) {
LOG_ERROR(Service_FS, "InitializeContentSystem called too early: init_stage={}",
static_cast<u8>(init_stage));
return;
}
CreateFactories(vfs, overwrite);
}
void FileSystemController::CreateFactories(FileSys::VfsFilesystem& vfs, bool overwrite) {
if (overwrite) {
bis_factory = nullptr;
sdmc_factory = nullptr;
external_provider = nullptr;
if (init_stage < InitStage::FS_READY) {
LOG_ERROR(Service_FS, "CreateFactories called too early: init_stage={}",
static_cast<u8>(init_stage));
return;
}
using CitronPath = Common::FS::CitronPath;
const auto sdmc_dir_path = Common::FS::GetCitronPath(CitronPath::SDMCDir);
const auto sdmc_load_dir_path = sdmc_dir_path / "atmosphere/contents";
const auto rw_mode = FileSys::OpenMode::ReadWrite;
const auto nand_dir_path = Common::FS::GetCitronPathString(CitronPath::NANDDir);
const auto load_dir_path = Common::FS::GetCitronPathString(CitronPath::LoadDir);
const auto dump_dir_path = Common::FS::GetCitronPathString(CitronPath::DumpDir);
auto nand_directory =
vfs.OpenDirectory(Common::FS::GetCitronPathString(CitronPath::NANDDir), rw_mode);
LOG_INFO(Service_FS, "CreateFactories: overwrite={}, NANDDir={}, SDMCDir={}, LoadDir={}",
overwrite, nand_dir_path, Common::FS::PathToUTF8String(sdmc_dir_path), load_dir_path);
auto nand_directory = vfs.OpenDirectory(nand_dir_path, rw_mode);
auto sd_directory = vfs.OpenDirectory(Common::FS::PathToUTF8String(sdmc_dir_path), rw_mode);
auto load_directory = vfs.OpenDirectory(Common::FS::GetCitronPathString(CitronPath::LoadDir),
FileSys::OpenMode::Read);
auto load_directory = vfs.OpenDirectory(load_dir_path, FileSys::OpenMode::Read);
auto sd_load_directory = vfs.OpenDirectory(Common::FS::PathToUTF8String(sdmc_load_dir_path),
FileSys::OpenMode::Read);
auto dump_directory =
vfs.OpenDirectory(Common::FS::GetCitronPathString(CitronPath::DumpDir), rw_mode);
auto dump_directory = vfs.OpenDirectory(dump_dir_path, rw_mode);
if (bis_factory == nullptr) {
bis_factory = std::make_unique<FileSys::BISFactory>(
LOG_INFO(Service_FS,
"CreateFactories: opened NAND={}, SDMC={}, Load={}, Dump={}",
nand_directory ? nand_directory->GetFullPath() : "<null>",
sd_directory ? sd_directory->GetFullPath() : "<null>",
load_directory ? load_directory->GetFullPath() : "<null>",
dump_directory ? dump_directory->GetFullPath() : "<null>");
std::unique_ptr<FileSys::BISFactory> new_bis_factory;
auto* active_bis_factory = bis_factory.get();
if (overwrite || active_bis_factory == nullptr) {
new_bis_factory = std::make_unique<FileSys::BISFactory>(
nand_directory, std::move(load_directory), std::move(dump_directory));
system.RegisterContentProvider(FileSys::ContentProviderUnionSlot::SysNAND,
bis_factory->GetSystemNANDContents());
system.RegisterContentProvider(FileSys::ContentProviderUnionSlot::UserNAND,
bis_factory->GetUserNANDContents());
active_bis_factory = new_bis_factory.get();
}
if (sdmc_factory == nullptr) {
sdmc_factory = std::make_unique<FileSys::SDMCFactory>(std::move(sd_directory),
std::move(sd_load_directory));
system.RegisterContentProvider(FileSys::ContentProviderUnionSlot::SDMC,
sdmc_factory->GetSDMCContents());
if (active_bis_factory != nullptr) {
const auto* sysnand = active_bis_factory->GetSystemNANDContents();
const auto* usernand = active_bis_factory->GetUserNANDContents();
LOG_INFO(Service_FS,
"CreateFactories: prepared NAND providers, sys_entries={}, user_entries={}",
sysnand ? sysnand->ListEntriesFilter().size() : 0,
usernand ? usernand->ListEntriesFilter().size() : 0);
}
if (external_provider == nullptr) {
std::unique_ptr<FileSys::SDMCFactory> new_sdmc_factory;
auto* active_sdmc_factory = sdmc_factory.get();
if (overwrite || active_sdmc_factory == nullptr) {
new_sdmc_factory = std::make_unique<FileSys::SDMCFactory>(std::move(sd_directory),
std::move(sd_load_directory));
active_sdmc_factory = new_sdmc_factory.get();
}
std::unique_ptr<FileSys::ExternalContentProvider> new_external_provider;
auto* active_external_provider = external_provider.get();
if (overwrite || active_external_provider == nullptr) {
std::vector<FileSys::VirtualDir> load_dirs;
for (const auto& path : Settings::values.external_content_dirs) {
auto dir = vfs.OpenDirectory(path, FileSys::OpenMode::Read);
@@ -849,16 +888,36 @@ void FileSystemController::CreateFactories(FileSys::VfsFilesystem& vfs, bool ove
load_dirs.push_back(std::move(dir));
}
}
external_provider =
new_external_provider =
std::make_unique<FileSys::ExternalContentProvider>(std::move(load_dirs));
system.RegisterContentProvider(FileSys::ContentProviderUnionSlot::External,
external_provider.get());
active_external_provider = new_external_provider.get();
}
system.GetContentProviderUnion().SetSlots(
{{FileSys::ContentProviderUnionSlot::SysNAND,
active_bis_factory != nullptr ? active_bis_factory->GetSystemNANDContents() : nullptr},
{FileSys::ContentProviderUnionSlot::UserNAND,
active_bis_factory != nullptr ? active_bis_factory->GetUserNANDContents() : nullptr},
{FileSys::ContentProviderUnionSlot::SDMC,
active_sdmc_factory != nullptr ? active_sdmc_factory->GetSDMCContents() : nullptr},
{FileSys::ContentProviderUnionSlot::External, active_external_provider}});
if (new_bis_factory != nullptr) {
bis_factory = std::move(new_bis_factory);
}
if (new_sdmc_factory != nullptr) {
sdmc_factory = std::move(new_sdmc_factory);
}
if (new_external_provider != nullptr) {
external_provider = std::move(new_external_provider);
}
// factory that handles sync tasks before a game is even selected
if (global_save_data_factory == nullptr || overwrite) {
global_save_data_factory = CreateSaveDataFactory(ProgramId{});
}
init_stage = InitStage::CONTENT_READY;
}
void FileSystemController::RefreshExternalContentProvider() {
@@ -871,9 +930,11 @@ void FileSystemController::RefreshExternalContentProvider() {
}
}
external_provider = std::make_unique<FileSys::ExternalContentProvider>(std::move(load_dirs));
auto new_external_provider =
std::make_unique<FileSys::ExternalContentProvider>(std::move(load_dirs));
system.RegisterContentProvider(FileSys::ContentProviderUnionSlot::External,
external_provider.get());
new_external_provider.get());
external_provider = std::move(new_external_provider);
}
void FileSystemController::Reset() {
+19 -3
View File
@@ -68,6 +68,13 @@ enum class ImageDirectoryId : u32 {
SdCard,
};
enum class InitStage : u8 {
NONE,
SETTINGS_READY,
FS_READY,
CONTENT_READY,
};
using ProcessId = u64;
using ProgramId = u64;
@@ -130,9 +137,11 @@ public:
void RefreshExternalContentProvider();
// Creates the SaveData, SDMC, and BIS Factories. Should be called once and before any function
// above is called.
void CreateFactories(FileSys::VfsFilesystem& vfs, bool overwrite = true);
// Single public entry for creating the SaveData, SDMC, BIS, and content providers.
// Should be called once and before any function above is called.
void InitializeContentSystem(FileSys::VfsFilesystem& vfs, bool overwrite = true);
void SetInitStage(InitStage stage);
InitStage GetInitStage() const;
// getter for main.cpp to trigger the sync between custom game paths for separate emulators
FileSys::SaveDataFactory& GetSaveDataFactory() {
@@ -143,6 +152,7 @@ public:
private:
std::shared_ptr<FileSys::SaveDataFactory> CreateSaveDataFactory(ProgramId program_id);
void CreateFactories(FileSys::VfsFilesystem& vfs, bool overwrite);
struct Registration {
ProgramId program_id;
@@ -165,6 +175,12 @@ private:
// Global factory for startup tasks and mirroring
std::shared_ptr<FileSys::SaveDataFactory> global_save_data_factory;
#ifdef ANDROID
InitStage init_stage{InitStage::NONE};
#else
InitStage init_stage{InitStage::FS_READY};
#endif
Core::System& system;
};
@@ -2,6 +2,7 @@
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <cinttypes>
#include <cstring>
#include <iterator>
@@ -16,12 +17,14 @@
#include "common/settings.h"
#include "common/string_util.h"
#include "core/core.h"
#include "core/file_sys/common_funcs.h"
#include "core/file_sys/content_archive.h"
#include "core/file_sys/errors.h"
#include "core/file_sys/fs_directory.h"
#include "core/file_sys/fs_filesystem.h"
#include "core/file_sys/nca_metadata.h"
#include "core/file_sys/patch_manager.h"
#include "core/file_sys/registered_cache.h"
#include "core/file_sys/romfs.h"
#include "core/file_sys/romfs_factory.h"
#include "core/file_sys/savedata_factory.h"
@@ -49,6 +52,21 @@
namespace Service::FileSystem {
static std::vector<u64> GetAOCPhysicalTitleIDsForBase(const FileSys::ContentProvider& content_provider,
u64 base) {
std::vector<u64> out;
const auto entries =
content_provider.ListEntriesFilter(FileSys::TitleType::AOC, FileSys::ContentRecordType::Data);
for (const auto& entry : entries) {
if (FileSys::GetBaseTitleID(entry.title_id) == base) {
out.push_back(entry.title_id);
}
}
std::sort(out.begin(), out.end());
return out;
}
FSP_SRV::FSP_SRV(Core::System& system_)
: ServiceFramework{system_, "fsp-srv"}, fsc{system.GetFileSystemController()},
content_provider{system.GetContentProvider()}, reporter{system.GetReporter()} {
@@ -490,10 +508,35 @@ Result FSP_SRV::OpenDataStorageByCurrentProcess(OutInterface<IStorage> out_inter
Result FSP_SRV::OpenDataStorageByDataId(OutInterface<IStorage> out_interface,
FileSys::StorageId storage_id, u32 unknown, u64 title_id) {
LOG_DEBUG(Service_FS, "called with storage_id={:02X}, unknown={:08X}, title_id={:016X}",
storage_id, unknown, title_id);
LOG_WARNING(Service_FS,
"OpenDataStorageByDataId called with storage_id={:02X}, unknown={:08X}, "
"title_id={:016X}",
storage_id, unknown, title_id);
auto data = romfs_controller->OpenRomFS(title_id, storage_id, FileSys::ContentRecordType::Data);
u64 resolved_title_id = title_id;
const auto requested_base = FileSys::GetBaseTitleID(title_id);
const auto requested_aoc_base = FileSys::GetAOCBaseTitleID(title_id);
const auto requested_aoc_id = FileSys::GetAOCID(title_id);
const auto requested_is_aoc =
requested_aoc_id > 0 && title_id >= requested_aoc_base &&
title_id <= requested_aoc_base + FileSys::AOC_TITLE_ID_MASK;
std::vector<u64> matching_aocs;
if (requested_is_aoc) {
matching_aocs = GetAOCPhysicalTitleIDsForBase(content_provider, requested_base);
if (requested_aoc_id <= matching_aocs.size()) {
resolved_title_id = matching_aocs[static_cast<std::size_t>(requested_aoc_id) - 1];
if (resolved_title_id != title_id) {
LOG_WARNING(Service_FS,
"OpenDataStorageByDataId resolved AOC guest index: "
"requested_title_id={:016X}, guest_index={}, physical_title_id={:016X}",
title_id, requested_aoc_id, resolved_title_id);
}
}
}
auto data =
romfs_controller->OpenRomFS(resolved_title_id, storage_id, FileSys::ContentRecordType::Data);
if (!data) {
const auto archive = FileSys::SystemArchive::SynthesizeSystemArchive(title_id);
@@ -503,16 +546,43 @@ Result FSP_SRV::OpenDataStorageByDataId(OutInterface<IStorage> out_interface,
R_SUCCEED();
}
if (requested_is_aoc) {
std::size_t matching_aoc_count = 0;
for (const auto entry_title_id : matching_aocs) {
++matching_aoc_count;
LOG_WARNING(Service_FS,
"OpenDataStorageByDataId miss context: installed_aoc_index={}, "
"installed_title_id={:016X}",
matching_aoc_count, entry_title_id);
}
LOG_WARNING(Service_FS,
"OpenDataStorageByDataId AOC miss: requested_base={:016X}, "
"requested_aoc_base={:016X}, requested_aoc_index={}, "
"resolved_title_id={:016X}, matching_installed_aoc_count={}",
requested_base, requested_aoc_base, requested_aoc_id, resolved_title_id,
matching_aoc_count);
}
LOG_ERROR(Service_FS,
"Could not open data storage with title_id={:016X}, storage_id={:02X}", title_id,
storage_id);
R_RETURN(FileSys::ResultTargetNotFound);
}
const FileSys::PatchManager pm{title_id, fsc, content_provider};
const auto opened_aoc_base = FileSys::GetAOCBaseTitleID(title_id);
if (title_id >= opened_aoc_base && title_id <= opened_aoc_base + FileSys::AOC_TITLE_ID_MASK) {
LOG_WARNING(Service_FS,
"OpenDataStorageByDataId opened AOC: base={:016X}, addon_index={}, "
"requested_title_id={:016X}, physical_title_id={:016X}",
FileSys::GetBaseTitleID(title_id), FileSys::GetAOCID(title_id), title_id,
resolved_title_id);
}
const FileSys::PatchManager pm{resolved_title_id, fsc, content_provider};
auto base =
romfs_controller->OpenBaseNca(title_id, storage_id, FileSys::ContentRecordType::Data);
romfs_controller->OpenBaseNca(resolved_title_id, storage_id, FileSys::ContentRecordType::Data);
auto storage = std::make_shared<IStorage>(
system, pm.PatchRomFS(base.get(), std::move(data), FileSys::ContentRecordType::Data));
+1 -1
View File
@@ -71,7 +71,7 @@ Services::Services(std::shared_ptr<SM::ServiceManager>& sm, Core::System& system
std::stop_token token) {
auto& kernel = system.Kernel();
system.GetFileSystemController().CreateFactories(*system.GetFilesystem(), false);
system.GetFileSystemController().InitializeContentSystem(*system.GetFilesystem(), false);
// clang-format off
kernel.RunOnHostCoreProcess("audio", [&] { Audio::LoopProcess(system); }).detach();