// SPDX-License-Identifier: MIT #include "fix/main.hpp" #include #include #include #include #include #include #include #include #include #include "cli.hpp" #include "diagnostics.hpp" #include "helpers.hpp" #include "platform.hpp" #include "style.hpp" #include "usage.hpp" #include "util.hpp" #include "version.hpp" #include "fix/fix.hpp" #include "fix/mbc.hpp" #include "fix/warning.hpp" Options options; // Flags which must be processed after the option parsing finishes static struct LocalOptions { std::optional outputFileName; // -o std::vector inputFileNames; // ... } localOptions; // Short options static char const *optstring = "Ccf:hi:jk:L:l:m:n:Oo:p:r:st:VvW:w"; // Long-only option variable static int longOpt; // `--color` // Equivalent long options // Please keep in the same order as short opts. // Also, make sure long opts don't create ambiguity: // A long opt's name should start with the same letter as its short opt, // except if it doesn't create any ambiguity (`verbose` versus `version`). // This is because long opt matching, even to a single char, is prioritized // over short opt matching. static option const longopts[] = { {"color-only", no_argument, nullptr, 'C'}, {"color-compatible", no_argument, nullptr, 'c'}, {"fix-spec", required_argument, nullptr, 'f'}, {"help", no_argument, nullptr, 'h'}, {"game-id", required_argument, nullptr, 'i'}, {"non-japanese", no_argument, nullptr, 'j'}, {"new-licensee", required_argument, nullptr, 'k'}, {"logo", required_argument, nullptr, 'L'}, {"old-licensee", required_argument, nullptr, 'l'}, {"mbc-type", required_argument, nullptr, 'm'}, {"rom-version", required_argument, nullptr, 'n'}, {"overwrite", no_argument, nullptr, 'O'}, {"output", required_argument, nullptr, 'o'}, {"pad-value", required_argument, nullptr, 'p'}, {"ram-size", required_argument, nullptr, 'r'}, {"sgb-compatible", no_argument, nullptr, 's'}, {"title", required_argument, nullptr, 't'}, {"version", no_argument, nullptr, 'V'}, {"validate", no_argument, nullptr, 'v'}, {"warning", required_argument, nullptr, 'W'}, {"color", required_argument, &longOpt, 'c'}, {nullptr, no_argument, nullptr, 0 }, }; // clang-format off: nested initializers static Usage usage = { .name = "rgbfix", .flags = { "[-hjOsVvw]", "[-C | -c]", "[-f ]", "[-i ]", "[-k ]", "[-L ]", "[-l ]", "[-m ]", "[-n ]", "[-p ]", "[-r ]", "[-t ]", "[-W warning]", " ...", }, .options = { { {"-m", "--mbc-type "}, { "set the MBC type byte to this value; \"-m help\"", "or \"-m list\" prints the accepted values", }, }, {{"-p", "--pad-value "}, {"pad to the next valid size using this value"}}, {{"-r", "--ram-size "}, {"set the cart RAM size byte to this value"}}, {{"-o", "--output "}, {"set the output file"}}, {{"-V", "--version"}, {"print RGBFIX version and exit"}}, {{"-v", "--validate"}, {"fix the header logo and both checksums (\"-f lhg\")"}}, {{"-W", "--warning "}, {"enable or disable warnings"}}, }, }; // clang-format on static uint16_t parseByte(char const *input, char name) { if (std::optional value = parseWholeNumber(input); !value) { fatal("Invalid argument for option '-%c'", name); } else if (*value > 0xFF) { fatal("Argument for option '-%c' must be between 0 and 0xFF", name); } else { return *value; } } static void parseArg(int ch, char *arg) { switch (ch) { case 'C': case 'c': options.model = ch == 'c' ? BOTH : CGB; if (options.titleLen > 15) { options.titleLen = 15; assume(options.title.has_value()); warning( WARNING_TRUNCATION, "Truncating title \"%s\" to 15 chars", options.title->c_str() ); } break; case 'f': options.fixSpec = 0; while (*arg) { switch (*arg) { #define overrideSpec(cur, bad, curFlag, badFlag) \ case cur: \ if (options.fixSpec & badFlag) { \ warnx("'%c' overriding '%c' in fix spec", cur, bad); \ } \ options.fixSpec = (options.fixSpec & ~badFlag) | curFlag; \ break #define overrideSpecPair(fix, fixFlag, trash, trashFlag) \ overrideSpec(fix, trash, fixFlag, trashFlag); \ overrideSpec(trash, fix, trashFlag, fixFlag) overrideSpecPair('l', FIX_LOGO, 'L', TRASH_LOGO); overrideSpecPair('h', FIX_HEADER_SUM, 'H', TRASH_HEADER_SUM); overrideSpecPair('g', FIX_GLOBAL_SUM, 'G', TRASH_GLOBAL_SUM); #undef overrideSpec #undef overrideSpecPair default: fatal("Invalid character '%c' in fix spec", *arg); } ++arg; } break; // LCOV_EXCL_START case 'h': usage.printAndExit(0); // LCOV_EXCL_STOP case 'i': { options.gameID = arg; size_t len = options.gameID->length(); if (len > 4) { len = 4; warning( WARNING_TRUNCATION, "Truncating game ID \"%s\" to 4 chars", options.gameID->c_str() ); } options.gameIDLen = len; if (options.titleLen > 11) { options.titleLen = 11; assume(options.title.has_value()); warning( WARNING_TRUNCATION, "Truncating title \"%s\" to 11 chars", options.title->c_str() ); } break; } case 'j': options.japanese = false; break; case 'k': { options.newLicensee = arg; size_t len = options.newLicensee->length(); if (len > 2) { len = 2; warning( WARNING_TRUNCATION, "Truncating new licensee \"%s\" to 2 chars", options.newLicensee->c_str() ); } options.newLicenseeLen = len; break; } case 'L': options.logoFilename = arg; break; case 'l': options.oldLicensee = parseByte(arg, 'l'); break; case 'm': options.cartridgeType = mbc_ParseName(arg, 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", arg); } break; case 'n': options.romVersion = parseByte(arg, 'n'); break; case 'O': warning(WARNING_OBSOLETE, "'-O' is deprecated; use '-Wno-overwrite' instead"); warnings.processWarningFlag("no-overwrite"); break; case 'o': localOptions.outputFileName = arg; break; case 'p': options.padValue = parseByte(arg, 'p'); break; case 'r': options.ramSize = parseByte(arg, 'r'); break; case 's': options.sgb = true; break; case 't': { options.title = arg; size_t len = options.title->length(); 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", options.title->c_str(), maxLen ); } options.titleLen = len; break; } // LCOV_EXCL_START case 'V': printf("rgbfix %s\n", get_package_version_string()); exit(0); case 'v': options.fixSpec = FIX_LOGO | FIX_HEADER_SUM | FIX_GLOBAL_SUM; break; // LCOV_EXCL_STOP case 'W': warnings.processWarningFlag(arg); break; case 'w': warnings.state.warningsEnabled = false; break; case 0: // Long-only options if (longOpt == 'c' && !style_Parse(arg)) { fatal("Invalid argument for option '--color'"); } break; case 1: // Positional arguments localOptions.inputFileNames.push_back(arg); break; // LCOV_EXCL_START default: usage.printAndExit(1); // LCOV_EXCL_STOP } } 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 (options.logoFilename) { FILE *logoFile; char const *logoFilename = options.logoFilename->c_str(); if (*options.logoFilename != "-") { logoFile = fopen(logoFilename, "rb"); } else { // LCOV_EXCL_START logoFilename = ""; (void)setmode(STDIN_FILENO, O_BINARY); logoFile = stdin; // LCOV_EXCL_STOP } if (!logoFile) { // LCOV_EXCL_START fatal("Failed to open \"%s\" for reading: %s", logoFilename, strerror(errno)); // LCOV_EXCL_STOP } Defer closeLogo{[&] { fclose(logoFile); }}; uint8_t logoBpp[sizeof(options.logo)]; if (size_t nbRead = fread(logoBpp, 1, sizeof(logoBpp), logoFile); nbRead != sizeof(options.logo) || fgetc(logoFile) != EOF || ferror(logoFile)) { fatal("\"%s\" is not %zu bytes", logoFilename, sizeof(options.logo)); } auto highs = [&logoBpp](size_t i) { return (logoBpp[i * 2] & 0xF0) | ((logoBpp[i * 2 + 1] & 0xF0) >> 4); }; auto lows = [&logoBpp](size_t i) { return ((logoBpp[i * 2] & 0x0F) << 4) | (logoBpp[i * 2 + 1] & 0x0F); }; constexpr size_t mid = sizeof(options.logo) / 2; for (size_t i = 0; i < mid; i += 4) { 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 { static_assert(sizeof(options.logo) == sizeof(nintendoLogo)); memcpy(options.logo, nintendoLogo, sizeof(nintendoLogo)); } if (options.fixSpec & TRASH_LOGO) { for (uint16_t i = 0; i < sizeof(options.logo); ++i) { options.logo[i] = 0xFF ^ options.logo[i]; } } } int main(int argc, char *argv[]) { cli_ParseArgs(argc, argv, optstring, longopts, parseArg, usage); 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 (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(options.cartridgeType) ); } } 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(options.cartridgeType) ); } else if (options.ramSize == 1) { warning( WARNING_MBC, "RAM size 1 (2 KiB) was specified for MBC \"%s\"", mbc_Name(options.cartridgeType) ); } } else if (options.ramSize) { warning( WARNING_MBC, "MBC \"%s\" has no RAM, but RAM size was set to %u", mbc_Name(options.cartridgeType), options.ramSize ); } } if (options.sgb && options.oldLicensee != UNSPECIFIED && options.oldLicensee != 0x33) { warning( WARNING_SGB, "SGB compatibility enabled, but old licensee is 0x%02x, not 0x33", options.oldLicensee ); } initLogo(); if (localOptions.inputFileNames.empty()) { usage.printAndExit("No input file specified (pass \"-\" to read from standard input)"); } if (localOptions.outputFileName && localOptions.inputFileNames.size() != 1) { usage.printAndExit("If '-o' is set then only a single input file may be specified"); } char const *outputFileName = localOptions.outputFileName ? localOptions.outputFileName->c_str() : nullptr; bool failed = warnings.nbErrors > 0; for (std::string const &inputFileName : localOptions.inputFileNames) { failed |= fix_ProcessFile(inputFileName.c_str(), outputFileName); } return failed; }