Skip to content

Commit 7919079

Browse files
terafinJustin Terryclaude
authored
fix(n64): don't 422 on Controller Pak images with blank note slots
extractLogicalEntries + countN64ControllerPakEntries iterated every pakfs ReadDirRoot entry and called fsys.Open on its name. Real-world mempaks (e.g. MiSTer-written .cpk) expose blank/uninitialised note slots with empty names, so fsys.Open("") returned "open : invalid argument" and the whole upload was rejected with HTTP 422 ("open controller pak entry \"\""). On a MiSTer SS1 this silently blocked ALL N64 save-sync (uploaded=0, errors=76/cycle). Skip empty-named slots in both functions (and make a single unreadable note non-fatal in extract) so valid Controller Paks sync and blank ones cleanly validate as "no save entries" instead of crashing. Count and extract now agree. Regression test uses a real blank mempak fixture that reproduced the 422. Co-authored-by: Justin Terry <terafin@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 5549005 commit 7919079

3 files changed

Lines changed: 64 additions & 4 deletions

File tree

backend/cmd/server/n64_controller_pak.go

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -389,10 +389,18 @@ func (s *n64ControllerPakStore) extractLogicalEntries(syncLineKey string, payloa
389389
}
390390
root := fsys.ReadDirRoot()
391391
out := make([]n64ControllerPakExtractedEntry, 0, len(root))
392-
for idx, dirEntry := range root {
392+
for _, dirEntry := range root {
393+
// Skip blank/empty note slots: some Controller Pak images expose free or
394+
// uninitialised slots whose name is empty, which pakfs.Open rejects with
395+
// "open : invalid argument". Ignoring them (rather than erroring) stops valid
396+
// .cpk uploads from being rejected with HTTP 422 ("open controller pak entry").
397+
if strings.TrimSpace(dirEntry.Name()) == "" {
398+
continue
399+
}
393400
opened, err := fsys.Open(dirEntry.Name())
394401
if err != nil {
395-
return nil, fmt.Errorf("open controller pak entry %q: %w", dirEntry.Name(), err)
402+
// A single unreadable note must not fail the whole Controller Pak upload.
403+
continue
396404
}
397405
file, ok := opened.(*pakfs.File)
398406
if !ok {
@@ -418,7 +426,7 @@ func (s *n64ControllerPakStore) extractLogicalEntries(syncLineKey string, payloa
418426
GameCode: strings.ToUpper(strings.TrimSpace(gameCode)),
419427
PublisherCode: strings.ToUpper(strings.TrimSpace(publisherCode)),
420428
NoteName: noteName,
421-
EntryIndex: idx + 1,
429+
EntryIndex: len(out) + 1,
422430
PageCount: int((file.Size() + 255) / 256),
423431
BlockUsage: int((file.Size() + 255) / 256),
424432
StructureValid: true,
@@ -448,7 +456,18 @@ func countN64ControllerPakEntries(payload []byte) (int, error) {
448456
if err != nil {
449457
return 0, fmt.Errorf("parse controller pak filesystem: %w", err)
450458
}
451-
return len(fsys.ReadDirRoot()), nil
459+
root := fsys.ReadDirRoot()
460+
count := 0
461+
for _, dirEntry := range root {
462+
// Count only real, named notes — ignore blank/empty slots that
463+
// extractLogicalEntries also skips, so validation and extraction agree
464+
// (a pak with only empty slots counts as 0 → "no save entries", not a
465+
// later HTTP 422 during extraction).
466+
if strings.TrimSpace(dirEntry.Name()) != "" {
467+
count++
468+
}
469+
}
470+
return count, nil
452471
}
453472

454473
func (logical n64ControllerPakLogicalSave) latestRevision() (n64ControllerPakLogicalRevision, bool) {
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package main
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"strings"
7+
"testing"
8+
)
9+
10+
// Regression for the N64 Controller Pak HTTP 422 upload bug: real-world mempaks
11+
// (e.g. a MiSTer-written .cpk) can contain a blank/empty-named note slot that
12+
// pakfs surfaces in ReadDirRoot but cannot Open, which previously made both
13+
// countN64ControllerPakEntries and extractLogicalEntries fail with
14+
// `open controller pak entry "": open : invalid argument` — rejecting the whole
15+
// (valid) save with HTTP 422. Fixture is a real Controller Pak exhibiting this.
16+
func TestN64ControllerPakEmptySlotDoesNotError(t *testing.T) {
17+
buf, err := os.ReadFile(filepath.Join("testdata", "n64_controller_pak_empty_slot.cpk"))
18+
if err != nil {
19+
t.Fatalf("read fixture: %v", err)
20+
}
21+
22+
count, err := countN64ControllerPakEntries(buf)
23+
if err != nil {
24+
t.Fatalf("countN64ControllerPakEntries errored on real mempak with empty slot: %v", err)
25+
}
26+
27+
store, err := newN64ControllerPakStore(t.TempDir())
28+
if err != nil {
29+
t.Fatalf("newN64ControllerPakStore: %v", err)
30+
}
31+
entries, err := store.extractLogicalEntries("synctest", buf)
32+
if err != nil {
33+
if strings.Contains(err.Error(), "open controller pak entry") {
34+
t.Fatalf("regression: extract still fails on empty slot: %v", err)
35+
}
36+
t.Fatalf("extractLogicalEntries errored: %v", err)
37+
}
38+
if len(entries) != count {
39+
t.Fatalf("extract/count disagree: count=%d extracted=%d", count, len(entries))
40+
}
41+
}
32 KB
Binary file not shown.

0 commit comments

Comments
 (0)