From c3d17459c68843b8666efe904122d6c1a831d6bb Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Wed, 13 Aug 2025 13:48:38 +0200 Subject: [PATCH] Calculate and verify ROM checksum --- cartridge.go | 301 ---------------------- gb/cartridge.go | 41 ++- cartridge_test.go => gb/cartridge_test.go | 16 +- roms/failed-checksum.gb | Bin 0 -> 32768 bytes 4 files changed, 32 insertions(+), 326 deletions(-) delete mode 100644 cartridge.go rename cartridge_test.go => gb/cartridge_test.go (54%) create mode 100644 roms/failed-checksum.gb diff --git a/cartridge.go b/cartridge.go deleted file mode 100644 index 525d31d..0000000 --- a/cartridge.go +++ /dev/null @@ -1,301 +0,0 @@ -package main - -import ( - "bytes" - "encoding/binary" - "io" - "log" - "os" -) - -// See https://gbdev.io/pandocs/The_Cartridge_Header.html#0104-0133--nintendo-logo -var expectedLogo = [48]byte{ - 0xCE, 0xED, 0x66, 0x66, 0xCC, 0x0D, 0x00, 0x0B, - 0x03, 0x73, 0x00, 0x83, 0x00, 0x0C, 0x00, 0x0D, - 0x00, 0x08, 0x11, 0x1F, 0x88, 0x89, 0x00, 0x0E, - 0xDC, 0xCC, 0x6E, 0xE6, 0xDD, 0xDD, 0xD9, 0x99, - 0xBB, 0xBB, 0x67, 0x63, 0x6E, 0x0E, 0xEC, 0xCC, - 0xDD, 0xDC, 0x99, 0x9F, 0xBB, 0xB9, 0x33, 0x3E, -} - -// Cartridge types -// See https://gbdev.io/pandocs/The_Cartridge_Header.html#0147--cartridge-type -var cartridgeTypes = map[byte]string{ - 0x00: "ROM ONLY", - 0x01: "MBC1", - 0x02: "MBC1+RAM", - 0x03: "MBC1+RAM+BATTERY", - 0x05: "MBC2", - 0x06: "MBC2+BATTERY", - 0x08: "ROM+RAM 9", - 0x09: "ROM+RAM+BATTERY 9", - 0x0B: "MMM01", - 0x0C: "MMM01+RAM", - 0x0D: "MMM01+RAM+BATTERY", - 0x0F: "MBC3+TIMER+BATTERY", - 0x10: "MBC3+TIMER+RAM+BATTERY 10", - 0x11: "MBC3", - 0x12: "MBC3+RAM 10", - 0x13: "MBC3+RAM+BATTERY 10", - 0x19: "MBC5", - 0x1A: "MBC5+RAM", - 0x1B: "MBC5+RAM+BATTERY", - 0x1C: "MBC5+RUMBLE", - 0x1D: "MBC5+RUMBLE+RAM", - 0x1E: "MBC5+RUMBLE+RAM+BATTERY", - 0x20: "MBC6", - 0x22: "MBC7+SENSOR+RUMBLE+RAM+BATTERY", - 0xFC: "POCKET CAMERA", - 0xFD: "BANDAI TAMA5", - 0xFE: "HuC3", - 0xFF: "HuC1+RAM+BATTERY", -} - -// RAM sizes -// https://gbdev.io/pandocs/The_Cartridge_Header.html#0149--ram-size -var ramSizes = map[byte]string{ - 0x00: "0 - No RAM", - 0x01: "UNUSED VALUE", - 0x02: "8 KiB - 1 bank", - 0x03: "32 KiB - 4 banks of 8 KiB each", - 0x04: "128 KiB 16 banks of 8 KiB each", - 0x05: "64 KiB 8 banks of 8 KiB each", -} - -// Old licensees -// https://gbdev.io/pandocs/The_Cartridge_Header.html#014b--old-licensee-code -var oldLicensees = map[byte]string{ - 0x00: "None", - 0x01: "Nintendo", - 0x08: "Capcom", - 0x09: "HOT-B", - 0x0A: "Jaleco", - 0x0B: "Coconuts Japan", - 0x0C: "Elite Systems", - 0x13: "EA (Electronic Arts)", - 0x18: "Hudson Soft", - 0x19: "ITC Entertainment", - 0x1A: "Yanoman", - 0x1D: "Japan Clary", - 0x1F: "Virgin Games Ltd.3", - 0x24: "PCM Complete", - 0x25: "San-X", - 0x28: "Kemco", - 0x29: "SETA Corporation", - 0x30: "Infogrames5", - 0x31: "Nintendo", - 0x32: "Bandai", - 0x33: "Indicates that the New licensee code should be used instead.", - 0x34: "Konami", - 0x35: "HectorSoft", - 0x38: "Capcom", - 0x39: "Banpresto", - 0x3C: "Entertainment Interactive (stub)", - 0x3E: "Gremlin", - 0x41: "Ubi Soft1", - 0x42: "Atlus", - 0x44: "Malibu Interactive", - 0x46: "Angel", - 0x47: "Spectrum HoloByte", - 0x49: "Irem", - 0x4A: "Virgin Games Ltd.3", - 0x4D: "Malibu Interactive", - 0x4F: "U.S. Gold", - 0x50: "Absolute", - 0x51: "Acclaim Entertainment", - 0x52: "Activision", - 0x53: "Sammy USA Corporation", - 0x54: "GameTek", - 0x55: "Park Place13", - 0x56: "LJN", - 0x57: "Matchbox", - 0x59: "Milton Bradley Company", - 0x5A: "Mindscape", - 0x5B: "Romstar", - 0x5C: "Naxat Soft14", - 0x5D: "Tradewest", - 0x60: "Titus Interactive", - 0x61: "Virgin Games Ltd.3", - 0x67: "Ocean Software", - 0x69: "EA (Electronic Arts)", - 0x6E: "Elite Systems", - 0x6F: "Electro Brain", - 0x70: "Infogrames5", - 0x71: "Interplay Entertainment", - 0x72: "Broderbund", - 0x73: "Sculptured Software6", - 0x75: "The Sales Curve Limited7", - 0x78: "THQ", - 0x79: "Accolade15", - 0x7A: "Triffix Entertainment", - 0x7C: "MicroProse", - 0x7F: "Kemco", - 0x80: "Misawa Entertainment", - 0x83: "LOZC G.", - 0x86: "Tokuma Shoten", - 0x8B: "Bullet-Proof Software2", - 0x8C: "Vic Tokai Corp.16", - 0x8E: "Ape Inc.17", - 0x8F: "I'Max18", - 0x91: "Chunsoft Co.8", - 0x92: "Video System", - 0x93: "Tsubaraya Productions", - 0x95: "Varie", - 0x96: "Yonezawa19/S'Pal", - 0x97: "Kemco", - 0x99: "Arc", - 0x9A: "Nihon Bussan", - 0x9B: "Tecmo", - 0x9C: "Imagineer", - 0x9D: "Banpresto", - 0x9F: "Nova", - 0xA1: "Hori Electric", - 0xA2: "Bandai", - 0xA4: "Konami", - 0xA6: "Kawada", - 0xA7: "Takara", - 0xA9: "Technos Japan", - 0xAA: "Broderbund", - 0xAC: "Toei Animation", - 0xAD: "Toho", - 0xAF: "Namco", - 0xB0: "Acclaim Entertainment", - 0xB1: "ASCII Corporation or Nexsoft", - 0xB2: "Bandai", - 0xB4: "Square Enix", - 0xB6: "HAL Laboratory", - 0xB7: "SNK", - 0xB9: "Pony Canyon", - 0xBA: "Culture Brain", - 0xBB: "Sunsoft", - 0xBD: "Sony Imagesoft", - 0xBF: "Sammy Corporation", - 0xC0: "Taito", - 0xC2: "Kemco", - 0xC3: "Square", - 0xC4: "Tokuma Shoten", - 0xC5: "Data East", - 0xC6: "Tonkin House", - 0xC8: "Koei", - 0xC9: "UFL", - 0xCA: "Ultra Games", - 0xCB: "VAP, Inc.", - 0xCC: "Use Corporation", - 0xCD: "Meldac", - 0xCE: "Pony Canyon", - 0xCF: "Angel", - 0xD0: "Taito", - 0xD1: "SOFEL (Software Engineering Lab)", - 0xD2: "Quest", - 0xD3: "Sigma Enterprises", - 0xD4: "ASK Kodansha Co.", - 0xD6: "Naxat Soft14", - 0xD7: "Copya System", - 0xD9: "Banpresto", - 0xDA: "Tomy", - 0xDB: "LJN", - 0xDD: "Nippon Computer Systems", - 0xDE: "Human Ent.", - 0xDF: "Altron", - 0xE0: "Jaleco", - 0xE1: "Towa Chiki", - 0xE2: "Yutaka # Needs more info", - 0xE3: "Varie", - 0xE5: "Epoch", - 0xE7: "Athena", - 0xE8: "Asmik Ace Entertainment", - 0xE9: "Natsume", - 0xEA: "King Records", - 0xEB: "Atlus", - 0xEC: "Epic/Sony Records", - 0xEE: "IGS", - 0xF0: "A Wave", - 0xF3: "Extreme Entertainment", - 0xFF: "LJN", -} - -type ROMHeader struct { - EntryPoint [4]byte - Logo [48]byte - // NOTE(m): Assuming "old" cartridges here. This may cause problems with newer cartridges. - // See https://gbdev.io/pandocs/The_Cartridge_Header.html#0134-0143--title - Title [16]byte - NewLicenseeCode [2]byte - SGBFlag byte - CartridgeType byte - ROMSize byte - RAMSize byte - DestinationCode byte - OldLicenseeCode byte - MaskROMVersionNumber byte - HeaderChecksum byte - GlobalChecksum [2]byte -} - -type Cartridge struct { - Filename string - Mapper string - Title string - Licensee string - SGBSupport bool - ROMSize int - RAMSize string - Destination string - Version int -} - -func Insert(filename string) Cartridge { - file, err := os.Open(filename) - if err != nil { - log.Fatal(err) - } - defer file.Close() - cartridge := Cartridge{Filename: file.Name()} - - // Jump to start of header - _, err = file.Seek(0x0100, io.SeekStart) - if err != nil { - log.Fatal(err) - } - - // Read header - var header ROMHeader - err = binary.Read(file, binary.LittleEndian, &header) - if err != nil { - log.Fatal(err) - } - - // Validate the ROM by checking presence of the Nintendo logo - if header.Logo != expectedLogo { - log.Fatal("Invalid ROM file: No valid logo found!") - } - - // Convert some header values - cartridge.Title = string(bytes.Trim(header.Title[:], "\x00")) - cartridge.Mapper = cartridgeTypes[header.CartridgeType] - if header.OldLicenseeCode == 0x33 { - // FIXME(m): Support new licensee codes - cartridge.Licensee = "Indicates that the New licensee code should be used instead." - } else { - cartridge.Licensee = oldLicensees[header.OldLicenseeCode] - } - cartridge.SGBSupport = (header.SGBFlag == 0x03) - cartridge.ROMSize = 32 * (1 << header.ROMSize) - cartridge.RAMSize = ramSizes[header.RAMSize] - switch header.DestinationCode { - case 0x00: - cartridge.Destination = "Japan (and possibly overseas)" - case 0x01: - cartridge.Destination = "Overseas only" - default: - cartridge.Destination = "UNKNOWN" - } - cartridge.Version = int(header.MaskROMVersionNumber) - - // TODO(m): Verify header checksum - - // NOTE(m): Ignoring global checksum which is not used, except by one emulator. - // See https://gbdev.io/pandocs/The_Cartridge_Header.html#014e-014f--global-checksum - - return cartridge -} diff --git a/gb/cartridge.go b/gb/cartridge.go index f13c48d..0fffbff 100644 --- a/gb/cartridge.go +++ b/gb/cartridge.go @@ -3,8 +3,7 @@ package gb import ( "bytes" "encoding/binary" - "io" - "log" + "fmt" "os" ) @@ -215,6 +214,7 @@ var oldLicensees = map[byte]string{ } type ROMHeader struct { + _ [256]byte EntryPoint [4]byte Logo [48]byte // NOTE(m): Assuming "old" cartridges here. This may cause problems with newer cartridges. @@ -228,7 +228,7 @@ type ROMHeader struct { DestinationCode byte OldLicenseeCode byte MaskROMVersionNumber byte - HeaderChecksum byte + Checksum byte GlobalChecksum [2]byte } @@ -242,32 +242,23 @@ type Cartridge struct { RAMSize string Destination string Version int + Checksum byte } -func InsertCartridge(path string) *Cartridge { - file, err := os.Open(path) - if err != nil { - log.Fatal(err) - } - defer file.Close() - cartridge := Cartridge{Filename: file.Name()} +func InsertCartridge(filename string) (*Cartridge, error) { + cartridge := Cartridge{Filename: filename} - // Jump to start of header - _, err = file.Seek(0x0100, io.SeekStart) + rom, err := os.ReadFile(filename) if err != nil { - log.Fatal(err) + return &cartridge, err } // Read header var header ROMHeader - err = binary.Read(file, binary.LittleEndian, &header) + buffer := bytes.NewReader(rom) + err = binary.Read(buffer, binary.LittleEndian, &header) if err != nil { - log.Fatal(err) - } - - // Validate the ROM by checking presence of the Nintendo logo - if header.Logo != expectedLogo { - log.Fatal("Invalid ROM file: No valid logo found!") + return &cartridge, nil } // Convert some header values @@ -292,10 +283,16 @@ func InsertCartridge(path string) *Cartridge { } cartridge.Version = int(header.MaskROMVersionNumber) - // TODO(m): Verify header checksum + // Calculate and verify checksum + for address := uint16(0x0134); address <= uint16(0x014C); address++ { + cartridge.Checksum = cartridge.Checksum - rom[address] - 1 + } + if cartridge.Checksum != header.Checksum { + return &cartridge, fmt.Errorf("ROM checksum failed: %X does not equal %X", cartridge.Checksum, header.Checksum) + } // NOTE(m): Ignoring global checksum which is not used, except by one emulator. // See https://gbdev.io/pandocs/The_Cartridge_Header.html#014e-014f--global-checksum - return &cartridge + return &cartridge, nil } diff --git a/cartridge_test.go b/gb/cartridge_test.go similarity index 54% rename from cartridge_test.go rename to gb/cartridge_test.go index 79d3f9a..dca011e 100644 --- a/cartridge_test.go +++ b/gb/cartridge_test.go @@ -1,4 +1,4 @@ -package main +package gb import ( "testing" @@ -6,10 +6,13 @@ import ( "github.com/stretchr/testify/assert" ) -func TestInsertCartridge(t *testing.T) { - cartridge := Insert("./roms/dmg-acid2.gb") +func TestInsert(t *testing.T) { + cartridge, err := InsertCartridge("../roms/dmg-acid2.gb") + assert := assert.New(t) + assert.Nil(err) + assert.Equal(cartridge.Filename, "../roms/dmg-acid2.gb") assert.Equal(cartridge.Title, "DMG-ACID2") assert.Equal(cartridge.Mapper, "ROM ONLY") assert.Equal(cartridge.Licensee, "None") @@ -18,4 +21,11 @@ func TestInsertCartridge(t *testing.T) { assert.Equal(cartridge.RAMSize, "0 - No RAM") assert.Equal(cartridge.Destination, "Japan (and possibly overseas)") assert.Equal(cartridge.Version, 0) + assert.Equal(cartridge.Checksum, byte(0x9F)) +} + +func TestFailedChecksum(t *testing.T) { + _, err := InsertCartridge("../roms/failed-checksum.gb") + + assert.EqualError(t, err, "ROM checksum failed: 9F does not equal 41") } diff --git a/roms/failed-checksum.gb b/roms/failed-checksum.gb new file mode 100644 index 0000000000000000000000000000000000000000..8ce13e973f372d91d79487f51754ddf3bcdb0571 GIT binary patch literal 32768 zcmeI4e{56N700iC5>dtw3e)oINdFP({xzG2%_XzoX?9ZQ1n*V;5nLf+nUqhk!D)N|Zgq$Fi zq>7X~n@*l0)t}5CUb=bnM(XO-LoXk${>%K$Pf{1I{(6_(^}^mA?VUYcJBzDr|M^!} zzCy1r?DH=)_}%Z8I*D$XAPZak?!OTr?VP&FV}{T8NbeXm%$#XHj^t ztHh~H8tq_-lO!yXg~dc5;aEZcgr!v^FL6#xSR~$7%ADl1CBbsaiq}QevEU_Q`idG|+Yu#>_wX|aWD5=k~-q&Oax^`G^ zFflv#H@Ow0Psq1qFNhxN4LKt+F3D}s@h#CI4?sI4B7ZN3L5v9T36S$bE?&_(OI#Mkn8l5v zC2}+ED6pJgl5))gx5MnTfSK*hev8ElW=3rCb1#qJDT@8zwq3MZ&Geh_b0&(&EgmAu($W1G}lHf&m!Vf8~GZq88jQ zDux0I`U;!5qKyVEHbOTZHd>Qy$h2xfEjSHkAvRJ*YDg1lBXNi%mG!>XXfP7YOl$0b zu%Ig-OEVA+md$2Xv}Nt?^rzFG_VwANz5#pdK%c#DV89;n zDwie{<L_K9L3v5Ge@P@Gy}g&=);OB-m8L-bxZy zDwVJw&OSH?s+4dL&M=&jD$@To2kG}!kp3ZdI*9kV5~4g;O1y_lT*~26SM-}T1(Poa zd-c_V4ruP(yHOXb+qd&Q<>iD-PUiKNEp>I7OkO7v3Rqq=;=9k&eV)r zUETfrrWW^2Pn)`%TQq8FiCZQkNt(v`oQ9cxyWQvO>hk$a&8VF@^U_PEW>-Zqt0RQ@ z$HtBvG5t?HwH@r*HB`CP7i{bs*;74=nAI2?An&Bpe*_I8V2duYOX!c6u_;@mT>z2^ZKHJ^h(z12yp+iE4LV-Xi)CDH= zwrzpH{{4YKON-DWBhAeokK1j&ZOPu9WN(+3mI{AMOCa#dD}g|NztEA0q6CAA5{U@C zdv`~N$5UH7I4Ja!Pxki@5BGx!Ju=eOH8|Mbe)Oo&g}-9G`O3rU6Qb+-8Gh&@N>%9` ztTsPY72Y3aPsU|9{{-hUXi8J`EcB+~3a=~2V)`uT-@%!V#|g>j7x{d?*umJos6VzZ zc94x1jq+AA?}fKCVf-n5R!_n?r9%zuem*99K67tn8d;3DI#pHeV728f8jbql0Dohn zyi*<2XS1)bO|rLZVLmhTG(82?&aiu8u~-*W!k&Ge-|(;SF=0Mjr|93!c!N(;zt>pX z*vRWm>gVYR`hcEb^{T4=A=gHJ;4?}e)HwJ7uMZpfX3Tu?IQ;ErZv+a7vbA75L`jrSn_vMjS18snd( z7hp`jK59tqpnrh-`TFTP%q1mGF(2TSf@otthCjt(n4gb+K~F*iufWv$`OJjit$zr< z(-6Y%)nJ%60vaAU1HEd0iP&gaga2h(`nkdOc0*b{I3msmWmKdklt==#TE>htlDrZ2@u z)aPTd+{dDb? z$D;4K*3$6hnR=U(Ot^_+A?5Hzne+h*<9u z{}ruW>x56tT+_PYBQtljy;t1u`5NL{c*YMcz53$9Zom8uO-a-EFZX|M$HQyFUL=47 zkN^@u0!RP}AOR$R1dsp{Kmter2_OL^fCP{L5