Stop parsing invalid CSI/SS3 sequences as alt-[/alt-o

This situation can be triggered in practice inside a terminal like tmux
3.5 by running 

	tmux new-session fish -C 'sleep 2' -d reader -o log-file

and typing "alt-escape x"

The log shows that we drop treat this as alt-[ and drop  the x on the floor.

	reader: Read char alt-\[ -- Key { modifiers: Modifiers { ctrl: false,
	alt: true, shift: false }, codepoint: '[' } -- [27, 91, 120]

This input ("\e[x") is ambiguous.

It looks like it could mean "alt-[,x".  However that conflicts with a
potential future CSI code, so it makes no sense to try to support this.

Returning "None" from parse_csi() causes this weird behavior of
returning "alt-[" and dropping the rest of the parsed sequence.
This is too easy; it has even crept into a bunch of places
where the input sequence is actually valid like "VT200 button released"
but where we definitely don't want to report any key.

Fix the default: report no key for all unknown sequences and
intentionally-suppressed sequences.  Treat it at "alt-[" only when
there is no input byte available, which is more or less unambiguous,
hence a strong enough signal that this is a actually "alt-[".
This commit is contained in:
Johannes Altmanninger 2024-12-30 07:47:54 +01:00
parent e1e963ae66
commit 3201cb9f01

@ -10,8 +10,8 @@ use crate::flog::{FloggableDebug, FLOG};
use crate::fork_exec::flog_safe::FLOG_SAFE;
use crate::global_safety::RelaxedAtomicBool;
use crate::key::{
self, alt, canonicalize_control_char, canonicalize_keyed_control_char, function_key, shift,
Key, Modifiers, ViewportPosition,
self, alt, canonicalize_control_char, canonicalize_keyed_control_char, ctrl, function_key,
shift, Key, Modifiers, ViewportPosition,
};
use crate::reader::{reader_current_data, reader_test_and_clear_interrupted};
use crate::threads::{iothread_port, is_main_thread};
@ -773,6 +773,7 @@ pub trait InputEventQueuer {
}
return None;
};
let invalid = Key::from_raw(key::Invalid);
if buffer.len() == 2 && next == b'\x1b' {
return Some(
match self.parse_escape_sequence(buffer, have_escape_prefix) {
@ -780,17 +781,17 @@ pub trait InputEventQueuer {
nested_sequence.modifiers.alt = true;
nested_sequence
}
None => Key::from_raw(key::Invalid),
None => invalid,
},
);
}
if next == b'[' {
// potential CSI
return Some(self.parse_csi(buffer).unwrap_or(alt('[')));
return Some(self.parse_csi(buffer).unwrap_or(invalid));
}
if next == b'O' {
// potential SS3
return Some(self.parse_ss3(buffer).unwrap_or(alt('O')));
return Some(self.parse_ss3(buffer).unwrap_or(invalid));
}
match canonicalize_control_char(next) {
Some(mut key) => {
@ -872,10 +873,12 @@ pub trait InputEventQueuer {
}
fn parse_csi(&mut self, buffer: &mut Vec<u8>) -> Option<Key> {
let mut next_char = |zelf: &mut Self| zelf.try_readb(buffer).unwrap_or(0xff);
// The maximum number of CSI parameters is defined by NPAR, nominally 16.
let mut params = [[0_u32; 4]; 16];
let mut c = next_char(self);
let Some(mut c) = self.try_readb(buffer) else {
return Some(ctrl('['));
};
let mut next_char = |zelf: &mut Self| zelf.try_readb(buffer).unwrap_or(0xff);
let private_mode;
if matches!(c, b'?' | b'<' | b'=' | b'>') {
// private mode
@ -1063,11 +1066,11 @@ pub trait InputEventQueuer {
} // rxvt style
200 => {
self.paste_start_buffering();
return Some(Key::from_raw(key::Invalid));
return None;
}
201 => {
self.paste_commit();
return Some(Key::from_raw(key::Invalid));
return None;
}
_ => return None,
},
@ -1113,11 +1116,11 @@ pub trait InputEventQueuer {
b'Z' => shift(key::Tab),
b'I' => {
self.push_front(CharEvent::Implicit(ImplicitEvent::FocusIn));
return Some(Key::from_raw(key::Invalid));
return None;
}
b'O' => {
self.push_front(CharEvent::Implicit(ImplicitEvent::FocusOut));
return Some(Key::from_raw(key::Invalid));
return None;
}
_ => return None,
};
@ -1140,13 +1143,12 @@ pub trait InputEventQueuer {
fn parse_ss3(&mut self, buffer: &mut Vec<u8>) -> Option<Key> {
let mut raw_mask = 0;
let mut code = b'0';
loop {
let Some(mut code) = self.try_readb(buffer) else {
return Some(alt('O'));
};
while (b'0'..=b'9').contains(&code) {
raw_mask = raw_mask * 10 + u32::from(code - b'0');
code = self.try_readb(buffer).unwrap_or(0xff);
if !(b'0'..=b'9').contains(&code) {
break;
}
}
let modifiers = parse_mask(raw_mask.saturating_sub(1));
#[rustfmt::skip]