Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion identifier/ps2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
// You should have received a copy of the GNU General Public License
// along with go-gameid. If not, see <https://www.gnu.org/licenses/>.

//nolint:dupl // PS2/PSP tests have similar structure but test different identifiers
package identifier

import (
Expand Down
18 changes: 9 additions & 9 deletions identifier/psp.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,17 +74,17 @@ func identifyPSPFromISO(iso *iso9660.ISO9660, database Database) (*Result, error
result := NewResult(ConsolePSP)

// Look for UMD_DATA.BIN in root
files, err := iso.IterFiles(true)
if err != nil {
return nil, fmt.Errorf("iterate files: %w", err)
}

var umdDataInfo *iso9660.FileInfo
for _, f := range files {
if strings.ToUpper(filepath.Base(f.Path)) == "UMD_DATA.BIN" {
umdDataInfo = &f
break
err := iso.WalkFiles(true, func(file iso9660.FileInfo) bool {
fileName := strings.ToUpper(filepath.Base(cleanISOFileName(file.Path)))
if fileName == "UMD_DATA.BIN" {
umdDataInfo = &file
return false
}
return true
})
if err != nil {
return nil, fmt.Errorf("iterate files: %w", err)
}

if umdDataInfo == nil {
Expand Down
31 changes: 30 additions & 1 deletion identifier/psp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,16 @@
// You should have received a copy of the GNU General Public License
// along with go-gameid. If not, see <https://www.gnu.org/licenses/>.

//nolint:dupl // PS2/PSP tests have similar structure but test different identifiers
package identifier

import (
"errors"
"os"
"path/filepath"
"strings"
"testing"

"github.com/ZaparooProject/go-gameid/internal/testiso"
)

func TestPSPIdentifier_Console(t *testing.T) {
Expand Down Expand Up @@ -54,3 +58,28 @@ func TestPSPIdentifier_IdentifyFromPath_NonExistent(t *testing.T) {
t.Error("IdentifyFromPath() should error for non-existent file")
}
}

func TestPSPIdentifier_IdentifyFromPath_UMDData(t *testing.T) {
t.Parallel()

isoData := testiso.CreateMinimal(t, "PSPTEST", "PLAYSTATION", "", []testiso.File{
{Name: "UMD_DATA.BIN;1", Data: []byte("UCUS-98765|Example Game")},
})
isoPath := filepath.Join(t.TempDir(), "game.iso")
if err := os.WriteFile(isoPath, isoData, 0o600); err != nil {
t.Fatalf("WriteFile() error = %v", err)
}

id := NewPSPIdentifier()
result, err := id.IdentifyFromPath(isoPath, nil)
if err != nil {
t.Fatalf("IdentifyFromPath() error = %v", err)
}

if result.ID != "UCUS-98765" {
t.Errorf("result.ID = %q, want %q", result.ID, "UCUS-98765")
}
if !strings.HasPrefix(result.Metadata["volume_ID"], "PSPTEST") {
t.Errorf("volume_ID metadata = %q, want prefix %q", result.Metadata["volume_ID"], "PSPTEST")
}
}
117 changes: 100 additions & 17 deletions identifier/psx.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ type playstationISO interface {
Close() error
}

type rootFileWalker interface {
WalkFiles(onlyRootDir bool, fn func(iso9660.FileInfo) bool) error
}

type isoFileReader interface {
ReadFile(info iso9660.FileInfo) ([]byte, error)
}

// openPlayStationISO opens an ISO from a path, handling CUE and CHD files.
func openPlayStationISO(path string) (playstationISO, error) {
ext := strings.ToLower(filepath.Ext(path))
Expand Down Expand Up @@ -88,26 +96,11 @@ func identifyPlayStation(
) (*Result, error) {
result := NewResult(console)

// Get root files
files, err := iso.IterFiles(true)
rootFiles, serial, err := playStationRootInfo(iso, console, database)
if err != nil {
return nil, fmt.Errorf("iterate files: %w", err)
}

// Build list of root filenames
rootFiles := make([]string, 0, len(files))
for _, f := range files {
name := strings.TrimPrefix(f.Path, "/")
// Remove version suffix (;1)
if idx := strings.Index(name, ";"); idx != -1 {
name = name[:idx]
}
rootFiles = append(rootFiles, name)
return nil, err
}

// Try to find serial from root files using ID prefixes
serial := findPlayStationSerial(rootFiles, console, database)

// 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.
Expand Down Expand Up @@ -141,6 +134,96 @@ func identifyPlayStation(
return result, nil
}

func playStationRootInfo(iso playstationISO, console Console, database Database) (
rootFiles []string,
serial string,
err error,
) {
if walker, ok := iso.(rootFileWalker); ok {
return playStationRootInfoWalk(iso, walker, console, database)
}

files, iterErr := iso.IterFiles(true)
if iterErr != nil {
return nil, "", fmt.Errorf("iterate files: %w", iterErr)
}
rootFiles = make([]string, 0, len(files))
for _, file := range files {
name := cleanISOFileName(file.Path)
rootFiles = append(rootFiles, name)
if serial == "" && strings.EqualFold(name, "SYSTEM.CNF") {
serial = serialFromSystemCNFFile(iso, file)
}
if serial == "" {
serial = serialFromRootFile(name)
}
}
if serial == "" {
serial = findPlayStationSerial(rootFiles, console, database)
}
return rootFiles, serial, nil
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

func playStationRootInfoWalk(
iso playstationISO,
walker rootFileWalker,
console Console,
database Database,
) (rootFiles []string, serial string, err error) {
rootFiles = make([]string, 0)
err = walker.WalkFiles(true, func(file iso9660.FileInfo) bool {
name := cleanISOFileName(file.Path)
rootFiles = append(rootFiles, name)
if serial == "" && strings.EqualFold(name, "SYSTEM.CNF") {
serial = serialFromSystemCNFFile(iso, file)
}
if serial == "" {
serial = serialFromRootFile(name)
}
return true
})
if err != nil {
return nil, "", fmt.Errorf("iterate files: %w", err)
}
if serial == "" {
serial = findPlayStationSerial(rootFiles, console, database)
}
return rootFiles, serial, nil
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

func cleanISOFileName(path string) string {
name := strings.TrimPrefix(path, "/")
if idx := strings.Index(name, ";"); idx != -1 {
name = name[:idx]
}
return name
}

func serialFromSystemCNFFile(iso playstationISO, file iso9660.FileInfo) string {
reader, ok := iso.(isoFileReader)
if !ok {
return ""
}
data, err := reader.ReadFile(file)
if err != nil {
return ""
}
return serialFromSystemCNF(string(data))
}

func serialFromSystemCNF(content string) string {
fields := strings.FieldsFunc(strings.ToUpper(content), func(runeValue rune) bool {
return !unicode.IsLetter(runeValue) && !unicode.IsDigit(runeValue) &&
runeValue != '_' && runeValue != '-' && runeValue != '.'
})
for _, field := range fields {
if serial := serialFromRootFile(field); serial != "" {
return serial
}
}
return ""
}

Comment thread
coderabbitai[bot] marked this conversation as resolved.
// findPlayStationSerial searches for serial in root files.
func findPlayStationSerial(rootFiles []string, console Console, database Database) string {
for _, fileName := range rootFiles {
Expand Down
144 changes: 144 additions & 0 deletions identifier/psx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ package identifier

import (
"errors"
"slices"
"testing"

"github.com/ZaparooProject/go-gameid/iso9660"
Expand All @@ -40,6 +41,51 @@ func (m *mockPlayStationISO) IterFiles(_ bool) ([]iso9660.FileInfo, error) {
}
func (*mockPlayStationISO) Close() error { return nil }

type mockWalkingPlayStationISO struct {
readData map[string][]byte
walkErr error
readErr error
mockPlayStationISO
walked int
read int
}

func (m *mockWalkingPlayStationISO) WalkFiles(_ bool, fn func(iso9660.FileInfo) bool) error {
if m.walkErr != nil {
return m.walkErr
}
for _, file := range m.files {
m.walked++
if !fn(file) {
return nil
}
}
return nil
}

func (m *mockWalkingPlayStationISO) ReadFile(info iso9660.FileInfo) ([]byte, error) {
m.read++
if m.readErr != nil {
return nil, m.readErr
}
return m.readData[info.Path], nil
}

type mockReadingPlayStationISO struct {
readData map[string][]byte
readErr error
mockPlayStationISO
read int
}

func (m *mockReadingPlayStationISO) ReadFile(info iso9660.FileInfo) ([]byte, error) {
m.read++
if m.readErr != nil {
return nil, m.readErr
}
return m.readData[info.Path], nil
}

// mockDatabase implements Database for testing.
type mockDatabase struct {
stringEntries map[Console]map[string]map[string]string
Expand Down Expand Up @@ -87,6 +133,104 @@ func (m *mockDatabase) setPrefixes(console Console, prefixes []string) {
m.idPrefixes[console] = prefixes
}

func TestSerialFromSystemCNF(t *testing.T) {
t.Parallel()

tests := []struct {
name string
content string
want string
}{
{"boot", "BOOT = cdrom:\\SLUS_005.94;1", "SLUS_00594"},
{"boot2", "BOOT2 = cdrom0:\\SLES-123.45;1", "SLES_12345"},
{"none", "BOOT = cdrom:\\MAIN.EXE;1", ""},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if got := serialFromSystemCNF(tt.content); got != tt.want {
t.Errorf("serialFromSystemCNF() = %q, want %q", got, tt.want)
}
})
}
}

func TestPlayStationRootInfo_SystemCNFReaderPaths(t *testing.T) {
t.Parallel()

files := []iso9660.FileInfo{
{Path: "/SYSTEM.CNF;1"},
{Path: "/README.TXT"},
}
readData := map[string][]byte{
"/SYSTEM.CNF;1": []byte("BOOT2 = cdrom0:\\SLES-123.45;1"),
}

t.Run("walk files dispatch", func(t *testing.T) {
t.Parallel()
mockISO := &mockWalkingPlayStationISO{
mockPlayStationISO: mockPlayStationISO{files: files},
readData: readData,
}

rootFiles, serial, err := playStationRootInfo(mockISO, ConsolePSX, nil)
if err != nil {
t.Fatalf("playStationRootInfo() error = %v", err)
}

assertPlayStationRootInfo(t, rootFiles, serial, mockISO.walked, mockISO.read)
})

t.Run("walk files direct", func(t *testing.T) {
t.Parallel()
mockISO := &mockWalkingPlayStationISO{
mockPlayStationISO: mockPlayStationISO{files: files},
readData: readData,
}

rootFiles, serial, err := playStationRootInfoWalk(mockISO, mockISO, ConsolePSX, nil)
if err != nil {
t.Fatalf("playStationRootInfoWalk() error = %v", err)
}

assertPlayStationRootInfo(t, rootFiles, serial, mockISO.walked, mockISO.read)
})

t.Run("iter files", func(t *testing.T) {
t.Parallel()
mockISO := &mockReadingPlayStationISO{
mockPlayStationISO: mockPlayStationISO{files: files},
readData: readData,
}

rootFiles, serial, err := playStationRootInfo(mockISO, ConsolePSX, nil)
if err != nil {
t.Fatalf("playStationRootInfo() error = %v", err)
}

assertPlayStationRootInfo(t, rootFiles, serial, len(files), mockISO.read)
})
}

func assertPlayStationRootInfo(t *testing.T, rootFiles []string, serial string, walked, read int) {
t.Helper()

if serial != "SLES_12345" {
t.Errorf("serial = %q, want %q", serial, "SLES_12345")
}
wantRootFiles := []string{"SYSTEM.CNF", "README.TXT"}
if !slices.Equal(rootFiles, wantRootFiles) {
t.Errorf("rootFiles = %v, want %v", rootFiles, wantRootFiles)
}
if walked != 2 {
t.Errorf("visited %d files, want 2", walked)
}
if read != 1 {
t.Errorf("ReadFile called %d times, want 1", read)
}
}

// Tests for serialFromVolumeID
func TestSerialFromVolumeID(t *testing.T) {
t.Parallel()
Expand Down
Loading
Loading