use crate::style::Presentation;
use crate::vocabulary::Role;
#[must_use]
pub(crate) fn sanitize_log_line(line: &str) -> String {
let mut out = String::with_capacity(line.len());
let mut chars = line.chars().peekable();
while let Some(c) = chars.next() {
if c == '\u{1b}' {
match chars.peek().copied() {
Some('[') => {
chars.next();
let mut seq = String::from("\u{1b}[");
let mut closed = false;
let mut sgr = true;
for c in chars.by_ref() {
if !is_sgr_parameter(c) && !is_csi_final(c) {
sgr = false;
}
seq.push(c);
if is_csi_final(c) {
closed = true;
sgr = sgr && c == 'm';
break;
}
}
if closed && sgr {
out.push_str(&seq);
}
}
Some(']') => {
chars.next();
for c in chars.by_ref() {
if c == '\u{7}' {
break;
}
if c == '\u{1b}' {
if chars.peek().copied() == Some('\\') {
chars.next();
}
break;
}
}
}
_ => {
chars.next();
}
}
} else if c == '\t' || !c.is_control() {
out.push(c);
}
}
out
}
fn is_csi_final(c: char) -> bool {
('\u{40}'..='\u{7e}').contains(&c)
}
fn is_sgr_parameter(c: char) -> bool {
matches!(c, '0'..='9' | ';' | ':')
}
#[must_use]
pub(crate) fn sets_colour(line: &str) -> bool {
let mut chars = line.chars().peekable();
while let Some(c) = chars.next() {
if c != '\u{1b}' || chars.peek().copied() != Some('[') {
continue;
}
chars.next();
let mut params = String::new();
while let Some(c) = chars.peek().copied() {
if !is_sgr_parameter(c) {
break;
}
params.push(c);
chars.next();
}
if chars.next() == Some('m') && params_set_colour(¶ms) {
return true;
}
}
false
}
fn params_set_colour(params: &str) -> bool {
params
.split([';', ':'])
.any(|part| part.chars().any(|d| d != '0'))
}
#[must_use]
pub(crate) fn paint_default(line: &str, stream: &str, style: Presentation) -> String {
if !style.colour || sets_colour(line) {
return line.to_owned();
}
let role = if stream == "err" {
Role::Bark
} else {
Role::Meadow
};
let painted = super::paint::style_for(role, style.deep_colour);
let plain = strip_bare_resets(line);
format!("{painted}{plain}{painted:#}")
}
fn strip_bare_resets(line: &str) -> String {
let mut out = String::with_capacity(line.len());
let mut chars = line.chars().peekable();
while let Some(c) = chars.next() {
if c != '\u{1b}' || chars.peek().copied() != Some('[') {
out.push(c);
continue;
}
chars.next();
let mut seq = String::from("\u{1b}[");
let mut params = String::new();
while let Some(p) = chars.peek().copied() {
if !is_sgr_parameter(p) {
break;
}
params.push(p);
seq.push(p);
chars.next();
}
match chars.next() {
Some('m') if !params_set_colour(¶ms) => {}
Some(c) => {
seq.push(c);
out.push_str(&seq);
}
None => out.push_str(&seq),
}
}
out
}
#[cfg(test)]
mod tests {
use crate::style::StyleLevel;
use super::*;
#[test]
fn an_sgr_colour_survives_and_a_cursor_move_does_not() {
let line = "\u{1b}[31mfailed\u{1b}[0m \u{1b}[2Coverwritten";
assert_eq!(
sanitize_log_line(line),
"\u{1b}[31mfailed\u{1b}[0m overwritten",
"the SGR is the sheep's own styling; `[2C` is a cursor move"
);
}
#[test]
fn a_clear_screen_an_osc_title_and_a_carriage_return_are_stripped() {
assert_eq!(
sanitize_log_line("\u{1b}[2Jbefore\rafter"),
"beforeafter",
"a clear screen and a bare CR must not redraw the row"
);
assert_eq!(
sanitize_log_line("\u{1b}]0;pwned\u{7}title gone"),
"title gone",
"an OSC runs to its BEL terminator and the whole of it goes"
);
assert_eq!(
sanitize_log_line("a\u{8}b\u{1b}[8c!"),
"ab!",
"backspace and the CSI after it drop; the tab is the only control kept"
);
assert_eq!(
sanitize_log_line("clean\u{9b}2Jhere"),
"clean2Jhere",
"the single-character CSI introducer is a control character and drops"
);
assert_eq!(
sanitize_log_line("keep\tthe\ttab"),
"keep\tthe\ttab",
"a tab is column layout the sheep chose, not a redraw"
);
}
#[test]
fn a_line_that_opens_a_colour_and_never_resets_counts_as_coloured() {
assert!(
sets_colour("\u{1b}[32mno reset here"),
"an unclosed colour is still the sheep's own colour"
);
}
#[test]
fn a_line_that_colours_one_word_counts_as_coloured() {
assert!(
sets_colour("plain \u{1b}[1mbold\u{1b}[0m plain"),
"one styled word makes the whole line styled"
);
}
#[test]
fn a_line_that_only_resets_is_still_plain() {
assert!(
!sets_colour("\u{1b}[0mback to normal\u{1b}[m"),
"a reset with nothing to reset is not a colour; both bare spellings count"
);
assert!(
!sets_colour("\u{1b}[00m\u{1b}[0;0mredundant resets only"),
"00 and 0;0 parse as the same reset as 0, so the line is still plain"
);
assert!(
sets_colour("\u{1b}[0;31mred after a reset"),
"a reset sharing the sequence with a real colour still colours"
);
assert!(!sets_colour("nothing styled at all"));
}
#[test]
fn a_bare_reset_inside_a_plain_line_does_not_cancel_the_stream_colour() {
let style = Presentation::new(StyleLevel::Full, None, None, None, 80);
assert_eq!(
paint_default("\u{1b}[0mhello", "out", style),
"\u{1b}[32mhello\u{1b}[0m",
"a leading reset would cancel the wrapper's green before the text"
);
assert_eq!(
paint_default("foo\u{1b}[0m bar", "err", style),
"\u{1b}[31mfoo bar\u{1b}[0m",
"an embedded reset would drop the colour off the tail of the line"
);
assert_eq!(
paint_default("\u{1b}[31mred stays\u{1b}[0m", "out", style),
"\u{1b}[31mred stays\u{1b}[0m",
"a line with its own colour is untouched, resets and all"
);
}
}