| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586 |
- //! ANSI escape-sequence stripping — keeps bench/console logs readable in
- //! `sm121 report` and under grep/less (the pty-wrapped tool emits colors,
- //! cursor moves, and OSC titles).
- /// Strip ANSI escape sequences out of `buf` into `out`, in place. The
- /// consumed prefix is drained from `buf`; a trailing partial escape (a
- /// sequence split across chunk boundaries) is left in `buf` as the carry
- /// for the next call. Handles CSI (`ESC [ ... final`), OSC (`ESC ] ... BEL`
- /// or `ST`), and generic escapes with intermediate bytes (`ESC ( B`).
- pub(crate) fn strip_ansi_step(buf: &mut Vec<u8>, out: &mut Vec<u8>) {
- let n = buf.len();
- let mut i = 0;
- while i < n {
- if buf[i] != 0x1b {
- out.push(buf[i]);
- i += 1;
- continue;
- }
- let rest = &buf[i + 1..];
- match rest.first() {
- Some(b'[') => match rest.iter().skip(1).position(|c| (0x40..=0x7e).contains(c)) {
- Some(p) => i += 1 + 1 + p + 1, // ESC [ params final
- None => break, // incomplete: wait for more data
- },
- Some(b']') => match rest.iter().skip(1).position(|c| *c == 0x07 || *c == 0x1b) {
- Some(p) => {
- i += 1 + 1 + p + 1;
- if rest[1 + p] == 0x1b {
- i += 1; // consume the backslash of the ST
- }
- }
- None => break,
- },
- None => break, // lone ESC at the chunk end
- Some(_) => {
- // Generic escape: intermediate bytes (0x20-0x2f), then one
- // final byte — drop all of them.
- let mut j = 1;
- while j < rest.len() && (0x20..=0x2f).contains(&rest[j]) {
- j += 1;
- }
- if j < rest.len() {
- j += 1; // final byte
- }
- i += 1 + j;
- }
- }
- }
- buf.drain(..i);
- }
- /// One-shot ANSI strip (whole buffer at once; for files already on disk).
- pub(crate) fn strip_ansi(bytes: &[u8]) -> Vec<u8> {
- let mut buf = bytes.to_vec();
- let mut out = Vec::new();
- strip_ansi_step(&mut buf, &mut out);
- out
- }
- #[cfg(test)]
- mod tests {
- use super::*;
- #[test]
- fn strip_ansi_removes_codes_keeps_text() {
- let raw = b"\x1b[38;5;214mred\x1b[0m plain \x1b]0;title\x07osc \x1b]2;x\x1b\\st \x1b(Bc";
- assert_eq!(strip_ansi(raw), b"red plain osc st c");
- // nothing to strip
- assert_eq!(strip_ansi(b"clean text"), b"clean text".to_vec());
- }
- #[test]
- fn strip_ansi_survives_split_chunks() {
- // the escape sequence straddles the chunk boundary
- let a = b"ab\x1b[38;5;2"; // split inside a CSI parameter
- let b2 = b"14mtext";
- let mut ca = Vec::new();
- ca.extend_from_slice(a);
- let mut out = Vec::new();
- strip_ansi_step(&mut ca, &mut out);
- ca.extend_from_slice(b2);
- strip_ansi_step(&mut ca, &mut out);
- assert_eq!(out, b"abtext");
- assert!(ca.is_empty(), "no dangling carry: {ca:?}");
- }
- }
|