0
0
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:
Ishan09811 2025-01-24 15:24:31 +05:30 committed by GitHub
parent 184ee1ab26
commit d165984c89
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
40 changed files with 1641 additions and 354 deletions

View File

@ -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

View File

@ -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) {

View File

@ -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++) {

View File

@ -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{};

View File

@ -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;

View File

@ -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);
}

View File

@ -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);

View File

@ -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) {

View File

@ -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);

View File

@ -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.");
}
}
}

View File

@ -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);
};
}

View File

@ -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 {};
}

View File

@ -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),

View File

@ -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 {};
}

View File

@ -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 {};

View 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;
}
}

View 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);
};
}

View 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;
}
}

View 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();
};
}

View File

@ -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);

View File

@ -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);

View File

@ -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 &sectionHeader{header.sectionHeaders.at(i)};
auto &sectionEntry{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 &section = 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 &sectionHeader, 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 &section = sections[i];
ValidateNCA(section);
if (section.raw.header.fsType == NcaSectionFsType::RomFs)
ReadRomFs(section, header.sectionTables[i]);
}
}
void NCA::ReadPfs0(const NCASectionHeader &section, 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 &sectionHeader, 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 &sectionHeader, 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 &sectionHeader, std::shared_ptr<Backing> rawBacking, size_t offset) {
std::shared_ptr<Backing> NCA::CreateBacking(const NCASectionHeader &sectionHeader, 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 &sectionHeader) {
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);
}
}

View File

@ -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 &sectionHeader, const NcaFsEntry &entry);
void ReadPfs0(const NCASectionHeader &sectionHeader, const NCASectionTableEntry &entry);
void ReadRomFs(const NcaSectionHeader &sectionHeader, const NcaFsEntry &entry);
void ReadRomFs(const NCASectionHeader &sectionHeader, const NCASectionTableEntry &entry);
std::shared_ptr<Backing> CreateBacking(const NcaSectionHeader &sectionHeader, std::shared_ptr<Backing> rawBacking, size_t offset);
std::shared_ptr<Backing> CreateBacking(const NCASectionHeader &sectionHeader, 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 &sectionHeader);
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);
};
}
}

View 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;
}
}

View 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);
};
}

View File

@ -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)
}
}

View File

@ -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")

View File

@ -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")

View File

@ -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))

View File

@ -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 }
}
}

View 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 ?: "" }}"
}

View File

@ -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)

View File

@ -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
)
}
/**

View File

@ -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())
}

View File

@ -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()

View File

@ -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)

View 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
}
}

View File

@ -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>

View File

@ -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"

View File

@ -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>