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
79 changes: 71 additions & 8 deletions pkg/platforms/mister/cores/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,12 +175,32 @@ func hookAmiga(cfg *config.Instance, _ *Core, path string) (string, error) {
}

gameName := filepath.Base(path)
listingName := filepath.Base(filepath.Dir(path))
installPath := filepath.Clean(filepath.Join(filepath.Dir(path), "..", ".."))
if !hasAmigaVisionBootImage(installPath) {
listingName := filepath.Base(filepath.Dir(path))
bootImagePath := amigaVisionBootImagePath(installPath)
bootImageFound := bootImagePath != ""
log.Debug().
Str("path", path).
Str("game", gameName).
Str("listing", listingName).
Str("candidate_install_path", installPath).
Str("candidate_boot_image", bootImagePath).
Bool("candidate_has_boot_image", bootImageFound).
Msg("AmigaVision launch hook detected listing entry")
if !bootImageFound {
activeInstallPath := findAmigaVisionInstallPath(cfg, listingName, gameName)
if activeInstallPath != "" {
log.Debug().
Str("candidate_install_path", installPath).
Str("active_install_path", activeInstallPath).
Msg("AmigaVision launch hook using active install path")
installPath = activeInstallPath
} else {
log.Warn().
Str("candidate_install_path", installPath).
Str("game", gameName).
Str("listing", listingName).
Msg("AmigaVision launch hook could not find install path with boot image")
}
}

Expand All @@ -191,31 +211,64 @@ func hookAmiga(cfg *config.Instance, _ *Core, path string) (string, error) {

bootFile := filepath.Join(sharedPath, "ags_boot")
if err := os.WriteFile(bootFile, []byte(gameName+"\n"), 0o600); err != nil {
log.Error().Err(err).
Str("install_path", installPath).
Str("shared_path", sharedPath).
Str("boot_file", bootFile).
Str("game", gameName).
Msg("failed to write AmigaVision boot file")
return "", fmt.Errorf("failed to write boot file: %w", err)
}
log.Info().
Str("install_path", installPath).
Str("shared_path", sharedPath).
Str("boot_file", bootFile).
Str("game", gameName).
Msg("AmigaVision boot file written")

return "\t<setname>Amiga</setname>\n", nil
}

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

func findAmigaVisionInstallPath(cfg *config.Instance, listingName, gameName string) string {
if cfg == nil {
log.Debug().Msg("AmigaVision install search skipped: config is nil")
return ""
}

var preferred []string
var other []string
for _, root := range misterconfig.RootDirs(cfg) {
roots := misterconfig.RootDirs(cfg)
log.Debug().
Strs("roots", roots).
Str("listing", listingName).
Str("game", gameName).
Msg("searching for AmigaVision install path")
for _, root := range roots {
candidate := filepath.Join(root, "Amiga")
if !hasAmigaVisionBootImage(candidate) || !amigaListingContains(candidate, listingName, gameName) {
bootImagePath := amigaVisionBootImagePath(candidate)
hasBootImage := bootImagePath != ""
listingContainsGame := false
if hasBootImage {
listingContainsGame = amigaListingContains(candidate, listingName, gameName)
}
log.Debug().
Str("root", root).
Str("candidate", candidate).
Str("boot_image", bootImagePath).
Bool("has_boot_image", hasBootImage).
Bool("listing_contains_game", listingContainsGame).
Msg("checked AmigaVision install candidate")
if !hasBootImage || !listingContainsGame {
continue
}
if strings.HasSuffix(strings.ToLower(filepath.Clean(candidate)), filepath.Join("games", "amiga")) {
Expand All @@ -225,18 +278,25 @@ func findAmigaVisionInstallPath(cfg *config.Instance, listingName, gameName stri
other = append(other, candidate)
}
if len(preferred) > 0 {
log.Debug().Str("install_path", preferred[0]).Msg("selected preferred AmigaVision install path")
return preferred[0]
}
if len(other) > 0 {
log.Debug().Str("install_path", other[0]).Msg("selected fallback AmigaVision install path")
return other[0]
}
log.Warn().
Str("listing", listingName).
Str("game", gameName).
Msg("no AmigaVision install path matched listing entry")
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 {
log.Debug().Err(err).Str("listing_path", listingPath).Msg("failed to open AmigaVision listing")
return false
}
defer func() {
Expand All @@ -251,6 +311,9 @@ func amigaListingContains(installPath, listingName, gameName string) bool {
return true
}
}
if err := scanner.Err(); err != nil {
log.Warn().Err(err).Str("listing_path", listingPath).Msg("failed to scan AmigaVision listing")
}
return false
}

Expand Down
32 changes: 27 additions & 5 deletions pkg/platforms/mister/mgls/mgls.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"os"
"path/filepath"
s "strings"
"unicode"

"github.com/ZaparooProject/zaparoo-core/v2/pkg/config"
"github.com/ZaparooProject/zaparoo-core/v2/pkg/platforms"
Expand Down Expand Up @@ -144,7 +145,21 @@ func writeTempFile(content string) (string, error) {
return tmpFile.Name(), nil
}

func validateLoadCorePath(path string) error {
for _, r := range path {
if unicode.IsControl(r) {
return fmt.Errorf("load_core path contains control character: %q", path)
}
}
return nil
}

func launchFile(path string) error {
validationErr := validateLoadCorePath(path)
if validationErr != nil {
return validationErr
}

_, err := os.Stat(misterconfig.CmdInterface)
if err != nil {
return fmt.Errorf("command interface not accessible: %w", err)
Expand All @@ -166,9 +181,11 @@ func launchFile(path string) error {
}
}()

if _, err := fmt.Fprintf(cmd, "load_core %s\n", path); err != nil {
command := "load_core " + path
if _, err := fmt.Fprintln(cmd, command); err != nil {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return fmt.Errorf("failed to write to command interface: %w", err)
}
log.Info().Str("command", command).Msg("command interface launch request sent")

return nil
}
Expand Down Expand Up @@ -299,10 +316,6 @@ func LaunchGame(cfg *config.Instance, system *cores.Core, path string) error {

// LaunchCore Launch a core given a possibly partial path, as per MGL files.
func LaunchCore(cfg *config.Instance, _ platforms.Platform, system *cores.Core) error {
if _, err := os.Stat(misterconfig.CmdInterface); err != nil {
return fmt.Errorf("command interface not accessible: %w", err)
}

if system.SetName != "" {
return LaunchGame(cfg, system, "")
}
Expand All @@ -312,6 +325,15 @@ func LaunchCore(cfg *config.Instance, _ platforms.Platform, system *cores.Core)
return fmt.Errorf("resolving core RBF: %w", err)
}
path := rbfInfo.Path
validationErr := validateLoadCorePath(path)
if validationErr != nil {
return validationErr
}

_, statErr := os.Stat(misterconfig.CmdInterface)
if statErr != nil {
return fmt.Errorf("command interface not accessible: %w", statErr)
}

cmd, err := os.OpenFile(misterconfig.CmdInterface, os.O_RDWR, 0)
if err != nil {
Expand Down
68 changes: 68 additions & 0 deletions pkg/platforms/mister/mgls/mgls_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
package mgls

import (
"fmt"
"os"
"path/filepath"
"testing"
Expand Down Expand Up @@ -150,6 +151,73 @@ func TestReadMRA_EmptyFile(t *testing.T) {
require.Error(t, err)
}

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

basePath := filepath.Join("media", "fat", ".LASTLAUNCH.mgl")
tests := []struct {
name string
path string
wantErr bool
}{
{name: "valid path", path: basePath},
{name: "newline rejected", path: basePath + "\nload_core " + filepath.Join("tmp", "evil.rbf"), wantErr: true},
{name: "carriage return rejected", path: basePath + "\r", wantErr: true},
{name: "tab rejected", path: basePath + "\t", wantErr: true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

err := validateLoadCorePath(tt.path)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
})
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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

basePath := filepath.Join("media", "fat", ".LASTLAUNCH.mgl")
for _, path := range []string{basePath + "\n", basePath + "\r", basePath + "\t"} {
t.Run(path, func(t *testing.T) {
t.Parallel()

err := launchFile(path)
require.EqualError(t, err, fmt.Sprintf("load_core path contains control character: %q", path))
})
}
}

func TestLaunchCoreRejectsControlCharacters(t *testing.T) {
oldCache := cores.GlobalRBFCache
t.Cleanup(func() {
cores.GlobalRBFCache = oldCache
})

basePath := filepath.Join("media", "fat", "_Console", "NES.rbf")
for _, path := range []string{basePath + "\n", basePath + "\r", basePath + "\t"} {
t.Run(path, func(t *testing.T) {
cache := &cores.RBFCache{}
cache.BuildFromRBFs([]cores.RBFInfo{{
Path: path,
Filename: "NES.rbf",
ShortName: "NES",
MglName: filepath.Join("_Console", "NES"),
}})
cores.GlobalRBFCache = cache

err := LaunchCore(nil, nil, &cores.Core{ID: "NES"})
require.EqualError(t, err, fmt.Sprintf("load_core path contains control character: %q", path))
})
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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

Expand Down
Loading