#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum MarkKind {
PromptStart,
CommandStart,
OutputStart,
CommandEnd,
}
impl MarkKind {
pub fn letter(self) -> u8 {
match self {
MarkKind::PromptStart => b'A',
MarkKind::CommandStart => b'B',
MarkKind::OutputStart => b'C',
MarkKind::CommandEnd => b'D',
}
}
fn from_letter(b: u8) -> Option<MarkKind> {
match b {
b'A' => Some(MarkKind::PromptStart),
b'B' => Some(MarkKind::CommandStart),
b'C' => Some(MarkKind::OutputStart),
b'D' => Some(MarkKind::CommandEnd),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ShellMark {
pub offset: u64,
pub kind: MarkKind,
pub exit_code: Option<i32>,
}
const MAX_SEQ: usize = 128;
#[derive(Debug, Default, Clone)]
pub struct MarkScanner {
pending: Vec<u8>,
start: u64,
pos: u64,
}
impl MarkScanner {
pub fn new() -> Self {
Self::default()
}
pub fn at(pos: u64) -> Self {
MarkScanner {
pos,
..Self::default()
}
}
pub fn position(&self) -> u64 {
self.pos
}
pub fn feed(&mut self, bytes: &[u8], out: &mut Vec<ShellMark>) {
let mut i = 0;
while i < bytes.len() {
if self.pending.is_empty() {
match bytes[i..].iter().position(|&b| b == 0x1B) {
None => {
self.pos += (bytes.len() - i) as u64;
return;
}
Some(p) => {
self.pos += p as u64;
i += p;
self.start = self.pos;
self.pending.push(0x1B);
self.pos += 1;
i += 1;
}
}
continue;
}
let b = bytes[i];
self.step(b, out);
self.pos += 1;
i += 1;
}
}
fn step(&mut self, b: u8, out: &mut Vec<ShellMark>) {
let p = &self.pending;
let n = p.len();
if n >= 2 && p[n - 1] == 0x1B {
if b == b'\\' {
self.pending.pop();
self.finish(out);
} else {
self.restart_at_last_esc(b);
}
return;
}
match b {
0x07 if n >= 6 => self.finish(out),
0x1B if n >= 6 => self.pending.push(0x1B), 0x1B => {
self.pending.clear();
self.pending.push(0x1B);
self.start = self.pos;
}
_ => {
let ok = match n {
1 => b == b']',
2 | 3 => b == b'1' || b == b'6' || b == b'3',
4 => b == b'3',
5 => b == b';',
6 => MarkKind::from_letter(b).is_some(),
_ => n < MAX_SEQ && b >= 0x20,
};
if ok {
self.pending.push(b);
if n == 4 && !matches!(&self.pending[2..5], b"133" | b"633") {
self.pending.clear();
}
} else {
self.pending.clear();
}
}
}
}
fn restart_at_last_esc(&mut self, b: u8) {
self.pending.clear();
self.pending.push(0x1B);
self.start = self.pos - 1;
let mut sink = Vec::new();
self.step(b, &mut sink);
}
fn finish(&mut self, out: &mut Vec<ShellMark>) {
let p = std::mem::take(&mut self.pending);
if p.len() < 7 {
return;
}
let Some(kind) = MarkKind::from_letter(p[6]) else {
return;
};
let params = &p[7..];
let exit_code = if kind == MarkKind::CommandEnd {
params
.strip_prefix(b";")
.and_then(|rest| rest.split(|&c| c == b';').next())
.and_then(|code| std::str::from_utf8(code).ok())
.and_then(|code| code.parse::<i32>().ok())
} else {
None
};
out.push(ShellMark {
offset: self.start,
kind,
exit_code,
});
}
}
#[cfg(test)]
mod tests {
use super::*;
fn scan(bytes: &[u8]) -> Vec<ShellMark> {
let mut s = MarkScanner::new();
let mut out = Vec::new();
s.feed(bytes, &mut out);
out
}
fn cycle() -> Vec<u8> {
b"\x1b]133;A\x07$ \x1b]133;B\x07cargo test\r\n\x1b]133;C\x07running...\r\nFAILED\r\n\x1b]133;D;101\x1b\\".to_vec()
}
#[test]
fn finds_a_whole_command_cycle() {
let data = cycle();
let marks = scan(&data);
let kinds: Vec<_> = marks.iter().map(|m| m.kind).collect();
assert_eq!(
kinds,
[
MarkKind::PromptStart,
MarkKind::CommandStart,
MarkKind::OutputStart,
MarkKind::CommandEnd
]
);
assert_eq!(marks[3].exit_code, Some(101));
for m in &marks {
assert_eq!(data[m.offset as usize], 0x1B, "{m:?}");
assert_eq!(data[m.offset as usize + 6], m.kind.letter());
}
}
#[test]
fn every_chunk_split_finds_the_same_marks() {
let mut data = b"noise \x1b[1mbold\x1b[0m ".to_vec();
data.extend(cycle());
data.extend_from_slice(b"\x1b]633;D\x07\x1b]0;title\x07tail");
let whole = scan(&data);
assert_eq!(whole.len(), 5);
for split in 0..=data.len() {
let mut s = MarkScanner::new();
let mut out = Vec::new();
s.feed(&data[..split], &mut out);
s.feed(&data[split..], &mut out);
assert_eq!(out, whole, "split at {split}");
assert_eq!(s.position(), data.len() as u64);
}
let mut s = MarkScanner::new();
let mut out = Vec::new();
for b in &data {
s.feed(std::slice::from_ref(b), &mut out);
}
assert_eq!(out, whole);
}
#[test]
fn exit_codes_are_optional_and_parsed_when_present() {
let m = scan(b"\x1b]133;D\x07\x1b]133;D;0\x07\x1b]133;D;-1;aid=7\x07\x1b]133;D;x\x07");
let codes: Vec<_> = m.iter().map(|m| m.exit_code).collect();
assert_eq!(codes, [None, Some(0), Some(-1), None]);
}
#[test]
fn look_alikes_are_not_marks() {
let m = scan(b"\x1b]0;title\x07\x1b]1330;A\x07\x1b]133;Z\x07\x1b[133;A\x07\x1b]133A\x07");
assert!(m.is_empty(), "{m:?}");
}
#[test]
fn an_esc_inside_a_candidate_restarts_it() {
let data = b"\x1b]133\x1b]133;A\x07";
let m = scan(data);
assert_eq!(m.len(), 1);
assert_eq!(m[0].offset, 5);
}
#[test]
fn offsets_continue_from_a_starting_position() {
let mut s = MarkScanner::at(1000);
let mut out = Vec::new();
s.feed(b"xx\x1b]133;C\x07", &mut out);
assert_eq!(out[0].offset, 1002);
}
}