From cd0f960466b70aaa5c081a9dfa85bf4d8ffa3b98 Mon Sep 17 00:00:00 2001 From: Liam Galvin Date: Sun, 15 Mar 2026 16:31:08 +0000 Subject: [PATCH] fix: Handle panic in ReadKey() --- internal/core/keys.go | 41 ++++++++++++++----- internal/core/keys_test.go | 82 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 10 deletions(-) create mode 100644 internal/core/keys_test.go diff --git a/internal/core/keys.go b/internal/core/keys.go index fc37eb0..74e1ea2 100644 --- a/internal/core/keys.go +++ b/internal/core/keys.go @@ -264,17 +264,38 @@ func (k *Keys) ReadKey() (key rune, isAbort bool) { k.mutex.RUnlock() }() - switch { - case len(k.macroKeys) > 0: - key = k.macroKeys[0] - k.macroKeys = k.macroKeys[1:] + for { + switch { + case len(k.macroKeys) > 0: + key = k.macroKeys[0] + k.macroKeys = k.macroKeys[1:] + case k.waiting: + buf := <-k.keysOnce + if len(buf) == 0 { + if k.eof { + return 0, true + } + continue + } + key = []rune(string(buf))[0] + default: + buf, err := k.readInputFiltered() + if err != nil { + if errors.Is(err, io.EOF) { + k.eof = true + } + return 0, true + } + if len(buf) == 0 { + if k.eof { + return 0, true + } + continue + } + key = []rune(string(buf))[0] + } - case k.waiting: - buf := <-k.keysOnce - key = []rune(string(buf))[0] - default: - buf, _ := k.readInputFiltered() - key = []rune(string(buf))[0] + break } // Always mark those keys as matched, so that diff --git a/internal/core/keys_test.go b/internal/core/keys_test.go new file mode 100644 index 0000000..ddfcf44 --- /dev/null +++ b/internal/core/keys_test.go @@ -0,0 +1,82 @@ +package core + +import ( + "io" + "testing" +) + +type readStep struct { + data []byte + err error +} + +type stubReadCloser struct { + steps []readStep + index int +} + +func (s *stubReadCloser) Read(p []byte) (int, error) { + if s.index >= len(s.steps) { + return 0, io.EOF + } + + step := s.steps[s.index] + s.index++ + + copy(p, step.data) + + return len(step.data), step.err +} + +func (s *stubReadCloser) Close() error { + return nil +} + +func TestKeysReadKeySkipsEmptyReads(t *testing.T) { + originalStdin := Stdin + Stdin = &stubReadCloser{ + steps: []readStep{ + {}, + {data: []byte("x")}, + }, + } + t.Cleanup(func() { + Stdin = originalStdin + }) + + keys := &Keys{} + + key, isAbort := keys.ReadKey() + + if isAbort { + t.Fatal("expected ReadKey to continue after an empty read") + } + + if key != 'x' { + t.Fatalf("expected key %q, got %q", 'x', key) + } +} + +func TestKeysReadKeyReturnsAbortOnEOF(t *testing.T) { + originalStdin := Stdin + Stdin = &stubReadCloser{ + steps: []readStep{ + {err: io.EOF}, + }, + } + t.Cleanup(func() { + Stdin = originalStdin + }) + + keys := &Keys{} + + key, isAbort := keys.ReadKey() + + if !isAbort { + t.Fatal("expected ReadKey to abort on EOF") + } + + if key != 0 { + t.Fatalf("expected zero key on EOF, got %q", key) + } +}