diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 654e0c72..26ee7944 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -97,7 +97,7 @@ All tests begin by assembling the `.asm` file into an object file, which will be These simply check that RGBLINK's output matches some expected output. -A `.out` file **must** exist, and RGBLINK's output must match that file's contents. +A `.out` file **must** exist, and RGBLINK's total output must match that file's contents. Additionally, if a `.out.bin` file exists, the `.gb` file generated by RGBLINK must match it. @@ -106,14 +106,14 @@ Additionally, if a `.out.bin` file exists, the `.gb` file generated by RGBLINK m These allow applying various linker scripts to the same object file. If one or more `.link` files exist, whose names start the same as the `.asm` file, then each of those files correspond to one test. -Each `.link` linker script **must** be accompanied by a `.out` file, and RGBLINK's output must match that file's contents when passed the corresponding linker script. +Each `.link` linker script **must** be accompanied by a `.out` file, and RGBLINK's total output must match that file's contents when passed the corresponding linker script. #### Variant tests These allow testing RGBLINK's `-d`, `-t`, and `-w` flags. If one or more -<flag>.out or -no-<flag>.out files exist, then each of them corresponds to one test. -The object file will be linked with and without said flag, respectively; and in each case, RGBLINK's output must match the `.out` file's contents. +The object file will be linked with and without said flag, respectively; and in each case, RGBLINK's total output must match the `.out` file's contents. ### RGBFIX @@ -123,8 +123,11 @@ Each one is a text file whose first line contains flags to pass to RGBFIX. RGBFIX will be invoked on the `.bin` file if it exists, or else on default-input.bin. +If no `.out` file exist, RGBFIX is not expected to output anything. +If one *does* exist, RGBFIX's output **must** match the `.out` file's contents. + If no `.err` file exists, RGBFIX is simply expected to be able to process the file normally. -If one *does* exist, RGBFIX's return status is ignored, but its output **must** match the `.err` file's contents. +If one *does* exist, RGBFIX's return status is ignored, but its error output **must** match the `.err` file's contents. Additionally, if a `.gb` file exists, the output of RGBFIX must match the `.gb`. diff --git a/Makefile b/Makefile index a0423d4a..909b2e92 100644 --- a/Makefile +++ b/Makefile @@ -97,7 +97,9 @@ src/link/main.o: src/link/script.hpp rgbfix_obj := \ ${common_obj} \ - src/fix/main.o + src/fix/main.o \ + src/fix/mbc.o \ + src/fix/warning.o rgbgfx_obj := \ ${common_obj} \ diff --git a/contrib/bash_compl/_rgbasm.bash b/contrib/bash_compl/_rgbasm.bash index 3ea76485..1ec6c85a 100755 --- a/contrib/bash_compl/_rgbasm.bash +++ b/contrib/bash_compl/_rgbasm.bash @@ -24,13 +24,13 @@ _rgbasm_completions() { # Empty long opt = it doesn't exit # See the `state` variable below for info about `state_after` declare -A opts=( - [V]="version:normal" [h]="help:normal" - [E]="export-all:normal" - [v]="verbose:normal" + [V]="version:normal" + [W]="warning:warning" [w]=":normal" [b]="binary-digits:unk" [D]="define:unk" + [E]="export-all:normal" [g]="gfx-chars:unk" [I]="include:dir" [M]="dependfile:glob-*.mk *.d" @@ -40,7 +40,7 @@ _rgbasm_completions() { [Q]="q-precision:unk" [r]="recursion-depth:unk" [s]="state:unk" - [W]="warning:warning" + [v]="verbose:normal" [X]="max-errors:unk" ) # Parse command-line up to current word diff --git a/contrib/bash_compl/_rgbfix.bash b/contrib/bash_compl/_rgbfix.bash index f36c96e9..9b827faf 100755 --- a/contrib/bash_compl/_rgbfix.bash +++ b/contrib/bash_compl/_rgbfix.bash @@ -7,15 +7,15 @@ _rgbfix_completions() { # Empty long opt = it doesn't exit # See the `state` variable below for info about `state_after` declare -A opts=( - [V]="version:normal" [h]="help:normal" - [j]="non-japanese:normal" - [s]="sgb-compatible:normal" - [v]="validate:normal" + [V]="version:normal" + [W]="warning:warning" + [w]=":normal" [C]="color-only:normal" [c]="color-compatible:normal" [f]="fix-spec:fix-spec" [i]="game-id:unk" + [j]="non-japanese:normal" [k]="new-licensee:unk" [L]="custom-logo:glob-*.1bpp" [l]="old-licensee:unk" @@ -24,7 +24,9 @@ _rgbfix_completions() { [o]="output:glob-*.gb *.gbc *.sgb" [p]="pad-value:unk" [r]="ram-size:unk" + [s]="sgb-compatible:normal" [t]="title:unk" + [v]="validate:normal" ) # Parse command-line up to current word local opt_ena=true @@ -140,6 +142,16 @@ _rgbfix_completions() { case "$state" in unk) # Return with no replies: no idea what to complete! ;; + warning) + mapfile -t COMPREPLY < <(compgen -W " + mbc + overwrite + sgb + truncation + all + everything + error" -P "${cur_word:0:$optlen}" -- "${cur_word:$optlen}") + ;; fix-spec) COMPREPLY=( "${cur_word}"{l,h,g,L,H,G} ) ;; diff --git a/contrib/bash_compl/_rgbgfx.bash b/contrib/bash_compl/_rgbgfx.bash index cf412a53..356db7b9 100755 --- a/contrib/bash_compl/_rgbgfx.bash +++ b/contrib/bash_compl/_rgbgfx.bash @@ -7,37 +7,38 @@ _rgbgfx_completions() { # Empty long opt = it doesn't exit # See the `state` variable below for info about `state_after` declare -A opts=( - [V]="version:normal" [h]="help:normal" - [C]="color-curve:normal" - [m]="mirror-tiles:normal" - [O]="group-outputs:normal" - [u]="unique-tiles:normal" - [v]="verbose:normal" - [X]="mirror-x:normal" - [Y]="mirror-y:normal" - [Z]="columns:normal" - [a]="attr-map:glob-*.attrmap" + [V]="version:normal" + [W]="warning:warning" + [w]=":normal" [A]="auto-attr-map:normal" + [a]="attr-map:glob-*.attrmap" [B]="background-color:unk" [b]="base-tiles:unk" + [C]="color-curve:normal" [c]="colors:unk" [d]="depth:unk" [i]="input-tileset:glob-*.2bpp" [L]="slice:unk" + [m]="mirror-tiles:normal" [N]="nb-tiles:unk" [n]="nb-palettes:unk" + [O]="group-outputs:normal" [o]="output:glob-*.2bpp" - [p]="palette:glob-*.pal" [P]="auto-palette:normal" - [q]="palette-map:glob-*.palmap" + [p]="palette:glob-*.pal" [Q]="auto-palette-map:normal" + [q]="palette-map:glob-*.palmap" [r]="reverse:unk" [s]="palette-size:unk" - [t]="tilemap:glob-*.tilemap" [T]="auto-tilemap:normal" - [W]="warning:warning" + [t]="tilemap:glob-*.tilemap" + [u]="unique-tiles:normal" + [v]="verbose:normal" + [X]="mirror-x:normal" [x]="trim-end:unk" + [Y]="mirror-y:normal" + [Z]="columns:normal" ) # Parse command-line up to current word local opt_ena=true diff --git a/contrib/bash_compl/_rgblink.bash b/contrib/bash_compl/_rgblink.bash index 33b43364..03191a73 100755 --- a/contrib/bash_compl/_rgblink.bash +++ b/contrib/bash_compl/_rgblink.bash @@ -7,21 +7,21 @@ _rgblink_completions() { # Empty long opt = it doesn't exit # See the `state` variable below for info about `state_after` declare -A opts=( - [V]="version:normal" [h]="help:normal" - [d]="dmg:normal" - [t]="tiny:normal" - [v]="verbose:normal" - [w]="wramx:normal" - [x]="nopad:normal" - [l]="linkerscript:glob-*" + [V]="version:normal" + [W]="warning:warning" [M]="no-sym-in-map:normal" + [d]="dmg:normal" + [l]="linkerscript:glob-*" [m]="map:glob-*.map" [n]="sym:glob-*.sym" [O]="overlay:glob-*.gb *.gbc *.sgb" [o]="output:glob-*.gb *.gbc *.sgb" [p]="pad:unk" - [W]="warning:warning" + [t]="tiny:normal" + [v]="verbose:normal" + [w]="wramx:normal" + [x]="nopad:normal" ) # Parse command-line up to current word local opt_ena=true diff --git a/contrib/zsh_compl/_rgbfix b/contrib/zsh_compl/_rgbfix index 11d157ac..4e490faf 100644 --- a/contrib/zsh_compl/_rgbfix +++ b/contrib/zsh_compl/_rgbfix @@ -34,6 +34,21 @@ _mbc_names() { _describe "MBC name" mbc_names } +_rgbfix_warnings() { + local warnings=( + 'error:Turn all warnings into errors' + + 'all:Enable most warning messages' + 'everything:Enable literally everything' + + 'mbc:Warn about issues with MBC specs' + 'overwrite:Warn when overwriting non-zero bytes' + 'sgb:Warn when SGB flag conflicts with old licensee code' + 'truncation:Warn when values are truncated to fit' + ) + _describe warning warnings +} + local args=( # Arguments are listed here in the same order as in the manual, except for the version and help '(- : * options)'{-V,--version}'[Print version number and exit]' @@ -45,6 +60,7 @@ local args=( '(-O --overwrite)'{-O,--overwrite}'[Allow overwriting non-zero bytes]' '(-s --sgb-compatible)'{-s,--sgb-compatible}'[Set the SGB flag]' '(-f --fix-spec -v --validate)'{-v,--validate}'[Shorthand for -f lhg]' + -w'[Disable all warnings]' '(-f --fix-spec -v --validate)'{-f,--fix-spec}'+[Fix or trash some header values]:fix spec:' '(-i --game-id)'{-i,--game-id}'+[Set game ID string]:4-char game ID:' @@ -57,6 +73,7 @@ local args=( '(-p --pad-value)'{-p,--pad-value}'+[Pad to next valid size using this byte as padding]:padding byte:' '(-r --ram-size)'{-r,--ram-size}'+[Set RAM size]:ram size byte:' '(-t --title)'{-t,--title}'+[Set title string]:11-char title string:' + '(-W --warning)'{-W,--warning}'+[Toggle warning flags]:warning flag:_rgbfix_warnings' '*'":ROM files:_files -g '*.{gb,sgb,gbc}'" ) diff --git a/contrib/zsh_compl/_rgbgfx b/contrib/zsh_compl/_rgbgfx index 59e39be0..ae9f0cd9 100644 --- a/contrib/zsh_compl/_rgbgfx +++ b/contrib/zsh_compl/_rgbgfx @@ -35,7 +35,7 @@ local args=( '(-q --palette-map -Q --auto-palette-map)'{-Q,--auto-palette-map}'[Shortcut for -p .palmap]' '(-t --tilemap -T --auto-tilemap)'{-T,--auto-tilemap}'[Shortcut for -t .tilemap]' '(-u --unique-tiles)'{-u,--unique-tiles}'[Eliminate redundant tiles]' - {-v,--verbose}'[Enable verbose output]' + '(-v --verbose)'{-v,--verbose}'[Enable verbose output]' -w'[Disable all warnings]' '(-X --mirror-x)'{-X,--mirror-x}'[Eliminate horizontally mirrored tiles from output]' '(-Y --mirror-y)'{-Y,--mirror-y}'[Eliminate vertically mirrored tiles from output]' diff --git a/include/fix/mbc.hpp b/include/fix/mbc.hpp new file mode 100644 index 00000000..d43b32a0 --- /dev/null +++ b/include/fix/mbc.hpp @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: MIT + +#ifndef RGBDS_FIX_MBC_HPP +#define RGBDS_FIX_MBC_HPP + +#include +#include + +constexpr uint16_t UNSPECIFIED = 0x200; +static_assert(UNSPECIFIED > 0xFF, "UNSPECIFIED should not be in byte range!"); + +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, + + // "Extended" values (still valid, but not directly actionable) + + // A high byte of 0x01 means TPP1, the low byte is the requested features + // This does not include SRAM, which is instead implied by a non-zero SRAM size + // Note: Multiple rumble speeds imply rumble + TPP1 = 0x100, + TPP1_RUMBLE = 0x101, + TPP1_MULTIRUMBLE = 0x102, // Should not be possible + TPP1_MULTIRUMBLE_RUMBLE = 0x103, + TPP1_TIMER = 0x104, + TPP1_TIMER_RUMBLE = 0x105, + TPP1_TIMER_MULTIRUMBLE = 0x106, // Should not be possible + TPP1_TIMER_MULTIRUMBLE_RUMBLE = 0x107, + TPP1_BATTERY = 0x108, + TPP1_BATTERY_RUMBLE = 0x109, + TPP1_BATTERY_MULTIRUMBLE = 0x10A, // Should not be possible + TPP1_BATTERY_MULTIRUMBLE_RUMBLE = 0x10B, + TPP1_BATTERY_TIMER = 0x10C, + TPP1_BATTERY_TIMER_RUMBLE = 0x10D, + TPP1_BATTERY_TIMER_MULTIRUMBLE = 0x10E, // Should not be possible + TPP1_BATTERY_TIMER_MULTIRUMBLE_RUMBLE = 0x10F, + + // Error values + MBC_NONE = UNSPECIFIED, // No MBC specified, do not act on it +}; + +void mbc_PrintAcceptedNames(FILE *file); + +bool mbc_HasRAM(MbcType type); +char const *mbc_Name(MbcType type); +MbcType mbc_ParseName(char const *name, uint8_t &tpp1Major, uint8_t &tpp1Minor); + +#endif // RGBDS_FIX_MBC_HPP diff --git a/include/fix/warning.hpp b/include/fix/warning.hpp new file mode 100644 index 00000000..6b9ed96d --- /dev/null +++ b/include/fix/warning.hpp @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT + +#ifndef RGBDS_FIX_WARNING_HPP +#define RGBDS_FIX_WARNING_HPP + +#include "diagnostics.hpp" + +enum WarningLevel { + LEVEL_DEFAULT, // Warnings that are enabled by default + LEVEL_ALL, // Warnings that probably indicate an error + LEVEL_EVERYTHING, // Literally every warning +}; + +enum WarningID { + WARNING_MBC, // Issues with MBC specs + WARNING_OVERWRITE, // Overwriting non-zero bytes + WARNING_SGB, // SGB flag conflicts with old licensee code + WARNING_TRUNCATION, // Truncating values to fit + + NB_PLAIN_WARNINGS, + + NB_WARNINGS = NB_PLAIN_WARNINGS, +}; + +extern Diagnostics warnings; + +// Warns the user about problems that don't prevent fixing the ROM +[[gnu::format(printf, 2, 3)]] +void warning(WarningID id, char const *fmt, ...); + +// Prints an error, and increments the error count +[[gnu::format(printf, 1, 2)]] +void error(char const *fmt, ...); + +// Prints a fatal error and exits +[[gnu::format(printf, 1, 2)]] +void fatal(char const *fmt, ...); + +void resetErrors(); +uint32_t checkErrors(char const *filename); + +#endif // RGBDS_FIX_WARNING_HPP diff --git a/man/rgbfix.1 b/man/rgbfix.1 index 66c723e9..8bec3cd5 100644 --- a/man/rgbfix.1 +++ b/man/rgbfix.1 @@ -8,7 +8,7 @@ .Nd Game Boy header utility and checksum fixer .Sh SYNOPSIS .Nm -.Op Fl hjOsVv +.Op Fl hjOsVvw .Op Fl C | c .Op Fl f Ar fix_spec .Op Fl i Ar game_id @@ -21,6 +21,7 @@ .Op Fl p Ar pad_value .Op Fl r Ar ram_size .Op Fl t Ar title_str +.Op Fl W Ar warning .Op Ar .Sh DESCRIPTION The @@ -98,7 +99,7 @@ Print help text for the program and exit. Set the game ID string .Pq Ad 0x13F Ns \(en Ns Ad 0x142 to a given string. -If it's longer than 4 chars, it will be truncated, and a warning emitted. +If it's longer than 4 characters, it will be truncated. .It Fl j , Fl \-non-japanese Set the non-Japanese region flag .Pq Ad 0x14A @@ -107,7 +108,7 @@ to 0x01. Set the new licensee string .Pq Ad 0x144 Ns \(en Ns Ad 0x145 to a given string. -If it's longer than 2 chars, it will be truncated, and a warning emitted. +If it's longer than 2 characters, it will be truncated. .It Fl L Ar logo_file , Fl \-logo Ar logo_file Specify a logo file to use instead of the official Nintendo logo. The file must be 48 bytes of 1bpp tile data; the source image should be 48 pixels wide and 8 pixels tall. @@ -124,6 +125,8 @@ to a given value from 0 to 0xFF. This value may also be an MBC name. The list of accepted names can be obtained by passing .Ql Cm help +or +.Ql Cm list as the argument. Any amount of whitespace (space and tabs) is allowed around plus signs, and the order of "components" is free, as long as the MBC name is first. There are special considerations to take for the TPP1 mapper; see the @@ -134,7 +137,8 @@ Set the ROM version .Pq Ad 0x14C to a given value from 0 to 0xFF. .It Fl O , Fl \-overwrite -Allow overwriting different non-zero bytes in the header without a warning being emitted. +Alias for +.Fl Wno-overwrite . .It Fl o Ar out_file , Fl \-output Ar out_file Write the modified ROM image to the given file, or '-' to write to standard output. If not specified, the input files are modified in-place, or written to standard output if read from standard input. @@ -154,15 +158,14 @@ to a given value from 0 to 0xFF. 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. +This flag will be ignored by the SGB unless the old licensee code +.Pq Fl -l +is 0x33! .It Fl t Ar title , Fl \-title Ar title Set the title string .Pq Ad 0x134 Ns \(en Ns Ad 0x143 to a given string. -If the title is longer than the max length, it will be truncated, and a warning emitted. +If the title is longer than the maximum length, it will be truncated. The max length is 11 characters if the game ID .Pq Fl i is specified, 15 characters if the CGB flag @@ -175,6 +178,77 @@ Print the version of the program and exit. .It Fl v , Fl \-validate Equivalent to .Fl f Cm lhg . +.It Fl W Ar warning , Fl \-warning Ar warning +Set warning flag +.Ar warning . +A warning message will be printed if +.Ar warning +is an unknown warning flag. +See the +.Sx DIAGNOSTICS +section for a list of warnings. +.It Fl w +Disable all warning output, even when turned into errors. +.El +.Sh DIAGNOSTICS +Warnings are diagnostic messages that indicate possibly erroneous behavior that does not necessarily compromise the header-fixing process. +The following options alter the way warnings are processed. +.Bl -tag -width Ds +.It Fl Werror +Make all warnings into errors. +This can be negated as +.Fl Wno-error +to prevent turning all warnings into errors. +.It Fl Werror= +Make the specified warning or meta warning into an error. +A warning's name is appended +.Pq example: Fl Werror=overwrite , +and this warning is implicitly enabled and turned into an error. +This can be negated as +.Fl Wno-error= +to prevent turning a specified warning into an error, even if +.Fl Werror +is in effect. +.El +.Pp +The following warnings are +.Dq meta +warnings, that enable a collection of other warnings. +If a specific warning is toggled via a meta flag and a specific one, the more specific one takes priority. +The position on the command-line acts as a tie breaker, the last one taking effect. +.Bl -tag -width Ds +.It Fl Wall +This enables warnings that are likely to indicate an error or undesired behavior, and that can easily be fixed. +.It Fl Weverything +Enables literally every warning. +.El +.Pp +The following warnings are actual warning flags; with each description, the corresponding warning flag is included. +Note that each of these flag also has a negation (for example, +.Fl Wtruncation +enables the warning that +.Fl Wno-truncation +disables; and +.Fl Wall +enables every warning that +.Fl Wno-all +disables). +Only the non-default flag is listed here. +Ignoring the +.Dq no- +prefix, entries are listed alphabetically. +.Bl -tag -width Ds +.It Fl Wno-mbc +Warn when there are inconsistencies with or caveats about the specified MBC type. +.It Fl Wno-overwrite +Warn when overwriting different non-zero bytes in the header. +.It Fl Wno-sgb +Warn when the SGB flag +.Pq Fl s +conflicts with the old licensee code +.Pq Fl l . +.It Fl Wno-truncation +Warn when truncating values to fit the available space. .El .Sh EXAMPLES Most values in the ROM header do not matter to the actual console, and most are seldom useful anyway. @@ -228,7 +302,7 @@ Therefore, .Nm will ignore the .Ql RAM -feature on a TPP1 mapper with a warning. +feature on a TPP1 mapper. .Ss Special considerations TPP1 overwrites the byte at .Ad 0x14A , diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e39fa860..93ae1779 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -72,6 +72,8 @@ set(rgblink_src set(rgbfix_src "fix/main.cpp" + "fix/mbc.cpp" + "fix/warning.cpp" ) set(rgbgfx_src diff --git a/src/fix/main.cpp b/src/fix/main.cpp index a5be7224..2a010f28 100644 --- a/src/fix/main.cpp +++ b/src/fix/main.cpp @@ -18,13 +18,13 @@ #include "platform.hpp" #include "version.hpp" -static constexpr uint16_t UNSPECIFIED = 0x200; -static_assert(UNSPECIFIED > 0xFF, "UNSPECIFIED should not be in byte range!"); +#include "fix/mbc.hpp" +#include "fix/warning.hpp" static constexpr off_t BANK_SIZE = 0x4000; // Short options -static char const *optstring = "Ccf:hi:jk:L:l:m:n:Oo:p:r:st:Vv"; +static char const *optstring = "Ccf:hi:jk:L:l:m:n:Oo:p:r:st:VvW:w"; // Equivalent long options // Please keep in the same order as short opts. @@ -53,16 +53,17 @@ static option const longopts[] = { {"title", required_argument, nullptr, 't'}, {"version", no_argument, nullptr, 'V'}, {"validate", no_argument, nullptr, 'v'}, + {"warning", required_argument, nullptr, 'W'}, {nullptr, no_argument, nullptr, 0 } }; // LCOV_EXCL_START static void printUsage() { fputs( - "Usage: rgbfix [-hjOsVv] [-C | -c] [-f ] [-i ] [-k ]\n" + "Usage: rgbfix [-hjOsVvw] [-C | -c] [-f ] [-i ] [-k ]\n" " [-L ] [-l ] [-m ]\n" " [-n ] [-p ] [-r ] [-t ]\n" - " ...\n" + " [-W warning] ...\n" "Useful options:\n" " -m, --mbc-type set the MBC type byte to this value; refer\n" " to the man page for a list of values\n" @@ -78,24 +79,8 @@ static void printUsage() { } // LCOV_EXCL_STOP -static uint32_t nbErrors; - -[[gnu::format(printf, 1, 2)]] -static void error(char const *fmt, ...) { - va_list ap; - fputs("error: ", stderr); - va_start(ap, fmt); - vfprintf(stderr, fmt, ap); - va_end(ap); - putc('\n', stderr); - - if (nbErrors != UINT32_MAX) { - ++nbErrors; - } -} - -[[gnu::format(printf, 1, 2)]] -static void fatal(char const *fmt, ...) { +[[gnu::format(printf, 1, 2), noreturn]] +static void fatalWithUsage(char const *fmt, ...) { va_list ap; fputs("FATAL: ", stderr); va_start(ap, fmt); @@ -103,680 +88,12 @@ static void fatal(char const *fmt, ...) { va_end(ap); putc('\n', stderr); - if (nbErrors != UINT32_MAX) { - ++nbErrors; - } -} - -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, - - // "Extended" values (still valid, but not directly actionable) - - // A high byte of 0x01 means TPP1, the low byte is the requested features - // This does not include SRAM, which is instead implied by a non-zero SRAM size - // Note: Multiple rumble speeds imply rumble - TPP1 = 0x100, - TPP1_RUMBLE = 0x101, - TPP1_MULTIRUMBLE = 0x102, // Should not be possible - TPP1_MULTIRUMBLE_RUMBLE = 0x103, - TPP1_TIMER = 0x104, - TPP1_TIMER_RUMBLE = 0x105, - TPP1_TIMER_MULTIRUMBLE = 0x106, // Should not be possible - TPP1_TIMER_MULTIRUMBLE_RUMBLE = 0x107, - TPP1_BATTERY = 0x108, - TPP1_BATTERY_RUMBLE = 0x109, - TPP1_BATTERY_MULTIRUMBLE = 0x10A, // Should not be possible - TPP1_BATTERY_MULTIRUMBLE_RUMBLE = 0x10B, - TPP1_BATTERY_TIMER = 0x10C, - TPP1_BATTERY_TIMER_RUMBLE = 0x10D, - TPP1_BATTERY_TIMER_MULTIRUMBLE = 0x10E, // Should not be possible - TPP1_BATTERY_TIMER_MULTIRUMBLE_RUMBLE = 0x10F, - - // 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 - MBC_BAD_TPP1, // Invalid TPP1 major or minor revision numbers -}; - -static void printAcceptedMBCNames() { - fputs("Accepted MBC names:\n", stderr); - fputs("\tROM ($00) [aka ROM_ONLY]\n", stderr); - fputs("\tMBC1 ($01), MBC1+RAM ($02), MBC1+RAM+BATTERY ($03)\n", stderr); - fputs("\tMBC2 ($05), MBC2+BATTERY ($06)\n", stderr); - fputs("\tROM+RAM ($08) [deprecated], ROM+RAM+BATTERY ($09) [deprecated]\n", stderr); - fputs("\tMMM01 ($0B), MMM01+RAM ($0C), MMM01+RAM+BATTERY ($0D)\n", stderr); - fputs("\tMBC3+TIMER+BATTERY ($0F), MBC3+TIMER+RAM+BATTERY ($10)\n", stderr); - fputs("\tMBC3 ($11), MBC3+RAM ($12), MBC3+RAM+BATTERY ($13)\n", stderr); - fputs("\tMBC5 ($19), MBC5+RAM ($1A), MBC5+RAM+BATTERY ($1B)\n", stderr); - fputs("\tMBC5+RUMBLE ($1C), MBC5+RUMBLE+RAM ($1D), MBC5+RUMBLE+RAM+BATTERY ($1E)\n", stderr); - fputs("\tMBC6 ($20)\n", stderr); - fputs("\tMBC7+SENSOR+RUMBLE+RAM+BATTERY ($22)\n", stderr); - fputs("\tPOCKET_CAMERA ($FC)\n", stderr); - fputs("\tBANDAI_TAMA5 ($FD) [aka TAMA5]\n", stderr); - fputs("\tHUC3 ($FE)\n", stderr); - fputs("\tHUC1+RAM+BATTERY ($FF)\n", stderr); - - fputs("\n\tTPP1_1.0, TPP1_1.0+RUMBLE, TPP1_1.0+MULTIRUMBLE, TPP1_1.0+TIMER,\n", stderr); - fputs("\tTPP1_1.0+TIMER+RUMBLE, TPP1_1.0+TIMER+MULTIRUMBLE, TPP1_1.0+BATTERY,\n", stderr); - fputs("\tTPP1_1.0+BATTERY+RUMBLE, TPP1_1.0+BATTERY+MULTIRUMBLE,\n", stderr); - fputs("\tTPP1_1.0+BATTERY+TIMER, TPP1_1.0+BATTERY+TIMER+RUMBLE,\n", stderr); - fputs("\tTPP1_1.0+BATTERY+TIMER+MULTIRUMBLE\n", stderr); + printUsage(); + exit(1); } static uint8_t tpp1Rev[2]; -static void skipWhitespace(char const *&ptr) { - while (*ptr == ' ' || *ptr == '\t') { - ++ptr; - } -} - -static void skipMBCSpace(char const *&ptr) { - while (*ptr == ' ' || *ptr == '\t' || *ptr == '_') { - ++ptr; - } -} - -static bool readMBCSlice(char const *&name, char const *expected) { - 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 true; -} - -static MbcType parseMBC(char const *name) { - if (!strcasecmp(name, "help")) { - printAcceptedMBCNames(); - exit(0); - } - - if ((name[0] >= '0' && name[0] <= '9') || name[0] == '$') { - int base = 0; - - if (name[0] == '$') { - ++name; - base = 16; - } - // Parse number, and return it as-is (unless it's too large) - char *endptr; - unsigned long mbc = strtoul(name, &endptr, base); - - if (*endptr) { - return MBC_BAD; - } - if (mbc > 0xFF) { - return MBC_BAD_RANGE; - } - return static_cast(mbc); - } - - // Begin by reading the MBC type: - uint16_t mbc; - char const *ptr = name; - - skipWhitespace(ptr); // Trim off leading whitespace - -#define tryReadSlice(expected) \ - do { \ - if (!readMBCSlice(ptr, expected)) { \ - return MBC_BAD; \ - } \ - } while (0) - - switch (*ptr++) { - case 'R': // ROM / ROM_ONLY - case 'r': - tryReadSlice("OM"); - // Handle optional " ONLY" - skipMBCSpace(ptr); - if (*ptr == 'O' || *ptr == 'o') { - ++ptr; - tryReadSlice("NLY"); - } - mbc = ROM; - break; - - 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 'P': // POCKET_CAMERA - case 'p': - tryReadSlice("OCKET CAMERA"); - mbc = POCKET_CAMERA; - break; - - case 'B': // BANDAI_TAMA5 - case 'b': - tryReadSlice("ANDAI TAMA5"); - mbc = BANDAI_TAMA5; - break; - - case 'T': // TAMA5 / TPP1 - case 't': - switch (*ptr++) { - case 'A': - tryReadSlice("MA5"); - mbc = BANDAI_TAMA5; - break; - case 'P': { - tryReadSlice("P1"); - // Parse version - skipMBCSpace(ptr); - // Major - char *endptr; - unsigned long val = strtoul(ptr, &endptr, 10); - - if (endptr == ptr) { - error("Failed to parse TPP1 major revision number"); - return MBC_BAD_TPP1; - } - ptr = endptr; - if (val != 1) { - error("RGBFIX only supports TPP1 version 1.0"); - return MBC_BAD_TPP1; - } - tpp1Rev[0] = val; - tryReadSlice("."); - // Minor - val = strtoul(ptr, &endptr, 10); - if (endptr == ptr) { - error("Failed to parse TPP1 minor revision number"); - return MBC_BAD_TPP1; - } - ptr = endptr; - if (val > 0xFF) { - error("TPP1 minor revision number must be 8-bit"); - return MBC_BAD_TPP1; - } - tpp1Rev[1] = val; - mbc = TPP1; - break; - } - default: - return MBC_BAD; - } - break; - - 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; - - default: - return MBC_BAD; - } - - // Read "additional features" - uint8_t features = 0; - // clang-format off: vertically align values - static constexpr uint8_t RAM = 1 << 7; - static constexpr uint8_t BATTERY = 1 << 6; - static constexpr uint8_t TIMER = 1 << 5; - static constexpr uint8_t RUMBLE = 1 << 4; - static constexpr uint8_t SENSOR = 1 << 3; - static constexpr uint8_t MULTIRUMBLE = 1 << 2; - // clang-format on - - for (;;) { - skipWhitespace(ptr); // Trim off trailing whitespace - - // If done, start processing "features" - if (!*ptr) { - break; - } - // We expect a '+' at this point - skipMBCSpace(ptr); - if (*ptr++ != '+') { - return MBC_BAD; - } - skipMBCSpace(ptr); - - switch (*ptr++) { - case 'B': // BATTERY - case 'b': - tryReadSlice("ATTERY"); - features |= BATTERY; - break; - - case 'M': - case 'm': - tryReadSlice("ULTIRUMBLE"); - features |= MULTIRUMBLE; - break; - - case 'R': // RAM or RUMBLE - case 'r': - switch (*ptr++) { - case 'U': - case 'u': - tryReadSlice("MBLE"); - features |= RUMBLE; - break; - case 'A': - case 'a': - tryReadSlice("M"); - features |= RAM; - break; - default: - return MBC_BAD; - } - break; - - case 'S': // SENSOR - case 's': - tryReadSlice("ENSOR"); - features |= SENSOR; - break; - - case 'T': // TIMER - case 't': - tryReadSlice("IMER"); - features |= TIMER; - break; - - 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 MBC2: - if (features == BATTERY) { - mbc = MBC2_BATTERY; - } else if (features) { - return MBC_WRONG_FEATURES; - } - break; - - case MBC3: - // Handle timer, which also requires battery - if (features & TIMER) { - if (!(features & BATTERY)) { - warnx("MBC3+TIMER implies 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 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; - - case MBC6: - case POCKET_CAMERA: - case BANDAI_TAMA5: - case HUC3: - // No extra features accepted - if (features) { - return MBC_WRONG_FEATURES; - } - break; - - 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 TPP1: - if (features & RAM) { - warnx("TPP1 requests RAM implicitly if given a non-zero RAM size"); - } - if (features & BATTERY) { - mbc |= 0x08; - } - if (features & TIMER) { - mbc |= 0x04; - } - if (features & MULTIRUMBLE) { - mbc |= 0x03; // Also set the rumble flag - } - if (features & RUMBLE) { - mbc |= 0x01; - } - if (features & SENSOR) { - return MBC_WRONG_FEATURES; - } - break; - } - - skipWhitespace(ptr); // Trim off trailing whitespace - - // If there is still something past the whitespace, error out - if (*ptr) { - return MBC_BAD; - } - - return static_cast(mbc); -} - -static char const *mbcName(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"; - case TPP1: - return "TPP1"; - case TPP1_RUMBLE: - return "TPP1+RUMBLE"; - case TPP1_MULTIRUMBLE: - case TPP1_MULTIRUMBLE_RUMBLE: - return "TPP1+MULTIRUMBLE"; - case TPP1_TIMER: - return "TPP1+TIMER"; - case TPP1_TIMER_RUMBLE: - return "TPP1+TIMER+RUMBLE"; - case TPP1_TIMER_MULTIRUMBLE: - case TPP1_TIMER_MULTIRUMBLE_RUMBLE: - return "TPP1+TIMER+MULTIRUMBLE"; - case TPP1_BATTERY: - return "TPP1+BATTERY"; - case TPP1_BATTERY_RUMBLE: - return "TPP1+BATTERY+RUMBLE"; - case TPP1_BATTERY_MULTIRUMBLE: - case TPP1_BATTERY_MULTIRUMBLE_RUMBLE: - return "TPP1+BATTERY+MULTIRUMBLE"; - case TPP1_BATTERY_TIMER: - return "TPP1+BATTERY+TIMER"; - case TPP1_BATTERY_TIMER_RUMBLE: - return "TPP1+BATTERY+TIMER+RUMBLE"; - case TPP1_BATTERY_TIMER_MULTIRUMBLE: - case TPP1_BATTERY_TIMER_MULTIRUMBLE_RUMBLE: - return "TPP1+BATTERY+TIMER+MULTIRUMBLE"; - - // Error values - case MBC_NONE: - case MBC_BAD: - case MBC_WRONG_FEATURES: - case MBC_BAD_RANGE: - case MBC_BAD_TPP1: - // LCOV_EXCL_START - unreachable_(); - } - - unreachable_(); - // LCOV_EXCL_STOP -} - -static bool hasRAM(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 BANDAI_TAMA5: // "Game de Hakken!! Tamagotchi - Osutchi to Mesutchi" has RAM size 0 - case MBC_NONE: - case MBC_BAD: - case MBC_WRONG_FEATURES: - case MBC_BAD_RANGE: - case MBC_BAD_TPP1: - return false; - - 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 MBC6: // "Net de Get - Minigame @ 100" has RAM size 3 (32 KiB) - case MBC7_SENSOR_RUMBLE_RAM_BATTERY: - case POCKET_CAMERA: - case HUC3: - case HUC1_RAM_BATTERY: - return true; - - // TPP1 may or may not have RAM, don't call this function for it - case TPP1: - case TPP1_RUMBLE: - case TPP1_MULTIRUMBLE: - case TPP1_MULTIRUMBLE_RUMBLE: - case TPP1_TIMER: - case TPP1_TIMER_RUMBLE: - case TPP1_TIMER_MULTIRUMBLE: - case TPP1_TIMER_MULTIRUMBLE_RUMBLE: - case TPP1_BATTERY: - case TPP1_BATTERY_RUMBLE: - case TPP1_BATTERY_MULTIRUMBLE: - case TPP1_BATTERY_MULTIRUMBLE_RUMBLE: - case TPP1_BATTERY_TIMER: - case TPP1_BATTERY_TIMER_RUMBLE: - case TPP1_BATTERY_TIMER_MULTIRUMBLE: - case TPP1_BATTERY_TIMER_MULTIRUMBLE_RUMBLE: - break; - } - - unreachable_(); // LCOV_EXCL_LINE -} - 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, @@ -804,7 +121,6 @@ static uint8_t newLicenseeLen; static uint16_t oldLicensee = UNSPECIFIED; static MbcType cartridgeType = MBC_NONE; static uint16_t romVersion = UNSPECIFIED; -static bool overwriteRom = false; // If false, warn when overwriting non-zero non-identical bytes static uint16_t padValue = UNSPECIFIED; static uint16_t ramSize = UNSPECIFIED; static bool sgb = false; // If false, SGB flags are left alone @@ -870,8 +186,8 @@ static ssize_t writeBytes(int fd, uint8_t *buf, size_t len) { static void overwriteByte(uint8_t *rom0, uint16_t addr, uint8_t fixedByte, char const *areaName) { uint8_t origByte = rom0[addr]; - if (!overwriteRom && origByte != 0 && origByte != fixedByte) { - warnx("Overwrote a non-zero byte in the %s", areaName); + if (origByte != 0 && origByte != fixedByte) { + warning(WARNING_OVERWRITE, "Overwrote a non-zero byte in the %s", areaName); } rom0[addr] = fixedByte; @@ -880,14 +196,12 @@ static void overwriteByte(uint8_t *rom0, uint16_t addr, uint8_t fixedByte, char static void overwriteBytes( uint8_t *rom0, uint16_t startAddr, uint8_t const *fixed, uint8_t size, char const *areaName ) { - if (!overwriteRom) { - for (uint8_t i = 0; i < size; ++i) { - uint8_t origByte = rom0[i + startAddr]; + for (uint8_t i = 0; i < size; ++i) { + uint8_t origByte = rom0[i + startAddr]; - if (origByte != 0 && origByte != fixed[i]) { - warnx("Overwrote a non-zero byte in the %s", areaName); - break; - } + if (origByte != 0 && origByte != fixed[i]) { + warning(WARNING_OVERWRITE, "Overwrote a non-zero byte in the %s", areaName); + break; } } @@ -909,11 +223,11 @@ static void if (rom0Len == -1) { // LCOV_EXCL_START - fatal("Failed to read \"%s\"'s header: %s", name, strerror(errno)); + error("Failed to read \"%s\"'s header: %s", name, strerror(errno)); return; // LCOV_EXCL_STOP } else if (rom0Len < headerSize) { - fatal( + error( "\"%s\" too short, expected at least %jd ($%jx) bytes, got only %jd", name, static_cast(headerSize), @@ -997,7 +311,11 @@ static void if (oldLicensee != UNSPECIFIED) { overwriteByte(rom0, 0x14B, oldLicensee, "old licensee code"); } else if (sgb && rom0[0x14B] != 0x33) { - warnx("SGB compatibility enabled, but old licensee was 0x%02x, not 0x33", rom0[0x14B]); + warning( + WARNING_SGB, + "SGB compatibility enabled, but old licensee was 0x%02x, not 0x33", + rom0[0x14B] + ); } if (romVersion != UNSPECIFIED) { @@ -1022,7 +340,7 @@ static void // Handle ROMX if (input == output) { if (fileSize >= 0x10000 * BANK_SIZE) { - fatal("\"%s\" has more than 65536 banks", name); + error("\"%s\" has more than 65536 banks", name); return; } // This should be guaranteed from the size cap... @@ -1043,7 +361,7 @@ static void 0x10000 * BANK_SIZE <= SSIZE_MAX, "Max input file size too large for OS" ); if (nbBanks == 0x10000) { - fatal("\"%s\" has more than 65536 banks", name); + error("\"%s\" has more than 65536 banks", name); return; } ++nbBanks; @@ -1145,7 +463,7 @@ static void if (input == output) { if (lseek(output, 0, SEEK_SET) == static_cast(-1)) { // LCOV_EXCL_START - fatal("Failed to rewind \"%s\": %s", name, strerror(errno)); + error("Failed to rewind \"%s\": %s", name, strerror(errno)); return; // LCOV_EXCL_STOP } @@ -1159,12 +477,12 @@ static void if (writeLen == -1) { // LCOV_EXCL_START - fatal("Failed to write \"%s\"'s ROM0: %s", name, strerror(errno)); + error("Failed to write \"%s\"'s ROM0: %s", name, strerror(errno)); return; // LCOV_EXCL_STOP } else if (writeLen < rom0Len) { // LCOV_EXCL_START - fatal( + error( "Could only write %jd of \"%s\"'s %jd ROM0 bytes", static_cast(writeLen), name, @@ -1181,12 +499,12 @@ static void writeLen = writeBytes(output, romx.data(), totalRomxLen); if (writeLen == -1) { // LCOV_EXCL_START - fatal("Failed to write \"%s\"'s ROMX: %s", name, strerror(errno)); + error("Failed to write \"%s\"'s ROMX: %s", name, strerror(errno)); return; // LCOV_EXCL_STOP } else if (static_cast(writeLen) < totalRomxLen) { // LCOV_EXCL_START - fatal( + error( "Could only write %jd of \"%s\"'s %zu ROMX bytes", static_cast(writeLen), name, @@ -1202,7 +520,7 @@ static void if (input == output) { if (lseek(output, 0, SEEK_END) == static_cast(-1)) { // LCOV_EXCL_START - fatal("Failed to seek to end of \"%s\": %s", name, strerror(errno)); + error("Failed to seek to end of \"%s\": %s", name, strerror(errno)); return; // LCOV_EXCL_STOP } @@ -1219,7 +537,7 @@ static void // so it's fine to cast to `size_t` if (static_cast(ret) != thisLen) { // LCOV_EXCL_START - fatal("Failed to write \"%s\"'s padding: %s", name, strerror(errno)); + error("Failed to write \"%s\"'s padding: %s", name, strerror(errno)); break; // LCOV_EXCL_STOP } @@ -1229,7 +547,7 @@ static void } static bool processFilename(char const *name, char const *outputName) { - nbErrors = 0; + resetErrors(); bool inputStdin = !strcmp(name, "-"); if (inputStdin && !outputName) { @@ -1245,7 +563,7 @@ static bool processFilename(char const *name, char const *outputName) { } else { output = open(outputName, O_WRONLY | O_BINARY | O_CREAT, 0600); if (output == -1) { - fatal("Failed to open \"%s\" for writing: %s", outputName, strerror(errno)); + error("Failed to open \"%s\" for writing: %s", outputName, strerror(errno)); return true; } openedOutput = true; @@ -1261,79 +579,112 @@ static bool processFilename(char const *name, char const *outputName) { name = ""; (void)setmode(STDIN_FILENO, O_BINARY); processFile(STDIN_FILENO, output, name, 0, false); - } else { + } 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 IO + // 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. - if (int input = open(name, (outputName ? O_RDONLY : O_RDWR) | O_BINARY); input == -1) { - fatal("Failed to open \"%s\" for reading+writing: %s", name, strerror(errno)); + error("Failed to open \"%s\" for reading+writing: %s", name, strerror(errno)); + } else { + Defer closeInput{[&] { close(input); }}; + struct stat stat; + if (fstat(input, &stat) == -1) { + error("Failed to stat \"%s\": %s", name, strerror(errno)); // LCOV_EXCL_LINE + } else if (!S_ISREG(stat.st_mode)) { // We do not support FIFOs or symlinks + // LCOV_EXCL_START + error("\"%s\" is not a regular file, and thus cannot be modified in-place", name); + // LCOV_EXCL_STOP + } else if (stat.st_size < 0x150) { + // This check is in theory redundant with the one in `processFile`, but it + // prevents passing a file size of 0, which usually indicates pipes + error( + "\"%s\" too short, expected at least 336 ($150) bytes, got only %jd", + name, + static_cast(stat.st_size) + ); } else { - Defer closeInput{[&] { close(input); }}; - struct stat stat; - if (fstat(input, &stat) == -1) { - // LCOV_EXCL_START - fatal("Failed to stat \"%s\": %s", name, strerror(errno)); - // LCOV_EXCL_STOP - } else if (!S_ISREG(stat.st_mode)) { // We do not support FIFOs or symlinks - // LCOV_EXCL_START - fatal("\"%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 - fatal( - "\"%s\" too short, expected at least 336 ($150) bytes, got only %jd", - name, - static_cast(stat.st_size) - ); - } else { - if (!outputName) { - output = input; - } - processFile(input, output, name, stat.st_size, true); + if (!outputName) { + output = input; } + processFile(input, output, name, stat.st_size, true); } } - if (nbErrors) { - fprintf( - stderr, - "Fixing \"%s\" failed with %u error%s\n", - name, - nbErrors, - nbErrors == 1 ? "" : "s" - ); - } - return nbErrors; + return checkErrors(name); } static void parseByte(uint16_t &output, char name) { if (musl_optarg[0] == 0) { - error("Argument to option '%c' may not be empty", name); - } else { - char *endptr; - unsigned long value; + fatal("Argument to option '%c' may not be empty", name); + } - if (musl_optarg[0] == '$') { - value = strtoul(&musl_optarg[1], &endptr, 16); + char *endptr; + unsigned long value; + if (musl_optarg[0] == '$') { + value = strtoul(&musl_optarg[1], &endptr, 16); + } else { + value = strtoul(musl_optarg, &endptr, 0); + } + + if (*endptr) { + fatal("Expected number as argument to option '%c', got %s", name, musl_optarg); + } else if (value > 0xFF) { + fatal("Argument to option '%c' is larger than 255: %lu", name, value); + } + + output = value; +} + +static void initLogo() { + if (logoFilename) { + FILE *logoFile; + if (strcmp(logoFilename, "-")) { + logoFile = fopen(logoFilename, "rb"); } else { - value = strtoul(musl_optarg, &endptr, 0); + logoFilename = ""; + (void)setmode(STDIN_FILENO, O_BINARY); + logoFile = stdin; } - if (*endptr) { - error("Expected number as argument to option '%c', got %s", name, musl_optarg); - } else if (value > 0xFF) { - error("Argument to option '%c' is larger than 255: %lu", name, value); - } else { - output = value; + if (!logoFile) { + fatal("Failed to open \"%s\" for reading: %s", logoFilename, strerror(errno)); + } + Defer closeLogo{[&] { fclose(logoFile); }}; + + uint8_t logoBpp[sizeof(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)); + } + auto highs = [&logoBpp](size_t i) { + return (logoBpp[i * 2] & 0xF0) | ((logoBpp[i * 2 + 1] & 0xF0) >> 4); + }; + auto lows = [&logoBpp](size_t i) { + return ((logoBpp[i * 2] & 0x0F) << 4) | (logoBpp[i * 2 + 1] & 0x0F); + }; + constexpr size_t mid = sizeof(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); + } + } else { + memcpy(logo, nintendoLogo, sizeof(nintendoLogo)); + } + + if (fixSpec & TRASH_LOGO) { + for (uint16_t i = 0; i < sizeof(logo); ++i) { + logo[i] = 0xFF ^ logo[i]; } } } int main(int argc, char *argv[]) { - nbErrors = 0; - char const *outputFilename = nullptr; for (int ch; (ch = musl_getopt_long_only(argc, argv, optstring, longopts, nullptr)) != -1;) { switch (ch) { @@ -1344,7 +695,7 @@ int main(int argc, char *argv[]) { model = ch == 'c' ? BOTH : CGB; if (titleLen > 15) { titleLen = 15; - warnx("Truncating title \"%s\" to 15 chars", title); + warning(WARNING_TRUNCATION, "Truncating title \"%s\" to 15 chars", title); } break; @@ -1369,7 +720,7 @@ int main(int argc, char *argv[]) { #undef overrideSpecs default: - warnx("Ignoring '%c' in fix spec", *musl_optarg); + fatal("Invalid character '%c' in fix spec", *musl_optarg); } ++musl_optarg; } @@ -1386,12 +737,12 @@ int main(int argc, char *argv[]) { len = strlen(gameID); if (len > 4) { len = 4; - warnx("Truncating game ID \"%s\" to 4 chars", gameID); + warning(WARNING_TRUNCATION, "Truncating game ID \"%s\" to 4 chars", gameID); } gameIDLen = len; if (titleLen > 11) { titleLen = 11; - warnx("Truncating title \"%s\" to 11 chars", title); + warning(WARNING_TRUNCATION, "Truncating title \"%s\" to 11 chars", title); } break; @@ -1404,7 +755,9 @@ int main(int argc, char *argv[]) { len = strlen(newLicensee); if (len > 2) { len = 2; - warnx("Truncating new licensee \"%s\" to 2 chars", newLicensee); + warning( + WARNING_TRUNCATION, "Truncating new licensee \"%s\" to 2 chars", newLicensee + ); } newLicenseeLen = len; break; @@ -1418,17 +771,11 @@ int main(int argc, char *argv[]) { break; case 'm': - cartridgeType = parseMBC(musl_optarg); - if (cartridgeType == MBC_BAD) { - error("Unknown MBC \"%s\"", musl_optarg); - printAcceptedMBCNames(); - } else if (cartridgeType == MBC_WRONG_FEATURES) { - error("Features incompatible with MBC (\"%s\")", musl_optarg); - printAcceptedMBCNames(); - } else if (cartridgeType == MBC_BAD_RANGE) { - error("Specified MBC ID out of range 0-255: %s", musl_optarg); - } else if (cartridgeType == ROM_RAM || cartridgeType == ROM_RAM_BATTERY) { - warnx("MBC \"%s\" is under-specified and poorly supported", musl_optarg); + cartridgeType = mbc_ParseName(musl_optarg, tpp1Rev[0], tpp1Rev[1]); + if (cartridgeType == ROM_RAM || cartridgeType == ROM_RAM_BATTERY) { + warning( + WARNING_MBC, "MBC \"%s\" is under-specified and poorly supported", musl_optarg + ); } break; @@ -1437,7 +784,7 @@ int main(int argc, char *argv[]) { break; case 'O': - overwriteRom = true; + warnings.processWarningFlag("no-overwrite"); break; case 'o': @@ -1463,7 +810,7 @@ int main(int argc, char *argv[]) { if (len > maxLen) { len = maxLen; - warnx("Truncating title \"%s\" to %u chars", title, maxLen); + warning(WARNING_TRUNCATION, "Truncating title \"%s\" to %u chars", title, maxLen); } titleLen = len; break; @@ -1479,6 +826,14 @@ int main(int argc, char *argv[]) { fixSpec = FIX_LOGO | FIX_HEADER_SUM | FIX_GLOBAL_SUM; break; + case 'W': + warnings.processWarningFlag(musl_optarg); + break; + + case 'w': + warnings.state.warningsEnabled = false; + break; + default: // LCOV_EXCL_START printUsage(); @@ -1488,93 +843,65 @@ int main(int argc, char *argv[]) { } if ((cartridgeType & 0xFF00) == TPP1 && !japanese) { - warnx("TPP1 overwrites region flag for its identification code, ignoring `-j`"); + 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) { - warnx("MBC \"%s\" should have 2 KiB of RAM (-r 1)", mbcName(cartridgeType)); + warning( + WARNING_MBC, + "MBC \"%s\" should have 2 KiB of RAM (-r 1)", + mbc_Name(cartridgeType) + ); } - } else if (hasRAM(cartridgeType)) { + } else if (mbc_HasRAM(cartridgeType)) { if (!ramSize) { - warnx("MBC \"%s\" has RAM, but RAM size was set to 0", mbcName(cartridgeType)); + warning( + WARNING_MBC, + "MBC \"%s\" has RAM, but RAM size was set to 0", + mbc_Name(cartridgeType) + ); } else if (ramSize == 1) { - warnx("RAM size 1 (2 KiB) was specified for MBC \"%s\"", mbcName(cartridgeType)); + warning( + WARNING_MBC, + "RAM size 1 (2 KiB) was specified for MBC \"%s\"", + mbc_Name(cartridgeType) + ); } } else if (ramSize) { - warnx( - "MBC \"%s\" has no RAM, but RAM size was set to %u", mbcName(cartridgeType), ramSize + warning( + WARNING_MBC, + "MBC \"%s\" has no RAM, but RAM size was set to %u", + mbc_Name(cartridgeType), + ramSize ); } } if (sgb && oldLicensee != UNSPECIFIED && oldLicensee != 0x33) { - warnx("SGB compatibility enabled, but old licensee is 0x%02x, not 0x33", oldLicensee); + warning( + WARNING_SGB, + "SGB compatibility enabled, but old licensee is 0x%02x, not 0x33", + oldLicensee + ); } + initLogo(); + argv += musl_optind; - bool failed = nbErrors; - - if (logoFilename) { - FILE *logoFile; - if (strcmp(logoFilename, "-")) { - logoFile = fopen(logoFilename, "rb"); - } else { - logoFilename = ""; - (void)setmode(STDIN_FILENO, O_BINARY); - logoFile = stdin; - } - if (!logoFile) { - fatal("Failed to open \"%s\" for reading: %s", logoFilename, strerror(errno)); - exit(1); - } - Defer closeLogo{[&] { fclose(logoFile); }}; - uint8_t logoBpp[sizeof(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)); - exit(1); - } - auto highs = [&logoBpp](size_t i) { - return (logoBpp[i * 2] & 0xF0) | ((logoBpp[i * 2 + 1] & 0xF0) >> 4); - }; - auto lows = [&logoBpp](size_t i) { - return ((logoBpp[i * 2] & 0x0F) << 4) | (logoBpp[i * 2 + 1] & 0x0F); - }; - constexpr size_t mid = sizeof(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); - } - } else { - memcpy(logo, nintendoLogo, sizeof(nintendoLogo)); - } - if (fixSpec & TRASH_LOGO) { - for (uint16_t i = 0; i < sizeof(logo); ++i) { - logo[i] = 0xFF ^ logo[i]; - } - } - if (!*argv) { - fatal("Please specify an input file (pass `-` to read from standard input)"); - printUsage(); - exit(1); + fatalWithUsage("Please specify an input file (pass `-` to read from standard input)"); } if (outputFilename && argc != musl_optind + 1) { - fatal("If `-o` is set then only a single input file may be specified"); - printUsage(); - exit(1); + fatalWithUsage("If `-o` is set then only a single input file may be specified"); } + bool failed = false; do { failed |= processFilename(*argv, outputFilename); } while (*++argv); diff --git a/src/fix/mbc.cpp b/src/fix/mbc.cpp new file mode 100644 index 00000000..bf70510d --- /dev/null +++ b/src/fix/mbc.cpp @@ -0,0 +1,611 @@ +#include "fix/mbc.hpp" + +#include + +#include "helpers.hpp" // unreachable_ +#include "platform.hpp" // strcasecmp + +#include "fix/warning.hpp" + +[[gnu::format(printf, 1, 2), noreturn]] +static void fatalWithMBCNames(char const *fmt, ...) { + va_list ap; + fputs("FATAL: ", stderr); + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + putc('\n', stderr); + + mbc_PrintAcceptedNames(stderr); + exit(1); +} + +void mbc_PrintAcceptedNames(FILE *file) { + fputs("Accepted MBC names:\n", file); + fputs("\tROM ($00) [aka ROM_ONLY]\n", file); + fputs("\tMBC1 ($01), MBC1+RAM ($02), MBC1+RAM+BATTERY ($03)\n", file); + fputs("\tMBC2 ($05), MBC2+BATTERY ($06)\n", file); + fputs("\tROM+RAM ($08) [deprecated], ROM+RAM+BATTERY ($09) [deprecated]\n", file); + fputs("\tMMM01 ($0B), MMM01+RAM ($0C), MMM01+RAM+BATTERY ($0D)\n", file); + fputs("\tMBC3+TIMER+BATTERY ($0F), MBC3+TIMER+RAM+BATTERY ($10)\n", file); + fputs("\tMBC3 ($11), MBC3+RAM ($12), MBC3+RAM+BATTERY ($13)\n", file); + fputs("\tMBC5 ($19), MBC5+RAM ($1A), MBC5+RAM+BATTERY ($1B)\n", file); + fputs("\tMBC5+RUMBLE ($1C), MBC5+RUMBLE+RAM ($1D), MBC5+RUMBLE+RAM+BATTERY ($1E)\n", file); + fputs("\tMBC6 ($20)\n", file); + fputs("\tMBC7+SENSOR+RUMBLE+RAM+BATTERY ($22)\n", file); + fputs("\tPOCKET_CAMERA ($FC)\n", file); + fputs("\tBANDAI_TAMA5 ($FD) [aka TAMA5]\n", file); + fputs("\tHUC3 ($FE)\n", file); + fputs("\tHUC1+RAM+BATTERY ($FF)\n", file); + + fputs("\n\tTPP1_1.0, TPP1_1.0+RUMBLE, TPP1_1.0+MULTIRUMBLE, TPP1_1.0+TIMER,\n", file); + fputs("\tTPP1_1.0+TIMER+RUMBLE, TPP1_1.0+TIMER+MULTIRUMBLE, TPP1_1.0+BATTERY,\n", file); + fputs("\tTPP1_1.0+BATTERY+RUMBLE, TPP1_1.0+BATTERY+MULTIRUMBLE,\n", file); + fputs("\tTPP1_1.0+BATTERY+TIMER, TPP1_1.0+BATTERY+TIMER+RUMBLE,\n", file); + fputs("\tTPP1_1.0+BATTERY+TIMER+MULTIRUMBLE\n", file); +} + +bool mbc_HasRAM(MbcType type) { + switch (type) { + 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 MBC6: // "Net de Get - Minigame @ 100" has RAM size 3 (32 KiB) + case MBC7_SENSOR_RUMBLE_RAM_BATTERY: + case POCKET_CAMERA: + case HUC3: + case HUC1_RAM_BATTERY: + return true; + + 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 BANDAI_TAMA5: // "Game de Hakken!! Tamagotchi - Osutchi to Mesutchi" has RAM size 0 + case MBC_NONE: + return false; + + // TPP1 may or may not have RAM, don't call this function for it + case TPP1: + case TPP1_RUMBLE: + case TPP1_MULTIRUMBLE: + case TPP1_MULTIRUMBLE_RUMBLE: + case TPP1_TIMER: + case TPP1_TIMER_RUMBLE: + case TPP1_TIMER_MULTIRUMBLE: + case TPP1_TIMER_MULTIRUMBLE_RUMBLE: + case TPP1_BATTERY: + case TPP1_BATTERY_RUMBLE: + case TPP1_BATTERY_MULTIRUMBLE: + case TPP1_BATTERY_MULTIRUMBLE_RUMBLE: + case TPP1_BATTERY_TIMER: + case TPP1_BATTERY_TIMER_RUMBLE: + case TPP1_BATTERY_TIMER_MULTIRUMBLE: + case TPP1_BATTERY_TIMER_MULTIRUMBLE_RUMBLE: + break; + } + + unreachable_(); // LCOV_EXCL_LINE +} + +char const *mbc_Name(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"; + case TPP1: + return "TPP1"; + case TPP1_RUMBLE: + return "TPP1+RUMBLE"; + case TPP1_MULTIRUMBLE: + case TPP1_MULTIRUMBLE_RUMBLE: + return "TPP1+MULTIRUMBLE"; + case TPP1_TIMER: + return "TPP1+TIMER"; + case TPP1_TIMER_RUMBLE: + return "TPP1+TIMER+RUMBLE"; + case TPP1_TIMER_MULTIRUMBLE: + case TPP1_TIMER_MULTIRUMBLE_RUMBLE: + return "TPP1+TIMER+MULTIRUMBLE"; + case TPP1_BATTERY: + return "TPP1+BATTERY"; + case TPP1_BATTERY_RUMBLE: + return "TPP1+BATTERY+RUMBLE"; + case TPP1_BATTERY_MULTIRUMBLE: + case TPP1_BATTERY_MULTIRUMBLE_RUMBLE: + return "TPP1+BATTERY+MULTIRUMBLE"; + case TPP1_BATTERY_TIMER: + return "TPP1+BATTERY+TIMER"; + case TPP1_BATTERY_TIMER_RUMBLE: + return "TPP1+BATTERY+TIMER+RUMBLE"; + case TPP1_BATTERY_TIMER_MULTIRUMBLE: + case TPP1_BATTERY_TIMER_MULTIRUMBLE_RUMBLE: + return "TPP1+BATTERY+TIMER+MULTIRUMBLE"; + case MBC_NONE: + break; + } + + unreachable_(); // LCOV_EXCL_LINE +} + +static void skipWhitespace(char const *&ptr) { + while (*ptr == ' ' || *ptr == '\t') { + ++ptr; + } +} + +static void skipMBCSpace(char const *&ptr) { + while (*ptr == ' ' || *ptr == '\t' || *ptr == '_') { + ++ptr; + } +} + +static char normalizeMBCChar(char c) { + if (c >= 'a' && c <= 'z') { // Uppercase for comparison with `mbc_Name`s + c = c - 'a' + 'A'; + } else if (c == '_') { // Treat underscores as spaces + c = ' '; + } + return c; +} + +static bool readMBCSlice(char const *&name, char const *expected) { + while (*expected) { + // If `name` is too short, the character will be '\0' and this will return `false` + if (normalizeMBCChar(*name++) != *expected++) { + return false; + } + } + return true; +} + +[[noreturn]] +static void fatalUnknownMBC(char const *fullName) { + fatalWithMBCNames("Unknown MBC \"%s\"", fullName); +} + +[[noreturn]] +static void fatalWrongMBCFeatures(char const *fullName) { + fatalWithMBCNames("Features incompatible with MBC (\"%s\")", fullName); +} + +MbcType mbc_ParseName(char const *name, uint8_t &tpp1Major, uint8_t &tpp1Minor) { + char const *fullName = name; + + if (!strcasecmp(name, "help") || !strcasecmp(name, "list")) { + mbc_PrintAcceptedNames(stdout); + exit(0); + } + + if ((name[0] >= '0' && name[0] <= '9') || name[0] == '$') { + int base = 0; + + if (name[0] == '$') { + ++name; + base = 16; + } + // Parse number, and return it as-is (unless it's too large) + char *endptr; + unsigned long mbc = strtoul(name, &endptr, base); + + if (*endptr) { + fatalUnknownMBC(fullName); + } + if (mbc > 0xFF) { + fatal("Specified MBC ID out of range 0-255: %s", fullName); + } + return static_cast(mbc); + } + + // Begin by reading the MBC type: + uint16_t mbc; + char const *ptr = name; + + skipWhitespace(ptr); // Trim off leading whitespace + +#define tryReadSlice(expected) \ + do { \ + if (!readMBCSlice(ptr, expected)) { \ + fatalUnknownMBC(fullName); \ + } \ + } while (0) + + switch (*ptr++) { + case 'R': // ROM / ROM_ONLY + case 'r': + tryReadSlice("OM"); + // Handle optional " ONLY" + skipMBCSpace(ptr); + if (*ptr == 'O' || *ptr == 'o') { + ++ptr; + tryReadSlice("NLY"); + } + mbc = ROM; + break; + + 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: + fatalUnknownMBC(fullName); + } + 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: + fatalUnknownMBC(fullName); + } + break; + case 'M': + case 'm': + tryReadSlice("M01"); + mbc = MMM01; + break; + default: + fatalUnknownMBC(fullName); + } + break; + + case 'P': // POCKET_CAMERA + case 'p': + tryReadSlice("OCKET CAMERA"); + mbc = POCKET_CAMERA; + break; + + case 'B': // BANDAI_TAMA5 + case 'b': + tryReadSlice("ANDAI TAMA5"); + mbc = BANDAI_TAMA5; + break; + + case 'T': // TAMA5 / TPP1 + case 't': + switch (*ptr++) { + case 'A': + tryReadSlice("MA5"); + mbc = BANDAI_TAMA5; + break; + case 'P': { + tryReadSlice("P1"); + // Parse version + skipMBCSpace(ptr); + // Major + char *endptr; + unsigned long val = strtoul(ptr, &endptr, 10); + + if (endptr == ptr) { + fatal("Failed to parse TPP1 major revision number"); + } + ptr = endptr; + if (val != 1) { + fatal("RGBFIX only supports TPP1 version 1.0"); + } + tpp1Major = val; + tryReadSlice("."); + // Minor + val = strtoul(ptr, &endptr, 10); + if (endptr == ptr) { + fatal("Failed to parse TPP1 minor revision number"); + } + ptr = endptr; + if (val > 0xFF) { + fatal("TPP1 minor revision number must be 8-bit"); + } + tpp1Minor = val; + mbc = TPP1; + break; + } + default: + fatalUnknownMBC(fullName); + } + break; + + case 'H': // HuC{1, 3} + case 'h': + tryReadSlice("UC"); + switch (*ptr++) { + case '1': + mbc = HUC1_RAM_BATTERY; + break; + case '3': + mbc = HUC3; + break; + default: + fatalUnknownMBC(fullName); + } + break; + + default: + fatalUnknownMBC(fullName); + } + + // Read "additional features" + uint8_t features = 0; + // clang-format off: vertically align values + static constexpr uint8_t RAM = 1 << 7; + static constexpr uint8_t BATTERY = 1 << 6; + static constexpr uint8_t TIMER = 1 << 5; + static constexpr uint8_t RUMBLE = 1 << 4; + static constexpr uint8_t SENSOR = 1 << 3; + static constexpr uint8_t MULTIRUMBLE = 1 << 2; + // clang-format on + + for (;;) { + skipWhitespace(ptr); // Trim off trailing whitespace + + // If done, start processing "features" + if (!*ptr) { + break; + } + // We expect a '+' at this point + skipMBCSpace(ptr); + if (*ptr++ != '+') { + fatalUnknownMBC(fullName); + } + skipMBCSpace(ptr); + + switch (*ptr++) { + case 'B': // BATTERY + case 'b': + tryReadSlice("ATTERY"); + features |= BATTERY; + break; + + case 'M': + case 'm': + tryReadSlice("ULTIRUMBLE"); + features |= MULTIRUMBLE; + break; + + case 'R': // RAM or RUMBLE + case 'r': + switch (*ptr++) { + case 'U': + case 'u': + tryReadSlice("MBLE"); + features |= RUMBLE; + break; + case 'A': + case 'a': + tryReadSlice("M"); + features |= RAM; + break; + default: + fatalUnknownMBC(fullName); + } + break; + + case 'S': // SENSOR + case 's': + tryReadSlice("ENSOR"); + features |= SENSOR; + break; + + case 'T': // TIMER + case 't': + tryReadSlice("IMER"); + features |= TIMER; + break; + + default: + fatalUnknownMBC(fullName); + } + } +#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) { + fatalWrongMBCFeatures(fullName); + } + break; + + case MBC2: + if (features == BATTERY) { + mbc = MBC2_BATTERY; + } else if (features) { + fatalWrongMBCFeatures(fullName); + } + break; + + case MBC3: + // Handle timer, which also requires battery + if (features & TIMER) { + if (!(features & BATTERY)) { + warning(WARNING_MBC, "MBC3+TIMER implies 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) { + fatalWrongMBCFeatures(fullName); + } + break; + + 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) { + fatalWrongMBCFeatures(fullName); + } + break; + + case MBC6: + case POCKET_CAMERA: + case BANDAI_TAMA5: + case HUC3: + // No extra features accepted + if (features) { + fatalWrongMBCFeatures(fullName); + } + break; + + case MBC7_SENSOR_RUMBLE_RAM_BATTERY: + if (features != (SENSOR | RUMBLE | RAM | BATTERY)) { + fatalWrongMBCFeatures(fullName); + } + break; + + case HUC1_RAM_BATTERY: + if (features != (RAM | BATTERY)) { // HuC1 expects RAM+BATTERY + fatalWrongMBCFeatures(fullName); + } + break; + + case TPP1: + if (features & RAM) { + warning(WARNING_MBC, "TPP1 requests RAM implicitly if given a non-zero RAM size"); + } + if (features & BATTERY) { + mbc |= 0x08; + } + if (features & TIMER) { + mbc |= 0x04; + } + if (features & MULTIRUMBLE) { + mbc |= 0x03; // Also set the rumble flag + } + if (features & RUMBLE) { + mbc |= 0x01; + } + if (features & SENSOR) { + fatalWrongMBCFeatures(fullName); + } + break; + } + + skipWhitespace(ptr); // Trim off trailing whitespace + + // If there is still something past the whitespace, error out + if (*ptr) { + fatalUnknownMBC(fullName); + } + + return static_cast(mbc); +} diff --git a/src/fix/warning.cpp b/src/fix/warning.cpp new file mode 100644 index 00000000..f60e63e2 --- /dev/null +++ b/src/fix/warning.cpp @@ -0,0 +1,93 @@ +#include "fix/warning.hpp" + +static uint32_t nbErrors; + +// clang-format off: nested initializers +Diagnostics warnings = { + .metaWarnings = { + {"all", LEVEL_ALL }, + {"everything", LEVEL_EVERYTHING}, + }, + .warningFlags = { + {"mbc", LEVEL_DEFAULT }, + {"overwrite", LEVEL_DEFAULT }, + {"sgb", LEVEL_DEFAULT }, + {"truncation", LEVEL_DEFAULT }, + }, + .paramWarnings = {}, + .state = DiagnosticsState(), +}; +// clang-format on + +void resetErrors() { + nbErrors = 0; +} + +uint32_t checkErrors(char const *filename) { + if (nbErrors > 0) { + fprintf( + stderr, + "Fixing \"%s\" failed with %u error%s\n", + filename, + nbErrors, + nbErrors == 1 ? "" : "s" + ); + } + return nbErrors; +} + +static void incrementErrors() { + if (nbErrors != UINT32_MAX) { + ++nbErrors; + } +} + +void error(char const *fmt, ...) { + va_list ap; + fputs("error: ", stderr); + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + putc('\n', stderr); + + incrementErrors(); +} + +void fatal(char const *fmt, ...) { + va_list ap; + fputs("FATAL: ", stderr); + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + putc('\n', stderr); + + exit(1); +} + +void warning(WarningID id, char const *fmt, ...) { + char const *flag = warnings.warningFlags[id].name; + va_list ap; + + switch (warnings.getWarningBehavior(id)) { + case WarningBehavior::DISABLED: + break; + + case WarningBehavior::ENABLED: + fprintf(stderr, "warning: [-W%s]\n ", flag); + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + putc('\n', stderr); + break; + + case WarningBehavior::ERROR: + fprintf(stderr, "error: [-Werror=%s]\n ", flag); + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + putc('\n', stderr); + + incrementErrors(); + break; + } +} diff --git a/test/fix/bad-fix-char.bin b/test/fix/bad-fix-char.bin deleted file mode 100644 index f5bfb2c6..00000000 --- a/test/fix/bad-fix-char.bin +++ /dev/null @@ -1,3 +0,0 @@ -‡Ýà!H0‡;<ýNË—ñãí=ÌÊÏç^{ KC}b“Q¤&3ð ñ/]¿—¹”dÙZ=ÃÀ¢bÄaËi:öeSŸ>ñ›U@¼Ay•‘¨þ©† ³s_CÓ› Úç&nWFoKÚšíX`'ñêÁ§ë·blƒ{‡›Œ¿ -‹¯­çÎ8îù†<$:VÍ"ã³™Úh™ÑD©¤Ð¶ñ­QcµïØÐ㸺Zû§T†ž©ø½¨ºVÛ¼ÝtÅ’ªÏ4ªBèÐd T“« ÄI‡ -Ô(/×»ç¿{hÝ:ƒQmÓ$*|  íVPéó.^"&é<äŒk!šé`àù2ÝÝêê &Bû lÍ7föèšZe¤yf.˜mƒýØ ¯!³ŒóN5„õd†EÁ¦qF× \ No newline at end of file diff --git a/test/fix/bad-fix-char.err b/test/fix/bad-fix-char.err index 80b1bba6..f54600fe 100644 --- a/test/fix/bad-fix-char.err +++ b/test/fix/bad-fix-char.err @@ -1,4 +1 @@ -warning: Ignoring 'm' in fix spec -warning: Ignoring 'a' in fix spec -warning: Ignoring 'o' in fix spec -warning: Overwrote a non-zero byte in the Nintendo logo +FATAL: Invalid character 'm' in fix spec diff --git a/test/fix/color.err b/test/fix/color.err index fc253168..abb49671 100644 --- a/test/fix/color.err +++ b/test/fix/color.err @@ -1 +1,2 @@ -warning: Overwrote a non-zero byte in the CGB flag +warning: [-Woverwrite] + Overwrote a non-zero byte in the CGB flag diff --git a/test/fix/compatible.err b/test/fix/compatible.err index fc253168..abb49671 100644 --- a/test/fix/compatible.err +++ b/test/fix/compatible.err @@ -1 +1,2 @@ -warning: Overwrote a non-zero byte in the CGB flag +warning: [-Woverwrite] + Overwrote a non-zero byte in the CGB flag diff --git a/test/fix/custom-logo.err b/test/fix/custom-logo.err index 345a97be..f2931373 100644 --- a/test/fix/custom-logo.err +++ b/test/fix/custom-logo.err @@ -1 +1,2 @@ -warning: Overwrote a non-zero byte in the logo +warning: [-Woverwrite] + Overwrote a non-zero byte in the logo diff --git a/test/fix/dollar-hex.err b/test/fix/dollar-hex.err index 84c93183..44fdfbcf 100644 --- a/test/fix/dollar-hex.err +++ b/test/fix/dollar-hex.err @@ -1 +1,2 @@ -warning: Overwrote a non-zero byte in the cartridge type +warning: [-Woverwrite] + Overwrote a non-zero byte in the cartridge type diff --git a/test/fix/empty.err b/test/fix/empty.err index 56582506..61d5a8ff 100644 --- a/test/fix/empty.err +++ b/test/fix/empty.err @@ -1,2 +1,2 @@ -FATAL: "" too short, expected at least 336 ($150) bytes, got only 0 +error: "" too short, expected at least 336 ($150) bytes, got only 0 Fixing "" failed with 1 error diff --git a/test/fix/fix-override.err b/test/fix/fix-override.err index 066f831f..6eb225b4 100644 --- a/test/fix/fix-override.err +++ b/test/fix/fix-override.err @@ -1,2 +1,3 @@ warning: 'l' overriding 'L' in fix spec -warning: Overwrote a non-zero byte in the Nintendo logo +warning: [-Woverwrite] + Overwrote a non-zero byte in the Nintendo logo diff --git a/test/fix/gameid-trunc.err b/test/fix/gameid-trunc.err index 5394dd8d..b5e30084 100644 --- a/test/fix/gameid-trunc.err +++ b/test/fix/gameid-trunc.err @@ -1,2 +1,4 @@ -warning: Truncating game ID "FOUR!" to 4 chars -warning: Overwrote a non-zero byte in the manufacturer code +warning: [-Wtruncation] + Truncating game ID "FOUR!" to 4 chars +warning: [-Woverwrite] + Overwrote a non-zero byte in the manufacturer code diff --git a/test/fix/gameid.err b/test/fix/gameid.err index 3129bb73..7bf2a80f 100644 --- a/test/fix/gameid.err +++ b/test/fix/gameid.err @@ -1 +1,2 @@ -warning: Overwrote a non-zero byte in the manufacturer code +warning: [-Woverwrite] + Overwrote a non-zero byte in the manufacturer code diff --git a/test/fix/global-large.err b/test/fix/global-large.err index d6e0d12d..864663dc 100644 --- a/test/fix/global-large.err +++ b/test/fix/global-large.err @@ -1 +1,2 @@ -warning: Overwrote a non-zero byte in the global checksum +warning: [-Woverwrite] + Overwrote a non-zero byte in the global checksum diff --git a/test/fix/global-larger.err b/test/fix/global-larger.err index d6e0d12d..864663dc 100644 --- a/test/fix/global-larger.err +++ b/test/fix/global-larger.err @@ -1 +1,2 @@ -warning: Overwrote a non-zero byte in the global checksum +warning: [-Woverwrite] + Overwrote a non-zero byte in the global checksum diff --git a/test/fix/global-trash.err b/test/fix/global-trash.err index d6e0d12d..864663dc 100644 --- a/test/fix/global-trash.err +++ b/test/fix/global-trash.err @@ -1 +1,2 @@ -warning: Overwrote a non-zero byte in the global checksum +warning: [-Woverwrite] + Overwrote a non-zero byte in the global checksum diff --git a/test/fix/global.err b/test/fix/global.err index d6e0d12d..864663dc 100644 --- a/test/fix/global.err +++ b/test/fix/global.err @@ -1 +1,2 @@ -warning: Overwrote a non-zero byte in the global checksum +warning: [-Woverwrite] + Overwrote a non-zero byte in the global checksum diff --git a/test/fix/header-edit.err b/test/fix/header-edit.err index c7137fc0..37fcdfb0 100644 --- a/test/fix/header-edit.err +++ b/test/fix/header-edit.err @@ -1,2 +1,4 @@ -warning: Overwrote a non-zero byte in the CGB flag -warning: Overwrote a non-zero byte in the header checksum +warning: [-Woverwrite] + Overwrote a non-zero byte in the CGB flag +warning: [-Woverwrite] + Overwrote a non-zero byte in the header checksum diff --git a/test/fix/header-trash.err b/test/fix/header-trash.err index 021584b6..87eb296e 100644 --- a/test/fix/header-trash.err +++ b/test/fix/header-trash.err @@ -1 +1,2 @@ -warning: Overwrote a non-zero byte in the header checksum +warning: [-Woverwrite] + Overwrote a non-zero byte in the header checksum diff --git a/test/fix/header.err b/test/fix/header.err index 021584b6..87eb296e 100644 --- a/test/fix/header.err +++ b/test/fix/header.err @@ -1 +1,2 @@ -warning: Overwrote a non-zero byte in the header checksum +warning: [-Woverwrite] + Overwrote a non-zero byte in the header checksum diff --git a/test/fix/incompatible-features.err b/test/fix/incompatible-features.err index dc5893fe..2496baa0 100644 --- a/test/fix/incompatible-features.err +++ b/test/fix/incompatible-features.err @@ -1,4 +1,4 @@ -error: Features incompatible with MBC ("mbc1+multirumble") +FATAL: Features incompatible with MBC ("mbc1+multirumble") Accepted MBC names: ROM ($00) [aka ROM_ONLY] MBC1 ($01), MBC1+RAM ($02), MBC1+RAM+BATTERY ($03) diff --git a/test/fix/jp.err b/test/fix/jp.err index 21b5612c..91e61485 100644 --- a/test/fix/jp.err +++ b/test/fix/jp.err @@ -1 +1,2 @@ -warning: Overwrote a non-zero byte in the destination code +warning: [-Woverwrite] + Overwrote a non-zero byte in the destination code diff --git a/test/fix/list-mbcs.err b/test/fix/list-mbcs.out similarity index 100% rename from test/fix/list-mbcs.err rename to test/fix/list-mbcs.out diff --git a/test/fix/logo-trash.err b/test/fix/logo-trash.err index b8822a17..4370b688 100644 --- a/test/fix/logo-trash.err +++ b/test/fix/logo-trash.err @@ -1 +1,2 @@ -warning: Overwrote a non-zero byte in the Nintendo logo +warning: [-Woverwrite] + Overwrote a non-zero byte in the Nintendo logo diff --git a/test/fix/logo.err b/test/fix/logo.err index b8822a17..4370b688 100644 --- a/test/fix/logo.err +++ b/test/fix/logo.err @@ -1 +1,2 @@ -warning: Overwrote a non-zero byte in the Nintendo logo +warning: [-Woverwrite] + Overwrote a non-zero byte in the Nintendo logo diff --git a/test/fix/mbc-rom-ram-battery.err b/test/fix/mbc-rom-ram-battery.err index 34bb2606..a43950c1 100644 --- a/test/fix/mbc-rom-ram-battery.err +++ b/test/fix/mbc-rom-ram-battery.err @@ -1 +1,2 @@ -warning: MBC "ROM+RAM+BATTERY" is under-specified and poorly supported +warning: [-Wmbc] + MBC "ROM+RAM+BATTERY" is under-specified and poorly supported diff --git a/test/fix/mbc-rom-ram.err b/test/fix/mbc-rom-ram.err index 22bcb1b2..d5a97246 100644 --- a/test/fix/mbc-rom-ram.err +++ b/test/fix/mbc-rom-ram.err @@ -1 +1,2 @@ -warning: MBC "ROM+RAM" is under-specified and poorly supported +warning: [-Wmbc] + MBC "ROM+RAM" is under-specified and poorly supported diff --git a/test/fix/mbc.err b/test/fix/mbc.err index 84c93183..44fdfbcf 100644 --- a/test/fix/mbc.err +++ b/test/fix/mbc.err @@ -1 +1,2 @@ -warning: Overwrote a non-zero byte in the cartridge type +warning: [-Woverwrite] + Overwrote a non-zero byte in the cartridge type diff --git a/test/fix/mbcless-ram.err b/test/fix/mbcless-ram.err index 92a31043..97355468 100644 --- a/test/fix/mbcless-ram.err +++ b/test/fix/mbcless-ram.err @@ -1,3 +1,6 @@ -warning: MBC "ROM" has no RAM, but RAM size was set to 2 -warning: Overwrote a non-zero byte in the cartridge type -warning: Overwrote a non-zero byte in the RAM size +warning: [-Wmbc] + MBC "ROM" has no RAM, but RAM size was set to 2 +warning: [-Woverwrite] + Overwrote a non-zero byte in the cartridge type +warning: [-Woverwrite] + Overwrote a non-zero byte in the RAM size diff --git a/test/fix/new-lic-trunc.err b/test/fix/new-lic-trunc.err index 44850e93..a95cb81b 100644 --- a/test/fix/new-lic-trunc.err +++ b/test/fix/new-lic-trunc.err @@ -1,2 +1,4 @@ -warning: Truncating new licensee "HOMEBREW" to 2 chars -warning: Overwrote a non-zero byte in the new licensee code +warning: [-Wtruncation] + Truncating new licensee "HOMEBREW" to 2 chars +warning: [-Woverwrite] + Overwrote a non-zero byte in the new licensee code diff --git a/test/fix/new-lic.err b/test/fix/new-lic.err index 490edc34..3eced6d6 100644 --- a/test/fix/new-lic.err +++ b/test/fix/new-lic.err @@ -1 +1,2 @@ -warning: Overwrote a non-zero byte in the new licensee code +warning: [-Woverwrite] + Overwrote a non-zero byte in the new licensee code diff --git a/test/fix/noexist.err b/test/fix/noexist.err index 833f0d15..c4940146 100644 --- a/test/fix/noexist.err +++ b/test/fix/noexist.err @@ -1,2 +1,2 @@ -FATAL: Failed to open "noexist" for reading+writing: No such file or directory +error: Failed to open "noexist" for reading+writing: No such file or directory Fixing "noexist" failed with 1 error diff --git a/test/fix/old-lic-hex.err b/test/fix/old-lic-hex.err index 6be69743..640f574a 100644 --- a/test/fix/old-lic-hex.err +++ b/test/fix/old-lic-hex.err @@ -1 +1,2 @@ -warning: Overwrote a non-zero byte in the old licensee code +warning: [-Woverwrite] + Overwrote a non-zero byte in the old licensee code diff --git a/test/fix/old-lic.err b/test/fix/old-lic.err index 6be69743..640f574a 100644 --- a/test/fix/old-lic.err +++ b/test/fix/old-lic.err @@ -1 +1,2 @@ -warning: Overwrote a non-zero byte in the old licensee code +warning: [-Woverwrite] + Overwrote a non-zero byte in the old licensee code diff --git a/test/fix/ram.err b/test/fix/ram.err index 1dcebcee..2aa4d105 100644 --- a/test/fix/ram.err +++ b/test/fix/ram.err @@ -1 +1,2 @@ -warning: Overwrote a non-zero byte in the RAM size +warning: [-Woverwrite] + Overwrote a non-zero byte in the RAM size diff --git a/test/fix/ramful-mbc-no-ram.err b/test/fix/ramful-mbc-no-ram.err index 019f604b..48823e44 100644 --- a/test/fix/ramful-mbc-no-ram.err +++ b/test/fix/ramful-mbc-no-ram.err @@ -1,3 +1,6 @@ -warning: MBC "MBC3+RAM" has RAM, but RAM size was set to 0 -warning: Overwrote a non-zero byte in the cartridge type -warning: Overwrote a non-zero byte in the RAM size +warning: [-Wmbc] + MBC "MBC3+RAM" has RAM, but RAM size was set to 0 +warning: [-Woverwrite] + Overwrote a non-zero byte in the cartridge type +warning: [-Woverwrite] + Overwrote a non-zero byte in the RAM size diff --git a/test/fix/ramful-mbc.err b/test/fix/ramful-mbc.err index dfa76d9b..e8717e74 100644 --- a/test/fix/ramful-mbc.err +++ b/test/fix/ramful-mbc.err @@ -1,2 +1,4 @@ -warning: Overwrote a non-zero byte in the cartridge type -warning: Overwrote a non-zero byte in the RAM size +warning: [-Woverwrite] + Overwrote a non-zero byte in the cartridge type +warning: [-Woverwrite] + Overwrote a non-zero byte in the RAM size diff --git a/test/fix/ramless-mbc.err b/test/fix/ramless-mbc.err index dfa76d9b..e8717e74 100644 --- a/test/fix/ramless-mbc.err +++ b/test/fix/ramless-mbc.err @@ -1,2 +1,4 @@ -warning: Overwrote a non-zero byte in the cartridge type -warning: Overwrote a non-zero byte in the RAM size +warning: [-Woverwrite] + Overwrote a non-zero byte in the cartridge type +warning: [-Woverwrite] + Overwrote a non-zero byte in the RAM size diff --git a/test/fix/sgb-licensee.err b/test/fix/sgb-licensee.err index 6075fb04..338c869a 100644 --- a/test/fix/sgb-licensee.err +++ b/test/fix/sgb-licensee.err @@ -1,3 +1,6 @@ -warning: SGB compatibility enabled, but old licensee is 0x45, not 0x33 -warning: Overwrote a non-zero byte in the SGB flag -warning: Overwrote a non-zero byte in the old licensee code +warning: [-Wsgb] + SGB compatibility enabled, but old licensee is 0x45, not 0x33 +warning: [-Woverwrite] + Overwrote a non-zero byte in the SGB flag +warning: [-Woverwrite] + Overwrote a non-zero byte in the old licensee code diff --git a/test/fix/sgb-old-licensee.err b/test/fix/sgb-old-licensee.err index 2edaa257..e471519b 100644 --- a/test/fix/sgb-old-licensee.err +++ b/test/fix/sgb-old-licensee.err @@ -1,2 +1,4 @@ -warning: Overwrote a non-zero byte in the SGB flag -warning: SGB compatibility enabled, but old licensee was 0xc5, not 0x33 +warning: [-Woverwrite] + Overwrote a non-zero byte in the SGB flag +warning: [-Wsgb] + SGB compatibility enabled, but old licensee was 0xc5, not 0x33 diff --git a/test/fix/sgb.err b/test/fix/sgb.err index 1e8cc8f4..314b763b 100644 --- a/test/fix/sgb.err +++ b/test/fix/sgb.err @@ -1 +1,2 @@ -warning: Overwrote a non-zero byte in the SGB flag +warning: [-Woverwrite] + Overwrote a non-zero byte in the SGB flag diff --git a/test/fix/test.sh b/test/fix/test.sh index bf39e95c..46e2586d 100755 --- a/test/fix/test.sh +++ b/test/fix/test.sh @@ -58,31 +58,33 @@ runTest () { fi if [[ -z "$variant" ]]; then cp "$desired_input" 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 + eval "$RGBFIX" $flags out.gb '>out.out' '2>out.err' subst=out.gb elif [[ "$variant" = ' piped' ]]; then # 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. # shellcheck disable=SC2002 - cat "$desired_input" | eval $RGBFIX "$flags" - '>out.gb' '2>out.err' + cat "$desired_input" | eval "$RGBFIX" $flags - '>out.gb' '2>out.err' subst='' elif [[ "$variant" = ' output' ]]; then cp "$desired_input" input.gb - if [[ -n "$(eval "$RGBFIX" $flags -o out.gb input.gb '2>out.err')" ]]; then - our_rc=1 - fi + eval "$RGBFIX" $flags -o out.gb input.gb '>out.out' '2>out.err' subst=input.gb fi + if [[ -r "$2/$1.out" ]]; then + desired_outname="$2/$1.out" + else + desired_outname=/dev/null + fi if [[ -r "$2/$1.err" ]]; then desired_errname="$2/$1.err" else desired_errname=/dev/null fi + sed "s/$subst//g" out.out | tryDiff "$desired_outname" - "$1.out${variant}" + (( our_rc = our_rc || $? )) sed "s/$subst//g" out.err | tryDiff "$desired_errname" - "$1.err${variant}" (( our_rc = our_rc || $? )) diff --git a/test/fix/title-color-trunc-rev.err b/test/fix/title-color-trunc-rev.err index da4c5c68..d49a95eb 100644 --- a/test/fix/title-color-trunc-rev.err +++ b/test/fix/title-color-trunc-rev.err @@ -1,3 +1,6 @@ -warning: Truncating title "0123456789ABCDEF" to 15 chars -warning: Overwrote a non-zero byte in the title -warning: Overwrote a non-zero byte in the CGB flag +warning: [-Wtruncation] + Truncating title "0123456789ABCDEF" to 15 chars +warning: [-Woverwrite] + Overwrote a non-zero byte in the title +warning: [-Woverwrite] + Overwrote a non-zero byte in the CGB flag diff --git a/test/fix/title-color-trunc.err b/test/fix/title-color-trunc.err index da4c5c68..d49a95eb 100644 --- a/test/fix/title-color-trunc.err +++ b/test/fix/title-color-trunc.err @@ -1,3 +1,6 @@ -warning: Truncating title "0123456789ABCDEF" to 15 chars -warning: Overwrote a non-zero byte in the title -warning: Overwrote a non-zero byte in the CGB flag +warning: [-Wtruncation] + Truncating title "0123456789ABCDEF" to 15 chars +warning: [-Woverwrite] + Overwrote a non-zero byte in the title +warning: [-Woverwrite] + Overwrote a non-zero byte in the CGB flag diff --git a/test/fix/title-compat-trunc-rev.err b/test/fix/title-compat-trunc-rev.err index da4c5c68..d49a95eb 100644 --- a/test/fix/title-compat-trunc-rev.err +++ b/test/fix/title-compat-trunc-rev.err @@ -1,3 +1,6 @@ -warning: Truncating title "0123456789ABCDEF" to 15 chars -warning: Overwrote a non-zero byte in the title -warning: Overwrote a non-zero byte in the CGB flag +warning: [-Wtruncation] + Truncating title "0123456789ABCDEF" to 15 chars +warning: [-Woverwrite] + Overwrote a non-zero byte in the title +warning: [-Woverwrite] + Overwrote a non-zero byte in the CGB flag diff --git a/test/fix/title-compat-trunc.err b/test/fix/title-compat-trunc.err index da4c5c68..d49a95eb 100644 --- a/test/fix/title-compat-trunc.err +++ b/test/fix/title-compat-trunc.err @@ -1,3 +1,6 @@ -warning: Truncating title "0123456789ABCDEF" to 15 chars -warning: Overwrote a non-zero byte in the title -warning: Overwrote a non-zero byte in the CGB flag +warning: [-Wtruncation] + Truncating title "0123456789ABCDEF" to 15 chars +warning: [-Woverwrite] + Overwrote a non-zero byte in the title +warning: [-Woverwrite] + Overwrote a non-zero byte in the CGB flag diff --git a/test/fix/title-gameid-trunc-rev.err b/test/fix/title-gameid-trunc-rev.err index 7c5d5237..4bc53864 100644 --- a/test/fix/title-gameid-trunc-rev.err +++ b/test/fix/title-gameid-trunc-rev.err @@ -1,3 +1,6 @@ -warning: Truncating title "0123456789ABCDEF" to 11 chars -warning: Overwrote a non-zero byte in the title -warning: Overwrote a non-zero byte in the manufacturer code +warning: [-Wtruncation] + Truncating title "0123456789ABCDEF" to 11 chars +warning: [-Woverwrite] + Overwrote a non-zero byte in the title +warning: [-Woverwrite] + Overwrote a non-zero byte in the manufacturer code diff --git a/test/fix/title-gameid-trunc.err b/test/fix/title-gameid-trunc.err index 7c5d5237..4bc53864 100644 --- a/test/fix/title-gameid-trunc.err +++ b/test/fix/title-gameid-trunc.err @@ -1,3 +1,6 @@ -warning: Truncating title "0123456789ABCDEF" to 11 chars -warning: Overwrote a non-zero byte in the title -warning: Overwrote a non-zero byte in the manufacturer code +warning: [-Wtruncation] + Truncating title "0123456789ABCDEF" to 11 chars +warning: [-Woverwrite] + Overwrote a non-zero byte in the title +warning: [-Woverwrite] + Overwrote a non-zero byte in the manufacturer code diff --git a/test/fix/title-pad.err b/test/fix/title-pad.err index c67f1407..2752acfd 100644 --- a/test/fix/title-pad.err +++ b/test/fix/title-pad.err @@ -1 +1,2 @@ -warning: Overwrote a non-zero byte in the title +warning: [-Woverwrite] + Overwrote a non-zero byte in the title diff --git a/test/fix/title-trunc.err b/test/fix/title-trunc.err index 7f4fb89c..ffbccb0e 100644 --- a/test/fix/title-trunc.err +++ b/test/fix/title-trunc.err @@ -1,2 +1,4 @@ -warning: Truncating title "0123456789ABCDEFGHIJK" to 16 chars -warning: Overwrote a non-zero byte in the title +warning: [-Wtruncation] + Truncating title "0123456789ABCDEFGHIJK" to 16 chars +warning: [-Woverwrite] + Overwrote a non-zero byte in the title diff --git a/test/fix/title.err b/test/fix/title.err index c67f1407..2752acfd 100644 --- a/test/fix/title.err +++ b/test/fix/title.err @@ -1 +1,2 @@ -warning: Overwrote a non-zero byte in the title +warning: [-Woverwrite] + Overwrote a non-zero byte in the title diff --git a/test/fix/tooshort.err b/test/fix/tooshort.err index 062743b4..d8944c14 100644 --- a/test/fix/tooshort.err +++ b/test/fix/tooshort.err @@ -1,2 +1,2 @@ -FATAL: "" too short, expected at least 336 ($150) bytes, got only 20 +error: "" too short, expected at least 336 ($150) bytes, got only 20 Fixing "" failed with 1 error diff --git a/test/fix/tpp1-bad-major.err b/test/fix/tpp1-bad-major.err index 48ca9552..632222ad 100644 --- a/test/fix/tpp1-bad-major.err +++ b/test/fix/tpp1-bad-major.err @@ -1 +1 @@ -error: Failed to parse TPP1 major revision number +FATAL: Failed to parse TPP1 major revision number diff --git a/test/fix/tpp1-bad-minor.err b/test/fix/tpp1-bad-minor.err index e9f99336..f76332ed 100644 --- a/test/fix/tpp1-bad-minor.err +++ b/test/fix/tpp1-bad-minor.err @@ -1 +1 @@ -error: Failed to parse TPP1 minor revision number +FATAL: Failed to parse TPP1 minor revision number diff --git a/test/fix/tpp1-features.err b/test/fix/tpp1-features.err index b696678c..e26c9886 100644 --- a/test/fix/tpp1-features.err +++ b/test/fix/tpp1-features.err @@ -1,5 +1,10 @@ -warning: Overwrote a non-zero byte in the cartridge type -warning: Overwrote a non-zero byte in the TPP1 identification code -warning: Overwrote a non-zero byte in the TPP1 revision number -warning: Overwrote a non-zero byte in the RAM size -warning: Overwrote a non-zero byte in the TPP1 feature flags +warning: [-Woverwrite] + Overwrote a non-zero byte in the cartridge type +warning: [-Woverwrite] + Overwrote a non-zero byte in the TPP1 identification code +warning: [-Woverwrite] + Overwrote a non-zero byte in the TPP1 revision number +warning: [-Woverwrite] + Overwrote a non-zero byte in the RAM size +warning: [-Woverwrite] + Overwrote a non-zero byte in the TPP1 feature flags diff --git a/test/fix/tpp1-nonjap.err b/test/fix/tpp1-nonjap.err index a93b3563..0da253ce 100644 --- a/test/fix/tpp1-nonjap.err +++ b/test/fix/tpp1-nonjap.err @@ -1,5 +1,10 @@ -warning: TPP1 overwrites region flag for its identification code, ignoring `-j` -warning: Overwrote a non-zero byte in the cartridge type -warning: Overwrote a non-zero byte in the TPP1 identification code -warning: Overwrote a non-zero byte in the TPP1 revision number -warning: Overwrote a non-zero byte in the TPP1 feature flags +warning: [-Wmbc] + TPP1 overwrites region flag for its identification code, ignoring `-j` +warning: [-Woverwrite] + Overwrote a non-zero byte in the cartridge type +warning: [-Woverwrite] + Overwrote a non-zero byte in the TPP1 identification code +warning: [-Woverwrite] + Overwrote a non-zero byte in the TPP1 revision number +warning: [-Woverwrite] + Overwrote a non-zero byte in the TPP1 feature flags diff --git a/test/fix/tpp1-too-short.err b/test/fix/tpp1-too-short.err index 402a6469..d544d812 100644 --- a/test/fix/tpp1-too-short.err +++ b/test/fix/tpp1-too-short.err @@ -1,2 +1,2 @@ -FATAL: "" too short, expected at least 340 ($154) bytes, got only 339 +error: "" too short, expected at least 340 ($154) bytes, got only 339 Fixing "" failed with 1 error diff --git a/test/fix/tpp1-unk-major.err b/test/fix/tpp1-unk-major.err index 6dc28b9a..69efcb21 100644 --- a/test/fix/tpp1-unk-major.err +++ b/test/fix/tpp1-unk-major.err @@ -1 +1 @@ -error: RGBFIX only supports TPP1 version 1.0 +FATAL: RGBFIX only supports TPP1 version 1.0 diff --git a/test/fix/tpp1-unk-minor.err b/test/fix/tpp1-unk-minor.err index 93c0d5a0..0276bc8e 100644 --- a/test/fix/tpp1-unk-minor.err +++ b/test/fix/tpp1-unk-minor.err @@ -1 +1 @@ -error: TPP1 minor revision number must be 8-bit +FATAL: TPP1 minor revision number must be 8-bit diff --git a/test/fix/unknown-mbc.err b/test/fix/unknown-mbc.err index 80ec19a7..43527aa9 100644 --- a/test/fix/unknown-mbc.err +++ b/test/fix/unknown-mbc.err @@ -1,4 +1,4 @@ -error: Unknown MBC "MBC1337" +FATAL: Unknown MBC "MBC1337" Accepted MBC names: ROM ($00) [aka ROM_ONLY] MBC1 ($01), MBC1+RAM ($02), MBC1+RAM+BATTERY ($03) diff --git a/test/fix/verify-pad.err b/test/fix/verify-pad.err index 9f90148f..48e7318e 100644 --- a/test/fix/verify-pad.err +++ b/test/fix/verify-pad.err @@ -1,3 +1,6 @@ -warning: Overwrote a non-zero byte in the Nintendo logo -warning: Overwrote a non-zero byte in the header checksum -warning: Overwrote a non-zero byte in the global checksum +warning: [-Woverwrite] + Overwrote a non-zero byte in the Nintendo logo +warning: [-Woverwrite] + Overwrote a non-zero byte in the header checksum +warning: [-Woverwrite] + Overwrote a non-zero byte in the global checksum diff --git a/test/fix/verify-trash.err b/test/fix/verify-trash.err index 9f90148f..48e7318e 100644 --- a/test/fix/verify-trash.err +++ b/test/fix/verify-trash.err @@ -1,3 +1,6 @@ -warning: Overwrote a non-zero byte in the Nintendo logo -warning: Overwrote a non-zero byte in the header checksum -warning: Overwrote a non-zero byte in the global checksum +warning: [-Woverwrite] + Overwrote a non-zero byte in the Nintendo logo +warning: [-Woverwrite] + Overwrote a non-zero byte in the header checksum +warning: [-Woverwrite] + Overwrote a non-zero byte in the global checksum diff --git a/test/fix/verify.err b/test/fix/verify.err index 9f90148f..48e7318e 100644 --- a/test/fix/verify.err +++ b/test/fix/verify.err @@ -1,3 +1,6 @@ -warning: Overwrote a non-zero byte in the Nintendo logo -warning: Overwrote a non-zero byte in the header checksum -warning: Overwrote a non-zero byte in the global checksum +warning: [-Woverwrite] + Overwrote a non-zero byte in the Nintendo logo +warning: [-Woverwrite] + Overwrote a non-zero byte in the header checksum +warning: [-Woverwrite] + Overwrote a non-zero byte in the global checksum diff --git a/test/fix/version.err b/test/fix/version.err index efd4d92a..1a8f2110 100644 --- a/test/fix/version.err +++ b/test/fix/version.err @@ -1 +1,2 @@ -warning: Overwrote a non-zero byte in the mask ROM version number +warning: [-Woverwrite] + Overwrote a non-zero byte in the mask ROM version number