diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 1d5dc8f4d..cabc7493e 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -243,6 +243,9 @@ add_library(skyline SHARED ${source_DIR}/skyline/vfs/npdm.cpp ${source_DIR}/skyline/vfs/nca.cpp ${source_DIR}/skyline/vfs/ticket.cpp + ${source_DIR}/skyline/vfs/cnmt.cpp + ${source_DIR}/skyline/vfs/bktr.cpp + ${source_DIR}/skyline/vfs/patch_manager.cpp ${source_DIR}/skyline/services/serviceman.cpp ${source_DIR}/skyline/services/base_service.cpp ${source_DIR}/skyline/services/sm/IUserInterface.cpp diff --git a/app/src/main/cpp/emu_jni.cpp b/app/src/main/cpp/emu_jni.cpp index be7d0beec..f02329993 100644 --- a/app/src/main/cpp/emu_jni.cpp +++ b/app/src/main/cpp/emu_jni.cpp @@ -61,6 +61,8 @@ extern "C" JNIEXPORT void Java_org_stratoemu_strato_EmulationActivity_executeApp jstring romUriJstring, jint romType, jint romFd, + jintArray dlcFds, + jint updateFd, jobject settingsInstance, jstring publicAppFilesPathJstring, jstring privateAppFilesPathJstring, @@ -75,6 +77,12 @@ extern "C" JNIEXPORT void Java_org_stratoemu_strato_EmulationActivity_executeApp auto jvmManager{std::make_shared(env, instance)}; + jsize dlcArrSize = dlcFds != nullptr ? env->GetArrayLength(dlcFds) : 0; + std::vector dlcFdsVector(dlcArrSize); + + if (dlcArrSize > 0) + env->GetIntArrayRegion(dlcFds, 0, dlcArrSize, &dlcFdsVector[0]); + std::shared_ptr settings{std::make_shared(env, settingsInstance)}; skyline::JniString publicAppFilesPath(env, publicAppFilesPathJstring); @@ -114,7 +122,7 @@ extern "C" JNIEXPORT void Java_org_stratoemu_strato_EmulationActivity_executeApp LOGDNF("Launching ROM {}", skyline::JniString(env, romUriJstring)); - os->Execute(romFd, static_cast(romType)); + os->Execute(romFd, dlcFdsVector, updateFd, static_cast(romType)); } catch (std::exception &e) { LOGENF("An uncaught exception has occurred: {}", e.what()); } catch (const skyline::signal::SignalException &e) { @@ -132,6 +140,13 @@ extern "C" JNIEXPORT void Java_org_stratoemu_strato_EmulationActivity_executeApp skyline::AsyncLogger::Finalize(true); close(romFd); + + close(updateFd); + + if (dlcArrSize > 0) + for (int i = 0; i < dlcArrSize; i++) + close(env->GetIntArrayElements(dlcFds, nullptr)[i]); + } extern "C" JNIEXPORT jboolean Java_org_stratoemu_strato_EmulationActivity_stopEmulation(JNIEnv *, jobject, jboolean join) { diff --git a/app/src/main/cpp/loader_jni.cpp b/app/src/main/cpp/loader_jni.cpp index 8a8922d91..bed333533 100644 --- a/app/src/main/cpp/loader_jni.cpp +++ b/app/src/main/cpp/loader_jni.cpp @@ -5,6 +5,7 @@ #include "skyline/vfs/nca.h" #include "skyline/vfs/os_backing.h" #include "skyline/vfs/os_filesystem.h" +#include "skyline/vfs/cnmt.h" #include "skyline/loader/nro.h" #include "skyline/loader/nso.h" #include "skyline/loader/nca.h" @@ -50,9 +51,12 @@ extern "C" JNIEXPORT jint JNICALL Java_org_stratoemu_strato_loader_RomFile_popul jclass clazz{env->GetObjectClass(thiz)}; jfieldID applicationNameField{env->GetFieldID(clazz, "applicationName", "Ljava/lang/String;")}; jfieldID applicationTitleIdField{env->GetFieldID(clazz, "applicationTitleId", "Ljava/lang/String;")}; + jfieldID addOnContentBaseIdField{env->GetFieldID(clazz, "addOnContentBaseId", "Ljava/lang/String;")}; jfieldID applicationAuthorField{env->GetFieldID(clazz, "applicationAuthor", "Ljava/lang/String;")}; jfieldID rawIconField{env->GetFieldID(clazz, "rawIcon", "[B")}; jfieldID applicationVersionField{env->GetFieldID(clazz, "applicationVersion", "Ljava/lang/String;")}; + jfieldID romType{env->GetFieldID(clazz, "romTypeInt", "I")}; + jfieldID parentTitleId{env->GetFieldID(clazz, "parentTitleId", "Ljava/lang/String;")}; if (loader->nacp) { auto language{skyline::language::GetApplicationLanguage(static_cast(systemLanguage))}; @@ -62,6 +66,7 @@ extern "C" JNIEXPORT jint JNICALL Java_org_stratoemu_strato_loader_RomFile_popul env->SetObjectField(thiz, applicationNameField, env->NewStringUTF(loader->nacp->GetApplicationName(language).c_str())); env->SetObjectField(thiz, applicationVersionField, env->NewStringUTF(loader->nacp->GetApplicationVersion().c_str())); env->SetObjectField(thiz, applicationTitleIdField, env->NewStringUTF(loader->nacp->GetSaveDataOwnerId().c_str())); + env->SetObjectField(thiz, addOnContentBaseIdField, env->NewStringUTF(loader->nacp->GetAddOnContentBaseId().c_str())); env->SetObjectField(thiz, applicationAuthorField, env->NewStringUTF(loader->nacp->GetApplicationPublisher(language).c_str())); auto icon{loader->GetIcon(language)}; @@ -70,6 +75,14 @@ extern "C" JNIEXPORT jint JNICALL Java_org_stratoemu_strato_loader_RomFile_popul env->SetObjectField(thiz, rawIconField, iconByteArray); } + if (loader->cnmt) { + auto contentMetaType{loader->cnmt->GetContentMetaType()}; + env->SetIntField(thiz, romType, static_cast(contentMetaType)); + + if (contentMetaType != skyline::vfs::ContentMetaType::Application) + env->SetObjectField(thiz, parentTitleId, env->NewStringUTF(loader->cnmt->GetParentTitleId().c_str())); + } + return static_cast(skyline::loader::LoaderResult::Success); } @@ -98,7 +111,7 @@ extern "C" JNIEXPORT jstring Java_org_stratoemu_strato_preference_FirmwareImport std::shared_ptr backing{systemArchivesFileSystem->OpenFile(entry.name)}; auto nca{skyline::vfs::NCA(backing, keyStore)}; - if (nca.header.programId == systemVersionProgramId && nca.romFs != nullptr) { + if (nca.header.titleId == systemVersionProgramId && nca.romFs != nullptr) { auto controlRomFs{std::make_shared(nca.romFs)}; auto file{controlRomFs->OpenFile("file")}; SystemVersion systemVersion; @@ -165,7 +178,7 @@ extern "C" JNIEXPORT void Java_org_stratoemu_strato_preference_FirmwareImportPre std::shared_ptr backing{systemArchivesFileSystem->OpenFile(entry.name)}; auto nca{skyline::vfs::NCA(backing, keyStore)}; - if (nca.header.programId >= firstFontProgramId && nca.header.programId <= lastFontProgramId && nca.romFs != nullptr) { + if (nca.header.titleId >= firstFontProgramId && nca.header.titleId <= lastFontProgramId && nca.romFs != nullptr) { auto controlRomFs{std::make_shared(nca.romFs)}; for (auto fileEntry = controlRomFs->fileMap.begin(); fileEntry != controlRomFs->fileMap.end(); fileEntry++) { diff --git a/app/src/main/cpp/skyline/common.h b/app/src/main/cpp/skyline/common.h index b670e01a6..25149be51 100644 --- a/app/src/main/cpp/skyline/common.h +++ b/app/src/main/cpp/skyline/common.h @@ -65,6 +65,8 @@ namespace skyline { std::shared_ptr jvm; std::shared_ptr settings; std::shared_ptr loader; + std::vector> dlcLoaders; + std::shared_ptr updateLoader; std::shared_ptr nce; std::shared_ptr process{}; static thread_local inline std::shared_ptr thread{}; //!< The KThread of the thread which accesses this object diff --git a/app/src/main/cpp/skyline/loader/loader.h b/app/src/main/cpp/skyline/loader/loader.h index 1ef1bea9f..9e9528607 100644 --- a/app/src/main/cpp/skyline/loader/loader.h +++ b/app/src/main/cpp/skyline/loader/loader.h @@ -5,6 +5,8 @@ #include #include +#include +#include #include #include "executable.h" @@ -32,6 +34,9 @@ namespace skyline::loader { MissingTitleKey, MissingTitleKek, MissingKeyArea, + + ErrorSparseNCA, + ErrorCompressedNCA, }; /** @@ -85,6 +90,10 @@ namespace skyline::loader { ExecutableLoadInfo LoadExecutable(const std::shared_ptr &process, const DeviceState &state, Executable &executable, size_t offset = 0, const std::string &name = {}, bool dynamicallyLinked = false); std::optional nacp; + std::optional cnmt; + std::optional programNca; //!< The main program NCA within the NSP + std::optional controlNca; //!< The main control NCA within the NSP + std::optional publicNca; std::shared_ptr romFs; virtual ~Loader() = default; diff --git a/app/src/main/cpp/skyline/loader/nsp.cpp b/app/src/main/cpp/skyline/loader/nsp.cpp index 6294dc918..d54e6102a 100644 --- a/app/src/main/cpp/skyline/loader/nsp.cpp +++ b/app/src/main/cpp/skyline/loader/nsp.cpp @@ -5,6 +5,7 @@ #include #include "nca.h" #include "nsp.h" +#include "vfs/patch_manager.h" namespace skyline::loader { static void ExtractTickets(const std::shared_ptr& dir, const std::shared_ptr &keyStore) { @@ -33,10 +34,14 @@ namespace skyline::loader { try { auto nca{vfs::NCA(nsp->OpenFile(entry.name), keyStore)}; - if (nca.contentType == vfs::NcaContentType::Program && nca.romFs != nullptr && nca.exeFs != nullptr) + if (nca.contentType == vfs::NCAContentType::Program && nca.romFs != nullptr && nca.exeFs != nullptr) programNca = std::move(nca); - else if (nca.contentType == vfs::NcaContentType::Control && nca.romFs != nullptr) + else if (nca.contentType == vfs::NCAContentType::Control && nca.romFs != nullptr) controlNca = std::move(nca); + else if (nca.contentType == vfs::NCAContentType::Meta) + metaNca = std::move(nca); + else if (nca.contentType == vfs::NCAContentType::PublicData) + publicNca = std::move(nca); } catch (const loader_exception &e) { throw loader_exception(e.error); } catch (const std::exception &e) { @@ -44,15 +49,24 @@ namespace skyline::loader { } } - if (!programNca || !controlNca) - throw exception("Incomplete NSP file"); + if (programNca) + romFs = programNca->romFs; - romFs = programNca->romFs; - controlRomFs = std::make_shared(controlNca->romFs); - nacp.emplace(controlRomFs->OpenFile("control.nacp")); + if (controlNca) { + controlRomFs = std::make_shared(controlNca->romFs); + nacp.emplace(controlRomFs->OpenFile("control.nacp")); + } + + if (metaNca) + cnmt = vfs::CNMT(metaNca->cnmt); } void *NspLoader::LoadProcessData(const std::shared_ptr &process, const DeviceState &state) { + if (state.updateLoader) { + auto patchManager{std::make_shared()}; + programNca->exeFs = patchManager->PatchExeFS(state, programNca->exeFs); + } + process->npdm = vfs::NPDM(programNca->exeFs->OpenFile("main.npdm")); return NcaLoader::LoadExeFs(this, programNca->exeFs, process, state); } diff --git a/app/src/main/cpp/skyline/loader/nsp.h b/app/src/main/cpp/skyline/loader/nsp.h index 3eae1065b..72de16675 100644 --- a/app/src/main/cpp/skyline/loader/nsp.h +++ b/app/src/main/cpp/skyline/loader/nsp.h @@ -18,8 +18,7 @@ namespace skyline::loader { private: std::shared_ptr nsp; //!< A shared pointer to the NSP's PFS0 std::shared_ptr controlRomFs; //!< A shared pointer to the control NCA's RomFS - std::optional programNca; //!< The main program NCA within the NSP - std::optional controlNca; //!< The main control NCA within the NSP + std::optional metaNca; //!< The main meta NCA within the NSP public: NspLoader(const std::shared_ptr &backing, const std::shared_ptr &keyStore); diff --git a/app/src/main/cpp/skyline/loader/xci.cpp b/app/src/main/cpp/skyline/loader/xci.cpp index 158afae46..3600d55df 100644 --- a/app/src/main/cpp/skyline/loader/xci.cpp +++ b/app/src/main/cpp/skyline/loader/xci.cpp @@ -38,10 +38,12 @@ namespace skyline::loader { try { auto nca{vfs::NCA(secure->OpenFile(entry.name), keyStore, true)}; - if (nca.contentType == vfs::NcaContentType::Program && nca.romFs != nullptr && nca.exeFs != nullptr) + if (nca.contentType == vfs::NCAContentType::Program && nca.romFs != nullptr && nca.exeFs != nullptr) programNca = std::move(nca); - else if (nca.contentType == vfs::NcaContentType::Control && nca.romFs != nullptr) + else if (nca.contentType == vfs::NCAContentType::Control && nca.romFs != nullptr) controlNca = std::move(nca); + else if (nca.contentType == vfs::NCAContentType::Meta) + metaNca = std::move(nca); } catch (const loader_exception &e) { throw loader_exception(e.error); } catch (const std::exception &e) { @@ -52,12 +54,16 @@ namespace skyline::loader { throw exception("Corrupted secure partition"); } - if (!programNca || !controlNca) - throw exception("Incomplete XCI file"); + if (programNca) + romFs = programNca->romFs; - romFs = programNca->romFs; - controlRomFs = std::make_shared(controlNca->romFs); - nacp.emplace(controlRomFs->OpenFile("control.nacp")); + if (controlNca) { + controlRomFs = std::make_shared(controlNca->romFs); + nacp.emplace(controlRomFs->OpenFile("control.nacp")); + } + + if (metaNca) + cnmt = vfs::CNMT(metaNca->cnmt); } void *XciLoader::LoadProcessData(const std::shared_ptr &process, const DeviceState &state) { diff --git a/app/src/main/cpp/skyline/loader/xci.h b/app/src/main/cpp/skyline/loader/xci.h index 28b5fb8ed..38285ea78 100644 --- a/app/src/main/cpp/skyline/loader/xci.h +++ b/app/src/main/cpp/skyline/loader/xci.h @@ -113,6 +113,7 @@ namespace skyline::loader { std::shared_ptr controlRomFs; //!< A shared pointer to the control NCA's RomFS std::optional programNca; //!< The main program NCA within the secure partition std::optional controlNca; //!< The main control NCA within the secure partition + std::optional metaNca; //!< The main meta NCA within the secure partition public: XciLoader(const std::shared_ptr &backing, const std::shared_ptr &keyStore); diff --git a/app/src/main/cpp/skyline/os.cpp b/app/src/main/cpp/skyline/os.cpp index 68b1d11de..e0c7d28cc 100644 --- a/app/src/main/cpp/skyline/os.cpp +++ b/app/src/main/cpp/skyline/os.cpp @@ -30,26 +30,18 @@ namespace skyline::kernel { state(this, jvmManager, settings), serviceManager(state) {} - void OS::Execute(int romFd, loader::RomFormat romType) { + void OS::Execute(int romFd, std::vector dlcFds, int updateFd, loader::RomFormat romType) { auto romFile{std::make_shared(romFd)}; - auto keyStore{std::make_shared(privateAppFilesPath + "keys/")}; + keyStore = std::make_shared(privateAppFilesPath + "keys/"); - state.loader = [&]() -> std::shared_ptr { - switch (romType) { - case loader::RomFormat::NRO: - return std::make_shared(std::move(romFile)); - case loader::RomFormat::NSO: - return std::make_shared(std::move(romFile)); - case loader::RomFormat::NCA: - return std::make_shared(std::move(romFile), std::move(keyStore)); - case loader::RomFormat::NSP: - return std::make_shared(romFile, keyStore); - case loader::RomFormat::XCI: - return std::make_shared(romFile, keyStore); - default: - throw exception("Unsupported ROM extension."); - } - }(); + state.loader = GetLoader(romFd, keyStore, romType); + + if (updateFd > 0) + state.updateLoader = GetLoader(updateFd, keyStore, romType); + + if (dlcFds.size() > 0) + for (int fd : dlcFds) + state.dlcLoaders.push_back(GetLoader(fd, keyStore, romType)); state.gpu->Initialise(); @@ -64,7 +56,15 @@ namespace skyline::kernel { name = nacp->GetApplicationName(nacp->GetFirstSupportedTitleLanguage()); if (publisher.empty()) publisher = nacp->GetApplicationPublisher(nacp->GetFirstSupportedTitleLanguage()); - LOGINF(R"(Starting "{}" ({}) v{} by "{}")", name, nacp->GetSaveDataOwnerId(), nacp->GetApplicationVersion(), publisher); + + if (state.updateLoader) + LOGINF("Applied update v{}", state.updateLoader->nacp->GetApplicationVersion()); + + if (state.dlcLoaders.size() > 0) + for (auto &loader : state.dlcLoaders) + LOGINF("Applied DLC {}", loader->cnmt->GetTitleId()); + + LOGINF(R"(Starting "{}" ({}) v{} by "{}")", name, nacp->GetSaveDataOwnerId(), state.updateLoader ? state.updateLoader->nacp->GetApplicationVersion() : nacp->GetApplicationVersion(), publisher); } process->InitializeHeapTls(); @@ -75,4 +75,22 @@ namespace skyline::kernel { process->Kill(true, true, true); } } + + std::shared_ptr OS::GetLoader(int fd, std::shared_ptr keyStore, loader::RomFormat romType) { + auto file{std::make_shared(fd)}; + switch (romType) { + case loader::RomFormat::NRO: + return std::make_shared(std::move(file)); + case loader::RomFormat::NSO: + return std::make_shared(std::move(file)); + case loader::RomFormat::NCA: + return std::make_shared(std::move(file), std::move(keyStore)); + case loader::RomFormat::NSP: + return std::make_shared(file, keyStore); + case loader::RomFormat::XCI: + return std::make_shared(file, keyStore); + default: + throw exception("Unsupported ROM extension."); + } + } } diff --git a/app/src/main/cpp/skyline/os.h b/app/src/main/cpp/skyline/os.h index b46684d41..20273537d 100644 --- a/app/src/main/cpp/skyline/os.h +++ b/app/src/main/cpp/skyline/os.h @@ -3,6 +3,7 @@ #pragma once +#include #include #include "vfs/filesystem.h" #include "loader/loader.h" @@ -19,6 +20,7 @@ namespace skyline::kernel { std::string privateAppFilesPath; //!< The full path to the app's private files directory std::string deviceTimeZone; //!< The timezone name (e.g. Europe/London) std::shared_ptr assetFileSystem; //!< A filesystem to be used for accessing emulator assets (like tzdata) + std::shared_ptr keyStore; DeviceState state; service::ServiceManager serviceManager; @@ -39,8 +41,12 @@ namespace skyline::kernel { /** * @brief Execute a particular ROM file * @param romFd A FD to the ROM file to execute + * @param dlcFds An array of FD to the DLC files + * @param updateFd A FD to the Update file * @param romType The type of the ROM file */ - void Execute(int romFd, loader::RomFormat romType); + void Execute(int romFd, std::vector dlcFds, int updateFd, loader::RomFormat romType); + + std::shared_ptr GetLoader(int fd, std::shared_ptr keyStore, loader::RomFormat romType); }; } diff --git a/app/src/main/cpp/skyline/services/aocsrv/IAddOnContentManager.cpp b/app/src/main/cpp/skyline/services/aocsrv/IAddOnContentManager.cpp index 9e9b063e7..08e492bd0 100644 --- a/app/src/main/cpp/skyline/services/aocsrv/IAddOnContentManager.cpp +++ b/app/src/main/cpp/skyline/services/aocsrv/IAddOnContentManager.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MPL-2.0 // Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/) +#include #include #include "IAddOnContentManager.h" #include "IPurchaseEventManager.h" @@ -11,12 +12,42 @@ namespace skyline::service::aocsrv { addOnContentListChangedEvent(std::make_shared(state, false)) {} Result IAddOnContentManager::CountAddOnContent(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response) { - response.Push(0); + response.Push(static_cast(state.dlcLoaders.size())); return {}; } Result IAddOnContentManager::ListAddOnContent(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response) { - response.Push(0); + struct Parameters { + u32 offset; + u32 count; + u64 processId; + }; + auto params{request.Pop()}; + + std::vector out; + std::vector aocTitleIds; + + for (u32 i = 0; i < state.dlcLoaders.size(); i++) + aocTitleIds.push_back(state.dlcLoaders[i]->cnmt->header.id); + + for (u64 contentId : aocTitleIds) + out.push_back(static_cast(contentId & constant::AOCTitleIdMask)); + + const auto outCount{static_cast(std::min(out.size() - params.offset, params.count))}; + std::rotate(out.begin(), out.begin() + params.offset, out.end()); + out.resize(outCount); + + request.outputBuf.at(0).copy_from(out); + response.Push(outCount); + return {}; + } + + Result IAddOnContentManager::GetAddOnContentBaseId(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response) { + response.Push(state.loader->nacp->nacpContents.addOnContentBaseId); + return {}; + } + + Result IAddOnContentManager::PrepareAddOnContent(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response) { return {}; } diff --git a/app/src/main/cpp/skyline/services/aocsrv/IAddOnContentManager.h b/app/src/main/cpp/skyline/services/aocsrv/IAddOnContentManager.h index c3d1acfb4..6ac4bf864 100644 --- a/app/src/main/cpp/skyline/services/aocsrv/IAddOnContentManager.h +++ b/app/src/main/cpp/skyline/services/aocsrv/IAddOnContentManager.h @@ -7,6 +7,11 @@ #include namespace skyline::service::aocsrv { + namespace constant { + constexpr u64 AOCTitleIdMask{0x7FF}; + + } + /** * @brief IAddOnContentManager or aoc:u is used by applications to access add-on content information * @url https://switchbrew.org/wiki/NS_Services#aoc:u @@ -22,6 +27,10 @@ namespace skyline::service::aocsrv { Result ListAddOnContent(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response); + Result GetAddOnContentBaseId(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response); + + Result PrepareAddOnContent(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response); + Result GetAddOnContentListChangedEvent(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response); Result GetAddOnContentListChangedEventWithProcessId(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response); @@ -33,6 +42,8 @@ namespace skyline::service::aocsrv { SERVICE_DECL( SFUNC(0x2, IAddOnContentManager, CountAddOnContent), SFUNC(0x3, IAddOnContentManager, ListAddOnContent), + SFUNC(0x5, IAddOnContentManager, GetAddOnContentBaseId), + SFUNC(0x7, IAddOnContentManager, PrepareAddOnContent), SFUNC(0x8, IAddOnContentManager, GetAddOnContentListChangedEvent), SFUNC(0xA, IAddOnContentManager, GetAddOnContentListChangedEventWithProcessId), SFUNC(0x32, IAddOnContentManager, CheckAddOnContentMountStatus), diff --git a/app/src/main/cpp/skyline/services/fssrv/IFileSystemProxy.cpp b/app/src/main/cpp/skyline/services/fssrv/IFileSystemProxy.cpp index 40a4b60f1..82efe4c2e 100644 --- a/app/src/main/cpp/skyline/services/fssrv/IFileSystemProxy.cpp +++ b/app/src/main/cpp/skyline/services/fssrv/IFileSystemProxy.cpp @@ -10,6 +10,7 @@ #include "IMultiCommitManager.h" #include "IFileSystemProxy.h" #include "ISaveDataInfoReader.h" +#include "vfs/patch_manager.h" namespace skyline::service::fssrv { IFileSystemProxy::IFileSystemProxy(const DeviceState &state, ServiceManager &manager) : BaseService(state, manager) {} @@ -92,10 +93,17 @@ namespace skyline::service::fssrv { } Result IFileSystemProxy::OpenDataStorageByCurrentProcess(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response) { - if (!state.loader->romFs) - return result::NoRomFsAvailable; - manager.RegisterService(std::make_shared(state.loader->romFs, state, manager), session, response); + if (state.updateLoader) { + auto patchManager{std::make_shared()}; + auto romFs{patchManager->PatchRomFS(state, state.updateLoader->programNca, state.loader->programNca->ivfcOffset)}; + manager.RegisterService(std::make_shared(romFs, state, manager), session, response); + } else { + if (!state.loader->romFs) + return result::NoRomFsAvailable; + + manager.RegisterService(std::make_shared(state.loader->romFs, state, manager), session, response); + } return {}; } @@ -103,6 +111,16 @@ namespace skyline::service::fssrv { auto storageId{request.Pop()}; request.Skip>(); // 7-bytes padding auto dataId{request.Pop()}; + auto patchManager{std::make_shared()}; + + // Try load DLC first + for (const auto &dlc : state.dlcLoaders) { + if (dlc->cnmt->header.id == dataId) { + auto romFs{patchManager->PatchRomFS(state, dlc->publicNca, state.loader->programNca->ivfcOffset)}; + manager.RegisterService(std::make_shared(romFs, state, manager), session, response); + return {}; + } + } auto systemArchivesFileSystem{std::make_shared(state.os->publicAppFilesPath + "/switch/nand/system/Contents/registered/")}; auto systemArchives{systemArchivesFileSystem->OpenDirectory("")}; @@ -112,7 +130,7 @@ namespace skyline::service::fssrv { std::shared_ptr backing{systemArchivesFileSystem->OpenFile(entry.name)}; auto nca{vfs::NCA(backing, keyStore)}; - if (nca.header.programId == dataId && nca.romFs != nullptr) { + if (nca.header.titleId == dataId && nca.romFs != nullptr) { manager.RegisterService(std::make_shared(nca.romFs, state, manager), session, response); return {}; } diff --git a/app/src/main/cpp/skyline/services/fssrv/IStorage.cpp b/app/src/main/cpp/skyline/services/fssrv/IStorage.cpp index 8e4825713..1e3476281 100644 --- a/app/src/main/cpp/skyline/services/fssrv/IStorage.cpp +++ b/app/src/main/cpp/skyline/services/fssrv/IStorage.cpp @@ -8,18 +8,8 @@ namespace skyline::service::fssrv { IStorage::IStorage(std::shared_ptr backing, const DeviceState &state, ServiceManager &manager) : backing(std::move(backing)), BaseService(state, manager) {} Result IStorage::Read(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response) { - auto offset{request.Pop()}; - auto size{request.Pop()}; - - if (offset < 0) { - LOGW("Trying to read a file with a negative offset"); - return result::InvalidOffset; - } - - if (size < 0) { - LOGW("Trying to read a file with a negative size"); - return result::InvalidSize; - } + auto offset{request.Pop()}; + auto size{request.Pop()}; backing->Read(request.outputBuf.at(0), static_cast(offset)); return {}; diff --git a/app/src/main/cpp/skyline/vfs/bktr.cpp b/app/src/main/cpp/skyline/vfs/bktr.cpp new file mode 100644 index 000000000..d39755fae --- /dev/null +++ b/app/src/main/cpp/skyline/vfs/bktr.cpp @@ -0,0 +1,227 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright © 2023 Strato Team and Contributors (https://github.com/strato-emu/) + +#include "bktr.h" +#include "region_backing.h" + +namespace skyline::vfs { + template + std::pair SearchBucketEntry(u64 offset, const BlockType &block, const BucketType &buckets, bool isSubsection) { + if (isSubsection) { + const auto &lastBucket{buckets[block.numberBuckets - 1]}; + if (offset >= lastBucket.entries[lastBucket.numberEntries].addressPatch) { + return {block.numberBuckets - 1, lastBucket.numberEntries}; + } + } + + u64 bucketId{static_cast(std::distance(block.baseOffsets.begin(), + std::upper_bound(block.baseOffsets.begin() + 1, + block.baseOffsets.begin() + block.numberBuckets, offset)) - 1)}; + + const auto &bucket{buckets[bucketId]}; + + if (bucket.numberEntries == 1) + return {bucketId, 0}; + + auto entryIt{std::upper_bound(bucket.entries.begin(), bucket.entries.begin() + bucket.numberEntries, offset, [](u64 offset, const auto &entry) { + return offset < entry.addressPatch; + })}; + + if (entryIt != bucket.entries.begin()) { + u64 entryIndex{static_cast(std::distance(bucket.entries.begin(), entryIt) - 1)}; + return {bucketId, entryIndex}; + } + LOGE("Offset could not be found."); + return {0, 0}; + } + + BKTR::BKTR(std::shared_ptr pBaseRomfs, std::shared_ptr pBktrRomfs, RelocationBlock pRelocation, + std::vector pRelocationBuckets, SubsectionBlock pSubsection, + std::vector pSubsectionBuckets, bool pIsEncrypted, std::array pKey, + u64 pBaseOffset, u64 pIvfcOffset, std::array pSectionCtr) + : baseRomFs(std::move(pBaseRomfs)), bktrRomFs(std::move(pBktrRomfs)), + relocation(pRelocation), relocationBuckets(std::move(pRelocationBuckets)), + subsection(pSubsection), subsectionBuckets(std::move(pSubsectionBuckets)), + isEncrypted(pIsEncrypted), key(pKey), baseOffset(pBaseOffset), ivfcOffset(pIvfcOffset), + sectionCtr(pSectionCtr) { + + for (std::size_t i = 0; i < relocation.numberBuckets - 1; ++i) + relocationBuckets[i].entries.push_back({relocation.baseOffsets[i + 1], 0, 0}); + + for (std::size_t i = 0; i < subsection.numberBuckets - 1; ++i) + subsectionBuckets[i].entries.push_back({subsectionBuckets[i + 1].entries[0].addressPatch, {0}, subsectionBuckets[i + 1].entries[0].ctr}); + + relocationBuckets.back().entries.push_back({relocation.size, 0, 0}); + } + + size_t BKTR::ReadImpl(span output, size_t offset) { + if (offset >= relocation.size) + return 0; + + const auto relocationEntry{GetRelocationEntry(offset)}; + const auto sectionOffset{offset - relocationEntry.addressPatch + relocationEntry.addressSource}; + + const auto nextRelocation{GetNextRelocationEntry(offset)}; + + if (offset + output.size() > nextRelocation.addressPatch) { + const u64 partition{nextRelocation.addressPatch - offset}; + span data(output.data() + partition, output.size() - partition); + return ReadWithPartition(data, output.size() - partition, offset + partition) + ReadWithPartition(output, partition, offset); + } + + if (!relocationEntry.fromPatch) { + auto regionBacking{std::make_shared(baseRomFs, sectionOffset - ivfcOffset, output.size())}; + return regionBacking->Read(output); + } + + if (!isEncrypted) + return bktrRomFs->Read(output, sectionOffset); + + const auto subsectionEntry{GetSubsectionEntry(sectionOffset)}; + + crypto::AesCipher cipher(key, MBEDTLS_CIPHER_AES_128_CTR); + cipher.SetIV(GetCipherIV(subsectionEntry, sectionOffset)); + + const auto nextSubsection{GetNextSubsectionEntry(sectionOffset)}; + + if (sectionOffset + output.size() > nextSubsection.addressPatch) { + const u64 partition{nextSubsection.addressPatch - sectionOffset}; + span data(output.data() + partition, output.size() - partition); + return ReadWithPartition(data, output.size() - partition, offset + partition) + + ReadWithPartition(output, partition, offset); + } + + const auto blockOffset{sectionOffset & 0xF}; + if (blockOffset != 0) { + std::vector block(0x10); + auto regionBacking{std::make_shared(bktrRomFs, sectionOffset & static_cast(~0xF), 0x10)}; + regionBacking->Read(block); + + cipher.Decrypt(block.data(), block.data(), block.size()); + if (output.size() + blockOffset < 0x10) { + std::memcpy(output.data(), block.data() + blockOffset, std::min(output.size(), block.size())); + return std::min(output.size(), block.size()); + } + + const auto read{0x10 - blockOffset}; + std::memcpy(output.data(), block.data() + blockOffset, read); + span data(output.data() + read, output.size() - read); + return read + ReadWithPartition(data, output.size() - read, offset + read); + } + + auto regionBacking{std::make_shared(bktrRomFs, sectionOffset, output.size())}; + auto readSize{regionBacking->Read(output)}; + cipher.Decrypt(output.data(), output.data(), readSize); + return readSize; + } + + size_t BKTR::ReadWithPartition(span output, size_t length, size_t offset) { + if (offset >= relocation.size) + return 0; + + const auto relocationEntry{GetRelocationEntry(offset)}; + const auto sectionOffset{offset - relocationEntry.addressPatch + relocationEntry.addressSource}; + + const auto nextRelocation{GetNextRelocationEntry(offset)}; + + if (offset + length > nextRelocation.addressPatch) { + const u64 partition{nextRelocation.addressPatch - offset}; + span data(output.data() + partition, length - partition); + return ReadWithPartition(data, length - partition, offset + partition) + ReadWithPartition(output, partition, offset); + } + + if (!relocationEntry.fromPatch) { + span data(output.data(), length); + auto regionBacking{std::make_shared(baseRomFs, sectionOffset - ivfcOffset, length)}; + return regionBacking->Read(data); + } + + if (!isEncrypted) + return bktrRomFs->Read(output, sectionOffset); + + const auto subsectionEntry{GetSubsectionEntry(sectionOffset)}; + + crypto::AesCipher cipher(key, MBEDTLS_CIPHER_AES_128_CTR); + cipher.SetIV(GetCipherIV(subsectionEntry, sectionOffset)); + + const auto nextSubsection{GetNextSubsectionEntry(sectionOffset)}; + + if (sectionOffset + length > nextSubsection.addressPatch) { + const u64 partition{nextSubsection.addressPatch - sectionOffset}; + span data(output.data() + partition, length - partition); + return ReadWithPartition(data, length - partition, offset + partition) + + ReadWithPartition(output, partition, offset); + } + + const auto blockOffset{sectionOffset & 0xF}; + if (blockOffset != 0) { + std::vector block(0x10); + auto regionBacking{std::make_shared(bktrRomFs, sectionOffset & static_cast(~0xF), 0x10)}; + regionBacking->Read(block); + + cipher.Decrypt(block.data(), block.data(), block.size()); + if (length + blockOffset < 0x10) { + std::memcpy(output.data(), block.data() + blockOffset, std::min(length, block.size())); + return std::min(length, block.size()); + } + + const auto read{0x10 - blockOffset}; + std::memcpy(output.data(), block.data() + blockOffset, read); + span data(output.data() + read, length - read); + return read + ReadWithPartition(data, length - read, offset + read); + } + + auto regionBacking{std::make_shared(bktrRomFs, sectionOffset, length)}; + span data(output.data(), length); + size_t readSize{0}; + if (length) + readSize = regionBacking->Read(data); + cipher.Decrypt(data.data(), data.data(), readSize); + return readSize; + } + + SubsectionEntry BKTR::GetNextSubsectionEntry(u64 offset) { + const auto entry{SearchBucketEntry(offset, subsection, subsectionBuckets, true)}; + const auto bucket{subsectionBuckets[entry.first]}; + if (entry.second + 1 < bucket.entries.size()) + return bucket.entries[entry.second + 1]; + return subsectionBuckets[entry.first + 1].entries[0]; + } + + RelocationEntry BKTR::GetRelocationEntry(u64 offset) { + const auto entry{SearchBucketEntry(offset, relocation, relocationBuckets, false)}; + return relocationBuckets[entry.first].entries[entry.second]; + } + + SubsectionEntry BKTR::GetSubsectionEntry(u64 offset) { + const auto entry{SearchBucketEntry(offset, subsection, subsectionBuckets, true)}; + return subsectionBuckets[entry.first].entries[entry.second]; + } + + RelocationEntry BKTR::GetNextRelocationEntry(u64 offset) { + const auto entry{SearchBucketEntry(offset, relocation, relocationBuckets, false)}; + const auto bucket{relocationBuckets[entry.first]}; + if (entry.second + 1 < bucket.entries.size()) + return bucket.entries[entry.second + 1]; + return relocationBuckets[entry.first + 1].entries[0]; + } + + std::array BKTR::GetCipherIV(SubsectionEntry subsectionEntry, u64 sectionOffset) { + std::array iv{}; + auto subsectionCtr{subsectionEntry.ctr}; + auto offset_iv{sectionOffset + baseOffset}; + for (std::size_t i = 0; i < sectionCtr.size(); ++i) { + iv[i] = sectionCtr[0x8 - i - 1]; + } + offset_iv >>= 4; + for (std::size_t i = 0; i < sizeof(u64); ++i) { + iv[0xF - i] = static_cast(offset_iv & 0xFF); + offset_iv >>= 8; + } + for (std::size_t i = 0; i < sizeof(u32); ++i) { + iv[0x7 - i] = static_cast(subsectionCtr & 0xFF); + subsectionCtr >>= 8; + } + return iv; + } +} diff --git a/app/src/main/cpp/skyline/vfs/bktr.h b/app/src/main/cpp/skyline/vfs/bktr.h new file mode 100644 index 000000000..287f71a97 --- /dev/null +++ b/app/src/main/cpp/skyline/vfs/bktr.h @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright © 2023 Strato Team and Contributors (https://github.com/strato-emu/) + +#pragma once + +#include "filesystem.h" +#include "nca.h" + +namespace skyline::vfs { + + /** + * @brief Allows reading patched RomFs + * @url https://switchbrew.org/wiki/NCA#RomFs_Patching + */ + class BKTR : public Backing { + private: + std::shared_ptr baseRomFs; + std::shared_ptr bktrRomFs; + RelocationBlock relocation; + SubsectionBlock subsection; + std::vector relocationBuckets; + std::vector subsectionBuckets; + bool isEncrypted; + u64 baseOffset; + u64 ivfcOffset; + std::array sectionCtr; + std::array key; + + SubsectionEntry GetNextSubsectionEntry(u64 offset); + + RelocationEntry GetRelocationEntry(u64 offset); + + RelocationEntry GetNextRelocationEntry(u64 offset); + + SubsectionEntry GetSubsectionEntry(u64 offset); + + std::array GetCipherIV(SubsectionEntry subsectionEntry, u64 sectionOffset); + + public: + + BKTR(std::shared_ptr pBaseRomfs, std::shared_ptr pBktrRomfs, RelocationBlock pRelocation, + std::vector pRelocationBuckets, SubsectionBlock pSubsection, + std::vector pSubsectionBuckets, bool pIsEncrypted, std::array pKey, + u64 pBaseOffset, u64 pIvfcOffset, std::array pSectionCtr); + + size_t ReadImpl(span output, size_t offset) override; + size_t ReadWithPartition(span output, size_t length, size_t offset); + }; +} diff --git a/app/src/main/cpp/skyline/vfs/cnmt.cpp b/app/src/main/cpp/skyline/vfs/cnmt.cpp new file mode 100644 index 000000000..8c83a5890 --- /dev/null +++ b/app/src/main/cpp/skyline/vfs/cnmt.cpp @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright © 2023 Strato Team and Contributors (https://github.com/strato-emu/) + +#include "cnmt.h" + +namespace skyline::vfs { + + CNMT::CNMT(std::shared_ptr cnmtSection) { + auto root{cnmtSection->OpenDirectory("")}; + std::shared_ptr cnmt; + if (root != nullptr) { + for (const auto &entry : root->Read()) { + cnmt = cnmtSection->OpenFile(entry.name); + } + } + + header = cnmt->Read(); + if (header.contentMetaType >= ContentMetaType::Application && header.contentMetaType <= ContentMetaType::AddOnContent) + optionalHeader = cnmt->Read(sizeof(PackagedContentMetaHeader)); + + for (u16 i = 0; i < header.contentCount; ++i) + contentInfos.emplace_back(cnmt->Read(sizeof(PackagedContentMetaHeader) + i * sizeof(PackagedContentInfo) + + header.extendedHeaderSize)); + + for (u16 i = 0; i < header.contentMetaCount; ++i) + contentMetaInfos.emplace_back(cnmt->Read(sizeof(PackagedContentMetaHeader) + i * sizeof(ContentMetaInfo) + + header.extendedHeaderSize)); + } + + std::string CNMT::GetTitleId() { + auto tilteId{header.id}; + return fmt::format("{:016X}", tilteId); + } + + std::string CNMT::GetParentTitleId() { + auto parentTilteId{optionalHeader.titleId}; + return fmt::format("{:016X}", parentTilteId); + } + + ContentMetaType CNMT::GetContentMetaType() { + return header.contentMetaType; + } + +} + diff --git a/app/src/main/cpp/skyline/vfs/cnmt.h b/app/src/main/cpp/skyline/vfs/cnmt.h new file mode 100644 index 000000000..c034977c7 --- /dev/null +++ b/app/src/main/cpp/skyline/vfs/cnmt.h @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright © 2023 Strato Team and Contributors (https://github.com/strato-emu/) + +#pragma once + +#include "filesystem.h" + +namespace skyline::vfs { + + /** + * @url https://switchbrew.org/wiki/NCM_services#ContentMetaType + */ + enum class ContentMetaType : u8 { + SystemProgram = 0x01, + SystemData = 0x02, + SystemUpdate = 0x03, + BootImagePackage = 0x04, + BootImagePackageSafe = 0x05, + Application = 0x80, + Patch = 0x81, + AddOnContent = 0x82, + Delta = 0x83, + DataPatch = 0x84, + }; + + enum class ContentType : u8 { + Meta = 0, + Program = 1, + Data = 2, + Control = 3, + HtmlDocument = 4, + LegalInformation = 5, + DeltaFragment = 6, + }; + + /** + * @url https://switchbrew.org/wiki/CNMT#PackagedContentMetaHeader + */ + struct PackagedContentMetaHeader { + u64 id; + u32 version; + ContentMetaType contentMetaType; + u8 _pad0_; + u16 extendedHeaderSize; + u16 contentCount; + u16 contentMetaCount; + u8 contentMetaAttributes; + u8 _pad1_[0x3]; + u32 requiredDownloadSystemVersion; + u8 _pad2_[0x4]; + }; + static_assert(sizeof(PackagedContentMetaHeader) == 0x20); + + /** + * @url https://switchbrew.org/wiki/CNMT#PackagedContentInfo + */ + struct PackagedContentInfo { + std::array hash; + std::array contentId; + std::array size; + ContentType contentType; + u8 idOffset; + }; + static_assert(sizeof(PackagedContentInfo) == 0x38); + + /** + * @url https://switchbrew.org/wiki/CNMT#ContentMetaInfo + */ + struct ContentMetaInfo { + u64 id; + u32 version; + ContentMetaType contentMetaType; + u8 contentMetaAttributes; + u8 _pad0_[0x2]; + }; + static_assert(sizeof(ContentMetaInfo) == 0x10); + + struct OptionalHeader { + u64 titleId; + u64 minimumVersion; + }; + static_assert(sizeof(OptionalHeader) == 0x10); + + /** + * @brief The CNMT class provides easy access to the data found in an CNMT file + * @url https://switchbrew.org/wiki/CNMT + */ + class CNMT { + private: + OptionalHeader optionalHeader; + std::vector contentInfos; + std::vector contentMetaInfos; + + public: + PackagedContentMetaHeader header; + + CNMT(std::shared_ptr file); + + std::string GetTitleId(); + + std::string GetParentTitleId(); + + ContentMetaType GetContentMetaType(); + }; + +} diff --git a/app/src/main/cpp/skyline/vfs/nacp.cpp b/app/src/main/cpp/skyline/vfs/nacp.cpp index 222e52daa..5aa13c276 100644 --- a/app/src/main/cpp/skyline/vfs/nacp.cpp +++ b/app/src/main/cpp/skyline/vfs/nacp.cpp @@ -34,6 +34,11 @@ namespace skyline::vfs { return std::string(applicationPublisher.as_string(true)); } + std::string NACP::GetAddOnContentBaseId() { + auto addOnContentBaseId{nacpContents.addOnContentBaseId}; + return fmt::format("{:016X}", addOnContentBaseId); + } + std::string NACP::GetSaveDataOwnerId() { auto applicationTitleId{nacpContents.saveDataOwnerId}; return fmt::format("{:016X}", applicationTitleId); diff --git a/app/src/main/cpp/skyline/vfs/nacp.h b/app/src/main/cpp/skyline/vfs/nacp.h index 11d230c42..7c8695fd0 100644 --- a/app/src/main/cpp/skyline/vfs/nacp.h +++ b/app/src/main/cpp/skyline/vfs/nacp.h @@ -25,15 +25,37 @@ namespace skyline::vfs { public: struct NacpData { std::array titleEntries; //!< Title entries for each language - u8 _pad0_[0x2C]; + std::array isbn; + u8 startupUserAccount; + u8 userAccountSwitchLock; + u8 addonContentRegistrationType; + u32 attributeFlag; u32 supportedLanguageFlag; //!< A bitmask containing the game's supported languages - u8 _pad1_[0x30]; + u32 parentalControlFlag; + u8 screenshotEnabled; + u8 videoCaptureMode; + u8 dataLossConfirmation; + u8 _pad0_[0x1]; + u64 presenceGroupId; + std::array ratingAge; std::array displayVersion; //!< The user-readable version of the application - u8 _pad4_[0x8]; + u64 addOnContentBaseId; u64 saveDataOwnerId; //!< The ID that should be used for this application's savedata - u8 _pad2_[0x78]; + u64 userAccountSaveDataSize; + u64 userAccountSaveDataJournalSize; + u64 deviceSaveDataSize; + u64 deviceSaveDataJournalSize; + u64 bcatDeliveryCacheStorageSize; + char applicationErrorCodeCategory[8]; + std::array localCommunicationId; + u8 logoType; + u8 logoHandling; + u8 runtimeAddOnContentInstall; + u8 runtimeParameterDelivery; + u8 appropriateAgeForChina; + u8 _pad1_[0x3]; std::array seedForPseudoDeviceId; //!< Seed that is combined with the device seed for generating the pseudo-device ID - u8 _pad3_[0xF00]; + u8 _pad2_[0xF00]; } nacpContents{}; static_assert(sizeof(NacpData) == 0x4000); @@ -49,6 +71,8 @@ namespace skyline::vfs { std::string GetApplicationVersion(); + std::string GetAddOnContentBaseId(); + std::string GetSaveDataOwnerId(); std::string GetApplicationPublisher(language::ApplicationLanguage language); diff --git a/app/src/main/cpp/skyline/vfs/nca.cpp b/app/src/main/cpp/skyline/vfs/nca.cpp index 9b19e81dc..3d2fb57e5 100644 --- a/app/src/main/cpp/skyline/vfs/nca.cpp +++ b/app/src/main/cpp/skyline/vfs/nca.cpp @@ -9,21 +9,22 @@ #include "partition_filesystem.h" #include "nca.h" #include "rom_filesystem.h" +#include "bktr.h" #include "directory.h" namespace skyline::vfs { using namespace loader; - NCA::NCA(std::shared_ptr pBacking, std::shared_ptr pKeyStore, bool pUseKeyArea) : backing(std::move(pBacking)), keyStore(std::move(pKeyStore)), useKeyArea(pUseKeyArea) { - header = backing->Read(); + NCA::NCA(std::shared_ptr pBacking, std::shared_ptr pKeyStore, bool pUseKeyArea) + : backing(std::move(pBacking)), keyStore(std::move(pKeyStore)), useKeyArea(pUseKeyArea) { + header = backing->Read(); if (header.magic != util::MakeMagic("NCA3")) { if (!keyStore->headerKey) throw loader_exception(LoaderResult::MissingHeaderKey); crypto::AesCipher cipher(*keyStore->headerKey, MBEDTLS_CIPHER_AES_128_XTS); - - cipher.XtsDecrypt({reinterpret_cast(&header), sizeof(NcaHeader)}, 0, 0x200); + cipher.XtsDecrypt({reinterpret_cast(&header), sizeof(NCAHeader)}, 0, 0x200); // Check if decryption was successful if (header.magic != util::MakeMagic("NCA3")) @@ -34,57 +35,143 @@ namespace skyline::vfs { contentType = header.contentType; rightsIdEmpty = header.rightsId == crypto::KeyStore::Key128{}; - for (size_t i{}; i < header.sectionHeaders.size(); i++) { - auto §ionHeader{header.sectionHeaders.at(i)}; - auto §ionEntry{header.fsEntries.at(i)}; + const std::size_t numberSections{static_cast(std::ranges::count_if(header.sectionTables, [](const NCASectionTableEntry &entry) { + return entry.mediaOffset > 0; + }))}; + + sections.resize(numberSections); + const auto lengthSections{constant::SectionHeaderSize * numberSections}; + + if (encrypted) { + std::vector raw(lengthSections); + + backing->Read(raw, constant::SectionHeaderOffset); - if (sectionHeader.fsType == NcaSectionFsType::PFS0 && sectionHeader.hashType == NcaSectionHashType::HierarchicalSha256) - ReadPfs0(sectionHeader, sectionEntry); - else if (sectionHeader.fsType == NcaSectionFsType::RomFs && sectionHeader.hashType == NcaSectionHashType::HierarchicalIntegrity) - ReadRomFs(sectionHeader, sectionEntry); + crypto::AesCipher cipher(*keyStore->headerKey, MBEDTLS_CIPHER_AES_128_XTS); + cipher.XtsDecrypt(reinterpret_cast(sections.data()), reinterpret_cast(raw.data()), lengthSections, 2, constant::SectionHeaderSize); + } else { + for (size_t i{}; i < lengthSections; i++) + sections.push_back(backing->Read()); + } + + for (std::size_t i = 0; i < sections.size(); ++i) { + const auto §ion = sections[i]; + + ValidateNCA(section); + + if (section.raw.header.fsType == NcaSectionFsType::RomFs) { + ReadRomFs(section, header.sectionTables[i]); + } else if (section.raw.header.fsType == NcaSectionFsType::PFS0) { + ReadPfs0(section, header.sectionTables[i]); + } } } - void NCA::ReadPfs0(const NcaSectionHeader §ionHeader, const NcaFsEntry &entry) { - size_t offset{static_cast(entry.startOffset) * constant::MediaUnitSize + sectionHeader.sha256HashInfo.pfs0Offset}; - size_t size{constant::MediaUnitSize * static_cast(entry.endOffset - entry.startOffset)}; + NCA::NCA(std::optional updateNca, std::shared_ptr pKeyStore, std::shared_ptr bktrBaseRomfs, + u64 bktrBaseIvfcOffset, bool pUseKeyArea) + : romFs(updateNca->romFs), header(updateNca->header), sections(std::move(updateNca->sections)), encrypted(updateNca->encrypted), backing(std::move(updateNca->backing)), + keyStore(std::move(pKeyStore)), bktrBaseRomfs(std::move(bktrBaseRomfs)), bktrBaseIvfcOffset(bktrBaseIvfcOffset), useKeyArea(pUseKeyArea) { + + useKeyArea = false; + contentType = header.contentType; + rightsIdEmpty = header.rightsId == crypto::KeyStore::Key128{}; - auto pfs{std::make_shared(CreateBacking(sectionHeader, std::make_shared(backing, offset, size), offset))}; + if (!updateNca) + throw loader_exception(LoaderResult::ParsingError); - if (contentType == NcaContentType::Program) { + for (std::size_t i = 0; i < sections.size(); ++i) { + const auto §ion = sections[i]; + + ValidateNCA(section); + + if (section.raw.header.fsType == NcaSectionFsType::RomFs) + ReadRomFs(section, header.sectionTables[i]); + } + } + + void NCA::ReadPfs0(const NCASectionHeader §ion, const NCASectionTableEntry &entry) { + size_t offset{static_cast(entry.mediaOffset) * constant::MediaUnitSize + section.pfs0.pfs0HeaderOffset}; + size_t size{constant::MediaUnitSize * static_cast(entry.mediaEndOffset - entry.mediaOffset)}; + + auto pfs{std::make_shared(CreateBacking(section, std::make_shared(backing, offset, size), offset))}; + + if (contentType == NCAContentType::Program) { // An ExeFS must always contain an NPDM and a main NSO, whereas the logo section will always contain a logo and a startup movie if (pfs->FileExists("main") && pfs->FileExists("main.npdm")) exeFs = std::move(pfs); else if (pfs->FileExists("NintendoLogo.png") && pfs->FileExists("StartupMovie.gif")) logo = std::move(pfs); - } else if (contentType == NcaContentType::Meta) { + } else if (contentType == NCAContentType::Meta) { cnmt = std::move(pfs); } } - void NCA::ReadRomFs(const NcaSectionHeader §ionHeader, const NcaFsEntry &entry) { - size_t offset{static_cast(entry.startOffset) * constant::MediaUnitSize + sectionHeader.integrityHashInfo.levels.back().offset}; - size_t size{sectionHeader.integrityHashInfo.levels.back().size}; - - romFs = CreateBacking(sectionHeader, std::make_shared(backing, offset, size), offset); + void NCA::ReadRomFs(const NCASectionHeader §ionHeader, const NCASectionTableEntry &entry) { + const std::size_t baseOffset{entry.mediaOffset * constant::MediaUnitSize}; + ivfcOffset = sectionHeader.romfs.ivfc.levels[constant::IvfcMaxLevel - 1].offset; + const std::size_t romFsOffset{baseOffset + ivfcOffset}; + const std::size_t romFsSize{sectionHeader.romfs.ivfc.levels[constant::IvfcMaxLevel - 1].size}; + auto decryptedBacking{CreateBacking(sectionHeader, std::make_shared(backing, romFsOffset, romFsSize), romFsOffset)}; + + if (sectionHeader.raw.header.encryptionType == NcaSectionEncryptionType::BKTR && bktrBaseRomfs && romFs) { + const u64 size{constant::MediaUnitSize * (entry.mediaEndOffset - entry.mediaOffset)}; + const u64 offset{sectionHeader.romfs.ivfc.levels[constant::IvfcMaxLevel - 1].offset}; + + RelocationBlock relocationBlock{romFs->Read(sectionHeader.bktr.relocation.offset - offset)}; + SubsectionBlock subsectionBlock{romFs->Read(sectionHeader.bktr.subsection.offset - offset)}; + + std::vector relocationBucketsRaw((sectionHeader.bktr.relocation.size - sizeof(RelocationBlock)) / sizeof(RelocationBucketRaw)); + auto regionBackingRelocation{std::make_shared(romFs, sectionHeader.bktr.relocation.offset + sizeof(RelocationBlock) - offset, sectionHeader.bktr.relocation.size - sizeof(RelocationBlock))}; + regionBackingRelocation->Read(relocationBucketsRaw); + + std::vector subsectionBucketsRaw((sectionHeader.bktr.subsection.size - sizeof(SubsectionBlock)) / sizeof(SubsectionBucketRaw)); + auto regionBackingSubsection{std::make_shared(romFs, sectionHeader.bktr.subsection.offset + sizeof(SubsectionBlock) - offset, sectionHeader.bktr.subsection.size - sizeof(SubsectionBlock))}; + regionBackingSubsection->Read(subsectionBucketsRaw); + + std::vector relocationBuckets; + relocationBuckets.reserve(relocationBucketsRaw.size()); + for (const RelocationBucketRaw &rawBucket : relocationBucketsRaw) + relocationBuckets.push_back(ConvertRelocationBucketRaw(rawBucket)); + + std::vector subsectionBuckets; + subsectionBuckets.reserve(subsectionBucketsRaw.size()); + for (const SubsectionBucketRaw &rawBucket : subsectionBucketsRaw) + subsectionBuckets.push_back(ConvertSubsectionBucketRaw(rawBucket)); + + u32 ctrLow; + std::memcpy(&ctrLow, sectionHeader.raw.sectionCtr.data(), sizeof(ctrLow)); + subsectionBuckets.back().entries.push_back({sectionHeader.bktr.relocation.offset, {0}, ctrLow}); + subsectionBuckets.back().entries.push_back({size, {0}, 0}); + + auto key{!(rightsIdEmpty || useKeyArea) ? GetTitleKey() : GetKeyAreaKey(sectionHeader.raw.header.encryptionType)}; + + auto bktr{std::make_shared( + bktrBaseRomfs, std::make_shared(backing, baseOffset, romFsSize), + relocationBlock, relocationBuckets, subsectionBlock, subsectionBuckets, encrypted, + encrypted ? key : std::array{}, baseOffset, bktrBaseIvfcOffset, + sectionHeader.raw.sectionCtr)}; + + romFs = std::make_shared(bktr, sectionHeader.romfs.ivfc.levels[constant::IvfcMaxLevel - 1].offset, romFsSize); + } else { + romFs = std::move(decryptedBacking); + } } - std::shared_ptr NCA::CreateBacking(const NcaSectionHeader §ionHeader, std::shared_ptr rawBacking, size_t offset) { + std::shared_ptr NCA::CreateBacking(const NCASectionHeader §ionHeader, std::shared_ptr rawBacking, size_t offset) { if (!encrypted) return rawBacking; - switch (sectionHeader.encryptionType) { + switch (sectionHeader.raw.header.encryptionType) { case NcaSectionEncryptionType::None: return rawBacking; case NcaSectionEncryptionType::CTR: case NcaSectionEncryptionType::BKTR: { - auto key{!(rightsIdEmpty || useKeyArea) ? GetTitleKey() : GetKeyAreaKey(sectionHeader.encryptionType)}; + auto key{!(rightsIdEmpty || useKeyArea) ? GetTitleKey() : GetKeyAreaKey(sectionHeader.raw.header.encryptionType)}; std::array ctr{}; - u32 secureValueLE{util::SwapEndianness(sectionHeader.secureValue)}; - u32 generationLE{util::SwapEndianness(sectionHeader.generation)}; - std::memcpy(ctr.data(), &secureValueLE, 4); - std::memcpy(ctr.data() + 4, &generationLE, 4); + for (std::size_t i = 0; i < 8; ++i) { + ctr[i] = sectionHeader.raw.sectionCtr[8 - i - 1]; + } return std::make_shared(ctr, key, std::move(rawBacking), offset); } @@ -94,8 +181,8 @@ namespace skyline::vfs { } u8 NCA::GetKeyGeneration() { - u8 legacyGen{static_cast(header.legacyKeyGenerationType)}; - u8 gen{static_cast(header.keyGenerationType)}; + u8 legacyGen{static_cast(header.cryptoType)}; + u8 gen{static_cast(header.cryptoType2)}; gen = std::max(legacyGen, gen); return gen > 0 ? gen - 1 : gen; } @@ -116,7 +203,7 @@ namespace skyline::vfs { return *titleKey; } - crypto::KeyStore::Key128 NCA::GetKeyAreaKey(NCA::NcaSectionEncryptionType type) { + crypto::KeyStore::Key128 NCA::GetKeyAreaKey(NcaSectionEncryptionType type) { auto keyArea{[this, &type](crypto::KeyStore::IndexedKeys128 &keys) { u8 keyGeneration{GetKeyGeneration()}; @@ -140,17 +227,27 @@ namespace skyline::vfs { crypto::KeyStore::Key128 decryptedKeyArea; crypto::AesCipher cipher(*keyArea, MBEDTLS_CIPHER_AES_128_ECB); - cipher.Decrypt(decryptedKeyArea.data(), header.encryptedKeyArea[keyAreaIndex].data(), decryptedKeyArea.size()); + cipher.Decrypt(decryptedKeyArea.data(), header.keyArea[keyAreaIndex].data(), decryptedKeyArea.size()); return decryptedKeyArea; }}; - switch (header.keyAreaEncryptionKeyType) { - case NcaKeyAreaEncryptionKeyType::Application: + switch (header.keyIndex) { + case NCAKeyAreaEncryptionKeyType::Application: return keyArea(keyStore->areaKeyApplication); - case NcaKeyAreaEncryptionKeyType::Ocean: + case NCAKeyAreaEncryptionKeyType::Ocean: return keyArea(keyStore->areaKeyOcean); - case NcaKeyAreaEncryptionKeyType::System: + case NCAKeyAreaEncryptionKeyType::System: return keyArea(keyStore->areaKeySystem); } } + + void NCA::ValidateNCA(const NCASectionHeader §ionHeader) { + if (sectionHeader.raw.sparseInfo.bucket.tableOffset != 0 && + sectionHeader.raw.sparseInfo.bucket.tableSize != 0) + throw loader_exception(LoaderResult::ErrorSparseNCA); + + if (sectionHeader.raw.compressionInfo.bucket.tableOffset != 0 && + sectionHeader.raw.compressionInfo.bucket.tableSize != 0) + throw loader_exception(LoaderResult::ErrorCompressedNCA); + } } diff --git a/app/src/main/cpp/skyline/vfs/nca.h b/app/src/main/cpp/skyline/vfs/nca.h index e6a2a65b4..d987bd79b 100644 --- a/app/src/main/cpp/skyline/vfs/nca.h +++ b/app/src/main/cpp/skyline/vfs/nca.h @@ -10,10 +10,13 @@ namespace skyline { namespace constant { constexpr size_t MediaUnitSize{0x200}; //!< The unit size of entries in an NCA + constexpr size_t IvfcMaxLevel{6}; + constexpr u64 SectionHeaderSize{0x200}; + constexpr u64 SectionHeaderOffset{0x400}; } namespace vfs { - enum class NcaContentType : u8 { + enum class NCAContentType : u8 { Program = 0x0, //!< Program NCA Meta = 0x1, //!< Metadata NCA Control = 0x2, //!< Control NCA @@ -22,164 +25,305 @@ namespace skyline { PublicData = 0x5, //!< Public data NCA }; + enum class NcaDistributionType : u8 { + System = 0x0, //!< This NCA was distributed on the EShop or is part of the system + GameCard = 0x1, //!< This NCA was distributed on a GameCard + }; + + /** + * @brief The key generation version in NCAs before HOS 3.0.1 + */ + enum class NcaLegacyKeyGenerationType : u8 { + Fw100 = 0x0, //!< 1.0.0 + Fw300 = 0x2, //!< 3.0.0 + }; + + /** + * @brief The key generation version in NCAs after HOS 3.0.0, this is changed by Nintendo frequently + */ + enum class NcaKeyGenerationType : u8 { + Fw301 = 0x3, //!< 3.0.1 + Fw400 = 0x4, //!< 4.0.0 + Fw500 = 0x5, //!< 5.0.0 + Fw600 = 0x6, //!< 6.0.0 + Fw620 = 0x7, //!< 6.2.0 + Fw700 = 0x8, //!< 7.0.0 + Fw810 = 0x9, //!< 8.1.0 + Fw900 = 0xA, //!< 9.0.0 + Fw910 = 0xB, //!< 9.1.0 + Invalid = 0xFF, //!< An invalid key generation type + }; + + enum class NCAKeyAreaEncryptionKeyType : u8 { + Application = 0x0, //!< This NCA uses the application key encryption area + Ocean = 0x1, //!< This NCA uses the ocean key encryption area + System = 0x2, //!< This NCA uses the system key encryption area + }; + + struct NcaFsEntry { + u32 startOffset; //!< The start offset of the filesystem in units of 0x200 bytes + u32 endOffset; //!< The start offset of the filesystem in units of 0x200 bytes + u64 _pad_; + }; + + enum class NcaSectionFsType : u8 { + PFS0 = 0x2, //!< This section contains a PFS0 filesystem + RomFs = 0x3, //!< This section contains a RomFs filesystem + }; + + enum class NcaSectionHashType : u8 { + HierarchicalSha256 = 0x2, //!< The hash header for this section is that of a PFS0 + HierarchicalIntegrity = 0x3, //!< The hash header for this section is that of a RomFS + }; + + enum class NcaSectionEncryptionType : u8 { + None = 0x1, //!< This NCA doesn't use any encryption + XTS = 0x2, //!< This NCA uses AES-XTS encryption + CTR = 0x3, //!< This NCA uses AES-CTR encryption + BKTR = 0x4, //!< This NCA uses BKTR together AES-CTR encryption + }; + + /** + * @brief The data for a single level of the hierarchical integrity scheme + */ + struct HierarchicalIntegrityLevel { + u64 offset; //!< The offset of the level data + u64 size; //!< The size of the level data + u32 blockSize; //!< The block size of the level data + u32 _pad_; + }; + static_assert(sizeof(HierarchicalIntegrityLevel) == 0x18); + + struct NCASectionHeaderBlock { + u8 _pad0_[0x3]; + NcaSectionFsType fsType; + NcaSectionEncryptionType encryptionType; + u8 _pad1_[0x3]; + }; + static_assert(sizeof(NCASectionHeaderBlock) == 0x8); + + struct PFS0Superblock { + NCASectionHeaderBlock headerBlock; + std::array hash; + u32 size; + u8 _pad0_[0x4]; + u64 hashTableOffset; + u64 hashTableSize; + u64 pfs0HeaderOffset; + u64 pfs0Size; + u8 _pad1_[0x1B0]; + }; + static_assert(sizeof(PFS0Superblock) == 0x200); + + /** + * @brief The hash info header of the SHA256 hashing scheme for PFS0 + */ + struct HierarchicalSha256HashInfo { + std::array hashTableHash; //!< A SHA256 hash over the hash table + u32 blockSize; //!< The block size of the filesystem + u32 _pad_; + u64 hashTableOffset; //!< The offset from the end of the section header of the hash table + u64 hashTableSize; //!< The size of the hash table + u64 pfs0Offset; //!< The offset from the end of the section header of the PFS0 + u64 pfs0Size; //!< The size of the PFS0 + u8 _pad1_[0xB0]; + }; + static_assert(sizeof(HierarchicalSha256HashInfo) == 0xF8); + + struct NCABucketInfo { + u64 tableOffset; + u64 tableSize; + std::array tableHeader; + }; + static_assert(sizeof(NCABucketInfo) == 0x20); + + struct NCASparseInfo { + NCABucketInfo bucket; + u64 physicalOffset; + u16 generation; + u8 _pad0_[0x6]; + }; + static_assert(sizeof(NCASparseInfo) == 0x30); + + struct NCACompressionInfo { + NCABucketInfo bucket; + u8 _pad0_[0x8]; + }; + static_assert(sizeof(NCACompressionInfo) == 0x28); + + struct NCASectionRaw { + NCASectionHeaderBlock header; + std::array blockData; + std::array sectionCtr; + NCASparseInfo sparseInfo; + NCACompressionInfo compressionInfo; + u8 _pad0_[0x60]; + }; + static_assert(sizeof(NCASectionRaw) == 0x200); + + struct IVFCLevel { + u64 offset; + u64 size; + u32 blockSize; + u32 reserved; + }; + static_assert(sizeof(IVFCLevel) == 0x18); + + struct IVFCHeader { + u32 magic; + u32 magicNumber; + u8 _pad0_[0x8]; + std::array levels; + u8 _pad1_[0x40]; + }; + static_assert(sizeof(IVFCHeader) == 0xE0); + + struct RomFSSuperblock { + NCASectionHeaderBlock headerBlock; + IVFCHeader ivfc; + u8 _pad0_[0x118]; + }; + static_assert(sizeof(RomFSSuperblock) == 0x200); + + struct BKTRHeader { + u64 offset; + u64 size; + u32 magic; + u8 _pad0_[0x4]; + u32 numberEntries; + u8 _pad1_[0x4]; + }; + static_assert(sizeof(BKTRHeader) == 0x20); + + struct BKTRSuperblock { + NCASectionHeaderBlock headerBlock; + IVFCHeader ivfc; + u8 _pad0_[0x18]; + BKTRHeader relocation; + BKTRHeader subsection; + u8 _pad1_[0xC0]; + }; + static_assert(sizeof(BKTRSuperblock) == 0x200); + + union NCASectionHeader { + NCASectionRaw raw{}; + PFS0Superblock pfs0; + RomFSSuperblock romfs; + BKTRSuperblock bktr; + }; + static_assert(sizeof(NCASectionHeader) == 0x200); + + struct RelocationBlock { + u8 _pad0_[0x4]; + u32 numberBuckets; + u64 size; + std::array baseOffsets; + }; + static_assert(sizeof(RelocationBlock) == 0x4000); + + #pragma pack(push, 1) + struct RelocationEntry { + u64 addressPatch; + u64 addressSource; + u32 fromPatch; + }; + #pragma pack(pop) + static_assert(sizeof(RelocationEntry) == 0x14); + + struct SubsectionBlock { + u8 _pad0_[0x4]; + u32 numberBuckets; + u64 size; + std::array baseOffsets; + }; + static_assert(sizeof(SubsectionBlock) == 0x4000); + + struct SubsectionEntry { + u64 addressPatch; + u8 _pad0_[0x4]; + u32 ctr; + }; + static_assert(sizeof(SubsectionEntry) == 0x10); + + struct NCASectionTableEntry { + u32 mediaOffset; + u32 mediaEndOffset; + u8 _pad0_[0x8]; + }; + static_assert(sizeof(NCASectionTableEntry) == 0x10); + + struct NCAHeader { + std::array rsaSignature1; + std::array rsaSignature2; + u32 magic; + u8 isSystem; + NCAContentType contentType; + u8 cryptoType; + NCAKeyAreaEncryptionKeyType keyIndex; + u64 size; + u64 titleId; + u8 _pad0_[0x4]; + u32 sdkVersion; + u8 cryptoType2; + u8 _pad1_[0xF]; + std::array rightsId; + std::array sectionTables; + std::array, 0x4> hashTables; + std::array, 4> keyArea; + u8 _pad2_[0xC0]; + }; + static_assert(sizeof(NCAHeader) == 0x400); + + struct RelocationBucketRaw { + u8 _pad0_[0x4]; + u32 numberEntries; + u64 endOffset; + std::array relocationEntries; + u8 _pad1_[0x8]; + }; + static_assert(sizeof(RelocationBucketRaw) == 0x4000); + + struct RelocationBucket { + u32 numberEntries; + u64 endOffset; + std::vector entries; + }; + + struct SubsectionBucket { + u32 numberEntries; + u64 endOffset; + std::vector entries; + }; + + struct SubsectionBucketRaw { + u8 _pad0_[0x4]; + u32 numberEntries; + u64 endOffset; + std::array subsectionEntries; + }; + static_assert(sizeof(SubsectionBucketRaw) == 0x4000); + /** * @brief The NCA class provides an easy way to access the contents of an Nintendo Content Archive * @url https://switchbrew.org/wiki/NCA_Format */ class NCA { private: - enum class NcaDistributionType : u8 { - System = 0x0, //!< This NCA was distributed on the EShop or is part of the system - GameCard = 0x1, //!< This NCA was distributed on a GameCard - }; - - /** - * @brief The key generation version in NCAs before HOS 3.0.1 - */ - enum class NcaLegacyKeyGenerationType : u8 { - Fw100 = 0x0, //!< 1.0.0 - Fw300 = 0x2, //!< 3.0.0 - }; - - /** - * @brief The key generation version in NCAs after HOS 3.0.0, this is changed by Nintendo frequently - */ - enum class NcaKeyGenerationType : u8 { - Fw301 = 0x3, //!< 3.0.1 - Fw400 = 0x4, //!< 4.0.0 - Fw500 = 0x5, //!< 5.0.0 - Fw600 = 0x6, //!< 6.0.0 - Fw620 = 0x7, //!< 6.2.0 - Fw700 = 0x8, //!< 7.0.0 - Fw810 = 0x9, //!< 8.1.0 - Fw900 = 0xA, //!< 9.0.0 - Fw910 = 0xB, //!< 9.1.0 - Invalid = 0xFF, //!< An invalid key generation type - }; - - enum class NcaKeyAreaEncryptionKeyType : u8 { - Application = 0x0, //!< This NCA uses the application key encryption area - Ocean = 0x1, //!< This NCA uses the ocean key encryption area - System = 0x2, //!< This NCA uses the system key encryption area - }; - - struct NcaFsEntry { - u32 startOffset; //!< The start offset of the filesystem in units of 0x200 bytes - u32 endOffset; //!< The start offset of the filesystem in units of 0x200 bytes - u64 _pad_; - }; - - enum class NcaSectionFsType : u8 { - RomFs = 0x0, //!< This section contains a RomFs filesystem - PFS0 = 0x1, //!< This section contains a PFS0 filesystem - }; - - enum class NcaSectionHashType : u8 { - HierarchicalSha256 = 0x2, //!< The hash header for this section is that of a PFS0 - HierarchicalIntegrity = 0x3, //!< The hash header for this section is that of a RomFS - }; - - enum class NcaSectionEncryptionType : u8 { - None = 0x1, //!< This NCA doesn't use any encryption - XTS = 0x2, //!< This NCA uses AES-XTS encryption - CTR = 0x3, //!< This NCA uses AES-CTR encryption - BKTR = 0x4, //!< This NCA uses BKTR together AES-CTR encryption - }; - - /** - * @brief The data for a single level of the hierarchical integrity scheme - */ - struct HierarchicalIntegrityLevel { - u64 offset; //!< The offset of the level data - u64 size; //!< The size of the level data - u32 blockSize; //!< The block size of the level data - u32 _pad_; - }; - static_assert(sizeof(HierarchicalIntegrityLevel) == 0x18); - - /** - * @brief The hash info header of the hierarchical integrity scheme - */ - struct HierarchicalIntegrityHashInfo { - u32 magic; //!< The hierarchical integrity magic, 'IVFC' - u32 magicNumber; //!< The magic number 0x2000 - u32 masterHashSize; //!< The size of the master hash - u32 numLevels; //!< The number of levels - std::array levels; //!< An array of the hierarchical integrity levels - u8 _pad0_[0x20]; - std::array masterHash; //!< The master hash of the hierarchical integrity system - u8 _pad1_[0x18]; - }; - static_assert(sizeof(HierarchicalIntegrityHashInfo) == 0xF8); - - /** - * @brief The hash info header of the SHA256 hashing scheme for PFS0 - */ - struct HierarchicalSha256HashInfo { - std::array hashTableHash; //!< A SHA256 hash over the hash table - u32 blockSize; //!< The block size of the filesystem - u32 _pad_; - u64 hashTableOffset; //!< The offset from the end of the section header of the hash table - u64 hashTableSize; //!< The size of the hash table - u64 pfs0Offset; //!< The offset from the end of the section header of the PFS0 - u64 pfs0Size; //!< The size of the PFS0 - u8 _pad1_[0xB0]; - }; - static_assert(sizeof(HierarchicalSha256HashInfo) == 0xF8); - - struct NcaSectionHeader { - u16 version; //!< The version, always 2 - NcaSectionFsType fsType; //!< The type of the filesystem in the section - NcaSectionHashType hashType; //!< The type of hash header that is used for this section - NcaSectionEncryptionType encryptionType; //!< The type of encryption that is used for this section - u8 _pad0_[0x3]; - union { - HierarchicalIntegrityHashInfo integrityHashInfo; //!< The HashInfo used for RomFS - HierarchicalSha256HashInfo sha256HashInfo; //!< The HashInfo used for PFS0 - }; - u8 _pad1_[0x40]; // PatchInfo - u32 generation; //!< The generation of the NCA section - u32 secureValue; //!< The secure value of the section - u8 _pad2_[0x30]; //!< SparseInfo - u8 _pad3_[0x88]; - }; - static_assert(sizeof(NcaSectionHeader) == 0x200); - - struct NcaHeader { - std::array fixed_key_sig; //!< An RSA-PSS signature over the header with fixed key - std::array npdm_key_sig; //!< An RSA-PSS signature over header with key in NPDM - u32 magic; //!< The magic of the NCA: 'NCA3' - NcaDistributionType distributionType; //!< Whether this NCA is from a gamecard or the E-Shop - NcaContentType contentType; - NcaLegacyKeyGenerationType legacyKeyGenerationType; //!< The keyblob to use for decryption - NcaKeyAreaEncryptionKeyType keyAreaEncryptionKeyType; //!< The index of the key area encryption key that is needed - u64 size; //!< The total size of the NCA - u64 programId; - u32 contentIndex; - u32 sdkVersion; //!< The version of the SDK the NCA was built with - NcaKeyGenerationType keyGenerationType; //!< The keyblob to use for decryption - u8 fixedKeyGeneration; //!< The fixed key index - u8 _pad0_[0xE]; - std::array rightsId; - std::array fsEntries; //!< The filesystem entries for this NCA - std::array, 4> sectionHashes; //!< SHA-256 hashes for each filesystem header - std::array, 4> encryptedKeyArea; //!< The encrypted key area for each filesystem - u8 _pad1_[0xC0]; - std::array sectionHeaders; - }; - static_assert(sizeof(NcaHeader) == 0xC00); - std::shared_ptr backing; std::shared_ptr keyStore; bool encrypted{false}; bool rightsIdEmpty; bool useKeyArea; + std::vector> files; + std::vector sections; + std::shared_ptr bktrBaseRomfs; + u64 bktrBaseIvfcOffset; - void ReadPfs0(const NcaSectionHeader §ionHeader, const NcaFsEntry &entry); + void ReadPfs0(const NCASectionHeader §ionHeader, const NCASectionTableEntry &entry); - void ReadRomFs(const NcaSectionHeader §ionHeader, const NcaFsEntry &entry); + void ReadRomFs(const NCASectionHeader §ionHeader, const NCASectionTableEntry &entry); - std::shared_ptr CreateBacking(const NcaSectionHeader §ionHeader, std::shared_ptr rawBacking, size_t offset); + std::shared_ptr CreateBacking(const NCASectionHeader §ionHeader, std::shared_ptr rawBacking, size_t offset); u8 GetKeyGeneration(); @@ -187,15 +331,29 @@ namespace skyline { crypto::KeyStore::Key128 GetKeyAreaKey(NcaSectionEncryptionType type); + void ValidateNCA(const NCASectionHeader §ionHeader); + + static RelocationBucket ConvertRelocationBucketRaw(RelocationBucketRaw raw) { + return {raw.numberEntries, raw.endOffset, {raw.relocationEntries.begin(), raw.relocationEntries.begin() + raw.numberEntries}}; + } + + static SubsectionBucket ConvertSubsectionBucketRaw(SubsectionBucketRaw raw) { + return {raw.numberEntries, raw.endOffset, {raw.subsectionEntries.begin(), raw.subsectionEntries.begin() + raw.numberEntries}}; + } + public: std::shared_ptr exeFs; //!< The PFS0 filesystem for this NCA's ExeFS section std::shared_ptr logo; //!< The PFS0 filesystem for this NCA's logo section std::shared_ptr cnmt; //!< The PFS0 filesystem for this NCA's CNMT section std::shared_ptr romFs; //!< The backing for this NCA's RomFS section - NcaHeader header; //!< The header of the NCA - NcaContentType contentType; //!< The content type of the NCA + NCAHeader header; //!< The header of the NCA + NCAContentType contentType; //!< The content type of the NCA + u64 ivfcOffset{0}; NCA(std::shared_ptr backing, std::shared_ptr keyStore, bool useKeyArea = false); + + NCA(std::optional updateNca, std::shared_ptr pKeyStore, std::shared_ptr bktrBaseRomfs, + u64 bktrBaseIvfcOffset, bool useKeyArea = false); }; } } diff --git a/app/src/main/cpp/skyline/vfs/patch_manager.cpp b/app/src/main/cpp/skyline/vfs/patch_manager.cpp new file mode 100644 index 000000000..b4b5727b7 --- /dev/null +++ b/app/src/main/cpp/skyline/vfs/patch_manager.cpp @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright © 2023 Strato Team and Contributors (https://github.com/strato-emu/) + +#include +#include +#include "patch_manager.h" +#include "region_backing.h" + +namespace skyline::vfs { + PatchManager::PatchManager() {} + + std::shared_ptr PatchManager::PatchExeFS(const DeviceState &state, std::shared_ptr exefs) { + auto updateProgramNCA{state.updateLoader->programNca}; + return updateProgramNCA->exeFs; + } + + std::shared_ptr PatchManager::PatchRomFS(const DeviceState &state, std::optional nca, u64 ivfcOffset) { + auto newNca{std::make_shared(nca, state.os->keyStore, state.loader->programNca->romFs, ivfcOffset)}; + return newNca->romFs; + } +} diff --git a/app/src/main/cpp/skyline/vfs/patch_manager.h b/app/src/main/cpp/skyline/vfs/patch_manager.h new file mode 100644 index 000000000..d99858755 --- /dev/null +++ b/app/src/main/cpp/skyline/vfs/patch_manager.h @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright © 2023 Strato Team and Contributors (https://github.com/strato-emu/) + +#pragma once + +#include +#include + +namespace skyline::vfs { + class PatchManager { + public: + PatchManager(); + + std::shared_ptr PatchRomFS(const DeviceState &state, std::optional nca , u64 ivfcOffset); + + std::shared_ptr PatchExeFS(const DeviceState &state, std::shared_ptr exefs); + }; +} diff --git a/app/src/main/java/org/stratoemu/strato/AppDialog.kt b/app/src/main/java/org/stratoemu/strato/AppDialog.kt index 49ec42c15..fca017efa 100644 --- a/app/src/main/java/org/stratoemu/strato/AppDialog.kt +++ b/app/src/main/java/org/stratoemu/strato/AppDialog.kt @@ -20,7 +20,7 @@ import androidx.appcompat.app.AlertDialog import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.snackbar.Snackbar -import org.stratoemu.strato.data.AppItem +import org.stratoemu.strato.data.BaseAppItem import org.stratoemu.strato.data.AppItemTag import org.stratoemu.strato.databinding.AppDialogBinding import org.stratoemu.strato.loader.LoaderResult @@ -34,9 +34,9 @@ import org.stratoemu.strato.utils.serializable class AppDialog : BottomSheetDialogFragment() { companion object { /** - * @param item This is used to hold the [AppItem] between instances + * @param item This is used to hold the [BaseAppItem] between instances */ - fun newInstance(item : AppItem) : AppDialog { + fun newInstance(item : BaseAppItem) : AppDialog { val args = Bundle() args.putSerializable(AppItemTag, item) @@ -48,7 +48,7 @@ class AppDialog : BottomSheetDialogFragment() { private lateinit var binding : AppDialogBinding - private val item by lazy { requireArguments().serializable(AppItemTag)!! } + private val item by lazy { requireArguments().serializable(AppItemTag)!! } /** * Used to manage save files @@ -107,7 +107,7 @@ class AppDialog : BottomSheetDialogFragment() { binding.gamePin.setOnClickListener { val info = ShortcutInfo.Builder(context, item.title) - info.setShortLabel(item.title) + item.title?.let { title -> info.setShortLabel(title) } info.setActivity(ComponentName(requireContext(), EmulationActivity::class.java)) info.setIcon(Icon.createWithAdaptiveBitmap(item.bitmapIcon)) diff --git a/app/src/main/java/org/stratoemu/strato/EmulationActivity.kt b/app/src/main/java/org/stratoemu/strato/EmulationActivity.kt index 95220e357..db2145dc1 100644 --- a/app/src/main/java/org/stratoemu/strato/EmulationActivity.kt +++ b/app/src/main/java/org/stratoemu/strato/EmulationActivity.kt @@ -21,6 +21,7 @@ import android.graphics.PointF import android.graphics.drawable.Icon import android.hardware.display.DisplayManager import android.net.DhcpInfo +import android.net.Uri import android.net.wifi.WifiManager import android.os.* import android.util.Log @@ -49,6 +50,7 @@ import kotlinx.coroutines.launch import org.stratoemu.strato.applet.swkbd.SoftwareKeyboardConfig import org.stratoemu.strato.applet.swkbd.SoftwareKeyboardDialog import org.stratoemu.strato.data.AppItem +import org.stratoemu.strato.data.BaseAppItem import org.stratoemu.strato.data.AppItemTag import org.stratoemu.strato.databinding.EmuActivityBinding import org.stratoemu.strato.emulation.PipelineLoadingFragment @@ -89,10 +91,14 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo private val binding by lazy { EmuActivityBinding.inflate(layoutInflater) } /** - * The [AppItem] of the app that is being emulated + * The [BaseAppItem] of the app that is being emulated */ lateinit var item : AppItem + lateinit var dlcUris : ArrayList + + lateinit var updateUri : Uri + /** * The built-in [Vibrator] of the device */ @@ -147,7 +153,7 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo * @param nativeLibraryPath The full path to the app native library directory * @param assetManager The asset manager used for accessing app assets */ - private external fun executeApplication(romUri : String, romType : Int, romFd : Int, nativeSettings : NativeSettings, publicAppFilesPath : String, privateAppFilesPath : String, nativeLibraryPath : String, assetManager : AssetManager) + private external fun executeApplication(romUri : String, romType : Int, romFd : Int, dlcFds : IntArray?, updateFd : Int, nativeSettings : NativeSettings, publicAppFilesPath : String, privateAppFilesPath : String, nativeLibraryPath : String, assetManager : AssetManager) /** * @param join If the function should only return after all the threads join or immediately @@ -249,9 +255,19 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo @SuppressLint("Recycle") val romFd = contentResolver.openFileDescriptor(rom, "r")!! + var dlcFds : IntArray? = null + if (dlcUris.isNotEmpty()) + dlcFds = dlcUris.map { contentResolver.openFileDescriptor(it, "r")!!.detachFd() }.toIntArray() + + var updateFd : Int = -1 + if (updateUri != Uri.EMPTY) { + @SuppressLint("Recycle") + updateFd = contentResolver.openFileDescriptor(updateUri, "r")!!.detachFd() + } + GpuDriverHelper.ensureFileRedirectDir(this) emulationThread = Thread { - executeApplication(rom.toString(), romType, romFd.detachFd(), NativeSettings(this, emulationSettings), applicationContext.getPublicFilesDir().canonicalPath + "/", applicationContext.filesDir.canonicalPath + "/", applicationInfo.nativeLibraryDir + "/", assets) + executeApplication(rom.toString(), romType, romFd.detachFd(), dlcFds, updateFd, NativeSettings(this, emulationSettings), applicationContext.getPublicFilesDir().canonicalPath + "/", applicationContext.filesDir.canonicalPath + "/", applicationInfo.nativeLibraryDir + "/", assets) returnFromEmulation() } @@ -265,6 +281,10 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo val intentItem = intent.serializable(AppItemTag) as AppItem? if (intentItem != null) { item = intentItem + + dlcUris = item.getEnabledDlcs().map { it.uri }.toCollection(ArrayList()) + + updateUri = item.getEnabledUpdate()?.uri ?: Uri.EMPTY return } @@ -273,7 +293,7 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo val romFormat = getRomFormat(uri, contentResolver) val romFile = RomFile(this, romFormat, uri, EmulationSettings.global.systemLanguage) - item = AppItem(romFile.takeIf { it.valid }!!.appEntry) + item = AppItem(romFile.takeIf { it.valid }!!.appEntry, emptyList(), emptyList()) } @SuppressLint("SetTextI18n", "ClickableViewAccessibility") diff --git a/app/src/main/java/org/stratoemu/strato/MainActivity.kt b/app/src/main/java/org/stratoemu/strato/MainActivity.kt index 9e2978e5c..ce4899a76 100644 --- a/app/src/main/java/org/stratoemu/strato/MainActivity.kt +++ b/app/src/main/java/org/stratoemu/strato/MainActivity.kt @@ -29,16 +29,19 @@ import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint import org.stratoemu.strato.adapter.* import org.stratoemu.strato.data.AppItem +import org.stratoemu.strato.data.BaseAppItem import org.stratoemu.strato.data.AppItemTag import org.stratoemu.strato.databinding.MainActivityBinding import org.stratoemu.strato.loader.AppEntry import org.stratoemu.strato.loader.LoaderResult +import org.stratoemu.strato.loader.RomType import org.stratoemu.strato.provider.DocumentsProvider import org.stratoemu.strato.settings.AppSettings import org.stratoemu.strato.settings.EmulationSettings import org.stratoemu.strato.settings.SettingsActivity import org.stratoemu.strato.utils.GpuDriverHelper import org.stratoemu.strato.utils.WindowInsetsHelper +import java.util.Collections import javax.inject.Inject import kotlin.math.ceil import com.google.android.material.R as MaterialR @@ -88,7 +91,7 @@ class MainActivity : AppCompatActivity() { if (appSettings.refreshRequired) loadRoms(false) } - private fun AppItem.toViewItem() = AppViewItem(layoutType, this, ::selectStartGame, ::selectShowGameDialog) + private fun BaseAppItem.toViewItem() = AppViewItem(layoutType, this, ::selectStartGame, ::selectShowGameDialog) override fun onCreate(savedInstanceState : Bundle?) { // Need to create new instance of settings, dependency injection happens @@ -216,7 +219,9 @@ class MainActivity : AppCompatActivity() { private fun getAppItems() = mutableListOf().apply { appEntries?.let { entries -> sortGameList(entries.toList()).forEach { entry -> - add(AppItem(entry).toViewItem()) + val updates : List = entries.filter { it.romType == RomType.Update && it.parentTitleId == entry.titleId }.map { BaseAppItem(it, true) } + val dlcs : List = entries.filter { it.romType == RomType.DLC && it.parentTitleId == entry.titleId }.map { BaseAppItem(it, true) } + add(AppItem(entry, updates, dlcs).toViewItem()) } } } @@ -224,7 +229,7 @@ class MainActivity : AppCompatActivity() { private fun sortGameList(gameList : List) : List { val sortedApps : MutableList = mutableListOf() gameList.forEach { entry -> - if (!appSettings.filterInvalidFiles || entry.loaderResult != LoaderResult.ParsingError) + if (validateAppEntry(entry)) sortedApps.add(entry) } when (appSettings.sortAppsBy) { @@ -234,6 +239,11 @@ class MainActivity : AppCompatActivity() { return sortedApps } + private fun validateAppEntry(entry : AppEntry) : Boolean { + // Unknown ROMs are shown because NROs have this type + return !appSettings.filterInvalidFiles || entry.loaderResult != LoaderResult.ParsingError && (entry.romType == RomType.Base || entry.romType == RomType.Unknown) + } + private fun handleState(state : MainState) = when (state) { MainState.Loading -> { binding.refreshIcon.apply { animate().rotation(rotation - 180f) } @@ -250,7 +260,7 @@ class MainActivity : AppCompatActivity() { is MainState.Error -> Snackbar.make(findViewById(android.R.id.content), getString(R.string.error) + ": ${state.ex.localizedMessage}", Snackbar.LENGTH_SHORT).show() } - private fun selectStartGame(appItem : AppItem) { + private fun selectStartGame(appItem : BaseAppItem) { if (binding.swipeRefreshLayout.isRefreshing) return if (appSettings.selectAction) { @@ -263,7 +273,7 @@ class MainActivity : AppCompatActivity() { } } - private fun selectShowGameDialog(appItem : AppItem) { + private fun selectShowGameDialog(appItem : BaseAppItem) { if (binding.swipeRefreshLayout.isRefreshing) return AppDialog.newInstance(appItem).show(supportFragmentManager, "game") diff --git a/app/src/main/java/org/stratoemu/strato/adapter/AppViewItem.kt b/app/src/main/java/org/stratoemu/strato/adapter/AppViewItem.kt index e6f0c810b..b7cdda393 100644 --- a/app/src/main/java/org/stratoemu/strato/adapter/AppViewItem.kt +++ b/app/src/main/java/org/stratoemu/strato/adapter/AppViewItem.kt @@ -17,7 +17,7 @@ import android.widget.RelativeLayout import android.widget.TextView import androidx.viewbinding.ViewBinding import org.stratoemu.strato.R -import org.stratoemu.strato.data.AppItem +import org.stratoemu.strato.data.BaseAppItem import org.stratoemu.strato.databinding.AppItemGridBinding import org.stratoemu.strato.databinding.AppItemGridCompactBinding import org.stratoemu.strato.databinding.AppItemLinearBinding @@ -86,9 +86,9 @@ class GridCompatBinding(parent : ViewGroup) : LayoutBinding Unit +private typealias InteractionFunction = (appItem : BaseAppItem) -> Unit -class AppViewItem(var layoutType : LayoutType, private val item : AppItem, private val onClick : InteractionFunction, private val onLongClick : InteractionFunction) : GenericListItem>() { +class AppViewItem(var layoutType : LayoutType, private val item : BaseAppItem, private val onClick : InteractionFunction, private val onLongClick : InteractionFunction) : GenericListItem>() { override fun getViewBindingFactory() = LayoutBindingFactory(layoutType) override fun bind(holder : GenericViewHolder>, position : Int) { @@ -116,7 +116,7 @@ class AppViewItem(var layoutType : LayoutType, private val item : AppItem, priva binding.root.findViewById(R.id.item_card)?.let { handleClicks(it) } } - private fun showIconDialog(context : Context, appItem : AppItem) { + private fun showIconDialog(context : Context, appItem : BaseAppItem) { val builder = Dialog(context) builder.requestWindowFeature(Window.FEATURE_NO_TITLE) builder.window!!.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) diff --git a/app/src/main/java/org/stratoemu/strato/data/AppItem.kt b/app/src/main/java/org/stratoemu/strato/data/AppItem.kt index 857131ecb..9d79c84f6 100644 --- a/app/src/main/java/org/stratoemu/strato/data/AppItem.kt +++ b/app/src/main/java/org/stratoemu/strato/data/AppItem.kt @@ -5,84 +5,17 @@ package org.stratoemu.strato.data -import android.content.Context -import android.graphics.Bitmap -import androidx.core.content.ContextCompat -import androidx.core.graphics.drawable.toBitmap -import org.stratoemu.strato.BuildConfig -import org.stratoemu.strato.R -import org.stratoemu.strato.StratoApplication import org.stratoemu.strato.loader.AppEntry -import org.stratoemu.strato.loader.LoaderResult import java.io.Serializable -/** - * The tag used to pass [AppItem]s between activities and fragments - */ -const val AppItemTag = BuildConfig.APPLICATION_ID + ".APP_ITEM" - -private val missingIcon by lazy { ContextCompat.getDrawable(StratoApplication.instance, R.drawable.default_icon)!!.toBitmap(256, 256) } - -/** - * This class is a wrapper around [AppEntry], it is used for passing around game metadata - */ @Suppress("SERIAL") -data class AppItem(private val meta : AppEntry) : Serializable { - /** - * The icon of the application - */ - val icon get() = meta.icon - - val bitmapIcon : Bitmap get() = meta.icon ?: missingIcon - - /** - * The title of the application - */ - val title get() = meta.name - - /** - * The title ID of the application - */ - val titleId get() = meta.titleId - - /** - * The application version - */ - val version get() = meta.version - - /** - * The application author - */ - val author get() = meta.author - - /** - * The URI of the application's image file - */ - val uri get() = meta.uri - - /** - * The format of the application - */ - val format get() = meta.format - - val loaderResult get() = meta.loaderResult - - fun loaderResultString(context : Context) = context.getString( - when (meta.loaderResult) { - LoaderResult.Success -> R.string.metadata_missing - - LoaderResult.ParsingError -> R.string.invalid_file - - LoaderResult.MissingTitleKey -> R.string.missing_title_key +class AppItem(meta : AppEntry, private val updates : List, private val dlcs : List) : BaseAppItem(meta), Serializable { - LoaderResult.MissingHeaderKey, - LoaderResult.MissingTitleKek, - LoaderResult.MissingKeyArea -> R.string.incomplete_prod_keys - } - ) + fun getEnabledDlcs() : List { + return dlcs.filter { it.enabled } + } - /** - * The name and author is used as the key - */ - fun key() = "${meta.name}${meta.author.let { it ?: "" }}" + fun getEnabledUpdate() : BaseAppItem? { + return updates.firstOrNull { it.enabled } + } } diff --git a/app/src/main/java/org/stratoemu/strato/data/BaseAppItem.kt b/app/src/main/java/org/stratoemu/strato/data/BaseAppItem.kt new file mode 100644 index 000000000..0bd5cac4f --- /dev/null +++ b/app/src/main/java/org/stratoemu/strato/data/BaseAppItem.kt @@ -0,0 +1,88 @@ +/* + * SPDX-License-Identifier: MPL-2.0 + * Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/) + */ + +package org.stratoemu.strato.data + +import android.content.Context +import android.graphics.Bitmap +import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.toBitmap +import org.stratoemu.strato.BuildConfig +import org.stratoemu.strato.R +import org.stratoemu.strato.StratoApplication +import org.stratoemu.strato.loader.AppEntry +import org.stratoemu.strato.loader.LoaderResult +import java.io.Serializable + +/** + * The tag used to pass [BaseAppItem]s between activities and fragments + */ +const val AppItemTag = BuildConfig.APPLICATION_ID + ".APP_ITEM" + +private val missingIcon by lazy { ContextCompat.getDrawable(StratoApplication.instance, R.drawable.default_icon)!!.toBitmap(256, 256) } + +/** + * This class is a wrapper around [AppEntry], it is used for passing around game metadata + */ +@Suppress("SERIAL") +open class BaseAppItem(private val meta : AppEntry, val enabled: Boolean = false) : Serializable { + /** + * The icon of the application + */ + val icon get() = meta.icon + + val bitmapIcon : Bitmap get() = meta.icon ?: missingIcon + + /** + * The title of the application + */ + val title get() = meta.name + + /** + * The title ID of the application + */ + val titleId get() = meta.titleId + + /** + * The application version + */ + val version get() = meta.version + + /** + * The application author + */ + val author get() = meta.author + + /** + * The URI of the application's image file + */ + val uri get() = meta.uri + + /** + * The format of the application + */ + val format get() = meta.format + + val loaderResult get() = meta.loaderResult + + fun loaderResultString(context : Context) = context.getString( + when (meta.loaderResult) { + LoaderResult.Success -> R.string.metadata_missing + + LoaderResult.ParsingError -> R.string.invalid_file + + LoaderResult.MissingTitleKey -> R.string.missing_title_key + + LoaderResult.MissingHeaderKey, + LoaderResult.MissingTitleKek, + LoaderResult.MissingKeyArea -> R.string.incomplete_prod_keys + } + ) + + /** + * The name and author is used as the key + */ + fun key() = "${meta.name}${meta.author.let { it ?: "" }}" +} diff --git a/app/src/main/java/org/stratoemu/strato/emulation/PipelineLoadingFragment.kt b/app/src/main/java/org/stratoemu/strato/emulation/PipelineLoadingFragment.kt index 91894ace7..5f75ece61 100644 --- a/app/src/main/java/org/stratoemu/strato/emulation/PipelineLoadingFragment.kt +++ b/app/src/main/java/org/stratoemu/strato/emulation/PipelineLoadingFragment.kt @@ -15,7 +15,7 @@ import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import com.google.android.renderscript.Toolkit -import org.stratoemu.strato.data.AppItem +import org.stratoemu.strato.data.BaseAppItem import org.stratoemu.strato.data.AppItemTag import org.stratoemu.strato.databinding.PipelineLoadingBinding import org.stratoemu.strato.utils.serializable @@ -24,7 +24,7 @@ private const val TotalPipelineCountTag = "PipelineLoadingFragment::TotalCount" private const val PipelineProgressTag = "PipelineLoadingFragment::Progress" class PipelineLoadingFragment : Fragment() { - private val item by lazy { requireArguments().serializable(AppItemTag)!! } + private val item by lazy { requireArguments().serializable(AppItemTag)!! } private val totalPipelineCount by lazy { requireArguments().getInt(TotalPipelineCountTag) } private lateinit var binding : PipelineLoadingBinding @@ -69,7 +69,7 @@ class PipelineLoadingFragment : Fragment() { } companion object { - fun newInstance(item : AppItem, totalPipelineCount : Int) = PipelineLoadingFragment().apply { + fun newInstance(item : BaseAppItem, totalPipelineCount : Int) = PipelineLoadingFragment().apply { arguments = Bundle().apply { putSerializable(AppItemTag, item) putInt(TotalPipelineCountTag, totalPipelineCount) diff --git a/app/src/main/java/org/stratoemu/strato/loader/RomFile.kt b/app/src/main/java/org/stratoemu/strato/loader/RomFile.kt index 619fa9771..fdad7dc50 100644 --- a/app/src/main/java/org/stratoemu/strato/loader/RomFile.kt +++ b/app/src/main/java/org/stratoemu/strato/loader/RomFile.kt @@ -28,6 +28,17 @@ enum class RomFormat(val format : Int) { NSP(4), } +enum class RomType(val value: Int) { + Unknown(0), + Base(128), + Update(129), + DLC(130); + + companion object { + fun getType(value: Int) = values().firstOrNull { it.value == value } ?: throw IllegalArgumentException("Invalid type: $value") + } +} + /** * This resolves the format of a ROM from it's URI so we can determine formats for ROMs launched from arbitrary locations * @@ -64,20 +75,18 @@ enum class LoaderResult(val value : Int) { * This class is used to hold an application's metadata in a serializable way */ data class AppEntry( - var name : String, + var name : String?, var version : String?, var titleId : String?, + var addOnContentBaseId : String?, var author : String?, var icon : Bitmap?, + var romType : RomType?, + var parentTitleId : String?, var format : RomFormat, var uri : Uri, var loaderResult : LoaderResult ) : Serializable { - constructor(context : Context, format : RomFormat, uri : Uri, loaderResult : LoaderResult) : this(context.contentResolver.query(uri, null, null, null, null)?.use { cursor -> - val nameIndex : Int = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) - cursor.moveToFirst() - cursor.getString(nameIndex) - }!!.dropLast(format.name.length + 1), null, null, null, null, format, uri, loaderResult) private fun writeObject(output : ObjectOutputStream) { output.writeUTF(name) @@ -89,10 +98,19 @@ data class AppEntry( output.writeBoolean(titleId != null) if (titleId != null) output.writeUTF(titleId) + output.writeBoolean(addOnContentBaseId != null) + if (addOnContentBaseId != null) + output.writeUTF(addOnContentBaseId) output.writeBoolean(author != null) if (author != null) output.writeUTF(author) output.writeInt(loaderResult.value) + output.writeBoolean(romType != null) + if (romType != null) + output.writeObject(romType) + output.writeBoolean(parentTitleId != null) + if (parentTitleId != null) + output.writeUTF(parentTitleId) output.writeBoolean(icon != null) icon?.let { @Suppress("DEPRECATION") @@ -111,9 +129,15 @@ data class AppEntry( version = input.readUTF() if (input.readBoolean()) titleId = input.readUTF() + if (input.readBoolean()) + addOnContentBaseId = input.readUTF() if (input.readBoolean()) author = input.readUTF() loaderResult = LoaderResult.get(input.readInt()) + if (input.readBoolean()) + romType = input.readObject() as RomType + if (input.readBoolean()) + parentTitleId = input.readUTF() if (input.readBoolean()) icon = BitmapFactory.decodeStream(input) } @@ -140,6 +164,11 @@ internal class RomFile(context : Context, format : RomFormat, uri : Uri, systemL */ private var applicationTitleId : String? = null + /** + * @note This field is filled in by native code + */ + private var addOnContentBaseId : String? = null + /** * @note This field is filled in by native code */ @@ -150,11 +179,18 @@ internal class RomFile(context : Context, format : RomFormat, uri : Uri, systemL */ private var applicationAuthor : String? = null + /** + * @note This field is filled in by native code + */ + private var parentTitleId : String? = null + /** * @note This field is filled in by native code */ private var rawIcon : ByteArray? = null + private var romTypeInt : Int = 0 + val appEntry : AppEntry var result = LoaderResult.Success @@ -167,17 +203,19 @@ internal class RomFile(context : Context, format : RomFormat, uri : Uri, systemL result = LoaderResult.get(populate(format.ordinal, it.fd, "${context.filesDir.canonicalPath}/keys/", systemLanguage)) } - appEntry = applicationName?.let { name -> - applicationVersion?.let { version -> - applicationTitleId?.let { titleId -> - applicationAuthor?.let { author -> - rawIcon?.let { icon -> - AppEntry(name, version, titleId, author, BitmapFactory.decodeByteArray(icon, 0, icon.size), format, uri, result) - } - } - } - } - } ?: AppEntry(context, format, uri, result) + appEntry = AppEntry( + applicationName ?: "", + applicationVersion ?: "", + applicationTitleId ?: "", + addOnContentBaseId ?: "", + applicationAuthor ?: "", + rawIcon?.let { BitmapFactory.decodeByteArray(it, 0, it.size) }, + romTypeInt.let { RomType.getType(it) }, + parentTitleId ?: "", + format, + uri, + result + ) } /** diff --git a/app/src/main/java/org/stratoemu/strato/preference/GpuDriverActivity.kt b/app/src/main/java/org/stratoemu/strato/preference/GpuDriverActivity.kt index 46e6b83d0..eadd6dcb9 100644 --- a/app/src/main/java/org/stratoemu/strato/preference/GpuDriverActivity.kt +++ b/app/src/main/java/org/stratoemu/strato/preference/GpuDriverActivity.kt @@ -22,7 +22,7 @@ import org.stratoemu.strato.adapter.GenericListItem import org.stratoemu.strato.adapter.GpuDriverViewItem import org.stratoemu.strato.adapter.SelectableGenericAdapter import org.stratoemu.strato.adapter.SpacingItemDecoration -import org.stratoemu.strato.data.AppItem +import org.stratoemu.strato.data.BaseAppItem import org.stratoemu.strato.data.AppItemTag import org.stratoemu.strato.databinding.GpuDriverActivityBinding import org.stratoemu.strato.settings.EmulationSettings @@ -41,7 +41,7 @@ import kotlinx.coroutines.launch class GpuDriverActivity : AppCompatActivity() { private val binding by lazy { GpuDriverActivityBinding.inflate(layoutInflater) } - private val item by lazy { intent.extras?.serializable(AppItemTag) as AppItem? } + private val item by lazy { intent.extras?.serializable(AppItemTag) as BaseAppItem? } private val adapter = SelectableGenericAdapter(0) @@ -142,7 +142,7 @@ class GpuDriverActivity : AppCompatActivity() { emulationSettings = if (item == null) { EmulationSettings.global } else { - val appItem = item as AppItem + val appItem = item as BaseAppItem EmulationSettings.forTitleId(appItem.titleId ?: appItem.key()) } diff --git a/app/src/main/java/org/stratoemu/strato/preference/GpuDriverPreference.kt b/app/src/main/java/org/stratoemu/strato/preference/GpuDriverPreference.kt index 278d63c17..3843a7ca1 100644 --- a/app/src/main/java/org/stratoemu/strato/preference/GpuDriverPreference.kt +++ b/app/src/main/java/org/stratoemu/strato/preference/GpuDriverPreference.kt @@ -13,7 +13,7 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.preference.Preference import androidx.preference.Preference.SummaryProvider import androidx.preference.R as AndroidR -import org.stratoemu.strato.data.AppItem +import org.stratoemu.strato.data.BaseAppItem import org.stratoemu.strato.data.AppItemTag import org.stratoemu.strato.settings.EmulationSettings import org.stratoemu.strato.utils.GpuDriverHelper @@ -31,7 +31,7 @@ class GpuDriverPreference @JvmOverloads constructor(context : Context, attrs : A * The app item being configured, used to load the correct settings in [GpuDriverActivity] * This is populated by [org.stratoemu.strato.settings.GameSettingsFragment] */ - var item : AppItem? = null + var item : BaseAppItem? = null init { val supportsCustomDriverLoading = GpuDriverHelper.supportsCustomDriverLoading() diff --git a/app/src/main/java/org/stratoemu/strato/settings/GameSettingsFragment.kt b/app/src/main/java/org/stratoemu/strato/settings/GameSettingsFragment.kt index 65104f364..bd1418124 100644 --- a/app/src/main/java/org/stratoemu/strato/settings/GameSettingsFragment.kt +++ b/app/src/main/java/org/stratoemu/strato/settings/GameSettingsFragment.kt @@ -11,7 +11,7 @@ import androidx.appcompat.app.AppCompatActivity import androidx.preference.* import org.stratoemu.strato.BuildConfig import org.stratoemu.strato.R -import org.stratoemu.strato.data.AppItem +import org.stratoemu.strato.data.BaseAppItem import org.stratoemu.strato.data.AppItemTag import org.stratoemu.strato.preference.GpuDriverPreference import org.stratoemu.strato.utils.GpuDriverHelper @@ -22,7 +22,7 @@ import org.stratoemu.strato.utils.serializable * This fragment is used to display custom game preferences */ class GameSettingsFragment : PreferenceFragmentCompat() { - private val item by lazy { requireArguments().serializable(AppItemTag)!! } + private val item by lazy { requireArguments().serializable(AppItemTag)!! } override fun onViewCreated(view : View, savedInstanceState : Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/app/src/main/res/xml/app_preferences.xml b/app/src/main/res/xml/app_preferences.xml index 3e0becd6a..febee014c 100644 --- a/app/src/main/res/xml/app_preferences.xml +++ b/app/src/main/res/xml/app_preferences.xml @@ -64,7 +64,7 @@ app:key="select_action" app:title="@string/select_action" />