diff --git a/src/bin/dosh-client.rs b/src/bin/dosh-client.rs index 4837842..0657db0 100644 --- a/src/bin/dosh-client.rs +++ b/src/bin/dosh-client.rs @@ -8436,7 +8436,7 @@ fn strip_unowned_terminal_reports(bytes: Vec, mouse_tracking: bool) -> (Vec< return (bytes, false); } let before = bytes.len(); - let stripped = strip_stale_mouse_reports(&bytes); + let stripped = strip_complete_terminal_mouse_reports(&bytes); let changed = stripped.len() != before; (stripped, changed) } @@ -8572,6 +8572,42 @@ fn strip_stale_mouse_reports(bytes: &[u8]) -> Vec { out } +fn strip_complete_terminal_mouse_reports(bytes: &[u8]) -> Vec { + let mut out = Vec::with_capacity(bytes.len()); + let mut offset = 0; + while offset < bytes.len() { + if let Some(len) = prefixed_mouse_report_len(bytes, offset) { + offset += len; + } else { + out.push(bytes[offset]); + offset += 1; + } + } + out +} + +fn prefixed_mouse_report_len(bytes: &[u8], offset: usize) -> Option { + match bytes.get(offset).copied()? { + 0x1b if bytes.get(offset + 1) == Some(&b'[') => match bytes.get(offset + 2).copied() { + Some(b'M') if offset + 6 <= bytes.len() => Some(6), + Some(b'<') => mouse_report_params_len(bytes, offset + 3, 2).map(|len| len + 3), + Some(byte) if byte.is_ascii_digit() => { + mouse_report_params_len(bytes, offset + 2, 2).map(|len| len + 2) + } + _ => None, + }, + 0x9b => match bytes.get(offset + 1).copied() { + Some(b'M') if offset + 5 <= bytes.len() => Some(5), + Some(b'<') => mouse_report_params_len(bytes, offset + 2, 2).map(|len| len + 2), + Some(byte) if byte.is_ascii_digit() => { + mouse_report_params_len(bytes, offset + 1, 2).map(|len| len + 1) + } + _ => None, + }, + _ => None, + } +} + fn stale_mouse_report_len(bytes: &[u8], offset: usize) -> Option { match bytes.get(offset).copied()? { 0x1b if bytes.get(offset + 1) == Some(&b'[') => match bytes.get(offset + 2).copied() { @@ -8643,25 +8679,27 @@ fn stale_mouse_report_maybe_incomplete(bytes: &[u8], offset: usize) -> bool { return false; }; match rest { - [0x1b] | [0x1b, b'['] | [0x1b, b'[', b'<'] => true, + [0x1b, b'[', b'<'] => true, [0x1b, b'[', b'M', ..] if rest.len() < 6 => true, - [0x1b, b'[', b'<', params @ ..] | [0x1b, b'[', params @ ..] - if params_are_mouse_prefix(params, 2) => - { - true - } - [0x9b] | [0x9b, b'<'] => true, + [0x1b, b'[', b'<', params @ ..] if params_are_terminal_report_prefix(params) => true, + [0x1b, b'[', params @ ..] if params_are_mouse_prefix(params, 2) => true, + [0x9b, b'<'] => true, [0x9b, b'M', ..] if rest.len() < 5 => true, - [0x9b, b'<', params @ ..] | [0x9b, params @ ..] if params_are_mouse_prefix(params, 2) => { - true - } - params if params_are_mouse_prefix(params, 2) => true, + [0x9b, b'<', params @ ..] if params_are_terminal_report_prefix(params) => true, + [0x9b, params @ ..] if params_are_mouse_prefix(params, 2) => true, _ => false, } } +fn params_are_terminal_report_prefix(params: &[u8]) -> bool { + !params.is_empty() + && params + .iter() + .all(|byte| byte.is_ascii_digit() || *byte == b';') +} + fn params_are_mouse_prefix(params: &[u8], min_semicolons: usize) -> bool { - let mut semicolons = 0usize; + let mut semicolons = 0; let mut saw_digit = false; for byte in params { match byte { @@ -13969,6 +14007,22 @@ mod tests { assert_eq!(preserved, b"\x1b[<35;10;1M"); } + #[test] + fn live_unowned_filter_preserves_ambiguous_user_input() { + for input in [ + b"\x1b".as_slice(), + b"\x1b[".as_slice(), + b"\x1b[A".as_slice(), + b"\x1b[200~paste\x1b[201~".as_slice(), + b"35;152;1M".as_slice(), + b"printf 35;152;1M\r".as_slice(), + ] { + let (preserved, changed) = strip_unowned_terminal_reports(input.to_vec(), false); + assert!(!changed, "live filter changed {input:?}"); + assert_eq!(preserved, input); + } + } + #[test] fn transient_udp_send_errors_are_not_terminal_fatal() { #[cfg(unix)] @@ -14059,12 +14113,19 @@ mod tests { } #[test] - fn incomplete_three_field_mouse_prefix_is_suppressed() { - assert_eq!(strip_stale_mouse_reports(b"35;152;"), b""); + fn only_explicit_incomplete_mouse_prefixes_are_suppressed() { + assert_eq!(strip_stale_mouse_reports(b"35;152;"), b"35;152;"); assert_eq!(strip_stale_mouse_reports(b"\x1b[<35;152;"), b""); assert_eq!(strip_stale_mouse_reports(b"\x9b35;152;"), b""); } + #[test] + fn lone_escape_survives_stale_reconnect_filter() { + assert_eq!(strip_stale_mouse_reports(b"\x1b"), b"\x1b"); + assert_eq!(strip_stale_mouse_reports(b"\x1b["), b"\x1b["); + assert_eq!(strip_stale_mouse_reports(b"\x1b[A"), b"\x1b[A"); + } + #[test] fn stale_focus_reports_are_stripped_from_pending_input() { assert_eq!(strip_stale_mouse_reports(b"\x1b[Ihello\x1b[O"), b"hello");