diff --git a/sphaira/CMakeLists.txt b/sphaira/CMakeLists.txt index 4905f9e..1182be7 100644 --- a/sphaira/CMakeLists.txt +++ b/sphaira/CMakeLists.txt @@ -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 diff --git a/sphaira/include/ui/menus/game_menu.hpp b/sphaira/include/ui/menus/game_menu.hpp index 405bbcd..af02fc3 100644 --- a/sphaira/include/ui/menus/game_menu.hpp +++ b/sphaira/include/ui/menus/game_menu.hpp @@ -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 tik_data{}; + std::vector 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 tickets{}; + // all the collections for this nsp, such as nca's and tickets. + std::vector collections{}; + // raw nsp data (header, file table and string table). + std::vector 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 content_infos{}; + std::vector 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& out); +Result BuildNspEntries(Entry& e, u32 flags, std::vector& out); + +// dumps the array of nsp entries. +void DumpNsp(const std::vector& entries); + } // namespace sphaira::ui::menu::game diff --git a/sphaira/include/ui/menus/game_meta_menu.hpp b/sphaira/include/ui/menus/game_meta_menu.hpp new file mode 100644 index 0000000..06198cd --- /dev/null +++ b/sphaira/include/ui/menus/game_meta_menu.hpp @@ -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 +#include + +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 m_entries{}; + s64 m_index{}; + s64 m_selected_count{}; + std::unique_ptr m_list{}; + bool m_dirty{}; + + std::vector m_common_tickets{}; + std::vector m_personalised_tickets{}; +}; + +} // namespace sphaira::ui::menu::game::meta diff --git a/sphaira/include/ui/menus/game_nca_menu.hpp b/sphaira/include/ui/menus/game_nca_menu.hpp new file mode 100644 index 0000000..199ca29 --- /dev/null +++ b/sphaira/include/ui/menus/game_nca_menu.hpp @@ -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 +#include + +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 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 m_entries{}; + s64 m_index{}; + s64 m_selected_count{}; + std::unique_ptr m_list{}; +}; + +} // namespace sphaira::ui::menu::game::meta_nca diff --git a/sphaira/include/yati/nx/es.hpp b/sphaira/include/yati/nx/es.hpp index 43706a6..42ca49b 100644 --- a/sphaira/include/yati/nx/es.hpp +++ b/sphaira/include/yati/nx/es.hpp @@ -203,4 +203,16 @@ Result ShouldPatchTicket(const TicketData& data, std::span ticket, std Result ShouldPatchTicket(std::span ticket, std::span cert_chain, bool patch_personalised, bool& should_patch); Result PatchTicket(std::vector& ticket, std::span 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& out); +Result GetPersonalisedTickets(std::vector& 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 ids); + } // namespace sphaira::es diff --git a/sphaira/include/yati/nx/nca.hpp b/sphaira/include/yati/nx/nca.hpp index 7f99d59..fd2e6c6 100644 --- a/sphaira/include/yati/nx/nca.hpp +++ b/sphaira/include/yati/nx/nca.hpp @@ -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); diff --git a/sphaira/include/yati/nx/ncm.hpp b/sphaira/include/yati/nx/ncm.hpp index a79222e..fa091bb 100644 --- a/sphaira/include/yati/nx/ncm.hpp +++ b/sphaira/include/yati/nx/ncm.hpp @@ -1,6 +1,9 @@ #pragma once +#include "fs.hpp" + #include +#include 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& 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& 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 diff --git a/sphaira/include/yati/nx/ns.hpp b/sphaira/include/yati/nx/ns.hpp index 3780ba1..b227edf 100644 --- a/sphaira/include/yati/nx/ns.hpp +++ b/sphaira/include/yati/nx/ns.hpp @@ -1,8 +1,11 @@ #pragma once -#include #include "ncm.hpp" +#include +#include +#include + 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& 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 records); + +static inline bool IsNsControlFetchSlow() { + return hosversionAtLeast(20,0,0); +} } // namespace sphaira::ns diff --git a/sphaira/source/app.cpp b/sphaira/source/app.cpp index 6ed7213..2a9e4a4 100644 --- a/sphaira/source/app.cpp +++ b/sphaira/source/app.cpp @@ -520,6 +520,12 @@ void App::Loop() { auto App::Push(std::unique_ptr&& 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("Dump options"_i18n, [left_side](){ + options->Add("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("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("Dump Options"_i18n, left_side ? ui::Sidebar::Side::LEFT : ui::Sidebar::Side::RIGHT); + auto options = std::make_unique("Export Options"_i18n, left_side ? ui::Sidebar::Side::LEFT : ui::Sidebar::Side::RIGHT); ON_SCOPE_EXIT(App::Push(std::move(options))); options->Add( diff --git a/sphaira/source/dumper.cpp b/sphaira/source/dumper.cpp index 32a98b6..09f00b6 100644 --- a/sphaira/source/dumper.cpp +++ b/sphaira/source/dumper.cpp @@ -355,7 +355,7 @@ void DumpGetLocation(const std::string& title, u32 location_flags, const OnLocat } void Dump(const std::shared_ptr& source, const DumpLocation& location, const std::vector& paths, const OnExit& on_exit) { - App::Push(0, "Dumping"_i18n, "", [source, paths, location](auto pbox) -> Result { + App::Push(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& 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& source, const DumpLocation& locatio } void Dump(const std::shared_ptr& source, const std::vector& 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); }); } diff --git a/sphaira/source/owo.cpp b/sphaira/source/owo.cpp index 56d8caf..bc0686c 100644 --- a/sphaira/source/owo.cpp +++ b/sphaira/source/owo.cpp @@ -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(); diff --git a/sphaira/source/threaded_file_transfer.cpp b/sphaira/source/threaded_file_transfer.cpp index 64c9af9..c2a975e 100644 --- a/sphaira/source/threaded_file_transfer.cpp +++ b/sphaira/source/threaded_file_transfer.cpp @@ -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()); } diff --git a/sphaira/source/title_info.cpp b/sphaira/source/title_info.cpp index 3552e42..071e52c 100644 --- a/sphaira/source/title_info.cpp +++ b/sphaira/source/title_info.cpp @@ -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(); - 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 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. diff --git a/sphaira/source/ui/menus/game_menu.cpp b/sphaira/source/ui/menus/game_menu.cpp index a6f663c..54b5843 100644 --- a/sphaira/source/ui/menus/game_menu.cpp +++ b/sphaira/source/ui/menus/game_menu.cpp @@ -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 content_infos{}; - std::vector ncm_rights_id{}; -}; - -struct TikEntry { - FsRightsId id{}; - u8 key_gen{}; - std::vector tik_data{}; - std::vector 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 tickets{}; - // all the collections for this nsp, such as nca's and tickets. - std::vector collections{}; - // raw nsp data (header, file table and string table). - std::vector 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& 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 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& 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("Info"_i18n, [this](){ - + options->Add("View application content"_i18n, [this](){ + App::Push(m_entries[m_index]); }); - #endif options->Add("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("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(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( - "Entries"_i18n, items, [this, meta_entries](auto op_index){ - #if 0 - if (op_index) { - const auto& e = meta_entries[*op_index]; - } - #endif - } - ); - }); - - options->Add("Dump"_i18n, [this](){ - auto options = std::make_unique("Select content to dump"_i18n, Sidebar::Side::RIGHT); + options->Add("Export NSP"_i18n, [this](){ + auto options = std::make_unique("Select content to export"_i18n, Sidebar::Side::RIGHT); ON_SCOPE_EXIT(App::Push(std::move(options))); - options->Add("Dump All"_i18n, [this](){ + options->Add("Export All"_i18n, [this](){ DumpGames(title::ContentFlag_All); }, true); - options->Add("Dump Application"_i18n, [this](){ + options->Add("Export Application"_i18n, [this](){ DumpGames(title::ContentFlag_Application); }, true); - options->Add("Dump Patch"_i18n, [this](){ + options->Add("Export Patch"_i18n, [this](){ DumpGames(title::ContentFlag_Patch); }, true); - options->Add("Dump AddOnContent"_i18n, [this](){ + options->Add("Export AddOnContent"_i18n, [this](){ DumpGames(title::ContentFlag_AddOnContent); }, true); - options->Add("Dump DataPatch"_i18n, [this](){ + options->Add("Export DataPatch"_i18n, [this](){ DumpGames(title::ContentFlag_DataPatch); }, true); }, true); - options->Add("Dump options"_i18n, [this](){ + options->Add("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 nsp_entries; for (auto& e : targets) { BuildNspEntries(e, flags, nsp_entries); } - std::vector paths; - for (auto& e : nsp_entries) { - paths.emplace_back(fs::AppendPath("/dumps/NSP", e.path)); - } - - auto source = std::make_shared(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(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 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 infos; + R_TRY(ncm::GetContentInfos(meta.db, &meta.key, infos)); + + std::vector 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& 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& out) { + title::MetaEntries meta_entries; + R_TRY(GetMetaEntries(e, meta_entries, flags)); + + return BuildNspEntries(e, meta_entries, out); +} + +void DumpNsp(const std::vector& entries) { + std::vector paths; + for (auto& e : entries) { + paths.emplace_back(fs::AppendPath("/dumps/NSP", e.path)); + } + + auto source = std::make_shared(entries); + dump::Dump(source, paths); +} + } // namespace sphaira::ui::menu::game diff --git a/sphaira/source/ui/menus/game_meta_menu.cpp b/sphaira/source/ui/menus/game_meta_menu.cpp new file mode 100644 index 0000000..b72f19f --- /dev/null +++ b/sphaira/source/ui/menus/game_meta_menu.cpp @@ -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 +#include + +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(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("Content Options"_i18n, Sidebar::Side::RIGHT); + ON_SCOPE_EXIT(App::Push(std::move(options))); + + if (!m_entries.empty()) { + options->Add("Export NSP"_i18n, [this](){ + DumpGames(); + }); + + options->Add("Export options"_i18n, [this](){ + App::DisplayDumpOptions(false); + }); + + options->Add("Delete"_i18n, [this](){ + App::Push( + "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("Reset required system version"_i18n, [this](){ + App::Push( + "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(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 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 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 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 diff --git a/sphaira/source/ui/menus/game_nca_menu.cpp b/sphaira/source/ui/menus/game_nca_menu.cpp new file mode 100644 index 0000000..f7d7563 --- /dev/null +++ b/sphaira/source/ui/menus/game_nca_menu.cpp @@ -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 +#include + +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& 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 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("NCA Options"_i18n, Sidebar::Side::RIGHT); + ON_SCOPE_EXIT(App::Push(std::move(options))); + + if (!m_entries.empty()) { + options->Add("Export NCA"_i18n, [this](){ + DumpNcas(); + }); + + options->Add("Verify NCA 256 hash"_i18n, [this](){ + static std::string hash_out; + hash_out.clear(); + + App::Push(m_entry.image, "Hashing"_i18n, hexIdToStr(GetEntry().content_id).str, [this](auto pbox) -> Result{ + auto source = std::make_unique(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("NCA hash missmatch!"_i18n, "OK"_i18n); + } else { + App::Push("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("Verify NCA fixed key"_i18n, [this](){ + if (R_FAILED(nca::VerifyFixedKey(GetEntry().header))) { + App::Push("NCA fixed key is invalid!"_i18n, "OK"_i18n); + } else { + App::Push("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 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(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 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(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 diff --git a/sphaira/source/ui/menus/gc_menu.cpp b/sphaira/source/ui/menus/gc_menu.cpp index e00d1d7..01becb8 100644 --- a/sphaira/source/ui/menus/gc_menu.cpp +++ b/sphaira/source/ui/menus/gc_menu.cpp @@ -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("Dump options"_i18n, [this](){ + options->Add("Export options"_i18n, [this](){ App::DisplayDumpOptions(false); }); }}) diff --git a/sphaira/source/ui/menus/ghdl.cpp b/sphaira/source/ui/menus/ghdl.cpp index 2d081b0..4d3ffbd 100644 --- a/sphaira/source/ui/menus/ghdl.cpp +++ b/sphaira/source/ui/menus/ghdl.cpp @@ -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]; diff --git a/sphaira/source/ui/menus/save_menu.cpp b/sphaira/source/ui/menus/save_menu.cpp index f925243..3218c32 100644 --- a/sphaira/source/ui/menus/save_menu.cpp +++ b/sphaira/source/ui/menus/save_menu.cpp @@ -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) { diff --git a/sphaira/source/ui/sidebar.cpp b/sphaira/source/ui/sidebar.cpp index d014562..0f9f150 100644 --- a/sphaira/source/ui/sidebar.cpp +++ b/sphaira/source/ui/sidebar.cpp @@ -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(); diff --git a/sphaira/source/yati/nx/es.cpp b/sphaira/source/yati/nx/es.cpp index 9f6a279..317d799 100644 --- a/sphaira/source/yati/nx/es.cpp +++ b/sphaira/source/yati/nx/es.cpp @@ -4,8 +4,10 @@ #include "yati/nx/service_guard.h" #include "defines.hpp" #include "log.hpp" + #include #include +#include namespace sphaira::es { namespace { @@ -329,4 +331,56 @@ Result PatchTicket(std::vector& ticket, std::span cert_chain, u8 k R_SUCCEED(); } +Result GetCommonTickets(std::vector& 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& 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 ids; + R_TRY(GetCommonTickets(ids)); + + *out = IsRightsIdFound(id, ids); + R_SUCCEED(); +} + +Result IsRightsIdPersonalised(const FsRightsId& id, bool* out) { + std::vector 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 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 diff --git a/sphaira/source/yati/nx/nca.cpp b/sphaira/source/yati/nx/nca.cpp index 7f945c7..f4221cb 100644 --- a/sphaira/source/yati/nx/nca.cpp +++ b/sphaira/source/yati/nx/nca.cpp @@ -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 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)); diff --git a/sphaira/source/yati/nx/ncm.cpp b/sphaira/source/yati/nx/ncm.cpp index 16049f6..dea46ab 100644 --- a/sphaira/source/yati/nx/ncm.cpp +++ b/sphaira/source/yati/nx/ncm.cpp @@ -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& 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& 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 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 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 diff --git a/sphaira/source/yati/nx/ns.cpp b/sphaira/source/yati/nx/ns.cpp index ecd88c3..2b8b145 100644 --- a/sphaira/source/yati/nx/ns.cpp +++ b/sphaira/source/yati/nx/ns.cpp @@ -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& 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 records; + R_TRY(GetApplicationRecords(id, records)); + + return SetLowestLaunchVersion(id, records); +} + +Result SetLowestLaunchVersion(u64 id, std::span 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 diff --git a/sphaira/source/yati/yati.cpp b/sphaira/source/yati/yati.cpp index 54648a8..2e4df31 100644 --- a/sphaira/source/yati/yati.cpp +++ b/sphaira/source/yati/yati.cpp @@ -348,7 +348,6 @@ struct Yati { NcmContentMetaDatabase db{}; NcmStorageId storage_id{}; - Service ns_app{}; std::unique_ptr 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 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 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());