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
27 changes: 23 additions & 4 deletions backend/cmd/server/n64_controller_pak.go
Original file line number Diff line number Diff line change
Expand Up @@ -389,10 +389,18 @@ func (s *n64ControllerPakStore) extractLogicalEntries(syncLineKey string, payloa
}
root := fsys.ReadDirRoot()
out := make([]n64ControllerPakExtractedEntry, 0, len(root))
for idx, dirEntry := range root {
for _, dirEntry := range root {
// Skip blank/empty note slots: some Controller Pak images expose free or
// uninitialised slots whose name is empty, which pakfs.Open rejects with
// "open : invalid argument". Ignoring them (rather than erroring) stops valid
// .cpk uploads from being rejected with HTTP 422 ("open controller pak entry").
if strings.TrimSpace(dirEntry.Name()) == "" {
continue
}
opened, err := fsys.Open(dirEntry.Name())
if err != nil {
return nil, fmt.Errorf("open controller pak entry %q: %w", dirEntry.Name(), err)
// A single unreadable note must not fail the whole Controller Pak upload.
continue
}
file, ok := opened.(*pakfs.File)
if !ok {
Expand All @@ -418,7 +426,7 @@ func (s *n64ControllerPakStore) extractLogicalEntries(syncLineKey string, payloa
GameCode: strings.ToUpper(strings.TrimSpace(gameCode)),
PublisherCode: strings.ToUpper(strings.TrimSpace(publisherCode)),
NoteName: noteName,
EntryIndex: idx + 1,
EntryIndex: len(out) + 1,
PageCount: int((file.Size() + 255) / 256),
BlockUsage: int((file.Size() + 255) / 256),
StructureValid: true,
Expand Down Expand Up @@ -448,7 +456,18 @@ func countN64ControllerPakEntries(payload []byte) (int, error) {
if err != nil {
return 0, fmt.Errorf("parse controller pak filesystem: %w", err)
}
return len(fsys.ReadDirRoot()), nil
root := fsys.ReadDirRoot()
count := 0
for _, dirEntry := range root {
// Count only real, named notes — ignore blank/empty slots that
// extractLogicalEntries also skips, so validation and extraction agree
// (a pak with only empty slots counts as 0 → "no save entries", not a
// later HTTP 422 during extraction).
if strings.TrimSpace(dirEntry.Name()) != "" {
count++
}
}
return count, nil
}

func (logical n64ControllerPakLogicalSave) latestRevision() (n64ControllerPakLogicalRevision, bool) {
Expand Down
41 changes: 41 additions & 0 deletions backend/cmd/server/n64_controller_pak_emptyslot_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package main

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

// Regression for the N64 Controller Pak HTTP 422 upload bug: real-world mempaks
// (e.g. a MiSTer-written .cpk) can contain a blank/empty-named note slot that
// pakfs surfaces in ReadDirRoot but cannot Open, which previously made both
// countN64ControllerPakEntries and extractLogicalEntries fail with
// `open controller pak entry "": open : invalid argument` — rejecting the whole
// (valid) save with HTTP 422. Fixture is a real Controller Pak exhibiting this.
func TestN64ControllerPakEmptySlotDoesNotError(t *testing.T) {
buf, err := os.ReadFile(filepath.Join("testdata", "n64_controller_pak_empty_slot.cpk"))
if err != nil {
t.Fatalf("read fixture: %v", err)
}

count, err := countN64ControllerPakEntries(buf)
if err != nil {
t.Fatalf("countN64ControllerPakEntries errored on real mempak with empty slot: %v", err)
}

store, err := newN64ControllerPakStore(t.TempDir())
if err != nil {
t.Fatalf("newN64ControllerPakStore: %v", err)
}
entries, err := store.extractLogicalEntries("synctest", buf)
if err != nil {
if strings.Contains(err.Error(), "open controller pak entry") {
t.Fatalf("regression: extract still fails on empty slot: %v", err)
}
t.Fatalf("extractLogicalEntries errored: %v", err)
}
if len(entries) != count {
t.Fatalf("extract/count disagree: count=%d extracted=%d", count, len(entries))
}
}
Binary file not shown.