aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/DSi_NAND.cpp144
-rw-r--r--src/DSi_NAND.h4
-rw-r--r--src/DSi_TMD.h119
-rw-r--r--src/frontend/qt_sdl/TitleManagerDialog.cpp20
-rw-r--r--src/frontend/qt_sdl/TitleManagerDialog.h8
5 files changed, 254 insertions, 41 deletions
diff --git a/src/DSi_NAND.cpp b/src/DSi_NAND.cpp
index 6af2dd2..6adfcff 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 a23e62f..76d5ee2 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 <vector>
#include <string>
@@ -49,7 +50,8 @@ void PatchUserData();
void ListTitles(u32 category, std::vector<u32>& 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 0000000..12226c5
--- /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 <array>
+
+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<const u32&>(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<const u32*>(&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 72d19ec..3d52bdd 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 cba7047..fc92fd8 100644
--- a/src/frontend/qt_sdl/TitleManagerDialog.h
+++ b/src/frontend/qt_sdl/TitleManagerDialog.h
@@ -29,6 +29,8 @@
#include <QNetworkReply>
#include <QNetworkAccessManager>
+#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];