mirror of
https://github.com/gbdev/rgbds.git
synced 2025-11-20 10:12:06 +00:00
Separate RGBFIX header fixing from CLI option parsing (#1808)
This commit is contained in:
1
Makefile
1
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
|
||||
|
||||
8
include/fix/fix.hpp
Normal file
8
include/fix/fix.hpp
Normal file
@@ -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
|
||||
49
include/fix/main.hpp
Normal file
49
include/fix/main.hpp
Normal file
@@ -0,0 +1,49 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#ifndef RGBDS_FIX_MAIN_HPP
|
||||
#define RGBDS_FIX_MAIN_HPP
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#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
|
||||
@@ -80,6 +80,7 @@ set(rgblink_src
|
||||
)
|
||||
|
||||
set(rgbfix_src
|
||||
"fix/fix.cpp"
|
||||
"fix/main.cpp"
|
||||
"fix/mbc.cpp"
|
||||
"fix/warning.cpp"
|
||||
|
||||
522
src/fix/fix.cpp
Normal file
522
src/fix/fix.cpp
Normal file
@@ -0,0 +1,522 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "fix/fix.hpp"
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include <errno.h>
|
||||
#include <limits.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <vector>
|
||||
|
||||
#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<intmax_t>(headerSize),
|
||||
static_cast<intmax_t>(headerSize),
|
||||
static_cast<intmax_t>(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<uint8_t const *>(options.title), options.titleLen, "title"
|
||||
);
|
||||
}
|
||||
|
||||
if (options.gameID) {
|
||||
overwriteBytes(
|
||||
rom0,
|
||||
0x13F,
|
||||
reinterpret_cast<uint8_t const *>(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<uint8_t const *>(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<uint8_t> 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<uint8_t>(globalSum >> 8), static_cast<uint8_t>(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<off_t>(-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<intmax_t>(writeLen),
|
||||
name,
|
||||
static_cast<intmax_t>(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<size_t>(writeLen) < totalRomxLen) {
|
||||
// LCOV_EXCL_START
|
||||
error(
|
||||
"Could only write %jd of \"%s\"'s %zu ROMX bytes",
|
||||
static_cast<intmax_t>(writeLen),
|
||||
name,
|
||||
totalRomxLen
|
||||
);
|
||||
return;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
}
|
||||
|
||||
// Output padding
|
||||
if (options.padValue != UNSPECIFIED) {
|
||||
if (input == output) {
|
||||
if (lseek(output, 0, SEEK_END) == static_cast<off_t>(-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<size_t>(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 = "<stdin>";
|
||||
(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<intmax_t>(stat.st_size)
|
||||
);
|
||||
} else {
|
||||
if (!outputName) {
|
||||
output = input;
|
||||
}
|
||||
processFile(input, output, name, stat.st_size, true);
|
||||
}
|
||||
}
|
||||
|
||||
return checkErrors(name);
|
||||
}
|
||||
682
src/fix/main.cpp
682
src/fix/main.cpp
@@ -1,6 +1,6 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include <sys/stat.h>
|
||||
#include "fix/main.hpp"
|
||||
|
||||
#include <errno.h>
|
||||
#include <limits.h>
|
||||
@@ -9,7 +9,6 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <vector>
|
||||
|
||||
#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<intmax_t>(headerSize),
|
||||
static_cast<intmax_t>(headerSize),
|
||||
static_cast<intmax_t>(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<uint8_t const *>(title), titleLen, "title");
|
||||
}
|
||||
|
||||
if (gameID) {
|
||||
overwriteBytes(
|
||||
rom0, 0x13F, reinterpret_cast<uint8_t const *>(gameID), gameIDLen, "manufacturer code"
|
||||
);
|
||||
}
|
||||
|
||||
if (model != DMG) {
|
||||
overwriteByte(rom0, 0x143, model == BOTH ? 0x80 : 0xC0, "CGB flag");
|
||||
}
|
||||
|
||||
if (newLicensee) {
|
||||
overwriteBytes(
|
||||
rom0,
|
||||
0x144,
|
||||
reinterpret_cast<uint8_t const *>(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<uint8_t> 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<uint8_t>(globalSum >> 8), static_cast<uint8_t>(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<off_t>(-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<intmax_t>(writeLen),
|
||||
name,
|
||||
static_cast<intmax_t>(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<size_t>(writeLen) < totalRomxLen) {
|
||||
// LCOV_EXCL_START
|
||||
error(
|
||||
"Could only write %jd of \"%s\"'s %zu ROMX bytes",
|
||||
static_cast<intmax_t>(writeLen),
|
||||
name,
|
||||
totalRomxLen
|
||||
);
|
||||
return;
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
}
|
||||
|
||||
// Output padding
|
||||
if (padValue != UNSPECIFIED) {
|
||||
if (input == output) {
|
||||
if (lseek(output, 0, SEEK_END) == static_cast<off_t>(-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<size_t>(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 = "<stdin>";
|
||||
(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<intmax_t>(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 = "<stdin>";
|
||||
options.logoFilename = "<stdin>";
|
||||
(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;
|
||||
|
||||
Reference in New Issue
Block a user