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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
111
sphaira/include/ui/menus/game_meta_menu.hpp
Normal file
111
sphaira/include/ui/menus/game_meta_menu.hpp
Normal 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
|
||||
91
sphaira/include/ui/menus/game_nca_menu.hpp
Normal file
91
sphaira/include/ui/menus/game_nca_menu.hpp
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>(
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
375
sphaira/source/ui/menus/game_meta_menu.cpp
Normal file
375
sphaira/source/ui/menus/game_meta_menu.cpp
Normal 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
|
||||
372
sphaira/source/ui/menus/game_nca_menu.cpp
Normal file
372
sphaira/source/ui/menus/game_nca_menu.cpp
Normal 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
|
||||
@@ -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);
|
||||
});
|
||||
}})
|
||||
|
||||
@@ -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];
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user