From 02b880e1b063002327c92793522069d140532f3c Mon Sep 17 00:00:00 2001 From: Rangi <35663410+Rangi42@users.noreply.github.com> Date: Thu, 28 Aug 2025 12:28:08 -0400 Subject: [PATCH] Separate RGBFIX header fixing from CLI option parsing (#1808) --- Makefile | 1 + include/fix/fix.hpp | 8 + include/fix/main.hpp | 49 ++++ src/CMakeLists.txt | 1 + src/fix/fix.cpp | 522 +++++++++++++++++++++++++++++++++ src/fix/main.cpp | 682 ++++++------------------------------------- 6 files changed, 668 insertions(+), 595 deletions(-) create mode 100644 include/fix/fix.hpp create mode 100644 include/fix/main.hpp create mode 100644 src/fix/fix.cpp diff --git a/Makefile b/Makefile index 14a572a0..410ecac1 100644 --- a/Makefile +++ b/Makefile @@ -106,6 +106,7 @@ src/link/lexer.o src/link/main.o: src/link/script.hpp rgbfix_obj := \ ${common_obj} \ + src/fix/fix.o \ src/fix/main.o \ src/fix/mbc.o \ src/fix/warning.o diff --git a/include/fix/fix.hpp b/include/fix/fix.hpp new file mode 100644 index 00000000..11917fc5 --- /dev/null +++ b/include/fix/fix.hpp @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT + +#ifndef RGBDS_FIX_FIX_HPP +#define RGBDS_FIX_FIX_HPP + +bool fix_ProcessFile(char const *name, char const *outputName); + +#endif // RGBDS_FIX_FIX_HPP diff --git a/include/fix/main.hpp b/include/fix/main.hpp new file mode 100644 index 00000000..597f6063 --- /dev/null +++ b/include/fix/main.hpp @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT + +#ifndef RGBDS_FIX_MAIN_HPP +#define RGBDS_FIX_MAIN_HPP + +#include + +#include "fix/mbc.hpp" // UNSPECIFIED, MbcType + +// clang-format off: vertically align values +static constexpr uint8_t FIX_LOGO = 1 << 7; +static constexpr uint8_t TRASH_LOGO = 1 << 6; +static constexpr uint8_t FIX_HEADER_SUM = 1 << 5; +static constexpr uint8_t TRASH_HEADER_SUM = 1 << 4; +static constexpr uint8_t FIX_GLOBAL_SUM = 1 << 3; +static constexpr uint8_t TRASH_GLOBAL_SUM = 1 << 2; +// clang-format on + +enum Model { DMG, BOTH, CGB }; + +struct Options { + uint8_t fixSpec = 0; // -f, -v + Model model = DMG; // -C, -c + bool japanese = true; // -j + uint16_t oldLicensee = UNSPECIFIED; // -l + uint16_t romVersion = UNSPECIFIED; // -n + uint16_t padValue = UNSPECIFIED; // -p + uint16_t ramSize = UNSPECIFIED; // -r + bool sgb = false; // -s + + char const *gameID = nullptr; // -i + uint8_t gameIDLen; + + char const *newLicensee = nullptr; // -k + uint8_t newLicenseeLen; + + char const *logoFilename = nullptr; // -L + uint8_t logo[48] = {}; + + MbcType cartridgeType = MBC_NONE; // -m + uint8_t tpp1Rev[2]; + + char const *title = nullptr; // -t + uint8_t titleLen; +}; + +extern Options options; + +#endif // RGBDS_FIX_MAIN_HPP diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index be7143ba..3f99e902 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -80,6 +80,7 @@ set(rgblink_src ) set(rgbfix_src + "fix/fix.cpp" "fix/main.cpp" "fix/mbc.cpp" "fix/warning.cpp" diff --git a/src/fix/fix.cpp b/src/fix/fix.cpp new file mode 100644 index 00000000..2776939c --- /dev/null +++ b/src/fix/fix.cpp @@ -0,0 +1,522 @@ +// SPDX-License-Identifier: MIT + +#include "fix/fix.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "diagnostics.hpp" +#include "helpers.hpp" +#include "platform.hpp" + +#include "fix/main.hpp" +#include "fix/mbc.hpp" +#include "fix/warning.hpp" + +static constexpr off_t BANK_SIZE = 0x4000; + +static ssize_t readBytes(int fd, uint8_t *buf, size_t len) { + // POSIX specifies that lengths greater than SSIZE_MAX yield implementation-defined results + assume(len <= SSIZE_MAX); + + ssize_t total = 0; + + while (len) { + ssize_t ret = read(fd, buf, len); + + // Return errors, unless we only were interrupted + if (ret == -1 && errno != EINTR) { + return -1; // LCOV_EXCL_LINE + } + // EOF reached + if (ret == 0) { + return total; + } + // If anything was read, accumulate it, and continue + if (ret != -1) { + total += ret; + len -= ret; + buf += ret; + } + } + + return total; +} + +static ssize_t writeBytes(int fd, uint8_t *buf, size_t len) { + // POSIX specifies that lengths greater than SSIZE_MAX yield implementation-defined results + assume(len <= SSIZE_MAX); + + ssize_t total = 0; + + while (len) { + ssize_t ret = write(fd, buf, len); + + // Return errors, unless we only were interrupted + if (ret == -1 && errno != EINTR) { + return -1; // LCOV_EXCL_LINE + } + // If anything was written, accumulate it, and continue + if (ret != -1) { + total += ret; + len -= ret; + buf += ret; + } + } + + return total; +} + +static void overwriteByte(uint8_t *rom0, uint16_t addr, uint8_t fixedByte, char const *areaName) { + uint8_t origByte = rom0[addr]; + + if (origByte != 0 && origByte != fixedByte) { + warning(WARNING_OVERWRITE, "Overwrote a non-zero byte in the %s", areaName); + } + + rom0[addr] = fixedByte; +} + +static void overwriteBytes( + uint8_t *rom0, uint16_t startAddr, uint8_t const *fixed, uint8_t size, char const *areaName +) { + for (uint8_t i = 0; i < size; ++i) { + uint8_t origByte = rom0[i + startAddr]; + + if (origByte != 0 && origByte != fixed[i]) { + warning(WARNING_OVERWRITE, "Overwrote a non-zero byte in the %s", areaName); + break; + } + } + + memcpy(&rom0[startAddr], fixed, size); +} + +static void + processFile(int input, int output, char const *name, off_t fileSize, bool expectFileSize) { + if (expectFileSize) { + assume(fileSize != 0); + } else { + assume(fileSize == 0); + } + + uint8_t rom0[BANK_SIZE]; + ssize_t rom0Len = readBytes(input, rom0, sizeof(rom0)); + // Also used as how many bytes to write back when fixing in-place + ssize_t headerSize = (options.cartridgeType & 0xFF00) == TPP1 ? 0x154 : 0x150; + + if (rom0Len == -1) { + // LCOV_EXCL_START + error("Failed to read \"%s\"'s header: %s", name, strerror(errno)); + return; + // LCOV_EXCL_STOP + } else if (rom0Len < headerSize) { + error( + "\"%s\" too short, expected at least %jd ($%jx) bytes, got only %jd", + name, + static_cast(headerSize), + static_cast(headerSize), + static_cast(rom0Len) + ); + return; + } + // Accept partial reads if the file contains at least the header + + if (options.fixSpec & (FIX_LOGO | TRASH_LOGO)) { + overwriteBytes( + rom0, + 0x0104, + options.logo, + sizeof(options.logo), + options.logoFilename ? "logo" : "Nintendo logo" + ); + } + + if (options.title) { + overwriteBytes( + rom0, 0x134, reinterpret_cast(options.title), options.titleLen, "title" + ); + } + + if (options.gameID) { + overwriteBytes( + rom0, + 0x13F, + reinterpret_cast(options.gameID), + options.gameIDLen, + "manufacturer code" + ); + } + + if (options.model != DMG) { + overwriteByte(rom0, 0x143, options.model == BOTH ? 0x80 : 0xC0, "CGB flag"); + } + + if (options.newLicensee) { + overwriteBytes( + rom0, + 0x144, + reinterpret_cast(options.newLicensee), + options.newLicenseeLen, + "new licensee code" + ); + } + + if (options.sgb) { + overwriteByte(rom0, 0x146, 0x03, "SGB flag"); + } + + // If a valid MBC was specified... + if (options.cartridgeType < MBC_NONE) { + uint8_t byte = options.cartridgeType; + + if ((options.cartridgeType & 0xFF00) == TPP1) { + // Cartridge type isn't directly actionable, translate it + byte = 0xBC; + // The other TPP1 identification bytes will be written below + } + overwriteByte(rom0, 0x147, byte, "cartridge type"); + } + + // ROM size will be written last, after evaluating the file's size + + if ((options.cartridgeType & 0xFF00) == TPP1) { + uint8_t const tpp1Code[2] = {0xC1, 0x65}; + + overwriteBytes(rom0, 0x149, tpp1Code, sizeof(tpp1Code), "TPP1 identification code"); + + overwriteBytes( + rom0, 0x150, options.tpp1Rev, sizeof(options.tpp1Rev), "TPP1 revision number" + ); + + if (options.ramSize != UNSPECIFIED) { + overwriteByte(rom0, 0x152, options.ramSize, "RAM size"); + } + + overwriteByte(rom0, 0x153, options.cartridgeType & 0xFF, "TPP1 feature flags"); + } else { + // Regular mappers + + if (options.ramSize != UNSPECIFIED) { + overwriteByte(rom0, 0x149, options.ramSize, "RAM size"); + } + + if (!options.japanese) { + overwriteByte(rom0, 0x14A, 0x01, "destination code"); + } + } + + if (options.oldLicensee != UNSPECIFIED) { + overwriteByte(rom0, 0x14B, options.oldLicensee, "old licensee code"); + } else if (options.sgb && rom0[0x14B] != 0x33) { + warning( + WARNING_SGB, + "SGB compatibility enabled, but old licensee was 0x%02x, not 0x33", + rom0[0x14B] + ); + } + + if (options.romVersion != UNSPECIFIED) { + overwriteByte(rom0, 0x14C, options.romVersion, "mask ROM version number"); + } + + // Remain to be handled the ROM size, and header checksum. + // The latter depends on the former, and so will be handled after it. + // The former requires knowledge of the file's total size, so read that first. + + uint16_t globalSum = 0; + + // To keep file sizes fairly reasonable, we'll cap the amount of banks at 65536. + // Official mappers only go up to 512 banks, but at least the TPP1 spec allows up to + // 65536 banks = 1 GiB. + // This should be reasonable for the time being, and may be extended later. + std::vector romx; // Buffer of ROMX bank data + uint32_t nbBanks = 1; // Number of banks *targeted*, including ROM0 + size_t totalRomxLen = 0; // *Actual* size of ROMX data + uint8_t bank[BANK_SIZE]; // Temp buffer used to store a whole bank's worth of data + + // Handle ROMX + if (input == output) { + if (fileSize >= 0x10000 * BANK_SIZE) { + error("\"%s\" has more than 65536 banks", name); + return; + } + // This should be guaranteed from the size cap... + static_assert(0x10000 * BANK_SIZE <= SSIZE_MAX, "Max input file size too large for OS"); + // Compute number of banks and ROMX len from file size + nbBanks = (fileSize + (BANK_SIZE - 1)) / BANK_SIZE; // ceil(fileSize / BANK_SIZE) + totalRomxLen = fileSize >= BANK_SIZE ? fileSize - BANK_SIZE : 0; + } else if (rom0Len == BANK_SIZE) { + // Copy ROMX when reading a pipe, and we're not at EOF yet + for (;;) { + romx.resize(nbBanks * BANK_SIZE); + ssize_t bankLen = readBytes(input, &romx[(nbBanks - 1) * BANK_SIZE], BANK_SIZE); + + // Update bank count, ONLY IF at least one byte was read + if (bankLen) { + // We're going to read another bank, check that it won't be too much + static_assert( + 0x10000 * BANK_SIZE <= SSIZE_MAX, "Max input file size too large for OS" + ); + if (nbBanks == 0x10000) { + error("\"%s\" has more than 65536 banks", name); + return; + } + ++nbBanks; + + // Update global checksum, too + for (uint16_t i = 0; i < bankLen; ++i) { + globalSum += romx[totalRomxLen + i]; + } + totalRomxLen += bankLen; + } + // Stop when an incomplete bank has been read + if (bankLen != BANK_SIZE) { + break; + } + } + } + + // Handle setting the ROM size if padding was requested + // Pad to the next valid power of 2. This is because padding is required by flashers, which + // flash to ROM chips, whose size is always a power of 2... so there'd be no point in + // padding to something else. + // Additionally, a ROM must be at least 32k, so we guarantee a whole amount of banks... + if (options.padValue != UNSPECIFIED) { + // We want at least 2 banks + if (nbBanks == 1) { + if (rom0Len != sizeof(rom0)) { + memset(&rom0[rom0Len], options.padValue, sizeof(rom0) - rom0Len); + // The global checksum hasn't taken ROM0 into consideration yet! + // ROM0 was padded, so treat it as entirely written: update its size + // Update how many bytes were read in total, too + rom0Len = sizeof(rom0); + } + nbBanks = 2; + } else { + assume(rom0Len == sizeof(rom0)); + } + assume(nbBanks >= 2); + // Alter number of banks to reflect required value + // x&(x-1) is zero iff x is a power of 2, or 0; we know for sure it's non-zero, + // so this is true (non-zero) when we don't have a power of 2 + if (nbBanks & (nbBanks - 1)) { + nbBanks = 1 << (CHAR_BIT * sizeof(nbBanks) - clz(nbBanks)); + } + // Write final ROM size + rom0[0x148] = ctz(nbBanks / 2); + // Alter global checksum based on how many bytes will be added (not counting ROM0) + globalSum += options.padValue * ((nbBanks - 1) * BANK_SIZE - totalRomxLen); + } + + // Handle the header checksum after the ROM size has been written + if (options.fixSpec & (FIX_HEADER_SUM | TRASH_HEADER_SUM)) { + uint8_t sum = 0; + + for (uint16_t i = 0x134; i < 0x14D; ++i) { + sum -= rom0[i] + 1; + } + + overwriteByte( + rom0, 0x14D, options.fixSpec & TRASH_HEADER_SUM ? ~sum : sum, "header checksum" + ); + } + + if (options.fixSpec & (FIX_GLOBAL_SUM | TRASH_GLOBAL_SUM)) { + // Computation of the global checksum does not include the checksum bytes + assume(rom0Len >= 0x14E); + for (uint16_t i = 0; i < 0x14E; ++i) { + globalSum += rom0[i]; + } + for (uint16_t i = 0x150; i < rom0Len; ++i) { + globalSum += rom0[i]; + } + // Pipes have already read ROMX and updated globalSum, but not regular files + if (input == output) { + for (;;) { + ssize_t bankLen = readBytes(input, bank, sizeof(bank)); + + for (uint16_t i = 0; i < bankLen; ++i) { + globalSum += bank[i]; + } + if (bankLen != sizeof(bank)) { + break; + } + } + } + + if (options.fixSpec & TRASH_GLOBAL_SUM) { + globalSum = ~globalSum; + } + + uint8_t bytes[2] = { + static_cast(globalSum >> 8), static_cast(globalSum & 0xFF) + }; + + overwriteBytes(rom0, 0x14E, bytes, sizeof(bytes), "global checksum"); + } + + ssize_t writeLen; + + // In case the output depends on the input, reset to the beginning of the file, and only + // write the header + if (input == output) { + if (lseek(output, 0, SEEK_SET) == static_cast(-1)) { + // LCOV_EXCL_START + error("Failed to rewind \"%s\": %s", name, strerror(errno)); + return; + // LCOV_EXCL_STOP + } + // If modifying the file in-place, we only need to edit the header + // However, padding may have modified ROM0 (added padding), so don't in that case + if (options.padValue == UNSPECIFIED) { + rom0Len = headerSize; + } + } + writeLen = writeBytes(output, rom0, rom0Len); + + if (writeLen == -1) { + // LCOV_EXCL_START + error("Failed to write \"%s\"'s ROM0: %s", name, strerror(errno)); + return; + // LCOV_EXCL_STOP + } else if (writeLen < rom0Len) { + // LCOV_EXCL_START + error( + "Could only write %jd of \"%s\"'s %jd ROM0 bytes", + static_cast(writeLen), + name, + static_cast(rom0Len) + ); + return; + // LCOV_EXCL_STOP + } + + // Output ROMX if it was buffered + if (!romx.empty()) { + // The value returned is either -1, or smaller than `totalRomxLen`, + // so it's fine to cast to `size_t` + writeLen = writeBytes(output, romx.data(), totalRomxLen); + if (writeLen == -1) { + // LCOV_EXCL_START + error("Failed to write \"%s\"'s ROMX: %s", name, strerror(errno)); + return; + // LCOV_EXCL_STOP + } else if (static_cast(writeLen) < totalRomxLen) { + // LCOV_EXCL_START + error( + "Could only write %jd of \"%s\"'s %zu ROMX bytes", + static_cast(writeLen), + name, + totalRomxLen + ); + return; + // LCOV_EXCL_STOP + } + } + + // Output padding + if (options.padValue != UNSPECIFIED) { + if (input == output) { + if (lseek(output, 0, SEEK_END) == static_cast(-1)) { + // LCOV_EXCL_START + error("Failed to seek to end of \"%s\": %s", name, strerror(errno)); + return; + // LCOV_EXCL_STOP + } + } + memset(bank, options.padValue, sizeof(bank)); + size_t len = (nbBanks - 1) * BANK_SIZE - totalRomxLen; // Don't count ROM0! + + while (len) { + static_assert(sizeof(bank) <= SSIZE_MAX, "Bank too large for reading"); + size_t thisLen = len > sizeof(bank) ? sizeof(bank) : len; + ssize_t ret = writeBytes(output, bank, thisLen); + + // The return value is either -1, or at most `thisLen`, + // so it's fine to cast to `size_t` + if (static_cast(ret) != thisLen) { + // LCOV_EXCL_START + error("Failed to write \"%s\"'s padding: %s", name, strerror(errno)); + break; + // LCOV_EXCL_STOP + } + len -= thisLen; + } + } +} + +bool fix_ProcessFile(char const *name, char const *outputName) { + warnings.nbErrors = 0; + + bool inputStdin = !strcmp(name, "-"); + if (inputStdin && !outputName) { + outputName = "-"; + } + + int output = -1; + bool openedOutput = false; + if (outputName) { + if (!strcmp(outputName, "-")) { + output = STDOUT_FILENO; + (void)setmode(STDOUT_FILENO, O_BINARY); + } else { + output = open(outputName, O_WRONLY | O_BINARY | O_CREAT, 0600); + if (output == -1) { + error("Failed to open \"%s\" for writing: %s", outputName, strerror(errno)); + return true; + } + openedOutput = true; + } + } + Defer closeOutput{[&] { + if (openedOutput) { + close(output); + } + }}; + + if (inputStdin) { + name = ""; + (void)setmode(STDIN_FILENO, O_BINARY); + processFile(STDIN_FILENO, output, name, 0, false); + } else if (int input = open(name, (outputName ? O_RDONLY : O_RDWR) | O_BINARY); input == -1) { + // POSIX specifies that the results of O_RDWR on a FIFO are undefined. + // However, this is necessary to avoid a TOCTTOU, if the file was changed between + // `stat()` and `open(O_RDWR)`, which could trigger the UB anyway. + // Thus, we're going to hope that either the `open` fails, or it succeeds but I/O + // operations may fail, all of which we handle. + error("Failed to open \"%s\" for reading+writing: %s", name, strerror(errno)); + } else { + Defer closeInput{[&] { close(input); }}; + struct stat stat; + if (fstat(input, &stat) == -1) { + error("Failed to stat \"%s\": %s", name, strerror(errno)); // LCOV_EXCL_LINE + } else if (!S_ISREG(stat.st_mode)) { // We do not support FIFOs or symlinks + // LCOV_EXCL_START + error("\"%s\" is not a regular file, and thus cannot be modified in-place", name); + // LCOV_EXCL_STOP + } else if (stat.st_size < 0x150) { + // This check is in theory redundant with the one in `processFile`, but it + // prevents passing a file size of 0, which usually indicates pipes + error( + "\"%s\" too short, expected at least 336 ($150) bytes, got only %jd", + name, + static_cast(stat.st_size) + ); + } else { + if (!outputName) { + output = input; + } + processFile(input, output, name, stat.st_size, true); + } + } + + return checkErrors(name); +} diff --git a/src/fix/main.cpp b/src/fix/main.cpp index 11ad2dfe..526cf59a 100644 --- a/src/fix/main.cpp +++ b/src/fix/main.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -#include +#include "fix/main.hpp" #include #include @@ -9,7 +9,6 @@ #include #include #include -#include #include "diagnostics.hpp" #include "extern/getopt.hpp" @@ -19,10 +18,11 @@ #include "usage.hpp" #include "version.hpp" +#include "fix/fix.hpp" #include "fix/mbc.hpp" #include "fix/warning.hpp" -static constexpr off_t BANK_SIZE = 0x4000; +Options options; // Short options static char const *optstring = "Ccf:hi:jk:L:l:m:n:Oo:p:r:st:VvW:w"; @@ -88,528 +88,6 @@ static Usage usage = { }; // clang-format on -static uint8_t tpp1Rev[2]; - -static uint8_t const nintendoLogo[] = { - 0xCE, 0xED, 0x66, 0x66, 0xCC, 0x0D, 0x00, 0x0B, 0x03, 0x73, 0x00, 0x83, 0x00, 0x0C, 0x00, 0x0D, - 0x00, 0x08, 0x11, 0x1F, 0x88, 0x89, 0x00, 0x0E, 0xDC, 0xCC, 0x6E, 0xE6, 0xDD, 0xDD, 0xD9, 0x99, - 0xBB, 0xBB, 0x67, 0x63, 0x6E, 0x0E, 0xEC, 0xCC, 0xDD, 0xDC, 0x99, 0x9F, 0xBB, 0xB9, 0x33, 0x3E, -}; - -static uint8_t fixSpec = 0; -// clang-format off: vertically align values -static constexpr uint8_t FIX_LOGO = 1 << 7; -static constexpr uint8_t TRASH_LOGO = 1 << 6; -static constexpr uint8_t FIX_HEADER_SUM = 1 << 5; -static constexpr uint8_t TRASH_HEADER_SUM = 1 << 4; -static constexpr uint8_t FIX_GLOBAL_SUM = 1 << 3; -static constexpr uint8_t TRASH_GLOBAL_SUM = 1 << 2; -// clang-format on - -static enum { DMG, BOTH, CGB } model = DMG; // If DMG, byte is left alone -static char const *gameID = nullptr; -static uint8_t gameIDLen; -static bool japanese = true; -static char const *logoFilename = nullptr; -static uint8_t logo[sizeof(nintendoLogo)] = {}; -static char const *newLicensee = nullptr; -static uint8_t newLicenseeLen; -static uint16_t oldLicensee = UNSPECIFIED; -static MbcType cartridgeType = MBC_NONE; -static uint16_t romVersion = UNSPECIFIED; -static uint16_t padValue = UNSPECIFIED; -static uint16_t ramSize = UNSPECIFIED; -static bool sgb = false; // If false, SGB flags are left alone -static char const *title = nullptr; -static uint8_t titleLen; - -static uint8_t maxTitleLen() { - return gameID ? 11 : model != DMG ? 15 : 16; -} - -static ssize_t readBytes(int fd, uint8_t *buf, size_t len) { - // POSIX specifies that lengths greater than SSIZE_MAX yield implementation-defined results - assume(len <= SSIZE_MAX); - - ssize_t total = 0; - - while (len) { - ssize_t ret = read(fd, buf, len); - - // Return errors, unless we only were interrupted - if (ret == -1 && errno != EINTR) { - return -1; // LCOV_EXCL_LINE - } - // EOF reached - if (ret == 0) { - return total; - } - // If anything was read, accumulate it, and continue - if (ret != -1) { - total += ret; - len -= ret; - buf += ret; - } - } - - return total; -} - -static ssize_t writeBytes(int fd, uint8_t *buf, size_t len) { - // POSIX specifies that lengths greater than SSIZE_MAX yield implementation-defined results - assume(len <= SSIZE_MAX); - - ssize_t total = 0; - - while (len) { - ssize_t ret = write(fd, buf, len); - - // Return errors, unless we only were interrupted - if (ret == -1 && errno != EINTR) { - return -1; // LCOV_EXCL_LINE - } - // If anything was written, accumulate it, and continue - if (ret != -1) { - total += ret; - len -= ret; - buf += ret; - } - } - - return total; -} - -static void overwriteByte(uint8_t *rom0, uint16_t addr, uint8_t fixedByte, char const *areaName) { - uint8_t origByte = rom0[addr]; - - if (origByte != 0 && origByte != fixedByte) { - warning(WARNING_OVERWRITE, "Overwrote a non-zero byte in the %s", areaName); - } - - rom0[addr] = fixedByte; -} - -static void overwriteBytes( - uint8_t *rom0, uint16_t startAddr, uint8_t const *fixed, uint8_t size, char const *areaName -) { - for (uint8_t i = 0; i < size; ++i) { - uint8_t origByte = rom0[i + startAddr]; - - if (origByte != 0 && origByte != fixed[i]) { - warning(WARNING_OVERWRITE, "Overwrote a non-zero byte in the %s", areaName); - break; - } - } - - memcpy(&rom0[startAddr], fixed, size); -} - -static void - processFile(int input, int output, char const *name, off_t fileSize, bool expectFileSize) { - if (expectFileSize) { - assume(fileSize != 0); - } else { - assume(fileSize == 0); - } - - uint8_t rom0[BANK_SIZE]; - ssize_t rom0Len = readBytes(input, rom0, sizeof(rom0)); - // Also used as how many bytes to write back when fixing in-place - ssize_t headerSize = (cartridgeType & 0xFF00) == TPP1 ? 0x154 : 0x150; - - if (rom0Len == -1) { - // LCOV_EXCL_START - error("Failed to read \"%s\"'s header: %s", name, strerror(errno)); - return; - // LCOV_EXCL_STOP - } else if (rom0Len < headerSize) { - error( - "\"%s\" too short, expected at least %jd ($%jx) bytes, got only %jd", - name, - static_cast(headerSize), - static_cast(headerSize), - static_cast(rom0Len) - ); - return; - } - // Accept partial reads if the file contains at least the header - - if (fixSpec & (FIX_LOGO | TRASH_LOGO)) { - overwriteBytes(rom0, 0x0104, logo, sizeof(logo), logoFilename ? "logo" : "Nintendo logo"); - } - - if (title) { - overwriteBytes(rom0, 0x134, reinterpret_cast(title), titleLen, "title"); - } - - if (gameID) { - overwriteBytes( - rom0, 0x13F, reinterpret_cast(gameID), gameIDLen, "manufacturer code" - ); - } - - if (model != DMG) { - overwriteByte(rom0, 0x143, model == BOTH ? 0x80 : 0xC0, "CGB flag"); - } - - if (newLicensee) { - overwriteBytes( - rom0, - 0x144, - reinterpret_cast(newLicensee), - newLicenseeLen, - "new licensee code" - ); - } - - if (sgb) { - overwriteByte(rom0, 0x146, 0x03, "SGB flag"); - } - - // If a valid MBC was specified... - if (cartridgeType < MBC_NONE) { - uint8_t byte = cartridgeType; - - if ((cartridgeType & 0xFF00) == TPP1) { - // Cartridge type isn't directly actionable, translate it - byte = 0xBC; - // The other TPP1 identification bytes will be written below - } - overwriteByte(rom0, 0x147, byte, "cartridge type"); - } - - // ROM size will be written last, after evaluating the file's size - - if ((cartridgeType & 0xFF00) == TPP1) { - uint8_t const tpp1Code[2] = {0xC1, 0x65}; - - overwriteBytes(rom0, 0x149, tpp1Code, sizeof(tpp1Code), "TPP1 identification code"); - - overwriteBytes(rom0, 0x150, tpp1Rev, sizeof(tpp1Rev), "TPP1 revision number"); - - if (ramSize != UNSPECIFIED) { - overwriteByte(rom0, 0x152, ramSize, "RAM size"); - } - - overwriteByte(rom0, 0x153, cartridgeType & 0xFF, "TPP1 feature flags"); - } else { - // Regular mappers - - if (ramSize != UNSPECIFIED) { - overwriteByte(rom0, 0x149, ramSize, "RAM size"); - } - - if (!japanese) { - overwriteByte(rom0, 0x14A, 0x01, "destination code"); - } - } - - if (oldLicensee != UNSPECIFIED) { - overwriteByte(rom0, 0x14B, oldLicensee, "old licensee code"); - } else if (sgb && rom0[0x14B] != 0x33) { - warning( - WARNING_SGB, - "SGB compatibility enabled, but old licensee was 0x%02x, not 0x33", - rom0[0x14B] - ); - } - - if (romVersion != UNSPECIFIED) { - overwriteByte(rom0, 0x14C, romVersion, "mask ROM version number"); - } - - // Remain to be handled the ROM size, and header checksum. - // The latter depends on the former, and so will be handled after it. - // The former requires knowledge of the file's total size, so read that first. - - uint16_t globalSum = 0; - - // To keep file sizes fairly reasonable, we'll cap the amount of banks at 65536. - // Official mappers only go up to 512 banks, but at least the TPP1 spec allows up to - // 65536 banks = 1 GiB. - // This should be reasonable for the time being, and may be extended later. - std::vector romx; // Buffer of ROMX bank data - uint32_t nbBanks = 1; // Number of banks *targeted*, including ROM0 - size_t totalRomxLen = 0; // *Actual* size of ROMX data - uint8_t bank[BANK_SIZE]; // Temp buffer used to store a whole bank's worth of data - - // Handle ROMX - if (input == output) { - if (fileSize >= 0x10000 * BANK_SIZE) { - error("\"%s\" has more than 65536 banks", name); - return; - } - // This should be guaranteed from the size cap... - static_assert(0x10000 * BANK_SIZE <= SSIZE_MAX, "Max input file size too large for OS"); - // Compute number of banks and ROMX len from file size - nbBanks = (fileSize + (BANK_SIZE - 1)) / BANK_SIZE; // ceil(fileSize / BANK_SIZE) - totalRomxLen = fileSize >= BANK_SIZE ? fileSize - BANK_SIZE : 0; - } else if (rom0Len == BANK_SIZE) { - // Copy ROMX when reading a pipe, and we're not at EOF yet - for (;;) { - romx.resize(nbBanks * BANK_SIZE); - ssize_t bankLen = readBytes(input, &romx[(nbBanks - 1) * BANK_SIZE], BANK_SIZE); - - // Update bank count, ONLY IF at least one byte was read - if (bankLen) { - // We're going to read another bank, check that it won't be too much - static_assert( - 0x10000 * BANK_SIZE <= SSIZE_MAX, "Max input file size too large for OS" - ); - if (nbBanks == 0x10000) { - error("\"%s\" has more than 65536 banks", name); - return; - } - ++nbBanks; - - // Update global checksum, too - for (uint16_t i = 0; i < bankLen; ++i) { - globalSum += romx[totalRomxLen + i]; - } - totalRomxLen += bankLen; - } - // Stop when an incomplete bank has been read - if (bankLen != BANK_SIZE) { - break; - } - } - } - - // Handle setting the ROM size if padding was requested - // Pad to the next valid power of 2. This is because padding is required by flashers, which - // flash to ROM chips, whose size is always a power of 2... so there'd be no point in - // padding to something else. - // Additionally, a ROM must be at least 32k, so we guarantee a whole amount of banks... - if (padValue != UNSPECIFIED) { - // We want at least 2 banks - if (nbBanks == 1) { - if (rom0Len != sizeof(rom0)) { - memset(&rom0[rom0Len], padValue, sizeof(rom0) - rom0Len); - // The global checksum hasn't taken ROM0 into consideration yet! - // ROM0 was padded, so treat it as entirely written: update its size - // Update how many bytes were read in total, too - rom0Len = sizeof(rom0); - } - nbBanks = 2; - } else { - assume(rom0Len == sizeof(rom0)); - } - assume(nbBanks >= 2); - // Alter number of banks to reflect required value - // x&(x-1) is zero iff x is a power of 2, or 0; we know for sure it's non-zero, - // so this is true (non-zero) when we don't have a power of 2 - if (nbBanks & (nbBanks - 1)) { - nbBanks = 1 << (CHAR_BIT * sizeof(nbBanks) - clz(nbBanks)); - } - // Write final ROM size - rom0[0x148] = ctz(nbBanks / 2); - // Alter global checksum based on how many bytes will be added (not counting ROM0) - globalSum += padValue * ((nbBanks - 1) * BANK_SIZE - totalRomxLen); - } - - // Handle the header checksum after the ROM size has been written - if (fixSpec & (FIX_HEADER_SUM | TRASH_HEADER_SUM)) { - uint8_t sum = 0; - - for (uint16_t i = 0x134; i < 0x14D; ++i) { - sum -= rom0[i] + 1; - } - - overwriteByte(rom0, 0x14D, fixSpec & TRASH_HEADER_SUM ? ~sum : sum, "header checksum"); - } - - if (fixSpec & (FIX_GLOBAL_SUM | TRASH_GLOBAL_SUM)) { - // Computation of the global checksum does not include the checksum bytes - assume(rom0Len >= 0x14E); - for (uint16_t i = 0; i < 0x14E; ++i) { - globalSum += rom0[i]; - } - for (uint16_t i = 0x150; i < rom0Len; ++i) { - globalSum += rom0[i]; - } - // Pipes have already read ROMX and updated globalSum, but not regular files - if (input == output) { - for (;;) { - ssize_t bankLen = readBytes(input, bank, sizeof(bank)); - - for (uint16_t i = 0; i < bankLen; ++i) { - globalSum += bank[i]; - } - if (bankLen != sizeof(bank)) { - break; - } - } - } - - if (fixSpec & TRASH_GLOBAL_SUM) { - globalSum = ~globalSum; - } - - uint8_t bytes[2] = { - static_cast(globalSum >> 8), static_cast(globalSum & 0xFF) - }; - - overwriteBytes(rom0, 0x14E, bytes, sizeof(bytes), "global checksum"); - } - - ssize_t writeLen; - - // In case the output depends on the input, reset to the beginning of the file, and only - // write the header - if (input == output) { - if (lseek(output, 0, SEEK_SET) == static_cast(-1)) { - // LCOV_EXCL_START - error("Failed to rewind \"%s\": %s", name, strerror(errno)); - return; - // LCOV_EXCL_STOP - } - // If modifying the file in-place, we only need to edit the header - // However, padding may have modified ROM0 (added padding), so don't in that case - if (padValue == UNSPECIFIED) { - rom0Len = headerSize; - } - } - writeLen = writeBytes(output, rom0, rom0Len); - - if (writeLen == -1) { - // LCOV_EXCL_START - error("Failed to write \"%s\"'s ROM0: %s", name, strerror(errno)); - return; - // LCOV_EXCL_STOP - } else if (writeLen < rom0Len) { - // LCOV_EXCL_START - error( - "Could only write %jd of \"%s\"'s %jd ROM0 bytes", - static_cast(writeLen), - name, - static_cast(rom0Len) - ); - return; - // LCOV_EXCL_STOP - } - - // Output ROMX if it was buffered - if (!romx.empty()) { - // The value returned is either -1, or smaller than `totalRomxLen`, - // so it's fine to cast to `size_t` - writeLen = writeBytes(output, romx.data(), totalRomxLen); - if (writeLen == -1) { - // LCOV_EXCL_START - error("Failed to write \"%s\"'s ROMX: %s", name, strerror(errno)); - return; - // LCOV_EXCL_STOP - } else if (static_cast(writeLen) < totalRomxLen) { - // LCOV_EXCL_START - error( - "Could only write %jd of \"%s\"'s %zu ROMX bytes", - static_cast(writeLen), - name, - totalRomxLen - ); - return; - // LCOV_EXCL_STOP - } - } - - // Output padding - if (padValue != UNSPECIFIED) { - if (input == output) { - if (lseek(output, 0, SEEK_END) == static_cast(-1)) { - // LCOV_EXCL_START - error("Failed to seek to end of \"%s\": %s", name, strerror(errno)); - return; - // LCOV_EXCL_STOP - } - } - memset(bank, padValue, sizeof(bank)); - size_t len = (nbBanks - 1) * BANK_SIZE - totalRomxLen; // Don't count ROM0! - - while (len) { - static_assert(sizeof(bank) <= SSIZE_MAX, "Bank too large for reading"); - size_t thisLen = len > sizeof(bank) ? sizeof(bank) : len; - ssize_t ret = writeBytes(output, bank, thisLen); - - // The return value is either -1, or at most `thisLen`, - // so it's fine to cast to `size_t` - if (static_cast(ret) != thisLen) { - // LCOV_EXCL_START - error("Failed to write \"%s\"'s padding: %s", name, strerror(errno)); - break; - // LCOV_EXCL_STOP - } - len -= thisLen; - } - } -} - -static bool processFilename(char const *name, char const *outputName) { - warnings.nbErrors = 0; - - bool inputStdin = !strcmp(name, "-"); - if (inputStdin && !outputName) { - outputName = "-"; - } - - int output = -1; - bool openedOutput = false; - if (outputName) { - if (!strcmp(outputName, "-")) { - output = STDOUT_FILENO; - (void)setmode(STDOUT_FILENO, O_BINARY); - } else { - output = open(outputName, O_WRONLY | O_BINARY | O_CREAT, 0600); - if (output == -1) { - error("Failed to open \"%s\" for writing: %s", outputName, strerror(errno)); - return true; - } - openedOutput = true; - } - } - Defer closeOutput{[&] { - if (openedOutput) { - close(output); - } - }}; - - if (inputStdin) { - name = ""; - (void)setmode(STDIN_FILENO, O_BINARY); - processFile(STDIN_FILENO, output, name, 0, false); - } else if (int input = open(name, (outputName ? O_RDONLY : O_RDWR) | O_BINARY); input == -1) { - // POSIX specifies that the results of O_RDWR on a FIFO are undefined. - // However, this is necessary to avoid a TOCTTOU, if the file was changed between - // `stat()` and `open(O_RDWR)`, which could trigger the UB anyway. - // Thus, we're going to hope that either the `open` fails, or it succeeds but I/O - // operations may fail, all of which we handle. - error("Failed to open \"%s\" for reading+writing: %s", name, strerror(errno)); - } else { - Defer closeInput{[&] { close(input); }}; - struct stat stat; - if (fstat(input, &stat) == -1) { - error("Failed to stat \"%s\": %s", name, strerror(errno)); // LCOV_EXCL_LINE - } else if (!S_ISREG(stat.st_mode)) { // We do not support FIFOs or symlinks - // LCOV_EXCL_START - error("\"%s\" is not a regular file, and thus cannot be modified in-place", name); - // LCOV_EXCL_STOP - } else if (stat.st_size < 0x150) { - // This check is in theory redundant with the one in `processFile`, but it - // prevents passing a file size of 0, which usually indicates pipes - error( - "\"%s\" too short, expected at least 336 ($150) bytes, got only %jd", - name, - static_cast(stat.st_size) - ); - } else { - if (!outputName) { - output = input; - } - processFile(input, output, name, stat.st_size, true); - } - } - - return checkErrors(name); -} - static void parseByte(uint16_t &output, char name) { if (musl_optarg[0] == 0) { fatal("Argument to option '-%c' may not be empty", name); @@ -632,25 +110,31 @@ static void parseByte(uint16_t &output, char name) { output = value; } +static uint8_t const nintendoLogo[] = { + 0xCE, 0xED, 0x66, 0x66, 0xCC, 0x0D, 0x00, 0x0B, 0x03, 0x73, 0x00, 0x83, 0x00, 0x0C, 0x00, 0x0D, + 0x00, 0x08, 0x11, 0x1F, 0x88, 0x89, 0x00, 0x0E, 0xDC, 0xCC, 0x6E, 0xE6, 0xDD, 0xDD, 0xD9, 0x99, + 0xBB, 0xBB, 0x67, 0x63, 0x6E, 0x0E, 0xEC, 0xCC, 0xDD, 0xDC, 0x99, 0x9F, 0xBB, 0xB9, 0x33, 0x3E, +}; + static void initLogo() { - if (logoFilename) { + if (options.logoFilename) { FILE *logoFile; - if (strcmp(logoFilename, "-")) { - logoFile = fopen(logoFilename, "rb"); + if (strcmp(options.logoFilename, "-")) { + logoFile = fopen(options.logoFilename, "rb"); } else { - logoFilename = ""; + options.logoFilename = ""; (void)setmode(STDIN_FILENO, O_BINARY); logoFile = stdin; } if (!logoFile) { - fatal("Failed to open \"%s\" for reading: %s", logoFilename, strerror(errno)); + fatal("Failed to open \"%s\" for reading: %s", options.logoFilename, strerror(errno)); } Defer closeLogo{[&] { fclose(logoFile); }}; - uint8_t logoBpp[sizeof(logo)]; + uint8_t logoBpp[sizeof(options.logo)]; if (size_t nbRead = fread(logoBpp, 1, sizeof(logoBpp), logoFile); - nbRead != sizeof(logo) || fgetc(logoFile) != EOF || ferror(logoFile)) { - fatal("\"%s\" is not %zu bytes", logoFilename, sizeof(logo)); + nbRead != sizeof(options.logo) || fgetc(logoFile) != EOF || ferror(logoFile)) { + fatal("\"%s\" is not %zu bytes", options.logoFilename, sizeof(options.logo)); } auto highs = [&logoBpp](size_t i) { return (logoBpp[i * 2] & 0xF0) | ((logoBpp[i * 2 + 1] & 0xF0) >> 4); @@ -658,24 +142,25 @@ static void initLogo() { auto lows = [&logoBpp](size_t i) { return ((logoBpp[i * 2] & 0x0F) << 4) | (logoBpp[i * 2 + 1] & 0x0F); }; - constexpr size_t mid = sizeof(logo) / 2; + constexpr size_t mid = sizeof(options.logo) / 2; for (size_t i = 0; i < mid; i += 4) { - logo[i + 0] = highs(i + 0); - logo[i + 1] = highs(i + 1); - logo[i + 2] = lows(i + 0); - logo[i + 3] = lows(i + 1); - logo[mid + i + 0] = highs(i + 2); - logo[mid + i + 1] = highs(i + 3); - logo[mid + i + 2] = lows(i + 2); - logo[mid + i + 3] = lows(i + 3); + options.logo[i + 0] = highs(i + 0); + options.logo[i + 1] = highs(i + 1); + options.logo[i + 2] = lows(i + 0); + options.logo[i + 3] = lows(i + 1); + options.logo[mid + i + 0] = highs(i + 2); + options.logo[mid + i + 1] = highs(i + 3); + options.logo[mid + i + 2] = lows(i + 2); + options.logo[mid + i + 3] = lows(i + 3); } } else { - memcpy(logo, nintendoLogo, sizeof(nintendoLogo)); + static_assert(sizeof(options.logo) == sizeof(nintendoLogo)); + memcpy(options.logo, nintendoLogo, sizeof(nintendoLogo)); } - if (fixSpec & TRASH_LOGO) { - for (uint16_t i = 0; i < sizeof(logo); ++i) { - logo[i] = 0xFF ^ logo[i]; + if (options.fixSpec & TRASH_LOGO) { + for (uint16_t i = 0; i < sizeof(options.logo); ++i) { + options.logo[i] = 0xFF ^ options.logo[i]; } } } @@ -688,23 +173,24 @@ int main(int argc, char *argv[]) { switch (ch) { case 'C': case 'c': - model = ch == 'c' ? BOTH : CGB; - if (titleLen > 15) { - titleLen = 15; - warning(WARNING_TRUNCATION, "Truncating title \"%s\" to 15 chars", title); + options.model = ch == 'c' ? BOTH : CGB; + if (options.titleLen > 15) { + options.titleLen = 15; + assume(options.title != nullptr); + warning(WARNING_TRUNCATION, "Truncating title \"%s\" to 15 chars", options.title); } break; case 'f': - fixSpec = 0; + options.fixSpec = 0; while (*musl_optarg) { switch (*musl_optarg) { #define OVERRIDE_SPEC(cur, bad, curFlag, badFlag) \ case STR(cur)[0]: \ - if (fixSpec & badFlag) { \ + if (options.fixSpec & badFlag) { \ warnx("'" STR(cur) "' overriding '" STR(bad) "' in fix spec"); \ } \ - fixSpec = (fixSpec & ~badFlag) | curFlag; \ + options.fixSpec = (options.fixSpec & ~badFlag) | curFlag; \ break #define overrideSpecs(fix, fixFlag, trash, trashFlag) \ OVERRIDE_SPEC(fix, trash, fixFlag, trashFlag); \ @@ -728,48 +214,52 @@ int main(int argc, char *argv[]) { // LCOV_EXCL_STOP case 'i': { - gameID = musl_optarg; - size_t len = strlen(gameID); + options.gameID = musl_optarg; + size_t len = strlen(options.gameID); if (len > 4) { len = 4; - warning(WARNING_TRUNCATION, "Truncating game ID \"%s\" to 4 chars", gameID); + warning(WARNING_TRUNCATION, "Truncating game ID \"%s\" to 4 chars", options.gameID); } - gameIDLen = len; - if (titleLen > 11) { - titleLen = 11; - warning(WARNING_TRUNCATION, "Truncating title \"%s\" to 11 chars", title); + options.gameIDLen = len; + if (options.titleLen > 11) { + options.titleLen = 11; + assume(options.title != nullptr); + warning(WARNING_TRUNCATION, "Truncating title \"%s\" to 11 chars", options.title); } break; } case 'j': - japanese = false; + options.japanese = false; break; case 'k': { - newLicensee = musl_optarg; - size_t len = strlen(newLicensee); + options.newLicensee = musl_optarg; + size_t len = strlen(options.newLicensee); if (len > 2) { len = 2; warning( - WARNING_TRUNCATION, "Truncating new licensee \"%s\" to 2 chars", newLicensee + WARNING_TRUNCATION, + "Truncating new licensee \"%s\" to 2 chars", + options.newLicensee ); } - newLicenseeLen = len; + options.newLicenseeLen = len; break; } case 'L': - logoFilename = musl_optarg; + options.logoFilename = musl_optarg; break; case 'l': - parseByte(oldLicensee, 'l'); + parseByte(options.oldLicensee, 'l'); break; case 'm': - cartridgeType = mbc_ParseName(musl_optarg, tpp1Rev[0], tpp1Rev[1]); - if (cartridgeType == ROM_RAM || cartridgeType == ROM_RAM_BATTERY) { + options.cartridgeType = + mbc_ParseName(musl_optarg, options.tpp1Rev[0], options.tpp1Rev[1]); + if (options.cartridgeType == ROM_RAM || options.cartridgeType == ROM_RAM_BATTERY) { warning( WARNING_MBC, "MBC \"%s\" is under-specified and poorly supported", musl_optarg ); @@ -777,7 +267,7 @@ int main(int argc, char *argv[]) { break; case 'n': - parseByte(romVersion, 'n'); + parseByte(options.romVersion, 'n'); break; case 'O': @@ -790,27 +280,29 @@ int main(int argc, char *argv[]) { break; case 'p': - parseByte(padValue, 'p'); + parseByte(options.padValue, 'p'); break; case 'r': - parseByte(ramSize, 'r'); + parseByte(options.ramSize, 'r'); break; case 's': - sgb = true; + options.sgb = true; break; case 't': { - title = musl_optarg; - size_t len = strlen(title); - uint8_t maxLen = maxTitleLen(); + options.title = musl_optarg; + size_t len = strlen(options.title); + uint8_t maxLen = options.gameID ? 11 : options.model != DMG ? 15 : 16; if (len > maxLen) { len = maxLen; - warning(WARNING_TRUNCATION, "Truncating title \"%s\" to %u chars", title, maxLen); + warning( + WARNING_TRUNCATION, "Truncating title \"%s\" to %u chars", options.title, maxLen + ); } - titleLen = len; + options.titleLen = len; break; } @@ -820,7 +312,7 @@ int main(int argc, char *argv[]) { exit(0); case 'v': - fixSpec = FIX_LOGO | FIX_HEADER_SUM | FIX_GLOBAL_SUM; + options.fixSpec = FIX_LOGO | FIX_HEADER_SUM | FIX_GLOBAL_SUM; break; // LCOV_EXCL_STOP @@ -843,51 +335,51 @@ int main(int argc, char *argv[]) { } } - if ((cartridgeType & 0xFF00) == TPP1 && !japanese) { + if ((options.cartridgeType & 0xFF00) == TPP1 && !options.japanese) { warning( WARNING_MBC, "TPP1 overwrites region flag for its identification code, ignoring '-j'" ); } // Check that RAM size is correct for "standard" mappers - if (ramSize != UNSPECIFIED && (cartridgeType & 0xFF00) == 0) { - if (cartridgeType == ROM_RAM || cartridgeType == ROM_RAM_BATTERY) { - if (ramSize != 1) { + if (options.ramSize != UNSPECIFIED && (options.cartridgeType & 0xFF00) == 0) { + if (options.cartridgeType == ROM_RAM || options.cartridgeType == ROM_RAM_BATTERY) { + if (options.ramSize != 1) { warning( WARNING_MBC, "MBC \"%s\" should have 2 KiB of RAM (\"-r 1\")", - mbc_Name(cartridgeType) + mbc_Name(options.cartridgeType) ); } - } else if (mbc_HasRAM(cartridgeType)) { - if (!ramSize) { + } else if (mbc_HasRAM(options.cartridgeType)) { + if (!options.ramSize) { warning( WARNING_MBC, "MBC \"%s\" has RAM, but RAM size was set to 0", - mbc_Name(cartridgeType) + mbc_Name(options.cartridgeType) ); - } else if (ramSize == 1) { + } else if (options.ramSize == 1) { warning( WARNING_MBC, "RAM size 1 (2 KiB) was specified for MBC \"%s\"", - mbc_Name(cartridgeType) + mbc_Name(options.cartridgeType) ); } - } else if (ramSize) { + } else if (options.ramSize) { warning( WARNING_MBC, "MBC \"%s\" has no RAM, but RAM size was set to %u", - mbc_Name(cartridgeType), - ramSize + mbc_Name(options.cartridgeType), + options.ramSize ); } } - if (sgb && oldLicensee != UNSPECIFIED && oldLicensee != 0x33) { + if (options.sgb && options.oldLicensee != UNSPECIFIED && options.oldLicensee != 0x33) { warning( WARNING_SGB, "SGB compatibility enabled, but old licensee is 0x%02x, not 0x33", - oldLicensee + options.oldLicensee ); } @@ -904,7 +396,7 @@ int main(int argc, char *argv[]) { bool failed = warnings.nbErrors > 0; do { - failed |= processFilename(*argv, outputFilename); + failed |= fix_ProcessFile(*argv, outputFilename); } while (*++argv); return failed;