From 0947e941b83b23b701edb31345c119f14e5ad56f Mon Sep 17 00:00:00 2001 From: Jesse Talavera-Greenberg Date: Sat, 8 Jul 2023 16:17:30 -0400 Subject: [PATCH] Modest cleanups for DSi_NAND (#1714) * Add a definition for TMD files * Wrap TitleMetadata in a namespace * Add a comment * Remove TitleMetadataCertificate - melonDS ignores it anyway * Refactor the use of title metadata - Move bitwise operations on the title ID into helper methods - Use TitleMetadata objects instead of pointers to raw data * Slight cleanup in DSi_NAND - Replace some constants with sizeof - Use an NDSHeader object instead of a raw array of bytes * Add a DSi_NAND::ImportFile overload that loads a file from memory * Split most of ImportTitle into InitTitleFileStructure - It will be reused in the next commit * Add ability to import title from memory * Fix another potential issue * Fix broken DSiWare installation - The bytes of the title ID/category were being swapped in most places, but not all * Add some logging calls * Declare array sizes in DSi_TMD in decimal, not hex * Add a space after the #endif - To adhere to the style guide * Assert the size of TitleMetadataContent * Change the type of SignatureName * Don't mark the TMD structs as packed * Remove extraneous comments * Cut down some newlines --- src/DSi_NAND.cpp | 144 +++++++++++++++++---- src/DSi_NAND.h | 4 +- src/DSi_TMD.h | 119 +++++++++++++++++ src/frontend/qt_sdl/TitleManagerDialog.cpp | 20 +-- src/frontend/qt_sdl/TitleManagerDialog.h | 8 +- 5 files changed, 254 insertions(+), 41 deletions(-) create mode 100644 src/DSi_TMD.h diff --git a/src/DSi_NAND.cpp b/src/DSi_NAND.cpp index 6af2dd21..6adfcffa 100644 --- a/src/DSi_NAND.cpp +++ b/src/DSi_NAND.cpp @@ -664,6 +664,40 @@ void debug_listfiles(const char* path) f_closedir(&dir); } +bool ImportFile(const char* path, const u8* data, size_t len) +{ + if (!data || !len || !path) + return false; + + FF_FIL file; + FRESULT res; + + res = f_open(&file, path, FA_CREATE_ALWAYS | FA_WRITE); + if (res != FR_OK) + { + return false; + } + + u8 buf[0x1000]; + for (u32 i = 0; i < len; i += sizeof(buf)) + { // For each block in the file... + u32 blocklen; + if ((i + sizeof(buf)) > len) + blocklen = len - i; + else + blocklen = sizeof(buf); + + u32 nwrite; + memcpy(buf, data + i, blocklen); + f_write(&file, buf, blocklen, &nwrite); + } + + f_close(&file); + Log(LogLevel::Debug, "Imported file from memory to %s\n", path); + + return true; +} + bool ImportFile(const char* path, const char* in) { FF_FIL file; @@ -686,13 +720,13 @@ bool ImportFile(const char* path, const char* in) } u8 buf[0x1000]; - for (u32 i = 0; i < len; i += 0x1000) + for (u32 i = 0; i < len; i += sizeof(buf)) { u32 blocklen; - if ((i + 0x1000) > len) + if ((i + sizeof(buf)) > len) blocklen = len - i; else - blocklen = 0x1000; + blocklen = sizeof(buf); u32 nwrite; fread(buf, blocklen, 1, fin); @@ -702,6 +736,8 @@ bool ImportFile(const char* path, const char* in) fclose(fin); f_close(&file); + Log(LogLevel::Debug, "Imported file from %s to %s\n", in, path); + return true; } @@ -741,6 +777,8 @@ bool ExportFile(const char* path, const char* out) fclose(fout); f_close(&file); + Log(LogLevel::Debug, "Exported file from %s to %s\n", path, out); + return true; } @@ -754,6 +792,7 @@ void RemoveFile(const char* path) f_chmod(path, 0, AM_RDO); f_unlink(path); + Log(LogLevel::Debug, "Removed file at %s\n", path); } void RemoveDir(const char* path) @@ -807,6 +846,7 @@ void RemoveDir(const char* path) } f_unlink(path); + Log(LogLevel::Debug, "Removed directory at %s\n", path); } @@ -1049,23 +1089,10 @@ bool CreateSaveFile(const char* path, u32 len) return true; } -bool ImportTitle(const char* appfile, u8* tmd, bool readonly) +bool InitTitleFileStructure(const NDSHeader& header, const DSi_TMD::TitleMetadata& tmd, bool readonly) { - u8 header[0x1000]; - { - FILE* f = fopen(appfile, "rb"); - if (!f) return false; - fread(header, 0x1000, 1, f); - fclose(f); - } - - u32 version = (tmd[0x1E4] << 24) | (tmd[0x1E5] << 16) | (tmd[0x1E6] << 8) | tmd[0x1E7]; - Log(LogLevel::Info, ".app version: %08x\n", version); - - u32 titleid0 = (tmd[0x18C] << 24) | (tmd[0x18D] << 16) | (tmd[0x18E] << 8) | tmd[0x18F]; - u32 titleid1 = (tmd[0x190] << 24) | (tmd[0x191] << 16) | (tmd[0x192] << 8) | tmd[0x193]; - Log(LogLevel::Info, "Title ID: %08x/%08x\n", titleid0, titleid1); - + u32 titleid0 = tmd.GetCategory(); + u32 titleid1 = tmd.GetID(); FRESULT res; FF_DIR ticketdir; FF_FILINFO info; @@ -1079,7 +1106,7 @@ bool ImportTitle(const char* appfile, u8* tmd, bool readonly) f_mkdir(fname); sprintf(fname, "0:/ticket/%08x/%08x.tik", titleid0, titleid1); - if (!CreateTicket(fname, *(u32*)&tmd[0x18C], *(u32*)&tmd[0x190], header[0x1E])) + if (!CreateTicket(fname, tmd.GetCategoryNoByteswap(), tmd.GetIDNoByteswap(), header.ROMVersion)) return false; if (readonly) f_chmod(fname, AM_RDO, AM_RDO); @@ -1098,14 +1125,14 @@ bool ImportTitle(const char* appfile, u8* tmd, bool readonly) // data sprintf(fname, "0:/title/%08x/%08x/data/public.sav", titleid0, titleid1); - if (!CreateSaveFile(fname, *(u32*)&header[0x238])) + if (!CreateSaveFile(fname, header.DSiPublicSavSize)) return false; sprintf(fname, "0:/title/%08x/%08x/data/private.sav", titleid0, titleid1); - if (!CreateSaveFile(fname, *(u32*)&header[0x23C])) + if (!CreateSaveFile(fname, header.DSiPrivateSavSize)) return false; - if (header[0x1BF] & 0x04) + if (header.AppFlags & 0x04) { // custom banner file sprintf(fname, "0:/title/%08x/%08x/data/banner.sav", titleid0, titleid1); @@ -1117,8 +1144,8 @@ bool ImportTitle(const char* appfile, u8* tmd, bool readonly) } u8 bannersav[0x4000]; - memset(bannersav, 0, 0x4000); - f_write(&file, bannersav, 0x4000, &nwrite); + memset(bannersav, 0, sizeof(bannersav)); + f_write(&file, bannersav, sizeof(bannersav), &nwrite); f_close(&file); } @@ -1133,18 +1160,81 @@ bool ImportTitle(const char* appfile, u8* tmd, bool readonly) return false; } - f_write(&file, tmd, 0x208, &nwrite); + f_write(&file, &tmd, sizeof(DSi_TMD::TitleMetadata), &nwrite); f_close(&file); if (readonly) f_chmod(fname, AM_RDO, AM_RDO); + return true; +} + +bool ImportTitle(const char* appfile, const DSi_TMD::TitleMetadata& tmd, bool readonly) +{ + NDSHeader header {}; + { + FILE* f = fopen(appfile, "rb"); + if (!f) return false; + fread(&header, sizeof(header), 1, f); + fclose(f); + } + + u32 version = tmd.Contents.GetVersion(); + Log(LogLevel::Info, ".app version: %08x\n", version); + + u32 titleid0 = tmd.GetCategory(); + u32 titleid1 = tmd.GetID(); + Log(LogLevel::Info, "Title ID: %08x/%08x\n", titleid0, titleid1); + + if (!InitTitleFileStructure(header, tmd, readonly)) + { + Log(LogLevel::Error, "ImportTitle: failed to initialize file structure for imported title\n"); + return false; + } + // executable + char fname[128]; sprintf(fname, "0:/title/%08x/%08x/content/%08x.app", titleid0, titleid1, version); if (!ImportFile(fname, appfile)) { - Log(LogLevel::Error, "ImportTitle: failed to create executable (%d)\n", res); + Log(LogLevel::Error, "ImportTitle: failed to create executable\n"); + return false; + } + + if (readonly) f_chmod(fname, AM_RDO, AM_RDO); + + return true; +} + +bool ImportTitle(const u8* app, size_t appLength, const DSi_TMD::TitleMetadata& tmd, bool readonly) +{ + if (!app || appLength < sizeof(NDSHeader)) + return false; + + NDSHeader header {}; + memcpy(&header, app, sizeof(header)); + + u32 version = tmd.Contents.GetVersion(); + Log(LogLevel::Info, ".app version: %08x\n", version); + + u32 titleid0 = tmd.GetCategory(); + u32 titleid1 = tmd.GetID(); + Log(LogLevel::Info, "Title ID: %08x/%08x\n", titleid0, titleid1); + + if (!InitTitleFileStructure(header, tmd, readonly)) + { + Log(LogLevel::Error, "ImportTitle: failed to initialize file structure for imported title\n"); + return false; + } + + // executable + + char fname[128]; + sprintf(fname, "0:/title/%08x/%08x/content/%08x.app", titleid0, titleid1, version); + if (!ImportFile(fname, app, appLength)) + { + Log(LogLevel::Error, "ImportTitle: failed to create executable\n"); return false; } diff --git a/src/DSi_NAND.h b/src/DSi_NAND.h index a23e62f6..76d5ee2b 100644 --- a/src/DSi_NAND.h +++ b/src/DSi_NAND.h @@ -21,6 +21,7 @@ #include "types.h" #include "NDS_Header.h" +#include "DSi_TMD.h" #include #include @@ -49,7 +50,8 @@ void PatchUserData(); void ListTitles(u32 category, std::vector& titlelist); bool TitleExists(u32 category, u32 titleid); void GetTitleInfo(u32 category, u32 titleid, u32& version, NDSHeader* header, NDSBanner* banner); -bool ImportTitle(const char* appfile, u8* tmd, bool readonly); +bool ImportTitle(const char* appfile, const DSi_TMD::TitleMetadata& tmd, bool readonly); +bool ImportTitle(const u8* app, size_t appLength, const DSi_TMD::TitleMetadata& tmd, bool readonly); void DeleteTitle(u32 category, u32 titleid); u32 GetTitleDataMask(u32 category, u32 titleid); diff --git a/src/DSi_TMD.h b/src/DSi_TMD.h new file mode 100644 index 00000000..12226c55 --- /dev/null +++ b/src/DSi_TMD.h @@ -0,0 +1,119 @@ +/* + Copyright 2016-2023 melonDS team + + This file is part of melonDS. + + melonDS is free software: you can redistribute it and/or modify it under + the terms of the GNU General Public License as published by the Free + Software Foundation, either version 3 of the License, or (at your option) + any later version. + + melonDS is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with melonDS. If not, see http://www.gnu.org/licenses/. +*/ + +#ifndef DSI_TMD_H +#define DSI_TMD_H + +#include "types.h" +#include + +namespace DSi_TMD +{ + +struct TitleMetadataContent { + u8 ContentId[4]; /// Content ID (00,00,00,vv) ;lowercase/hex ;"0000000vv.app" + u8 ContentIndex[2]; /// Content Index (00,00) + u8 ContentType[2]; /// Content Type (00,01) ;aka DSi .app + u8 ContentSize[8]; /// Content Size (00,00,00,00,00,19,E4,00) + u8 ContentSha1Hash[20]; /// Content SHA1 (on decrypted ".app" file) + + [[nodiscard]] u32 GetVersion() const noexcept + { + return (ContentId[0] << 24) | (ContentId[1] << 16) | (ContentId[2] << 8) | ContentId[3]; + } +}; + +static_assert(sizeof(TitleMetadataContent) == 36, "TitleMetadataContent is not 36 bytes!"); + +/// Metadata for a DSiWare title. +/// Used to install DSiWare titles to NAND. +/// @see https://problemkaputt.de/gbatek.htm#dsisdmmcdsiwareticketsandtitlemetadata +struct TitleMetadata +{ + u32 SignatureType; + u8 Signature[256]; + u8 SignatureAlignment[60]; + char SignatureName[64]; + + u8 TmdVersion; + u8 CaCrlVersion; + u8 SignerCrlVersion; + u8 Padding0; + + u8 SystemVersion[8]; + u8 TitleId[8]; + u32 TitleType; + u8 GroupId[2]; + u8 PublicSaveSize[4]; + u8 PrivateSaveSize[4]; + u8 Padding1[4]; + + u8 SrlFlag; + u8 Padding2[3]; + + u8 AgeRatings[16]; + u8 Padding3[30]; + + u32 AccessRights; + u16 TitleVersion; + + u16 NumberOfContents; /// There's always one or zero content entries in practice + u16 BootContentIndex; + u8 Padding4[2]; + + TitleMetadataContent Contents; + + [[nodiscard]] bool HasPublicSaveData() const noexcept { return GetPublicSaveSize() != 0; } + [[nodiscard]] bool HasPrivateSaveData() const noexcept { return GetPrivateSaveSize() != 0; } + + [[nodiscard]] u32 GetPublicSaveSize() const noexcept + { + return (PublicSaveSize[0] << 24) | (PublicSaveSize[1] << 16) | (PublicSaveSize[2] << 8) | PublicSaveSize[3]; + } + + [[nodiscard]] u32 GetPrivateSaveSize() const noexcept + { + return (PrivateSaveSize[0] << 24) | (PrivateSaveSize[1] << 16) | (PrivateSaveSize[2] << 8) | PrivateSaveSize[3]; + } + + [[nodiscard]] u32 GetCategory() const noexcept + { + return (TitleId[0] << 24) | (TitleId[1] << 16) | (TitleId[2] << 8) | TitleId[3]; + } + + [[nodiscard]] u32 GetCategoryNoByteswap() const noexcept + { + return reinterpret_cast(TitleId); + } + + [[nodiscard]] u32 GetID() const noexcept + { + return (TitleId[4] << 24) | (TitleId[5] << 16) | (TitleId[6] << 8) | TitleId[7]; + } + + [[nodiscard]] u32 GetIDNoByteswap() const noexcept + { + return *reinterpret_cast(&TitleId[4]); + } +}; + +static_assert(sizeof(TitleMetadata) == 520, "TitleMetadata is not 520 bytes!"); + +} + +#endif // DSI_TMD_H diff --git a/src/frontend/qt_sdl/TitleManagerDialog.cpp b/src/frontend/qt_sdl/TitleManagerDialog.cpp index 72d19ece..3d52bddf 100644 --- a/src/frontend/qt_sdl/TitleManagerDialog.cpp +++ b/src/frontend/qt_sdl/TitleManagerDialog.cpp @@ -176,7 +176,7 @@ void TitleManagerDialog::done(int r) void TitleManagerDialog::on_btnImportTitle_clicked() { - TitleImportDialog* importdlg = new TitleImportDialog(this, importAppPath, importTmdData, importReadOnly); + TitleImportDialog* importdlg = new TitleImportDialog(this, importAppPath, &importTmdData, importReadOnly); importdlg->open(); connect(importdlg, &TitleImportDialog::finished, this, &TitleManagerDialog::onImportTitleFinished); @@ -188,8 +188,8 @@ void TitleManagerDialog::onImportTitleFinished(int res) if (res != QDialog::Accepted) return; u32 titleid[2]; - titleid[0] = (importTmdData[0x18C] << 24) | (importTmdData[0x18D] << 16) | (importTmdData[0x18E] << 8) | importTmdData[0x18F]; - titleid[1] = (importTmdData[0x190] << 24) | (importTmdData[0x191] << 16) | (importTmdData[0x192] << 8) | importTmdData[0x193]; + titleid[0] = importTmdData.GetCategory(); + titleid[1] = importTmdData.GetID(); // remove anything that might hinder the install DSi_NAND::DeleteTitle(titleid[0], titleid[1]); @@ -381,7 +381,7 @@ void TitleManagerDialog::onExportTitleData() } -TitleImportDialog::TitleImportDialog(QWidget* parent, QString& apppath, u8* tmd, bool& readonly) +TitleImportDialog::TitleImportDialog(QWidget* parent, QString& apppath, const DSi_TMD::TitleMetadata* tmd, bool& readonly) : QDialog(parent), ui(new Ui::TitleImportDialog), appPath(apppath), tmdData(tmd), readOnly(readonly) { ui->setupUi(this); @@ -440,12 +440,12 @@ void TitleImportDialog::accept() return; } - fread(tmdData, 0x208, 1, f); + fread((void *) tmdData, sizeof(DSi_TMD::TitleMetadata), 1, f); fclose(f); u32 tmdtitleid[2]; - tmdtitleid[0] = (tmdData[0x18C] << 24) | (tmdData[0x18D] << 16) | (tmdData[0x18E] << 8) | tmdData[0x18F]; - tmdtitleid[1] = (tmdData[0x190] << 24) | (tmdData[0x191] << 16) | (tmdData[0x192] << 8) | tmdData[0x193]; + tmdtitleid[0] = tmdData->GetCategory(); + tmdtitleid[1] = tmdData->GetID(); if (tmdtitleid[1] != titleid[0] || tmdtitleid[0] != titleid[1]) { @@ -507,11 +507,11 @@ void TitleImportDialog::tmdDownloaded() } else { - netreply->read((char*)tmdData, 520); + netreply->read((char*)tmdData, sizeof(*tmdData)); u32 tmdtitleid[2]; - tmdtitleid[0] = (tmdData[0x18C] << 24) | (tmdData[0x18D] << 16) | (tmdData[0x18E] << 8) | tmdData[0x18F]; - tmdtitleid[1] = (tmdData[0x190] << 24) | (tmdData[0x191] << 16) | (tmdData[0x192] << 8) | tmdData[0x193]; + tmdtitleid[0] = tmdData->GetCategory(); + tmdtitleid[1] = tmdData->GetID(); if (tmdtitleid[1] != titleid[0] || tmdtitleid[0] != titleid[1]) { diff --git a/src/frontend/qt_sdl/TitleManagerDialog.h b/src/frontend/qt_sdl/TitleManagerDialog.h index cba70470..fc92fd81 100644 --- a/src/frontend/qt_sdl/TitleManagerDialog.h +++ b/src/frontend/qt_sdl/TitleManagerDialog.h @@ -29,6 +29,8 @@ #include #include +#include "DSi_TMD.h" + namespace Ui { class TitleManagerDialog; @@ -90,7 +92,7 @@ private: Ui::TitleManagerDialog* ui; QString importAppPath; - u8 importTmdData[0x208]; + DSi_TMD::TitleMetadata importTmdData; bool importReadOnly; QAction* actImportTitleData[3]; @@ -104,7 +106,7 @@ class TitleImportDialog : public QDialog Q_OBJECT public: - explicit TitleImportDialog(QWidget* parent, QString& apppath, u8* tmd, bool& readonly); + explicit TitleImportDialog(QWidget* parent, QString& apppath, const DSi_TMD::TitleMetadata* tmd, bool& readonly); ~TitleImportDialog(); private slots: @@ -124,7 +126,7 @@ private: QNetworkReply* netreply; QString& appPath; - u8* tmdData; + const DSi_TMD::TitleMetadata* tmdData; bool& readOnly; u32 titleid[2];