mirror of
https://github.com/Ishan09811/pine.git
synced 2025-04-24 08:55:10 +00:00
Vfs part 1 Update and DLCs (#62)
Co-authored-by: Dzmitry Dubrova <dimaxdqwerty@gmail.com>
This commit is contained in:
parent
184ee1ab26
commit
d165984c89
@ -257,6 +257,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
|
||||
|
@ -73,6 +73,8 @@ extern "C" JNIEXPORT void Java_emu_skyline_EmulationActivity_executeApplication(
|
||||
jstring romUriJstring,
|
||||
jint romType,
|
||||
jint romFd,
|
||||
jintArray dlcFds,
|
||||
jint updateFd,
|
||||
jobject settingsInstance,
|
||||
jstring publicAppFilesPathJstring,
|
||||
jstring privateAppFilesPathJstring,
|
||||
@ -87,6 +89,12 @@ extern "C" JNIEXPORT void Java_emu_skyline_EmulationActivity_executeApplication(
|
||||
|
||||
auto jvmManager{std::make_shared<skyline::JvmManager>(env, instance)};
|
||||
|
||||
jsize dlcArrSize = dlcFds != nullptr ? env->GetArrayLength(dlcFds) : 0;
|
||||
std::vector<int> dlcFdsVector(dlcArrSize);
|
||||
|
||||
if (dlcArrSize > 0)
|
||||
env->GetIntArrayRegion(dlcFds, 0, dlcArrSize, &dlcFdsVector[0]);
|
||||
|
||||
std::shared_ptr<skyline::Settings> settings{std::make_shared<skyline::AndroidSettings>(env, settingsInstance)};
|
||||
|
||||
skyline::JniString publicAppFilesPath(env, publicAppFilesPathJstring);
|
||||
@ -126,7 +134,7 @@ extern "C" JNIEXPORT void Java_emu_skyline_EmulationActivity_executeApplication(
|
||||
|
||||
LOGDNF("Launching ROM {}", skyline::JniString(env, romUriJstring));
|
||||
|
||||
os->Execute(romFd, static_cast<skyline::loader::RomFormat>(romType));
|
||||
os->Execute(romFd, dlcFdsVector, updateFd, static_cast<skyline::loader::RomFormat>(romType));
|
||||
} catch (std::exception &e) {
|
||||
LOGENF("An uncaught exception has occurred: {}", e.what());
|
||||
} catch (const skyline::signal::SignalException &e) {
|
||||
@ -144,6 +152,13 @@ extern "C" JNIEXPORT void Java_emu_skyline_EmulationActivity_executeApplication(
|
||||
|
||||
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_emu_skyline_EmulationActivity_stopEmulation(JNIEnv *, jobject, jboolean join) {
|
||||
|
@ -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_emu_skyline_loader_RomFile_populate(JNIEn
|
||||
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<skyline::language::SystemLanguage>(systemLanguage))};
|
||||
@ -62,6 +66,7 @@ extern "C" JNIEXPORT jint JNICALL Java_emu_skyline_loader_RomFile_populate(JNIEn
|
||||
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_emu_skyline_loader_RomFile_populate(JNIEn
|
||||
env->SetObjectField(thiz, rawIconField, iconByteArray);
|
||||
}
|
||||
|
||||
if (loader->cnmt) {
|
||||
auto contentMetaType{loader->cnmt->GetContentMetaType()};
|
||||
env->SetIntField(thiz, romType, static_cast<skyline::u8>(contentMetaType));
|
||||
|
||||
if (contentMetaType != skyline::vfs::ContentMetaType::Application)
|
||||
env->SetObjectField(thiz, parentTitleId, env->NewStringUTF(loader->cnmt->GetParentTitleId().c_str()));
|
||||
}
|
||||
|
||||
return static_cast<jint>(skyline::loader::LoaderResult::Success);
|
||||
}
|
||||
|
||||
@ -98,7 +111,7 @@ extern "C" JNIEXPORT jstring Java_emu_skyline_preference_FirmwareImportPreferenc
|
||||
std::shared_ptr<skyline::vfs::Backing> 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<skyline::vfs::RomFileSystem>(nca.romFs)};
|
||||
auto file{controlRomFs->OpenFile("file")};
|
||||
SystemVersion systemVersion;
|
||||
@ -165,7 +178,7 @@ extern "C" JNIEXPORT void Java_emu_skyline_preference_FirmwareImportPreference_e
|
||||
std::shared_ptr<skyline::vfs::Backing> 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<skyline::vfs::RomFileSystem>(nca.romFs)};
|
||||
|
||||
for (auto fileEntry = controlRomFs->fileMap.begin(); fileEntry != controlRomFs->fileMap.end(); fileEntry++) {
|
||||
|
@ -68,6 +68,8 @@ namespace skyline {
|
||||
std::shared_ptr<JvmManager> jvm;
|
||||
std::shared_ptr<Settings> settings;
|
||||
std::shared_ptr<loader::Loader> loader;
|
||||
std::vector<std::shared_ptr<loader::Loader>> dlcLoaders;
|
||||
std::shared_ptr<loader::Loader> updateLoader;
|
||||
std::shared_ptr<nce::NCE> nce;
|
||||
std::shared_ptr<jit::Jit32> jit32;
|
||||
std::shared_ptr<kernel::type::KProcess> process{};
|
||||
|
@ -5,6 +5,8 @@
|
||||
|
||||
#include <linux/elf.h>
|
||||
#include <vfs/nacp.h>
|
||||
#include <vfs/cnmt.h>
|
||||
#include <vfs/nca.h>
|
||||
#include <common/signal.h>
|
||||
#include "executable.h"
|
||||
|
||||
@ -39,6 +41,9 @@ namespace skyline::loader {
|
||||
MissingTitleKey,
|
||||
MissingTitleKek,
|
||||
MissingKeyArea,
|
||||
|
||||
ErrorSparseNCA,
|
||||
ErrorCompressedNCA,
|
||||
};
|
||||
|
||||
/**
|
||||
@ -92,6 +97,10 @@ namespace skyline::loader {
|
||||
ExecutableLoadInfo LoadExecutable(const std::shared_ptr<kernel::type::KProcess> &process, const DeviceState &state, Executable &executable, size_t offset = 0, const std::string &name = {}, bool dynamicallyLinked = false);
|
||||
|
||||
std::optional<vfs::NACP> nacp;
|
||||
std::optional<vfs::CNMT> cnmt;
|
||||
std::optional<vfs::NCA> programNca; //!< The main program NCA within the NSP
|
||||
std::optional<vfs::NCA> controlNca; //!< The main control NCA within the NSP
|
||||
std::optional<vfs::NCA> publicNca;
|
||||
std::shared_ptr<vfs::Backing> romFs;
|
||||
|
||||
virtual ~Loader() = default;
|
||||
|
@ -5,6 +5,7 @@
|
||||
#include <vfs/ticket.h>
|
||||
#include "nca.h"
|
||||
#include "nsp.h"
|
||||
#include "vfs/patch_manager.h"
|
||||
|
||||
namespace skyline::loader {
|
||||
static void ExtractTickets(const std::shared_ptr<vfs::PartitionFileSystem>& dir, const std::shared_ptr<crypto::KeyStore> &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<vfs::RomFileSystem>(controlNca->romFs);
|
||||
nacp.emplace(controlRomFs->OpenFile("control.nacp"));
|
||||
if (controlNca) {
|
||||
controlRomFs = std::make_shared<vfs::RomFileSystem>(controlNca->romFs);
|
||||
nacp.emplace(controlRomFs->OpenFile("control.nacp"));
|
||||
}
|
||||
|
||||
if (metaNca)
|
||||
cnmt = vfs::CNMT(metaNca->cnmt);
|
||||
}
|
||||
|
||||
void *NspLoader::LoadProcessData(const std::shared_ptr<kernel::type::KProcess> &process, const DeviceState &state) {
|
||||
if (state.updateLoader) {
|
||||
auto patchManager{std::make_shared<vfs::PatchManager>()};
|
||||
programNca->exeFs = patchManager->PatchExeFS(state, programNca->exeFs);
|
||||
}
|
||||
|
||||
process->npdm = vfs::NPDM(programNca->exeFs->OpenFile("main.npdm"));
|
||||
return NcaLoader::LoadExeFs(this, programNca->exeFs, process, state);
|
||||
}
|
||||
|
@ -18,8 +18,7 @@ namespace skyline::loader {
|
||||
private:
|
||||
std::shared_ptr<vfs::PartitionFileSystem> nsp; //!< A shared pointer to the NSP's PFS0
|
||||
std::shared_ptr<vfs::RomFileSystem> controlRomFs; //!< A shared pointer to the control NCA's RomFS
|
||||
std::optional<vfs::NCA> programNca; //!< The main program NCA within the NSP
|
||||
std::optional<vfs::NCA> controlNca; //!< The main control NCA within the NSP
|
||||
std::optional<vfs::NCA> metaNca; //!< The main meta NCA within the NSP
|
||||
|
||||
public:
|
||||
NspLoader(const std::shared_ptr<vfs::Backing> &backing, const std::shared_ptr<crypto::KeyStore> &keyStore);
|
||||
|
@ -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<vfs::RomFileSystem>(controlNca->romFs);
|
||||
nacp.emplace(controlRomFs->OpenFile("control.nacp"));
|
||||
if (controlNca) {
|
||||
controlRomFs = std::make_shared<vfs::RomFileSystem>(controlNca->romFs);
|
||||
nacp.emplace(controlRomFs->OpenFile("control.nacp"));
|
||||
}
|
||||
|
||||
if (metaNca)
|
||||
cnmt = vfs::CNMT(metaNca->cnmt);
|
||||
}
|
||||
|
||||
void *XciLoader::LoadProcessData(const std::shared_ptr<kernel::type::KProcess> &process, const DeviceState &state) {
|
||||
|
@ -113,6 +113,7 @@ namespace skyline::loader {
|
||||
std::shared_ptr<vfs::RomFileSystem> controlRomFs; //!< A shared pointer to the control NCA's RomFS
|
||||
std::optional<vfs::NCA> programNca; //!< The main program NCA within the secure partition
|
||||
std::optional<vfs::NCA> controlNca; //!< The main control NCA within the secure partition
|
||||
std::optional<vfs::NCA> metaNca; //!< The main meta NCA within the secure partition
|
||||
|
||||
public:
|
||||
XciLoader(const std::shared_ptr<vfs::Backing> &backing, const std::shared_ptr<crypto::KeyStore> &keyStore);
|
||||
|
@ -34,26 +34,18 @@ namespace skyline::kernel {
|
||||
|
||||
bool isJitEnabled = false;
|
||||
|
||||
void OS::Execute(int romFd, loader::RomFormat romType) {
|
||||
void OS::Execute(int romFd, std::vector<int> dlcFds, int updateFd, loader::RomFormat romType) {
|
||||
auto romFile{std::make_shared<vfs::OsBacking>(romFd)};
|
||||
auto keyStore{std::make_shared<crypto::KeyStore>(privateAppFilesPath + "keys/")};
|
||||
keyStore = std::make_shared<crypto::KeyStore>(privateAppFilesPath + "keys/");
|
||||
|
||||
state.loader = [&]() -> std::shared_ptr<loader::Loader> {
|
||||
switch (romType) {
|
||||
case loader::RomFormat::NRO:
|
||||
return std::make_shared<loader::NroLoader>(std::move(romFile));
|
||||
case loader::RomFormat::NSO:
|
||||
return std::make_shared<loader::NsoLoader>(std::move(romFile));
|
||||
case loader::RomFormat::NCA:
|
||||
return std::make_shared<loader::NcaLoader>(std::move(romFile), std::move(keyStore));
|
||||
case loader::RomFormat::NSP:
|
||||
return std::make_shared<loader::NspLoader>(romFile, keyStore);
|
||||
case loader::RomFormat::XCI:
|
||||
return std::make_shared<loader::XciLoader>(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();
|
||||
|
||||
@ -68,7 +60,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);
|
||||
}
|
||||
|
||||
// Scheduler retrieves information from the NPDM of the process so it needs to be initialized after the process is created
|
||||
@ -89,4 +89,22 @@ namespace skyline::kernel {
|
||||
process->Kill(true, true, true);
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<loader::Loader> OS::GetLoader(int fd, std::shared_ptr<crypto::KeyStore> keyStore, loader::RomFormat romType) {
|
||||
auto file{std::make_shared<vfs::OsBacking>(fd)};
|
||||
switch (romType) {
|
||||
case loader::RomFormat::NRO:
|
||||
return std::make_shared<loader::NroLoader>(std::move(file));
|
||||
case loader::RomFormat::NSO:
|
||||
return std::make_shared<loader::NsoLoader>(std::move(file));
|
||||
case loader::RomFormat::NCA:
|
||||
return std::make_shared<loader::NcaLoader>(std::move(file), std::move(keyStore));
|
||||
case loader::RomFormat::NSP:
|
||||
return std::make_shared<loader::NspLoader>(file, keyStore);
|
||||
case loader::RomFormat::XCI:
|
||||
return std::make_shared<loader::XciLoader>(file, keyStore);
|
||||
default:
|
||||
throw exception("Unsupported ROM extension.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <crypto/key_store.h>
|
||||
#include <common/language.h>
|
||||
#include "vfs/filesystem.h"
|
||||
#include "loader/loader.h"
|
||||
@ -21,6 +22,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<vfs::FileSystem> assetFileSystem; //!< A filesystem to be used for accessing emulator assets (like tzdata)
|
||||
std::shared_ptr<crypto::KeyStore> keyStore;
|
||||
DeviceState state;
|
||||
service::ServiceManager serviceManager;
|
||||
|
||||
@ -41,8 +43,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<int> dlcFds, int updateFd, loader::RomFormat romType);
|
||||
|
||||
std::shared_ptr<loader::Loader> GetLoader(int fd, std::shared_ptr<crypto::KeyStore> keyStore, loader::RomFormat romType);
|
||||
};
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
|
||||
#include <os.h>
|
||||
#include <kernel/types/KProcess.h>
|
||||
#include "IAddOnContentManager.h"
|
||||
#include "IPurchaseEventManager.h"
|
||||
@ -11,12 +12,42 @@ namespace skyline::service::aocsrv {
|
||||
addOnContentListChangedEvent(std::make_shared<type::KEvent>(state, false)) {}
|
||||
|
||||
Result IAddOnContentManager::CountAddOnContent(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response) {
|
||||
response.Push<u32>(0);
|
||||
response.Push<u32>(static_cast<u32>(state.dlcLoaders.size()));
|
||||
return {};
|
||||
}
|
||||
|
||||
Result IAddOnContentManager::ListAddOnContent(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response) {
|
||||
response.Push<u32>(0);
|
||||
struct Parameters {
|
||||
u32 offset;
|
||||
u32 count;
|
||||
u64 processId;
|
||||
};
|
||||
auto params{request.Pop<Parameters>()};
|
||||
|
||||
std::vector<u32> out;
|
||||
std::vector<u64> 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<u32>(contentId & constant::AOCTitleIdMask));
|
||||
|
||||
const auto outCount{static_cast<u32>(std::min<size_t>(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<u32>(outCount);
|
||||
return {};
|
||||
}
|
||||
|
||||
Result IAddOnContentManager::GetAddOnContentBaseId(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response) {
|
||||
response.Push<u64>(state.loader->nacp->nacpContents.addOnContentBaseId);
|
||||
return {};
|
||||
}
|
||||
|
||||
Result IAddOnContentManager::PrepareAddOnContent(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response) {
|
||||
return {};
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,11 @@
|
||||
#include <services/serviceman.h>
|
||||
|
||||
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),
|
||||
|
@ -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<IStorage>(state.loader->romFs, state, manager), session, response);
|
||||
if (state.updateLoader) {
|
||||
auto patchManager{std::make_shared<vfs::PatchManager>()};
|
||||
auto romFs{patchManager->PatchRomFS(state, state.updateLoader->programNca, state.loader->programNca->ivfcOffset)};
|
||||
manager.RegisterService(std::make_shared<IStorage>(romFs, state, manager), session, response);
|
||||
} else {
|
||||
if (!state.loader->romFs)
|
||||
return result::NoRomFsAvailable;
|
||||
|
||||
manager.RegisterService(std::make_shared<IStorage>(state.loader->romFs, state, manager), session, response);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
@ -103,6 +111,16 @@ namespace skyline::service::fssrv {
|
||||
auto storageId{request.Pop<StorageId>()};
|
||||
request.Skip<std::array<u8, 7>>(); // 7-bytes padding
|
||||
auto dataId{request.Pop<u64>()};
|
||||
auto patchManager{std::make_shared<vfs::PatchManager>()};
|
||||
|
||||
// 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<IStorage>(romFs, state, manager), session, response);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
auto systemArchivesFileSystem{std::make_shared<vfs::OsFileSystem>(state.os->publicAppFilesPath + "/switch/nand/system/Contents/registered/")};
|
||||
auto systemArchives{systemArchivesFileSystem->OpenDirectory("")};
|
||||
@ -112,7 +130,7 @@ namespace skyline::service::fssrv {
|
||||
std::shared_ptr<vfs::Backing> 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<IStorage>(nca.romFs, state, manager), session, response);
|
||||
return {};
|
||||
}
|
||||
|
@ -8,18 +8,8 @@ namespace skyline::service::fssrv {
|
||||
IStorage::IStorage(std::shared_ptr<vfs::Backing> 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<i64>()};
|
||||
auto size{request.Pop<i64>()};
|
||||
|
||||
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<u64>()};
|
||||
auto size{request.Pop<u64>()};
|
||||
|
||||
backing->Read(request.outputBuf.at(0), static_cast<size_t>(offset));
|
||||
return {};
|
||||
|
227
app/src/main/cpp/skyline/vfs/bktr.cpp
Normal file
227
app/src/main/cpp/skyline/vfs/bktr.cpp
Normal file
@ -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 <typename BlockType, typename BucketType>
|
||||
std::pair<u64, u64> 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<u64>(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<u64>(std::distance(bucket.entries.begin(), entryIt) - 1)};
|
||||
return {bucketId, entryIndex};
|
||||
}
|
||||
LOGE("Offset could not be found.");
|
||||
return {0, 0};
|
||||
}
|
||||
|
||||
BKTR::BKTR(std::shared_ptr<vfs::Backing> pBaseRomfs, std::shared_ptr<vfs::Backing> pBktrRomfs, RelocationBlock pRelocation,
|
||||
std::vector<RelocationBucket> pRelocationBuckets, SubsectionBlock pSubsection,
|
||||
std::vector<SubsectionBucket> pSubsectionBuckets, bool pIsEncrypted, std::array<u8, 16> pKey,
|
||||
u64 pBaseOffset, u64 pIvfcOffset, std::array<u8, 8> 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<u8> 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<u8> 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<RegionBacking>(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<u8> 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<u8> block(0x10);
|
||||
auto regionBacking{std::make_shared<RegionBacking>(bktrRomFs, sectionOffset & static_cast<u32>(~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<u8> data(output.data() + read, output.size() - read);
|
||||
return read + ReadWithPartition(data, output.size() - read, offset + read);
|
||||
}
|
||||
|
||||
auto regionBacking{std::make_shared<RegionBacking>(bktrRomFs, sectionOffset, output.size())};
|
||||
auto readSize{regionBacking->Read(output)};
|
||||
cipher.Decrypt(output.data(), output.data(), readSize);
|
||||
return readSize;
|
||||
}
|
||||
|
||||
size_t BKTR::ReadWithPartition(span<u8> 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<u8> data(output.data() + partition, length - partition);
|
||||
return ReadWithPartition(data, length - partition, offset + partition) + ReadWithPartition(output, partition, offset);
|
||||
}
|
||||
|
||||
if (!relocationEntry.fromPatch) {
|
||||
span<u8> data(output.data(), length);
|
||||
auto regionBacking{std::make_shared<RegionBacking>(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<u8> 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<u8> block(0x10);
|
||||
auto regionBacking{std::make_shared<RegionBacking>(bktrRomFs, sectionOffset & static_cast<u32>(~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<u8> data(output.data() + read, length - read);
|
||||
return read + ReadWithPartition(data, length - read, offset + read);
|
||||
}
|
||||
|
||||
auto regionBacking{std::make_shared<RegionBacking>(bktrRomFs, sectionOffset, length)};
|
||||
span<u8> 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<u8, 16> BKTR::GetCipherIV(SubsectionEntry subsectionEntry, u64 sectionOffset) {
|
||||
std::array<u8, 16> 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<u8>(offset_iv & 0xFF);
|
||||
offset_iv >>= 8;
|
||||
}
|
||||
for (std::size_t i = 0; i < sizeof(u32); ++i) {
|
||||
iv[0x7 - i] = static_cast<u8>(subsectionCtr & 0xFF);
|
||||
subsectionCtr >>= 8;
|
||||
}
|
||||
return iv;
|
||||
}
|
||||
}
|
49
app/src/main/cpp/skyline/vfs/bktr.h
Normal file
49
app/src/main/cpp/skyline/vfs/bktr.h
Normal file
@ -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<vfs::Backing> baseRomFs;
|
||||
std::shared_ptr<vfs::Backing> bktrRomFs;
|
||||
RelocationBlock relocation;
|
||||
SubsectionBlock subsection;
|
||||
std::vector<RelocationBucket> relocationBuckets;
|
||||
std::vector<SubsectionBucket> subsectionBuckets;
|
||||
bool isEncrypted;
|
||||
u64 baseOffset;
|
||||
u64 ivfcOffset;
|
||||
std::array<u8, 8> sectionCtr;
|
||||
std::array<u8, 16> key;
|
||||
|
||||
SubsectionEntry GetNextSubsectionEntry(u64 offset);
|
||||
|
||||
RelocationEntry GetRelocationEntry(u64 offset);
|
||||
|
||||
RelocationEntry GetNextRelocationEntry(u64 offset);
|
||||
|
||||
SubsectionEntry GetSubsectionEntry(u64 offset);
|
||||
|
||||
std::array<u8, 16> GetCipherIV(SubsectionEntry subsectionEntry, u64 sectionOffset);
|
||||
|
||||
public:
|
||||
|
||||
BKTR(std::shared_ptr<vfs::Backing> pBaseRomfs, std::shared_ptr<vfs::Backing> pBktrRomfs, RelocationBlock pRelocation,
|
||||
std::vector<RelocationBucket> pRelocationBuckets, SubsectionBlock pSubsection,
|
||||
std::vector<SubsectionBucket> pSubsectionBuckets, bool pIsEncrypted, std::array<u8, 16> pKey,
|
||||
u64 pBaseOffset, u64 pIvfcOffset, std::array<u8, 8> pSectionCtr);
|
||||
|
||||
size_t ReadImpl(span<u8> output, size_t offset) override;
|
||||
size_t ReadWithPartition(span<u8> output, size_t length, size_t offset);
|
||||
};
|
||||
}
|
45
app/src/main/cpp/skyline/vfs/cnmt.cpp
Normal file
45
app/src/main/cpp/skyline/vfs/cnmt.cpp
Normal file
@ -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<FileSystem> cnmtSection) {
|
||||
auto root{cnmtSection->OpenDirectory("")};
|
||||
std::shared_ptr<vfs::Backing> cnmt;
|
||||
if (root != nullptr) {
|
||||
for (const auto &entry : root->Read()) {
|
||||
cnmt = cnmtSection->OpenFile(entry.name);
|
||||
}
|
||||
}
|
||||
|
||||
header = cnmt->Read<PackagedContentMetaHeader>();
|
||||
if (header.contentMetaType >= ContentMetaType::Application && header.contentMetaType <= ContentMetaType::AddOnContent)
|
||||
optionalHeader = cnmt->Read<OptionalHeader>(sizeof(PackagedContentMetaHeader));
|
||||
|
||||
for (u16 i = 0; i < header.contentCount; ++i)
|
||||
contentInfos.emplace_back(cnmt->Read<PackagedContentInfo>(sizeof(PackagedContentMetaHeader) + i * sizeof(PackagedContentInfo) +
|
||||
header.extendedHeaderSize));
|
||||
|
||||
for (u16 i = 0; i < header.contentMetaCount; ++i)
|
||||
contentMetaInfos.emplace_back(cnmt->Read<ContentMetaInfo>(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;
|
||||
}
|
||||
|
||||
}
|
||||
|
106
app/src/main/cpp/skyline/vfs/cnmt.h
Normal file
106
app/src/main/cpp/skyline/vfs/cnmt.h
Normal file
@ -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<u8, 0x20> hash;
|
||||
std::array<u8, 0x10> contentId;
|
||||
std::array<u8, 0x6> 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<PackagedContentInfo> contentInfos;
|
||||
std::vector<ContentMetaInfo> contentMetaInfos;
|
||||
|
||||
public:
|
||||
PackagedContentMetaHeader header;
|
||||
|
||||
CNMT(std::shared_ptr<FileSystem> file);
|
||||
|
||||
std::string GetTitleId();
|
||||
|
||||
std::string GetParentTitleId();
|
||||
|
||||
ContentMetaType GetContentMetaType();
|
||||
};
|
||||
|
||||
}
|
@ -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);
|
||||
|
@ -25,15 +25,37 @@ namespace skyline::vfs {
|
||||
public:
|
||||
struct NacpData {
|
||||
std::array<ApplicationTitle, 0x10> titleEntries; //!< Title entries for each language
|
||||
u8 _pad0_[0x2C];
|
||||
std::array<u8, 0x25> 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<u8, 0x20> ratingAge;
|
||||
std::array<char, 0x10> 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<u64, 0x8> localCommunicationId;
|
||||
u8 logoType;
|
||||
u8 logoHandling;
|
||||
u8 runtimeAddOnContentInstall;
|
||||
u8 runtimeParameterDelivery;
|
||||
u8 appropriateAgeForChina;
|
||||
u8 _pad1_[0x3];
|
||||
std::array<u8, 8> 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);
|
||||
|
@ -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<vfs::Backing> pBacking, std::shared_ptr<crypto::KeyStore> pKeyStore, bool pUseKeyArea) : backing(std::move(pBacking)), keyStore(std::move(pKeyStore)), useKeyArea(pUseKeyArea) {
|
||||
header = backing->Read<NcaHeader>();
|
||||
NCA::NCA(std::shared_ptr<vfs::Backing> pBacking, std::shared_ptr<crypto::KeyStore> pKeyStore, bool pUseKeyArea)
|
||||
: backing(std::move(pBacking)), keyStore(std::move(pKeyStore)), useKeyArea(pUseKeyArea) {
|
||||
header = backing->Read<NCAHeader>();
|
||||
|
||||
if (header.magic != util::MakeMagic<u32>("NCA3")) {
|
||||
if (!keyStore->headerKey)
|
||||
throw loader_exception(LoaderResult::MissingHeaderKey);
|
||||
|
||||
crypto::AesCipher cipher(*keyStore->headerKey, MBEDTLS_CIPHER_AES_128_XTS);
|
||||
|
||||
cipher.XtsDecrypt({reinterpret_cast<u8 *>(&header), sizeof(NcaHeader)}, 0, 0x200);
|
||||
cipher.XtsDecrypt({reinterpret_cast<u8 *>(&header), sizeof(NCAHeader)}, 0, 0x200);
|
||||
|
||||
// Check if decryption was successful
|
||||
if (header.magic != util::MakeMagic<u32>("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<size_t>(std::ranges::count_if(header.sectionTables, [](const NCASectionTableEntry &entry) {
|
||||
return entry.mediaOffset > 0;
|
||||
}))};
|
||||
|
||||
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);
|
||||
sections.resize(numberSections);
|
||||
const auto lengthSections{constant::SectionHeaderSize * numberSections};
|
||||
|
||||
if (encrypted) {
|
||||
std::vector<u8> raw(lengthSections);
|
||||
|
||||
backing->Read(raw, constant::SectionHeaderOffset);
|
||||
|
||||
crypto::AesCipher cipher(*keyStore->headerKey, MBEDTLS_CIPHER_AES_128_XTS);
|
||||
cipher.XtsDecrypt(reinterpret_cast<u8 *>(sections.data()), reinterpret_cast<u8 *>(raw.data()), lengthSections, 2, constant::SectionHeaderSize);
|
||||
} else {
|
||||
for (size_t i{}; i < lengthSections; i++)
|
||||
sections.push_back(backing->Read<NCASectionHeader>());
|
||||
}
|
||||
|
||||
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<size_t>(entry.startOffset) * constant::MediaUnitSize + sectionHeader.sha256HashInfo.pfs0Offset};
|
||||
size_t size{constant::MediaUnitSize * static_cast<size_t>(entry.endOffset - entry.startOffset)};
|
||||
NCA::NCA(std::optional<vfs::NCA> updateNca, std::shared_ptr<crypto::KeyStore> pKeyStore, std::shared_ptr<vfs::Backing> 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) {
|
||||
|
||||
auto pfs{std::make_shared<PartitionFileSystem>(CreateBacking(sectionHeader, std::make_shared<RegionBacking>(backing, offset, size), offset))};
|
||||
useKeyArea = false;
|
||||
contentType = header.contentType;
|
||||
rightsIdEmpty = header.rightsId == crypto::KeyStore::Key128{};
|
||||
|
||||
if (contentType == NcaContentType::Program) {
|
||||
if (!updateNca)
|
||||
throw loader_exception(LoaderResult::ParsingError);
|
||||
|
||||
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<size_t>(entry.mediaOffset) * constant::MediaUnitSize + section.pfs0.pfs0HeaderOffset};
|
||||
size_t size{constant::MediaUnitSize * static_cast<size_t>(entry.mediaEndOffset - entry.mediaOffset)};
|
||||
|
||||
auto pfs{std::make_shared<PartitionFileSystem>(CreateBacking(section, std::make_shared<RegionBacking>(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<size_t>(entry.startOffset) * constant::MediaUnitSize + sectionHeader.integrityHashInfo.levels.back().offset};
|
||||
size_t size{sectionHeader.integrityHashInfo.levels.back().size};
|
||||
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<RegionBacking>(backing, romFsOffset, romFsSize), romFsOffset)};
|
||||
|
||||
romFs = CreateBacking(sectionHeader, std::make_shared<RegionBacking>(backing, offset, size), offset);
|
||||
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<RelocationBlock>(sectionHeader.bktr.relocation.offset - offset)};
|
||||
SubsectionBlock subsectionBlock{romFs->Read<SubsectionBlock>(sectionHeader.bktr.subsection.offset - offset)};
|
||||
|
||||
std::vector<RelocationBucketRaw> relocationBucketsRaw((sectionHeader.bktr.relocation.size - sizeof(RelocationBlock)) / sizeof(RelocationBucketRaw));
|
||||
auto regionBackingRelocation{std::make_shared<RegionBacking>(romFs, sectionHeader.bktr.relocation.offset + sizeof(RelocationBlock) - offset, sectionHeader.bktr.relocation.size - sizeof(RelocationBlock))};
|
||||
regionBackingRelocation->Read<RelocationBucketRaw>(relocationBucketsRaw);
|
||||
|
||||
std::vector<SubsectionBucketRaw> subsectionBucketsRaw((sectionHeader.bktr.subsection.size - sizeof(SubsectionBlock)) / sizeof(SubsectionBucketRaw));
|
||||
auto regionBackingSubsection{std::make_shared<RegionBacking>(romFs, sectionHeader.bktr.subsection.offset + sizeof(SubsectionBlock) - offset, sectionHeader.bktr.subsection.size - sizeof(SubsectionBlock))};
|
||||
regionBackingSubsection->Read<SubsectionBucketRaw>(subsectionBucketsRaw);
|
||||
|
||||
std::vector<RelocationBucket> relocationBuckets;
|
||||
relocationBuckets.reserve(relocationBucketsRaw.size());
|
||||
for (const RelocationBucketRaw &rawBucket : relocationBucketsRaw)
|
||||
relocationBuckets.push_back(ConvertRelocationBucketRaw(rawBucket));
|
||||
|
||||
std::vector<SubsectionBucket> 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<BKTR>(
|
||||
bktrBaseRomfs, std::make_shared<RegionBacking>(backing, baseOffset, romFsSize),
|
||||
relocationBlock, relocationBuckets, subsectionBlock, subsectionBuckets, encrypted,
|
||||
encrypted ? key : std::array<u8, 0x10>{}, baseOffset, bktrBaseIvfcOffset,
|
||||
sectionHeader.raw.sectionCtr)};
|
||||
|
||||
romFs = std::make_shared<RegionBacking>(bktr, sectionHeader.romfs.ivfc.levels[constant::IvfcMaxLevel - 1].offset, romFsSize);
|
||||
} else {
|
||||
romFs = std::move(decryptedBacking);
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<Backing> NCA::CreateBacking(const NcaSectionHeader §ionHeader, std::shared_ptr<Backing> rawBacking, size_t offset) {
|
||||
std::shared_ptr<Backing> NCA::CreateBacking(const NCASectionHeader §ionHeader, std::shared_ptr<Backing> 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<u8, 0x10> 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<CtrEncryptedBacking>(ctr, key, std::move(rawBacking), offset);
|
||||
}
|
||||
@ -94,8 +181,8 @@ namespace skyline::vfs {
|
||||
}
|
||||
|
||||
u8 NCA::GetKeyGeneration() {
|
||||
u8 legacyGen{static_cast<u8>(header.legacyKeyGenerationType)};
|
||||
u8 gen{static_cast<u8>(header.keyGenerationType)};
|
||||
u8 legacyGen{static_cast<u8>(header.cryptoType)};
|
||||
u8 gen{static_cast<u8>(header.cryptoType2)};
|
||||
gen = std::max<u8>(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);
|
||||
}
|
||||
}
|
||||
|
@ -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<u8, 0x20> 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<u8, 0x20> 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<u8, 0x10> 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<u8, 0x138> blockData;
|
||||
std::array<u8, 0x8> 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<IVFCLevel, 6> 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<u64, 0x7FE> 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<u64, 0x7FE> 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<u8, 0x100> rsaSignature1;
|
||||
std::array<u8, 0x100> 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<u8, 0x10> rightsId;
|
||||
std::array<NCASectionTableEntry, 0x4> sectionTables;
|
||||
std::array<std::array<u8, 0x20>, 0x4> hashTables;
|
||||
std::array<std::array<u8, 0x10>, 4> keyArea;
|
||||
u8 _pad2_[0xC0];
|
||||
};
|
||||
static_assert(sizeof(NCAHeader) == 0x400);
|
||||
|
||||
struct RelocationBucketRaw {
|
||||
u8 _pad0_[0x4];
|
||||
u32 numberEntries;
|
||||
u64 endOffset;
|
||||
std::array<RelocationEntry, 0x332> relocationEntries;
|
||||
u8 _pad1_[0x8];
|
||||
};
|
||||
static_assert(sizeof(RelocationBucketRaw) == 0x4000);
|
||||
|
||||
struct RelocationBucket {
|
||||
u32 numberEntries;
|
||||
u64 endOffset;
|
||||
std::vector<RelocationEntry> entries;
|
||||
};
|
||||
|
||||
struct SubsectionBucket {
|
||||
u32 numberEntries;
|
||||
u64 endOffset;
|
||||
std::vector<SubsectionEntry> entries;
|
||||
};
|
||||
|
||||
struct SubsectionBucketRaw {
|
||||
u8 _pad0_[0x4];
|
||||
u32 numberEntries;
|
||||
u64 endOffset;
|
||||
std::array<SubsectionEntry, 0x3FF> 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<HierarchicalIntegrityLevel, 6> levels; //!< An array of the hierarchical integrity levels
|
||||
u8 _pad0_[0x20];
|
||||
std::array<u8, 0x20> 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<u8, 0x20> 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<u8, 0x100> fixed_key_sig; //!< An RSA-PSS signature over the header with fixed key
|
||||
std::array<u8, 0x100> 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<u8, 0x10> rightsId;
|
||||
std::array<NcaFsEntry, 4> fsEntries; //!< The filesystem entries for this NCA
|
||||
std::array<std::array<u8, 0x20>, 4> sectionHashes; //!< SHA-256 hashes for each filesystem header
|
||||
std::array<std::array<u8, 0x10>, 4> encryptedKeyArea; //!< The encrypted key area for each filesystem
|
||||
u8 _pad1_[0xC0];
|
||||
std::array<NcaSectionHeader, 4> sectionHeaders;
|
||||
};
|
||||
static_assert(sizeof(NcaHeader) == 0xC00);
|
||||
|
||||
std::shared_ptr<Backing> backing;
|
||||
std::shared_ptr<crypto::KeyStore> keyStore;
|
||||
bool encrypted{false};
|
||||
bool rightsIdEmpty;
|
||||
bool useKeyArea;
|
||||
std::vector<std::shared_ptr<Backing>> files;
|
||||
std::vector<NCASectionHeader> sections;
|
||||
std::shared_ptr<vfs::Backing> 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<Backing> CreateBacking(const NcaSectionHeader §ionHeader, std::shared_ptr<Backing> rawBacking, size_t offset);
|
||||
std::shared_ptr<Backing> CreateBacking(const NCASectionHeader §ionHeader, std::shared_ptr<Backing> 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<FileSystem> exeFs; //!< The PFS0 filesystem for this NCA's ExeFS section
|
||||
std::shared_ptr<FileSystem> logo; //!< The PFS0 filesystem for this NCA's logo section
|
||||
std::shared_ptr<FileSystem> cnmt; //!< The PFS0 filesystem for this NCA's CNMT section
|
||||
std::shared_ptr<Backing> 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<vfs::Backing> backing, std::shared_ptr<crypto::KeyStore> keyStore, bool useKeyArea = false);
|
||||
|
||||
NCA(std::optional<vfs::NCA> updateNca, std::shared_ptr<crypto::KeyStore> pKeyStore, std::shared_ptr<vfs::Backing> bktrBaseRomfs,
|
||||
u64 bktrBaseIvfcOffset, bool useKeyArea = false);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
21
app/src/main/cpp/skyline/vfs/patch_manager.cpp
Normal file
21
app/src/main/cpp/skyline/vfs/patch_manager.cpp
Normal file
@ -0,0 +1,21 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Copyright © 2023 Strato Team and Contributors (https://github.com/strato-emu/)
|
||||
|
||||
#include <os.h>
|
||||
#include <vfs/nca.h>
|
||||
#include "patch_manager.h"
|
||||
#include "region_backing.h"
|
||||
|
||||
namespace skyline::vfs {
|
||||
PatchManager::PatchManager() {}
|
||||
|
||||
std::shared_ptr<FileSystem> PatchManager::PatchExeFS(const DeviceState &state, std::shared_ptr<FileSystem> exefs) {
|
||||
auto updateProgramNCA{state.updateLoader->programNca};
|
||||
return updateProgramNCA->exeFs;
|
||||
}
|
||||
|
||||
std::shared_ptr<vfs::Backing> PatchManager::PatchRomFS(const DeviceState &state, std::optional<vfs::NCA> nca, u64 ivfcOffset) {
|
||||
auto newNca{std::make_shared<vfs::NCA>(nca, state.os->keyStore, state.loader->programNca->romFs, ivfcOffset)};
|
||||
return newNca->romFs;
|
||||
}
|
||||
}
|
18
app/src/main/cpp/skyline/vfs/patch_manager.h
Normal file
18
app/src/main/cpp/skyline/vfs/patch_manager.h
Normal file
@ -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 <os.h>
|
||||
#include <vfs/nca.h>
|
||||
|
||||
namespace skyline::vfs {
|
||||
class PatchManager {
|
||||
public:
|
||||
PatchManager();
|
||||
|
||||
std::shared_ptr<vfs::Backing> PatchRomFS(const DeviceState &state, std::optional<vfs::NCA> nca , u64 ivfcOffset);
|
||||
|
||||
std::shared_ptr<FileSystem> PatchExeFS(const DeviceState &state, std::shared_ptr<FileSystem> exefs);
|
||||
};
|
||||
}
|
@ -8,6 +8,7 @@ package emu.skyline
|
||||
import android.app.Activity
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.content.pm.ShortcutInfo
|
||||
import android.content.pm.ShortcutManager
|
||||
import android.graphics.drawable.Icon
|
||||
@ -21,20 +22,33 @@ import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import emu.skyline.data.AppItem
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import emu.skyline.data.BaseAppItem
|
||||
import emu.skyline.data.AppItemTag
|
||||
import emu.skyline.databinding.AppDialogBinding
|
||||
import emu.skyline.loader.LoaderResult
|
||||
import emu.skyline.loader.RomFile
|
||||
import emu.skyline.loader.RomType
|
||||
import emu.skyline.loader.RomFormat
|
||||
import emu.skyline.loader.RomFormat.*
|
||||
import emu.skyline.loader.AppEntry
|
||||
import emu.skyline.settings.SettingsActivity
|
||||
import emu.skyline.settings.EmulationSettings
|
||||
import emu.skyline.utils.CacheManagementUtils
|
||||
import emu.skyline.utils.SaveManagementUtils
|
||||
import emu.skyline.utils.serializable
|
||||
import emu.skyline.utils.ContentsHelper
|
||||
import emu.skyline.model.TaskViewModel
|
||||
import emu.skyline.fragments.IndeterminateProgressDialogFragment
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.InputStream
|
||||
@ -46,9 +60,9 @@ import java.io.OutputStream
|
||||
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)
|
||||
|
||||
@ -60,7 +74,7 @@ class AppDialog : BottomSheetDialogFragment() {
|
||||
|
||||
private lateinit var binding : AppDialogBinding
|
||||
|
||||
private val item by lazy { requireArguments().serializable<AppItem>(AppItemTag)!! }
|
||||
private val item by lazy { requireArguments().serializable<BaseAppItem>(AppItemTag)!! }
|
||||
|
||||
/**
|
||||
* Used to manage save files
|
||||
@ -68,6 +82,14 @@ class AppDialog : BottomSheetDialogFragment() {
|
||||
private lateinit var documentPicker : ActivityResultLauncher<Array<String>>
|
||||
private lateinit var startForResultExportSave : ActivityResultLauncher<Intent>
|
||||
|
||||
private val contents by lazy { ContentsHelper(requireContext()) }
|
||||
|
||||
private lateinit var expectedContentType: RomType
|
||||
|
||||
private lateinit var contentPickerLauncher: ActivityResultLauncher<Intent>
|
||||
|
||||
private val taskViewModel : TaskViewModel by activityViewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState : Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
documentPicker = SaveManagementUtils.registerDocumentPicker(requireActivity()) {
|
||||
@ -95,6 +117,43 @@ class AppDialog : BottomSheetDialogFragment() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contentPickerLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) { result ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
val uri: Uri? = result.data?.data
|
||||
val task: () -> Unit = {
|
||||
val result = loadContent(uri)
|
||||
val contentType = if (expectedContentType == RomType.DLC) "DLCs" else "Update"
|
||||
when (result) {
|
||||
LoaderResult.Success -> {
|
||||
Snackbar.make(binding.root, "Imported ${contentType} successfully", Snackbar.LENGTH_SHORT).show()
|
||||
}
|
||||
LoaderResult.ParsingError -> {
|
||||
Snackbar.make(binding.root, "Failed to import ${contentType}", Snackbar.LENGTH_SHORT).show()
|
||||
}
|
||||
else -> {
|
||||
Snackbar.make(binding.root, "Unknown error occurred", Snackbar.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val uriSize = contents.getUriSize(requireContext(), uri!!) ?: null
|
||||
|
||||
if (uriSize != null && uriSize.toInt() > 100 * 1024 * 1024) {
|
||||
IndeterminateProgressDialogFragment.newInstance(requireActivity() as AppCompatActivity, R.string.importing, task).show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG)
|
||||
} else {
|
||||
ViewModelProvider(requireActivity())[TaskViewModel::class.java].task = task
|
||||
taskViewModel.runTask()
|
||||
taskViewModel.isComplete.observe(this) { isComplete ->
|
||||
if (!isComplete)
|
||||
return@observe
|
||||
taskViewModel.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -138,7 +197,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))
|
||||
|
||||
@ -190,6 +249,48 @@ class AppDialog : BottomSheetDialogFragment() {
|
||||
SaveManagementUtils.exportSave(requireContext(), startForResultExportSave, item.titleId, "${item.title} (v${binding.gameVersion.text}) [${item.titleId}]")
|
||||
}
|
||||
|
||||
binding.importUpdate.setOnClickListener {
|
||||
expectedContentType = RomType.Update // we expects Update
|
||||
openContentPicker()
|
||||
}
|
||||
|
||||
binding.importDlcs.setOnClickListener {
|
||||
expectedContentType = RomType.DLC // we expects DLC
|
||||
openContentPicker()
|
||||
}
|
||||
|
||||
binding.deleteContents.isEnabled = !contents.loadContents().filter { appEntry ->
|
||||
(appEntry as AppEntry).parentTitleId == item.titleId
|
||||
}.isEmpty()
|
||||
|
||||
binding.deleteContents.setOnClickListener {
|
||||
var contentList = contents.loadContents().toMutableList()
|
||||
val currentItemContentList = contentList.filter { appEntry ->
|
||||
(appEntry as AppEntry).parentTitleId == item.titleId
|
||||
}
|
||||
val contentNames = currentItemContentList.map { contents.getFileName((it as AppEntry).uri!!, requireContext().contentResolver) }.toTypedArray()
|
||||
var selectedItemIndex = 0
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle("Contents")
|
||||
.setSingleChoiceItems(contentNames, selectedItemIndex) { _, which ->
|
||||
selectedItemIndex = which
|
||||
}
|
||||
.setPositiveButton("Remove") { _, _ ->
|
||||
val selectedContent = currentItemContentList[selectedItemIndex]
|
||||
File((selectedContent as AppEntry).uri.path).delete()
|
||||
contentList.remove(selectedContent)
|
||||
contents.saveContents(contentList)
|
||||
Snackbar.make(binding.root, "Successfully removed ${contentNames[selectedItemIndex].toString()}", Snackbar.LENGTH_SHORT).show()
|
||||
binding.deleteContents.isEnabled = !contents.loadContents().filter { appEntry ->
|
||||
(appEntry as AppEntry).parentTitleId == item.titleId
|
||||
}.isEmpty()
|
||||
}
|
||||
.setNegativeButton("Cancel") { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
binding.gameTitleId.setOnLongClickListener {
|
||||
val clipboard = requireActivity().getSystemService(android.content.Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager
|
||||
clipboard.setPrimaryClip(android.content.ClipData.newPlainText("Title ID", item.titleId))
|
||||
@ -206,4 +307,45 @@ class AppDialog : BottomSheetDialogFragment() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadContent(uri: Uri?): LoaderResult {
|
||||
if (uri == Uri.EMPTY || uri == null) {
|
||||
return LoaderResult.ParsingError
|
||||
}
|
||||
|
||||
mapOf(
|
||||
"nro" to NRO,
|
||||
"nso" to NSO,
|
||||
"nca" to NCA,
|
||||
"nsp" to NSP,
|
||||
"xci" to XCI
|
||||
)[contents.getFileName(uri!!, requireContext().contentResolver)?.substringAfterLast(".")?.lowercase()]?.let { contentFormat ->
|
||||
|
||||
// creates a new RomFile with a copied file uri so we don't need to create it by 2 times
|
||||
val newContent = RomFile(
|
||||
requireContext(),
|
||||
contentFormat,
|
||||
contents.save(uri!!, requireContext().contentResolver)!!,
|
||||
EmulationSettings.global.systemLanguage
|
||||
)
|
||||
|
||||
val currentContents = contents.loadContents().toMutableList()
|
||||
val isDuplicate = currentContents.any { (it as AppEntry).uri == newContent.appEntry.uri }
|
||||
if (!isDuplicate && newContent.result == LoaderResult.Success && newContent.appEntry.romType == expectedContentType && newContent.appEntry.parentTitleId == item.titleId) {
|
||||
currentContents.add(newContent.appEntry)
|
||||
contents.saveContents(currentContents)
|
||||
return LoaderResult.Success
|
||||
} else if (!isDuplicate) File(newContent.appEntry.uri.path).delete()
|
||||
if (isDuplicate) return LoaderResult.Success // if it is duplicate then we indicate that it is reimported successfully
|
||||
}
|
||||
|
||||
return LoaderResult.ParsingError
|
||||
}
|
||||
|
||||
private fun openContentPicker() {
|
||||
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
type = "*/*"
|
||||
}
|
||||
contentPickerLauncher.launch(intent)
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,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.os.PowerManager
|
||||
@ -66,6 +67,8 @@ import emu.skyline.emulation.PipelineLoadingFragment
|
||||
import emu.skyline.input.*
|
||||
import emu.skyline.loader.RomFile
|
||||
import emu.skyline.loader.getRomFormat
|
||||
import emu.skyline.loader.AppEntry
|
||||
import emu.skyline.loader.RomType
|
||||
import emu.skyline.settings.AppSettings
|
||||
import emu.skyline.settings.EmulationSettings
|
||||
import emu.skyline.settings.NativeSettings
|
||||
@ -74,6 +77,7 @@ import emu.skyline.utils.ByteBufferSerializable
|
||||
import emu.skyline.utils.GpuDriverHelper
|
||||
import emu.skyline.utils.serializable
|
||||
import emu.skyline.utils.AmbientHelper
|
||||
import emu.skyline.utils.ContentsHelper
|
||||
import emu.skyline.input.onscreen.OnScreenEditActivity
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@ -105,10 +109,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
|
||||
|
||||
var dlcUris: ArrayList<Uri> = arrayListOf()
|
||||
|
||||
var updateUri : Uri = Uri.EMPTY
|
||||
|
||||
/**
|
||||
* The built-in [Vibrator] of the device
|
||||
*/
|
||||
@ -171,7 +179,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
|
||||
@ -286,9 +294,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()
|
||||
}
|
||||
|
||||
@ -302,6 +320,17 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
|
||||
val intentItem = intent.serializable(AppItemTag) as AppItem?
|
||||
if (intentItem != null) {
|
||||
item = intentItem
|
||||
|
||||
val contents = ContentsHelper(this@EmulationActivity)
|
||||
|
||||
contents.loadContents().filter { appEntry ->
|
||||
(appEntry as AppEntry).parentTitleId == item.titleId
|
||||
}.forEach { appEntry ->
|
||||
(appEntry as AppEntry).uri?.let { uri: Uri ->
|
||||
if ((appEntry as AppEntry).romType == RomType.DLC) dlcUris.add(uri)
|
||||
if ((appEntry as AppEntry).romType == RomType.Update) updateUri = uri
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@ -310,7 +339,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")
|
||||
|
@ -40,9 +40,11 @@ import dagger.hilt.android.AndroidEntryPoint
|
||||
import emu.skyline.adapter.*
|
||||
import emu.skyline.di.getSettings
|
||||
import emu.skyline.data.AppItem
|
||||
import emu.skyline.data.BaseAppItem
|
||||
import emu.skyline.data.AppItemTag
|
||||
import emu.skyline.databinding.MainActivityBinding
|
||||
import emu.skyline.loader.AppEntry
|
||||
import emu.skyline.loader.RomType
|
||||
import emu.skyline.loader.LoaderResult
|
||||
import emu.skyline.provider.DocumentsProvider
|
||||
import emu.skyline.settings.AppSettings
|
||||
@ -52,6 +54,7 @@ import emu.skyline.utils.GpuDriverHelper
|
||||
import emu.skyline.utils.SearchLocationHelper
|
||||
import emu.skyline.utils.WindowInsetsHelper
|
||||
import emu.skyline.SkylineApplication
|
||||
import java.util.Collections
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.ceil
|
||||
import kotlinx.coroutines.launch
|
||||
@ -102,7 +105,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
|
||||
@ -257,7 +260,9 @@ class MainActivity : AppCompatActivity() {
|
||||
private fun getAppItems() = mutableListOf<AppViewItem>().apply {
|
||||
appEntries?.let { entries ->
|
||||
sortGameList(entries.toList()).forEach { entry ->
|
||||
add(AppItem(entry).toViewItem())
|
||||
val updates : List<BaseAppItem> = entries.filter { it.romType == RomType.Update && it.parentTitleId == entry.titleId }.map { BaseAppItem(it, true) }
|
||||
val dlcs : List<BaseAppItem> = entries.filter { it.romType == RomType.DLC && it.parentTitleId == entry.titleId }.map { BaseAppItem(it, true) }
|
||||
add(AppItem(entry, updates, dlcs).toViewItem())
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -265,7 +270,7 @@ class MainActivity : AppCompatActivity() {
|
||||
private fun sortGameList(gameList : List<AppEntry>) : List<AppEntry> {
|
||||
val sortedApps : MutableList<AppEntry> = mutableListOf()
|
||||
gameList.forEach { entry ->
|
||||
if (!appSettings.filterInvalidFiles || entry.loaderResult != LoaderResult.ParsingError)
|
||||
if (validateAppEntry(entry))
|
||||
sortedApps.add(entry)
|
||||
}
|
||||
when (appSettings.sortAppsBy) {
|
||||
@ -275,6 +280,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) }
|
||||
@ -291,7 +301,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) {
|
||||
@ -304,7 +314,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")
|
||||
|
@ -17,7 +17,7 @@ import android.widget.RelativeLayout
|
||||
import android.widget.TextView
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import emu.skyline.R
|
||||
import emu.skyline.data.AppItem
|
||||
import emu.skyline.data.BaseAppItem
|
||||
import emu.skyline.databinding.AppItemGridBinding
|
||||
import emu.skyline.databinding.AppItemGridCompactBinding
|
||||
import emu.skyline.databinding.AppItemLinearBinding
|
||||
@ -86,9 +86,9 @@ class GridCompatBinding(parent : ViewGroup) : LayoutBinding<AppItemGridCompactBi
|
||||
override val icon = binding.icon
|
||||
}
|
||||
|
||||
private typealias InteractionFunction = (appItem : AppItem) -> 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<LayoutBinding<*>>() {
|
||||
class AppViewItem(var layoutType : LayoutType, private val item : BaseAppItem, private val onClick : InteractionFunction, private val onLongClick : InteractionFunction) : GenericListItem<LayoutBinding<*>>() {
|
||||
override fun getViewBindingFactory() = LayoutBindingFactory(layoutType)
|
||||
|
||||
override fun bind(holder : GenericViewHolder<LayoutBinding<*>>, position : Int) {
|
||||
@ -116,7 +116,7 @@ class AppViewItem(var layoutType : LayoutType, private val item : AppItem, priva
|
||||
binding.root.findViewById<View>(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))
|
||||
|
@ -16,73 +16,17 @@ import emu.skyline.loader.AppEntry
|
||||
import emu.skyline.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(SkylineApplication.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
|
||||
class AppItem(meta : AppEntry, private val updates : List<BaseAppItem>, private val dlcs : List<BaseAppItem>) : BaseAppItem(meta), Serializable {
|
||||
|
||||
val bitmapIcon : Bitmap get() = meta.icon ?: missingIcon
|
||||
fun getEnabledDlcs() : List<BaseAppItem> {
|
||||
return dlcs.filter { it.enabled }
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 ?: "" }}"
|
||||
fun getEnabledUpdate() : BaseAppItem? {
|
||||
return updates.firstOrNull { it.enabled }
|
||||
}
|
||||
}
|
||||
|
88
app/src/main/java/emu/skyline/data/BaseAppItem.kt
Normal file
88
app/src/main/java/emu/skyline/data/BaseAppItem.kt
Normal file
@ -0,0 +1,88 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||
*/
|
||||
|
||||
package emu.skyline.data
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import emu.skyline.BuildConfig
|
||||
import emu.skyline.R
|
||||
import emu.skyline.SkylineApplication
|
||||
import emu.skyline.loader.AppEntry
|
||||
import emu.skyline.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(SkylineApplication.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 ?: "" }}"
|
||||
}
|
@ -15,7 +15,7 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.google.android.renderscript.Toolkit
|
||||
import emu.skyline.data.AppItem
|
||||
import emu.skyline.data.BaseAppItem
|
||||
import emu.skyline.data.AppItemTag
|
||||
import emu.skyline.databinding.PipelineLoadingBinding
|
||||
import emu.skyline.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<AppItem>(AppItemTag)!! }
|
||||
private val item by lazy { requireArguments().serializable<BaseAppItem>(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)
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -25,7 +25,7 @@ import emu.skyline.adapter.GenericListItem
|
||||
import emu.skyline.adapter.GpuDriverViewItem
|
||||
import emu.skyline.adapter.SelectableGenericAdapter
|
||||
import emu.skyline.adapter.SpacingItemDecoration
|
||||
import emu.skyline.data.AppItem
|
||||
import emu.skyline.data.BaseAppItem
|
||||
import emu.skyline.data.AppItemTag
|
||||
import emu.skyline.databinding.GpuDriverActivityBinding
|
||||
import emu.skyline.settings.EmulationSettings
|
||||
@ -47,7 +47,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
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)
|
||||
|
||||
@ -149,7 +149,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())
|
||||
}
|
||||
|
||||
|
@ -13,7 +13,7 @@ import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.Preference.SummaryProvider
|
||||
import androidx.preference.R
|
||||
import emu.skyline.data.AppItem
|
||||
import emu.skyline.data.BaseAppItem
|
||||
import emu.skyline.data.AppItemTag
|
||||
import emu.skyline.settings.EmulationSettings
|
||||
import emu.skyline.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 [emu.skyline.settings.GameSettingsFragment]
|
||||
*/
|
||||
var item : AppItem? = null
|
||||
var item : BaseAppItem? = null
|
||||
|
||||
init {
|
||||
val supportsCustomDriverLoading = GpuDriverHelper.supportsCustomDriverLoading()
|
||||
|
@ -11,7 +11,7 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.preference.*
|
||||
import emu.skyline.BuildConfig
|
||||
import emu.skyline.R
|
||||
import emu.skyline.data.AppItem
|
||||
import emu.skyline.data.BaseAppItem
|
||||
import emu.skyline.data.AppItemTag
|
||||
import emu.skyline.preference.GpuDriverPreference
|
||||
import emu.skyline.preference.SeekBarPreference
|
||||
@ -23,7 +23,7 @@ import emu.skyline.utils.serializable
|
||||
* This fragment is used to display custom game preferences
|
||||
*/
|
||||
class GameSettingsFragment : PreferenceFragmentCompat() {
|
||||
private val item by lazy { requireArguments().serializable<AppItem>(AppItemTag)!! }
|
||||
private val item by lazy { requireArguments().serializable<BaseAppItem>(AppItemTag)!! }
|
||||
|
||||
override fun onViewCreated(view : View, savedInstanceState : Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
93
app/src/main/java/emu/skyline/utils/ContentsHelper.kt
Normal file
93
app/src/main/java/emu/skyline/utils/ContentsHelper.kt
Normal file
@ -0,0 +1,93 @@
|
||||
|
||||
package emu.skyline.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.content.ContentResolver
|
||||
import android.net.Uri
|
||||
import android.provider.OpenableColumns
|
||||
import emu.skyline.SkylineApplication
|
||||
import emu.skyline.getPublicFilesDir
|
||||
import java.io.*
|
||||
|
||||
class ContentsHelper(private val context: Context) {
|
||||
|
||||
private val fileName = "contents.dat"
|
||||
|
||||
fun saveContents(contents: List<Serializable>) {
|
||||
try {
|
||||
val fileOutputStream = context.openFileOutput(fileName, Context.MODE_PRIVATE)
|
||||
val objectOutputStream = ObjectOutputStream(fileOutputStream)
|
||||
objectOutputStream.writeObject(contents)
|
||||
objectOutputStream.close()
|
||||
fileOutputStream.close()
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun loadContents(): List<Serializable> {
|
||||
return try {
|
||||
val fileInputStream = context.openFileInput(fileName)
|
||||
val objectInputStream = ObjectInputStream(fileInputStream)
|
||||
val contents = objectInputStream.readObject() as List<Serializable>
|
||||
objectInputStream.close()
|
||||
fileInputStream.close()
|
||||
contents
|
||||
} catch (e: FileNotFoundException) {
|
||||
emptyList()
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
emptyList()
|
||||
} catch (e: ClassNotFoundException) {
|
||||
e.printStackTrace()
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
fun save(uri: Uri, cr: ContentResolver): Uri? {
|
||||
val destinationDir = File("${SkylineApplication.instance.getPublicFilesDir().canonicalPath}/contents/")
|
||||
if (!destinationDir.exists()) destinationDir.mkdirs()
|
||||
|
||||
val fileName = getFileName(uri, cr) ?: return null
|
||||
val destinationFile = File(destinationDir, fileName)
|
||||
|
||||
try {
|
||||
cr.openInputStream(uri)?.use { inputStream ->
|
||||
FileOutputStream(destinationFile).use { outputStream ->
|
||||
inputStream.copyTo(outputStream)
|
||||
}
|
||||
}
|
||||
return Uri.fromFile(destinationFile)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun getFileName(uri: Uri, cr: ContentResolver): String? {
|
||||
if (uri.scheme == "content") {
|
||||
val cursor = cr.query(uri, null, null, null, null)
|
||||
cursor?.use {
|
||||
if (it.moveToFirst()) {
|
||||
val nameIndex = it.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)
|
||||
return it.getString(nameIndex)
|
||||
}
|
||||
}
|
||||
} else if (uri.scheme == "file") {
|
||||
return File(uri.path!!).name
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun getUriSize(context: Context, uri: Uri): Long? {
|
||||
var size: Long? = null
|
||||
context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
|
||||
val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
|
||||
if (sizeIndex != -1) {
|
||||
cursor.moveToFirst()
|
||||
size = cursor.getLong(sizeIndex)
|
||||
}
|
||||
}
|
||||
return size
|
||||
}
|
||||
}
|
@ -218,5 +218,58 @@
|
||||
app:icon="@drawable/ic_delete" />
|
||||
|
||||
</com.google.android.flexbox.FlexboxLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/contents"
|
||||
style="?attr/textAppearanceLabelLarge"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/manage_contents"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/flexboxCache" />
|
||||
|
||||
<com.google.android.flexbox.FlexboxLayout
|
||||
android:id="@+id/flexboxContents"
|
||||
style="@style/ThemeOverlay.Material3.Button.IconButton.Filled.Tonal"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
app:alignItems="center"
|
||||
app:flexWrap="nowrap"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/contents"
|
||||
app:layout_constraintVertical_bias="1">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/import_update"
|
||||
style="@style/Widget.Material3.Button.TonalButton.Icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/import_update"
|
||||
android:text="@string/import_update"
|
||||
app:icon="@drawable/ic_add" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/import_dlcs"
|
||||
style="@style/Widget.Material3.Button.TonalButton.Icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:contentDescription="@string/import_dlcs"
|
||||
android:text="@string/import_dlcs"
|
||||
app:icon="@drawable/ic_add" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/delete_contents"
|
||||
style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
app:icon="@drawable/ic_delete"
|
||||
app:iconGravity="textStart" />
|
||||
|
||||
</com.google.android.flexbox.FlexboxLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</LinearLayout>
|
||||
|
@ -5,7 +5,7 @@
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/progress_bar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -24,6 +24,10 @@
|
||||
<string name="delete_save">Delete save</string>
|
||||
<string name="import_save">Import</string>
|
||||
<string name="export_save">Export</string>
|
||||
<string name="manage_contents">Contents</string>
|
||||
<string name="import_update">Update</string>
|
||||
<string name="import_dlcs">DLCs</string>
|
||||
<string name="importing">Importing</string>
|
||||
<string name="searching_roms">Searching for ROMs</string>
|
||||
<string name="invalid_file">Invalid file</string>
|
||||
<string name="missing_title_key">Missing title key</string>
|
||||
|
Loading…
x
Reference in New Issue
Block a user