Files
fzf/src/tui/light_csi_test.go
T
Junegunn Choi 78d45f26fb End a string sequence at an ESC that does not start ST
An ESC inside a DCS, OSC or APC ends the string and introduces a sequence of
its own. Giving up at that point left the payload to be typed into the query,
and scanning on to a later ST would swallow that following sequence.

https://vt100.net/emu/dec_ansi_parser

Reported by Copilot on #4926.
2026-09-27 21:16:03 +09:00

108 lines
3.3 KiB
Go

package tui
import "testing"
// An unrecognized CSI sequence must be consumed whole. Consuming only part of
// it leaves the rest to be read as input and typed into the query.
func TestUnknownCSISequence(t *testing.T) {
for _, c := range []struct {
sequence string
event EventType
size int
}{
// Key encodings fzf does not implement
{"\x1b[97;5u", Invalid, 7},
{"\x1b[127;5u", Invalid, 8},
{"\x1b[27;5;127~", Invalid, 11},
{"\x1b[57441;1u", Invalid, 10},
{"\x1b\x1b[97;5u", Invalid, 7}, // ALT prefixed, the first ESC is dropped
// Replies to queries fzf did not send, or sent and stopped waiting for
{"\x1b[?1;2c", Invalid, 7},
{"\x1b[>0;95;0c", Invalid, 10},
// Mouse report arriving while mouse input is off
{"\x1b[<0;1;1M", Invalid, 9},
// String sequences: OSC, DCS, APC, PM, SOS
{"\x1b]11;rgb:4a4a/4a4a/4a4a\x1b\\", Invalid, 25}, // background color reply
{"\x1b]0;a title\a", Invalid, 12}, // BEL terminated
{"\x1bP>|kitty(0.48.2)\x1b\\", Invalid, 19}, // XTVERSION reply
{"\x1b_Gi=1;OK\x1b\\", Invalid, 11},
{"\x1bP\ax\x1b\\", Invalid, 6}, // BEL inside a DCS payload is not a terminator
{"\x1b]foo\x1bX\x1b\\", Invalid, 5}, // ESC aborts the string, consuming only "\e]foo" // kitty graphics reply
// Left alone: this is how ALT-[ and ALT-] arrive
{"\x1b[a", Alt, 2},
{"\x1b]abc", Alt, 2},
{"\x1b]11;rgb:", Alt, 2}, // terminator has not arrived
// SOS and PM are not framed, so ALT-X and ALT-^ are not delayed
{"\x1bXsos\x1b\\", Alt, 2},
{"\x1b^status\x1b\\", Alt, 2},
// Left alone: no final byte yet, so the sequence may still be arriving
{"\x1b[", Invalid, 2},
// Recognized sequences keep their existing parsing
{"\x1b[1;5A", CtrlUp, 6},
{"\x1b[3;5~", CtrlDelete, 6},
{"\x1b[2~", Insert, 4},
{"\x1b[200~", BracketedPasteBegin, 6},
{"\x1b[Z", ShiftTab, 3},
{"\x1bOA", Up, 3},
{"\x1b[12;34R", Invalid, 8},
{"\x1b[?2004;2$y", Invalid, 11},
} {
r := &LightRenderer{buffer: []byte(c.sequence)}
sz := 1
event := r.escSequence(&sz)
if event.Type != c.event {
t.Errorf("escSequence(%q) = %s, want %s",
c.sequence, event.Type.String(), c.event.String())
}
if sz != c.size {
t.Errorf("escSequence(%q) consumed %d bytes, want %d", c.sequence, sz, c.size)
}
}
}
func TestStringEnd(t *testing.T) {
for _, c := range []struct {
buffer string
want int
}{
{"\x1b]0;t\a", 6},
{"\x1bP\ax\x1b\\", 6}, // BEL in the payload ignored, ST ends it
{"\x1bP\a", 0}, // BEL does not terminate a DCS
{"\x1b_Gi=1\a", 0}, // nor an APC
{"\x1b]0;t\x1b\\", 7},
{"\x1b_G\x1b\\", 5},
{"\x1b]0;t", 0}, // no terminator
{"\x1b]0;t\x1b", 0}, // terminator half arrived
} {
if got := stringEnd([]byte(c.buffer)); got != c.want {
t.Errorf("stringEnd(%q) = %d, want %d", c.buffer, got, c.want)
}
}
}
func TestCsiEnd(t *testing.T) {
for _, c := range []struct {
buffer string
want int
}{
{"\x1b[97;5u", 7},
{"\x1b[A", 3},
{"\x1b[<0;1;1M", 9},
{"\x1b[?2004;2$y", 11},
{"\x1b[97;5", 0}, // no final byte
{"\x1b[", 0}, // no final byte
{"\x1b[1\x01A", 0}, // malformed, do not frame it
} {
if got := csiEnd([]byte(c.buffer)); got != c.want {
t.Errorf("csiEnd(%q) = %d, want %d", c.buffer, got, c.want)
}
}
}