From 0b6de1acaaa7e37d8d2c43aa3d52f02fb73110d5 Mon Sep 17 00:00:00 2001 From: Callan Barrett Date: Mon, 6 Jul 2026 12:31:42 +0800 Subject: [PATCH 1/4] fix: handle disc identification edge cases --- archive/archive.go | 2 +- archive/archive_test.go | 2 +- archive/detect.go | 2 +- archive/detect_test.go | 2 +- archive/errors.go | 2 +- archive/errors_test.go | 2 +- archive/path.go | 2 +- archive/path_test.go | 2 +- archive/rar.go | 4 +- archive/sevenzip.go | 2 +- archive/zip.go | 2 +- blockdevice_unix.go | 2 +- chd/bitstream.go | 2 +- chd/chd.go | 2 +- chd/chd_test.go | 6 +- chd/codec.go | 10 ++-- chd/codec_flac.go | 20 +++---- chd/codec_lzma.go | 3 +- chd/codec_zlib.go | 3 +- chd/codec_zstd.go | 2 +- chd/errors.go | 2 +- chd/header.go | 2 +- chd/hunk.go | 2 +- chd/metadata.go | 2 +- cmd/dbgen/main.go | 2 +- cmd/gameid/main.go | 2 +- console.go | 2 +- console_test.go | 2 +- database.go | 2 +- database_test.go | 2 +- gameid.go | 21 +++++-- gameid_test.go | 28 ++++++++- identifier/fuzz_test.go | 2 +- identifier/gb.go | 2 +- identifier/gb_test.go | 2 +- identifier/gba.go | 2 +- identifier/gba_test.go | 2 +- identifier/gc.go | 2 +- identifier/gc_test.go | 2 +- identifier/genesis.go | 2 +- identifier/genesis_test.go | 2 +- identifier/identifier.go | 2 +- identifier/n64.go | 2 +- identifier/n64_test.go | 17 ++---- identifier/neogeocd.go | 2 +- identifier/neogeocd_test.go | 2 +- identifier/nes.go | 2 +- identifier/nes_test.go | 2 +- identifier/ps2.go | 2 +- identifier/ps2_test.go | 2 +- identifier/psp.go | 2 +- identifier/psp_test.go | 2 +- identifier/psx.go | 96 +++++++++++++++++++++++++++++-- identifier/psx_test.go | 100 +++++++++++++++++++++++++-------- identifier/saturn.go | 2 +- identifier/saturn_test.go | 2 +- identifier/segacd.go | 5 +- identifier/segacd_test.go | 21 ++++++- identifier/snes.go | 7 ++- identifier/snes_test.go | 2 +- internal/binary/fuzz_test.go | 2 +- internal/binary/reader.go | 2 +- internal/binary/reader_test.go | 2 +- iso9660/chd.go | 2 +- iso9660/cue.go | 2 +- iso9660/cue_test.go | 2 +- iso9660/fuzz_test.go | 2 +- iso9660/iso9660.go | 49 +++++++++++++--- iso9660/iso9660_test.go | 52 ++++++++++++++++- iso9660/mounted.go | 2 +- iso9660/mounted_test.go | 2 +- 71 files changed, 412 insertions(+), 140 deletions(-) diff --git a/archive/archive.go b/archive/archive.go index 4493fe3..ef3a287 100644 --- a/archive/archive.go +++ b/archive/archive.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/archive/archive_test.go b/archive/archive_test.go index 2488ecb..0f96c3d 100644 --- a/archive/archive_test.go +++ b/archive/archive_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/archive/detect.go b/archive/detect.go index dc64f97..8bcbcde 100644 --- a/archive/detect.go +++ b/archive/detect.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/archive/detect_test.go b/archive/detect_test.go index e49c1a6..78f4fcd 100644 --- a/archive/detect_test.go +++ b/archive/detect_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/archive/errors.go b/archive/errors.go index 1467b35..1f32c0e 100644 --- a/archive/errors.go +++ b/archive/errors.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/archive/errors_test.go b/archive/errors_test.go index 808febf..c943a71 100644 --- a/archive/errors_test.go +++ b/archive/errors_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/archive/path.go b/archive/path.go index 5b5c74a..53dd69b 100644 --- a/archive/path.go +++ b/archive/path.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/archive/path_test.go b/archive/path_test.go index 1ff7ae0..7e89c7c 100644 --- a/archive/path_test.go +++ b/archive/path_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/archive/rar.go b/archive/rar.go index 137d47d..630faac 100644 --- a/archive/rar.go +++ b/archive/rar.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. @@ -60,7 +60,7 @@ func (ra *RARArchive) List() ([]FileInfo, error) { return nil, fmt.Errorf("create RAR reader: %w", err) } - var files []FileInfo //nolint:prealloc // RAR file count unknown until full scan + var files []FileInfo for { header, err := reader.Next() if errors.Is(err, io.EOF) { diff --git a/archive/sevenzip.go b/archive/sevenzip.go index 9bfea29..a1f0ed7 100644 --- a/archive/sevenzip.go +++ b/archive/sevenzip.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/archive/zip.go b/archive/zip.go index a39e134..3e65d70 100644 --- a/archive/zip.go +++ b/archive/zip.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/blockdevice_unix.go b/blockdevice_unix.go index 12874d0..180b2a7 100644 --- a/blockdevice_unix.go +++ b/blockdevice_unix.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/chd/bitstream.go b/chd/bitstream.go index 5da9735..e7f2e9a 100644 --- a/chd/bitstream.go +++ b/chd/bitstream.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/chd/chd.go b/chd/chd.go index 2ed7025..b36171e 100644 --- a/chd/chd.go +++ b/chd/chd.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/chd/chd_test.go b/chd/chd_test.go index 45b24db..d224589 100644 --- a/chd/chd_test.go +++ b/chd/chd_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. @@ -1070,7 +1070,9 @@ func TestNeoGeoCDCHD(t *testing.T) { codecName := "?" if int(entry.CompType) < len(header.Compressors) { tag := header.Compressors[entry.CompType] - codecName = string([]byte{byte(tag >> 24), byte(tag >> 16), byte(tag >> 8), byte(tag)}) + codecName = string([]byte{ + byte(tag >> 24 & 0xFF), byte(tag >> 16 & 0xFF), byte(tag >> 8 & 0xFF), byte(tag & 0xFF), + }) } t.Logf("Hunk %d: CompType=%d (%s), CompLength=%d, Offset=%d", idx, entry.CompType, codecName, entry.CompLength, entry.Offset) diff --git a/chd/codec.go b/chd/codec.go index d4f38c9..ab475c8 100644 --- a/chd/codec.go +++ b/chd/codec.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. @@ -112,10 +112,10 @@ func codecTagToString(tag uint32) string { return "none" } tagBytes := []byte{ - byte(tag >> 24), - byte(tag >> 16), - byte(tag >> 8), - byte(tag), + byte(tag >> 24 & 0xFF), + byte(tag >> 16 & 0xFF), + byte(tag >> 8 & 0xFF), + byte(tag & 0xFF), } return string(tagBytes) } diff --git a/chd/codec_flac.go b/chd/codec_flac.go index 41eb4d9..b1c9b34 100644 --- a/chd/codec_flac.go +++ b/chd/codec_flac.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. @@ -76,8 +76,8 @@ func writeFLACFrameSamples(audioFrame *frame.Frame, dst []byte, offset int) int for ch := range numChannels { sample := audioFrame.Subframes[ch].Samples[i] if offset+2 <= len(dst) { - dst[offset] = byte(sample >> 8) - dst[offset+1] = byte(sample) + dst[offset] = byte(sample >> 8 & 0xFF) + dst[offset+1] = byte(sample & 0xFF) offset += 2 } } @@ -198,18 +198,18 @@ func buildFLACHeader(sampleRate uint32, numChannels uint8, blockSize uint16) []b copy(header, flacHeaderTemplate) // Patch block sizes at offsets 0x08 and 0x0A (big-endian 16-bit) - header[0x08] = byte(blockSize >> 8) - header[0x09] = byte(blockSize) - header[0x0A] = byte(blockSize >> 8) - header[0x0B] = byte(blockSize) + header[0x08] = byte(blockSize >> 8 & 0xFF) + header[0x09] = byte(blockSize & 0xFF) + header[0x0A] = byte(blockSize >> 8 & 0xFF) + header[0x0B] = byte(blockSize & 0xFF) // Patch sample rate, channels, bits at offset 0x12 (big-endian 24-bit) // Format: (sample_rate << 4) | ((num_channels - 1) << 1) | (bits_per_sample - 1 >> 4) // For 16-bit audio: bits_per_sample = 16, so (16-1) >> 4 = 0 val := (sampleRate << 4) | (uint32(numChannels-1) << 1) - header[0x12] = byte(val >> 16) - header[0x13] = byte(val >> 8) - header[0x14] = byte(val) + header[0x12] = byte(val >> 16 & 0xFF) + header[0x13] = byte(val >> 8 & 0xFF) + header[0x14] = byte(val & 0xFF) return header } diff --git a/chd/codec_lzma.go b/chd/codec_lzma.go index 0107010..0b81f8b 100644 --- a/chd/codec_lzma.go +++ b/chd/codec_lzma.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. @@ -142,7 +142,6 @@ func (*cdLZMACodec) DecompressCD(dst, src []byte, destLen, frames int) (int, err // Extract compressed base length var compLenBase int if compLenBytes > 2 { - //nolint:gosec // G602: bounds checked via headerBytes = eccBytes + compLenBytes check above compLenBase = int(src[eccBytes])<<16 | int(src[eccBytes+1])<<8 | int(src[eccBytes+2]) } else { compLenBase = int(binary.BigEndian.Uint16(src[eccBytes : eccBytes+2])) diff --git a/chd/codec_zlib.go b/chd/codec_zlib.go index 50e922f..c75c24d 100644 --- a/chd/codec_zlib.go +++ b/chd/codec_zlib.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. @@ -87,7 +87,6 @@ func (*cdZlibCodec) DecompressCD(dst, src []byte, destLen, frames int) (int, err // Extract compressed base length var compLenBase int if compLenBytes > 2 { - //nolint:gosec // G602: bounds checked via headerBytes = eccBytes + compLenBytes check above compLenBase = int(src[eccBytes])<<16 | int(src[eccBytes+1])<<8 | int(src[eccBytes+2]) } else { compLenBase = int(binary.BigEndian.Uint16(src[eccBytes : eccBytes+2])) diff --git a/chd/codec_zstd.go b/chd/codec_zstd.go index 5a4eb59..ec6ed48 100644 --- a/chd/codec_zstd.go +++ b/chd/codec_zstd.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/chd/errors.go b/chd/errors.go index eeef7cf..bc5dcd1 100644 --- a/chd/errors.go +++ b/chd/errors.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/chd/header.go b/chd/header.go index f84dc81..7bf4eb0 100644 --- a/chd/header.go +++ b/chd/header.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/chd/hunk.go b/chd/hunk.go index 8924307..b328992 100644 --- a/chd/hunk.go +++ b/chd/hunk.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/chd/metadata.go b/chd/metadata.go index dd8006a..4f4accb 100644 --- a/chd/metadata.go +++ b/chd/metadata.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/cmd/dbgen/main.go b/cmd/dbgen/main.go index 46b7d0b..ed89c17 100644 --- a/cmd/dbgen/main.go +++ b/cmd/dbgen/main.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/cmd/gameid/main.go b/cmd/gameid/main.go index 896c928..76b02b2 100644 --- a/cmd/gameid/main.go +++ b/cmd/gameid/main.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/console.go b/console.go index 1e0cb7b..cc1e10d 100644 --- a/console.go +++ b/console.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/console_test.go b/console_test.go index 150d2cf..1f33de1 100644 --- a/console_test.go +++ b/console_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/database.go b/database.go index 87beb2a..a812302 100644 --- a/database.go +++ b/database.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/database_test.go b/database_test.go index 34a6195..ed1944e 100644 --- a/database_test.go +++ b/database_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/gameid.go b/gameid.go index 81c5c7f..1e3da5b 100644 --- a/gameid.go +++ b/gameid.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. @@ -22,6 +22,7 @@ package gameid import ( + "errors" "fmt" "os" "strings" @@ -140,10 +141,16 @@ func IdentifyWithConsole(path string, console Console, db *GameDatabase) (*Resul // Check if this identifier needs the file path (disc-based games) if pid, ok := id.(pathIdentifier); ok { result, pathErr := pid.IdentifyFromPath(path, dbInterface) - if pathErr != nil { + if pathErr == nil { + return result, nil + } + // Identifiers like GameCube only need the path for specific container + // formats (CHD) and report NotSupportedError otherwise; fall through + // to reader-based identification for plain files. + var notSupported identifier.NotSupportedError + if !errors.As(pathErr, ¬Supported) { return nil, fmt.Errorf("identify from path: %w", pathErr) } - return result, nil } // Open file and identify using reader @@ -284,10 +291,14 @@ func identifyFromBlockDevice(path string, _ Console, ident identifier.Identifier // For disc-based consoles, use IdentifyFromPath which handles block devices if pid, ok := ident.(pathIdentifier); ok { result, err := pid.IdentifyFromPath(path, database) - if err != nil { + if err == nil { + return result, nil + } + var notSupported identifier.NotSupportedError + if !errors.As(err, ¬Supported) { return nil, fmt.Errorf("identify from path: %w", err) } - return result, nil + // Fall through to raw block device reading below. } // Open block device directly diff --git a/gameid_test.go b/gameid_test.go index a38e635..855a795 100644 --- a/gameid_test.go +++ b/gameid_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. @@ -209,6 +209,32 @@ func TestIdentifyWithConsole(t *testing.T) { } } +func TestIdentifyWithConsole_GCPlainFile(t *testing.T) { + t.Parallel() + + // GameCube's IdentifyFromPath only handles CHD; plain .gcm/.iso files must + // fall back to reader-based identification instead of erroring out. + header := make([]byte, 0x440) + copy(header[0x0000:], "GALE") + copy(header[0x0004:], "01") + copy(header[0x001C:], []byte{0xC2, 0x33, 0x9F, 0x3D}) + copy(header[0x0020:], "Test Game") + + path := filepath.Join(t.TempDir(), "game.gcm") + if err := os.WriteFile(path, header, 0o600); err != nil { + t.Fatalf("failed to write test file: %v", err) + } + + result, err := IdentifyWithConsole(path, ConsoleGC, nil) + if err != nil { + t.Fatalf("IdentifyWithConsole() error = %v", err) + } + + if result.ID != "GALE" { + t.Errorf("ID = %q, want %q", result.ID, "GALE") + } +} + func TestIdentify(t *testing.T) { t.Parallel() diff --git a/identifier/fuzz_test.go b/identifier/fuzz_test.go index 75e16f7..0e8cd8c 100644 --- a/identifier/fuzz_test.go +++ b/identifier/fuzz_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/identifier/gb.go b/identifier/gb.go index aa898c4..c4a80d4 100644 --- a/identifier/gb.go +++ b/identifier/gb.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/identifier/gb_test.go b/identifier/gb_test.go index 30e1687..dc9ddb3 100644 --- a/identifier/gb_test.go +++ b/identifier/gb_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/identifier/gba.go b/identifier/gba.go index ea6431b..c7c210d 100644 --- a/identifier/gba.go +++ b/identifier/gba.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/identifier/gba_test.go b/identifier/gba_test.go index f98209a..daa06a4 100644 --- a/identifier/gba_test.go +++ b/identifier/gba_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/identifier/gc.go b/identifier/gc.go index 1c2384f..acd2f19 100644 --- a/identifier/gc.go +++ b/identifier/gc.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/identifier/gc_test.go b/identifier/gc_test.go index 861d545..f2dae1f 100644 --- a/identifier/gc_test.go +++ b/identifier/gc_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/identifier/genesis.go b/identifier/genesis.go index 5c6a090..86069b4 100644 --- a/identifier/genesis.go +++ b/identifier/genesis.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/identifier/genesis_test.go b/identifier/genesis_test.go index cd4b361..94e2660 100644 --- a/identifier/genesis_test.go +++ b/identifier/genesis_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/identifier/identifier.go b/identifier/identifier.go index 93fb682..914aacf 100644 --- a/identifier/identifier.go +++ b/identifier/identifier.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/identifier/n64.go b/identifier/n64.go index 8f2ec5d..b95b4e6 100644 --- a/identifier/n64.go +++ b/identifier/n64.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/identifier/n64_test.go b/identifier/n64_test.go index 73cef21..95f98a0 100644 --- a/identifier/n64_test.go +++ b/identifier/n64_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. @@ -33,18 +33,9 @@ func createN64HeaderBigEndian(cartID, countryCode, title string) []byte { header[2] = 0x12 header[3] = 0x40 - // Title at 0x20 (20 bytes) - titleBytes := []byte(title) - if len(titleBytes) > 20 { - titleBytes = titleBytes[:20] - } - for idx := range 20 { - if idx < len(titleBytes) { - header[0x20+idx] = titleBytes[idx] - } else { - header[0x20+idx] = ' ' - } - } + // Title at 0x20 (20 bytes, space-padded) + titleBytes := append([]byte(title), bytes.Repeat([]byte{' '}, 20)...) + copy(header[0x20:0x34], titleBytes[:20]) // Cartridge ID at 0x3C (2 bytes) if len(cartID) >= 2 { diff --git a/identifier/neogeocd.go b/identifier/neogeocd.go index 66ec56d..6b69d9e 100644 --- a/identifier/neogeocd.go +++ b/identifier/neogeocd.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/identifier/neogeocd_test.go b/identifier/neogeocd_test.go index 26975b3..c51b8af 100644 --- a/identifier/neogeocd_test.go +++ b/identifier/neogeocd_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/identifier/nes.go b/identifier/nes.go index 44ae10d..aac8c20 100644 --- a/identifier/nes.go +++ b/identifier/nes.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/identifier/nes_test.go b/identifier/nes_test.go index edc026e..8b6292c 100644 --- a/identifier/nes_test.go +++ b/identifier/nes_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/identifier/ps2.go b/identifier/ps2.go index 41dbc6f..c849d94 100644 --- a/identifier/ps2.go +++ b/identifier/ps2.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/identifier/ps2_test.go b/identifier/ps2_test.go index b689a39..c3c3498 100644 --- a/identifier/ps2_test.go +++ b/identifier/ps2_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/identifier/psp.go b/identifier/psp.go index 414cf0e..ca4a5ea 100644 --- a/identifier/psp.go +++ b/identifier/psp.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/identifier/psp_test.go b/identifier/psp_test.go index 0dc77de..998c46b 100644 --- a/identifier/psp_test.go +++ b/identifier/psp_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/identifier/psx.go b/identifier/psx.go index 4c98656..89a9f19 100644 --- a/identifier/psx.go +++ b/identifier/psx.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. @@ -23,10 +23,26 @@ import ( "io" "path/filepath" "strings" + "unicode" "github.com/ZaparooProject/go-gameid/iso9660" ) +var playStationSerialPrefixes = map[string]struct{}{ + "PAPX": {}, + "PBPX": {}, + "PCPX": {}, + "SCES": {}, + "SCPS": {}, + "SCUS": {}, + "SLES": {}, + "SLKA": {}, + "SLPM": {}, + "SLPS": {}, + "SLUS": {}, + "TCPS": {}, +} + // playstationISO is the interface for PlayStation disc images. type playstationISO interface { GetUUID() string @@ -92,14 +108,21 @@ func identifyPlayStation( // Try to find serial from root files using ID prefixes serial := findPlayStationSerial(rootFiles, console, database) - // Fallback to volume ID + // Fallback to volume ID. Unlike upstream GameID this is accepted without + // a database match: the volume ID is read from the disc itself, so an + // image and the physical disc it was dumped from still agree on it. if serial == "" { serial = serialFromVolumeID(iso.GetVolumeID()) } - // Fallback to filename - if serial == "" && sourcePath != "" { - serial = serialFromFilename(sourcePath) + // Fallback to filename, only when the database confirms it is a real + // serial. A bare filename (or "sr0" for a block device) is not an + // identifier and would produce junk IDs. + if serial == "" && sourcePath != "" && database != nil { + candidate := strings.ReplaceAll(serialFromFilename(sourcePath), "-", "_") + if _, found := database.LookupByString(console, candidate); found { + serial = candidate + } } result.ID = strings.ReplaceAll(serial, "_", "-") @@ -118,8 +141,14 @@ func identifyPlayStation( return result, nil } -// findPlayStationSerial searches for serial in root files using ID prefixes. +// findPlayStationSerial searches for serial in root files. func findPlayStationSerial(rootFiles []string, console Console, database Database) string { + for _, fileName := range rootFiles { + if serial := serialFromRootFile(fileName); serial != "" { + return serial + } + } + if database == nil { return "" } @@ -133,6 +162,61 @@ func findPlayStationSerial(rootFiles []string, console Console, database Databas return "" } +// serialFromRootFile extracts PlayStation serials from executable names in the disc root. +func serialFromRootFile(fileName string) string { + name := strings.ToUpper(filepath.Base(fileName)) + if idx := strings.Index(name, ";"); idx != -1 { + name = name[:idx] + } + name = strings.TrimSpace(name) + if len(name) < 9 { + return "" + } + + prefix := name[:4] + if _, ok := playStationSerialPrefixes[prefix]; !ok { + return "" + } + + pos := 4 + if pos < len(name) && isSerialSeparator(rune(name[pos])) { + pos++ + } + + firstDigits, firstOK := consumeDigits(name, pos, 3) + if !firstOK { + return "" + } + pos += len(firstDigits) + + if pos < len(name) && isSerialSeparator(rune(name[pos])) { + pos++ + } + + secondDigits, secondOK := consumeDigits(name, pos, 2) + if !secondOK { + return "" + } + + return prefix + "_" + firstDigits + secondDigits +} + +func consumeDigits(value string, start, count int) (string, bool) { + if start+count > len(value) { + return "", false + } + for pos := start; pos < start+count; pos++ { + if !unicode.IsDigit(rune(value[pos])) { + return "", false + } + } + return value[start : start+count], true +} + +func isSerialSeparator(value rune) bool { + return value == '_' || value == '-' || value == '.' || unicode.IsSpace(value) +} + // findSerialWithPrefix searches for a serial matching the given prefix. func findSerialWithPrefix(rootFiles []string, prefix string, console Console, database Database) string { for _, fileName := range rootFiles { diff --git a/identifier/psx_test.go b/identifier/psx_test.go index 2428052..45acefa 100644 --- a/identifier/psx_test.go +++ b/identifier/psx_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. @@ -143,6 +143,35 @@ func TestSerialFromFilename(t *testing.T) { } } +func TestSerialFromRootFile(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + fileName string + want string + }{ + {"underscore dotted", "SLUS_123.45", "SLUS_12345"}, + {"version suffix", "SCUS_123.45;1", "SCUS_12345"}, + {"hyphen compact", "SLES-12345", "SLES_12345"}, + {"compact", "SCES12345", "SCES_12345"}, + {"lowercase", "slpm_123.45", "SLPM_12345"}, + {"unknown prefix", "GAME_123.45", ""}, + {"not enough digits", "SLUS_123.4", ""}, + {"non digit", "SLUS_ABC.DE", ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := serialFromRootFile(tt.fileName) + if got != tt.want { + t.Errorf("serialFromRootFile(%q) = %q, want %q", tt.fileName, got, tt.want) + } + }) + } +} + // Tests for findPlayStationSerial func TestFindPlayStationSerial(t *testing.T) { t.Parallel() @@ -150,8 +179,8 @@ func TestFindPlayStationSerial(t *testing.T) { t.Run("nil database", func(t *testing.T) { t.Parallel() got := findPlayStationSerial([]string{"SLUS_123.45"}, ConsolePSX, nil) - if got != "" { - t.Errorf("findPlayStationSerial with nil db = %q, want empty", got) + if got != "SLUS_12345" { + t.Errorf("findPlayStationSerial with nil db = %q, want %q", got, "SLUS_12345") } }) @@ -159,7 +188,7 @@ func TestFindPlayStationSerial(t *testing.T) { t.Parallel() db := newMockDatabase() // No prefixes set - got := findPlayStationSerial([]string{"SLUS_123.45"}, ConsolePSX, db) + got := findPlayStationSerial([]string{"README.TXT"}, ConsolePSX, db) if got != "" { t.Errorf("findPlayStationSerial with no prefixes = %q, want empty", got) } @@ -296,24 +325,6 @@ func TestIdentifyPlayStation(t *testing.T) { } }) - t.Run("filename fallback", func(t *testing.T) { - t.Parallel() - mockISO := &mockPlayStationISO{ - uuid: "", - volumeID: "", - files: []iso9660.FileInfo{}, - } - - result, err := identifyPlayStation(mockISO, ConsolePSX, nil, "/games/SLPM-12345.iso") - if err != nil { - t.Fatalf("identifyPlayStation() error = %v", err) - } - - if result.ID != "SLPM-12345" { - t.Errorf("result.ID = %q, want %q", result.ID, "SLPM-12345") - } - }) - t.Run("IterFiles error", func(t *testing.T) { t.Parallel() mockISO := &mockPlayStationISO{ @@ -355,6 +366,51 @@ func TestIdentifyPlayStation(t *testing.T) { } // Tests for PSXIdentifier +func TestIdentifyPlayStation_FilenameFallbackWithDatabase(t *testing.T) { + t.Parallel() + + db := newMockDatabase() + db.addEntry(ConsolePSX, "SLPM_12345", map[string]string{ + "title": "Test Game", + }) + mockISO := &mockPlayStationISO{ + uuid: "", + volumeID: "", + files: []iso9660.FileInfo{}, + } + + result, err := identifyPlayStation(mockISO, ConsolePSX, db, "/games/SLPM-12345.iso") + if err != nil { + t.Fatalf("identifyPlayStation() error = %v", err) + } + + if result.ID != "SLPM-12345" { + t.Errorf("result.ID = %q, want %q", result.ID, "SLPM-12345") + } +} + +func TestIdentifyPlayStation_FilenameFallbackWithoutDatabase(t *testing.T) { + t.Parallel() + + mockISO := &mockPlayStationISO{ + uuid: "", + volumeID: "", + files: []iso9660.FileInfo{}, + } + + // A block device path must not become the ID ("sr0"), and neither should + // an arbitrary image filename when nothing confirms it. + for _, sourcePath := range []string{"/dev/sr0", "/games/SLPM-12345.iso"} { + result, err := identifyPlayStation(mockISO, ConsolePSX, nil, sourcePath) + if err != nil { + t.Fatalf("identifyPlayStation(%q) error = %v", sourcePath, err) + } + if result.ID != "" { + t.Errorf("identifyPlayStation(%q) ID = %q, want empty", sourcePath, result.ID) + } + } +} + func TestPSXIdentifier_Console(t *testing.T) { t.Parallel() diff --git a/identifier/saturn.go b/identifier/saturn.go index e632f48..1fb02b9 100644 --- a/identifier/saturn.go +++ b/identifier/saturn.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/identifier/saturn_test.go b/identifier/saturn_test.go index cf3552d..543f595 100644 --- a/identifier/saturn_test.go +++ b/identifier/saturn_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/identifier/segacd.go b/identifier/segacd.go index 01c3d26..60cb8f4 100644 --- a/identifier/segacd.go +++ b/identifier/segacd.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. @@ -269,6 +269,9 @@ func parseSegaCDBuildDate(raw string) string { // parseSegaCDDeviceSupport extracts device support codes from header. func parseSegaCDDeviceSupport(header []byte, magicIdx int) []string { var deviceSupport []string + if magicIdx+0x1A0 > len(header) { + return deviceSupport + } deviceSupportBytes := header[magicIdx+0x190 : magicIdx+0x1A0] for _, b := range deviceSupportBytes { if b == 0 || b == ' ' { diff --git a/identifier/segacd_test.go b/identifier/segacd_test.go index 361353d..11302ee 100644 --- a/identifier/segacd_test.go +++ b/identifier/segacd_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. @@ -206,6 +206,25 @@ func TestSegaCDIdentifier_InvalidMagic(t *testing.T) { } } +func TestSegaCDIdentifier_MagicWordNearEndOfHeader(t *testing.T) { + t.Parallel() + + identifier := NewSegaCDIdentifier() + + // A magic word appearing near the end of the 0x300 search window (as can + // happen with corrupt data or a non-SegaCD disc) must not cause the field + // parsers to slice past the header. + header := make([]byte, 0x300) + copy(header[0x2F0:], "SEGADISC") + + reader := bytes.NewReader(header) + + if _, err := identifier.Identify(reader, int64(len(header)), nil); err != nil { + // An error is acceptable; a panic is not. + t.Logf("Identify() returned error (acceptable): %v", err) + } +} + func TestSegaCDIdentifier_TooSmall(t *testing.T) { t.Parallel() diff --git a/identifier/snes.go b/identifier/snes.go index 2efa0c6..7dd9ec1 100644 --- a/identifier/snes.go +++ b/identifier/snes.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. @@ -249,10 +249,11 @@ func snesGetCoprocessor(mapMode byte, data []byte, headerStart int) string { // snesGetExtendedCoprocessor determines extended coprocessor type (0xF chip byte). func snesGetExtendedCoprocessor(data []byte, headerStart int) string { - if headerStart <= 0 { + idx := headerStart - 1 + if idx < 0 || idx >= len(data) { return "" } - prevByte := data[headerStart-1] + prevByte := data[idx] switch prevByte & 0x0F { case 0: return "SPC7110" diff --git a/identifier/snes_test.go b/identifier/snes_test.go index 4916bc7..ead04af 100644 --- a/identifier/snes_test.go +++ b/identifier/snes_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/internal/binary/fuzz_test.go b/internal/binary/fuzz_test.go index 3d322e3..3cc72a0 100644 --- a/internal/binary/fuzz_test.go +++ b/internal/binary/fuzz_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/internal/binary/reader.go b/internal/binary/reader.go index 0d61a81..92be956 100644 --- a/internal/binary/reader.go +++ b/internal/binary/reader.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/internal/binary/reader_test.go b/internal/binary/reader_test.go index 05e835e..ce90087 100644 --- a/internal/binary/reader_test.go +++ b/internal/binary/reader_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/iso9660/chd.go b/iso9660/chd.go index 99e1d18..8fd0da2 100644 --- a/iso9660/chd.go +++ b/iso9660/chd.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/iso9660/cue.go b/iso9660/cue.go index c35c247..4a2da6e 100644 --- a/iso9660/cue.go +++ b/iso9660/cue.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/iso9660/cue_test.go b/iso9660/cue_test.go index 9912b83..f3dcff9 100644 --- a/iso9660/cue_test.go +++ b/iso9660/cue_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/iso9660/fuzz_test.go b/iso9660/fuzz_test.go index 0fff176..6c412ec 100644 --- a/iso9660/fuzz_test.go +++ b/iso9660/fuzz_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/iso9660/iso9660.go b/iso9660/iso9660.go index 995021e..9eef89e 100644 --- a/iso9660/iso9660.go +++ b/iso9660/iso9660.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. @@ -84,6 +84,20 @@ func Open(path string) (*ISO9660, error) { size: info.Size(), } + // Block devices (e.g. /dev/sr0) report a stat size of 0; the kernel + // exposes cooked 2048-byte sectors, so seek to find the real size and + // pin the block size rather than guessing it from the size. + if info.Mode()&os.ModeDevice != 0 && info.Mode()&os.ModeCharDevice == 0 { + end, seekErr := isoFile.Seek(0, io.SeekEnd) + if seekErr != nil { + _ = isoFile.Close() + + return nil, fmt.Errorf("seek block device end: %w", seekErr) + } + iso.size = end + iso.blockSize = 2048 + } + if err := iso.init(); err != nil { _ = isoFile.Close() @@ -118,18 +132,26 @@ func OpenReaderWithCloser(reader io.ReaderAt, size int64, closer io.Closer) (*IS return iso, nil } -// init initializes the ISO9660 structure by finding PVD and parsing path table. -func (iso *ISO9660) init() error { - // Determine block size from file size +// detectBlockSize guesses the sector size of a disc image from its total size. +func detectBlockSize(size int64) int { switch { - case iso.size%2352 == 0: - iso.blockSize = 2352 - case iso.size%2048 == 0: - iso.blockSize = 2048 + case size%2352 == 0: + return 2352 + case size%2048 == 0: + return 2048 default: // For CHD sources, block size may not divide evenly // Default to 2048 for ISO9660 standard - iso.blockSize = 2048 + return 2048 + } +} + +// init initializes the ISO9660 structure by finding PVD and parsing path table. +func (iso *ISO9660) init() error { + // Determine block size from file size unless the caller already knows it + // (block devices always expose 2048-byte sectors). + if iso.blockSize == 0 { + iso.blockSize = detectBlockSize(iso.size) } // Search for PVD in first ~1MB @@ -372,8 +394,17 @@ func (iso *ISO9660) IterFiles(onlyRootDir bool) ([]FileInfo, error) { return files, nil } +// maxReadFileSize caps ReadFile allocations. Directory records store sizes as +// uint32, so a corrupt or malicious image can claim up to 4GB for a file; +// identification only ever reads small files, so refuse anything larger. +const maxReadFileSize = 16 * 1024 * 1024 + // ReadFile reads the contents of a file by its FileInfo. func (iso *ISO9660) ReadFile(info FileInfo) ([]byte, error) { + if info.Size > maxReadFileSize { + return nil, fmt.Errorf("%w: file %s claims %d bytes (max %d)", + ErrInvalidISO, info.Path, info.Size, maxReadFileSize) + } offset := iso.blockOffset + int64(info.LBA)*int64(iso.blockSize) data := make([]byte, info.Size) if _, err := iso.reader.ReadAt(data, offset); err != nil && err != io.EOF { diff --git a/iso9660/iso9660_test.go b/iso9660/iso9660_test.go index f0ffe52..782776a 100644 --- a/iso9660/iso9660_test.go +++ b/iso9660/iso9660_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. @@ -19,6 +19,7 @@ package iso9660 import ( + "bytes" "encoding/binary" "errors" "os" @@ -524,6 +525,55 @@ func TestOpenReaderWithCloser(t *testing.T) { } } +// TestInitPresetBlockSize verifies that a caller-pinned block size (used for +// block devices, which always expose 2048-byte sectors) is not overridden by +// the size-based heuristic, even when the size happens to divide by 2352. +func TestInitPresetBlockSize(t *testing.T) { + t.Parallel() + + isoData := createMinimalISO("TEST", "SYS", "PUB") + // Pad so the total size divides evenly by 2352 (lcm(2048,2352) = 301056): + // the heuristic would pick 2352 for this size, but the pinned value must win. + const lcmBlockSizes = 301056 + padded := make([]byte, ((len(isoData)/lcmBlockSizes)+1)*lcmBlockSizes) + copy(padded, isoData) + + iso := &ISO9660{ + reader: bytes.NewReader(padded), + size: int64(len(padded)), + blockSize: 2048, + } + if err := iso.init(); err != nil { + t.Fatalf("init() error = %v", err) + } + if iso.BlockSize() != 2048 { + t.Errorf("BlockSize() = %d, want 2048", iso.BlockSize()) + } + if got := strings.Trim(iso.GetVolumeID(), "\x00 "); got != "TEST" { + t.Errorf("GetVolumeID() = %q, want %q", got, "TEST") + } +} + +// TestReadFileSizeCap verifies that a corrupt directory record claiming a huge +// file size is rejected instead of allocating the claimed amount. +func TestReadFileSizeCap(t *testing.T) { + t.Parallel() + + isoData := createMinimalISO("TEST", "SYS", "PUB") + iso, err := OpenReader(bytes.NewReader(isoData), int64(len(isoData))) + if err != nil { + t.Fatalf("OpenReader failed: %v", err) + } + + _, err = iso.ReadFile(FileInfo{Path: "/HUGE.BIN", LBA: 20, Size: maxReadFileSize + 1}) + if err == nil { + t.Fatal("ReadFile() should reject sizes above maxReadFileSize") + } + if !errors.Is(err, ErrInvalidISO) { + t.Errorf("ReadFile() error = %v, want ErrInvalidISO", err) + } +} + // TestGetDataPreparerID verifies data preparer ID extraction. func TestGetDataPreparerID(t *testing.T) { t.Parallel() diff --git a/iso9660/mounted.go b/iso9660/mounted.go index 6afffb4..c207e3e 100644 --- a/iso9660/mounted.go +++ b/iso9660/mounted.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. diff --git a/iso9660/mounted_test.go b/iso9660/mounted_test.go index fb5a329..0aeb801 100644 --- a/iso9660/mounted_test.go +++ b/iso9660/mounted_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Niema Moshiri and The Zaparoo Project. +// Copyright (c) 2026 Niema Moshiri and The Zaparoo Project. // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of go-gameid. From b162512aa2e71e1890979a418ff586af9a667259 Mon Sep 17 00:00:00 2001 From: Callan Barrett Date: Mon, 6 Jul 2026 12:50:45 +0800 Subject: [PATCH 2/4] chore: bump go version --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 465a6ad..a95bb9e 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/ZaparooProject/go-gameid -go 1.24.11 +go 1.26.4 require ( github.com/bodgit/sevenzip v1.6.1 From 5f1c625dce14662cc126bc784e54ebf859cf4206 Mon Sep 17 00:00:00 2001 From: Callan Barrett Date: Mon, 6 Jul 2026 12:54:22 +0800 Subject: [PATCH 3/4] refactor: address disc identification review --- gameid.go | 64 ++++++++++++++++++++++++----------------- iso9660/iso9660.go | 17 +++++++---- iso9660/iso9660_test.go | 4 +-- 3 files changed, 51 insertions(+), 34 deletions(-) diff --git a/gameid.go b/gameid.go index 1e3da5b..04f6b58 100644 --- a/gameid.go +++ b/gameid.go @@ -138,19 +138,12 @@ func IdentifyWithConsole(path string, console Console, db *GameDatabase) (*Resul return identifyFromDirectory(path, console, dbInterface) } - // Check if this identifier needs the file path (disc-based games) - if pid, ok := id.(pathIdentifier); ok { - result, pathErr := pid.IdentifyFromPath(path, dbInterface) - if pathErr == nil { - return result, nil - } - // Identifiers like GameCube only need the path for specific container - // formats (CHD) and report NotSupportedError otherwise; fall through - // to reader-based identification for plain files. - var notSupported identifier.NotSupportedError - if !errors.As(pathErr, ¬Supported) { - return nil, fmt.Errorf("identify from path: %w", pathErr) - } + result, handled, pathErr := identifyFromPathIfSupported(id, path, dbInterface) + if pathErr != nil { + return nil, pathErr + } + if handled { + return result, nil } // Open file and identify using reader @@ -172,6 +165,28 @@ func IdentifyWithConsole(path string, console Console, db *GameDatabase) (*Resul return result, nil } +func identifyFromPathIfSupported( + ident identifier.Identifier, + path string, + database identifier.Database, +) (result *Result, handled bool, err error) { + pid, ok := ident.(pathIdentifier) + if !ok { + return nil, false, nil + } + + result, err = pid.IdentifyFromPath(path, database) + if err == nil { + return result, true, nil + } + + var notSupported identifier.NotSupportedError + if errors.As(err, ¬Supported) { + return nil, false, nil + } + return nil, false, fmt.Errorf("identify from path: %w", err) +} + // identifyFromDirectory identifies a game from a mounted disc directory. func identifyFromDirectory(path string, console Console, database identifier.Database) (*Result, error) { id, ok := identifiers[console] @@ -288,18 +303,15 @@ func IsCartridgeBased(console Console) bool { // //nolint:revive // Line length acceptable for function signature with ignored parameter func identifyFromBlockDevice(path string, _ Console, ident identifier.Identifier, database identifier.Database) (*Result, error) { - // For disc-based consoles, use IdentifyFromPath which handles block devices - if pid, ok := ident.(pathIdentifier); ok { - result, err := pid.IdentifyFromPath(path, database) - if err == nil { - return result, nil - } - var notSupported identifier.NotSupportedError - if !errors.As(err, ¬Supported) { - return nil, fmt.Errorf("identify from path: %w", err) - } - // Fall through to raw block device reading below. + // For disc-based consoles, use IdentifyFromPath which handles block devices. + result, handled, pathErr := identifyFromPathIfSupported(ident, path, database) + if pathErr != nil { + return nil, pathErr } + if handled { + return result, nil + } + // Fall through to raw block device reading when path identification is unsupported. // Open block device directly blockDev, err := os.Open(path) //nolint:gosec // Path from user input is expected for block device @@ -313,11 +325,11 @@ func identifyFromBlockDevice(path string, _ Console, ident identifier.Identifier // Most identifiers only need the first few KB size := int64(700 * 1024 * 1024) // 700MB typical CD size - result, err := ident.Identify(blockDev, size, database) + identified, err := ident.Identify(blockDev, size, database) if err != nil { return nil, fmt.Errorf("identify: %w", err) } - return result, nil + return identified, nil } // identifyFromArchive identifies a game file inside an archive. diff --git a/iso9660/iso9660.go b/iso9660/iso9660.go index 9eef89e..67740cd 100644 --- a/iso9660/iso9660.go +++ b/iso9660/iso9660.go @@ -394,16 +394,21 @@ func (iso *ISO9660) IterFiles(onlyRootDir bool) ([]FileInfo, error) { return files, nil } -// maxReadFileSize caps ReadFile allocations. Directory records store sizes as -// uint32, so a corrupt or malicious image can claim up to 4GB for a file; -// identification only ever reads small files, so refuse anything larger. -const maxReadFileSize = 16 * 1024 * 1024 +// DefaultReadFileSizeLimit caps ReadFile allocations. Directory records store +// sizes as uint32, so corrupt images can claim up to 4GB for a file. +const DefaultReadFileSizeLimit uint32 = 16 * 1024 * 1024 // ReadFile reads the contents of a file by its FileInfo. func (iso *ISO9660) ReadFile(info FileInfo) ([]byte, error) { - if info.Size > maxReadFileSize { + return iso.ReadFileWithLimit(info, DefaultReadFileSizeLimit) +} + +// ReadFileWithLimit reads the contents of a file with a caller-provided size limit. +// A limit of 0 disables the guard. +func (iso *ISO9660) ReadFileWithLimit(info FileInfo, maxSize uint32) ([]byte, error) { + if maxSize > 0 && info.Size > maxSize { return nil, fmt.Errorf("%w: file %s claims %d bytes (max %d)", - ErrInvalidISO, info.Path, info.Size, maxReadFileSize) + ErrInvalidISO, info.Path, info.Size, maxSize) } offset := iso.blockOffset + int64(info.LBA)*int64(iso.blockSize) data := make([]byte, info.Size) diff --git a/iso9660/iso9660_test.go b/iso9660/iso9660_test.go index 782776a..ecd1eae 100644 --- a/iso9660/iso9660_test.go +++ b/iso9660/iso9660_test.go @@ -565,9 +565,9 @@ func TestReadFileSizeCap(t *testing.T) { t.Fatalf("OpenReader failed: %v", err) } - _, err = iso.ReadFile(FileInfo{Path: "/HUGE.BIN", LBA: 20, Size: maxReadFileSize + 1}) + _, err = iso.ReadFile(FileInfo{Path: "/HUGE.BIN", LBA: 20, Size: DefaultReadFileSizeLimit + 1}) if err == nil { - t.Fatal("ReadFile() should reject sizes above maxReadFileSize") + t.Fatal("ReadFile() should reject sizes above DefaultReadFileSizeLimit") } if !errors.Is(err, ErrInvalidISO) { t.Errorf("ReadFile() error = %v, want ErrInvalidISO", err) From f674af011ffb5141326a7d2a4b5a42730e03424d Mon Sep 17 00:00:00 2001 From: Callan Barrett Date: Mon, 6 Jul 2026 12:55:47 +0800 Subject: [PATCH 4/4] ci: align release go version --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 98e8e46..798ea1e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -36,7 +36,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v6 with: - go-version: '1.24.x' + go-version: '1.26.4' - name: Build gameid env: