Add content/nca viewer for games menu, fix manual nca title fetch for chinese lang icon, rename "dump" to "export".

This commit is contained in:
ITotalJustice
2025-08-06 14:11:05 +01:00
parent 3fee702ee2
commit a0370912da
25 changed files with 1738 additions and 434 deletions

View File

@@ -49,6 +49,8 @@ add_executable(sphaira
source/ui/menus/mtp_menu.cpp
source/ui/menus/gc_menu.cpp
source/ui/menus/game_menu.cpp
source/ui/menus/game_meta_menu.cpp
source/ui/menus/game_nca_menu.cpp
source/ui/menus/grid_menu_base.cpp
source/ui/menus/install_stream_menu_base.cpp
@@ -207,7 +209,7 @@ FetchContent_Declare(libusbhsfs
FetchContent_Declare(libnxtc
GIT_REPOSITORY https://github.com/ITotalJustice/libnxtc.git
GIT_TAG 0d369b8
GIT_TAG 88ce3d8
)
FetchContent_Declare(nvjpg

View File

@@ -2,6 +2,10 @@
#include "ui/menus/grid_menu_base.hpp"
#include "ui/list.hpp"
#include "yati/container/base.hpp"
#include "yati/nx/keys.hpp"
#include "title_info.hpp"
#include "fs.hpp"
#include "option.hpp"
@@ -101,4 +105,69 @@ private:
option::OptionBool m_title_cache{INI_SECTION, "title_cache", true};
};
struct NcmMetaData {
// points to global service, do not close manually!
NcmContentStorage* cs{};
NcmContentMetaDatabase* db{};
u64 app_id{};
NcmContentMetaKey key{};
};
Result GetMetaEntries(const Entry& e, title::MetaEntries& out, u32 flags = title::ContentFlag_All);
Result GetNcmMetaFromMetaStatus(const NsApplicationContentMetaStatus& status, NcmMetaData& out);
void DeleteMetaEntries(u64 app_id, int image, const std::string& name, const title::MetaEntries& entries);
struct TikEntry {
FsRightsId id{};
u8 key_gen{};
std::vector<u8> tik_data{};
std::vector<u8> cert_data{};
};
struct NspEntry {
// application name.
std::string application_name{};
// name of the nsp (name [id][v0][BASE].nsp).
fs::FsPath path{};
// tickets and cert data, will be empty if title key crypto isn't used.
std::vector<TikEntry> tickets{};
// all the collections for this nsp, such as nca's and tickets.
std::vector<yati::container::CollectionEntry> collections{};
// raw nsp data (header, file table and string table).
std::vector<u8> nsp_data{};
// size of the entier nsp.
s64 nsp_size{};
// copy of ncm cs, it is not closed.
NcmContentStorage cs{};
// copy of the icon, if invalid, it will use the default icon.
int icon{};
Result Read(void* buf, s64 off, s64 size, u64* bytes_read);
private:
static auto InRange(s64 off, s64 offset, s64 size) -> bool {
return off < offset + size && off >= offset;
}
static auto ClipSize(s64 off, s64 size, s64 file_size) -> s64 {
return std::min(size, file_size - off);
}
};
struct ContentInfoEntry {
NsApplicationContentMetaStatus status{};
std::vector<NcmContentInfo> content_infos{};
std::vector<NcmRightsId> ncm_rights_id{};
};
auto BuildNspPath(const Entry& e, const NsApplicationContentMetaStatus& status) -> fs::FsPath;
Result BuildContentEntry(const NsApplicationContentMetaStatus& status, ContentInfoEntry& out);
Result BuildNspEntry(const Entry& e, const ContentInfoEntry& info, const keys::Keys& keys, NspEntry& out);
Result BuildNspEntries(Entry& e, const title::MetaEntries& meta_entries, std::vector<NspEntry>& out);
Result BuildNspEntries(Entry& e, u32 flags, std::vector<NspEntry>& out);
// dumps the array of nsp entries.
void DumpNsp(const std::vector<NspEntry>& entries);
} // namespace sphaira::ui::menu::game

View File

@@ -0,0 +1,111 @@
#pragma once
#include "ui/menus/menu_base.hpp"
#include "ui/menus/game_menu.hpp"
#include "ui/list.hpp"
#include "yati/nx/ncm.hpp"
#include <span>
#include <memory>
namespace sphaira::ui::menu::game::meta {
enum TicketType : u8 {
TicketType_None,
TicketType_Common,
TicketType_Personalised,
TicketType_Missing,
};
struct MiniNacp {
char display_version[0x10];
};
struct MetaEntry {
NsApplicationContentMetaStatus status{};
ncm::ContentMeta content_meta{};
// small version of nacp to speed up loading.
MiniNacp nacp{};
// total size of all ncas.
s64 size{};
// set to the key gen (if possible), only if title key encrypted.
u8 key_gen{};
// set to the ticket type.
u8 ticket_type{TicketType_None};
// set if it has missing ncas.
u8 missing_count{};
// set if selected.
bool selected{};
// set if we have checked the above meta data.
bool checked{};
};
struct Menu final : MenuBase {
Menu(Entry& entry);
~Menu();
auto GetShortTitle() const -> const char* override { return "Meta"; };
void Update(Controller* controller, TouchInfo* touch) override;
void Draw(NVGcontext* vg, Theme* theme) override;
private:
void SetIndex(s64 index);
void Scan();
void UpdateSubheading();
auto GetSelectedEntries() const {
title::MetaEntries out;
for (auto& e : m_entries) {
if (e.selected) {
out.emplace_back(e.status);
}
}
if (!m_entries.empty() && out.empty()) {
out.emplace_back(m_entries[m_index].status);
}
return out;
}
void ClearSelection() {
for (auto& e : m_entries) {
e.selected = false;
}
m_selected_count = 0;
}
auto GetEntry(u32 index) -> MetaEntry& {
return m_entries[index];
}
auto GetEntry(u32 index) const -> const MetaEntry& {
return m_entries[index];
}
auto GetEntry() -> MetaEntry& {
return GetEntry(m_index);
}
auto GetEntry() const -> const MetaEntry& {
return GetEntry(m_index);
}
void DumpGames();
void DeleteGames();
Result ResetRequiredSystemVersion(MetaEntry& entry) const;
Result GetNcmSizeOfMetaStatus(MetaEntry& entry) const;
private:
Entry& m_entry;
std::vector<MetaEntry> m_entries{};
s64 m_index{};
s64 m_selected_count{};
std::unique_ptr<List> m_list{};
bool m_dirty{};
std::vector<FsRightsId> m_common_tickets{};
std::vector<FsRightsId> m_personalised_tickets{};
};
} // namespace sphaira::ui::menu::game::meta

View File

@@ -0,0 +1,91 @@
#pragma once
#include "ui/menus/menu_base.hpp"
#include "ui/menus/game_meta_menu.hpp"
#include "ui/list.hpp"
#include "yati/nx/nca.hpp"
#include "yati/nx/ncm.hpp"
#include <span>
#include <memory>
namespace sphaira::ui::menu::game::meta_nca {
struct NcaEntry {
NcmContentId content_id{};
u64 size{};
u8 content_type{};
// decrypted nca header.
nca::Header header{};
// set if missing.
bool missing{};
// set if selected.
bool selected{};
// set if we have checked the above meta data.
bool checked{};
};
struct Menu final : MenuBase {
Menu(Entry& entry, const meta::MetaEntry& meta_entry);
~Menu();
auto GetShortTitle() const -> const char* override { return "Nca"; };
void Update(Controller* controller, TouchInfo* touch) override;
void Draw(NVGcontext* vg, Theme* theme) override;
private:
void SetIndex(s64 index);
void Scan();
void UpdateSubheading();
auto GetSelectedEntries() const {
std::vector<NcaEntry> out;
for (auto& e : m_entries) {
if (e.selected && !e.missing) {
out.emplace_back(e);
}
}
if (!m_entries.empty() && out.empty()) {
out.emplace_back(m_entries[m_index]);
}
return out;
}
void ClearSelection() {
for (auto& e : m_entries) {
e.selected = false;
}
m_selected_count = 0;
}
auto GetEntry(u32 index) -> NcaEntry& {
return m_entries[index];
}
auto GetEntry(u32 index) const -> const NcaEntry& {
return m_entries[index];
}
auto GetEntry() -> NcaEntry& {
return GetEntry(m_index);
}
auto GetEntry() const -> const NcaEntry& {
return GetEntry(m_index);
}
void DumpNcas();
private:
Entry& m_entry;
const meta::MetaEntry& m_meta_entry;
NcmMetaData m_meta{};
std::vector<NcaEntry> m_entries{};
s64 m_index{};
s64 m_selected_count{};
std::unique_ptr<List> m_list{};
};
} // namespace sphaira::ui::menu::game::meta_nca

View File

@@ -203,4 +203,16 @@ Result ShouldPatchTicket(const TicketData& data, std::span<const u8> ticket, std
Result ShouldPatchTicket(std::span<const u8> ticket, std::span<const u8> cert_chain, bool patch_personalised, bool& should_patch);
Result PatchTicket(std::vector<u8>& ticket, std::span<const u8> cert_chain, u8 key_gen, const keys::Keys& keys, bool patch_personalised);
// fills out with the list of common / personalised rights ids.
Result GetCommonTickets(std::vector<FsRightsId>& out);
Result GetPersonalisedTickets(std::vector<FsRightsId>& out);
// checks if the rights id is found in common / personalised.
Result IsRightsIdCommon(const FsRightsId& id, bool* out);
Result IsRightsIdPersonalised(const FsRightsId& id, bool* out);
// helper for the above if the db has already been parsed.
bool IsRightsIdValid(const FsRightsId& id);
bool IsRightsIdFound(const FsRightsId& id, std::span<const FsRightsId> ids);
} // namespace sphaira::es

View File

@@ -181,7 +181,15 @@ struct Header {
u64 size;
u64 program_id;
u32 context_id;
u32 sdk_version;
union {
u32 sdk_version;
struct {
u8 sdk_revision;
u8 sdk_micro;
u8 sdk_minor;
u8 sdk_major;
};
};
u8 key_gen; // see KeyGeneration.
u8 sig_key_gen;
u8 _0x222[0xE]; // empty.
@@ -215,6 +223,9 @@ struct Header {
};
static_assert(sizeof(Header) == 0xC00);
auto GetContentTypeStr(u8 content_type) -> const char*;
auto GetDistributionTypeStr(u8 distribution_type) -> const char*;
Result DecryptKeak(const keys::Keys& keys, Header& header);
Result EncryptKeak(const keys::Keys& keys, Header& header, u8 key_generation);
Result VerifyFixedKey(const Header& header);

View File

@@ -1,6 +1,9 @@
#pragma once
#include "fs.hpp"
#include <switch.h>
#include <vector>
namespace sphaira::ncm {
@@ -31,10 +34,19 @@ union ExtendedHeader {
NcmDataPatchMetaExtendedHeader data_patch;
};
struct ContentMeta {
NcmContentMetaHeader header;
ExtendedHeader extened;
};
auto GetMetaTypeStr(u8 meta_type) -> const char*;
auto GetContentTypeStr(u8 content_type) -> const char*;
auto GetStorageIdStr(u8 storage_id) -> const char*;
auto GetMetaTypeShortStr(u8 meta_type) -> const char*;
auto GetReadableMetaTypeStr(u8 meta_type) -> const char*;
auto GetReadableStorageIdStr(u8 storage_id) -> const char*;
auto GetAppId(u8 meta_type, u64 id) -> u64;
auto GetAppId(const NcmContentMetaKey& key) -> u64;
auto GetAppId(const PackagedContentMeta& meta) -> u64;
@@ -44,4 +56,30 @@ auto GetContentIdFromStr(const char* str) -> NcmContentId;
Result Delete(NcmContentStorage* cs, const NcmContentId *content_id);
Result Register(NcmContentStorage* cs, const NcmContentId *content_id, const NcmPlaceHolderId *placeholder_id);
// fills out with the content header, which includes the normal and extended header.
Result GetContentMeta(NcmContentMetaDatabase *db, const NcmContentMetaKey *key, ContentMeta& out);
// fills out will a list of all content infos tied to the key.
Result GetContentInfos(NcmContentMetaDatabase *db, const NcmContentMetaKey *key, std::vector<NcmContentInfo>& out);
// same as above but accepts the ncm header rather than fetching it.
Result GetContentInfos(NcmContentMetaDatabase *db, const NcmContentMetaKey *key, const NcmContentMetaHeader& header, std::vector<NcmContentInfo>& out);
// removes key from ncm, including ncas and setting the db.
Result DeleteKey(NcmContentStorage* cs, NcmContentMetaDatabase *db, const NcmContentMetaKey *key);
// sets the required system version.
Result SetRequiredSystemVersion(NcmContentMetaDatabase *db, const NcmContentMetaKey *key, u32 version);
// returns true if type is application or update.
static constexpr inline bool HasRequiredSystemVersion(u8 meta_type) {
return meta_type == NcmContentMetaType_Application || meta_type == NcmContentMetaType_Patch;
}
static constexpr inline bool HasRequiredSystemVersion(const NcmContentMetaKey *key) {
return HasRequiredSystemVersion(key->type);
}
// fills program id and out path of the control nca.
Result GetControlPathFromContentId(NcmContentStorage* cs, const NcmContentMetaKey& key, const NcmContentId& id, u64* out_program_id, fs::FsPath* out_path);
} // namespace sphaira::ncm

View File

@@ -1,8 +1,11 @@
#pragma once
#include <switch.h>
#include "ncm.hpp"
#include <switch.h>
#include <span>
#include <vector>
namespace sphaira::ns {
enum ApplicationRecordType {
@@ -16,9 +19,26 @@ enum ApplicationRecordType {
ApplicationRecordType_Archived = 0xB,
};
Result PushApplicationRecord(Service* srv, u64 tid, const ncm::ContentStorageRecord* records, u32 count);
Result ListApplicationRecordContentMeta(Service* srv, u64 offset, u64 tid, ncm::ContentStorageRecord* out_records, u32 count, s32* entries_read);
Result DeleteApplicationRecord(Service* srv, u64 tid);
Result InvalidateApplicationControlCache(Service* srv, u64 tid);
Result Initialize();
void Exit();
Result PushApplicationRecord(u64 tid, const ncm::ContentStorageRecord* records, u32 count);
Result ListApplicationRecordContentMeta(u64 offset, u64 tid, ncm::ContentStorageRecord* out_records, u32 count, s32* entries_read);
Result DeleteApplicationRecord(u64 tid);
Result InvalidateApplicationControlCache(u64 tid);
// helpers
// fills out with the number or records available
Result GetApplicationRecords(u64 id, std::vector<ncm::ContentStorageRecord>& out);
// sets the lowest launch version based on the current record list.
Result SetLowestLaunchVersion(u64 id);
// same as above, but uses the provided record list.
Result SetLowestLaunchVersion(u64 id, std::span<const ncm::ContentStorageRecord> records);
static inline bool IsNsControlFetchSlow() {
return hosversionAtLeast(20,0,0);
}
} // namespace sphaira::ns

View File

@@ -520,6 +520,12 @@ void App::Loop() {
auto App::Push(std::unique_ptr<ui::Widget>&& widget) -> void {
log_write("[Mui] pushing widget\n");
// check if the widget wants to pop before adding.
// this can happen if something failed in the constructor and the widget wants to exit.
if (widget->ShouldPop()) {
return;
}
if (!g_app->m_widgets.empty()) {
g_app->m_widgets.back()->OnFocusLost();
}
@@ -1767,9 +1773,9 @@ void App::DisplayAdvancedOptions(bool left_side) {
}, "Change the install options.\n"\
"You can enable installing from here."_i18n);
options->Add<ui::SidebarEntryCallback>("Dump options"_i18n, [left_side](){
options->Add<ui::SidebarEntryCallback>("Export options"_i18n, [left_side](){
App::DisplayDumpOptions(left_side);
}, "Change the dump options."_i18n);
}, "Change the export options."_i18n);
static const char* erpt_path = "/atmosphere/erpt_reports";
options->Add<ui::SidebarEntryBool>("Disable erpt_reports"_i18n, fs::FsNativeSd().FileExists(erpt_path), [](bool& enable){
@@ -1879,7 +1885,7 @@ void App::DisplayInstallOptions(bool left_side) {
}
void App::DisplayDumpOptions(bool left_side) {
auto options = std::make_unique<ui::Sidebar>("Dump Options"_i18n, left_side ? ui::Sidebar::Side::LEFT : ui::Sidebar::Side::RIGHT);
auto options = std::make_unique<ui::Sidebar>("Export Options"_i18n, left_side ? ui::Sidebar::Side::LEFT : ui::Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(std::move(options)));
options->Add<ui::SidebarEntryBool>(

View File

@@ -355,7 +355,7 @@ void DumpGetLocation(const std::string& title, u32 location_flags, const OnLocat
}
void Dump(const std::shared_ptr<BaseSource>& source, const DumpLocation& location, const std::vector<fs::FsPath>& paths, const OnExit& on_exit) {
App::Push<ui::ProgressBox>(0, "Dumping"_i18n, "", [source, paths, location](auto pbox) -> Result {
App::Push<ui::ProgressBox>(0, "Exporting"_i18n, "", [source, paths, location](auto pbox) -> Result {
if (location.entry.type == DumpLocationType_Network) {
R_TRY(DumpToNetwork(pbox, location.network[location.entry.index], source.get(), paths));
} else if (location.entry.type == DumpLocationType_Stdio) {
@@ -372,10 +372,10 @@ void Dump(const std::shared_ptr<BaseSource>& source, const DumpLocation& locatio
R_SUCCEED();
}, [on_exit](Result rc){
App::PushErrorBox(rc, "Dump failed!"_i18n);
App::PushErrorBox(rc, "Export failed!"_i18n);
if (R_SUCCEEDED(rc)) {
App::Notify("Dump successfull!"_i18n);
App::Notify("Export successfull!"_i18n);
log_write("dump successfull!!!\n");
}
@@ -384,7 +384,7 @@ void Dump(const std::shared_ptr<BaseSource>& source, const DumpLocation& locatio
}
void Dump(const std::shared_ptr<BaseSource>& source, const std::vector<fs::FsPath>& paths, const OnExit& on_exit, u32 location_flags) {
DumpGetLocation("Select dump location"_i18n, location_flags, [source, paths, on_exit](const DumpLocation& loc) {
DumpGetLocation("Select export location"_i18n, location_flags, [source, paths, on_exit](const DumpLocation& loc) {
Dump(source, loc, paths, on_exit);
});
}

View File

@@ -851,8 +851,8 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor
R_TRY(ncmInitialize());
ON_SCOPE_EXIT(ncmExit());
R_TRY(nsInitialize());
ON_SCOPE_EXIT(nsExit());
R_TRY(ns::Initialize());
ON_SCOPE_EXIT(ns::Exit());
keys::Keys keys;
R_TRY(keys::parse_keys(keys, false));
@@ -974,20 +974,6 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor
// push record
{
pbox->NewTransfer("Pushing application record"_i18n).UpdateTransfer(5, 8);
Service srv{}, *srv_ptr = &srv;
bool already_installed{};
if (hosversionAtLeast(3,0,0)) {
R_TRY(nsGetApplicationManagerInterface(&srv));
} else {
srv_ptr = nsGetServiceSession_ApplicationManagerInterface();
}
ON_SCOPE_EXIT(serviceClose(&srv));
if (hosversionAtLeast(2,0,0)) {
R_TRY(nsIsAnyApplicationEntityInstalled(tid, &already_installed));
}
// remove old id for forwarders.
const auto rc = nsDeleteApplicationCompletely(old_tid);
@@ -995,19 +981,13 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor
App::Notify("Failed to remove old forwarder, please manually remove it!"_i18n);
}
// remove previous application record
if (already_installed || hosversionBefore(2,0,0)) {
const auto rc = ns::DeleteApplicationRecord(srv_ptr, tid);
R_UNLESS(R_SUCCEEDED(rc) || hosversionBefore(2,0,0), rc);
}
// remove previous ncas.
nsDeleteApplicationEntity(tid);
R_TRY(ns::PushApplicationRecord(srv_ptr, tid, &content_storage_record, 1));
R_TRY(ns::PushApplicationRecord(tid, &content_storage_record, 1));
// force flush
if (already_installed || hosversionBefore(2,0,0)) {
const auto rc = ns::InvalidateApplicationControlCache(srv_ptr, tid);
R_UNLESS(R_SUCCEEDED(rc) || hosversionBefore(2,0,0), rc);
}
// force flush.
ns::InvalidateApplicationControlCache(tid);
}
R_SUCCEED();

View File

@@ -99,6 +99,10 @@ struct ThreadData {
void SetReadResult(Result result) {
read_result = result;
// wake up write thread as it may be waiting on data that never comes.
condvarWakeOne(std::addressof(can_write));
if (R_FAILED(result)) {
ueventSignal(GetDoneEvent());
}

View File

@@ -3,6 +3,7 @@
#include "ui/types.hpp"
#include "log.hpp"
#include "yati/nx/ns.hpp"
#include "yati/nx/nca.hpp"
#include "yati/nx/ncm.hpp"
@@ -251,36 +252,37 @@ auto ThreadData::Get(u64 app_id, bool* cached) -> ThreadResultData* {
*cached = false;
}
bool manual_load = true;
bool has_nacp = false;
bool manual_load = false;
u64 actual_size{};
auto control = std::make_unique<NsApplicationControlData>();
if (hosversionBefore(20,0,0)) {
if (!ns::IsNsControlFetchSlow()) {
TimeStamp ts;
if (R_SUCCEEDED(nsGetApplicationControlData(NsApplicationControlSource_CacheOnly, app_id, control.get(), sizeof(NsApplicationControlData), &actual_size))) {
manual_load = false;
has_nacp = true;
log_write("\t\t[ns control cache] time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs());
}
}
if (manual_load) {
manual_load = R_SUCCEEDED(LoadControlManual(app_id, control->nacp, result.get()));
if (!has_nacp) {
has_nacp = manual_load = R_SUCCEEDED(LoadControlManual(app_id, control->nacp, result.get()));
}
Result rc{};
if (!manual_load) {
if (!has_nacp) {
TimeStamp ts;
if (R_SUCCEEDED(rc = nsGetApplicationControlData(NsApplicationControlSource_Storage, app_id, control.get(), sizeof(NsApplicationControlData), &actual_size))) {
if (R_SUCCEEDED(nsGetApplicationControlData(NsApplicationControlSource_Storage, app_id, control.get(), sizeof(NsApplicationControlData), &actual_size))) {
has_nacp = true;
log_write("\t\t[ns control storage] time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs());
}
}
if (R_FAILED(rc)) {
if (!has_nacp) {
FakeNacpEntry(result.get());
} else {
bool valid = true;
NacpLanguageEntry* lang;
if (R_SUCCEEDED(nsGetApplicationDesiredLanguage(&control->nacp, &lang))) {
if (R_SUCCEEDED(nsGetApplicationDesiredLanguage(&control->nacp, &lang)) && lang) {
result->lang = *lang;
} else {
FakeNacpEntry(result.get());
@@ -373,7 +375,7 @@ Result Init() {
SCOPED_MUTEX(&g_mutex);
if (!g_ref_count) {
R_TRY(nsInitialize());
R_TRY(ns::Initialize());
R_TRY(ncmInitialize());
for (auto& e : ncm_entries) {
@@ -409,7 +411,7 @@ void Exit() {
e.Close();
}
nsExit();
ns::Exit();
ncmExit();
}
}
@@ -453,17 +455,16 @@ auto GetNcmDb(u8 storage_id) -> NcmContentMetaDatabase& {
}
Result GetMetaEntries(u64 id, MetaEntries& out, u32 flags) {
for (s32 i = 0; ; i++) {
s32 count;
NsApplicationContentMetaStatus status;
R_TRY(nsListApplicationContentMetaStatus(id, i, &status, 1, &count));
s32 count;
R_TRY(nsCountApplicationContentMeta(id, &count));
if (!count) {
break;
}
std::vector<NsApplicationContentMetaStatus> entries(count);
R_TRY(nsListApplicationContentMetaStatus(id, 0, entries.data(), entries.size(), &count));
entries.resize(count);
if (flags & ContentMetaTypeToContentFlag(status.meta_type)) {
out.emplace_back(status);
for (const auto& e : entries) {
if (flags & ContentMetaTypeToContentFlag(e.meta_type)) {
out.emplace_back(e);
}
}
@@ -485,10 +486,7 @@ Result GetControlPathFromStatus(const NsApplicationContentMetaStatus& status, u6
NcmContentId content_id;
R_TRY(ncmContentMetaDatabaseGetContentIdByType(&db, &content_id, &key, NcmContentType_Control));
R_TRY(ncmContentStorageGetProgramId(&cs, out_program_id, &content_id, FsContentAttributes_All));
R_TRY(ncmContentStorageGetPath(&cs, out_path->s, sizeof(*out_path), &content_id));
R_SUCCEED();
return ncm::GetControlPathFromContentId(&cs, key, content_id, out_program_id, out_path);
}
// taken from nxdumptool.

View File

@@ -8,6 +8,7 @@
#include "swkbd.hpp"
#include "ui/menus/game_menu.hpp"
#include "ui/menus/game_meta_menu.hpp"
#include "ui/menus/save_menu.hpp"
#include "ui/sidebar.hpp"
#include "ui/error_box.hpp"
@@ -18,6 +19,7 @@
#include "yati/nx/ncm.hpp"
#include "yati/nx/nca.hpp"
#include "yati/nx/ns.hpp"
#include "yati/nx/es.hpp"
#include "yati/container/base.hpp"
#include "yati/container/nsp.hpp"
@@ -30,87 +32,6 @@
namespace sphaira::ui::menu::game {
namespace {
struct ContentInfoEntry {
NsApplicationContentMetaStatus status{};
std::vector<NcmContentInfo> content_infos{};
std::vector<NcmRightsId> ncm_rights_id{};
};
struct TikEntry {
FsRightsId id{};
u8 key_gen{};
std::vector<u8> tik_data{};
std::vector<u8> cert_data{};
};
struct NspEntry {
// application name.
std::string application_name{};
// name of the nsp (name [id][v0][BASE].nsp).
fs::FsPath path{};
// tickets and cert data, will be empty if title key crypto isn't used.
std::vector<TikEntry> tickets{};
// all the collections for this nsp, such as nca's and tickets.
std::vector<yati::container::CollectionEntry> collections{};
// raw nsp data (header, file table and string table).
std::vector<u8> nsp_data{};
// size of the entier nsp.
s64 nsp_size{};
// copy of ncm cs, it is not closed.
NcmContentStorage cs{};
// copy of the icon, if invalid, it will use the default icon.
int icon{};
// todo: benchmark manual sdcard read and decryption vs ncm.
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) {
if (off < nsp_data.size()) {
*bytes_read = size = ClipSize(off, size, nsp_data.size());
std::memcpy(buf, nsp_data.data() + off, size);
R_SUCCEED();
}
// adjust offset.
off -= nsp_data.size();
for (const auto& collection : collections) {
if (InRange(off, collection.offset, collection.size)) {
// adjust offset relative to the collection.
off -= collection.offset;
*bytes_read = size = ClipSize(off, size, collection.size);
if (collection.name.ends_with(".nca")) {
const auto id = ncm::GetContentIdFromStr(collection.name.c_str());
return ncmContentStorageReadContentIdFile(&cs, buf, size, &id, off);
} else if (collection.name.ends_with(".tik") || collection.name.ends_with(".cert")) {
FsRightsId id;
keys::parse_hex_key(&id, collection.name.c_str());
const auto it = std::ranges::find_if(tickets, [&id](auto& e){
return !std::memcmp(&id, &e.id, sizeof(id));
});
R_UNLESS(it != tickets.end(), Result_GameBadReadForDump);
const auto& data = collection.name.ends_with(".tik") ? it->tik_data : it->cert_data;
std::memcpy(buf, data.data() + off, size);
R_SUCCEED();
}
}
}
log_write("did not find collection...\n");
return 0x1;
}
private:
static auto InRange(s64 off, s64 offset, s64 size) -> bool {
return off < offset + size && off >= offset;
}
static auto ClipSize(s64 off, s64 size, s64 file_size) -> s64 {
return std::min(size, file_size - off);
}
};
struct NspSource final : dump::BaseSource {
NspSource(const std::vector<NspEntry>& entries) : m_entries{entries} {
m_is_file_based_emummc = App::IsFileBaseEmummc();
@@ -181,9 +102,6 @@ Result Notify(Result rc, const std::string& error_message) {
return rc;
}
Result GetMetaEntries(const Entry& e, title::MetaEntries& out, u32 flags = title::ContentFlag_All) {
return title::GetMetaEntries(e.app_id, out, flags);
}
bool LoadControlImage(Entry& e, title::ThreadResultData* result) {
if (!e.image && result && !result->icon.empty()) {
@@ -217,11 +135,6 @@ void LoadControlEntry(Entry& e, bool force_image_load = false) {
}
}
auto isRightsIdValid(FsRightsId id) -> bool {
FsRightsId empty_id{};
return 0 != std::memcmp(std::addressof(id), std::addressof(empty_id), sizeof(id));
}
struct HashStr {
char str[0x21];
};
@@ -234,170 +147,6 @@ HashStr hexIdToStr(auto id) {
return str;
}
auto BuildNspPath(const Entry& e, const NsApplicationContentMetaStatus& status) -> fs::FsPath {
fs::FsPath name_buf = e.GetName();
title::utilsReplaceIllegalCharacters(name_buf, true);
char version[sizeof(NacpStruct::display_version) + 1]{};
if (status.meta_type == NcmContentMetaType_Patch) {
u64 program_id;
fs::FsPath path;
if (R_SUCCEEDED(title::GetControlPathFromStatus(status, &program_id, &path))) {
char display_version[0x10];
if (R_SUCCEEDED(nca::ParseControl(path, program_id, display_version, sizeof(display_version), nullptr, offsetof(NacpStruct, display_version)))) {
std::snprintf(version, sizeof(version), "%s ", display_version);
}
}
}
fs::FsPath path;
if (App::GetApp()->m_dump_app_folder.Get()) {
std::snprintf(path, sizeof(path), "%s/%s %s[%016lX][v%u][%s].nsp", name_buf.s, name_buf.s, version, status.application_id, status.version, ncm::GetMetaTypeShortStr(status.meta_type));
} else {
std::snprintf(path, sizeof(path), "%s %s[%016lX][v%u][%s].nsp", name_buf.s, version, status.application_id, status.version, ncm::GetMetaTypeShortStr(status.meta_type));
}
return path;
}
Result BuildContentEntry(const NsApplicationContentMetaStatus& status, ContentInfoEntry& out) {
auto& cs = title::GetNcmCs(status.storageID);
auto& db = title::GetNcmDb(status.storageID);
const auto app_id = ncm::GetAppId(status.meta_type, status.application_id);
auto id_min = status.application_id;
auto id_max = status.application_id;
// workaround N bug where they don't check the full range in the ID filter.
// https://github.com/Atmosphere-NX/Atmosphere/blob/1d3f3c6e56b994b544fc8cd330c400205d166159/libraries/libstratosphere/source/ncm/ncm_on_memory_content_meta_database_impl.cpp#L22
if (status.storageID == NcmStorageId_None || status.storageID == NcmStorageId_GameCard) {
id_min -= 1;
id_max += 1;
}
s32 meta_total;
s32 meta_entries_written;
NcmContentMetaKey key;
R_TRY(ncmContentMetaDatabaseList(std::addressof(db), std::addressof(meta_total), std::addressof(meta_entries_written), std::addressof(key), 1, (NcmContentMetaType)status.meta_type, app_id, id_min, id_max, NcmContentInstallType_Full));
log_write("ncmContentMetaDatabaseList(): AppId: %016lX Id: %016lX total: %d written: %d storageID: %u key.id %016lX\n", app_id, status.application_id, meta_total, meta_entries_written, status.storageID, key.id);
R_UNLESS(meta_total == 1, Result_GameMultipleKeysFound);
R_UNLESS(meta_entries_written == 1, Result_GameMultipleKeysFound);
std::vector<NcmContentInfo> cnmt_infos;
for (s32 i = 0; ; i++) {
s32 entries_written;
NcmContentInfo info_out;
R_TRY(ncmContentMetaDatabaseListContentInfo(std::addressof(db), std::addressof(entries_written), std::addressof(info_out), 1, std::addressof(key), i));
if (!entries_written) {
break;
}
// check if we need to fetch tickets.
NcmRightsId ncm_rights_id;
R_TRY(ncmContentStorageGetRightsIdFromContentId(std::addressof(cs), std::addressof(ncm_rights_id), std::addressof(info_out.content_id), FsContentAttributes_All));
if (isRightsIdValid(ncm_rights_id.rights_id)) {
const auto it = std::ranges::find_if(out.ncm_rights_id, [&ncm_rights_id](auto& e){
return !std::memcmp(&e, &ncm_rights_id, sizeof(ncm_rights_id));
});
if (it == out.ncm_rights_id.end()) {
out.ncm_rights_id.emplace_back(ncm_rights_id);
}
}
if (info_out.content_type == NcmContentType_Meta) {
cnmt_infos.emplace_back(info_out);
} else {
out.content_infos.emplace_back(info_out);
}
}
// append cnmt at the end of the list, following StandardNSP spec.
out.content_infos.insert_range(out.content_infos.end(), cnmt_infos);
out.status = status;
R_SUCCEED();
}
Result BuildNspEntry(const Entry& e, const ContentInfoEntry& info, const keys::Keys& keys, NspEntry& out) {
out.application_name = e.GetName();
out.path = BuildNspPath(e, info.status);
s64 offset{};
for (auto& e : info.content_infos) {
char nca_name[0x200];
std::snprintf(nca_name, sizeof(nca_name), "%s%s", hexIdToStr(e.content_id).str, e.content_type == NcmContentType_Meta ? ".cnmt.nca" : ".nca");
u64 size;
ncmContentInfoSizeToU64(std::addressof(e), std::addressof(size));
out.collections.emplace_back(nca_name, offset, size);
offset += size;
}
for (auto& ncm_rights_id : info.ncm_rights_id) {
const auto rights_id = ncm_rights_id.rights_id;
const auto key_gen = ncm_rights_id.key_generation;
TikEntry entry{rights_id, key_gen};
log_write("rights id is valid, fetching common ticket and cert\n");
u64 tik_size;
u64 cert_size;
R_TRY(es::GetCommonTicketAndCertificateSize(&tik_size, &cert_size, &rights_id));
log_write("got tik_size: %zu cert_size: %zu\n", tik_size, cert_size);
entry.tik_data.resize(tik_size);
entry.cert_data.resize(cert_size);
R_TRY(es::GetCommonTicketAndCertificateData(&tik_size, &cert_size, entry.tik_data.data(), entry.tik_data.size(), entry.cert_data.data(), entry.cert_data.size(), &rights_id));
log_write("got tik_data: %zu cert_data: %zu\n", tik_size, cert_size);
// patch fake ticket / convert personalised to common if needed.
R_TRY(es::PatchTicket(entry.tik_data, entry.cert_data, key_gen, keys, App::GetApp()->m_dump_convert_to_common_ticket.Get()));
char tik_name[0x200];
std::snprintf(tik_name, sizeof(tik_name), "%s%s", hexIdToStr(rights_id).str, ".tik");
char cert_name[0x200];
std::snprintf(cert_name, sizeof(cert_name), "%s%s", hexIdToStr(rights_id).str, ".cert");
out.collections.emplace_back(tik_name, offset, entry.tik_data.size());
offset += entry.tik_data.size();
out.collections.emplace_back(cert_name, offset, entry.cert_data.size());
offset += entry.cert_data.size();
out.tickets.emplace_back(entry);
}
out.nsp_data = yati::container::Nsp::Build(out.collections, out.nsp_size);
out.cs = title::GetNcmCs(info.status.storageID);
R_SUCCEED();
}
Result BuildNspEntries(Entry& e, u32 flags, std::vector<NspEntry>& out) {
LoadControlEntry(e);
title::MetaEntries meta_entries;
R_TRY(GetMetaEntries(e, meta_entries, flags));
keys::Keys keys;
R_TRY(keys::parse_keys(keys, true));
for (const auto& status : meta_entries) {
ContentInfoEntry info;
R_TRY(BuildContentEntry(status, info));
NspEntry nsp;
R_TRY(BuildNspEntry(e, info, keys, nsp));
out.emplace_back(nsp).icon = e.image;
}
R_UNLESS(!out.empty(), Result_GameNoNspEntriesBuilt);
R_SUCCEED();
}
void FreeEntry(NVGcontext* vg, Entry& e) {
nvgDeleteImage(vg, e.image);
e.image = 0;
@@ -437,6 +186,45 @@ Result CreateSave(u64 app_id, AccountUid uid) {
} // namespace
Result NspEntry::Read(void* buf, s64 off, s64 size, u64* bytes_read) {
if (off < nsp_data.size()) {
*bytes_read = size = ClipSize(off, size, nsp_data.size());
std::memcpy(buf, nsp_data.data() + off, size);
R_SUCCEED();
}
// adjust offset.
off -= nsp_data.size();
for (const auto& collection : collections) {
if (InRange(off, collection.offset, collection.size)) {
// adjust offset relative to the collection.
off -= collection.offset;
*bytes_read = size = ClipSize(off, size, collection.size);
if (collection.name.ends_with(".nca")) {
const auto id = ncm::GetContentIdFromStr(collection.name.c_str());
return ncmContentStorageReadContentIdFile(&cs, buf, size, &id, off);
} else if (collection.name.ends_with(".tik") || collection.name.ends_with(".cert")) {
FsRightsId id;
keys::parse_hex_key(&id, collection.name.c_str());
const auto it = std::ranges::find_if(tickets, [&id](auto& e){
return !std::memcmp(&id, &e.id, sizeof(id));
});
R_UNLESS(it != tickets.end(), Result_GameBadReadForDump);
const auto& data = collection.name.ends_with(".tik") ? it->tik_data : it->cert_data;
std::memcpy(buf, data.data() + off, size);
R_SUCCEED();
}
}
}
log_write("did not find collection...\n");
return 0x1;
}
Menu::Menu(u32 flags) : grid::Menu{"Games"_i18n, flags} {
this->SetActions(
std::make_pair(Button::L3, Action{[this](){
@@ -517,11 +305,9 @@ Menu::Menu(u32 flags) : grid::Menu{"Games"_i18n, flags} {
});
});
#if 0
options->Add<SidebarEntryCallback>("Info"_i18n, [this](){
options->Add<SidebarEntryCallback>("View application content"_i18n, [this](){
App::Push<meta::Menu>(m_entries[m_index]);
});
#endif
options->Add<SidebarEntryCallback>("Launch random game"_i18n, [this](){
const auto random_index = randomGet64() % std::size(m_entries);
@@ -538,61 +324,28 @@ Menu::Menu(u32 flags) : grid::Menu{"Games"_i18n, flags} {
);
});
options->Add<SidebarEntryCallback>("List meta records"_i18n, [this](){
title::MetaEntries meta_entries;
const auto rc = GetMetaEntries(m_entries[m_index], meta_entries);
if (R_FAILED(rc)) {
App::Push<ui::ErrorBox>(rc,
i18n::get("Failed to list application meta entries")
);
return;
}
if (meta_entries.empty()) {
App::Notify("No meta entries found...\n"_i18n);
return;
}
PopupList::Items items;
for (auto& e : meta_entries) {
char buf[256];
std::snprintf(buf, sizeof(buf), "Type: %s Storage: %s [%016lX][v%u]", ncm::GetMetaTypeStr(e.meta_type), ncm::GetStorageIdStr(e.storageID), e.application_id, e.version);
items.emplace_back(buf);
}
App::Push<PopupList>(
"Entries"_i18n, items, [this, meta_entries](auto op_index){
#if 0
if (op_index) {
const auto& e = meta_entries[*op_index];
}
#endif
}
);
});
options->Add<SidebarEntryCallback>("Dump"_i18n, [this](){
auto options = std::make_unique<Sidebar>("Select content to dump"_i18n, Sidebar::Side::RIGHT);
options->Add<SidebarEntryCallback>("Export NSP"_i18n, [this](){
auto options = std::make_unique<Sidebar>("Select content to export"_i18n, Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(std::move(options)));
options->Add<SidebarEntryCallback>("Dump All"_i18n, [this](){
options->Add<SidebarEntryCallback>("Export All"_i18n, [this](){
DumpGames(title::ContentFlag_All);
}, true);
options->Add<SidebarEntryCallback>("Dump Application"_i18n, [this](){
options->Add<SidebarEntryCallback>("Export Application"_i18n, [this](){
DumpGames(title::ContentFlag_Application);
}, true);
options->Add<SidebarEntryCallback>("Dump Patch"_i18n, [this](){
options->Add<SidebarEntryCallback>("Export Patch"_i18n, [this](){
DumpGames(title::ContentFlag_Patch);
}, true);
options->Add<SidebarEntryCallback>("Dump AddOnContent"_i18n, [this](){
options->Add<SidebarEntryCallback>("Export AddOnContent"_i18n, [this](){
DumpGames(title::ContentFlag_AddOnContent);
}, true);
options->Add<SidebarEntryCallback>("Dump DataPatch"_i18n, [this](){
options->Add<SidebarEntryCallback>("Export DataPatch"_i18n, [this](){
DumpGames(title::ContentFlag_DataPatch);
}, true);
}, true);
options->Add<SidebarEntryCallback>("Dump options"_i18n, [this](){
options->Add<SidebarEntryCallback>("Export options"_i18n, [this](){
App::DisplayDumpOptions(false);
});
@@ -666,7 +419,7 @@ Menu::Menu(u32 flags) : grid::Menu{"Games"_i18n, flags} {
OnLayoutChange();
nsInitialize();
ns::Initialize();
es::Initialize();
title::Init();
}
@@ -675,7 +428,7 @@ Menu::~Menu() {
title::Exit();
FreeEntries();
nsExit();
ns::Exit();
es::Exit();
}
@@ -893,21 +646,14 @@ void Menu::DeleteGames() {
void Menu::DumpGames(u32 flags) {
auto targets = GetSelectedEntries();
ClearSelection();
std::vector<NspEntry> nsp_entries;
for (auto& e : targets) {
BuildNspEntries(e, flags, nsp_entries);
}
std::vector<fs::FsPath> paths;
for (auto& e : nsp_entries) {
paths.emplace_back(fs::AppendPath("/dumps/NSP", e.path));
}
auto source = std::make_shared<NspSource>(nsp_entries);
dump::Dump(source, paths, [this](Result rc){
ClearSelection();
});
DumpNsp(nsp_entries);
}
void Menu::CreateSaves(AccountUid uid) {
@@ -941,4 +687,238 @@ void Menu::CreateSaves(AccountUid uid) {
});
}
Result GetMetaEntries(const Entry& e, title::MetaEntries& out, u32 flags) {
return title::GetMetaEntries(e.app_id, out, flags);
}
Result GetNcmMetaFromMetaStatus(const NsApplicationContentMetaStatus& status, NcmMetaData& out) {
out.cs = &title::GetNcmCs(status.storageID);
out.db = &title::GetNcmDb(status.storageID);
out.app_id = ncm::GetAppId(status.meta_type, status.application_id);
auto id_min = status.application_id;
auto id_max = status.application_id;
// workaround N bug where they don't check the full range in the ID filter.
// https://github.com/Atmosphere-NX/Atmosphere/blob/1d3f3c6e56b994b544fc8cd330c400205d166159/libraries/libstratosphere/source/ncm/ncm_on_memory_content_meta_database_impl.cpp#L22
if (status.storageID == NcmStorageId_None || status.storageID == NcmStorageId_GameCard) {
id_min -= 1;
id_max += 1;
}
s32 meta_total;
s32 meta_entries_written;
R_TRY(ncmContentMetaDatabaseList(out.db, &meta_total, &meta_entries_written, &out.key, 1, (NcmContentMetaType)status.meta_type, out.app_id, id_min, id_max, NcmContentInstallType_Full));
// log_write("ncmContentMetaDatabaseList(): AppId: %016lX Id: %016lX total: %d written: %d storageID: %u key.id %016lX\n", out.app_id, status.application_id, meta_total, meta_entries_written, status.storageID, out.key.id);
R_UNLESS(meta_total == 1, Result_GameMultipleKeysFound);
R_UNLESS(meta_entries_written == 1, Result_GameMultipleKeysFound);
R_SUCCEED();
}
// deletes the array of entries (remove nca, remove ncm db, remove ns app records).
void DeleteMetaEntries(u64 app_id, int image, const std::string& name, const title::MetaEntries& entries) {
App::Push<ProgressBox>(image, "Delete"_i18n, name, [app_id, entries](ProgressBox* pbox) -> Result {
R_TRY(ns::Initialize());
ON_SCOPE_EXIT(ns::Exit());
// fetch current app records.
std::vector<ncm::ContentStorageRecord> records;
R_TRY(ns::GetApplicationRecords(app_id, records));
// on exit, delete old record list and push the new one.
ON_SCOPE_EXIT(
R_TRY(ns::DeleteApplicationRecord(app_id));
return ns::PushApplicationRecord(app_id, records.data(), records.size());
)
// on exit, set the new lowest version.
ON_SCOPE_EXIT(
ns::SetLowestLaunchVersion(app_id, records);
)
for (u32 i = 0; i < std::size(entries); i++) {
const auto& status = entries[i];
// check if the user wants to exit, only in-between each successful delete.
R_TRY(pbox->ShouldExitResult());
char transfer_str[33];
std::snprintf(transfer_str, sizeof(transfer_str), "%016lX", status.application_id);
pbox->NewTransfer(transfer_str).UpdateTransfer(i, std::size(entries));
NcmMetaData meta;
R_TRY(GetNcmMetaFromMetaStatus(status, meta));
// only delete form non read-only storage.
if (status.storageID == NcmStorageId_BuiltInUser || status.storageID == NcmStorageId_SdCard) {
R_TRY(ncm::DeleteKey(meta.cs, meta.db, &meta.key));
}
// find and remove record.
std::erase_if(records, [&meta](auto& e){
return meta.key.id == e.key.id;
});
}
R_SUCCEED();
}, [](Result rc){
App::PushErrorBox(rc, "Failed to delete meta entry");
});
}
auto BuildNspPath(const Entry& e, const NsApplicationContentMetaStatus& status) -> fs::FsPath {
fs::FsPath name_buf = e.GetName();
title::utilsReplaceIllegalCharacters(name_buf, true);
char version[sizeof(NacpStruct::display_version) + 1]{};
if (status.meta_type == NcmContentMetaType_Patch) {
u64 program_id;
fs::FsPath path;
if (R_SUCCEEDED(title::GetControlPathFromStatus(status, &program_id, &path))) {
char display_version[0x10];
if (R_SUCCEEDED(nca::ParseControl(path, program_id, display_version, sizeof(display_version), nullptr, offsetof(NacpStruct, display_version)))) {
std::snprintf(version, sizeof(version), "%s ", display_version);
}
}
}
fs::FsPath path;
if (App::GetApp()->m_dump_app_folder.Get()) {
std::snprintf(path, sizeof(path), "%s/%s %s[%016lX][v%u][%s].nsp", name_buf.s, name_buf.s, version, status.application_id, status.version, ncm::GetMetaTypeShortStr(status.meta_type));
} else {
std::snprintf(path, sizeof(path), "%s %s[%016lX][v%u][%s].nsp", name_buf.s, version, status.application_id, status.version, ncm::GetMetaTypeShortStr(status.meta_type));
}
return path;
}
Result BuildContentEntry(const NsApplicationContentMetaStatus& status, ContentInfoEntry& out) {
NcmMetaData meta;
R_TRY(GetNcmMetaFromMetaStatus(status, meta));
std::vector<NcmContentInfo> infos;
R_TRY(ncm::GetContentInfos(meta.db, &meta.key, infos));
std::vector<NcmContentInfo> cnmt_infos;
for (const auto& info : infos) {
// check if we need to fetch tickets.
NcmRightsId ncm_rights_id;
R_TRY(ncmContentStorageGetRightsIdFromContentId(meta.cs, std::addressof(ncm_rights_id), std::addressof(info.content_id), FsContentAttributes_All));
if (es::IsRightsIdValid(ncm_rights_id.rights_id)) {
const auto it = std::ranges::find_if(out.ncm_rights_id, [&ncm_rights_id](auto& e){
return !std::memcmp(&e, &ncm_rights_id, sizeof(ncm_rights_id));
});
if (it == out.ncm_rights_id.end()) {
out.ncm_rights_id.emplace_back(ncm_rights_id);
}
}
if (info.content_type == NcmContentType_Meta) {
cnmt_infos.emplace_back(info);
} else {
out.content_infos.emplace_back(info);
}
}
// append cnmt at the end of the list, following StandardNSP spec.
out.content_infos.insert_range(out.content_infos.end(), cnmt_infos);
out.status = status;
R_SUCCEED();
}
Result BuildNspEntry(const Entry& e, const ContentInfoEntry& info, const keys::Keys& keys, NspEntry& out) {
out.application_name = e.GetName();
out.path = BuildNspPath(e, info.status);
s64 offset{};
for (auto& e : info.content_infos) {
char nca_name[64];
std::snprintf(nca_name, sizeof(nca_name), "%s%s", hexIdToStr(e.content_id).str, e.content_type == NcmContentType_Meta ? ".cnmt.nca" : ".nca");
u64 size;
ncmContentInfoSizeToU64(std::addressof(e), std::addressof(size));
out.collections.emplace_back(nca_name, offset, size);
offset += size;
}
for (auto& ncm_rights_id : info.ncm_rights_id) {
const auto rights_id = ncm_rights_id.rights_id;
const auto key_gen = ncm_rights_id.key_generation;
TikEntry entry{rights_id, key_gen};
log_write("rights id is valid, fetching common ticket and cert\n");
u64 tik_size;
u64 cert_size;
R_TRY(es::GetCommonTicketAndCertificateSize(&tik_size, &cert_size, &rights_id));
log_write("got tik_size: %zu cert_size: %zu\n", tik_size, cert_size);
entry.tik_data.resize(tik_size);
entry.cert_data.resize(cert_size);
R_TRY(es::GetCommonTicketAndCertificateData(&tik_size, &cert_size, entry.tik_data.data(), entry.tik_data.size(), entry.cert_data.data(), entry.cert_data.size(), &rights_id));
log_write("got tik_data: %zu cert_data: %zu\n", tik_size, cert_size);
// patch fake ticket / convert personalised to common if needed.
R_TRY(es::PatchTicket(entry.tik_data, entry.cert_data, key_gen, keys, App::GetApp()->m_dump_convert_to_common_ticket.Get()));
char tik_name[0x200];
std::snprintf(tik_name, sizeof(tik_name), "%s%s", hexIdToStr(rights_id).str, ".tik");
char cert_name[0x200];
std::snprintf(cert_name, sizeof(cert_name), "%s%s", hexIdToStr(rights_id).str, ".cert");
out.collections.emplace_back(tik_name, offset, entry.tik_data.size());
offset += entry.tik_data.size();
out.collections.emplace_back(cert_name, offset, entry.cert_data.size());
offset += entry.cert_data.size();
out.tickets.emplace_back(entry);
}
out.nsp_data = yati::container::Nsp::Build(out.collections, out.nsp_size);
out.cs = title::GetNcmCs(info.status.storageID);
R_SUCCEED();
}
Result BuildNspEntries(Entry& e, const title::MetaEntries& meta_entries, std::vector<NspEntry>& out) {
LoadControlEntry(e);
keys::Keys keys;
R_TRY(keys::parse_keys(keys, true));
for (const auto& status : meta_entries) {
ContentInfoEntry info;
R_TRY(BuildContentEntry(status, info));
NspEntry nsp;
R_TRY(BuildNspEntry(e, info, keys, nsp));
out.emplace_back(nsp).icon = e.image;
}
R_UNLESS(!out.empty(), Result_GameNoNspEntriesBuilt);
R_SUCCEED();
}
Result BuildNspEntries(Entry& e, u32 flags, std::vector<NspEntry>& out) {
title::MetaEntries meta_entries;
R_TRY(GetMetaEntries(e, meta_entries, flags));
return BuildNspEntries(e, meta_entries, out);
}
void DumpNsp(const std::vector<NspEntry>& entries) {
std::vector<fs::FsPath> paths;
for (auto& e : entries) {
paths.emplace_back(fs::AppendPath("/dumps/NSP", e.path));
}
auto source = std::make_shared<NspSource>(entries);
dump::Dump(source, paths);
}
} // namespace sphaira::ui::menu::game

View File

@@ -0,0 +1,375 @@
#include "ui/menus/game_meta_menu.hpp"
#include "ui/menus/game_nca_menu.hpp"
#include "ui/nvg_util.hpp"
#include "ui/sidebar.hpp"
#include "ui/option_box.hpp"
#include "yati/nx/ns.hpp"
#include "yati/nx/nca.hpp"
#include "yati/nx/ncm.hpp"
#include "yati/nx/es.hpp"
#include "title_info.hpp"
#include "app.hpp"
#include "defines.hpp"
#include "log.hpp"
#include "i18n.hpp"
#include "image.hpp"
#include <cstring>
#include <algorithm>
namespace sphaira::ui::menu::game::meta {
namespace {
#define SYSVER_MAJOR(x) (((x) >> 26) & 0x003F)
#define SYSVER_MINOR(x) (((x) >> 20) & 0x003F)
#define SYSVER_MICRO(x) (((x) >> 16) & 0x003F)
#define SYSVER_RELSTEP(x) (((x) >> 00) & 0xFFFF)
constexpr const char* TICKET_STR[] = {
[TicketType_None] = "None",
[TicketType_Common] = "Common",
[TicketType_Personalised] = "Personalised",
[TicketType_Missing] = "Missing",
};
constexpr u64 MINI_NACP_OFFSET = offsetof(NacpStruct, display_version);
Result GetMiniNacpFromContentId(NcmContentStorage* cs, const NcmContentMetaKey& key, const NcmContentId& id, MiniNacp& out) {
u64 program_id;
fs::FsPath path;
R_TRY(ncm::GetControlPathFromContentId(cs, key, id, &program_id, &path));
return nca::ParseControl(path, program_id, &out, sizeof(out), nullptr, MINI_NACP_OFFSET);
}
} // namespace
Menu::Menu(Entry& entry) : MenuBase{entry.GetName(), MenuFlag_None}, m_entry{entry} {
this->SetActions(
std::make_pair(Button::L2, Action{"Select"_i18n, [this](){
// if both set, select all.
if (App::GetApp()->m_controller.GotHeld(Button::R2)) {
const auto set = m_selected_count != m_entries.size();
for (u32 i = 0; i < m_entries.size(); i++) {
auto& e = GetEntry(i);
if (e.selected != set) {
e.selected = set;
if (set) {
m_selected_count++;
} else {
m_selected_count--;
}
}
}
} else {
GetEntry().selected ^= 1;
if (GetEntry().selected) {
m_selected_count++;
} else {
m_selected_count--;
}
}
}}),
std::make_pair(Button::A, Action{"View Content"_i18n, [this](){
App::Push<meta_nca::Menu>(m_entry, GetEntry());
}}),
std::make_pair(Button::B, Action{"Back"_i18n, [this](){
SetPop();
}}),
std::make_pair(Button::X, Action{"Options"_i18n, [this](){
auto options = std::make_unique<Sidebar>("Content Options"_i18n, Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(std::move(options)));
if (!m_entries.empty()) {
options->Add<SidebarEntryCallback>("Export NSP"_i18n, [this](){
DumpGames();
});
options->Add<SidebarEntryCallback>("Export options"_i18n, [this](){
App::DisplayDumpOptions(false);
});
options->Add<SidebarEntryCallback>("Delete"_i18n, [this](){
App::Push<OptionBox>(
"Are you sure you want to delete the selected entries?"_i18n,
"Back"_i18n, "Delete"_i18n, 0, [this](auto op_index){
if (op_index && *op_index) {
DeleteGames();
}
}
);
}, true);
if (ncm::HasRequiredSystemVersion(GetEntry().status.meta_type)) {
options->Add<SidebarEntryCallback>("Reset required system version"_i18n, [this](){
App::Push<OptionBox>(
"Are you sure you want to reset required system version?"_i18n,
"Back"_i18n, "Reset"_i18n, 0, [this](auto op_index){
if (op_index && *op_index) {
const auto rc = ResetRequiredSystemVersion(GetEntry());
App::PushErrorBox(rc, "Failed to reset required system version"_i18n);
}
}
);
});
}
}
}})
);
// todo: maybe width is broken here?
const Vec4 v{485, GetY() + 1.f + 42.f, 720, 60};
m_list = std::make_unique<List>(1, 8, m_pos, v);
es::Initialize();
ON_SCOPE_EXIT(es::Exit());
// pre-fetch all ticket rights ids.
es::GetCommonTickets(m_common_tickets);
es::GetPersonalisedTickets(m_personalised_tickets);
char subtitle[128];
std::snprintf(subtitle, sizeof(subtitle), "by %s", entry.GetAuthor());
SetTitleSubHeading(subtitle);
Scan();
}
Menu::~Menu() {
}
void Menu::Update(Controller* controller, TouchInfo* touch) {
if (m_dirty) {
m_dirty = false;
Scan();
}
MenuBase::Update(controller, touch);
m_list->OnUpdate(controller, touch, m_index, m_entries.size(), [this](bool touch, auto i) {
if (touch && m_index == i) {
FireAction(Button::A);
} else {
App::PlaySoundEffect(SoundEffect_Focus);
SetIndex(i);
}
});
}
void Menu::Draw(NVGcontext* vg, Theme* theme) {
MenuBase::Draw(vg, theme);
// draw left-side grid background.
gfx::drawRect(vg, 30, 90, 375, 555, theme->GetColour(ThemeEntryID_GRID));
// draw the game icon (maybe remove this or reduce it's size).
const auto& e = m_entries[m_index];
gfx::drawImage(vg, 90, 130, 256, 256, m_entry.image ? m_entry.image : App::GetDefaultImage());
nvgSave(vg);
nvgIntersectScissor(vg, 50, 90, 325, 555);
char req_vers_buf[128];
const auto ver = e.content_meta.extened.application.required_system_version;
switch (e.status.meta_type) {
case NcmContentMetaType_Application: std::snprintf(req_vers_buf, sizeof(req_vers_buf), "Required System Version: %u.%u.%u", SYSVER_MAJOR(ver), SYSVER_MINOR(ver), SYSVER_MICRO(ver)); break;
case NcmContentMetaType_Patch: std::snprintf(req_vers_buf, sizeof(req_vers_buf), "Required System Version: %u.%u.%u", SYSVER_MAJOR(ver), SYSVER_MINOR(ver), SYSVER_MICRO(ver)); break;
case NcmContentMetaType_AddOnContent: std::snprintf(req_vers_buf, sizeof(req_vers_buf), "Required Application Version: v%u", ver >> 16); break;
}
if (e.missing_count) {
gfx::drawTextArgs(vg, 50, 415, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Content Count: %u (%u missing)", e.content_meta.header.content_count, e.missing_count);
} else {
gfx::drawTextArgs(vg, 50, 415, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Content Count: %u", e.content_meta.header.content_count);
}
gfx::drawTextArgs(vg, 50, 455, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Ticket: %s", TICKET_STR[e.ticket_type]);
gfx::drawTextArgs(vg, 50, 495, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Key Generation: %u (%s)", e.key_gen, nca::GetKeyGenStr(e.key_gen));
gfx::drawTextArgs(vg, 50, 535, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "%s", req_vers_buf);
if (e.status.meta_type == NcmContentMetaType_Application || e.status.meta_type == NcmContentMetaType_Patch) {
gfx::drawTextArgs(vg, 50, 575, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Display Version: %s", e.nacp.display_version);
}
nvgRestore(vg);
// exit early if we have no entries (maybe?)
if (m_entries.empty()) {
// todo: center this.
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Empty..."_i18n.c_str());
return;
}
constexpr float text_xoffset{15.f};
m_list->Draw(vg, theme, m_entries.size(), [this](auto* vg, auto* theme, auto& v, auto i) {
const auto& [x, y, w, h] = v;
auto& e = m_entries[i];
auto text_id = ThemeEntryID_TEXT;
if (m_index == i) {
text_id = ThemeEntryID_TEXT_SELECTED;
gfx::drawRectOutline(vg, theme, 4.f, v);
} else {
if (i != m_entries.size() - 1) {
gfx::drawRect(vg, x, y + h, w, 1.f, theme->GetColour(ThemeEntryID_LINE_SEPARATOR));
}
}
gfx::drawTextArgs(vg, x + text_xoffset, y + (h / 2.f), 20.f, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(text_id), "%s", ncm::GetReadableMetaTypeStr(e.status.meta_type));
gfx::drawTextArgs(vg, x + text_xoffset + 150, y + (h / 2.f), 20.f, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(text_id), "%016lX", e.status.application_id);
gfx::drawTextArgs(vg, x + text_xoffset + 400, y + (h / 2.f), 20.f, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(text_id), "v%u (%u)", e.status.version >> 16, e.status.version);
if (!e.checked) {
e.checked = true;
GetNcmSizeOfMetaStatus(e);
}
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) + 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_TOP, theme->GetColour(text_id), "%s", ncm::GetReadableStorageIdStr(e.status.storageID));
if ((double)e.size / 1024.0 / 1024.0 <= 0.009) {
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) - 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_BOTTOM, theme->GetColour(text_id), "%.2f KiB", (double)e.size / 1024.0);
} else if ((double)e.size / 1024.0 / 1024.0 / 1024.0 <= 0.009) {
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) - 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_BOTTOM, theme->GetColour(text_id), "%.2f MiB", (double)e.size / 1024.0 / 1024.0);
} else {
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) - 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_BOTTOM, theme->GetColour(text_id), "%.2f GiB", (double)e.size / 1024.0 / 1024.0 / 1024.0);
}
if (e.selected) {
gfx::drawText(vg, x + text_xoffset - 80 / 2, y + (h / 2.f) - (24.f / 2), 24.f, "\uE14B", nullptr, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT_SELECTED));
}
});
}
void Menu::SetIndex(s64 index) {
m_index = index;
if (!m_index) {
m_list->SetYoff(0);
}
UpdateSubheading();
}
void Menu::Scan() {
m_dirty = false;
m_index = 0;
m_selected_count = 0;
m_entries.clear();
// todo: log errors here.
title::MetaEntries meta_entries;
if (R_SUCCEEDED(title::GetMetaEntries(m_entry.app_id, meta_entries))) {
m_entries.reserve(meta_entries.size());
for (const auto& e : meta_entries) {
m_entries.emplace_back(e);
}
}
SetIndex(0);
}
void Menu::UpdateSubheading() {
const auto index = m_entries.empty() ? 0 : m_index + 1;
this->SetSubHeading(std::to_string(index) + " / " + std::to_string(m_entries.size()));
}
Result Menu::GetNcmSizeOfMetaStatus(MetaEntry& entry) const {
entry.size = 0;
entry.missing_count = 0;
NcmMetaData meta;
R_TRY(GetNcmMetaFromMetaStatus(entry.status, meta));
// get the content meta header.
R_TRY(ncm::GetContentMeta(meta.db, &meta.key, entry.content_meta));
// fetch all the content infos.
std::vector<NcmContentInfo> infos;
R_TRY(ncm::GetContentInfos(meta.db, &meta.key, entry.content_meta.header, infos));
// calculate the size and fetch the rights id (if possible).
NcmRightsId rights_id{};
bool has_nacp{};
for (const auto& info : infos) {
u64 size;
ncmContentInfoSizeToU64(&info, &size);
entry.size += size;
// try and load nacp.
if (!has_nacp && info.content_type == NcmContentType_Control) {
// try and load from nca.
if (R_SUCCEEDED(GetMiniNacpFromContentId(meta.cs, meta.key, info.content_id, entry.nacp))) {
has_nacp = true;
} else {
// fallback to ns
std::vector<u8> buf(sizeof(NsApplicationControlData));
u64 actual_size;
if (R_SUCCEEDED(nsGetApplicationControlData(NsApplicationControlSource_Storage, meta.app_id, (NsApplicationControlData*)buf.data(), buf.size(), &actual_size))) {
has_nacp = true;
std::memcpy(&entry.nacp, buf.data() + MINI_NACP_OFFSET, sizeof(entry.nacp));
}
}
}
// ensure that we have the content id.
bool has;
R_TRY(ncmContentMetaDatabaseHasContent(meta.db, &has, &meta.key, &info.content_id));
if (!has) {
entry.missing_count++;
}
if (!es::IsRightsIdValid(rights_id.rights_id)) {
// todo: check if this gets the key gen if standard crypto is used.
if (R_SUCCEEDED(ncmContentStorageGetRightsIdFromContentId(meta.cs, &rights_id, &info.content_id, FsContentAttributes_All))) {
entry.key_gen = std::max(entry.key_gen, rights_id.key_generation);
}
}
}
// if we found a valid rights id, find the ticket type.
if (es::IsRightsIdValid(rights_id.rights_id)) {
if (es::IsRightsIdFound(rights_id.rights_id, m_common_tickets)) {
entry.ticket_type = TicketType_Common;
} else if (es::IsRightsIdFound(rights_id.rights_id, m_personalised_tickets)) {
entry.ticket_type = TicketType_Personalised;
} else {
entry.ticket_type = TicketType_Missing;
}
} else {
entry.ticket_type = TicketType_None;
}
R_SUCCEED();
}
void Menu::DumpGames() {
const auto entries = GetSelectedEntries();
App::PopToMenu();
std::vector<NspEntry> nsps;
BuildNspEntries(m_entry, entries, nsps);
DumpNsp(nsps);
}
void Menu::DeleteGames() {
m_dirty = true;
const auto entries = GetSelectedEntries();
App::PopToMenu();
DeleteMetaEntries(m_entry.app_id, m_entry.image, m_entry.GetName(), entries);
}
Result Menu::ResetRequiredSystemVersion(MetaEntry& entry) const {
entry.checked = false;
NcmMetaData meta;
R_TRY(GetNcmMetaFromMetaStatus(entry.status, meta));
return ncm::SetRequiredSystemVersion(meta.db, &meta.key, 0);
}
} // namespace sphaira::ui::menu::game::meta

View File

@@ -0,0 +1,372 @@
#include "ui/menus/game_nca_menu.hpp"
#include "ui/nvg_util.hpp"
#include "ui/sidebar.hpp"
#include "ui/option_box.hpp"
#include "ui/progress_box.hpp"
#include "yati/nx/nca.hpp"
#include "yati/nx/ncm.hpp"
#include "yati/nx/keys.hpp"
#include "yati/nx/crypto.hpp"
#include "title_info.hpp"
#include "app.hpp"
#include "dumper.hpp"
#include "defines.hpp"
#include "log.hpp"
#include "i18n.hpp"
#include "image.hpp"
#include "hasher.hpp"
#include <cstring>
#include <algorithm>
namespace sphaira::ui::menu::game::meta_nca {
namespace {
struct HashStr {
char str[0x21];
};
HashStr hexIdToStr(auto id) {
HashStr str{};
const auto id_lower = std::byteswap(*(u64*)id.c);
const auto id_upper = std::byteswap(*(u64*)(id.c + 0x8));
std::snprintf(str.str, 0x21, "%016lx%016lx", id_lower, id_upper);
return str;
}
struct NcaHashSource final : hash::BaseSource {
NcaHashSource(NcmContentStorage* cs, const NcaEntry& entry) : m_cs{cs}, m_entry{entry} {
}
Result Size(s64* out) override {
*out = m_entry.size;
R_SUCCEED();
}
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override {
const auto rc = ncmContentStorageReadContentIdFile(m_cs, buf, size, &m_entry.content_id, off);
if (R_SUCCEEDED(rc)) {
*bytes_read = size;
}
return rc;
}
private:
NcmContentStorage* const m_cs;
const NcaEntry& m_entry{};
};
struct NcaSource final : dump::BaseSource {
NcaSource(NcmContentStorage* cs, int icon, const std::vector<NcaEntry>& entries) : m_cs{cs}, m_icon{icon}, m_entries{entries} {
m_is_file_based_emummc = App::IsFileBaseEmummc();
}
Result Read(const std::string& path, void* buf, s64 off, s64 size, u64* bytes_read) override {
const auto it = std::ranges::find_if(m_entries, [&path](auto& e){
return path.find(hexIdToStr(e.content_id).str) != path.npos;
});
R_UNLESS(it != m_entries.end(), Result_GameBadReadForDump);
const auto rc = ncmContentStorageReadContentIdFile(m_cs, buf, size, &it->content_id, off);
if (R_SUCCEEDED(rc)) {
*bytes_read = size;
}
if (m_is_file_based_emummc) {
svcSleepThread(2e+6); // 2ms
}
return rc;
}
auto GetName(const std::string& path) const -> std::string {
const auto it = std::ranges::find_if(m_entries, [&path](auto& e){
return path.find(hexIdToStr(e.content_id).str) != path.npos;
});
if (it != m_entries.end()) {
return hexIdToStr(it->content_id).str;
}
return {};
}
auto GetSize(const std::string& path) const -> s64 {
const auto it = std::ranges::find_if(m_entries, [&path](auto& e){
return path.find(hexIdToStr(e.content_id).str) != path.npos;
});
if (it != m_entries.end()) {
return it->size;
}
return 0;
}
auto GetIcon(const std::string& path) const -> int override {
return m_icon ? m_icon : App::GetDefaultImage();
}
private:
NcmContentStorage* const m_cs;
const int m_icon;
std::vector<NcaEntry> m_entries{};
bool m_is_file_based_emummc{};
};
} // namespace
Menu::Menu(Entry& entry, const meta::MetaEntry& meta_entry)
: MenuBase{entry.GetName(), MenuFlag_None}
, m_entry{entry}
, m_meta_entry{meta_entry} {
this->SetActions(
std::make_pair(Button::L2, Action{"Select"_i18n, [this](){
// if both set, select all.
if (App::GetApp()->m_controller.GotHeld(Button::R2)) {
const auto set = m_selected_count != m_entries.size();
for (u32 i = 0; i < m_entries.size(); i++) {
auto& e = GetEntry(i);
if (e.selected != set) {
e.selected = set;
if (set) {
m_selected_count++;
} else {
m_selected_count--;
}
}
}
} else {
GetEntry().selected ^= 1;
if (GetEntry().selected) {
m_selected_count++;
} else {
m_selected_count--;
}
}
}}),
std::make_pair(Button::B, Action{"Back"_i18n, [this](){
SetPop();
}}),
std::make_pair(Button::X, Action{"Options"_i18n, [this](){
auto options = std::make_unique<Sidebar>("NCA Options"_i18n, Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(std::move(options)));
if (!m_entries.empty()) {
options->Add<SidebarEntryCallback>("Export NCA"_i18n, [this](){
DumpNcas();
});
options->Add<SidebarEntryCallback>("Verify NCA 256 hash"_i18n, [this](){
static std::string hash_out;
hash_out.clear();
App::Push<ProgressBox>(m_entry.image, "Hashing"_i18n, hexIdToStr(GetEntry().content_id).str, [this](auto pbox) -> Result{
auto source = std::make_unique<NcaHashSource>(m_meta.cs, GetEntry());
return hash::Hash(pbox, hash::Type::Sha256, source.get(), hash_out);
}, [this](Result rc){
App::PushErrorBox(rc, "Failed to hash file..."_i18n);
const auto str = hexIdToStr(GetEntry().content_id);
if (R_SUCCEEDED(rc)) {
if (std::strncmp(hash_out.c_str(), str.str, std::strlen(str.str))) {
App::Push<OptionBox>("NCA hash missmatch!"_i18n, "OK"_i18n);
} else {
App::Push<OptionBox>("NCA hash valid."_i18n, "OK"_i18n);
}
}
});
}, "Performs sha256 hash over the NCA to check if it's valid.\n\n"
"NOTE: This only detects if the hash is missmatched, it does not validate if \
the content has been modified at all."_i18n);
options->Add<SidebarEntryCallback>("Verify NCA fixed key"_i18n, [this](){
if (R_FAILED(nca::VerifyFixedKey(GetEntry().header))) {
App::Push<OptionBox>("NCA fixed key is invalid!"_i18n, "OK"_i18n);
} else {
App::Push<OptionBox>("NCA fixed key is valid."_i18n, "OK"_i18n);
}
}, "Performs RSA NCA fixed key verification. "\
"This is a hash over the NCA header. It is used to verify that the header has not been modified. "\
"The header is signed by nintendo, thus it cannot be forged, and is reliable to detect modified NCA headers (such as NSP/XCI converts)."_i18n);
}
}})
);
keys::Keys keys;
parse_keys(keys, false);
if (R_FAILED(GetNcmMetaFromMetaStatus(m_meta_entry.status, m_meta))) {
SetPop();
return;
}
// get the content meta header.
ncm::ContentMeta content_meta;
if (R_FAILED(ncm::GetContentMeta(m_meta.db, &m_meta.key, content_meta))) {
SetPop();
return;
}
// fetch all the content infos.
std::vector<NcmContentInfo> infos;
if (R_FAILED(ncm::GetContentInfos(m_meta.db, &m_meta.key, content_meta.header, infos))) {
SetPop();
return;
}
for (const auto& info : infos) {
NcaEntry entry{};
entry.content_id = info.content_id;
entry.content_type = info.content_type;
ncmContentInfoSizeToU64(&info, &entry.size);
bool has = false;
ncmContentMetaDatabaseHasContent(m_meta.db, &has, &m_meta.key, &info.content_id);
entry.missing = !has;
if (has && R_SUCCEEDED(ncmContentStorageReadContentIdFile(m_meta.cs, &entry.header, sizeof(entry.header), &info.content_id, 0))) {
// decrypt header.
crypto::cryptoAes128Xts(&entry.header, &entry.header, keys.header_key, 0, 0x200, sizeof(entry.header), false);
}
m_entries.emplace_back(entry);
}
// todo: maybe width is broken here?
const Vec4 v{485, GetY() + 1.f + 42.f, 720, 60};
m_list = std::make_unique<List>(1, 8, m_pos, v);
char subtitle[128];
std::snprintf(subtitle, sizeof(subtitle), "by %s", entry.GetAuthor());
SetTitleSubHeading(subtitle);
SetIndex(0);
}
Menu::~Menu() {
}
void Menu::Update(Controller* controller, TouchInfo* touch) {
MenuBase::Update(controller, touch);
m_list->OnUpdate(controller, touch, m_index, m_entries.size(), [this](bool touch, auto i) {
if (touch && m_index == i) {
FireAction(Button::A);
} else {
App::PlaySoundEffect(SoundEffect_Focus);
SetIndex(i);
}
});
}
void Menu::Draw(NVGcontext* vg, Theme* theme) {
MenuBase::Draw(vg, theme);
// draw left-side grid background.
gfx::drawRect(vg, 30, 90, 375, 555, theme->GetColour(ThemeEntryID_GRID));
// draw the game icon (maybe remove this or reduce it's size).
const auto& e = m_entries[m_index];
gfx::drawImage(vg, 90, 130, 256, 256, m_entry.image ? m_entry.image : App::GetDefaultImage());
if (e.header.magic != NCA3_MAGIC) {
gfx::drawTextArgs(vg, 50, 415, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Failed to decrypt NCA");
} else {
nvgSave(vg);
nvgIntersectScissor(vg, 50, 90, 325, 555);
gfx::drawTextArgs(vg, 50, 415, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Application Type: %s", ncm::GetReadableMetaTypeStr(m_meta_entry.status.meta_type));
gfx::drawTextArgs(vg, 50, 455, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Content Type: %s", nca::GetContentTypeStr(e.header.content_type));
gfx::drawTextArgs(vg, 50, 495, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Distribution Type: %s", nca::GetDistributionTypeStr(e.header.distribution_type));
gfx::drawTextArgs(vg, 50, 535, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Program ID: %016lX", e.header.program_id);
gfx::drawTextArgs(vg, 50, 575, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Key Generation: %u (%s)", e.header.GetKeyGeneration(), nca::GetKeyGenStr(e.header.GetKeyGeneration()));
gfx::drawTextArgs(vg, 50, 615, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "SDK Version: %u.%u.%u.%u", e.header.sdk_major, e.header.sdk_minor, e.header.sdk_micro, e.header.sdk_revision);
nvgRestore(vg);
}
// exit early if we have no entries (maybe?)
if (m_entries.empty()) {
// todo: center this.
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Empty..."_i18n.c_str());
return;
}
constexpr float text_xoffset{15.f};
m_list->Draw(vg, theme, m_entries.size(), [this](auto* vg, auto* theme, auto& v, auto i) {
const auto& [x, y, w, h] = v;
auto& e = m_entries[i];
auto text_id = ThemeEntryID_TEXT;
if (m_index == i) {
text_id = ThemeEntryID_TEXT_SELECTED;
gfx::drawRectOutline(vg, theme, 4.f, v);
} else {
if (i != m_entries.size() - 1) {
gfx::drawRect(vg, x, y + h, w, 1.f, theme->GetColour(ThemeEntryID_LINE_SEPARATOR));
}
}
gfx::drawTextArgs(vg, x + text_xoffset, y + (h / 2.f), 20.f, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(text_id), "%s", ncm::GetContentTypeStr(e.content_type));
gfx::drawTextArgs(vg, x + text_xoffset + 185, y + (h / 2.f), 20.f, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(text_id), "%s", hexIdToStr(e.content_id).str);
if ((double)e.size / 1024.0 / 1024.0 <= 0.009) {
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f), 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_MIDDLE, theme->GetColour(text_id), "%.2f KiB", (double)e.size / 1024.0);
} else if ((double)e.size / 1024.0 / 1024.0 / 1024.0 <= 0.009) {
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f), 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_MIDDLE, theme->GetColour(text_id), "%.2f MiB", (double)e.size / 1024.0 / 1024.0);
} else {
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f), 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_MIDDLE, theme->GetColour(text_id), "%.2f GiB", (double)e.size / 1024.0 / 1024.0 / 1024.0);
}
if (e.missing) {
gfx::drawText(vg, x + text_xoffset - 80 / 2, y + (h / 2.f) - (24.f / 2), 24.f, "\uE140", nullptr, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_ERROR));
} else if (e.selected) {
gfx::drawText(vg, x + text_xoffset - 80 / 2, y + (h / 2.f) - (24.f / 2), 24.f, "\uE14B", nullptr, NVG_ALIGN_CENTER | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT_SELECTED));
}
});
}
void Menu::SetIndex(s64 index) {
m_index = index;
if (!m_index) {
m_list->SetYoff(0);
}
UpdateSubheading();
}
void Menu::UpdateSubheading() {
const auto index = m_entries.empty() ? 0 : m_index + 1;
this->SetSubHeading(std::to_string(index) + " / " + std::to_string(m_entries.size()));
}
void Menu::DumpNcas() {
const auto entries = GetSelectedEntries();
App::PopToMenu();
fs::FsPath name_buf = m_entry.GetName();
title::utilsReplaceIllegalCharacters(name_buf, true);
char version[sizeof(NacpStruct::display_version) + 1]{};
if (m_meta_entry.status.meta_type == NcmContentMetaType_Patch) {
std::snprintf(version, sizeof(version), "%s ", m_meta_entry.nacp.display_version);
}
std::vector<fs::FsPath> paths;
for (auto& e : entries) {
char nca_name[64];
std::snprintf(nca_name, sizeof(nca_name), "%s%s", hexIdToStr(e.content_id).str, e.content_type == NcmContentType_Meta ? ".cnmt.nca" : ".nca");
fs::FsPath path;
std::snprintf(path, sizeof(path), "/dumps/NCA/%s %s[%016lX][v%u][%s]/%s", name_buf.s, version, m_meta_entry.status.application_id, m_meta_entry.status.version, ncm::GetMetaTypeShortStr(m_meta_entry.status.meta_type), nca_name);
paths.emplace_back(path);
}
auto source = std::make_shared<NcaSource>(m_meta.cs, m_entry.image, entries);
dump::Dump(source, paths, [](Result){}, dump::DumpLocationFlag_All &~ dump::DumpLocationFlag_UsbS2S);
}
} // namespace sphaira::ui::menu::game::meta_nca

View File

@@ -47,7 +47,7 @@ enum DumpFileFlag {
const char *g_option_list[] = {
"Install",
"Dump",
"Export",
"Exit",
};
@@ -401,13 +401,13 @@ Menu::Menu(u32 flags) : MenuBase{"GameCard"_i18n, flags} {
}, true);
};
add("Dump All"_i18n, DumpFileFlag_All);
add("Dump All Bins"_i18n, DumpFileFlag_AllBin);
add("Dump XCI"_i18n, DumpFileFlag_XCI);
add("Dump Card ID Set"_i18n, DumpFileFlag_Set);
add("Dump Card UID"_i18n, DumpFileFlag_UID);
add("Dump Certificate"_i18n, DumpFileFlag_Cert);
add("Dump Initial Data"_i18n, DumpFileFlag_Initial);
add("Export All"_i18n, DumpFileFlag_All);
add("Export All Bins"_i18n, DumpFileFlag_AllBin);
add("Export XCI"_i18n, DumpFileFlag_XCI);
add("Export Card ID Set"_i18n, DumpFileFlag_Set);
add("Export Card UID"_i18n, DumpFileFlag_UID);
add("Export Certificate"_i18n, DumpFileFlag_Cert);
add("Export Initial Data"_i18n, DumpFileFlag_Initial);
}
}
}}),
@@ -422,7 +422,7 @@ Menu::Menu(u32 flags) : MenuBase{"GameCard"_i18n, flags} {
App::DisplayInstallOptions(false);
});
options->Add<SidebarEntryCallback>("Dump options"_i18n, [this](){
options->Add<SidebarEntryCallback>("Export options"_i18n, [this](){
App::DisplayDumpOptions(false);
});
}})

View File

@@ -206,8 +206,6 @@ void Menu::Update(Controller* controller, TouchInfo* touch) {
void Menu::Draw(NVGcontext* vg, Theme* theme) {
MenuBase::Draw(vg, theme);
const auto& text_col = theme->GetColour(ThemeEntryID_TEXT);
if (m_entries.empty()) {
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Empty..."_i18n.c_str());
return;
@@ -215,7 +213,7 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
constexpr float text_xoffset{15.f};
m_list->Draw(vg, theme, m_entries.size(), [this, text_col](auto* vg, auto* theme, auto& v, auto i) {
m_list->Draw(vg, theme, m_entries.size(), [this](auto* vg, auto* theme, auto& v, auto i) {
const auto& [x, y, w, h] = v;
auto& e = m_entries[i];

View File

@@ -20,6 +20,7 @@
#include "ui/popup_list.hpp"
#include "ui/nvg_util.hpp"
#include "yati/nx/ns.hpp"
#include "yati/nx/ncm.hpp"
#include "yati/nx/nca.hpp"
@@ -428,7 +429,7 @@ Menu::Menu(u32 flags) : grid::Menu{"Saves"_i18n, flags} {
);
OnLayoutChange();
nsInitialize();
ns::Initialize();
m_accounts = App::GetAccountList();
@@ -457,7 +458,7 @@ Menu::~Menu() {
title::Exit();
FreeEntries();
nsExit();
ns::Exit();
}
void Menu::Update(Controller* controller, TouchInfo* touch) {

View File

@@ -327,6 +327,13 @@ Sidebar::Sidebar(const std::string& title, const std::string& sub, Side side)
auto Sidebar::Update(Controller* controller, TouchInfo* touch) -> void {
Widget::Update(controller, touch);
// pop if we have no more entries.
if (m_items.empty()) {
App::Notify("Closing empty sidebar"_i18n);
SetPop();
return;
}
// if touched out of bounds, pop the sidebar and all widgets below it.
if (touch->is_clicked && !touch->in_range(GetPos())) {
App::PopToMenu();

View File

@@ -4,8 +4,10 @@
#include "yati/nx/service_guard.h"
#include "defines.hpp"
#include "log.hpp"
#include <memory>
#include <cstring>
#include <ranges>
namespace sphaira::es {
namespace {
@@ -329,4 +331,56 @@ Result PatchTicket(std::vector<u8>& ticket, std::span<const u8> cert_chain, u8 k
R_SUCCEED();
}
Result GetCommonTickets(std::vector<FsRightsId>& out) {
s32 count;
R_TRY(es::CountCommonTicket(&count));
s32 written;
out.resize(count);
R_TRY(es::ListCommonTicket(&written, out.data(), out.size()));
out.resize(written);
R_SUCCEED();
}
Result GetPersonalisedTickets(std::vector<FsRightsId>& out) {
s32 count;
R_TRY(es::CountPersonalizedTicket(&count));
s32 written;
out.resize(count);
R_TRY(es::ListPersonalizedTicket(&written, out.data(), out.size()));
out.resize(written);
R_SUCCEED();
}
Result IsRightsIdCommon(const FsRightsId& id, bool* out) {
std::vector<FsRightsId> ids;
R_TRY(GetCommonTickets(ids));
*out = IsRightsIdFound(id, ids);
R_SUCCEED();
}
Result IsRightsIdPersonalised(const FsRightsId& id, bool* out) {
std::vector<FsRightsId> ids;
R_TRY(GetPersonalisedTickets(ids));
*out = IsRightsIdFound(id, ids);
R_SUCCEED();
}
bool IsRightsIdValid(const FsRightsId& id) {
const FsRightsId empty_id{};
return 0 != std::memcmp(std::addressof(id), std::addressof(empty_id), sizeof(id));
}
bool IsRightsIdFound(const FsRightsId& id, std::span<const FsRightsId> ids) {
const auto it = std::ranges::find_if(ids, [&id](auto& e){
return !std::memcmp(&id, &e, sizeof(e));
});
return it != ids.end();
}
} // namespace sphaira::es

View File

@@ -96,6 +96,28 @@ const unsigned char acid_fixed_key_moduli_retail[0x2][0x100] = { /* Fixed RSA ke
} // namespace
auto GetContentTypeStr(u8 content_type) -> const char* {
switch (content_type) {
case ContentType_Program: return "Program";
case ContentType_Meta: return "Meta";
case ContentType_Control: return "Control";
case ContentType_Manual: return "Manual";
case ContentType_Data: return "Data";
case ContentType_PublicData: return "PublicData";
}
return "Unknown";
}
auto GetDistributionTypeStr(u8 distribution_type) -> const char* {
switch (distribution_type) {
case DistributionType_System: return "System";
case DistributionType_GameCard: return "GameCard";
}
return "Unknown";
}
Result DecryptKeak(const keys::Keys& keys, Header& header) {
const auto key_generation = header.GetKeyGeneration();
@@ -209,46 +231,34 @@ Result ParseControl(const fs::FsPath& path, u64 program_id, void* nacp_out, s64
}
}
static const char* icon_names[] = {
static const char* icon_names[SetLanguage_Total] = {
[SetLanguage_JA] = "icon_Japanese.dat",
[SetLanguage_ENUS] = "icon_AmericanEnglish.dat",
[SetLanguage_FR] = "icon_French.dat",
[SetLanguage_DE] = "icon_German.dat",
[SetLanguage_IT] = "icon_Italian.dat",
[SetLanguage_ES] = "icon_Spanish.dat",
[SetLanguage_ZHCN] = "icon_Chinese.dat",
[SetLanguage_ZHCN] = "icon_SimplifiedChinese.dat",
[SetLanguage_KO] = "icon_Korean.dat",
[SetLanguage_NL] = "icon_Dutch.dat",
[SetLanguage_PT] = "icon_Portuguese.dat",
[SetLanguage_RU] = "icon_Russian.dat",
[SetLanguage_ZHTW] = "icon_Taiwanese.dat",
[SetLanguage_ZHTW] = "icon_TraditionalChinese.dat",
[SetLanguage_ENGB] = "icon_BritishEnglish.dat",
[SetLanguage_FRCA] = "icon_CanadianFrench.dat",
[SetLanguage_ES419] = "icon_LatinAmericanSpanish.dat",
[SetLanguage_ZHHANS] = "icon_SimplifiedChinese.dat",
[SetLanguage_ZHHANT] = "icon_TraditionalChinese.dat",
[SetLanguage_PTBR] = "icon_BrazilianPortuguese.dat",
};
// load all icon entries and try and find the one that we want.
fs::Dir dir;
R_TRY(fs.OpenDirectory("/", FsDirOpenMode_ReadFiles, &dir));
std::vector<FsDirectoryEntry> entries;
R_TRY(dir.ReadAll(entries));
for (const auto& e : entries) {
if (!std::strcmp(e.name, icon_names[setLanguage])) {
fs::File file;
R_TRY(fs.OpenFile(fs::AppendPath("/", e.name), FsOpenMode_Read, &file));
icon_out->resize(e.file_size);
u64 bytes_read;
R_TRY(file.Read(0, icon_out->data(), icon_out->size(), 0, &bytes_read));
R_SUCCEED();
}
}
// otherwise, fallback to US icon.
// try and open the icon for the specific langauge.
fs::File file;
R_TRY(fs.OpenFile(fs::AppendPath("/", icon_names[SetLanguage_ENUS]), FsOpenMode_Read, &file));
const auto file_name = icon_names[setLanguage];
if (!std::strlen(file_name) || R_FAILED(fs.OpenFile(fs::AppendPath("/", file_name), FsOpenMode_Read, &file))) {
// otherwise, fallback to US icon.
R_TRY(fs.OpenFile(fs::AppendPath("/", icon_names[SetLanguage_ENUS]), FsOpenMode_Read, &file));
}
s64 size;
R_TRY(file.GetSize(&size));

View File

@@ -28,6 +28,31 @@ auto GetMetaTypeStr(u8 meta_type) -> const char* {
return "Unknown";
}
auto GetContentTypeStr(u8 content_type) -> const char* {
switch (content_type) {
case NcmContentType_Meta: return "Meta";
case NcmContentType_Program: return "Program";
case NcmContentType_Data: return "Data";
case NcmContentType_Control: return "Control";
case NcmContentType_HtmlDocument: return "HtmlDocument";
case NcmContentType_LegalInformation: return "LegalInformation";
case NcmContentType_DeltaFragment: return "DeltaFragment";
}
return "Unknown";
}
auto GetReadableMetaTypeStr(u8 meta_type) -> const char* {
switch (meta_type) {
default: return "Unknown";
case NcmContentMetaType_Application: return "Application";
case NcmContentMetaType_Patch: return "Update";
case NcmContentMetaType_AddOnContent: return "DLC";
case NcmContentMetaType_Delta: return "Delta";
case NcmContentMetaType_DataPatch: return "DLC Update";
}
}
// taken from nxdumptool
auto GetMetaTypeShortStr(u8 meta_type) -> const char* {
switch (meta_type) {
@@ -61,6 +86,16 @@ auto GetStorageIdStr(u8 storage_id) -> const char* {
return "Unknown";
}
auto GetReadableStorageIdStr(u8 storage_id) -> const char* {
switch (storage_id) {
default: return "Unknown";
case NcmStorageId_None: return "None";
case NcmStorageId_GameCard: return "Game Card";
case NcmStorageId_BuiltInUser: return "System memory";
case NcmStorageId_SdCard: return "microSD card";
}
}
auto GetAppId(u8 meta_type, u64 id) -> u64 {
if (meta_type == NcmContentMetaType_Patch) {
return id ^ 0x800;
@@ -105,4 +140,80 @@ Result Register(NcmContentStorage* cs, const NcmContentId *content_id, const Ncm
return ncmContentStorageRegister(cs, content_id, placeholder_id);
}
Result GetContentMeta(NcmContentMetaDatabase *db, const NcmContentMetaKey *key, ContentMeta& out) {
u64 size;
return ncmContentMetaDatabaseGet(db, key, &size, &out, sizeof(out));
}
Result GetContentInfos(NcmContentMetaDatabase *db, const NcmContentMetaKey *key, std::vector<NcmContentInfo>& out) {
ContentMeta content_meta;
R_TRY(GetContentMeta(db, key, content_meta));
return GetContentInfos(db, key, content_meta.header, out);
}
Result GetContentInfos(NcmContentMetaDatabase *db, const NcmContentMetaKey *key, const NcmContentMetaHeader& header, std::vector<NcmContentInfo>& out) {
s32 entries_written;
out.resize(header.content_count);
R_TRY(ncmContentMetaDatabaseListContentInfo(db, &entries_written, out.data(), out.size(), key, 0));
out.resize(entries_written);
R_SUCCEED();
}
Result DeleteKey(NcmContentStorage* cs, NcmContentMetaDatabase *db, const NcmContentMetaKey *key) {
// get list of infos.
std::vector<NcmContentInfo> infos;
R_TRY(GetContentInfos(db, key, infos));
// delete ncas
for (const auto& info : infos) {
R_TRY(ncmContentStorageDelete(cs, &info.content_id));
}
// remove from ncm db.
R_TRY(ncmContentMetaDatabaseRemove(db, key));
R_TRY(ncmContentMetaDatabaseCommit(db));
R_SUCCEED();
}
Result SetRequiredSystemVersion(NcmContentMetaDatabase *db, const NcmContentMetaKey *key, u32 version) {
// ensure that we can even reset the sys version.
if (!HasRequiredSystemVersion(key)) {
R_SUCCEED();
}
// get the old data size.
u64 size;
R_TRY(ncmContentMetaDatabaseGetSize(db, &size, key));
// fetch the old data.
u64 out_size;
std::vector<u8> data;
R_TRY(ncmContentMetaDatabaseGet(db, key, &out_size, data.data(), data.size()));
// ensure that we have enough data.
R_UNLESS(data.size() == out_size, 0x1);
R_UNLESS(data.size() >= offsetof(ContentMeta, extened.application.required_application_version), 0x1);
// patch the version.
auto content_meta = (ContentMeta*)data.data();
content_meta->extened.application.required_system_version = version;
// write the new data back.
return ncmContentMetaDatabaseSet(db, key, data.data(), data.size());
}
Result GetControlPathFromContentId(NcmContentStorage* cs, const NcmContentMetaKey& key, const NcmContentId& id, u64* out_program_id, fs::FsPath* out_path) {
if (out_program_id) {
*out_program_id = key.id; // todo: verify.
if (hosversionAtLeast(17,0,0)) {
R_TRY(ncmContentStorageGetProgramId(cs, out_program_id, &id, FsContentAttributes_All));
}
}
return ncmContentStorageGetPath(cs, out_path->s, sizeof(*out_path), &id);
}
} // namespace sphaira::ncm

View File

@@ -1,39 +1,101 @@
#include "yati/nx/ns.hpp"
#include "yati/nx/service_guard.h"
#include "defines.hpp"
namespace sphaira::ns {
namespace {
Service g_nsAppSrv;
NX_GENERATE_SERVICE_GUARD(nsEx);
Result _nsExInitialize() {
R_TRY(nsInitialize());
if (hosversionAtLeast(3,0,0)) {
R_TRY(nsGetApplicationManagerInterface(&g_nsAppSrv));
} else {
g_nsAppSrv = *nsGetServiceSession_ApplicationManagerInterface();
}
R_SUCCEED();
}
void _nsExCleanup() {
serviceClose(&g_nsAppSrv);
nsExit();
}
} // namespace
Result PushApplicationRecord(Service* srv, u64 tid, const ncm::ContentStorageRecord* records, u32 count) {
Result Initialize() {
return nsExInitialize();
}
void Exit() {
nsExExit();
}
Result PushApplicationRecord(u64 tid, const ncm::ContentStorageRecord* records, u32 count) {
const struct {
u8 last_modified_event;
u8 padding[0x7];
u64 tid;
} in = { ApplicationRecordType_Installed, {0}, tid };
return serviceDispatchIn(srv, 16, in,
return serviceDispatchIn(&g_nsAppSrv, 16, in,
.buffer_attrs = { SfBufferAttr_HipcMapAlias | SfBufferAttr_In },
.buffers = { { records, sizeof(*records) * count } });
}
Result ListApplicationRecordContentMeta(Service* srv, u64 offset, u64 tid, ncm::ContentStorageRecord* out_records, u32 count, s32* entries_read) {
Result ListApplicationRecordContentMeta(u64 offset, u64 tid, ncm::ContentStorageRecord* out_records, u32 count, s32* entries_read) {
struct {
u64 offset;
u64 tid;
} in = { offset, tid };
return serviceDispatchInOut(srv, 17, in, *entries_read,
return serviceDispatchInOut(&g_nsAppSrv, 17, in, *entries_read,
.buffer_attrs = { SfBufferAttr_HipcMapAlias | SfBufferAttr_Out },
.buffers = { { out_records, sizeof(*out_records) * count } });
}
Result DeleteApplicationRecord(Service* srv, u64 tid) {
return serviceDispatchIn(srv, 27, tid);
Result DeleteApplicationRecord(u64 tid) {
return serviceDispatchIn(&g_nsAppSrv, 27, tid);
}
Result InvalidateApplicationControlCache(Service* srv, u64 tid) {
return serviceDispatchIn(srv, 404, tid);
Result InvalidateApplicationControlCache(u64 tid) {
return serviceDispatchIn(&g_nsAppSrv, 404, tid);
}
Result GetApplicationRecords(u64 id, std::vector<ncm::ContentStorageRecord>& out) {
s32 count;
R_TRY(nsCountApplicationContentMeta(id, &count));
s32 records_read;
out.resize(count);
R_TRY(ns::ListApplicationRecordContentMeta(0, id, out.data(), out.size(), &records_read));
out.resize(records_read);
R_SUCCEED();
}
Result SetLowestLaunchVersion(u64 id) {
std::vector<ncm::ContentStorageRecord> records;
R_TRY(GetApplicationRecords(id, records));
return SetLowestLaunchVersion(id, records);
}
Result SetLowestLaunchVersion(u64 id, std::span<const ncm::ContentStorageRecord> records) {
R_TRY(avmInitialize());
ON_SCOPE_EXIT(avmExit());
u32 new_version = 0;
for (const auto& record : records) {
new_version = std::max(new_version, record.key.version);
}
return avmPushLaunchVersion(id, new_version);
}
} // namespace sphaira::ns

View File

@@ -348,7 +348,6 @@ struct Yati {
NcmContentMetaDatabase db{};
NcmStorageId storage_id{};
Service ns_app{};
std::unique_ptr<container::Base> container{};
Config config{};
keys::Keys keys{};
@@ -382,11 +381,6 @@ Result ThreadData::Read(void* buf, s64 size, u64* bytes_read) {
return rc;
}
auto isRightsIdValid(FsRightsId id) -> bool {
FsRightsId empty_id{};
return 0 != std::memcmp(std::addressof(id), std::addressof(empty_id), sizeof(id));
}
struct HashStr {
char str[0x21];
};
@@ -402,7 +396,7 @@ HashStr hexIdToStr(auto id) {
auto GetTicketCollection(const nca::Header& header, std::span<TikCollection> tik) -> TikCollection* {
TikCollection* ticket{};
if (isRightsIdValid(header.rights_id)) {
if (es::IsRightsIdValid(header.rights_id)) {
auto it = std::ranges::find_if(tik, [&header](auto& e){
return !std::memcmp(&header.rights_id, &e.rights_id, sizeof(e.rights_id));
});
@@ -418,7 +412,7 @@ auto GetTicketCollection(const nca::Header& header, std::span<TikCollection> tik
}
Result HasRequiredTicket(const nca::Header& header, TikCollection* ticket) {
if (isRightsIdValid(header.rights_id)) {
if (es::IsRightsIdValid(header.rights_id)) {
log_write("looking for ticket %s\n", hexIdToStr(header.rights_id).str);
R_UNLESS(ticket, Result_YatiTicketNotFound);
log_write("ticket found\n");
@@ -879,8 +873,7 @@ Yati::Yati(ui::ProgressBox* _pbox, source::Base* _source) : pbox{_pbox}, source{
Yati::~Yati() {
splCryptoExit();
serviceClose(std::addressof(ns_app));
nsExit();
ns::Exit();
es::Exit();
for (size_t i = 0; i < std::size(NCM_STORAGE_IDS); i++) {
@@ -913,8 +906,7 @@ Result Yati::Setup(const ConfigOverride& override) {
R_TRY(source->GetOpenResult());
R_TRY(splCryptoInitialize());
R_TRY(nsInitialize());
R_TRY(nsGetApplicationManagerInterface(std::addressof(ns_app)));
R_TRY(ns::Initialize());
R_TRY(es::Initialize());
for (size_t i = 0; i < std::size(NCM_STORAGE_IDS); i++) {
@@ -1373,7 +1365,7 @@ Result Yati::RegisterNcasAndPushRecord(const CnmtCollection& cnmt, u32 latest_ve
content_storage_record.storage_id = storage_id;
pbox->NewTransfer("Pushing application record"_i18n);
R_TRY(ns::PushApplicationRecord(std::addressof(ns_app), app_id, std::addressof(content_storage_record), 1));
R_TRY(ns::PushApplicationRecord(app_id, std::addressof(content_storage_record), 1));
if (hosversionAtLeast(6,0,0)) {
R_TRY(avmInitialize());
ON_SCOPE_EXIT(avmExit());