Wait for a string terminator split across reads

A trailing ESC was read as a lone ESC, so the read loop stopped waiting and
the parser was handed an unterminated sequence, whose payload was typed into
the query. With ESCDELAY=0 nothing covered it.

Reported by Copilot on #4926.
This commit is contained in:
Junegunn Choi
2026-09-27 21:03:07 +09:00
parent b05683edf0
commit 9c667afb73
2 changed files with 20 additions and 0 deletions
+8
View File
@@ -414,6 +414,14 @@ func incompleteEscape(buffer []byte) bool {
tail = tail[len(tail)-escapeLookback:]
}
start := bytes.LastIndexByte(tail, Esc.Byte())
// A trailing ESC can be the first half of a string terminator. Reading it as
// a lone ESC ends the wait and hands the parser an unterminated sequence, so
// fall back to the one this ESC would have terminated.
if start == len(tail)-1 && start > 0 {
if prev := bytes.LastIndexByte(tail[:start], Esc.Byte()); prev >= 0 {
start = prev
}
}
if start < 0 || len(tail)-start < 2 {
return false
}
+12
View File
@@ -65,6 +65,18 @@ func TestIncompleteStringEscape(t *testing.T) {
{"\x1b]0;title\x1b\\", false},
{"\x1bP>|kitty(0.48.2)\x1b\\", false},
{"ab\x1b]11;rgb:", true},
// A string terminator split across reads: the ESC has arrived, the
// backslash has not
{"\x1b]0;t\x1b", true},
{"\x1bP>|kitty\x1b", true},
{"\x1b_G\x1b", true},
// Complete sequence followed by a lone ESC, which is the ESC key
{"\x1b]0;title\a\x1b", false},
{"\x1b\x1b", false},
{"\x1ba\x1b", false},
{"\x1b[A\x1b", false},
} {
if got := incompleteEscape([]byte(c.buffer)); got != c.want {
t.Errorf("incompleteEscape(%q) = %v, want %v", c.buffer, got, c.want)