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
46 changes: 46 additions & 0 deletions pkg/platforms/mister/cores/cores_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,55 @@
package cores

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

"github.com/ZaparooProject/zaparoo-core/v2/pkg/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

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

root := t.TempDir()
stalePath := filepath.Join(root, "Amiga")
validPath := filepath.Join(root, "games", "Amiga")
writeAmigaVisionTestInstall(t, stalePath, "Stale Game", false)
writeAmigaVisionTestInstall(t, validPath, "Valid Game", true)

cfg, err := config.NewConfig(t.TempDir(), config.Values{
Launchers: config.Launchers{
IndexRoot: []string{root, filepath.Join(root, "games")},
},
})
require.NoError(t, err)

_, err = hookAmiga(cfg, nil, filepath.Join(stalePath, "listings", "games.txt", "Valid Game"))
require.NoError(t, err)

bootFile, err := os.ReadFile(filepath.Join(validPath, "shared", "ags_boot")) //nolint:gosec // Test temp path
require.NoError(t, err)
assert.Equal(t, "Valid Game\n", string(bootFile))
assert.NoFileExists(t, filepath.Join(stalePath, "shared", "ags_boot"))
}

func writeAmigaVisionTestInstall(t *testing.T, path, game string, withImage bool) {
t.Helper()

err := os.MkdirAll(filepath.Join(path, "listings"), 0o700)
require.NoError(t, err)
err = os.MkdirAll(filepath.Join(path, "shared"), 0o700)
require.NoError(t, err)
err = os.WriteFile(filepath.Join(path, "listings", "games.txt"), []byte(game+"\n"), 0o600)
require.NoError(t, err)
if withImage {
err = os.WriteFile(filepath.Join(path, "AmigaVision.hdf"), []byte("test"), 0o600)
require.NoError(t, err)
}
}

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

Expand Down
71 changes: 69 additions & 2 deletions pkg/platforms/mister/cores/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ along with Zaparoo Core. If not, see <http://www.gnu.org/licenses/>.
package cores

import (
"bufio"
"encoding/hex"
"fmt"
"os"
Expand Down Expand Up @@ -167,14 +168,23 @@ func hookAo486(_ *config.Instance, system *Core, path string) (string, error) {
return mgl, nil
}

func hookAmiga(_ *config.Instance, _ *Core, path string) (string, error) {
func hookAmiga(cfg *config.Instance, _ *Core, path string) (string, error) {
dirPath := strings.ToLower(filepath.Dir(path))
if !strings.HasSuffix(dirPath, "listings/games.txt") && !strings.HasSuffix(dirPath, "listings/demos.txt") {
return "", nil
}

gameName := filepath.Base(path)
sharedPath, err := filepath.Abs(filepath.Join(filepath.Dir(path), "..", "..", "shared"))
installPath := filepath.Clean(filepath.Join(filepath.Dir(path), "..", ".."))
if !hasAmigaVisionBootImage(installPath) {
listingName := filepath.Base(filepath.Dir(path))
activeInstallPath := findAmigaVisionInstallPath(cfg, listingName, gameName)
if activeInstallPath != "" {
installPath = activeInstallPath
}
}

sharedPath, err := filepath.Abs(filepath.Join(installPath, "shared"))
if err != nil {
return "", fmt.Errorf("failed to get absolute path: %w", err)
}
Expand All @@ -187,6 +197,63 @@ func hookAmiga(_ *config.Instance, _ *Core, path string) (string, error) {
return "\t<setname>Amiga</setname>\n", nil
}

func hasAmigaVisionBootImage(path string) bool {
for _, image := range []string{"AmigaVision.hdf", "MegaAGS.hdf"} {
if _, err := os.Stat(filepath.Join(path, image)); err == nil {
return true
}
}
return false
}

func findAmigaVisionInstallPath(cfg *config.Instance, listingName, gameName string) string {
if cfg == nil {
return ""
}

var preferred []string
var other []string
for _, root := range misterconfig.RootDirs(cfg) {
candidate := filepath.Join(root, "Amiga")
if !hasAmigaVisionBootImage(candidate) || !amigaListingContains(candidate, listingName, gameName) {
continue
}
if strings.HasSuffix(strings.ToLower(filepath.Clean(candidate)), filepath.Join("games", "amiga")) {
preferred = append(preferred, candidate)
continue
}
other = append(other, candidate)
}
if len(preferred) > 0 {
return preferred[0]
}
if len(other) > 0 {
return other[0]
}
return ""
}

func amigaListingContains(installPath, listingName, gameName string) bool {
listingPath := filepath.Join(installPath, "listings", listingName)
f, err := os.Open(listingPath) //nolint:gosec // Internal AmigaVision listing path
if err != nil {
return false
}
defer func() {
if closeErr := f.Close(); closeErr != nil {
log.Error().Err(closeErr).Msg("failed to close AmigaVision listing")
}
}()

scanner := bufio.NewScanner(f)
for scanner.Scan() {
if scanner.Text() == gameName {
return true
}
}
return false
Comment on lines +248 to +254
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Handle scanner read errors in listing checks.

On Line 248, bufio.Scanner errors are ignored; this can silently return false on I/O failure and mis-select the install path.

💡 Proposed fix
 scanner := bufio.NewScanner(f)
 for scanner.Scan() {
 	if scanner.Text() == gameName {
 		return true
 	}
 }
+if err := scanner.Err(); err != nil {
+	log.Warn().Err(err).Str("listing_path", listingPath).Msg("failed reading AmigaVision listing")
+}
 return false
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/platforms/mister/cores/hooks.go` around lines 248 - 254, The scanner loop
that searches for gameName (using bufio.NewScanner, scanner.Scan(),
scanner.Text()) currently ignores scanner.Err() and can silently return false on
I/O errors; after the for scanner.Scan() loop, check scanner.Err() and handle it
(return the error or log it and propagate) instead of unconditionally returning
false so calling code can distinguish "not found" from a read failure; update
the function that performs this file listing check to surface or log the scanner
error and only return false when no error occurred and the name wasn't found.

}

func hookNeoGeo(_ *config.Instance, _ *Core, path string) (string, error) {
// neogeo core allows launching zips and folders
if strings.HasSuffix(strings.ToLower(path), ".zip") || filepath.Ext(path) == "" {
Expand Down
82 changes: 82 additions & 0 deletions pkg/platforms/mister/custom_scanners_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ package mister

import (
"context"
"os"
"path/filepath"
"testing"
"time"

Expand Down Expand Up @@ -196,6 +198,86 @@ func TestAmigaScanner_HandlesNoPaths(t *testing.T) {
})
}

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

root := t.TempDir()
stalePath := filepath.Join(root, "Amiga")
validPath := filepath.Join(root, "games", "Amiga")
writeAmigaListings(t, stalePath, "Stale Game")
writeAmigaVisionInstall(t, validPath, "Valid Game")

cfg, err := config.NewConfig(t.TempDir(), config.Values{
Launchers: config.Launchers{
IndexRoot: []string{root, filepath.Join(root, "games")},
},
})
require.NoError(t, err)

p := NewPlatform()
amigaLauncher := findAmigaLauncher(t, p.Launchers(cfg))

results, err := amigaLauncher.Scanner(context.Background(), cfg, "Amiga", nil)
require.NoError(t, err)
require.Len(t, results, 1)
assert.Equal(t, filepath.Join(validPath, "listings", "games.txt", "Valid Game"), results[0].Path)
assert.Equal(t, "Valid Game", results[0].Name)
}

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

root := t.TempDir()
stalePath := filepath.Join(root, "Amiga")
writeAmigaListings(t, stalePath, "Stale Game")

cfg, err := config.NewConfig(t.TempDir(), config.Values{
Launchers: config.Launchers{
IndexRoot: []string{root},
},
})
require.NoError(t, err)

p := NewPlatform()
amigaLauncher := findAmigaLauncher(t, p.Launchers(cfg))

results, err := amigaLauncher.Scanner(context.Background(), cfg, "Amiga", nil)
require.NoError(t, err)
assert.Empty(t, results)
}

func findAmigaLauncher(t *testing.T, launchers []platforms.Launcher) *platforms.Launcher {
t.Helper()

for i := range launchers {
if launchers[i].ID == "Amiga" {
return &launchers[i]
}
}
require.FailNow(t, "Amiga launcher should exist")
return nil
}

func writeAmigaVisionInstall(t *testing.T, path, game string) {
t.Helper()

writeAmigaListings(t, path, game)
err := os.WriteFile(filepath.Join(path, "AmigaVision.hdf"), []byte("test"), 0o600)
require.NoError(t, err)
}

func writeAmigaListings(t *testing.T, path, game string) {
t.Helper()

listingPath := filepath.Join(path, "listings")
err := os.MkdirAll(listingPath, 0o700)
require.NoError(t, err)
err = os.MkdirAll(filepath.Join(path, "shared"), 0o700)
require.NoError(t, err)
err = os.WriteFile(filepath.Join(listingPath, "games.txt"), []byte(game+"\n"), 0o600)
require.NoError(t, err)
}

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

Expand Down
42 changes: 41 additions & 1 deletion pkg/platforms/mister/platform.go
Original file line number Diff line number Diff line change
Expand Up @@ -890,6 +890,41 @@ func filterNeoGeoZipToNeoOnly(results []platforms.ScanResult) []platforms.ScanRe
return filtered
}

func splitAmigaVisionInstallPaths(paths []mediascanner.PathResult) (
preferred []mediascanner.PathResult,
other []mediascanner.PathResult,
) {
preferred = make([]mediascanner.PathResult, 0, len(paths))
other = make([]mediascanner.PathResult, 0, len(paths))

for _, path := range paths {
if !hasAmigaVisionImage(path.Path) {
log.Debug().Str("path", path.Path).Msg("skipping AmigaVision path without boot image")
continue
}
if isPreferredAmigaVisionPath(path.Path) {
preferred = append(preferred, path)
continue
}
other = append(other, path)
}

return preferred, other
}

func hasAmigaVisionImage(path string) bool {
for _, image := range []string{"AmigaVision.hdf", "MegaAGS.hdf"} {
if _, err := os.Stat(filepath.Join(path, image)); err == nil {
return true
}
}
return false
}

func isPreferredAmigaVisionPath(path string) bool {
return strings.HasSuffix(strings.ToLower(filepath.Clean(path)), filepath.Join("games", "amiga"))
}

func (p *Platform) Launchers(cfg *config.Instance) []platforms.Launcher {
// Launchers is invoked from many hot paths (token scans, RPC handlers,
// indexing). The Refresh fast path stats only the snapshot directories
Expand Down Expand Up @@ -936,7 +971,12 @@ func (p *Platform) Launchers(cfg *config.Instance) []platforms.Launcher {
sfs := mediascanner.GetSystemPaths(ctx, cfg, p, p.RootDirs(cfg), []systemdefs.System{*s})
log.Debug().Int("paths", len(sfs)).Msg("amigavision scan paths found")

for _, sf := range sfs {
preferredPaths, otherPaths := splitAmigaVisionInstallPaths(sfs)
validPaths := make([]mediascanner.PathResult, 0, len(preferredPaths)+len(otherPaths))
validPaths = append(validPaths, preferredPaths...)
validPaths = append(validPaths, otherPaths...)

for _, sf := range validPaths {
select {
case <-ctx.Done():
return results, ctx.Err()
Expand Down
Loading