mirror of
https://github.com/junegunn/fzf.git
synced 2026-10-10 15:25:59 +08:00
Check escape sequence bytes before counting them (#4929)
The parser decided how many bytes a key used before checking them, so a key that arrived in the same read was lost or misread. - Recognize F1-F8 and modified Delete, PageUp and PageDown when more input follows them, as with a paste or tmux send-keys. They required the sequence to be the whole buffer. - After \e[ and a digit, do not consume a byte that cannot continue the sequence. ALT-[, 2 and Enter lost the Enter. - Report Home and End for \e[7, \e[4 and \e[8 only before ~, $, ^ or @. ALT-[, 4 and x fired End and lost the x. - Look for the end of a mouse report within its frame. A later M swallowed the keys before it, and repeated \e[< took quadratic time.
This commit is contained in:
1 parent
c009c106b9
commit
b563e208d6
4 files changed
+75
-14
No files matched your search
@@ -5,6 +5,7 @@ CHANGELOG
|
||||
------
|
||||
- Fixed `--gap-line` cutting a grapheme cluster when filling the last cells of the line (#4920)
|
||||
- Fixed an escape sequence fzf does not recognize being partly consumed, which leaked the rest into the query (#4926)
|
||||
- Fixed a key lost when it arrived in the same read as an escape sequence
|
||||
|
||||
0.74.4
|
||||
------
|
||||
|
||||
+32
-13
@@ -400,6 +400,12 @@ func stringEnd(buffer []byte) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
// rxvtKeyEnd reports whether the byte ends a key such as \e[7~ for Home.
|
||||
// rxvt marks modifiers with the final byte: $ for SHIFT, ^ for CTRL, @ for both.
|
||||
func rxvtKeyEnd(b byte) bool {
|
||||
return b == '~' || b == '$' || b == '^' || b == '@'
|
||||
}
|
||||
|
||||
// csiIntroducer reports whether the byte after ESC starts a sequence that is
|
||||
// framed by a final byte. The parser routes CSI and SS3 through the same
|
||||
// parameterized subcases, so both are framed by csiEnd.
|
||||
@@ -747,6 +753,11 @@ func (r *LightRenderer) parseEscSequence(sz *int) Event {
|
||||
if len(r.buffer) < 4 {
|
||||
return Event{Invalid, 0, nil}
|
||||
}
|
||||
// A byte that cannot continue the sequence, such as Enter after
|
||||
// ALT-[ and a digit, is a key of its own
|
||||
if csiEnd(r.buffer) < 0 {
|
||||
break
|
||||
}
|
||||
*sz = 4
|
||||
switch r.buffer[2] {
|
||||
case '2':
|
||||
@@ -779,7 +790,7 @@ func (r *LightRenderer) parseEscSequence(sz *int) Event {
|
||||
if r.buffer[3] == '~' {
|
||||
return Event{Delete, 0, nil}
|
||||
}
|
||||
if len(r.buffer) == 7 && r.buffer[6] == '~' && r.buffer[4] == '1' {
|
||||
if len(r.buffer) > 6 && r.buffer[6] == '~' && r.buffer[4] == '1' {
|
||||
*sz = 7
|
||||
switch r.buffer[5] {
|
||||
case '0':
|
||||
@@ -798,7 +809,7 @@ func (r *LightRenderer) parseEscSequence(sz *int) Event {
|
||||
return Event{CtrlAltShiftDelete, 0, nil}
|
||||
}
|
||||
}
|
||||
if len(r.buffer) == 6 && r.buffer[5] == '~' {
|
||||
if len(r.buffer) > 5 && r.buffer[5] == '~' {
|
||||
*sz = 6
|
||||
switch r.buffer[4] {
|
||||
case '2':
|
||||
@@ -821,12 +832,14 @@ func (r *LightRenderer) parseEscSequence(sz *int) Event {
|
||||
}
|
||||
return Event{Invalid, 0, nil}
|
||||
case '4':
|
||||
return Event{End, 0, nil}
|
||||
if rxvtKeyEnd(r.buffer[3]) {
|
||||
return Event{End, 0, nil}
|
||||
}
|
||||
case '5':
|
||||
if r.buffer[3] == '~' {
|
||||
return Event{PageUp, 0, nil}
|
||||
}
|
||||
if len(r.buffer) == 7 && r.buffer[6] == '~' && r.buffer[4] == '1' {
|
||||
if len(r.buffer) > 6 && r.buffer[6] == '~' && r.buffer[4] == '1' {
|
||||
*sz = 7
|
||||
switch r.buffer[5] {
|
||||
case '0':
|
||||
@@ -845,7 +858,7 @@ func (r *LightRenderer) parseEscSequence(sz *int) Event {
|
||||
return Event{CtrlAltShiftPageUp, 0, nil}
|
||||
}
|
||||
}
|
||||
if len(r.buffer) == 6 && r.buffer[5] == '~' {
|
||||
if len(r.buffer) > 5 && r.buffer[5] == '~' {
|
||||
*sz = 6
|
||||
switch r.buffer[4] {
|
||||
case '2':
|
||||
@@ -871,7 +884,7 @@ func (r *LightRenderer) parseEscSequence(sz *int) Event {
|
||||
if r.buffer[3] == '~' {
|
||||
return Event{PageDown, 0, nil}
|
||||
}
|
||||
if len(r.buffer) == 7 && r.buffer[6] == '~' && r.buffer[4] == '1' {
|
||||
if len(r.buffer) > 6 && r.buffer[6] == '~' && r.buffer[4] == '1' {
|
||||
*sz = 7
|
||||
switch r.buffer[5] {
|
||||
case '0':
|
||||
@@ -890,7 +903,7 @@ func (r *LightRenderer) parseEscSequence(sz *int) Event {
|
||||
return Event{CtrlAltShiftPageDown, 0, nil}
|
||||
}
|
||||
}
|
||||
if len(r.buffer) == 6 && r.buffer[5] == '~' {
|
||||
if len(r.buffer) > 5 && r.buffer[5] == '~' {
|
||||
*sz = 6
|
||||
switch r.buffer[4] {
|
||||
case '2':
|
||||
@@ -913,15 +926,19 @@ func (r *LightRenderer) parseEscSequence(sz *int) Event {
|
||||
}
|
||||
return Event{Invalid, 0, nil}
|
||||
case '7':
|
||||
return Event{Home, 0, nil}
|
||||
if rxvtKeyEnd(r.buffer[3]) {
|
||||
return Event{Home, 0, nil}
|
||||
}
|
||||
case '8':
|
||||
return Event{End, 0, nil}
|
||||
if rxvtKeyEnd(r.buffer[3]) {
|
||||
return Event{End, 0, nil}
|
||||
}
|
||||
case '1':
|
||||
switch r.buffer[3] {
|
||||
case '~':
|
||||
return Event{Home, 0, nil}
|
||||
case '1', '2', '3', '4', '5', '7', '8', '9':
|
||||
if len(r.buffer) == 5 && r.buffer[4] == '~' {
|
||||
if len(r.buffer) > 4 && r.buffer[4] == '~' {
|
||||
*sz = 5
|
||||
switch r.buffer[3] {
|
||||
case '1':
|
||||
@@ -1162,11 +1179,13 @@ func (r *LightRenderer) mouseSequence(sz *int) Event {
|
||||
return Event{Invalid, 0, nil}
|
||||
}
|
||||
|
||||
rest := r.buffer[*sz:]
|
||||
end := bytes.IndexAny(rest, "mM")
|
||||
if end == -1 {
|
||||
// Not past the frame, where M or m can be a key typed later
|
||||
frame := csiEnd(r.buffer)
|
||||
if frame <= 0 || r.buffer[frame-1] != 'm' && r.buffer[frame-1] != 'M' {
|
||||
return Event{Invalid, 0, nil}
|
||||
}
|
||||
rest := r.buffer[*sz:]
|
||||
end := frame - 1 - *sz
|
||||
|
||||
elems := strings.SplitN(string(rest[:end]), ";", 3)
|
||||
if len(elems) != 3 {
|
||||
|
||||
@@ -231,3 +231,44 @@ func TestCsiEnd(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A key that follows a sequence in the same read must not be consumed with it
|
||||
func TestKeyAfterSequence(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
sequence string
|
||||
mouse bool
|
||||
event EventType
|
||||
size int
|
||||
}{
|
||||
// Recognized when more input follows, as from a paste or tmux send-keys
|
||||
{"\x1b[15~x", false, F5, 5},
|
||||
{"\x1b[3;5~x", false, CtrlDelete, 6},
|
||||
{"\x1b[5;13~x", false, CtrlAltPageUp, 7},
|
||||
{"\x1b[6;2~\x1b[A", false, ShiftPageDown, 6},
|
||||
|
||||
// ALT-[ or ALT-O and a digit, then a key that cannot continue a sequence
|
||||
{"\x1b[2\r", false, Alt, 2},
|
||||
{"\x1b[12\x03", false, Alt, 2},
|
||||
{"\x1bO2\a", false, Alt, 2},
|
||||
{"\x1b[4x", false, Alt, 2}, // not End
|
||||
{"\x1b[7\r", false, Alt, 2}, // not Home
|
||||
{"\x1b[7~x", false, Home, 4},
|
||||
{"\x1b[8^x", false, End, 4}, // rxvt CTRL-END
|
||||
|
||||
// A mouse report ends at its own final byte
|
||||
{"\x1b[<0;5;3Mx", true, Mouse, 9},
|
||||
{"\x1b[<0;1;\x1b[AM", true, Invalid, 3},
|
||||
{"\x1b[<0;1;1xM", true, Invalid, 9},
|
||||
} {
|
||||
r := &LightRenderer{buffer: []byte(c.sequence), mouse: c.mouse}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,7 +66,6 @@ func TestLightRenderer(t *testing.T) {
|
||||
assertEscSequence("\x1b[1;1R", "!Invalid")
|
||||
assertEscSequence("\x1b[", "!Invalid")
|
||||
assertEscSequence("\x1b[1", "!Invalid")
|
||||
assertEscSequence("\x1b[3;3~1", "!Invalid")
|
||||
assertEscSequence("\x1b[13", "!Invalid")
|
||||
assertEscSequence("\x1b[1;3", "!Invalid")
|
||||
assertEscSequence("\x1b[1;10", "!Invalid")
|
||||
@@ -200,6 +199,7 @@ func TestLightRenderer(t *testing.T) {
|
||||
assertEscSequence("\x1b[1;3H", "alt-home")
|
||||
assertEscSequence("\x1b[1;3F", "alt-end")
|
||||
assertEscSequence("\x1b[3;3~", "alt-delete")
|
||||
assertEscSequence("\x1b[3;3~1", "alt-delete") // then 1
|
||||
assertEscSequence("\x1b[5;3~", "alt-page-up")
|
||||
assertEscSequence("\x1b[6;3~", "alt-page-down")
|
||||
|
||||
|
||||
Reference in new issue
Block a user