diff --git a/src/DSi_NAND.cpp b/src/DSi_NAND.cpp index 58a0d278..304ede89 100644 --- a/src/DSi_NAND.cpp +++ b/src/DSi_NAND.cpp @@ -509,22 +509,25 @@ void debug_listfiles(const char* path) f_closedir(&dir); } -void DumpFile(const char* path, const char* out) +bool ImportFile(const char* path, const char* in) { FIL file; - FILE* fout; + FILE* fin; FRESULT res; - res = f_open(&file, path, FA_OPEN_EXISTING | FA_READ); - if (res != FR_OK) return; + fin = fopen(in, "rb"); + if (!fin) + return false; - u32 len = f_size(&file); + fseek(fin, 0, SEEK_END); + u32 len = (u32)ftell(fin); + fseek(fin, 0, SEEK_SET); - fout = fopen(out, "wb"); - if (!fout) + res = f_open(&file, path, FA_CREATE_ALWAYS | FA_WRITE); + if (res != FR_OK) { - f_close(&file); - return; + fclose(fin); + return false; } u8 buf[0x200]; @@ -536,13 +539,54 @@ void DumpFile(const char* path, const char* out) else blocklen = 0x200; - u32 burp; - f_read(&file, buf, blocklen, &burp); + u32 nwrite; + fread(buf, blocklen, 1, fin); + f_write(&file, buf, blocklen, &nwrite); + } + + fclose(fin); + f_close(&file); + + return true; +} + +bool ExportFile(const char* path, const char* out) +{ + FIL file; + FILE* fout; + FRESULT res; + + res = f_open(&file, path, FA_OPEN_EXISTING | FA_READ); + if (res != FR_OK) + return false; + + u32 len = f_size(&file); + + fout = fopen(out, "wb"); + if (!fout) + { + f_close(&file); + return false; + } + + u8 buf[0x200]; + for (u32 i = 0; i < len; i += 0x200) + { + u32 blocklen; + if ((i + 0x200) > len) + blocklen = len - i; + else + blocklen = 0x200; + + u32 nread; + f_read(&file, buf, blocklen, &nread); fwrite(buf, blocklen, 1, fout); } fclose(fout); f_close(&file); + + return true; } void RemoveFile(const char* path) @@ -692,6 +736,9 @@ bool TitleExists(u32 category, u32 titleid) void GetTitleInfo(u32 category, u32 titleid, u32& version, u8* header, u8* banner) { version = GetTitleVersion(category, titleid); + if (version == 0xFFFFFFFF) + return; + FRESULT res; char path[256]; @@ -704,15 +751,18 @@ void GetTitleInfo(u32 category, u32 titleid, u32& version, u8* header, u8* banne u32 nread; f_read(&file, header, 0x1000, &nread); - u32 banneraddr = *(u32*)&header[0x68]; - if (!banneraddr) + if (banner) { - memset(banner, 0, 0x2400); - } - else - { - f_lseek(&file, banneraddr); - f_read(&file, banner, 0x2400, &nread); + u32 banneraddr = *(u32*)&header[0x68]; + if (!banneraddr) + { + memset(banner, 0, 0x2400); + } + else + { + f_lseek(&file, banneraddr); + f_read(&file, banner, 0x2400, &nread); + } } f_close(&file); @@ -916,29 +966,12 @@ bool ImportTitle(const char* appfile, u8* tmd, bool readonly) // executable sprintf(fname, "0:/title/%08x/%08x/content/%08x.app", titleid0, titleid1, version); - res = f_open(&file, fname, FA_CREATE_ALWAYS | FA_WRITE); - if (res != FR_OK) + if (!ImportFile(fname, appfile)) { printf("ImportTitle: failed to create executable (%d)\n", res); return false; } - FILE* app = fopen(appfile, "rb"); - fseek(app, 0, SEEK_END); - u32 applen = (u32)ftell(app); - fseek(app, 0, SEEK_SET); - - for (u32 i = 0; i < applen; i += 0x200) - { - u8 data[0x200]; - - u32 lenread = fread(data, 1, 0x200, app); - f_write(&file, data, lenread, &nwrite); - } - - fclose(app); - f_close(&file); - if (readonly) f_chmod(fname, AM_RDO, AM_RDO); return true; @@ -955,4 +988,72 @@ void DeleteTitle(u32 category, u32 titleid) RemoveDir(fname); } +u32 GetTitleDataMask(u32 category, u32 titleid) +{ + u32 version; + u8 header[0x1000]; + + GetTitleInfo(category, titleid, version, header, nullptr); + if (version == 0xFFFFFFFF) + return 0; + + u32 ret = 0; + if (*(u32*)&header[0x238] != 0) ret |= (1 << TitleData_PublicSav); + if (*(u32*)&header[0x23C] != 0) ret |= (1 << TitleData_PrivateSav); + if (header[0x1BF] & 0x04) ret |= (1 << TitleData_BannerSav); + + return ret; +} + +bool ImportTitleData(u32 category, u32 titleid, int type, const char* file) +{ + char fname[128]; + + switch (type) + { + case TitleData_PublicSav: + sprintf(fname, "0:/title/%08x/%08x/data/public.sav", category, titleid); + break; + + case TitleData_PrivateSav: + sprintf(fname, "0:/title/%08x/%08x/data/private.sav", category, titleid); + break; + + case TitleData_BannerSav: + sprintf(fname, "0:/title/%08x/%08x/data/banner.sav", category, titleid); + break; + + default: + return false; + } + + RemoveFile(fname); + return ImportFile(fname, file); +} + +bool ExportTitleData(u32 category, u32 titleid, int type, const char* file) +{ + char fname[128]; + + switch (type) + { + case TitleData_PublicSav: + sprintf(fname, "0:/title/%08x/%08x/data/public.sav", category, titleid); + break; + + case TitleData_PrivateSav: + sprintf(fname, "0:/title/%08x/%08x/data/private.sav", category, titleid); + break; + + case TitleData_BannerSav: + sprintf(fname, "0:/title/%08x/%08x/data/banner.sav", category, titleid); + break; + + default: + return false; + } + + return ExportFile(fname, file); +} + } diff --git a/src/DSi_NAND.h b/src/DSi_NAND.h index 41898928..fbd75934 100644 --- a/src/DSi_NAND.h +++ b/src/DSi_NAND.h @@ -26,6 +26,13 @@ namespace DSi_NAND { +enum +{ + TitleData_PublicSav, + TitleData_PrivateSav, + TitleData_BannerSav, +}; + bool Init(FILE* nand, u8* es_keyY); void DeInit(); @@ -39,6 +46,10 @@ void GetTitleInfo(u32 category, u32 titleid, u32& version, u8* header, u8* banne bool ImportTitle(const char* appfile, u8* tmd, bool readonly); void DeleteTitle(u32 category, u32 titleid); +u32 GetTitleDataMask(u32 category, u32 titleid); +bool ImportTitleData(u32 category, u32 titleid, int type, const char* file); +bool ExportTitleData(u32 category, u32 titleid, int type, const char* file); + } #endif // DSI_NAND_H diff --git a/src/frontend/qt_sdl/TitleImportDialog.ui b/src/frontend/qt_sdl/TitleImportDialog.ui index 4fa78cab..ca2010b6 100644 --- a/src/frontend/qt_sdl/TitleImportDialog.ui +++ b/src/frontend/qt_sdl/TitleImportDialog.ui @@ -94,6 +94,9 @@ + + <html><head/><body><p>Makes the title executable and TMD read-only. Prevents DSi system utilities from deleting them.</p></body></html> + Make title files read-only diff --git a/src/frontend/qt_sdl/TitleManagerDialog.cpp b/src/frontend/qt_sdl/TitleManagerDialog.cpp index ff0cec34..117b8b94 100644 --- a/src/frontend/qt_sdl/TitleManagerDialog.cpp +++ b/src/frontend/qt_sdl/TitleManagerDialog.cpp @@ -18,6 +18,7 @@ #include #include +#include #include "types.h" #include "Platform.h" @@ -59,6 +60,42 @@ TitleManagerDialog::TitleManagerDialog(QWidget* parent) : QDialog(parent), ui(ne ui->btnImportTitleData->setEnabled(false); ui->btnExportTitleData->setEnabled(false); ui->btnDeleteTitle->setEnabled(false); + + { + QMenu* menu = new QMenu(ui->btnImportTitleData); + + actImportTitleData[0] = menu->addAction("public.sav"); + actImportTitleData[0]->setData(QVariant(DSi_NAND::TitleData_PublicSav)); + connect(actImportTitleData[0], &QAction::triggered, this, &TitleManagerDialog::onImportTitleData); + + actImportTitleData[1] = menu->addAction("private.sav"); + actImportTitleData[1]->setData(QVariant(DSi_NAND::TitleData_PrivateSav)); + connect(actImportTitleData[1], &QAction::triggered, this, &TitleManagerDialog::onImportTitleData); + + actImportTitleData[2] = menu->addAction("banner.sav"); + actImportTitleData[2]->setData(QVariant(DSi_NAND::TitleData_BannerSav)); + connect(actImportTitleData[2], &QAction::triggered, this, &TitleManagerDialog::onImportTitleData); + + ui->btnImportTitleData->setMenu(menu); + } + + { + QMenu* menu = new QMenu(ui->btnExportTitleData); + + actExportTitleData[0] = menu->addAction("public.sav"); + actExportTitleData[0]->setData(QVariant(DSi_NAND::TitleData_PublicSav)); + connect(actExportTitleData[0], &QAction::triggered, this, &TitleManagerDialog::onExportTitleData); + + actExportTitleData[1] = menu->addAction("private.sav"); + actExportTitleData[1]->setData(QVariant(DSi_NAND::TitleData_PrivateSav)); + connect(actExportTitleData[1], &QAction::triggered, this, &TitleManagerDialog::onExportTitleData); + + actExportTitleData[2] = menu->addAction("banner.sav"); + actExportTitleData[2]->setData(QVariant(DSi_NAND::TitleData_BannerSav)); + connect(actExportTitleData[2], &QAction::triggered, this, &TitleManagerDialog::onExportTitleData); + + ui->btnExportTitleData->setMenu(menu); + } } TitleManagerDialog::~TitleManagerDialog() @@ -99,6 +136,9 @@ void TitleManagerDialog::createTitleItem(u32 category, u32 titleid) QListWidgetItem* item = new QListWidgetItem(title + QString(extra)); item->setIcon(icon); item->setData(Qt::UserRole, (((u64)category<<32) | (u64)titleid)); + item->setData(Qt::UserRole+1, *(u32*)&header[0x238]); // public.sav size + item->setData(Qt::UserRole+2, *(u32*)&header[0x23C]); // private.sav size + item->setData(Qt::UserRole+3, (u32)((header[0x1BF] & 0x04) ? 0x4000 : 0)); // banner.sav size ui->lstTitleList->addItem(item); } @@ -183,16 +223,6 @@ void TitleManagerDialog::onImportTitleFinished(int res) } } -void TitleManagerDialog::on_btnImportTitleData_clicked() -{ - // -} - -void TitleManagerDialog::on_btnExportTitleData_clicked() -{ - // -} - void TitleManagerDialog::on_btnDeleteTitle_clicked() { QListWidgetItem* cur = ui->lstTitleList->currentItem(); @@ -205,7 +235,7 @@ void TitleManagerDialog::on_btnDeleteTitle_clicked() QMessageBox::No) != QMessageBox::Yes) return; - u64 titleid = cur->data(Qt::UserRole).toLongLong(); + u64 titleid = cur->data(Qt::UserRole).toULongLong(); DSi_NAND::DeleteTitle((u32)(titleid >> 32), (u32)titleid); delete cur; @@ -225,7 +255,125 @@ void TitleManagerDialog::on_lstTitleList_currentItemChanged(QListWidgetItem* cur ui->btnExportTitleData->setEnabled(true); ui->btnDeleteTitle->setEnabled(true); - // + u32 val; + val = cur->data(Qt::UserRole+1).toUInt(); + actImportTitleData[0]->setEnabled(val != 0); + actExportTitleData[0]->setEnabled(val != 0); + val = cur->data(Qt::UserRole+2).toUInt(); + actImportTitleData[1]->setEnabled(val != 0); + actExportTitleData[1]->setEnabled(val != 0); + val = cur->data(Qt::UserRole+3).toUInt(); + actImportTitleData[2]->setEnabled(val != 0); + actExportTitleData[2]->setEnabled(val != 0); + } +} + +void TitleManagerDialog::onImportTitleData() +{ + int type = ((QAction*)sender())->data().toInt(); + + QListWidgetItem* cur = ui->lstTitleList->currentItem(); + if (!cur) + { + printf("what??\n"); + return; + } + + u32 wantedsize; + switch (type) + { + case DSi_NAND::TitleData_PublicSav: wantedsize = cur->data(Qt::UserRole+1).toUInt(); break; + case DSi_NAND::TitleData_PrivateSav: wantedsize = cur->data(Qt::UserRole+2).toUInt(); break; + case DSi_NAND::TitleData_BannerSav: wantedsize = cur->data(Qt::UserRole+3).toUInt(); break; + default: + printf("what??\n"); + return; + } + + QString file = QFileDialog::getOpenFileName(this, + "Select file to import...", + EmuDirectory, + "Title data files (*.sav);;Any file (*.*)"); + + if (file.isEmpty()) return; + + FILE* f = fopen(file.toStdString().c_str(), "rb"); + if (!f) + { + QMessageBox::critical(this, + "Import title data - melonDS", + "Could not open data file.\nCheck that the file is accessible."); + return; + } + + fseek(f, 0, SEEK_END); + u64 len = ftell(f); + fclose(f); + + if (len != wantedsize) + { + QMessageBox::critical(this, + "Import title data - melonDS", + QString("Cannot import this data file: size is incorrect (expected: %1 bytes).").arg(wantedsize)); + return; + } + + u64 titleid = cur->data(Qt::UserRole).toULongLong(); + bool res = DSi_NAND::ImportTitleData((u32)(titleid >> 32), (u32)titleid, type, file.toStdString().c_str()); + if (!res) + { + QMessageBox::critical(this, + "Import title data - melonDS", + "Failed to import the data file. Check that your NAND is accessible and valid."); + } +} + +void TitleManagerDialog::onExportTitleData() +{ + int type = ((QAction*)sender())->data().toInt(); + + QListWidgetItem* cur = ui->lstTitleList->currentItem(); + if (!cur) + { + printf("what??\n"); + return; + } + + QString exportname; + u32 wantedsize; + switch (type) + { + case DSi_NAND::TitleData_PublicSav: + exportname = "/public.sav"; + wantedsize = cur->data(Qt::UserRole+1).toUInt(); + break; + case DSi_NAND::TitleData_PrivateSav: + exportname = "/private.sav"; + wantedsize = cur->data(Qt::UserRole+2).toUInt(); + break; + case DSi_NAND::TitleData_BannerSav: + exportname = "/banner.sav"; + wantedsize = cur->data(Qt::UserRole+3).toUInt(); + break; + default: + printf("what??\n"); + return; + } + + QString file = QFileDialog::getSaveFileName(this, + "Select path to export to...", + QString(EmuDirectory) + exportname, + "Title data files (*.sav);;Any file (*.*)"); + + if (file.isEmpty()) return; + + u64 titleid = cur->data(Qt::UserRole).toULongLong(); + bool res = DSi_NAND::ExportTitleData((u32)(titleid >> 32), (u32)titleid, type, file.toStdString().c_str()); + if (!res) + { + QMessageBox::critical(this, + "Export title data - melonDS", + "Failed to Export the data file. Check that the destination directory is writable."); } } diff --git a/src/frontend/qt_sdl/TitleManagerDialog.h b/src/frontend/qt_sdl/TitleManagerDialog.h index e0f80cb8..682362ab 100644 --- a/src/frontend/qt_sdl/TitleManagerDialog.h +++ b/src/frontend/qt_sdl/TitleManagerDialog.h @@ -81,10 +81,10 @@ private slots: void on_btnImportTitle_clicked(); void onImportTitleFinished(int res); - void on_btnImportTitleData_clicked(); - void on_btnExportTitleData_clicked(); void on_btnDeleteTitle_clicked(); void on_lstTitleList_currentItemChanged(QListWidgetItem* cur, QListWidgetItem* prev); + void onImportTitleData(); + void onExportTitleData(); private: Ui::TitleManagerDialog* ui; @@ -93,8 +93,8 @@ private: u8 importTmdData[0x208]; bool importReadOnly; - QAction* importAction[3]; - QAction* exportAction[3]; + QAction* actImportTitleData[3]; + QAction* actExportTitleData[3]; void createTitleItem(u32 category, u32 titleid); }; diff --git a/src/frontend/qt_sdl/TitleManagerDialog.ui b/src/frontend/qt_sdl/TitleManagerDialog.ui index 3d177d72..be52752d 100644 --- a/src/frontend/qt_sdl/TitleManagerDialog.ui +++ b/src/frontend/qt_sdl/TitleManagerDialog.ui @@ -44,7 +44,7 @@ <html><head/><body><p>Import data (save, banner...) for the selected title.</p></body></html> - Import title data... + Import title data @@ -54,7 +54,7 @@ <html><head/><body><p>Export the data (save, banner...) associated with the selected title.</p></body></html> - Export title data... + Export title data