ansi.rs 3.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
  1. //! ANSI escape-sequence stripping — keeps bench/console logs readable in
  2. //! `sm121 report` and under grep/less (the pty-wrapped tool emits colors,
  3. //! cursor moves, and OSC titles).
  4. /// Strip ANSI escape sequences out of `buf` into `out`, in place. The
  5. /// consumed prefix is drained from `buf`; a trailing partial escape (a
  6. /// sequence split across chunk boundaries) is left in `buf` as the carry
  7. /// for the next call. Handles CSI (`ESC [ ... final`), OSC (`ESC ] ... BEL`
  8. /// or `ST`), and generic escapes with intermediate bytes (`ESC ( B`).
  9. pub(crate) fn strip_ansi_step(buf: &mut Vec<u8>, out: &mut Vec<u8>) {
  10. let n = buf.len();
  11. let mut i = 0;
  12. while i < n {
  13. if buf[i] != 0x1b {
  14. out.push(buf[i]);
  15. i += 1;
  16. continue;
  17. }
  18. let rest = &buf[i + 1..];
  19. match rest.first() {
  20. Some(b'[') => match rest.iter().skip(1).position(|c| (0x40..=0x7e).contains(c)) {
  21. Some(p) => i += 1 + 1 + p + 1, // ESC [ params final
  22. None => break, // incomplete: wait for more data
  23. },
  24. Some(b']') => match rest.iter().skip(1).position(|c| *c == 0x07 || *c == 0x1b) {
  25. Some(p) => {
  26. i += 1 + 1 + p + 1;
  27. if rest[1 + p] == 0x1b {
  28. i += 1; // consume the backslash of the ST
  29. }
  30. }
  31. None => break,
  32. },
  33. None => break, // lone ESC at the chunk end
  34. Some(_) => {
  35. // Generic escape: intermediate bytes (0x20-0x2f), then one
  36. // final byte — drop all of them.
  37. let mut j = 1;
  38. while j < rest.len() && (0x20..=0x2f).contains(&rest[j]) {
  39. j += 1;
  40. }
  41. if j < rest.len() {
  42. j += 1; // final byte
  43. }
  44. i += 1 + j;
  45. }
  46. }
  47. }
  48. buf.drain(..i);
  49. }
  50. /// One-shot ANSI strip (whole buffer at once; for files already on disk).
  51. pub(crate) fn strip_ansi(bytes: &[u8]) -> Vec<u8> {
  52. let mut buf = bytes.to_vec();
  53. let mut out = Vec::new();
  54. strip_ansi_step(&mut buf, &mut out);
  55. out
  56. }
  57. #[cfg(test)]
  58. mod tests {
  59. use super::*;
  60. #[test]
  61. fn strip_ansi_removes_codes_keeps_text() {
  62. let raw = b"\x1b[38;5;214mred\x1b[0m plain \x1b]0;title\x07osc \x1b]2;x\x1b\\st \x1b(Bc";
  63. assert_eq!(strip_ansi(raw), b"red plain osc st c");
  64. // nothing to strip
  65. assert_eq!(strip_ansi(b"clean text"), b"clean text".to_vec());
  66. }
  67. #[test]
  68. fn strip_ansi_survives_split_chunks() {
  69. // the escape sequence straddles the chunk boundary
  70. let a = b"ab\x1b[38;5;2"; // split inside a CSI parameter
  71. let b2 = b"14mtext";
  72. let mut ca = Vec::new();
  73. ca.extend_from_slice(a);
  74. let mut out = Vec::new();
  75. strip_ansi_step(&mut ca, &mut out);
  76. ca.extend_from_slice(b2);
  77. strip_ansi_step(&mut ca, &mut out);
  78. assert_eq!(out, b"abtext");
  79. assert!(ca.is_empty(), "no dangling carry: {ca:?}");
  80. }
  81. }