From 8d2f6cab4753e392602526eb1ab2daa9bb62b9b4 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 | 41 +++++++++++++++++++--------------------- cartridge_test.go | 14 ++++++++++++-- roms/failed-checksum.gb | Bin 0 -> 32768 bytes 3 files changed, 31 insertions(+), 24 deletions(-) create mode 100644 roms/failed-checksum.gb diff --git a/cartridge.go b/cartridge.go index 525d31d..e4bd8fe 100644 --- a/cartridge.go +++ b/cartridge.go @@ -3,8 +3,7 @@ package main 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 Insert(filename string) Cartridge { - file, err := os.Open(filename) - if err != nil { - log.Fatal(err) - } - defer file.Close() - cartridge := Cartridge{Filename: file.Name()} +func Insert(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 Insert(filename 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/cartridge_test.go index 79d3f9a..05c9eef 100644 --- a/cartridge_test.go +++ b/cartridge_test.go @@ -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 := Insert("./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 := Insert("./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