From ec81e2bfbeb4f19fca5ad84935f1d7e34aee55c0 Mon Sep 17 00:00:00 2001 From: NyuBlara <61560065+NyuBlara@users.noreply.github.com> Date: Thu, 17 Nov 2022 02:46:13 +0900 Subject: [PATCH] Feat/fat decoding (#14) * First step in implementing FAT file decoding : brute-force extraction without FNT lookup. The FAT alone contains enough information to extract every file in a folder chosen by the user. However, reference to the file name table (FNT) is needed to restore the files' names and the directory structure (and avoid dumping useless data that may be left over in the data section). This commit only implements the dumping of all data by incrementing through the FAT and writing every section marked by a FAT range into a separate file, giving them incremental names. The next commit will use the FNT to name them properly. * Proper FAT decoding : FNT lookup and directory structure Code comments explain the algorithm (which was understood thanks to the wonderfully simple "FNT-Tool" script at : https://github.com/RoadrunnerWMC/FNT-Tool) ; the implementation is more or less a C++ port of the Python code. Everything may still be a little dirty, but it works ! * Updated README! * Updated README! * MINOR compliance modifications - Removed extra entry in .gitignore - Removed superfluous debug inclusions - Replaced magic values by preprocessor macros Co-authored-by: rblard --- .gitignore | 2 + README.md | 14 +- ndsfactory/ndsfactory.cpp | 6 + ndsfactory/ndsfactory.h | 2 + ui/dialogs/about/revision.h | 2 +- ui/mainwindow.h | 2 +- ui/tabs/unpacker/unpackertabfunctions.cpp | 162 +++++++++++++++++++++- ui/tabs/unpacker/unpackertabsignals.cpp | 13 +- 8 files changed, 185 insertions(+), 18 deletions(-) diff --git a/.gitignore b/.gitignore index 191e487..807c0c4 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,5 @@ CMakeLists.txt.user* # OS specific .DS_Store + +build/ diff --git a/README.md b/README.md index 810b3c6..78a96ea 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,7 @@ [![GPL Licence](https://badges.frapsoft.com/os/gpl/gpl.png?v=103)](https://opensource.org/licenses/GPL-3.0/) -A tool to unpack & repack Nintendo DS roms (.nds) - +A tool to unpack & repack Nintendo DS ROMs (.nds) If you find this software useful, please [![ko-fi](https://www.ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/Z8Z511SOI) @@ -27,8 +26,8 @@ Basically an NDS software is composed of the following sections: * Icon/Title Logo * FAT Files (The actual files used by the software, like Graphics, Music etc.) -With NDSFactory you can extract these sections, modify them using your prefered way and the rebuild the rom with your edited sections. -If the modified sections are bigger than the original ones, you can specify their new physical adddress and size in the header: if so, **make +With NDSFactory you can extract these sections, modify them using your preferred way and the rebuild the rom with your edited sections. +If the modified sections are bigger than the original ones, you can specify their new physical address and size in the header: if so, **make sure that they DON'T OVERLAP, and remember to PATCH THE FAT.BIN** (more on this later). **This software will be particularly useful if you want to mod your games or write a trainer for them.** @@ -36,24 +35,22 @@ sure that they DON'T OVERLAP, and remember to PATCH THE FAT.BIN** (more on this # How to use it ## Unpacker Tab -In the upacker tab you can load your Nintendo DS software (.nds) and then you can extract the rom sections. +In the unpacker tab you can load your Nintendo DS software (.nds) and then you can extract the ROM sections, including the individual FAT files. Please note the Original Address of the FAT Files, you will need this value later if you are going to alter the addresses and size of the sections. **You can then do what you want with these sections (inject code, apply patches etc.)** ## Packer Tab -In the packer tab you can re-create an .nds file using your edited sections. If your sections are bigger than the originals, you have to edit their addresses and size (in the header). **Make sure that the addresses don't overlap, or the final rom will be broken**. If you are repacking edited sections, and the FAT Files Address is different than the original one, **make sure to patch the FAT (fat.bin)**: the FAT is a list of absolute addresses (representing each file start adddress and end andress), so you need to update them (you can easily do this using the FAT Patching Tab). +In the packer tab you can re-create an .nds file using your edited sections. If your sections are bigger than the originals, you have to edit their addresses and size (in the header). **Make sure that the addresses don't overlap, or the final rom will be broken**. If you are repacking edited sections, and the FAT Files Address is different than the original one, **make sure to patch the FAT (fat.bin)**: the FAT is a list of absolute addresses (representing each file start address and end address), so you need to update them (you can easily do this using the FAT Patching Tab). ## Fat Patching Tab In this tab you can easily patch the FAT section (fat.bin). You have to do this only if the FAT Files (fat_data.bin) final address is different than the original one. Patching the FAT is easy, all you have to do is load your fat.bin, and fill the original address and the new address of fat_data.bin. This will produce a patched fat.bin that you can use in the packing process. - # Known Limitations/Possible Future Features/Bugs * Add support for roms with OVERLAY -* Add support to decode FAT Files (extract single files one by one) * Add support to rebuild a new fat_data.bin and fat.bin from a set of files inside a directory * Design a nice logo/icon @@ -61,4 +58,3 @@ If you found a bug, feel free to open an issue or send a PR :) ### Developed with ❤ by Luca D'Amico ### Special thanks to Antonio Barba & Davide Trogu - diff --git a/ndsfactory/ndsfactory.cpp b/ndsfactory/ndsfactory.cpp index 0647f40..0c9476a 100644 --- a/ndsfactory/ndsfactory.cpp +++ b/ndsfactory/ndsfactory.cpp @@ -80,6 +80,12 @@ bool NDSFactory::writeSectionToFile(const std::string& sectionPath, const std::s return false; } +bool NDSFactory::writeFatSectionToFile(const std::string& romPath, FatRange* pfatrange, const std::string& savePath){ + uint32_t size=pfatrange->endAddr-pfatrange->startAddr; + if(!dumpDataFromFile(romPath, savePath, pfatrange->startAddr, size)) return false; + return true; +} + bool NDSFactory::writeBytesToFile(std::vector& byteBuffer, const std::string& savePath, uint32_t startAddr, uint32_t size) { std::ofstream savedFile (savePath, std::ios::out|std::ios::binary|std::ios::app); diff --git a/ndsfactory/ndsfactory.h b/ndsfactory/ndsfactory.h index cad54eb..25aea53 100644 --- a/ndsfactory/ndsfactory.h +++ b/ndsfactory/ndsfactory.h @@ -5,6 +5,7 @@ #include #include #include "ndsheader.h" +#include "fatstruct.h" @@ -16,6 +17,7 @@ public: bool dumpDataFromFile(const std::string& romPath, const std::string& savePath, uint32_t startAddr, uint32_t size); bool readBytesFromFile(std::vector& byteBuffer, const std::string& romPath, uint32_t startAddr, uint32_t size); bool writeSectionToFile(const std::string& sectionPath, const std::string& savePath, uint32_t startAddr, uint32_t size); + bool writeFatSectionToFile(const std::string& romPath, FatRange* pfatrange, const std::string& savePath); bool writeBytesToFile(std::vector& byteBuffer, const std::string& savePath, uint32_t startAddr, uint32_t size); bool writePaddingToFile(char paddingChar, const std::string& savePath, uint32_t startAddr, uint32_t size); int getCardSizeInBytes(int cardType); diff --git a/ui/dialogs/about/revision.h b/ui/dialogs/about/revision.h index 9fabee6..e91ab8c 100644 --- a/ui/dialogs/about/revision.h +++ b/ui/dialogs/about/revision.h @@ -1,6 +1,6 @@ #ifndef REVISION_H #define REVISION_H -#define GIT_COMMIT_HASH "319d0e2" +#define GIT_COMMIT_HASH "30425b6" #endif diff --git a/ui/mainwindow.h b/ui/mainwindow.h index de2e951..b86a2c0 100644 --- a/ui/mainwindow.h +++ b/ui/mainwindow.h @@ -108,7 +108,7 @@ private: //QString extractUnpackerHeaderTableData(int index); QString extractPackerHeaderTableData(int index); - bool decodeFatFiles(); + bool decodeFatFiles(QString dirPath); bool patchFat(const std::string& loadPath, uint32_t shiftSize, const std::string& savePath); }; diff --git a/ui/tabs/unpacker/unpackertabfunctions.cpp b/ui/tabs/unpacker/unpackertabfunctions.cpp index 53cbd15..3d55dee 100644 --- a/ui/tabs/unpacker/unpackertabfunctions.cpp +++ b/ui/tabs/unpacker/unpackertabfunctions.cpp @@ -1,13 +1,26 @@ #include #include -#include #include #include #include "../../mainwindow.h" #include "../../ui_mainwindow.h" #include "../commons/headernames.h" #include "../../models/ndsheadermodel.h" +#include "../../../ndsfactory/fatstruct.h" +// Byte offsets for interpreting memory + +#define SECOND_BYTE_SHIFT 8 +#define THIRD_BYTE_SHIFT 16 +#define FOURTH_BYTE_SHIFT 24 + +// Magic values for FAT extraction + +#define CONTROL_BYTE_LENGTH_MASK 0x7F +#define CONTROL_BYTE_DIR_MASK 0x80 +#define DUMMY_CONTROL_VALUE 0xFF +#define FNT_HEADER_OFFSET_MASK 0XFFF +#define ROOT_DIRECTORY_ADDRESS 0xF000 void MainWindow::populateHeader(NDSHeader* ndsHeader) { @@ -167,8 +180,149 @@ bool MainWindow::dumpEverything(QString dirPath) return true; } -bool MainWindow::decodeFatFiles() +bool MainWindow::decodeFatFiles(QString dirPath) { - // TODO: implement me! - return false; + // Prepare necessary info from ROM + + std::string romPath = ui->loadedRomPath->text().toStdString(); // ROM itself + + // Addresses of the file allocation table and file name table + + uint32_t fatAddr = ui->unpackerHeaderDataTable->model()->index(NDSHeaderNames::FATAddress, 1).data().toString().toUInt(nullptr,16); + uint32_t fatSize = ui->unpackerHeaderDataTable->model()->index(NDSHeaderNames::FATSize, 1).data().toString().toUInt(nullptr,16); + + // Sizes of these tables + + uint32_t fntAddr = ui->unpackerHeaderDataTable->model()->index(NDSHeaderNames::FilenameTableAddress, 1).data().toString().toUInt(nullptr,16); + uint32_t fntSize = ui->unpackerHeaderDataTable->model()->index(NDSHeaderNames::FilenameTableSize, 1).data().toString().toUInt(nullptr,16); + + // Buffers to receive the contents of the FAT and FNT + + std::vector fatBytes(static_cast(fatSize)); + std::vector fntBytes(static_cast(fntSize)); + + // Fill them + + if(!ndsFactory.readBytesFromFile(fatBytes, romPath, fatAddr, fatSize)) return false; + if(!ndsFactory.readBytesFromFile(fntBytes, romPath, fntAddr, fntSize)) return false; + + // Use the available FAT range struct and read the FAT bytes as such + + FatRange* pfatrange = reinterpret_cast(fatBytes.data()); + + // Recursive function that looks up FNT info to find file names and directory structures, + // And writes the ROM data in the ranges indicated by the FAT simultaneously. + + auto parseFolder = [this, fntBytes, pfatrange, romPath](uint32_t folderId, std::string curPath, auto& parseFolder){ + + if(false) return false; // this is stupid, but it's C++ + // If we take it out, the compiler will complain because it doesn't known the return type of the lambda + // But we can't make the lambda bool either...so this is the best option... + + QDir curDir(QString::fromStdString(curPath)); // useful a bit later + + uint32_t currentOffset = 8 * (folderId & FNT_HEADER_OFFSET_MASK); // offset for the current directory's info in the FNT header + // Only the lower 12 bit of the given offset are relevant + + // --------------------------------------------------------------------- + // About how the FAT and FNT work : + + // The FNT has two sections : + // a "header" where every entry contains : + // - a 4-byte address where the corresponding directory's data starts in the body + // - a 2-byte offset that is the index of the first file of the directory in the FAT + // (e.g. : if the offset is 42, the first file in the directory is situated at the ROM addresses stored in the 42nd FAT entry) + // (and its second will be 43, etc.) + // a "body" where every entry contains : + // - a length+status/control byte : lower 7 bits (control byte & 0x7F) are a length, highest bit (control byte & 0x80) is set if entry is a directory, and not set if it's a file + // - a name which length is the length portion of the previous control byte (e.g. : if the control byte was 0x83, the name is three bytes long) + // - if the entry is a directory, a 2-byte address (where only the lower 12 bit are relevant for some reason) at which this directory's info is located in the FNT header + + // Thus, the FNT reading operation will consist in bouncing back and forth between body and header every time we must process a subdirectory + // Thank Heavens for random-access containers ! + + // --------------------------------------------------------------------- + + // Get the 4-byte address for the folder data + + uint32_t fntBodyOffset = + (uint32_t)((unsigned char) fntBytes[currentOffset+3] << (uint32_t) FOURTH_BYTE_SHIFT | + (unsigned char) fntBytes[currentOffset+2] << (uint32_t) THIRD_BYTE_SHIFT | + (unsigned char) fntBytes[currentOffset + 1] << (uint32_t) SECOND_BYTE_SHIFT | + (unsigned char) fntBytes[currentOffset]); + currentOffset+=4; + + // Get the 2-byte offset for the folder's first file in the FAT + + uint16_t fatOffset = + (uint16_t)((unsigned char) fntBytes[currentOffset+1] << SECOND_BYTE_SHIFT | + (unsigned char) fntBytes[currentOffset]); + + // Jump to FNT body a specified address + + currentOffset = fntBodyOffset; + + uint8_t controlByte = DUMMY_CONTROL_VALUE; + + while(true){ + + controlByte = fntBytes[currentOffset]; // Entry's control byte + if(controlByte==0) break; // A control byte of 0 terminates the directory's contents + currentOffset++; + + uint8_t nameLength = controlByte & CONTROL_BYTE_LENGTH_MASK; // length of entry name + bool isDir = controlByte & CONTROL_BYTE_DIR_MASK; // set if entry is a directory + + // Reconstitute name from bytes + // Btw I wish I could use the actual byte type but I have to comply with the software's choice of using char + + std::vector nameString; + for(size_t i = 0 ; i