Files
sphaira/sphaira/source/ui/menus/gc_menu.cpp

987 lines
34 KiB
C++

#include "ui/menus/gc_menu.hpp"
#include "ui/nvg_util.hpp"
#include "ui/sidebar.hpp"
#include "ui/popup_list.hpp"
#include "ui/option_box.hpp"
#include "yati/yati.hpp"
#include "yati/nx/nca.hpp"
#include "app.hpp"
#include "defines.hpp"
#include "log.hpp"
#include "i18n.hpp"
#include "download.hpp"
#include "dumper.hpp"
#include <cstring>
#include <algorithm>
namespace sphaira::ui::menu::gc {
namespace {
constexpr u32 XCI_MAGIC = std::byteswap(0x48454144);
enum DumpFileType {
DumpFileType_XCI,
DumpFileType_TrimmedXCI,
DumpFileType_Set,
DumpFileType_UID,
DumpFileType_Cert,
DumpFileType_Initial,
};
enum DumpFileFlag {
DumpFileFlag_XCI = 1 << 0,
DumpFileFlag_Set = 1 << 1,
DumpFileFlag_UID = 1 << 2,
DumpFileFlag_Cert = 1 << 3,
DumpFileFlag_Initial = 1 << 4,
DumpFileFlag_AllBin = DumpFileFlag_Set | DumpFileFlag_UID | DumpFileFlag_Cert | DumpFileFlag_Initial,
DumpFileFlag_All = DumpFileFlag_XCI | DumpFileFlag_AllBin,
};
const char *g_option_list[] = {
"Install",
"Dump",
"Exit",
};
// taken from nxdumptool.
void utilsReplaceIllegalCharacters(char *str, bool ascii_only)
{
static const char g_illegalFileSystemChars[] = "\\/:*?\"<>|";
size_t str_size = 0, cur_pos = 0;
if (!str || !(str_size = strlen(str))) return;
u8 *ptr1 = (u8*)str, *ptr2 = ptr1;
ssize_t units = 0;
u32 code = 0;
bool repl = false;
while(cur_pos < str_size)
{
units = decode_utf8(&code, ptr1);
if (units < 0) break;
if (code < 0x20 || (!ascii_only && code == 0x7F) || (ascii_only && code >= 0x7F) || \
(units == 1 && memchr(g_illegalFileSystemChars, (int)code, std::size(g_illegalFileSystemChars))))
{
if (!repl)
{
*ptr2++ = '_';
repl = true;
}
} else {
if (ptr2 != ptr1) memmove(ptr2, ptr1, (size_t)units);
ptr2 += units;
repl = false;
}
ptr1 += units;
cur_pos += (size_t)units;
}
*ptr2 = '\0';
}
auto GetDumpTypeStr(u8 type) -> const char* {
switch (type) {
case DumpFileType_XCI: return ".xci";
case DumpFileType_TrimmedXCI: return " (trimmed).xci";
case DumpFileType_Set: return " (Card ID Set).bin";
case DumpFileType_UID: return " (Card UID).bin";
case DumpFileType_Cert: return " (Certificate).bin";
case DumpFileType_Initial: return " (Initial Data).bin";
}
return "";
}
auto BuildXciName(const ApplicationEntry& e) -> fs::FsPath {
fs::FsPath name_buf = e.lang_entry.name;
utilsReplaceIllegalCharacters(name_buf, true);
fs::FsPath path;
std::snprintf(path, sizeof(path), "%s [%016lX][v%u]", name_buf.s, e.app_id, e.version);
return path;
}
auto BuildXciBasePath(std::span<const ApplicationEntry> entries) -> fs::FsPath {
fs::FsPath path;
for (s64 i = 0; i < std::size(entries); i++) {
if (i) {
path += " + ";
}
path += BuildXciName(entries[i]);
}
return path;
}
// builds path suiteable for usb transfer.
auto BuildFilePath(DumpFileType type, std::span<const ApplicationEntry> entries) -> fs::FsPath {
return BuildXciBasePath(entries) + GetDumpTypeStr(type);
}
// builds path suiteable for file dumps.
auto BuildFullDumpPath(DumpFileType type, std::span<const ApplicationEntry> entries) -> fs::FsPath {
const auto base_path = BuildXciBasePath(entries);
if (App::GetApp()->m_dump_app_folder.Get()) {
if (App::GetApp()->m_dump_append_folder_with_xci.Get()) {
return base_path + ".xci/" + base_path + GetDumpTypeStr(type);
} else {
return base_path + "/" + base_path + GetDumpTypeStr(type);
}
} else {
return base_path + GetDumpTypeStr(type);
}
}
// @Gc is the mount point, S is for secure partion, the remaining is the
// the gamecard handle value in lower-case hex.
auto BuildGcPath(const char* name, const FsGameCardHandle* handle, FsGameCardPartition partiton = FsGameCardPartition_Secure) -> fs::FsPath {
static const char mount_parition[] = {
[FsGameCardPartition_Update] = 'U',
[FsGameCardPartition_Normal] = 'N',
[FsGameCardPartition_Secure] = 'S',
[FsGameCardPartition_Logo] = 'L',
};
fs::FsPath path;
std::snprintf(path, sizeof(path), "@Gc%c%08x://%s", mount_parition[partiton], handle->value, name);
return path;
}
struct XciSource final : dump::BaseSource {
// application name.
std::string application_name{};
// extra
std::vector<u8> id_set{};
std::vector<u8> uid{};
std::vector<u8> cert{};
std::vector<u8> initial{};
// size of the entire xci.
s64 xci_size{};
Menu* menu{};
Result Read(const std::string& path, void* buf, s64 off, s64 size, u64* bytes_read) override {
if (path.ends_with(GetDumpTypeStr(DumpFileType_XCI))) {
size = ClipSize(off, size, xci_size);
*bytes_read = size;
return menu->GcStorageRead(buf, off, size);
} else {
std::span<const u8> span;
if (path.ends_with(GetDumpTypeStr(DumpFileType_Set))) {
span = id_set;
} else if (path.ends_with(GetDumpTypeStr(DumpFileType_UID))) {
span = uid;
} else if (path.ends_with(GetDumpTypeStr(DumpFileType_Cert))) {
span = cert;
} else if (path.ends_with(GetDumpTypeStr(DumpFileType_Initial))) {
span = initial;
}
R_UNLESS(!span.empty(), 0x1);
size = ClipSize(off, size, span.size());
*bytes_read = size;
std::memcpy(buf, span.data() + off, size);
R_SUCCEED();
}
}
auto GetName(const std::string& path) const -> std::string override {
return application_name;
}
auto GetSize(const std::string& path) const -> s64 override {
if (path.ends_with(GetDumpTypeStr(DumpFileType_XCI))) {
return xci_size;
} else if (path.ends_with(GetDumpTypeStr(DumpFileType_Set))) {
return id_set.size();
} else if (path.ends_with(GetDumpTypeStr(DumpFileType_UID))) {
return uid.size();
} else if (path.ends_with(GetDumpTypeStr(DumpFileType_Cert))) {
return cert.size();
} else if (path.ends_with(GetDumpTypeStr(DumpFileType_Initial))) {
return initial.size();
}
return 0;
}
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 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;
}
// from Gamecard-Installer-NX
Result fsOpenGameCardStorage(FsStorage* out, const FsGameCardHandle* handle, FsGameCardStoragePartition partition) {
const struct {
FsGameCardHandle handle;
u32 partition;
} in = { *handle, (u32)partition };
return serviceDispatchIn(fsGetServiceSession(), 30, in, .out_num_objects = 1, .out_objects = &out->s);
}
Result fsOpenGameCardDetectionEventNotifier(FsEventNotifier* out) {
return serviceDispatch(fsGetServiceSession(), 501,
.out_num_objects = 1,
.out_objects = &out->s
);
}
struct GcSource final : yati::source::Base {
GcSource(const ApplicationEntry& entry, fs::FsNativeGameCard* fs);
~GcSource();
Result Read(void* buf, s64 off, s64 size, u64* bytes_read);
yati::container::Collections m_collections{};
yati::ConfigOverride m_config{};
fs::FsNativeGameCard* m_fs{};
FsFile m_file{};
s64 m_offset{};
s64 m_size{};
private:
static auto InRange(s64 off, s64 offset, s64 size) -> bool {
return off < offset + size && off >= offset;
}
};
GcSource::GcSource(const ApplicationEntry& entry, fs::FsNativeGameCard* fs)
: m_fs{fs} {
m_offset = -1;
s64 offset{};
const auto add_collections = [&](const auto& collections) {
for (auto collection : collections) {
collection.offset = offset;
m_collections.emplace_back(collection);
offset += collection.size;
}
};
const auto add_entries = [&](const auto& entries) {
for (auto& e : entries) {
add_collections(e);
}
};
// yati can handle all of this for use, however, yati lacks information
// for ncas until it installs the cnmt and parses it.
// as we already have this info, we can only send yati what we want to install.
if (App::GetApp()->m_ticket_only.Get()) {
add_collections(entry.tickets);
} else {
if (!App::GetApp()->m_skip_base.Get()) {
add_entries(entry.application);
}
if (!App::GetApp()->m_skip_patch.Get()) {
add_entries(entry.patch);
}
if (!App::GetApp()->m_skip_addon.Get()) {
add_entries(entry.add_on);
}
if (!App::GetApp()->m_skip_data_patch.Get()) {
add_entries(entry.data_patch);
}
if (!App::GetApp()->m_skip_ticket.Get()) {
add_collections(entry.tickets);
}
}
// we don't need to verify the nca's, this speeds up installs.
m_config.skip_nca_hash_verify = true;
m_config.skip_rsa_header_fixed_key_verify = true;
m_config.skip_rsa_npdm_fixed_key_verify = true;
}
GcSource::~GcSource() {
fsFileClose(&m_file);
}
Result GcSource::Read(void* buf, s64 off, s64 size, u64* bytes_read) {
// check is we need to open a new file.
if (!InRange(off, m_offset, m_size)) {
fsFileClose(&m_file);
m_file = {};
// find new file based on the offset.
bool found = false;
for (auto& collection : m_collections) {
if (InRange(off, collection.offset, collection.size)) {
found = true;
m_offset = collection.offset;
m_size = collection.size;
R_TRY(m_fs->OpenFile(fs::AppendPath("/", collection.name), FsOpenMode_Read, &m_file));
break;
}
}
// this will never fail, unless i break something in yati.
R_UNLESS(found, 0x1);
}
return fsFileRead(&m_file, off - m_offset, buf, size, 0, bytes_read);
}
} // namespace
auto ApplicationEntry::GetSize(const std::vector<GcCollections>& entries) const -> s64 {
s64 size{};
for (auto& e : entries) {
for (auto& collection : e) {
size += collection.size;
}
}
return size;
}
auto ApplicationEntry::GetSize() const -> s64 {
s64 size{};
size += GetSize(application);
size += GetSize(patch);
size += GetSize(add_on);
size += GetSize(data_patch);
return size;
}
Menu::Menu() : MenuBase{"GameCard"_i18n} {
this->SetActions(
std::make_pair(Button::B, Action{"Back"_i18n, [this](){
SetPop();
}}),
std::make_pair(Button::X, Action{"Options"_i18n, [this](){
auto options = std::make_shared<Sidebar>("Game Options"_i18n, Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(options));
options->Add(std::make_shared<SidebarEntryCallback>("Install options"_i18n, [this](){
App::DisplayInstallOptions(false);
}));
options->Add(std::make_shared<SidebarEntryCallback>("Dump options"_i18n, [this](){
App::DisplayDumpOptions(false);
}));
}})
);
const Vec4 v{485, 275, 720, 70};
const Vec2 pad{0, 125 - v.h};
m_list = std::make_unique<List>(1, 3, m_pos, v, pad);
nsInitialize();
fsOpenDeviceOperator(std::addressof(m_dev_op));
fsOpenGameCardDetectionEventNotifier(std::addressof(m_event_notifier));
fsEventNotifierGetEventHandle(std::addressof(m_event_notifier), std::addressof(m_event), true);
}
Menu::~Menu() {
GcUmountStorage();
GcUnmount();
eventClose(std::addressof(m_event));
fsEventNotifierClose(std::addressof(m_event_notifier));
fsDeviceOperatorClose(std::addressof(m_dev_op));
}
void Menu::Update(Controller* controller, TouchInfo* touch) {
// poll for the gamecard first before handling inputs as the gamecard
// may have been removed, thus pressing A would fail.
if (R_SUCCEEDED(eventWait(std::addressof(m_event), 0))) {
GcOnEvent();
}
MenuBase::Update(controller, touch);
m_list->OnUpdate(controller, touch, m_option_index, std::size(g_option_list), [this](bool touch, auto i) {
if (touch && m_option_index == i) {
FireAction(Button::A);
} else {
App::PlaySoundEffect(SoundEffect_Focus);
m_option_index = i;
}
});
}
void Menu::Draw(NVGcontext* vg, Theme* theme) {
MenuBase::Draw(vg, theme);
#define STORAGE_BAR_W 325
#define STORAGE_BAR_H 14
const auto size_sd_gb = (double)m_size_free_sd / 0x40000000;
const auto size_nand_gb = (double)m_size_free_nand / 0x40000000;
gfx::drawTextArgs(vg, 490, 135, 23.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "System memory %.1f GB", size_nand_gb);
gfx::drawRect(vg, 480, 170, STORAGE_BAR_W, STORAGE_BAR_H, theme->GetColour(ThemeEntryID_TEXT));
gfx::drawRect(vg, 480 + 1, 170 + 1, STORAGE_BAR_W - 2, STORAGE_BAR_H - 2, theme->GetColour(ThemeEntryID_BACKGROUND));
gfx::drawRect(vg, 480 + 2, 170 + 2, STORAGE_BAR_W - (((double)m_size_free_nand / (double)m_size_total_nand) * STORAGE_BAR_W) - 4, STORAGE_BAR_H - 4, theme->GetColour(ThemeEntryID_TEXT));
gfx::drawTextArgs(vg, 870, 135, 23.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "microSD card %.1f GB", size_sd_gb);
gfx::drawRect(vg, 860, 170, STORAGE_BAR_W, STORAGE_BAR_H, theme->GetColour(ThemeEntryID_TEXT));
gfx::drawRect(vg, 860 + 1, 170 + 1, STORAGE_BAR_W - 2, STORAGE_BAR_H - 2, theme->GetColour(ThemeEntryID_BACKGROUND));
gfx::drawRect(vg, 860 + 2, 170 + 2, STORAGE_BAR_W - (((double)m_size_free_sd / (double)m_size_total_sd) * STORAGE_BAR_W) - 4, STORAGE_BAR_H - 4, theme->GetColour(ThemeEntryID_TEXT));
gfx::drawRect(vg, 30, 90, 375, 555, theme->GetColour(ThemeEntryID_GRID));
if (!m_entries.empty()) {
const auto& e = m_entries[m_entry_index];
const auto size = e.GetSize();
gfx::drawImage(vg, 90, 130, 256, 256, m_icon ? m_icon : App::GetDefaultImage());
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), "%s", e.lang_entry.name);
gfx::drawTextArgs(vg, 50, 455, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "%s", e.lang_entry.author);
gfx::drawTextArgs(vg, 50, 495, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "App-ID: 0%lX", e.app_id);
gfx::drawTextArgs(vg, 50, 535, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Key-Gen: %u (%s)", e.key_gen, nca::GetKeyGenStr(e.key_gen));
gfx::drawTextArgs(vg, 50, 575, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Size: %.2f GB", (double)size / 0x40000000);
gfx::drawTextArgs(vg, 50, 615, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Base: %zu Patch: %zu Addon: %zu Data: %zu", e.application.size(), e.patch.size(), e.add_on.size(), e.data_patch.size());
nvgRestore(vg);
}
m_list->Draw(vg, theme, std::size(g_option_list), [this](auto* vg, auto* theme, auto v, auto i) {
const auto& [x, y, w, h] = v;
const auto text_y = y + (h / 2.f);
auto colour = ThemeEntryID_TEXT;
if (i == m_option_index) {
gfx::drawRectOutline(vg, theme, 4.f, v);
// g_background.selected_bar = create_shape(Colour_Nintendo_Cyan, 90, 230, 4, 45, true);
// draw_shape_position(&g_background.selected_bar, 485, g_options[i].text->rect.y - 10);
gfx::drawRect(vg, 490, text_y - 45.f / 2.f, 2, 45, theme->GetColour(ThemeEntryID_TEXT_SELECTED));
colour = ThemeEntryID_TEXT_SELECTED;
}
if (i != 2 && !m_mounted) {
colour = ThemeEntryID_TEXT_INFO;
}
gfx::drawTextArgs(vg, x + 15, y + (h / 2.f), 23.f, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(colour), "%s", g_option_list[i]);
});
}
void Menu::OnFocusGained() {
MenuBase::OnFocusGained();
GcOnEvent();
UpdateStorageSize();
}
Result Menu::GcMount() {
GcUnmount();
// after storage has been mounted, it will take 2 attempts to mount
// the fs, same as mounting storage.
for (int i = 0; i < 2; i++) {
R_TRY(fsDeviceOperatorGetGameCardHandle(std::addressof(m_dev_op), std::addressof(m_handle)));
m_fs = std::make_unique<fs::FsNativeGameCard>(std::addressof(m_handle), FsGameCardPartition_Secure, false);
if (R_SUCCEEDED(m_fs->GetFsOpenResult())) {
break;
}
}
R_TRY(m_fs->GetFsOpenResult());
FsDir dir;
R_TRY(m_fs->OpenDirectory("/", FsDirOpenMode_ReadFiles, std::addressof(dir)));
ON_SCOPE_EXIT(fsDirClose(std::addressof(dir)));
s64 count;
R_TRY(m_fs->DirGetEntryCount(std::addressof(dir), std::addressof(count)));
std::vector<FsDirectoryEntry> buf(count);
s64 total_entries;
R_TRY(m_fs->DirRead(std::addressof(dir), std::addressof(total_entries), buf.size(), buf.data()));
R_UNLESS(buf.size() == total_entries, 0x1);
yati::container::Collections ticket_collections;
for (const auto& e : buf) {
if (!std::string_view(e.name).ends_with(".tik") && !std::string_view(e.name).ends_with(".cert")) {
continue;
}
ticket_collections.emplace_back(e.name, 0, e.file_size);
}
for (const auto& e : buf) {
// we could use ncm to handle finding all the ncas for us
// however, we can parse faster than ncm.
// not only that, the first few calls trying to mount ncm db for
// the gamecard will fail as it has not yet been parsed (or it's locked?).
// we could, of course, just wait until ncm is ready, which is about
// 32ms, but i already have code for manually parsing cnmt so lets re-use it.
if (!std::string_view(e.name).ends_with(".cnmt.nca")) {
continue;
}
// we don't yet use the header or extended header.
ncm::PackagedContentMeta header;
std::vector<u8> extended_header;
std::vector<NcmPackagedContentInfo> infos;
const auto path = BuildGcPath(e.name, &m_handle);
R_TRY(nca::ParseCnmt(path, 0, header, extended_header, infos));
u8 key_gen;
FsRightsId rights_id;
R_TRY(fsGetRightsIdAndKeyGenerationByPath(path, FsContentAttributes_All, &key_gen, &rights_id));
// always add tickets, yati will ignore them if not needed.
GcCollections collections;
// add cnmt file.
collections.emplace_back(e.name, e.file_size, NcmContentType_Meta, 0);
for (const auto& packed_info : infos) {
const auto& info = packed_info.info;
// these don't exist for gamecards, however i may copy/paste this code
// somewhere so i'm future proofing against myself.
if (info.content_type == NcmContentType_DeltaFragment) {
continue;
}
// find the nca file, this will never fail for gamecards, see above comment.
const auto str = hexIdToStr(info.content_id);
const auto it = std::find_if(buf.cbegin(), buf.cend(), [str](auto& e){
return !std::strncmp(str.str, e.name, std::strlen(str.str));
});
R_UNLESS(it != buf.cend(), yati::Result_NcaNotFound);
collections.emplace_back(it->name, it->file_size, info.content_type, info.id_offset);
}
const auto app_id = ncm::GetAppId(header);
ApplicationEntry* app_entry{};
for (auto& app : m_entries) {
if (app.app_id == app_id) {
app_entry = &app;
break;
}
}
if (!app_entry) {
app_entry = &m_entries.emplace_back(app_id, header.title_version);
}
app_entry->version = std::max(app_entry->version, header.title_version);
app_entry->key_gen = std::max(app_entry->key_gen, key_gen);
if (header.meta_type == NcmContentMetaType_Application) {
app_entry->application.emplace_back(collections);
} else if (header.meta_type == NcmContentMetaType_Patch) {
app_entry->patch.emplace_back(collections);
} else if (header.meta_type == NcmContentMetaType_AddOnContent) {
app_entry->add_on.emplace_back(collections);
} else if (header.meta_type == NcmContentMetaType_DataPatch) {
app_entry->data_patch.emplace_back(collections);
}
}
R_UNLESS(m_entries.size(), 0x1);
// append tickets to every application, yati will ignore if undeeded.
for (auto& e : m_entries) {
e.tickets = ticket_collections;
}
// load all control data, icons are loaded when displayed.
for (auto& e : m_entries) {
R_TRY(LoadControlData(e));
NacpLanguageEntry* lang_entry{};
R_TRY(nacpGetLanguageEntry(&e.control->nacp, &lang_entry));
if (lang_entry) {
e.lang_entry = *lang_entry;
}
}
SetAction(Button::A, Action{"OK"_i18n, [this](){
if (m_option_index == 2) {
SetPop();
} else {
if (!m_mounted) {
return;
}
if (m_option_index == 0) {
if (!App::GetInstallEnable()) {
App::Push(std::make_shared<ui::OptionBox>(
"Install disabled...\n"
"Please enable installing via the install options."_i18n,
"OK"_i18n
));
} else {
App::Push(std::make_shared<ui::ProgressBox>(m_icon, "Installing "_i18n, m_entries[m_entry_index].lang_entry.name, [this](auto pbox) -> Result {
auto source = std::make_shared<GcSource>(m_entries[m_entry_index], m_fs.get());
return yati::InstallFromCollections(pbox, source, source->m_collections, source->m_config);
}, [this](Result rc){
App::PushErrorBox(rc, "Gc install failed"_i18n);
if (R_SUCCEEDED(rc)) {
App::Notify("Gc install success!"_i18n);
}
}));
}
} else {
auto options = std::make_shared<Sidebar>("Select content to dump"_i18n, Sidebar::Side::RIGHT);
ON_SCOPE_EXIT(App::Push(options));
options->Add(std::make_shared<SidebarEntryCallback>("Dump XCI"_i18n, [this](){
DumpGames(DumpFileFlag_XCI);
}, true));
options->Add(std::make_shared<SidebarEntryCallback>("Dump All"_i18n, [this](){
DumpGames(DumpFileFlag_All);
}, true));
options->Add(std::make_shared<SidebarEntryCallback>("Dump All Bins"_i18n, [this](){
DumpGames(DumpFileFlag_AllBin);
}, true));
options->Add(std::make_shared<SidebarEntryCallback>("Dump Card ID Set"_i18n, [this](){
DumpGames(DumpFileFlag_Set);
}, true));
// todo:
// options->Add(std::make_shared<SidebarEntryCallback>("Dump Card UID"_i18n, [this](){
// DumpGames(DumpFileFlag_UID);
// }, true));
options->Add(std::make_shared<SidebarEntryCallback>("Dump Certificate"_i18n, [this](){
DumpGames(DumpFileFlag_Cert);
}, true));
// todo:
// options->Add(std::make_shared<SidebarEntryCallback>("Dump Initial Data"_i18n, [this](){
// DumpGames(DumpFileFlag_Initial);
// }, true));
}
}
}});
if (m_entries.size() > 1) {
SetAction(Button::L2, Action{"Prev"_i18n, [this](){
if (m_entry_index != 0) {
OnChangeIndex(m_entry_index - 1);
}
}});
SetAction(Button::R2, Action{"Next"_i18n, [this](){
if (m_entry_index < m_entries.size()) {
OnChangeIndex(m_entry_index + 1);
}
}});
}
OnChangeIndex(0);
m_mounted = true;
R_SUCCEED();
}
void Menu::GcUnmount() {
m_fs.reset();
m_entries.clear();
m_entry_index = 0;
m_mounted = false;
FreeImage();
RemoveAction(Button::L2);
RemoveAction(Button::R2);
}
Result Menu::GcMountStorage() {
GcUmountStorage();
R_TRY(GcMountPartition(FsGameCardStoragePartition_Normal));
u8 header[0x200];
R_TRY(fsStorageRead(&m_storage, 0, header, sizeof(header)));
u32 magic;
u32 trim_size;
std::memcpy(&magic, header + 0x100, sizeof(magic));
std::memcpy(&trim_size, header + 0x118, sizeof(trim_size));
std::memcpy(m_initial_data_hash, header + 0x160, sizeof(m_initial_data_hash));
R_UNLESS(magic == XCI_MAGIC, 0x1);
R_TRY(fsStorageGetSize(&m_storage, &m_parition_normal_size));
R_TRY(GcMountPartition(FsGameCardStoragePartition_Secure));
R_TRY(fsStorageGetSize(&m_storage, &m_parition_secure_size));
m_storage_trimmed_size = sizeof(header) + trim_size * 512ULL;
m_storage_total_size = m_parition_normal_size + m_parition_secure_size;
m_storage_mounted = true;
R_SUCCEED();
}
void Menu::GcUmountStorage() {
if (m_storage_mounted) {
m_storage_mounted = false;
GcUnmountPartition();
}
}
Result Menu::GcMountPartition(FsGameCardStoragePartition partition) {
if (m_partition == partition) {
R_SUCCEED();
}
GcUnmountPartition();
// first attempt always fails due to qlaunch having the secure area mounted.
// the 2nd attempt will succeeded, but qlaunch will fail to mount
// the gamecard as it will only attempt to mount once.
Result rc;
for (int i = 0; i < 2; i++) {
R_TRY(fsDeviceOperatorGetGameCardHandle(&m_dev_op, &m_handle));
if (R_SUCCEEDED(rc = fsOpenGameCardStorage(&m_storage, &m_handle, partition))){
break;
}
}
m_partition = partition;
return rc;
}
void Menu::GcUnmountPartition() {
if (m_partition != FsGameCardStoragePartition_None) {
m_partition = FsGameCardStoragePartition_None;
fsStorageClose(&m_storage);
}
}
Result Menu::GcStorageReadInternal(void* buf, s64 off, s64 size, u64* bytes_read) {
if (off < m_parition_normal_size) {
size = std::min<s64>(size, m_parition_normal_size - off);
R_TRY(GcMountPartition(FsGameCardStoragePartition_Normal));
} else {
off = off - m_parition_normal_size;
R_TRY(GcMountPartition(FsGameCardStoragePartition_Secure));
}
R_TRY(fsStorageRead(&m_storage, off, buf, size));
*bytes_read = size;
R_SUCCEED();
}
Result Menu::GcStorageRead(void* _buf, s64 off, s64 size) {
auto buf = static_cast<u8*>(_buf);
u64 bytes_read;
u8 data[0x200];
size = std::min(size, m_storage_total_size - off);
if (size <= 0) {
R_SUCCEED();
}
const auto unaligned_off = off % 0x200;
off -= unaligned_off;
if (size > 0 && unaligned_off) {
R_TRY(GcStorageReadInternal(data, off, sizeof(data), &bytes_read));
const auto csize = std::min<s64>(size, 0x200 - unaligned_off);
std::memcpy(buf, data + unaligned_off, csize);
off += bytes_read;
size -= csize;
buf += csize;
}
const auto unaligned_size = size % 0x200;
size -= unaligned_size;
while (size > 0) {
R_TRY(GcStorageReadInternal(buf, off, size, &bytes_read));
off += bytes_read;
size -= bytes_read;
buf += bytes_read;
}
if (unaligned_size) {
R_TRY(GcStorageReadInternal(data, off, sizeof(data), &bytes_read));
const auto csize = std::min<s64>(size, 0x200 - unaligned_size);
std::memcpy(buf, data + unaligned_size, csize);
}
R_SUCCEED();
}
Result Menu::GcPoll(bool* inserted) {
R_TRY(fsDeviceOperatorIsGameCardInserted(&m_dev_op, inserted));
// if the handle changed, re-mount the game card.
if (*inserted && m_mounted) {
FsGameCardHandle handle;
R_TRY(fsDeviceOperatorGetGameCardHandle(std::addressof(m_dev_op), std::addressof(handle)));
if (handle.value != m_handle.value) {
R_TRY(GcMount());
}
}
R_SUCCEED();
}
Result Menu::GcOnEvent() {
bool inserted{};
R_TRY(GcPoll(&inserted));
if (m_mounted != inserted) {
log_write("gc state changed\n");
m_mounted = inserted;
if (m_mounted) {
log_write("trying to mount\n");
m_mounted = R_SUCCEEDED(GcMount());
if (m_mounted) {
App::PlaySoundEffect(SoundEffect::SoundEffect_Startup);
}
} else {
log_write("trying to unmount\n");
GcUnmount();
}
}
R_SUCCEED();
}
Result Menu::UpdateStorageSize() {
fs::FsNativeContentStorage fs_nand{FsContentStorageId_User};
fs::FsNativeContentStorage fs_sd{FsContentStorageId_SdCard};
R_TRY(fs_sd.GetFreeSpace("/", &m_size_free_sd));
R_TRY(fs_sd.GetTotalSpace("/", &m_size_total_sd));
R_TRY(fs_nand.GetFreeSpace("/", &m_size_free_nand));
R_TRY(fs_nand.GetTotalSpace("/", &m_size_total_nand));
R_SUCCEED();
}
void Menu::FreeImage() {
if (m_icon) {
nvgDeleteImage(App::GetVg(), m_icon);
m_icon = 0;
}
}
Result Menu::LoadControlData(ApplicationEntry& e) {
const auto id = e.app_id;
e.control = std::make_unique<NsApplicationControlData>();
if (hosversionBefore(20,0,0)) {
TimeStamp ts;
if (R_SUCCEEDED(nsGetApplicationControlData(NsApplicationControlSource_CacheOnly, id, e.control.get(), sizeof(NsApplicationControlData), &e.control_size))) {
log_write("\t\t[ns control cache] time taken: %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs());
R_SUCCEED();
}
}
// nsGetApplicationControlData() will fail if it's the first time
// mounting a gamecard if the image is not already cached.
// waiting 1-2s after mount, then calling seems to work.
// however, we can just manually parse the nca to get the data we need,
// which always works and *is* faster too ;)
for (const auto& app : e.application) {
for (const auto& collection : app) {
if (collection.type == NcmContentType_Control) {
const auto path = BuildGcPath(collection.name.c_str(), &m_handle);
u64 program_id = id | collection.id_offset;
if (hosversionAtLeast(17, 0, 0)) {
fsGetProgramId(&program_id, path, FsContentAttributes_All);
}
TimeStamp ts;
std::vector<u8> icon;
if (R_SUCCEEDED(nca::ParseControl(path, program_id, &e.control->nacp, sizeof(e.control->nacp), &icon))) {
std::memcpy(e.control->icon, icon.data(), icon.size());
e.control_size = sizeof(e.control->nacp) + icon.size();
log_write("\t\tnca::ParseControl(): %.2fs %zums\n", ts.GetSecondsD(), ts.GetMs());
R_SUCCEED();
} else {
log_write("\tFAILED to parse control nca %s\n", path.s);
}
}
}
}
return 0x1;
}
void Menu::OnChangeIndex(s64 new_index) {
FreeImage();
m_entry_index = new_index;
if (m_entries.empty()) {
this->SetSubHeading("No GameCard inserted");
} else {
const auto index = m_entries.empty() ? 0 : m_entry_index + 1;
this->SetSubHeading(std::to_string(index) + " / " + std::to_string(m_entries.size()));
const auto& e = m_entries[m_entry_index];
const auto jpeg_size = e.control_size - sizeof(NacpStruct);
m_icon = nvgCreateImageMem(App::GetVg(), 0, e.control->icon, jpeg_size);
}
}
Result Menu::DumpGames(u32 flags) {
R_TRY(GcMountStorage());
auto source = std::make_shared<XciSource>();
source->menu = this;
source->application_name = m_entries[m_entry_index].lang_entry.name;
std::vector<fs::FsPath> paths;
if (flags & DumpFileFlag_XCI) {
if (App::GetApp()->m_dump_trim_xci.Get()) {
source->xci_size = m_storage_trimmed_size;
paths.emplace_back(BuildFullDumpPath(DumpFileType_TrimmedXCI, m_entries));
} else {
source->xci_size = m_storage_total_size;
paths.emplace_back(BuildFullDumpPath(DumpFileType_XCI, m_entries));
}
}
if (flags & DumpFileFlag_Set) {
source->id_set.resize(0xC);
R_TRY(fsDeviceOperatorGetGameCardIdSet(&m_dev_op, source->id_set.data(), source->id_set.size(), source->id_set.size()));
paths.emplace_back(BuildFullDumpPath(DumpFileType_Set, m_entries));
}
// todo:
if (flags & DumpFileFlag_UID) {
// paths.emplace_back(BuildFullDumpPath(DumpFileType_UID, m_entries));
}
if (flags & DumpFileFlag_Cert) {
s64 size;
source->cert.resize(0x200);
R_TRY(fsDeviceOperatorGetGameCardDeviceCertificate(&m_dev_op, &m_handle, source->cert.data(), source->cert.size(), &size, source->cert.size()));
paths.emplace_back(BuildFullDumpPath(DumpFileType_Cert, m_entries));
}
// todo:
if (flags & DumpFileFlag_Initial) {
// paths.emplace_back(BuildFullDumpPath(DumpFileType_Initial, m_entries));
}
dump::Dump(source, paths, [this](Result rc){
GcUmountStorage();
GcUnmount();
});
R_SUCCEED();
}
} // namespace sphaira::ui::menu::gc