#[derive(Debug, PartialEq, Eq)]
enum State {
Ground,
Esc,
Csi,
}
pub(super) struct ColorNormalizer {
state: State,
pending: Vec<u8>,
}
impl ColorNormalizer {
pub(super) fn new() -> Self {
Self {
state: State::Ground,
pending: Vec::new(),
}
}
pub(super) fn feed(&mut self, bytes: &[u8]) -> Vec<u8> {
rewrite_stream(
&mut self.state,
&mut self.pending,
bytes,
normalize_colon_colours,
)
}
}
pub(super) struct AttrShadow {
parser: ::vt100::Parser,
state: State,
pending: Vec<u8>,
}
impl AttrShadow {
pub(super) fn new(rows: u16, cols: u16, scrollback: usize) -> Self {
Self {
parser: ::vt100::Parser::new(rows, cols, scrollback),
state: State::Ground,
pending: Vec::new(),
}
}
pub(super) fn set_scrollback(&mut self, rows: usize) {
self.parser.screen_mut().set_scrollback(rows);
}
pub(super) fn set_size(&mut self, rows: u16, cols: u16) {
self.parser.screen_mut().set_size(rows, cols);
}
pub(super) fn cell(&self, row: u16, col: u16) -> Option<&::vt100::Cell> {
self.parser.screen().cell(row, col)
}
pub(super) fn contents(&self) -> String {
self.parser.screen().contents()
}
pub(super) fn feed(&mut self, bytes: &[u8]) {
let shadowed = self.rewrite_stream(bytes);
self.parser.process(&shadowed);
}
fn rewrite_stream(&mut self, bytes: &[u8]) -> Vec<u8> {
rewrite_stream(&mut self.state, &mut self.pending, bytes, rewrite_sgr)
}
}
fn rewrite_stream(
state: &mut State,
pending: &mut Vec<u8>,
bytes: &[u8],
rewrite: fn(&[u8]) -> Option<Vec<u8>>,
) -> Vec<u8> {
let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
for &b in bytes {
if b == 0x1b {
out.append(pending);
pending.push(b);
*state = State::Esc;
continue;
}
match state {
State::Ground => out.push(b),
State::Esc => {
pending.push(b);
if b == b'[' {
*state = State::Csi;
} else {
out.append(pending);
*state = State::Ground;
}
}
State::Csi => {
pending.push(b);
match b {
0x20..=0x3f => {}
0x40..=0x7e => {
let seq = std::mem::take(pending);
*state = State::Ground;
match (b == b'm').then(|| rewrite(&seq)).flatten() {
Some(rewritten) => out.extend_from_slice(&rewritten),
None => out.extend_from_slice(&seq),
}
}
_ => {
out.append(pending);
*state = State::Ground;
}
}
}
}
}
out
}
fn normalize_colon_colours(seq: &[u8]) -> Option<Vec<u8>> {
let params = seq.get(2..seq.len().checked_sub(1)?)?;
if params
.iter()
.any(|b| !matches!(b, b'0'..=b'9' | b';' | b':'))
{
return None;
}
if !params.contains(&b':') {
return None;
}
let groups: Vec<&[u8]> = params.split(|&b| b == b';').collect();
let mut normalized = Vec::with_capacity(groups.len());
let mut changed = false;
for group in groups {
let parts: Vec<&[u8]> = group.split(|&b| b == b':').collect();
let replacement = match parts.as_slice() {
[first @ (b"38" | b"48"), b"2", b"", red, green, blue]
| [first @ (b"38" | b"48"), b"2", red, green, blue] => {
Some(vec![*first, b"2", *red, *green, *blue])
}
[first @ (b"38" | b"48"), b"5", index] => Some(vec![*first, b"5", *index]),
_ => None,
};
if let Some(replacement) = replacement {
normalized.extend(replacement.into_iter().map(<[u8]>::to_vec));
changed = true;
} else {
normalized.push(group.to_vec());
}
}
if !changed {
return None;
}
let mut out = Vec::with_capacity(seq.len());
out.extend_from_slice(b"\x1b[");
for (index, group) in normalized.iter().enumerate() {
if index > 0 {
out.push(b';');
}
out.extend_from_slice(group);
}
out.push(b'm');
Some(out)
}
fn rewrite_sgr(seq: &[u8]) -> Option<Vec<u8>> {
let params = seq.get(2..seq.len().checked_sub(1)?)?;
if params
.iter()
.any(|b| !matches!(b, b'0'..=b'9' | b';' | b':'))
{
return None;
}
fn value(group: &[u8]) -> u32 {
let first = group.split(|&b| b == b':').next().unwrap_or(b"");
first.iter().fold(0u32, |acc, &d| {
acc.saturating_mul(10).saturating_add(u32::from(d - b'0'))
})
}
let groups: Vec<&[u8]> = params.split(|&b| b == b';').collect();
let mut out: Vec<u32> = Vec::new();
let mut i = 0;
while i < groups.len() {
let group = groups[i];
let v = value(group);
match v {
38 | 48 | 58 => {
if group.contains(&b':') {
i += 1;
} else {
i += match groups.get(i + 1).map(|g| value(g)) {
Some(2) => 5, Some(5) => 3, _ => 1, };
}
}
_ => {
if let Some(carrier) = carrier(v) {
out.push(carrier);
}
i += 1;
}
}
}
let mut bytes = Vec::new();
if !out.is_empty() {
bytes.extend_from_slice(b"\x1b[");
for (n, param) in out.iter().enumerate() {
if n > 0 {
bytes.push(b';');
}
bytes.extend_from_slice(param.to_string().as_bytes());
}
bytes.push(b'm');
}
Some(bytes)
}
fn carrier(param: u32) -> Option<u32> {
match param {
0 => Some(0), 5 | 6 => Some(1), 8 => Some(3), 9 => Some(4), 25 => Some(22), 28 => Some(23), 29 => Some(24), _ => None, }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_recovered_parameters_are_exactly_what_the_tracker_declines() {
let recovered: Vec<u16> = (0..=255u32)
.filter(|&p| p != 0 && carrier(p).is_some())
.map(|p| p as u16)
.collect();
let mut declined = crate::emu::unhandled::SHADOW_SGR_PARAMS.to_vec();
declined.sort_unstable();
assert_eq!(
recovered, declined,
"shadow::carrier recovers {recovered:?}, unhandled declines {declined:?}"
);
}
fn shadowed(bytes: &[u8]) -> String {
let mut shadow = AttrShadow::new(4, 20, 0);
let mut out = shadow.rewrite_stream(bytes);
out.append(&mut shadow.pending);
String::from_utf8_lossy(&out).replace('\x1b', "E")
}
fn normalized(bytes: &[u8]) -> String {
let mut state = State::Ground;
let mut pending = Vec::new();
let mut out = rewrite_stream(&mut state, &mut pending, bytes, normalize_colon_colours);
out.append(&mut pending);
String::from_utf8_lossy(&out).replace('\x1b', "E")
}
#[test]
fn the_three_dropped_attributes_get_carriers() {
assert_eq!(shadowed(b"\x1b[5mX"), "E[1mX"); assert_eq!(shadowed(b"\x1b[6mX"), "E[1mX"); assert_eq!(shadowed(b"\x1b[8mX"), "E[3mX"); assert_eq!(shadowed(b"\x1b[9mX"), "E[4mX"); assert_eq!(shadowed(b"\x1b[25m"), "E[22m");
assert_eq!(shadowed(b"\x1b[28m"), "E[23m");
assert_eq!(shadowed(b"\x1b[29m"), "E[24m");
}
#[test]
fn the_primarys_own_attributes_are_dropped_from_the_shadow() {
assert_eq!(shadowed(b"\x1b[1mX"), "X");
assert_eq!(shadowed(b"\x1b[3mX"), "X");
assert_eq!(shadowed(b"\x1b[4mX"), "X");
assert_eq!(shadowed(b"\x1b[7;31;44mX"), "X");
assert_eq!(shadowed(b"\x1b[22m\x1b[23m\x1b[24m\x1b[27m"), "");
assert_eq!(shadowed(b"\x1b[0mX"), "E[0mX");
assert_eq!(shadowed(b"\x1b[mX"), "E[0mX");
}
#[test]
fn mixed_parameters_keep_only_the_carriers_in_order() {
assert_eq!(shadowed(b"\x1b[1;5;31mX"), "E[1mX");
assert_eq!(shadowed(b"\x1b[0;9;1;8mX"), "E[0;4;3mX");
}
#[test]
fn an_extended_colour_never_looks_like_a_carrier() {
assert_eq!(shadowed(b"\x1b[38;5;196mX"), "X");
assert_eq!(shadowed(b"\x1b[48;5;9mX"), "X");
assert_eq!(shadowed(b"\x1b[38;2;255;0;8mX"), "X"); assert_eq!(shadowed(b"\x1b[38;2;0;9;0;5mX"), "E[1mX"); assert_eq!(shadowed(b"\x1b[38:5:196mX"), "X");
assert_eq!(shadowed(b"\x1b[38:2::255:0:9mX"), "X");
assert_eq!(shadowed(b"\x1b[4:3mX"), "X"); assert_eq!(shadowed(b"\x1b[38;5;196;9mX"), "E[4mX");
}
#[test]
fn anything_that_is_not_a_plain_sgr_passes_through_verbatim() {
for seq in [
&b"\x1b[?2026h"[..],
&b"\x1b[2J"[..],
&b"\x1b[H"[..],
&b"\x1b[3;7Hhi"[..],
&b"\x1b[?25l"[..],
&b"\x1b[>4;2m"[..], &b"\x1b[4$p"[..], &b"\x1b]0;title\x07"[..],
&b"\x1b7\x1b8"[..],
&b"plain text\r\n\t"[..],
] {
assert_eq!(
shadowed(seq),
String::from_utf8_lossy(seq).replace('\x1b', "E"),
"sequence {:?} must pass through untouched",
String::from_utf8_lossy(seq)
);
}
}
#[test]
fn colon_colour_normalization_preserves_unrecognized_bytes() {
assert_eq!(normalized(b"\x1b[38:2::10:20:30m"), "E[38;2;10;20;30m");
assert_eq!(normalized(b"\x1b[38:5:196m"), "E[38;5;196m");
assert_eq!(normalized(b"\x1b[1;38:2::4:5:6;3m"), "E[1;38;2;4;5;6;3m");
for seq in [
&b"\x1b[4:3m"[..],
&b"\x1b[38:2::9m"[..],
&b"\x1b[38:2:1:10:20:30m"[..],
&b"\x1b[?25l"[..],
&b"\x1b]8;;http://x/\x1b\\"[..],
] {
assert_eq!(
normalized(seq),
String::from_utf8_lossy(seq).replace('\x1b', "E"),
"sequence {:?} must pass through untouched",
String::from_utf8_lossy(seq)
);
}
}
#[test]
fn a_sequence_split_across_feeds_is_not_lost() {
let mut shadow = AttrShadow::new(2, 10, 0);
shadow.feed(b"\x1b[8");
assert!(!shadow.cell(0, 0).is_some_and(::vt100::Cell::italic));
shadow.feed(b"mX");
assert!(
shadow.cell(0, 0).is_some_and(::vt100::Cell::italic),
"the conceal carrier must survive a chunk boundary"
);
}
#[test]
fn an_aborted_csi_keeps_its_bytes() {
assert_eq!(shadowed(b"\x1b[31\x18X"), "E[31\u{18}X");
assert_eq!(shadowed(b"\x1b[31\x18\x1b[9mX"), "E[31\u{18}E[4mX");
assert_eq!(shadowed(b"\x1b[31\x1b[9mX"), "E[31E[4mX");
}
}