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

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