shithub: rgbds

Download patch

ref: 41d544a4ebb7ef7d6343bfb5a96bcbd553d57b4f
parent: f28b4abafcedefc73a8406ca8cb35c286b916866
author: ISSOtm <[email protected]>
date: Fri Dec 25 20:53:16 EST 2020

Rewrite RGBFIX

- Make it work inside pipelines
- Add RGBFIX tests to the suite
- Be more flexible in accepted MBC names
- Add warnings for dangerous or nonsensical input params
- Improve man page

--- a/Makefile
+++ b/Makefile
@@ -203,7 +203,7 @@
 	$Qenv $(MAKE) -j WARNFLAGS="-Werror -Wall -Wextra -Wpedantic -Wno-type-limits \
 		-Wno-sign-compare -Wvla -Wformat -Wformat-security -Wformat-overflow=2 \
 		-Wformat-truncation=1 -Wformat-y2k -Wswitch-enum -Wunused \
-		-Wuninitialized -Wunknown-pragmas -Wstrict-overflow=5 \
+		-Wuninitialized -Wunknown-pragmas -Wstrict-overflow=4 \
 		-Wstringop-overflow=4 -Walloc-zero -Wduplicated-cond \
 		-Wfloat-equal -Wshadow -Wcast-qual -Wcast-align -Wlogical-op \
 		-Wnested-externs -Wno-aggressive-loop-optimizations -Winline \
--- a/include/helpers.h
+++ b/include/helpers.h
@@ -32,6 +32,58 @@
 	static inline _Noreturn unreachable_(void) {}
 #endif
 
+// Use builtins whenever possible, and shim them otherwise
+#ifdef __GNUC__ // GCC or compatible
+	#define ctz __builtin_ctz
+	#define clz __builtin_clz
+
+#elif defined(_MSC_VER)
+	#include <assert.h>
+	#include <intrin.h>
+	#pragma intrinsic(_BitScanReverse, _BitScanForward)
+	static inline int ctz(unsigned int x)
+	{
+		unsigned long cnt;
+
+		assert(x != 0);
+		_BitScanForward(&cnt, x);
+		return cnt;
+	}
+	static inline int clz(unsigned int x)
+	{
+		unsigned long cnt;
+
+		assert(x != 0);
+		_BitScanReverse(&cnt, x);
+		return 31 - cnt;
+	}
+
+#else
+	// FIXME: these are rarely used, and need testing...
+	#include <limits.h>
+	static inline int ctz(unsigned int x)
+	{
+		int cnt = 0;
+
+		while (!(x & 1)) {
+			x >>= 1;
+			cnt++;
+		}
+		return cnt;
+	}
+
+	static inline int clz(unsigned int x)
+	{
+		int cnt = 0;
+
+		while (x <= UINT_MAX / 2) {
+			x <<= 1;
+			cnt++;
+		}
+		return cnt;
+	}
+#endif
+
 // Macros for stringification
 #define STR(x) #x
 #define EXPAND_AND_STR(x) STR(x)
--- a/include/platform.h
+++ b/include/platform.h
@@ -34,9 +34,15 @@
 
 /* MSVC doesn't use POSIX types or defines for `read` */
 #ifdef _MSC_VER
+# include <io.h>
 # define STDIN_FILENO 0
+# define STDOUT_FILENO 1
+# define STDERR_FILENO 2
 # define ssize_t int
 # define SSIZE_MAX INT_MAX
+#else
+# include <fcntl.h>
+# include <unistd.h>
 #endif
 
 /* MSVC doesn't support `[static N]` for array arguments from C99 */
@@ -44,6 +50,24 @@
 # define MIN_NB_ELMS(N)
 #else
 # define MIN_NB_ELMS(N) static (N)
+#endif
+
+// MSVC uses a different name for O_RDWR, and needs an additional _O_BINARY flag
+#ifdef _MSC_VER
+# include <fcntl.h>
+# define O_RDWR _O_RDWR
+# define S_ISREG(field) ((field) & _S_IFREG)
+# define O_BINARY _O_BINARY
+#elif !defined(O_BINARY) // Cross-compilers define O_BINARY
+# define O_BINARY 0 // POSIX says we shouldn't care!
+#endif // _MSC_VER
+
+// Windows has stdin and stdout open as text by default, which we may not want
+#if defined(_MSC_VER) || defined(__MINGW32__)
+# include <io.h>
+# define setmode(fd, mode) _setmode(fd, mode)
+#else
+# define setmode(fd, mode) ((void)0)
 #endif
 
 #endif /* RGBDS_PLATFORM_H */
--- a/src/fix/main.c
+++ b/src/fix/main.c
@@ -1,11 +1,18 @@
 /*
  * This file is part of RGBDS.
  *
- * Copyright (c) 2010-2018, Anthony J. Bentley and RGBDS contributors.
+ * Copyright (c) 2020, Eldred habert and RGBDS contributors.
  *
  * SPDX-License-Identifier: MIT
  */
 
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <assert.h>
+#include <errno.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <stdarg.h>
 #include <stdbool.h>
 #include <stdint.h>
 #include <stdio.h>
@@ -12,13 +19,18 @@
 #include <stdlib.h>
 #include <string.h>
 
-#include "extern/err.h"
 #include "extern/getopt.h"
 
+#include "helpers.h"
+#include "platform.h"
 #include "version.h"
 
+#define UNSPECIFIED 0x100 // May not be in byte range
+
+#define BANK_SIZE 0x4000
+
 /* Short options */
-static char const *optstring = "Ccf:i:jk:l:m:n:p:r:st:Vv";
+static const char *optstring = "Ccf:i:jk:l:m:n:p:r:st:Vv";
 
 /*
  * Equivalent long options
@@ -49,12 +61,12 @@
 	{ NULL,               no_argument,       NULL, 0   }
 };
 
-static void print_usage(void)
+static void printUsage(void)
 {
 	fputs(
 "Usage: rgbfix [-jsVv] [-C | -c] [-f <fix_spec>] [-i <game_id>] [-k <licensee>]\n"
 "              [-l <licensee_byte>] [-m <mbc_type>] [-n <rom_version>]\n"
-"              [-p <pad_value>] [-r <ram_size>] [-t <title_str>] <file>\n"
+"              [-p <pad_value>] [-r <ram_size>] [-t <title_str>] [<file> ...]\n"
 "Useful options:\n"
 "    -m, --mbc-type <value>      set the MBC type byte to this value; refer\n"
 "                                  to the man page for a list of values\n"
@@ -65,535 +77,1109 @@
 "\n"
 "For help, use `man rgbfix' or go to https://rgbds.gbdev.io/docs/\n",
 	      stderr);
-	exit(1);
 }
 
-/*
- * Cartridge type names from Pan Docs, also allowing "_" instead of " "
- * and with "ROM" as an alias for "ROM ONLY".
- * https://gbdev.io/pandocs/#_0147-cartridge-type
- */
-struct {
-	long value;
-	const char *name;
-} cartridge_types[] = {
-	{0x00, "ROM ONLY"},
-	{0x00, "ROM_ONLY"},
-	{0x00, "ROM"},
-	{0x01, "MBC1"},
-	{0x02, "MBC1+RAM"},
-	{0x03, "MBC1+RAM+BATTERY"},
-	{0x05, "MBC2"},
-	{0x06, "MBC2+BATTERY"},
-	{0x08, "ROM+RAM"},
-	{0x09, "ROM+RAM+BATTERY"},
-	{0x0B, "MMM01"},
-	{0x0C, "MMM01+RAM"},
-	{0x0D, "MMM01+RAM+BATTERY"},
-	{0x0F, "MBC3+TIMER+BATTERY"},
-	{0x10, "MBC3+TIMER+RAM+BATTERY"},
-	{0x11, "MBC3"},
-	{0x12, "MBC3+RAM"},
-	{0x13, "MBC3+RAM+BATTERY"},
-	{0x19, "MBC5"},
-	{0x1A, "MBC5+RAM"},
-	{0x1B, "MBC5+RAM+BATTERY"},
-	{0x1C, "MBC5+RUMBLE"},
-	{0x1D, "MBC5+RUMBLE+RAM"},
-	{0x1E, "MBC5+RUMBLE+RAM+BATTERY"},
-	{0x20, "MBC6"},
-	{0x22, "MBC7+SENSOR+RUMBLE+RAM+BATTERY"},
-	{0xFC, "POCKET CAMERA"},
-	{0xFC, "POCKET_CAMERA"},
-	{0xFD, "BANDAI TAMA5"},
-	{0xFD, "BANDAI_TAMA5"},
-	{0xFE, "HuC3"},
-	{0xFF, "HuC1+RAM+BATTERY"},
+enum MbcType {
+	ROM  = 0x00,
+	ROM_RAM = 0x08,
+	ROM_RAM_BATTERY = 0x09,
+
+	MBC1 = 0x01,
+	MBC1_RAM = 0x02,
+	MBC1_RAM_BATTERY = 0x03,
+
+	MBC2 = 0x05,
+	MBC2_BATTERY = 0x06,
+
+	MMM01 = 0x0B,
+	MMM01_RAM = 0x0C,
+	MMM01_RAM_BATTERY = 0x0D,
+
+	MBC3 = 0x11,
+	MBC3_TIMER_BATTERY = 0x0F,
+	MBC3_TIMER_RAM_BATTERY = 0x10,
+	MBC3_RAM = 0x12,
+	MBC3_RAM_BATTERY = 0x13,
+
+	MBC5 = 0x19,
+	MBC5_RAM = 0x1A,
+	MBC5_RAM_BATTERY = 0x1B,
+	MBC5_RUMBLE = 0x1C,
+	MBC5_RUMBLE_RAM = 0x1D,
+	MBC5_RUMBLE_RAM_BATTERY = 0x1E,
+
+	MBC6 = 0x20,
+
+	MBC7_SENSOR_RUMBLE_RAM_BATTERY = 0x22,
+
+	POCKET_CAMERA = 0xFC,
+
+	BANDAI_TAMA5 = 0xFD,
+
+	HUC3 = 0xFE,
+
+	HUC1_RAM_BATTERY = 0xFF,
+
+	// Error values
+	MBC_NONE = UNSPECIFIED, // No MBC specified, do not act on it
+	MBC_BAD, // Specified MBC does not exist / syntax error
+	MBC_WRONG_FEATURES, // MBC incompatible with specified features
+	MBC_BAD_RANGE, // MBC number out of range
 };
 
-static long parse_cartridge(char *arg, char **ep)
+/**
+ * @return False on failure
+ */
+static bool readMBCSlice(char const **name, char const *expected)
 {
-	for (int i = 0; i < sizeof(cartridge_types) / sizeof(cartridge_types[0]); i++) {
-		if (!strcmp(arg, cartridge_types[i].name)) {
-			*ep = arg + strlen(arg);
-			return cartridge_types[i].value;
-		}
+	while (*expected) {
+		char c = *(*name)++;
+
+		if (c == '\0') // Name too short
+			return false;
+
+		if (c >= 'a' && c <= 'z') // Perform the comparison case-insensitive
+			c = c - 'a' + 'A';
+		else if (c == '_') // Treat underscores as spaces
+			c = ' ';
+
+		if (c != *expected++)
+			return false;
 	}
-	return strtoul(arg, ep, 0);
+	return true;
 }
 
-int main(int argc, char *argv[])
+static enum MbcType parseMBC(char const *name)
 {
-	FILE *rom;
-	int ch;
-	char *ep;
+	if (name[0] >= '0' && name[0] <= '9') {
+		// Parse number, and return it as-is (unless it's too large)
+		char *endptr;
+		unsigned long mbc = strtoul(name, &endptr, 0);
 
-	/*
-	 * Parse command-line options
-	 */
+		if (*endptr)
+			return MBC_BAD;
+		if (mbc > 0xFF)
+			return MBC_BAD_RANGE;
+		return mbc;
 
-	/* all flags default to false unless options specify otherwise */
-	bool fixlogo = false;
-	bool fixheadsum = false;
-	bool fixglobalsum = false;
-	bool trashlogo = false;
-	bool trashheadsum = false;
-	bool trashglobalsum = false;
-	bool settitle = false;
-	bool setid = false;
-	bool colorcompatible = false;
-	bool coloronly = false;
-	bool nonjapan = false;
-	bool setlicensee = false;
-	bool setnewlicensee = false;
-	bool super = false;
-	bool setcartridge = false;
-	bool setramsize = false;
-	bool resize = false;
-	bool setversion = false;
+	} else {
+		// Begin by reading the MBC type:
+		uint16_t mbc;
+		char const *ptr = name;
 
-	char *title = NULL; /* game title in ASCII */
-	char *id = NULL; /* game ID in ASCII */
-	char *newlicensee = NULL; /* new licensee ID, two ASCII characters */
+		// Trim off leading whitespace
+		while (*ptr == ' ' || *ptr == '\t')
+			ptr++;
 
-	int licensee = 0;  /* old licensee ID */
-	int cartridge = 0; /* cartridge hardware ID */
-	int ramsize = 0;   /* RAM size ID */
-	int version = 0;   /* mask ROM version number */
-	int padvalue = 0;  /* to pad the rom with if it changes size */
+#define tryReadSlice(expected) \
+do { \
+	if (!readMBCSlice(&ptr, expected)) \
+		return MBC_BAD; \
+} while (0)
 
-	while ((ch = musl_getopt_long_only(argc, argv, optstring, longopts,
-					   NULL)) != -1) {
-		switch (ch) {
-		case 'C':
-			coloronly = true;
-			/* FALLTHROUGH */
-		case 'c':
-			colorcompatible = true;
+		switch (*ptr++) {
+		case 'R': // ROM / ROM_ONLY
+		case 'r':
+			tryReadSlice("OM");
+			// Handle optional " ONLY"
+			while (*ptr == ' ' || *ptr == '\t' || *ptr == '_')
+				ptr++;
+			if (*ptr == 'O' || *ptr == 'o') {
+				ptr++;
+				tryReadSlice("NLY");
+			}
+			mbc = ROM;
 			break;
-		case 'f':
-			fixlogo = strchr(optarg, 'l');
-			fixheadsum = strchr(optarg, 'h');
-			fixglobalsum = strchr(optarg, 'g');
-			trashlogo = strchr(optarg, 'L');
-			trashheadsum = strchr(optarg, 'H');
-			trashglobalsum = strchr(optarg, 'G');
+
+		case 'M': // MBC{1, 2, 3, 5, 6, 7} / MMM01
+		case 'm':
+			switch (*ptr++) {
+			case 'B':
+			case 'b':
+				switch (*ptr++) {
+				case 'C':
+				case 'c':
+					break;
+				default:
+					return MBC_BAD;
+				}
+				switch (*ptr++) {
+				case '1':
+					mbc = MBC1;
+					break;
+				case '2':
+					mbc = MBC2;
+					break;
+				case '3':
+					mbc = MBC3;
+					break;
+				case '5':
+					mbc = MBC5;
+					break;
+				case '6':
+					mbc = MBC6;
+					break;
+				case '7':
+					mbc = MBC7_SENSOR_RUMBLE_RAM_BATTERY;
+					break;
+				default:
+					return MBC_BAD;
+				}
+				break;
+			case 'M':
+			case 'm':
+				tryReadSlice("M01");
+				mbc = MMM01;
+				break;
+			default:
+				return MBC_BAD;
+			}
 			break;
-		case 'i':
-			setid = true;
 
-			if (strlen(optarg) != 4)
-				errx(1, "Game ID %s must be exactly 4 characters",
-				     optarg);
+		case 'P': // POCKET_CAMERA
+		case 'p':
+			tryReadSlice("OCKET CAMERA");
+			mbc = POCKET_CAMERA;
+			break;
 
-			id = optarg;
+		case 'B': // BANDAI_TAMA5
+		case 'b':
+			tryReadSlice("ANDAI TAMA5");
+			mbc = BANDAI_TAMA5;
 			break;
-		case 'j':
-			nonjapan = true;
+
+		case 'T': // TAMA5
+		case 't':
+			tryReadSlice("AMA5");
+			mbc = BANDAI_TAMA5;
 			break;
-		case 'k':
-			setnewlicensee = true;
 
-			if (strlen(optarg) != 2)
-				errx(1, "New licensee code %s is not the correct length of 2 characters",
-				     optarg);
-
-			newlicensee = optarg;
+		case 'H': // HuC{1, 3}
+		case 'h':
+			tryReadSlice("UC");
+			switch (*ptr++) {
+			case '1':
+				mbc = HUC1_RAM_BATTERY;
+				break;
+			case '3':
+				mbc = HUC3;
+				break;
+			default:
+				return MBC_BAD;
+			}
 			break;
-		case 'l':
-			setlicensee = true;
 
-			licensee = strtoul(optarg, &ep, 0);
-			if (optarg[0] == '\0' || *ep != '\0')
-				errx(1, "Invalid argument for option 'l'");
+		default:
+			return MBC_BAD;
+		}
 
-			if (licensee < 0 || licensee > 0xFF)
-				errx(1, "Argument for option 'l' must be between 0 and 255");
+		// Read "additional features"
+		uint8_t features = 0;
+#define RAM 0x80
+#define BATTERY 0x40
+#define TIMER 0x20
+#define RUMBLE 0x10
+#define SENSOR 0x08
 
-			break;
-		case 'm':
-			setcartridge = true;
+		for (;;) {
+			// Trim off trailing whitespace
+			while (*ptr == ' ' || *ptr == '\t' || *ptr == '_')
+				ptr++;
 
-			cartridge = parse_cartridge(optarg, &ep);
-			if (optarg[0] == '\0' || *ep != '\0')
-				errx(1, "Invalid argument for option 'm'");
+			// If done, start processing "features"
+			if (!*ptr)
+				break;
+			// We expect a '+' at this point
+			if (*ptr++ != '+')
+				return MBC_BAD;
+			// Trim off leading whitespace
+			while (*ptr == ' ' || *ptr == '\t' || *ptr == '_')
+				ptr++;
 
-			if (cartridge < 0 || cartridge > 0xFF)
-				errx(1, "Argument for option 'm' must be between 0 and 255");
+			switch (*ptr++) {
+			case 'B': // BATTERY
+			case 'b':
+				tryReadSlice("ATTERY");
+				features |= BATTERY;
+				break;
 
-			break;
-		case 'n':
-			setversion = true;
+			case 'R': // RAM or RUMBLE
+			case 'r':
+				switch (*ptr++) {
+				case 'U':
+				case 'u':
+					tryReadSlice("MBLE");
+					features |= RUMBLE;
+					break;
+				case 'A':
+				case 'a':
+					if (*ptr != 'M' && *ptr != 'm')
+						return MBC_BAD;
+					ptr++;
+					features |= RAM;
+					break;
+				default:
+					return MBC_BAD;
+				}
+				break;
 
-			version = strtoul(optarg, &ep, 0);
+			case 'S': // SENSOR
+			case 's':
+				tryReadSlice("ENSOR");
+				features |= SENSOR;
+				break;
 
-			if (optarg[0] == '\0' || *ep != '\0')
-				errx(1, "Invalid argument for option 'n'");
+			case 'T': // TIMER
+			case 't':
+				tryReadSlice("IMER");
+				features |= TIMER;
+				break;
 
-			if (version < 0 || version > 0xFF)
-				errx(1, "Argument for option 'n' must be between 0 and 255");
+			default:
+				return MBC_BAD;
+			}
+		}
+#undef tryReadSlice
 
+		switch (mbc) {
+		case ROM:
+			if (!features)
+				break;
+			mbc = ROM_RAM - 1;
+			static_assert(ROM_RAM + 1 == ROM_RAM_BATTERY, "Enum sanity check failed!");
+			static_assert(MBC1 + 1 == MBC1_RAM, "Enum sanity check failed!");
+			static_assert(MBC1 + 2 == MBC1_RAM_BATTERY, "Enum sanity check failed!");
+			static_assert(MMM01 + 1 == MMM01_RAM, "Enum sanity check failed!");
+			static_assert(MMM01 + 2 == MMM01_RAM_BATTERY, "Enum sanity check failed!");
+			// fallthrough
+		case MBC1:
+		case MMM01:
+			if (features == RAM)
+				mbc++;
+			else if (features == (RAM | BATTERY))
+				mbc += 2;
+			else if (features)
+				return MBC_WRONG_FEATURES;
 			break;
-		case 'p':
-			resize = true;
 
-			padvalue = strtoul(optarg, &ep, 0);
+		case MBC2:
+			if (features == BATTERY)
+				mbc = MBC2_BATTERY;
+			else if (features)
+				return MBC_WRONG_FEATURES;
+			break;
 
-			if (optarg[0] == '\0' || *ep != '\0')
-				errx(1, "Invalid argument for option 'p'");
-
-			if (padvalue < 0 || padvalue > 0xFF)
-				errx(1, "Argument for option 'p' must be between 0 and 255");
-
+		case MBC3:
+			// Handle timer, which also requires battery
+			if (features & (TIMER & BATTERY)) {
+				features &= ~(TIMER | BATTERY); // Reset those bits
+				mbc = MBC3_TIMER_BATTERY;
+				// RAM is handled below
+			}
+			static_assert(MBC3 + 1 == MBC3_RAM, "Enum sanity check failed!");
+			static_assert(MBC3 + 2 == MBC3_RAM_BATTERY, "Enum sanity check failed!");
+			static_assert(MBC3_TIMER_BATTERY + 1 == MBC3_TIMER_RAM_BATTERY,
+				      "Enum sanity check failed!");
+			if (features == RAM)
+				mbc++;
+			else if (features == (RAM | BATTERY))
+				mbc += 2;
+			else if (features)
+				return MBC_WRONG_FEATURES;
 			break;
-		case 'r':
-			setramsize = true;
 
-			ramsize = strtoul(optarg, &ep, 0);
+		case MBC5:
+			if (features & RUMBLE) {
+				features &= ~RUMBLE;
+				mbc = MBC5_RUMBLE;
+			}
+			static_assert(MBC5 + 1 == MBC5_RAM, "Enum sanity check failed!");
+			static_assert(MBC5 + 2 == MBC5_RAM_BATTERY, "Enum sanity check failed!");
+			static_assert(MBC5_RUMBLE + 1 == MBC5_RUMBLE_RAM, "Enum sanity check failed!");
+			static_assert(MBC5_RUMBLE + 2 == MBC5_RUMBLE_RAM_BATTERY,
+				      "Enum sanity check failed!");
+			if (features == RAM)
+				mbc++;
+			else if (features == (RAM | BATTERY))
+				mbc += 2;
+			else if (features)
+				return MBC_WRONG_FEATURES;
+			break;
 
-			if (optarg[0] == '\0' || *ep != '\0')
-				errx(1, "Invalid argument for option 'r'");
+		case MBC6:
+		case POCKET_CAMERA:
+		case BANDAI_TAMA5:
+		case HUC3:
+			// No extra features accepted
+			if (features)
+				return MBC_WRONG_FEATURES;
+			break;
 
-			if (ramsize < 0 || ramsize > 0xFF)
-				errx(1, "Argument for option 'r' must be between 0 and 255");
+		case MBC7_SENSOR_RUMBLE_RAM_BATTERY:
+			if (features != (SENSOR | RUMBLE | RAM | BATTERY))
+				return MBC_WRONG_FEATURES;
+			break;
 
+		case HUC1_RAM_BATTERY:
+			if (features != (RAM | BATTERY)) // HuC1 expects RAM+BATTERY
+				return MBC_WRONG_FEATURES;
 			break;
-		case 's':
-			super = true;
-			break;
-		case 't':
-			settitle = true;
+		}
 
-			if (strlen(optarg) > 16)
-				errx(1, "Title \"%s\" is greater than the maximum of 16 characters",
-				     optarg);
+		// Trim off trailing whitespace
+		while (*ptr == ' ' || *ptr == '\t')
+			ptr++;
 
-			if (strlen(optarg) == 16)
-				warnx("Title \"%s\" is 16 chars, it is best to keep it to 15 or fewer",
-				      optarg);
+		// If there is still something past the whitespace, error out
+		if (*ptr)
+			return MBC_BAD;
 
-			title = optarg;
-			break;
-		case 'V':
-			printf("rgbfix %s\n", get_package_version_string());
-			exit(0);
-		case 'v':
-			fixlogo = true;
-			fixheadsum = true;
-			fixglobalsum = true;
-			break;
-		default:
-			print_usage();
-			/* NOTREACHED */
-		}
+		return mbc;
 	}
+}
 
-	argc -= optind;
-	argv += optind;
+static char const *mbcName(enum MbcType type)
+{
+	switch (type) {
+	case ROM:
+		return "ROM";
+	case ROM_RAM:
+		return "ROM+RAM";
+	case ROM_RAM_BATTERY:
+		return "ROM+RAM+BATTERY";
+	case MBC1:
+		return "MBC1";
+	case MBC1_RAM:
+		return "MBC1+RAM";
+	case MBC1_RAM_BATTERY:
+		return "MBC1+RAM+BATTERY";
+	case MBC2:
+		return "MBC2";
+	case MBC2_BATTERY:
+		return "MBC2+BATTERY";
+	case MMM01:
+		return "MMM01";
+	case MMM01_RAM:
+		return "MMM01+RAM";
+	case MMM01_RAM_BATTERY:
+		return "MMM01+RAM+BATTERY";
+	case MBC3:
+		return "MBC3";
+	case MBC3_TIMER_BATTERY:
+		return "MBC3+TIMER+BATTERY";
+	case MBC3_TIMER_RAM_BATTERY:
+		return "MBC3+TIMER+RAM+BATTERY";
+	case MBC3_RAM:
+		return "MBC3+RAM";
+	case MBC3_RAM_BATTERY:
+		return "MBC3+RAM+BATTERY";
+	case MBC5:
+		return "MBC5";
+	case MBC5_RAM:
+		return "MBC5+RAM";
+	case MBC5_RAM_BATTERY:
+		return "MBC5+RAM+BATTERY";
+	case MBC5_RUMBLE:
+		return "MBC5+RUMBLE";
+	case MBC5_RUMBLE_RAM:
+		return "MBC5+RUMBLE+RAM";
+	case MBC5_RUMBLE_RAM_BATTERY:
+		return "MBC5+RUMBLE+RAM+BATTERY";
+	case MBC6:
+		return "MBC6";
+	case MBC7_SENSOR_RUMBLE_RAM_BATTERY:
+		return "MBC7+SENSOR+RUMBLE+RAM+BATTERY";
+	case POCKET_CAMERA:
+		return "POCKET CAMERA";
+	case BANDAI_TAMA5:
+		return "BANDAI TAMA5";
+	case HUC3:
+		return "HUC3";
+	case HUC1_RAM_BATTERY:
+		return "HUC1+RAM+BATTERY";
 
-	if (argc == 0) {
-		fputs("FATAL: no input files\n", stderr);
-		print_usage();
+	// Error values
+	case MBC_NONE:
+	case MBC_BAD:
+	case MBC_WRONG_FEATURES:
+	case MBC_BAD_RANGE:
+		unreachable_();
 	}
 
-	/*
-	 * Open the ROM file
-	 */
+	unreachable_();
+}
 
-	rom = fopen(argv[argc - 1], "rb+");
+static bool hasRAM(enum MbcType type)
+{
+	switch (type) {
+	case ROM:
+	case MBC1:
+	case MBC2: // Technically has RAM, but not marked as such
+	case MBC2_BATTERY:
+	case MMM01:
+	case MBC3:
+	case MBC3_TIMER_BATTERY:
+	case MBC5:
+	case MBC5_RUMBLE:
+	case MBC6: // TODO: not sure
+	case BANDAI_TAMA5: // TODO: not sure
+	case MBC_NONE:
+	case MBC_BAD:
+	case MBC_WRONG_FEATURES:
+	case MBC_BAD_RANGE:
+		return false;
 
-	if (rom == NULL)
-		err(1, "Error opening file %s", argv[argc - 1]);
+	case ROM_RAM:
+	case ROM_RAM_BATTERY:
+	case MBC1_RAM:
+	case MBC1_RAM_BATTERY:
+	case MMM01_RAM:
+	case MMM01_RAM_BATTERY:
+	case MBC3_TIMER_RAM_BATTERY:
+	case MBC3_RAM:
+	case MBC3_RAM_BATTERY:
+	case MBC5_RAM:
+	case MBC5_RAM_BATTERY:
+	case MBC5_RUMBLE_RAM:
+	case MBC5_RUMBLE_RAM_BATTERY:
+	case MBC7_SENSOR_RUMBLE_RAM_BATTERY:
+	case POCKET_CAMERA:
+	case HUC3:
+	case HUC1_RAM_BATTERY:
+		return true;
+	}
 
-	/*
-	 * Read ROM header
-	 *
-	 * Offsets in the buffer are 0x100 less than the equivalent in ROM.
-	 */
+	unreachable_();
+}
 
-	uint8_t header[0x50];
+static uint8_t nbErrors;
 
-	if (fseek(rom, 0x100, SEEK_SET) != 0)
-		err(1, "Could not locate ROM header");
-	if (fread(header, sizeof(uint8_t), sizeof(header), rom)
-	    != sizeof(header))
-		err(1, "Could not read ROM header");
+static format_(printf, 1, 2) void report(char const *fmt, ...)
+{
+	va_list ap;
 
-	if (fixlogo || trashlogo) {
-		/*
-		 * Offset 0x104–0x133: Nintendo Logo
-		 * This is a bitmap image that displays when the Game Boy is
-		 * turned on. It must be intact, or the game will not boot.
-		 */
+	va_start(ap, fmt);
+	vfprintf(stderr, fmt, ap);
+	va_end(ap);
 
-		/*
-		 * See also: global checksums at 0x14D–0x14F, They must
-		 * also be correct for the game to boot, so we fix them
-		 * as well when requested with the -f flag.
-		 */
+	if (nbErrors != UINT8_MAX)
+		nbErrors++;
+}
 
-		uint8_t ninlogo[48] = {
-			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 const uint8_t ninLogo[] = {
+	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
+};
 
-		if (trashlogo) {
-			for (int i = 0; i < sizeof(ninlogo); i++)
-				ninlogo[i] = ~ninlogo[i];
-		}
+static enum { DMG, BOTH, CGB } model = DMG; // If DMG, byte is left alone
+#define   FIX_LOGO        0x80
+#define TRASH_LOGO        0x40
+#define   FIX_HEADER_SUM  0x20
+#define TRASH_HEADER_SUM  0x10
+#define   FIX_GLOBAL_SUM  0x08
+#define TRASH_GLOBAL_SUM  0x04
+static uint8_t fixSpec = 0;
+static const char *gameID = NULL;
+static uint8_t gameIDLen;
+static bool japanese = true;
+static const char *newLicensee = NULL;
+static uint8_t newLicenseeLen;
+static uint16_t oldLicensee = UNSPECIFIED;
+static enum 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 const char *title = NULL;
+static uint8_t titleLen;
 
-		memcpy(header + 0x04, ninlogo, sizeof(ninlogo));
-	}
+static uint8_t maxTitleLen(void)
+{
+	return gameID ? 11 :  model != DMG ? 15 : 16;
+}
 
-	if (settitle) {
-		/*
-		 * Offset 0x134–0x143: Game Title
-		 * This is a sixteen-character game title in ASCII (no high-
-		 * bit characters).
-		 */
+static ssize_t readBytes(int fd, uint8_t *buf, size_t len)
+{
+	// POSIX specifies that lengths greater than SSIZE_MAX yield implementation-defined results
+	assert(len <= SSIZE_MAX);
 
-		/*
-		 * See also: CGB flag at 0x143. The sixteenth character of
-		 * the title is co-opted for use as the CGB flag, so they
-		 * may conflict.
-		 */
+	ssize_t total = 0;
 
-		/*
-		 * See also: Game ID at 0x13F–0x142. These four ASCII
-		 * characters may conflict with the title.
-		 */
+	while (len) {
+		ssize_t ret = read(fd, buf, len);
 
-		strncpy((char *)header + 0x34, title, 16);
+		if (ret == -1 && errno != EINTR) // Return errors, unless we only were interrupted
+			return -1;
+		// EOF reached
+		if (ret == 0)
+			return total;
+		// If anything was read, accumulate it, and continue
+		if (ret != -1) {
+			total += ret;
+			len -= ret;
+			buf += ret;
+		}
 	}
 
-	if (setid) {
-		/*
-		 * Offset 0x13F–0x142: Game ID
-		 * This is a four-character game ID in ASCII (no high-bit
-		 * characters).
-		 */
+	return total;
+}
 
-		memcpy(header + 0x3F, id, 4);
+static ssize_t writeBytes(int fd, void *buf, size_t len)
+{
+	// POSIX specifies that lengths greater than SSIZE_MAX yield implementation-defined results
+	assert(len <= SSIZE_MAX);
+
+	ssize_t total = 0;
+
+	while (len) {
+		ssize_t ret = write(fd, buf, len);
+
+		if (ret == -1 && errno != EINTR) // Return errors, unless we only were interrupted
+			return -1;
+		// EOF reached
+		if (ret == 0)
+			return total;
+		// If anything was read, accumulate it, and continue
+		if (ret != -1) {
+			total += ret;
+			len -= ret;
+		}
 	}
 
-	if (colorcompatible) {
-		/*
-		 * Offset 0x143: Game Boy Color Flag
-		 * If bit 7 is set, the ROM has Game Boy Color features.
-		 * If bit 6 is also set, the ROM is for the Game Boy Color
-		 * only. (However, this is not actually enforced by the
-		 * Game Boy.)
-		 */
+	return total;
+}
 
-		/*
-		 * See also: Game Title at 0x134–0x143. The sixteenth
-		 * character of the title overlaps with this flag, so they
-		 * may conflict.
-		 */
+/**
+ * @param input File descriptor to be used for reading
+ * @param output File descriptor to be used for writing, may be equal to `input`
+ * @param name The file's name, to be displayed for error output
+ * @param fileSize The file's size if known, 0 if not.
+ */
+static void processFile(int input, int output, char const *name, off_t fileSize)
+{
+	// Both of these should be true for seekable files, and neither otherwise
+	if (input == output)
+		assert(fileSize != 0);
+	else
+		assert(fileSize == 0);
 
-		header[0x43] |= 1 << 7;
-		if (coloronly)
-			header[0x43] |= 1 << 6;
+	uint8_t rom0[BANK_SIZE];
+	ssize_t rom0Len = readBytes(input, rom0, sizeof(rom0));
 
-		if (header[0x43] & 0x3F)
-			warnx("Color flag conflicts with game title");
+	if (rom0Len == -1) {
+		report("FATAL: Failed to read \"%s\"'s header: %s\n", name, strerror(errno));
+		return;
+	} else if (rom0Len < 0x150) {
+		report("FATAL: \"%s\" too short, expected at least 336 ($150) bytes, got only %ld\n",
+		       name, rom0Len);
+		return;
 	}
+	// Accept partial reads if the file contains at least the header
 
-	if (setnewlicensee) {
-		/*
-		 * Offset 0x144–0x145: New Licensee Code
-		 * This is a two-character code identifying which company
-		 * created the game.
-		 */
+	if (fixSpec & (FIX_LOGO | TRASH_LOGO)) {
+		if (fixSpec & FIX_LOGO) {
+			memcpy(&rom0[0x104], ninLogo, sizeof(ninLogo));
+		} else {
+			for (uint8_t i = 0; i < sizeof(ninLogo); i++)
+				rom0[i + 0x104] = ~ninLogo[i];
+		}
+	}
 
-		/*
-		 * See also: the original Licensee ID at 0x14B.
-		 * This is deprecated and in all newer games is used instead
-		 * as a Super Game Boy flag.
-		 */
+	if (title)
+		memcpy(&rom0[0x134], title, titleLen);
 
-		header[0x44] = newlicensee[0];
-		header[0x45] = newlicensee[1];
-	}
+	if (gameID)
+		memcpy(&rom0[0x13f], gameID, gameIDLen);
 
-	if (super) {
-		/*
-		 * Offset 0x146: Super Game Boy Flag
-		 * If not equal to 3, Super Game Boy functions will be
-		 * disabled.
-		 */
+	if (model != DMG)
+		rom0[0x143] = model == BOTH ? 0x80 : 0xc0;
 
-		/*
-		 * See also: the original Licensee ID at 0x14B.
-		 * If the Licensee code is not equal to 0x33, Super Game Boy
-		 * functions will be disabled.
-		 */
+	if (newLicensee)
+		memcpy(&rom0[0x144], newLicensee, newLicenseeLen);
 
-		if (!setlicensee)
-			warnx("You should probably set both '-s' and '-l 0x33'");
+	if (sgb)
+		rom0[0x146] = 0x03;
 
-		header[0x46] = 3;
-	}
+	// If a valid MBC was specified...
+	if (cartridgeType < MBC_NONE)
+		rom0[0x147] = cartridgeType;
 
-	if (setcartridge) {
-		/*
-		 * Offset 0x147: Cartridge Type
-		 * Identifies whether the ROM uses a memory bank controller,
-		 * external RAM, timer, rumble, or battery.
-		 */
+	if (ramSize != UNSPECIFIED)
+		rom0[0x149] = ramSize;
 
-		header[0x47] = cartridge;
-	}
+	if (!japanese)
+		rom0[0x14a] = 0x01;
 
-	if (resize) {
-		/*
-		 * Offset 0x148: Cartridge Size
-		 * Identifies the size of the cartridge ROM.
-		 */
+	if (oldLicensee != UNSPECIFIED)
+		rom0[0x14b] = oldLicensee;
 
-		/* We will pad the ROM to match the size given in the header. */
-		long romsize, newsize;
-		int headbyte;
-		uint8_t *buf;
+	if (romVersion != UNSPECIFIED)
+		rom0[0x14c] = romVersion;
 
-		if (fseek(rom, 0, SEEK_END) != 0)
-			err(1, "Could not pad ROM file");
+	// 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.
 
-		romsize = ftell(rom);
-		if (romsize == -1)
-			err(1, "Could not pad ROM file");
+	uint16_t globalSum = 0;
 
-		newsize = 0x8000;
+	// 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.
+	uint8_t *romx = NULL; // Pointer to ROMX banks when they've been buffered
+	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
 
-		headbyte = 0;
-		while (romsize > newsize) {
-			newsize <<= 1;
-			headbyte++;
+	// Handle ROMX
+	if (input == output) {
+		if (fileSize >= 0x10000 * BANK_SIZE) {
+			report("FATAL: \"%s\" has more than 65536 banks\n", 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(totalRomxLen / 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 = realloc(romx, nbBanks * BANK_SIZE);
+			if (!romx) {
+				report("FATAL: Failed to realloc ROMX buffer: %s\n",
+				       strerror(errno));
+				return;
+			}
+			ssize_t bankLen = readBytes(input, &romx[(nbBanks - 1) * BANK_SIZE],
+						    BANK_SIZE);
 
-		if (newsize > 0x800000) /* ROM is bigger than 8MiB */
-			warnx("ROM size is bigger than 8MiB");
+			// Update bank count, ONLY IF at least one byte was read
+			if (bankLen) {
+				// We're gonna 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) {
+					report("FATAL: \"%s\" has more than 65536 banks\n", name);
+					free(romx);
+					return;
+				}
+				nbBanks++;
 
-		buf = malloc(newsize - romsize);
-		if (buf == NULL)
-			errx(1, "Couldn't allocate memory for padded ROM.");
+				// 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;
+		}
+	}
 
-		memset(buf, padvalue, newsize - romsize);
-		if (fwrite(buf, 1, newsize - romsize, rom) != newsize - romsize)
-			err(1, "Could not pad ROM file");
+	// 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 {
+			assert(rom0Len == sizeof(rom0));
+		}
+		assert(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);
+	}
 
-		header[0x48] = headbyte;
+	// Handle the header checksum after the ROM size has been written
+	if (fixSpec & (FIX_HEADER_SUM | TRASH_HEADER_SUM)) {
+		uint8_t sum = 0;
 
-		free(buf);
+		for (uint16_t i = 0x134; i < 0x14d; i++)
+			sum -= rom0[i] + 1;
+		rom0[0x14d] = fixSpec & TRASH_HEADER_SUM ? ~sum : sum;
 	}
 
-	if (setramsize) {
-		/*
-		 * Offset 0x149: RAM Size
-		 */
+	if (fixSpec & (FIX_GLOBAL_SUM | TRASH_GLOBAL_SUM)) {
+		// Computation of the global checksum assumes 0s being stored in its place
+		rom0[0x14e] = 0;
+		rom0[0x14f] = 0;
+		for (uint16_t i = 0; i < rom0Len; i++)
+			globalSum += rom0[i];
+		// Pipes have already read ROMX and updated globalSum, but not regular files
+		if (input == output) {
+			for (;;) {
+				ssize_t romxLen = readBytes(input, bank, sizeof(bank));
 
-		header[0x49] = ramsize;
+				for (uint16_t i = 0; i < romxLen; i++)
+					globalSum += bank[i];
+				if (romxLen != sizeof(bank))
+					break;
+			}
+		}
+
+		if (fixSpec & TRASH_GLOBAL_SUM)
+			globalSum = ~globalSum;
+		rom0[0x14e] = globalSum >> 8;
+		rom0[0x14f] = globalSum & 0xff;
 	}
 
-	if (nonjapan) {
-		/*
-		 * Offset 0x14A: Non-Japanese Region Flag
-		 */
+	// 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) == (off_t)-1) {
+			report("FATAL: Failed to rewind \"%s\": %s\n", name, strerror(errno));
+			goto free_romx;
+		}
+		// 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 = 0x150;
+	}
+	ssize_t writeLen = writeBytes(output, rom0, rom0Len);
 
-		header[0x4A] = 1;
+	if (writeLen == -1) {
+		report("FATAL: Failed to write \"%s\"'s ROM0: %s\n", name, strerror(errno));
+		goto free_romx;
+	} else if (writeLen < rom0Len) {
+		report("FATAL: Could only write %ld of \"%s\"'s %ld ROM0 bytes\n",
+		       writeLen, name, rom0Len);
+		goto free_romx;
 	}
 
-	if (setlicensee) {
-		/*
-		 * Offset 0x14B: Licensee Code
-		 * This identifies which company created the game.
-		 *
-		 * This byte is deprecated and should be set to 0x33 in new
-		 * releases.
-		 */
+	// Output ROMX if it was buffered
+	if (romx) {
+		writeLen = writeBytes(output, romx, totalRomxLen);
+		if (writeLen == -1) {
+			report("FATAL: Failed to write \"%s\"'s ROMX: %s\n", name, strerror(errno));
+			goto free_romx;
+		} else if (writeLen < totalRomxLen) {
+			report("FATAL: Could only write %ld of \"%s\"'s %ld ROMX bytes\n",
+			       writeLen, name, totalRomxLen);
+			goto free_romx;
+		}
+	}
 
-		/*
-		 * See also: the New Licensee ID at 0x144–0x145.
-		 */
+	// Output padding
+	if (padValue != UNSPECIFIED) {
+		if (input == output) {
+			if (lseek(output, 0, SEEK_END) == (off_t)-1) {
+				report("FATAL: Failed to seek to end of \"%s\": %s\n",
+				       name, strerror(errno));
+				goto free_romx;
+			}
+		}
+		memset(bank, padValue, sizeof(bank));
+		size_t len = (nbBanks - 1) * BANK_SIZE - totalRomxLen; // Don't count ROM0!
 
-		header[0x4B] = licensee;
+		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);
+
+			if (ret != thisLen) {
+				report("FATAL: Failed to write \"%s\"'s padding: %s\n",
+				       name, strerror(errno));
+				break;
+			}
+			len -= thisLen;
+		}
 	}
 
-	if (setversion) {
-		/*
-		 * Offset 0x14C: Mask ROM Version Number
-		 * Which version of the ROM this is.
-		 */
+free_romx:
+	free(romx);
+}
 
-		header[0x4C] = version;
-	}
+#undef trySeek
 
-	if (fixheadsum || trashheadsum) {
-		/*
-		 * Offset 0x14D: Header Checksum
-		 */
+static bool processFilename(char const *name)
+{
+	nbErrors = 0;
+	if (!strcmp(name, "-")) {
+		setmode(STDIN_FILENO, O_BINARY);
+		setmode(STDOUT_FILENO, O_BINARY);
+		name = "<stdin>";
+		processFile(STDIN_FILENO, STDOUT_FILENO, name, 0);
 
-		uint8_t headcksum = 0;
+	} else {
+		// 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 IO
+		// operations may fail, all of which we handle.
+		int input = open(name, O_RDWR | O_BINARY);
+		struct stat stat;
 
-		for (int i = 0x34; i < 0x4D; ++i)
-			headcksum = headcksum - header[i] - 1;
+		if (input == -1) {
+			report("FATAL: Failed to open \"%s\" for reading+writing: %s\n",
+				name, strerror(errno));
+			goto fail;
+		}
 
-		if (trashheadsum)
-			headcksum = ~headcksum;
+		if (fstat(input, &stat) == -1) {
+			report("FATAL: Failed to stat \"%s\": %s\n", name, strerror(errno));
+		} else if (!S_ISREG(stat.st_mode)) { // FIXME: Do we want to support other types?
+			report("FATAL: \"%s\" is not a regular file, and thus cannot be modified in-place\n",
+				name);
+		} 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
+			report("FATAL: \"%s\" too short, expected at least 336 ($150) bytes, got only %ld\n",
+			       name, stat.st_size);
+		} else {
+			processFile(input, input, name, stat.st_size);
+		}
 
-		header[0x4D] = headcksum;
+		close(input);
 	}
+	if (nbErrors)
+fail:
+		fprintf(stderr, "Fixing \"%s\" failed with %u error%s\n",
+			name, nbErrors, nbErrors == 1 ? "" : "s");
+	return nbErrors;
+}
 
-	/*
-	 * Before calculating the global checksum, we must write the modified
-	 * header to the ROM.
-	 */
+int main(int argc, char *argv[])
+{
+	nbErrors = 0;
+	char ch;
 
-	if (fseek(rom, 0x100, SEEK_SET) != 0)
-		err(1, "Could not locate header for writing");
+	while ((ch = musl_getopt_long_only(argc, argv, optstring, longopts, NULL)) != -1) {
+		switch (ch) {
+			size_t len;
+#define parseByte(output, name) \
+do { \
+	char *endptr; \
+	unsigned long tmp; \
+	\
+	if (optarg[0] == 0) { \
+		report("error: Argument to option '" name "' may not be empty\n"); \
+	} else { \
+		if (optarg[0] == '$') { \
+			tmp = strtoul(&optarg[1], &endptr, 16); \
+		} else { \
+			tmp = strtoul(optarg, &endptr, 0); \
+		} \
+		if (*endptr) \
+			report("error: Expected number as argument to option '" name "', got %s\n", \
+			       optarg); \
+		else if (tmp > 0xFF) \
+			report("error: Argument to option '" name "' is larger than 255: %lu\n", tmp); \
+		else \
+			output = tmp; \
+	} \
+} while (0)
 
-	if (fwrite(header, sizeof(uint8_t), sizeof(header), rom)
-	    != sizeof(header))
-		err(1, "Could not write modified ROM header");
+		case 'C':
+		case 'c':
+			model = ch == 'c' ? BOTH : CGB;
+			if (titleLen > 15) {
+				titleLen = 15;
+				fprintf(stderr, "warning: Truncating title \"%s\" to 15 chars\n",
+					title);
+			}
+			break;
 
-	if (fixglobalsum || trashglobalsum) {
-		/*
-		 * Offset 0x14E–0x14F: Global Checksum
-		 */
+		case 'f':
+			fixSpec = 0;
+			while (*optarg) {
+				switch (*optarg) {
+#define SPEC_l FIX_LOGO
+#define SPEC_L TRASH_LOGO
+#define SPEC_h FIX_HEADER_SUM
+#define SPEC_H TRASH_HEADER_SUM
+#define SPEC_g FIX_GLOBAL_SUM
+#define SPEC_G TRASH_GLOBAL_SUM
+#define or(new, bad) \
+do { \
+	if (fixSpec & SPEC_##bad) \
+		fprintf(stderr, \
+			"warning: '" #new "' overriding '" #bad "' in fix spec\n"); \
+	fixSpec = (fixSpec & ~SPEC_##bad) | SPEC_##new; \
+} while (0)
+				case 'l':
+					or(l, L);
+					break;
+				case 'L':
+					or(L, l);
+					break;
 
-		uint16_t globalcksum = 0;
+				case 'h':
+					or(h, H);
+					break;
+				case 'H':
+					or(H, h);
+					break;
 
-		if (fseek(rom, 0, SEEK_SET) != 0)
-			err(1, "Could not start calculating global checksum");
+				case 'g':
+					or(g, G);
+					break;
+				case 'G':
+					or(G, g);
+					break;
 
-		int i = 0;
-		int byte;
+				default:
+					fprintf(stderr, "warning: Ignoring '%c' in fix spec\n",
+						*optarg);
+#undef or
+				}
+				optarg++;
+			}
+			break;
 
-		while ((byte = fgetc(rom)) != EOF) {
-			if (i != 0x14E && i != 0x14F)
-				globalcksum += byte;
-			i++;
-		}
+		case 'i':
+			gameID = optarg;
+			len = strlen(gameID);
+			if (len > 4) {
+				len = 4;
+				fprintf(stderr, "warning: Truncating game ID \"%s\" to 4 chars\n",
+					gameID);
+			}
+			gameIDLen = len;
+			if (titleLen > 11) {
+				titleLen = 11;
+				fprintf(stderr, "warning: Truncating title \"%s\" to 11 chars\n",
+					title);
+			}
+			break;
 
-		if (ferror(rom))
-			err(1, "Could not calculate global checksum");
+		case 'j':
+			japanese = false;
+			break;
 
-		if (trashglobalsum)
-			globalcksum = ~globalcksum;
+		case 'k':
+			newLicensee = optarg;
+			len = strlen(newLicensee);
+			if (len > 2) {
+				len = 2;
+				fprintf(stderr,
+					"warning: Truncating new licensee \"%s\" to 2 chars\n",
+					newLicensee);
+			}
+			newLicenseeLen = len;
+			break;
 
-		fseek(rom, 0x14E, SEEK_SET);
-		fputc(globalcksum >> 8, rom);
-		fputc(globalcksum & 0xFF, rom);
-		if (ferror(rom))
-			err(1, "Could not write global checksum");
+		case 'l':
+			parseByte(oldLicensee, "l");
+			break;
+
+		case 'm':
+			cartridgeType = parseMBC(optarg);
+			if (cartridgeType == MBC_BAD) {
+				report("error: Unknown MBC \"%s\"\n", optarg);
+			} else if (cartridgeType == MBC_WRONG_FEATURES) {
+				report("error: Features incompatible with MBC (\"%s\")\n", optarg);
+			} else if (cartridgeType == MBC_BAD_RANGE) {
+				report("error: Specified MBC ID out of range 0-255: %s\n",
+				       optarg);
+			} else if (cartridgeType == ROM_RAM || cartridgeType == ROM_RAM_BATTERY) {
+				fprintf(stderr, "warning: ROM+RAM / ROM+RAM+BATTERY are under-specified and poorly supported\n");
+			}
+			break;
+
+		case 'n':
+			parseByte(romVersion, "n");
+			break;
+
+		case 'p':
+			parseByte(padValue, "p");
+			break;
+
+		case 'r':
+			parseByte(ramSize, "r");
+			break;
+
+		case 's':
+			sgb = true;
+			break;
+
+		case 't':
+			title = optarg;
+			len = strlen(title);
+			uint8_t maxLen = maxTitleLen();
+
+			if (len > maxLen) {
+				len = maxLen;
+				fprintf(stderr, "warning: Truncating title \"%s\" to %u chars\n",
+					title, maxLen);
+			}
+			titleLen = len;
+			break;
+
+		case 'V':
+			printf("rgbfix %s\n", get_package_version_string());
+			exit(0);
+
+		case 'v':
+			fixSpec = FIX_LOGO | FIX_HEADER_SUM | FIX_GLOBAL_SUM;
+			break;
+
+		default:
+			fprintf(stderr, "FATAL: unknown option '%c'\n", ch);
+			printUsage();
+			exit(1);
+		}
+#undef parseByte
 	}
 
-	if (fclose(rom) != 0)
-		err(1, "Could not complete ROM write");
+	if (ramSize != UNSPECIFIED && cartridgeType < UNSPECIFIED) {
+		if (cartridgeType == ROM_RAM || cartridgeType == ROM_RAM_BATTERY) {
+			if (ramSize != 1)
+				fprintf(stderr, "warning: MBC \"%s\" should have 2kiB of RAM (-r 1)\n",
+					mbcName(cartridgeType));
+		} else if (hasRAM(cartridgeType)) {
+			if (!ramSize) {
+				fprintf(stderr,
+					"warning: MBC \"%s\" has RAM, but RAM size was set to 0\n",
+					mbcName(cartridgeType));
+			} else if (ramSize == 1) {
+				fprintf(stderr,
+					"warning: RAM size 1 (2 kiB) was specified for MBC \"%s\"\n",
+					mbcName(cartridgeType));
+			} // TODO: check possible values?
+		} else if (ramSize) {
+			fprintf(stderr,
+				"warning: MBC \"%s\" has no RAM, but RAM size was set to %u\n",
+				mbcName(cartridgeType), ramSize);
+		}
+	}
 
-	return 0;
+	if (sgb && oldLicensee != UNSPECIFIED && oldLicensee != 0x33)
+		fprintf(stderr,
+			"warning: SGB compatibility enabled, but old licensee is %#x, not 0x33\n",
+			oldLicensee);
+
+	argv += optind;
+	bool failed = nbErrors;
+
+	if (!*argv) {
+		failed |= processFilename("-");
+	} else {
+		do {
+			failed |= processFilename(*argv);
+		} while (*++argv);
+	}
+
+	return failed;
 }
--- a/src/fix/rgbfix.1
+++ b/src/fix/rgbfix.1
@@ -24,39 +24,48 @@
 .Op Fl p Ar pad_value
 .Op Fl r Ar ram_size
 .Op Fl t Ar title_str
-.Ar file
+.Op Ar
 .Sh DESCRIPTION
 The
 .Nm
-program changes headers of Game Boy ROM images.
+program changes headers of Game Boy ROM images, typically generated by
+.Xr rgblink 1 ,
+though it will work with
+.Em any
+Game Boy ROM.
 It also performs other correctness operations, such as padding.
+.Nm
+only changes the fields for which it has values specified.
+Developers are advised to fill those fields with 0x00 bytes in their source code before running
+.Nm ,
+and to have already populated whichever fields they don't specify using
+.Nm .
 .Pp
 Note that options can be abbreviated as long as the abbreviation is unambiguous:
-.Fl Fl verb
+.Fl Fl color-o
 is
-.Fl Fl verbose ,
+.Fl Fl color-only ,
 but
-.Fl Fl ver
+.Fl Fl color
 is invalid because it could also be
-.Fl Fl version .
-The arguments are as follows:
+.Fl Fl color-compatible .
+Options later in the command line override those set earlier.
+Accepted options are as follows:
 .Bl -tag -width Ds
 .It Fl C , Fl Fl color-only
-Set the Game Boy Color\(enonly flag:
-.Ad 0x143
-= 0xC0.
-If both this and the
+Set the Game Boy Color\(enonly flag
+.Pq Ad 0x143
+to 0xC0.
+This overrides
 .Fl c
-flag are set, this takes precedence.
+if it was set prior.
 .It Fl c , Fl Fl color-compatible
 Set the Game Boy Color\(encompatible flag:
-.Ad 0x143
-= 0x80.
-If both this and the
-.Fl C
-flag are set,
-.Fl C
-takes precedence.
+.Pq Ad 0x143
+to 0x80.
+This overrides
+.Fl c
+if it was set prior.
 .It Fl f Ar fix_spec , Fl Fl fix-spec Ar fix_spec
 Fix certain header values that the Game Boy checks for correctness.
 Alternatively, intentionally trash these values by writing their binary inverse instead.
@@ -83,55 +92,63 @@
 .It Fl i Ar game_id , Fl Fl game-id Ar game_id
 Set the game ID string
 .Pq Ad 0x13F Ns \(en Ns Ad 0x142
-to a given string of exactly 4 characters.
-If both this and the title are set, the game ID will overwrite the overlapping portion of the title.
+to a given string.
+If it's longer than 4 chars, it will be truncated, and a warning emitted.
 .It Fl j , Fl Fl non-japanese
-Set the non-Japanese region flag:
-.Ad 0x14A
-= 1.
+Set the non-Japanese region flag
+.Pq Ad 0x14A
+to 0x01.
 .It Fl k Ar licensee_str , Fl Fl new-licensee Ar licensee_str
 Set the new licensee string
 .Pq Ad 0x144 Ns \(en Ns Ad 0x145
-to a given string, truncated to at most two characters.
+to a given string.
+If it's longer than 2 chars, it will be truncated, and a warning emitted.
 .It Fl l Ar licensee_id , Fl Fl old-licensee Ar licensee_id
-Set the old licensee code,
-.Ad 0x14B ,
+Set the old licensee code
+.Pq Ad 0x14B
 to a given value from 0 to 0xFF.
 This value is deprecated and should be set to 0x33 in all new software.
 .It Fl m Ar mbc_type , Fl Fl mbc-type Ar mbc_type
-Set the MBC type,
-.Ad 0x147 ,
+Set the MBC type
+.Pq Ad 0x147
 to a given value from 0 to 0xFF.
 This value may also be an MBC name from the Pan Docs.
 .It Fl n Ar rom_version , Fl Fl rom-version Ar rom_version
-Set the ROM version,
-.Ad 0x14C ,
+Set the ROM version
+.Pq Ad 0x14C
 to a given value from 0 to 0xFF.
 .It Fl p Ar pad_value , Fl Fl pad-value Ar pad_value
-Pad the image to a valid size with a given pad value from 0 to 0xFF.
+Pad the ROM image to a valid size with a given pad value from 0 to 255 (0xFF).
 .Nm
 will automatically pick a size from 32 KiB, 64 KiB, 128 KiB, ..., 8192 KiB.
 The cartridge size byte
 .Pq Ad 0x148
 will be changed to reflect this new size.
+The recommended padding value is 0xFF, to speed up writing the ROM to flash chips, and to avoid "nop slides" into VRAM.
 .It Fl r Ar ram_size , Fl Fl ram-size Ar ram_size
-Set the RAM size,
-.Ad 0x149 ,
+Set the RAM size
+.Pq Ad 0x149
 to a given value from 0 to 0xFF.
 .It Fl s , Fl Fl sgb-compatible
-Set the SGB flag:
-.Ad 0x146
-= 3. This flag will be ignored by the SGB unless the old licensee code is 0x33!
+Set the SGB flag
+.Pq Ad 0x146
+to 0x03.
+This flag will be ignored by the SGB unless the old licensee code is 0x33!
+If this is given as well as
+.Fl l ,
+but is not set to 0x33, a warning will be printed.
 .It Fl t Ar title , Fl Fl title Ar title
 Set the title string
 .Pq Ad 0x134 Ns \(en Ns Ad 0x143
-to a given string, truncated to at most 16 characters.
-It is recommended to use 15 characters instead, to avoid clashing with the CGB flag
-.Po Fl c
+to a given string.
+If the title is longer than the max length, it will be truncated, and a warning emitted.
+The max length is 11 characters if the game ID
+.Pq Fl i
+is specified, 15 characters if the CGB flag
+.Fl ( c
 or
-.Fl C
-.Pc .
-If both this and the game ID are set, the game ID will overwrite the overlapping portion of the title.
+.Fl C )
+is specified but the game ID is not, and 16 characters otherwise.
 .It Fl V , Fl Fl version
 Print the version of the program and exit.
 .It Fl v , Fl Fl validate
@@ -139,7 +156,7 @@
 .Fl f Cm lhg .
 .El
 .Sh EXAMPLES
-Most values in the ROM header are only cosmetic.
+Most values in the ROM header do not matter to the actual console, and most are seldom useful anyway.
 The bare minimum requirements for a workable program are the header checksum, the Nintendo logo, and (if needed) the CGB/SGB flags.
 It is a good idea to pad the image to a valid size as well
 .Pq Do valid Dc meaning a power of 2, times 32 KiB .
@@ -152,14 +169,13 @@
 The following will make a SGB-enabled, color-enabled game with a title of
 .Dq foobar ,
 and pad it to a valid size.
-.Po
-The Game Boy itself does not use the title, but some emulators or ROM managers do.
-.Pc
+.Pq The Game Boy itself does not use the title, but some emulators or ROM managers do.
 .Pp
 .D1 $ rgbfix -vcs -l 0x33 -p 255 -t foobar baz.gb
 .Pp
-The following will duplicate the header (sans global checksum) of the game
-.Dq Survival Kids :
+The following will duplicate the header of the game
+.Dq Survival Kids ,
+sans global checksum:
 .Pp
 .D1 $ rgbfix -cjsv -k A4 -l 0x33 -m 0x1B -p 0xFF -r 3 -t SURVIVALKIDAVKE \
 SurvivalKids.gbc
--- /dev/null
+++ b/test/fix/.gitignore
@@ -1,0 +1,1 @@
+/padding*_*
--- /dev/null
+++ b/test/fix/README.md
@@ -1,0 +1,16 @@
+# RGBFIX tests
+
+These tests check that RGBFIX behaves properly.
+
+## Structure of a test
+
+- `test.bin`: The file passed as input to RGBFIX.
+- `test.flags`: The command-line flags passed to RGBFIX's invocation.
+  Actually, only the first line is considered; the rest of the file may contain comments about the test.
+- `test.gb`: The expected output.
+  May not exist, generally when the test expects an error, in which case the comparison is skipped.
+- `test.err`: The expected error output.
+
+## Special tests
+
+- `noexist.err` is the expected error output when RGBFIX is given a non-existent input file.
--- /dev/null
+++ b/test/fix/bad-fix-char.bin
@@ -1,0 +1,3 @@
+���!H0�;<�N˗���=����^{KC}b�Q�&�3� �/]����d��Z=���b�a�i:�eS�>��U@�Ay<ae���$vf�孈┚��g�	VC��~>������	�s_Cӛ��&nWFoK���X`'������bl�{����
+�����8���<$:V�"㳙ځh��D��ж��Qc����㸺Z��T�������Vۼ�tŒ��4�B��dT��	�I��
+�(/׻��{h�:��Qm�$*|
��VP��.^"&�<��k!���`��2����
&B�	l�7f���Ze�yf.�m����!�����N5��d�E��qF
\ No newline at end of file
--- /dev/null
+++ b/test/fix/bad-fix-char.err
@@ -1,0 +1,3 @@
+warning: Ignoring 'm' in fix spec
+warning: Ignoring 'a' in fix spec
+warning: Ignoring 'o' in fix spec
--- /dev/null
+++ b/test/fix/bad-fix-char.flags
@@ -1,0 +1,1 @@
+-f lmao
--- /dev/null
+++ b/test/fix/color.bin
@@ -1,0 +1,2 @@
+��+2l}~��Ѧ��}��1x����&)Y�q*����ɒ��^ H�p@v6}��[��&���h���Bn~�T�0��uf�\�8���������Ñ�W����		5^Bq<�K�"S%��U!��r,7��g�4Ȩ��������hL��؝�)����6U@�׍`ݑT�T����Dɦ�8z�5��~A�鱽ܜ�|
�>�]�/�K���d~��"gF*AU�"W�7]}��/�}<��s[lx�ܔ}Ы[���8������
�����,�������!d�,!O��� [=��������28h���/�0��}��x��Դ�����{�}l�"J�����۟�k���0_8z���\�-�r��F��R�vO�h�r�S�HN��]�uƽ#�ʼ�;ߧr��6���Lz�m���5[�9+.Hs���_~z�5�
/�b�ɊS](�6k��Pu�9�J1��ٵ�
,(C���%�k�����4���=��Ѫ���ı&O����
�.��"�
\ No newline at end of file
--- /dev/null
+++ b/test/fix/color.flags
@@ -1,0 +1,1 @@
+-C
--- /dev/null
+++ b/test/fix/color.gb
@@ -1,0 +1,2 @@
+��+2l}~��Ѧ��}��1x����&)Y�q*����ɒ��^ H�p@v6}��[��&���h���Bn~�T�0��uf�\�8���������Ñ�W����		5^Bq<�K�"S%��U!��r,7��g�4Ȩ��������hL��؝�)����6U@�׍`ݑT�T����Dɦ�8z�5��~A�鱽ܜ�|
�>�]�/�K���d~��"gF*AU�"W�7]}��/�}<��s[lx�ܔ}Ы[���8������
�����,�������!d�,!O��� [=��������28h���/�0��}��x��Դ�����{�}l�"J�����۟�k���0_8z���\�-�r��F��R�vO�h�r�S�HN��]�uƽ#�ʼ�;ߧr��6���Lz�m���5[�9+.Hs���_~z�5�
/�b�ɊS](�6k��Pu�9�J1��ٵ�
,(C���%�k�����4���=��Ѫ���ı&O����
�.��"�
\ No newline at end of file
binary files /dev/null b/test/fix/compatible.bin differ
--- /dev/null
+++ b/test/fix/compatible.flags
@@ -1,0 +1,1 @@
+-c
binary files /dev/null b/test/fix/compatible.gb differ
--- /dev/null
+++ b/test/fix/empty.err
@@ -1,0 +1,2 @@
+FATAL: "<filename>" too short, expected at least 336 ($150) bytes, got only 0
+Fixing "<filename>" failed with 1 error
binary files /dev/null b/test/fix/fix-override.bin differ
--- /dev/null
+++ b/test/fix/fix-override.err
@@ -1,0 +1,1 @@
+warning: 'l' overriding 'L' in fix spec
--- /dev/null
+++ b/test/fix/fix-override.flags
@@ -1,0 +1,1 @@
+-f Ll
binary files /dev/null b/test/fix/fix-override.gb differ
binary files /dev/null b/test/fix/gameid-trunc.bin differ
--- /dev/null
+++ b/test/fix/gameid-trunc.err
@@ -1,0 +1,1 @@
+warning: Truncating game ID "FOUR!" to 4 chars
--- /dev/null
+++ b/test/fix/gameid-trunc.flags
@@ -1,0 +1,1 @@
+-i 'FOUR!'
binary files /dev/null b/test/fix/gameid-trunc.gb differ
binary files /dev/null b/test/fix/gameid.bin differ
--- /dev/null
+++ b/test/fix/gameid.flags
@@ -1,0 +1,1 @@
+-i RGBD
binary files /dev/null b/test/fix/gameid.gb differ
binary files /dev/null b/test/fix/global-large.bin differ
--- /dev/null
+++ b/test/fix/global-large.flags
@@ -1,0 +1,1 @@
+-fg
binary files /dev/null b/test/fix/global-large.gb differ
binary files /dev/null b/test/fix/global-larger.bin differ
--- /dev/null
+++ b/test/fix/global-larger.flags
@@ -1,0 +1,1 @@
+-fg
binary files /dev/null b/test/fix/global-larger.gb differ
binary files /dev/null b/test/fix/global-trash.bin differ
--- /dev/null
+++ b/test/fix/global-trash.flags
@@ -1,0 +1,1 @@
+-f G
binary files /dev/null b/test/fix/global-trash.gb differ
binary files /dev/null b/test/fix/global.bin differ
--- /dev/null
+++ b/test/fix/global.flags
@@ -1,0 +1,1 @@
+-f g
binary files /dev/null b/test/fix/global.gb differ
binary files /dev/null b/test/fix/header-edit.bin differ
--- /dev/null
+++ b/test/fix/header-edit.flags
@@ -1,0 +1,2 @@
+-Cf h
+Checks that the header checksum properly accounts for header modifications
binary files /dev/null b/test/fix/header-edit.gb differ
binary files /dev/null b/test/fix/header-trash.bin differ
--- /dev/null
+++ b/test/fix/header-trash.flags
@@ -1,0 +1,1 @@
+-f H
binary files /dev/null b/test/fix/header-trash.gb differ
binary files /dev/null b/test/fix/header.bin differ
--- /dev/null
+++ b/test/fix/header.flags
@@ -1,0 +1,1 @@
+-f h
binary files /dev/null b/test/fix/header.gb differ
binary files /dev/null b/test/fix/jp.bin differ
--- /dev/null
+++ b/test/fix/jp.flags
@@ -1,0 +1,1 @@
+-j
binary files /dev/null b/test/fix/jp.gb differ
binary files /dev/null b/test/fix/logo-trash.bin differ
--- /dev/null
+++ b/test/fix/logo-trash.flags
@@ -1,0 +1,1 @@
+-f L
binary files /dev/null b/test/fix/logo-trash.gb differ
binary files /dev/null b/test/fix/logo.bin differ
--- /dev/null
+++ b/test/fix/logo.flags
@@ -1,0 +1,1 @@
+-f l
binary files /dev/null b/test/fix/logo.gb differ
binary files /dev/null b/test/fix/mbc.bin differ
--- /dev/null
+++ b/test/fix/mbc.flags
@@ -1,0 +1,1 @@
+-m 177
binary files /dev/null b/test/fix/mbc.gb differ
binary files /dev/null b/test/fix/mbcless-ram.bin differ
--- /dev/null
+++ b/test/fix/mbcless-ram.err
@@ -1,0 +1,1 @@
+warning: MBC "ROM" has no RAM, but RAM size was set to 2
--- /dev/null
+++ b/test/fix/mbcless-ram.flags
@@ -1,0 +1,1 @@
+-m ROM -r 2
binary files /dev/null b/test/fix/mbcless-ram.gb differ
--- /dev/null
+++ b/test/fix/new-lic-trunc.bin
@@ -1,0 +1,3 @@
+���i& ���y��S����aJ���'ZD�y���1n�
���9�Q�H�p��yF������ߘ��8^��]�b��	F��mœ�
+��ьM��F��W��w�k��� O`����U�0��r����Ιܥ��7"��"f����fw� X��:�޸��Xp�V�Ī\-�`x���*N��y�a4�J�������܊�p���.7�c+�Q��Վ�B�He�z���1�O<n_&KHh���v���x���Y���A����;C�o�l��:�����̒�ܩ!�f����P�`@�:�z�v���d��Z�"�a*m*.:?�����Y˛c3�Ƞ>#`��\_�w���
+w4����"�q��yڷp$��i!�� �z/nuV�C�DuA5,﹥��3M �IO�=H��̲�u1��x�־���z_0�=�i��Q��;,t�*ێv;E��Zk�a>)��E�8�u��+f@�}oHDq��J�#��dљs�"��m8���V;
���']|��x
\ No newline at end of file
--- /dev/null
+++ b/test/fix/new-lic-trunc.err
@@ -1,0 +1,1 @@
+warning: Truncating new licensee "HOMEBREW" to 2 chars
--- /dev/null
+++ b/test/fix/new-lic-trunc.flags
@@ -1,0 +1,1 @@
+-k HOMEBREW
--- /dev/null
+++ b/test/fix/new-lic-trunc.gb
@@ -1,0 +1,3 @@
+���i& ���y��S����aJ���'ZD�y���1n�
���9�Q�H�p��yF������ߘ��8^��]�b��	F��mœ�
+��ьM��F��W��w�k��� O`����U�0��r����Ιܥ��7"��"f����fw� X��:�޸��Xp�V�Ī\-�`x���*N��y�a4�J�������܊�p���.7�c+�Q��Վ�B�He�z���1�O<n_&KHh���v���x���Y���A����;C�o�l��:�����̒�ܩ!�f����P�`@�:�z�v���d��Z�"�a*m*.:?�����YHOc3�Ƞ>#`��\_�w���
+w4����"�q��yڷp$��i!�� �z/nuV�C�DuA5,﹥��3M �IO�=H��̲�u1��x�־���z_0�=�i��Q��;,t�*ێv;E��Zk�a>)��E�8�u��+f@�}oHDq��J�#��dљs�"��m8���V;
���']|��x
\ No newline at end of file
--- /dev/null
+++ b/test/fix/new-lic.bin
@@ -1,0 +1,3 @@
+���i& ���y��S����aJ���'ZD�y���1n�
���9�Q�H�p��yF������ߘ��8^��]�b��	F��mœ�
+��ьM��F��W��w�k��� O`����U�0��r����Ιܥ��7"��"f����fw� X��:�޸��Xp�V�Ī\-�`x���*N��y�a4�J�������܊�p���.7�c+�Q��Վ�B�He�z���1�O<n_&KHh���v���x���Y���A����;C�o�l��:�����̒�ܩ!�f����P�`@�:�z�v���d��Z�"�a*m*.:?�����Y˛c3�Ƞ>#`��\_�w���
+w4����"�q��yڷp$��i!�� �z/nuV�C�DuA5,﹥��3M �IO�=H��̲�u1��x�־���z_0�=�i��Q��;,t�*ێv;E��Zk�a>)��E�8�u��+f@�}oHDq��J�#��dљs�"��m8���V;
���']|��x
\ No newline at end of file
--- /dev/null
+++ b/test/fix/new-lic.flags
@@ -1,0 +1,1 @@
+-k HB
--- /dev/null
+++ b/test/fix/new-lic.gb
@@ -1,0 +1,3 @@
+���i& ���y��S����aJ���'ZD�y���1n�
���9�Q�H�p��yF������ߘ��8^��]�b��	F��mœ�
+��ьM��F��W��w�k��� O`����U�0��r����Ιܥ��7"��"f����fw� X��:�޸��Xp�V�Ī\-�`x���*N��y�a4�J�������܊�p���.7�c+�Q��Վ�B�He�z���1�O<n_&KHh���v���x���Y���A����;C�o�l��:�����̒�ܩ!�f����P�`@�:�z�v���d��Z�"�a*m*.:?�����YHBc3�Ƞ>#`��\_�w���
+w4����"�q��yڷp$��i!�� �z/nuV�C�DuA5,﹥��3M �IO�=H��̲�u1��x�־���z_0�=�i��Q��;,t�*ێv;E��Zk�a>)��E�8�u��+f@�}oHDq��J�#��dљs�"��m8���V;
���']|��x
\ No newline at end of file
--- /dev/null
+++ b/test/fix/noexist.err
@@ -1,0 +1,2 @@
+FATAL: Failed to open "noexist" for reading+writing: No such file or directory
+Fixing "noexist" failed with 1 error
binary files /dev/null b/test/fix/old-lic-hex.bin differ
--- /dev/null
+++ b/test/fix/old-lic-hex.flags
@@ -1,0 +1,1 @@
+-l 0x2a
binary files /dev/null b/test/fix/old-lic-hex.gb differ
--- /dev/null
+++ b/test/fix/old-lic.bin
@@ -1,0 +1,2 @@
+�EZ/:�����Ѐ��J/����_<�&�A�����XgAP 
+`�1@Wh��||ø�e뻬e^"7(*YV��T���OY!���̔]���=���y�V~x�a�]�;���¡�zUm��z:d@֪�b� 'vʆq�W��_�����2)G�v(k���g,�p*@��!�Dõ�0k�����e�%ǚod���$r���<�:��cgi�"aF'=f����EcVcL.03"w),�	HQi�D�f�3�s(-$?a0�@��.O�B{y�z�ʫ�	h�/\�S�]�o��m'2D#/gr�W3�Χ��&U���EgMh�-A�6�����4M|1�q0O��*$�O�/n���g��2R��Q�X�_���[숔�p��\�)ƧTS�T�aP*'[�x��>߅�Ub[Ԧ�9s��I��P
��ģ�dn��$��(�ɔ���Eys�ڈ�Ƈ6�v���H	�A����qnK�O{ֻw�k&��kQ�OjCe��@
\ No newline at end of file
--- /dev/null
+++ b/test/fix/old-lic.flags
@@ -1,0 +1,1 @@
+-l 42
--- /dev/null
+++ b/test/fix/old-lic.gb
@@ -1,0 +1,2 @@
+�EZ/:�����Ѐ��J/����_<�&�A�����XgAP 
+`�1@Wh��||ø�e뻬e^"7(*YV��T���OY!���̔]���=���y�V~x�a�]�;���¡�zUm��z:d@֪�b� 'vʆq�W��_�����2)G�v(k���g,�p*@��!�Dõ�0k�����e�%ǚod���$r���<�:��cgi�"aF'=f����EcVcL.03"w),�	HQi�D�f�3�s(-$?a0�@��.O�B{y�z�ʫ�	h�/\�S�]�o��m'2D#/gr�W3�Χ��&U���EgMh�-A�*6�����4M|1�q0O��*$�O�/n���g��2R��Q�X�_���[숔�p��\�)ƧTS�T�aP*'[�x��>߅�Ub[Ԧ�9s��I��P
��ģ�dn��$��(�ɔ���Eys�ڈ�Ƈ6�v���H	�A����qnK�O{ֻw�k&��kQ�OjCe��@
\ No newline at end of file
binary files /dev/null b/test/fix/padding-bigperfect.bin differ
--- /dev/null
+++ b/test/fix/padding-bigperfect.flags
@@ -1,0 +1,1 @@
+-p0xff
binary files /dev/null b/test/fix/padding-bigperfect.gb differ
binary files /dev/null b/test/fix/padding-imperfect.bin differ
--- /dev/null
+++ b/test/fix/padding-imperfect.flags
@@ -1,0 +1,1 @@
+-p255
binary files /dev/null b/test/fix/padding-imperfect.gb differ
binary files /dev/null b/test/fix/padding-large.bin differ
--- /dev/null
+++ b/test/fix/padding-large.flags
@@ -1,0 +1,1 @@
+-p 0xff
binary files /dev/null b/test/fix/padding-large.gb differ
binary files /dev/null b/test/fix/padding-larger.bin differ
--- /dev/null
+++ b/test/fix/padding-larger.flags
@@ -1,0 +1,1 @@
+-p 255
binary files /dev/null b/test/fix/padding-larger.gb differ
binary files /dev/null b/test/fix/padding-perfect.bin differ
--- /dev/null
+++ b/test/fix/padding-perfect.flags
@@ -1,0 +1,1 @@
+-p 0xFF
binary files /dev/null b/test/fix/padding-perfect.gb differ
binary files /dev/null b/test/fix/padding-rom0.bin differ
--- /dev/null
+++ b/test/fix/padding-rom0.flags
@@ -1,0 +1,1 @@
+-p 255
binary files /dev/null b/test/fix/padding-rom0.gb differ
binary files /dev/null b/test/fix/padding.bin differ
--- /dev/null
+++ b/test/fix/padding.flags
@@ -1,0 +1,1 @@
+-p0xFF
binary files /dev/null b/test/fix/padding.gb differ
binary files /dev/null b/test/fix/plain.bin differ
binary files /dev/null b/test/fix/plain.gb differ
binary files /dev/null b/test/fix/ram.bin differ
--- /dev/null
+++ b/test/fix/ram.flags
@@ -1,0 +1,1 @@
+-r 232
binary files /dev/null b/test/fix/ram.gb differ
binary files /dev/null b/test/fix/ramful-mbc-no-ram.bin differ
--- /dev/null
+++ b/test/fix/ramful-mbc-no-ram.err
@@ -1,0 +1,1 @@
+warning: MBC "MBC3+RAM" has RAM, but RAM size was set to 0
--- /dev/null
+++ b/test/fix/ramful-mbc-no-ram.flags
@@ -1,0 +1,1 @@
+-m MBC3+RAM -r 0
binary files /dev/null b/test/fix/ramful-mbc-no-ram.gb differ
binary files /dev/null b/test/fix/ramful-mbc.bin differ
--- /dev/null
+++ b/test/fix/ramful-mbc.flags
@@ -1,0 +1,1 @@
+-m MBC3+RAM -r 2
binary files /dev/null b/test/fix/ramful-mbc.gb differ
binary files /dev/null b/test/fix/ramless-mbc.bin differ
--- /dev/null
+++ b/test/fix/ramless-mbc.flags
@@ -1,0 +1,1 @@
+-m MBC3 -r 0
binary files /dev/null b/test/fix/ramless-mbc.gb differ
binary files /dev/null b/test/fix/rom-ram.bin differ
--- /dev/null
+++ b/test/fix/rom-ram.err
@@ -1,0 +1,1 @@
+warning: ROM+RAM / ROM+RAM+BATTERY are under-specified and poorly supported
--- /dev/null
+++ b/test/fix/rom-ram.flags
@@ -1,0 +1,1 @@
+-m8
binary files /dev/null b/test/fix/rom-ram.gb differ
binary files /dev/null b/test/fix/sgb-licensee.bin differ
--- /dev/null
+++ b/test/fix/sgb-licensee.err
@@ -1,0 +1,1 @@
+warning: SGB compatibility enabled, but old licensee is 0x45, not 0x33
--- /dev/null
+++ b/test/fix/sgb-licensee.flags
@@ -1,0 +1,1 @@
+-sl69
binary files /dev/null b/test/fix/sgb.bin differ
--- /dev/null
+++ b/test/fix/sgb.flags
@@ -1,0 +1,1 @@
+-s
binary files /dev/null b/test/fix/sgb.gb differ
--- /dev/null
+++ b/test/fix/test.sh
@@ -1,0 +1,99 @@
+#!/bin/bash
+
+export LC_ALL=C
+
+tmpdir="$(mktemp -d)"
+src="$PWD"
+rc=0
+
+cp ../../{rgbfix,contrib/gbdiff.bash} "$tmpdir"
+cd "$tmpdir"
+trap "cd; rm -rf '$tmpdir'" EXIT
+
+bold="$(tput bold)"
+resbold="$(tput sgr0)"
+red="$(tput setaf 1)"
+green="$(tput setaf 2)"
+rescolors="$(tput op)"
+
+tryDiff () {
+	if ! diff -u --strip-trailing-cr "$1" "$2"; then
+		echo "${bold}${red}${3:-$1} mismatch!${rescolors}${resbold}"
+		false
+	fi
+}
+
+tryCmp () {
+	if ! cmp "$1" "$2"; then
+		./gbdiff.bash "$1" "$2"
+		echo "${bold}${red}${3:-$1} mismatch!${rescolors}${resbold}"
+		false
+	fi
+}
+
+runTest () {
+	flags="$(head -n 1 "$2/$1.flags")" # Allow other lines to serve as comments
+
+	for variant in '' ' piped'; do
+		our_rc=0
+		if [[ $progress -ne 0 ]]; then
+			echo "${bold}${green}$1${variant}...${rescolors}${resbold}"
+		fi
+		if [[ -z "$variant" ]]; then
+			cp "$2/$1.bin" out.gb
+			if [[ -n "$(eval ./rgbfix $flags out.gb '2>out.err')" ]]; then
+				echo "${bold}${red}Fixing $1 in-place shouldn't output anything on stdout!${rescolors}${resbold}"
+				our_rc=1
+			fi
+			subst='out.gb'
+		else
+			# Stop! This is not a Useless Use Of Cat. Using cat instead of
+			# stdin redirection makes the input an unseekable pipe - a scenario
+			# that's harder to deal with.
+			cat "$2/$1.bin" | eval ./rgbfix "$flags" '>out.gb' '2>out.err'
+			subst='<stdin>'
+		fi
+
+		sed "s/$subst/<filename>/g" "out.err" | tryDiff "$2/$1.err" -  "$1.err${variant}"
+		our_rc=$(($? || $our_rc))
+		if [[ -r "$2/$1.gb" ]]; then
+			tryCmp "$2/$1.gb" "out.gb"  "$1.gb${variant}"
+			our_rc=$(($? || $our_rc))
+		fi
+
+		rc=$(($rc || $our_rc))
+		if [[ $our_rc -ne 0 ]]; then break; fi
+	done
+}
+
+rm -f padding*_* # Delete padding test cases generated but not deleted (e.g. interrupted)
+
+progress=1
+for i in "$src"/*.bin; do
+	runTest "$(basename "$i" .bin)" "$src"
+done
+
+# Check the result with all different padding bytes
+echo "${bold}Checking padding...${resbold}"
+cp "$src"/padding{,-large,-larger}.bin .
+touch padding{,-large,-larger}.err
+progress=0
+for b in {0..254}; do
+	printf "\r$b..."
+	for suffix in '' -large -larger; do
+		cat <<<'-p $b' >padding$suffix.flags
+		tr '\377' \\$(($b / 64))$((($b / 8) % 8))$(($b % 8)) <"$src/padding$suffix.gb" >padding$suffix.gb # OK because $FF bytes are only used for padding
+		runTest padding${suffix} .
+	done
+done
+printf "\rDone! \n"
+
+# TODO: check MBC names
+
+# Check that RGBFIX errors out when inputting a non-existent file...
+./rgbfix noexist 2>out.err
+rc=$(($rc || $? != 1))
+tryDiff "$src/noexist.err" out.err  noexist.err
+rc=$(($rc || $?))
+
+exit $rc
binary files /dev/null b/test/fix/title-color-trunc-rev.bin differ
--- /dev/null
+++ b/test/fix/title-color-trunc-rev.err
@@ -1,0 +1,1 @@
+warning: Truncating title "0123456789ABCDEF" to 15 chars
--- /dev/null
+++ b/test/fix/title-color-trunc-rev.flags
@@ -1,0 +1,3 @@
+-t 0123456789ABCDEF -C
+Checks that the CGB flag correctly truncates the title to 15 chars only,
+even when it's specified *after* the title..!
binary files /dev/null b/test/fix/title-color-trunc-rev.gb differ
binary files /dev/null b/test/fix/title-color-trunc.bin differ
--- /dev/null
+++ b/test/fix/title-color-trunc.err
@@ -1,0 +1,1 @@
+warning: Truncating title "0123456789ABCDEF" to 15 chars
--- /dev/null
+++ b/test/fix/title-color-trunc.flags
@@ -1,0 +1,2 @@
+-C -t 0123456789ABCDEF
+Checks that the CGB flag correctly truncates the title to 15 chars only
binary files /dev/null b/test/fix/title-color-trunc.gb differ
binary files /dev/null b/test/fix/title-compat-trunc-rev.bin differ
--- /dev/null
+++ b/test/fix/title-compat-trunc-rev.err
@@ -1,0 +1,1 @@
+warning: Truncating title "0123456789ABCDEF" to 15 chars
--- /dev/null
+++ b/test/fix/title-compat-trunc-rev.flags
@@ -1,0 +1,3 @@
+-t 0123456789ABCDEF -c
+Checks that the CGB compat flag correctly truncates the title to 15 chars only,
+even when it's specified *after* the title..!
binary files /dev/null b/test/fix/title-compat-trunc-rev.gb differ
binary files /dev/null b/test/fix/title-compat-trunc.bin differ
--- /dev/null
+++ b/test/fix/title-compat-trunc.err
@@ -1,0 +1,1 @@
+warning: Truncating title "0123456789ABCDEF" to 15 chars
--- /dev/null
+++ b/test/fix/title-compat-trunc.flags
@@ -1,0 +1,2 @@
+-c -t 0123456789ABCDEF
+Checks that the CGB compat flag correctly truncates the title to 15 chars only
binary files /dev/null b/test/fix/title-compat-trunc.gb differ
binary files /dev/null b/test/fix/title-gameid-trunc-rev.bin differ
--- /dev/null
+++ b/test/fix/title-gameid-trunc-rev.err
@@ -1,0 +1,1 @@
+warning: Truncating title "0123456789ABCDEF" to 11 chars
--- /dev/null
+++ b/test/fix/title-gameid-trunc-rev.flags
@@ -1,0 +1,3 @@
+-t 0123456789ABCDEF -i rgbd
+Checks that the game ID flag correctly truncates the title to 11 chars only,
+even when it's specified *after* the title..!
binary files /dev/null b/test/fix/title-gameid-trunc-rev.gb differ
binary files /dev/null b/test/fix/title-gameid-trunc.bin differ
--- /dev/null
+++ b/test/fix/title-gameid-trunc.err
@@ -1,0 +1,1 @@
+warning: Truncating title "0123456789ABCDEF" to 11 chars
--- /dev/null
+++ b/test/fix/title-gameid-trunc.flags
@@ -1,0 +1,2 @@
+-i rgbd -t 0123456789ABCDEF
+Checks that the game ID flag correctly truncates the title to 11 chars only
binary files /dev/null b/test/fix/title-gameid-trunc.gb differ
binary files /dev/null b/test/fix/title-pad.bin differ
--- /dev/null
+++ b/test/fix/title-pad.flags
@@ -1,0 +1,1 @@
+-t "I LOVE YOU"
binary files /dev/null b/test/fix/title-pad.gb differ
binary files /dev/null b/test/fix/title-trunc.bin differ
--- /dev/null
+++ b/test/fix/title-trunc.err
@@ -1,0 +1,1 @@
+warning: Truncating title "0123456789ABCDEFGHIJK" to 16 chars
--- /dev/null
+++ b/test/fix/title-trunc.flags
@@ -1,0 +1,1 @@
+-t 0123456789ABCDEFGHIJK
binary files /dev/null b/test/fix/title-trunc.gb differ
binary files /dev/null b/test/fix/title.bin differ
--- /dev/null
+++ b/test/fix/title.flags
@@ -1,0 +1,1 @@
+-t "Game Boy dev rox"
binary files /dev/null b/test/fix/title.gb differ
--- /dev/null
+++ b/test/fix/tooshort.bin
@@ -1,0 +1,1 @@
+g�1B���m�k��&)�v
\ No newline at end of file
--- /dev/null
+++ b/test/fix/tooshort.err
@@ -1,0 +1,2 @@
+FATAL: "<filename>" too short, expected at least 336 ($150) bytes, got only 20
+Fixing "<filename>" failed with 1 error
binary files /dev/null b/test/fix/verify-pad.bin differ
--- /dev/null
+++ b/test/fix/verify-pad.flags
@@ -1,0 +1,4 @@
+-vp 69
+Check that the global checksum is correctly affected by padding:
+Padding adds extra bytes (carefully picked *not* to be 0, or other values),
+which must be properly accounted for.
binary files /dev/null b/test/fix/verify-pad.gb differ
binary files /dev/null b/test/fix/verify-trash.bin differ
--- /dev/null
+++ b/test/fix/verify-trash.flags
@@ -1,0 +1,2 @@
+-f LHG
+Checks that the global checksum is correctly affected by the header checksum
binary files /dev/null b/test/fix/verify-trash.gb differ
binary files /dev/null b/test/fix/verify.bin differ
--- /dev/null
+++ b/test/fix/verify.flags
@@ -1,0 +1,2 @@
+-v
+Checks that the global checksum is correctly affected by the header checksum
binary files /dev/null b/test/fix/verify.gb differ
binary files /dev/null b/test/fix/version.bin differ
--- /dev/null
+++ b/test/fix/version.flags
@@ -1,0 +1,1 @@
+-n 11
binary files /dev/null b/test/fix/version.gb differ
--- a/test/run-tests.sh
+++ b/test/run-tests.sh
@@ -14,13 +14,11 @@
 
 # Tests included with the repository
 
-pushd asm
-./test.sh
-popd
-
-pushd link
-./test.sh
-popd
+for dir in asm link fix; do
+	pushd $dir
+	./test.sh
+	popd
+done
 
 # Test some significant external projects that use RGBDS
 # When adding new ones, don't forget to add them to the .gitignore!