#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum State {
Ground,
Esc,
EscIntermediate,
Csi,
Osc,
Dcs,
OscEsc,
DcsEsc,
}
#[derive(Debug)]
pub(crate) struct SeqTracker {
state: State,
utf8_remaining: u8,
}
impl SeqTracker {
pub(crate) fn new() -> Self {
Self {
state: State::Ground,
utf8_remaining: 0,
}
}
pub(crate) fn feed(&mut self, bytes: &[u8]) {
for &b in bytes {
self.step(b);
}
}
pub(crate) fn mid_sequence(&self) -> bool {
self.state != State::Ground || self.utf8_remaining > 0
}
fn step(&mut self, b: u8) {
const ESC: u8 = 0x1b;
const CAN: u8 = 0x18;
const SUB: u8 = 0x1a;
const BEL: u8 = 0x07;
self.state = match self.state {
State::Ground => {
if b == ESC {
self.utf8_remaining = 0;
State::Esc
} else {
self.track_utf8(b);
State::Ground
}
}
State::Esc => match b {
b'[' => State::Csi,
b']' => State::Osc,
b'P' | b'X' | b'^' | b'_' => State::Dcs,
0x20..=0x2f => State::EscIntermediate,
ESC => State::Esc,
CAN | SUB => State::Ground,
_ => State::Ground,
},
State::EscIntermediate => match b {
0x20..=0x2f => State::EscIntermediate,
ESC => State::Esc,
CAN | SUB => State::Ground,
_ => State::Ground,
},
State::Csi => match b {
0x40..=0x7e => State::Ground,
ESC => State::Esc,
CAN | SUB => State::Ground,
_ => State::Csi,
},
State::Osc => match b {
BEL => State::Ground,
ESC => State::OscEsc,
CAN | SUB => State::Ground,
_ => State::Osc,
},
State::Dcs => match b {
ESC => State::DcsEsc,
CAN | SUB => State::Ground,
_ => State::Dcs, },
State::OscEsc | State::DcsEsc => match b {
b'\\' => State::Ground, ESC => self.state,
_ => {
self.state = State::Esc;
self.step(b);
return;
}
},
};
}
fn track_utf8(&mut self, b: u8) {
if self.utf8_remaining > 0 && (0x80..=0xbf).contains(&b) {
self.utf8_remaining -= 1;
return;
}
self.utf8_remaining = match b {
0xc2..=0xdf => 1,
0xe0..=0xef => 2,
0xf0..=0xf4 => 3,
_ => 0,
};
}
}
#[cfg(test)]
mod tests {
use super::*;
fn fed(bytes: &[u8]) -> SeqTracker {
let mut t = SeqTracker::new();
t.feed(bytes);
t
}
#[test]
fn plain_text_is_ground() {
assert!(!fed(b"hello world\r\n").mid_sequence());
}
#[test]
fn split_csi_is_mid_sequence_until_final_byte() {
let mut t = SeqTracker::new();
t.feed(b"\x1b[3");
assert!(t.mid_sequence());
t.feed(b"1");
assert!(t.mid_sequence());
t.feed(b"m");
assert!(!t.mid_sequence());
}
#[test]
fn two_char_escape_completes() {
assert!(!fed(b"\x1b7").mid_sequence()); assert!(fed(b"\x1b").mid_sequence());
}
#[test]
fn esc_intermediate_completes_on_final() {
assert!(fed(b"\x1b(").mid_sequence()); assert!(!fed(b"\x1b(B").mid_sequence());
}
#[test]
fn osc_terminated_by_bel_or_st() {
assert!(fed(b"\x1b]0;title").mid_sequence());
assert!(!fed(b"\x1b]0;title\x07").mid_sequence());
assert!(!fed(b"\x1b]0;title\x1b\\").mid_sequence());
}
#[test]
fn dcs_terminated_by_st_only() {
assert!(fed(b"\x1bPdata").mid_sequence());
assert!(fed(b"\x1bPdata\x07").mid_sequence()); assert!(!fed(b"\x1bPdata\x1b\\").mid_sequence());
}
#[test]
fn esc_inside_string_starts_new_sequence() {
assert!(!fed(b"\x1b]0;title\x1bc").mid_sequence());
assert!(fed(b"\x1b]0;title\x1b[3").mid_sequence());
}
#[test]
fn can_aborts_sequences() {
assert!(!fed(b"\x1b[31\x18").mid_sequence());
}
#[test]
fn split_utf8_is_mid_sequence() {
let bytes = "汉".as_bytes(); let mut t = SeqTracker::new();
t.feed(&bytes[..1]);
assert!(t.mid_sequence());
t.feed(&bytes[1..2]);
assert!(t.mid_sequence());
t.feed(&bytes[2..]);
assert!(!t.mid_sequence());
}
}