diff --git a/crates/anstyle-stream/Cargo.toml b/crates/anstyle-stream/Cargo.toml index 18cbbe4f..5613ccbf 100644 --- a/crates/anstyle-stream/Cargo.toml +++ b/crates/anstyle-stream/Cargo.toml @@ -50,3 +50,7 @@ harness = false [[bench]] name = "wincon" harness = false + +[[bench]] +name = "stream" +harness = false diff --git a/crates/anstyle-stream/benches/stream.rs b/crates/anstyle-stream/benches/stream.rs new file mode 100644 index 00000000..fb40ac8b --- /dev/null +++ b/crates/anstyle-stream/benches/stream.rs @@ -0,0 +1,82 @@ +use std::io::Write as _; + +use criterion::{black_box, Criterion}; + +fn stream(c: &mut Criterion) { + for (name, content) in [ + ("demo.vte", &include_bytes!("../tests/demo.vte")[..]), + ("rg_help.vte", &include_bytes!("../tests/rg_help.vte")[..]), + ("rg_linus.vte", &include_bytes!("../tests/rg_linus.vte")[..]), + ( + "state_changes", + &b"\x1b]2;X\x1b\\ \x1b[0m \x1bP0@\x1b\\"[..], + ), + ] { + let mut group = c.benchmark_group(name); + group.bench_function("nop", |b| { + b.iter(|| { + let buffer = anstyle_stream::Buffer::with_capacity(content.len()); + let mut stream = buffer; + + stream.write_all(content).unwrap(); + + black_box(stream) + }) + }); + group.bench_function("StripStream", |b| { + b.iter(|| { + let buffer = anstyle_stream::Buffer::with_capacity(content.len()); + let mut stream = anstyle_stream::StripStream::new(buffer); + + stream.write_all(content).unwrap(); + + black_box(stream) + }) + }); + #[cfg(feature = "wincon")] + group.bench_function("WinconStream", |b| { + b.iter(|| { + let buffer = anstyle_stream::Buffer::with_capacity(content.len()); + let mut stream = + anstyle_stream::WinconStream::new(anstyle_wincon::Console::new(buffer)); + + stream.write_all(content).unwrap(); + + black_box(stream) + }) + }); + group.bench_function("AutoStream::always_ansi", |b| { + b.iter(|| { + let buffer = anstyle_stream::Buffer::with_capacity(content.len()); + let mut stream = anstyle_stream::AutoStream::always_ansi(buffer); + + stream.write_all(content).unwrap(); + + black_box(stream) + }) + }); + group.bench_function("AutoStream::always", |b| { + b.iter(|| { + let buffer = anstyle_stream::Buffer::with_capacity(content.len()); + let mut stream = anstyle_stream::AutoStream::always(buffer); + + stream.write_all(content).unwrap(); + + black_box(stream) + }) + }); + group.bench_function("AutoStream::never", |b| { + b.iter(|| { + let buffer = anstyle_stream::Buffer::with_capacity(content.len()); + let mut stream = anstyle_stream::AutoStream::never(buffer); + + stream.write_all(content).unwrap(); + + black_box(stream) + }) + }); + } +} + +criterion::criterion_group!(benches, stream); +criterion::criterion_main!(benches); diff --git a/crates/anstyle-stream/src/adapter/strip.rs b/crates/anstyle-stream/src/adapter/strip.rs index b5795562..0403556a 100644 --- a/crates/anstyle-stream/src/adapter/strip.rs +++ b/crates/anstyle-stream/src/adapter/strip.rs @@ -155,7 +155,10 @@ unsafe fn from_utf8_unchecked<'b>(bytes: &'b [u8], safety_justification: &'stati #[inline] fn is_printable_str(action: Action, byte: u8) -> bool { - action == Action::Print + // VT320 considered 0x7f to be `Print`able but we expect to be working in UTF-8 systems and not + // ISO Latin-1, making it DEL and non-printable + const DEL: u8 = 0x7f; + (action == Action::Print && byte != DEL) || action == Action::BeginUtf8 // since we know the input is valid UTF-8, the only thing we can do with // continuations is to print them @@ -364,8 +367,12 @@ impl<'a> utf8parse::Receiver for VtUtf8Receiver<'a> { #[inline] fn is_printable_bytes(action: Action, byte: u8) -> bool { + // VT320 considered 0x7f to be `Print`able but we expect to be working in UTF-8 systems and not + // ISO Latin-1, making it DEL and non-printable + const DEL: u8 = 0x7f; + // Continuations aren't included as they may also be control codes, requiring more context - action == Action::Print + (action == Action::Print && byte != DEL) || action == Action::BeginUtf8 || (action == Action::Execute && byte.is_ascii_whitespace()) } @@ -450,6 +457,22 @@ mod test { assert_eq!(expected, actual); } + #[test] + fn test_strip_str_del() { + let input = std::str::from_utf8(&[0x7f]).unwrap(); + let expected = ""; + let actual = strip_str(input).to_string(); + assert_eq!(expected, actual); + } + + #[test] + fn test_strip_byte_del() { + let bytes = [0x7f]; + let expected = ""; + let actual = String::from_utf8(strip_byte(&bytes).to_vec()).unwrap(); + assert_eq!(expected, actual); + } + proptest! { #[test] #[cfg_attr(miri, ignore)] // See https://github.com/AltSysrq/proptest/issues/253 diff --git a/crates/anstyle-stream/src/auto.rs b/crates/anstyle-stream/src/auto.rs index 582ea9a1..d019a92d 100644 --- a/crates/anstyle-stream/src/auto.rs +++ b/crates/anstyle-stream/src/auto.rs @@ -13,7 +13,7 @@ enum StreamInner { PassThrough(S), Strip(StripStream), #[cfg(feature = "wincon")] - Wincon(Box>), + Wincon(WinconStream), } impl AutoStream @@ -47,7 +47,7 @@ where if raw.is_terminal() && !concolor_query::windows::enable_ansi_colors().unwrap_or(true) { let console = anstyle_wincon::Console::new(raw); Self { - inner: StreamInner::Wincon(Box::new(WinconStream::new(console))), + inner: StreamInner::Wincon(WinconStream::new(console)), } } else { Self::always_ansi_(raw) @@ -63,6 +63,27 @@ where let inner = StreamInner::Strip(StripStream::new(raw)); AutoStream { inner } } + + #[cfg(feature = "auto")] + #[inline] + pub(crate) fn auto(raw: S) -> Self { + if raw.is_terminal() { + Self::always(raw) + } else { + Self::never(raw) + } + } + + /// Get the wrapped [`RawStream`] + #[inline] + pub fn into_inner(self) -> S { + match self.inner { + StreamInner::PassThrough(w) => w, + StreamInner::Strip(w) => w.into_inner(), + #[cfg(feature = "wincon")] + StreamInner::Wincon(w) => w.into_inner().into_inner(), + } + } } impl AutoStream @@ -81,28 +102,12 @@ where StreamInner::PassThrough(w) => StreamInner::PassThrough(w.lock()), StreamInner::Strip(w) => StreamInner::Strip(w.lock()), #[cfg(feature = "wincon")] - StreamInner::Wincon(w) => StreamInner::Wincon(Box::new(w.lock())), + StreamInner::Wincon(w) => StreamInner::Wincon(w.lock()), }; AutoStream { inner } } } -#[cfg(feature = "auto")] -impl AutoStream -where - S: RawStream, -{ - #[cfg(feature = "auto")] - #[inline] - pub(crate) fn auto(raw: S) -> Self { - if raw.is_terminal() { - Self::always(raw) - } else { - Self::never(raw) - } - } -} - impl std::io::Write for AutoStream where S: RawStream, diff --git a/crates/anstyle-stream/src/strip.rs b/crates/anstyle-stream/src/strip.rs index 49bb9b05..ca587a5c 100644 --- a/crates/anstyle-stream/src/strip.rs +++ b/crates/anstyle-stream/src/strip.rs @@ -20,6 +20,12 @@ where state: Default::default(), } } + + /// Get the wrapped [`RawStream`] + #[inline] + pub fn into_inner(self) -> S { + self.raw + } } impl std::io::Write for StripStream @@ -29,21 +35,19 @@ where fn write(&mut self, buf: &[u8]) -> std::io::Result { let initial_state = self.state.clone(); - let mut written = 0; - let mut possible = 0; for printable in self.state.strip_next(buf) { - possible += printable.len(); - written += self.raw.write(printable)?; + let possible = printable.len(); + let written = self.raw.write(printable)?; if possible != written { let divergence = &printable[written..]; let offset = offset_to(buf, divergence); let consumed = &buf[offset..]; self.state = initial_state; self.state.strip_next(consumed).last(); - break; + return Ok(offset); } } - Ok(written) + Ok(buf.len()) } #[inline] fn flush(&mut self) -> std::io::Result<()> { @@ -91,3 +95,66 @@ where } } } + +#[cfg(test)] +mod test { + use super::*; + use proptest::prelude::*; + use std::io::Write as _; + + proptest! { + #[test] + #[cfg_attr(miri, ignore)] // See https://github.com/AltSysrq/proptest/issues/253 + fn write_all_no_escapes(s in "\\PC*") { + let buffer = crate::Buffer::new(); + let mut stream = StripStream::new(buffer); + stream.write_all(s.as_bytes()).unwrap(); + let buffer = stream.into_inner(); + let actual = std::str::from_utf8(buffer.as_ref()).unwrap(); + assert_eq!(s, actual); + } + + #[test] + #[cfg_attr(miri, ignore)] // See https://github.com/AltSysrq/proptest/issues/253 + fn write_byte_no_escapes(s in "\\PC*") { + let buffer = crate::Buffer::new(); + let mut stream = StripStream::new(buffer); + for byte in s.as_bytes() { + stream.write_all(&[*byte]).unwrap(); + } + let buffer = stream.into_inner(); + let actual = std::str::from_utf8(buffer.as_ref()).unwrap(); + assert_eq!(s, actual); + } + + #[test] + #[cfg_attr(miri, ignore)] // See https://github.com/AltSysrq/proptest/issues/253 + fn write_all_random(s in any::>()) { + let buffer = crate::Buffer::new(); + let mut stream = StripStream::new(buffer); + stream.write_all(s.as_slice()).unwrap(); + let buffer = stream.into_inner(); + if let Ok(actual) = std::str::from_utf8(buffer.as_ref()) { + for char in actual.chars() { + assert!(!char.is_ascii() || !char.is_control() || char.is_ascii_whitespace(), "{:?} -> {:?}: {:?}", String::from_utf8_lossy(&s), actual, char); + } + } + } + + #[test] + #[cfg_attr(miri, ignore)] // See https://github.com/AltSysrq/proptest/issues/253 + fn write_byte_random(s in any::>()) { + let buffer = crate::Buffer::new(); + let mut stream = StripStream::new(buffer); + for byte in s.as_slice() { + stream.write_all(&[*byte]).unwrap(); + } + let buffer = stream.into_inner(); + if let Ok(actual) = std::str::from_utf8(buffer.as_ref()) { + for char in actual.chars() { + assert!(!char.is_ascii() || !char.is_control() || char.is_ascii_whitespace(), "{:?} -> {:?}: {:?}", String::from_utf8_lossy(&s), actual, char); + } + } + } + } +} diff --git a/crates/anstyle-stream/src/wincon.rs b/crates/anstyle-stream/src/wincon.rs index d2cb3173..0d96bc27 100644 --- a/crates/anstyle-stream/src/wincon.rs +++ b/crates/anstyle-stream/src/wincon.rs @@ -9,7 +9,10 @@ where S: RawStream, { console: anstyle_wincon::Console, - state: WinconBytes, + // `WinconBytes` is especially large compared to other variants of `AutoStream`, so boxing it + // here so `AutoStream` doesn't have to discard one allocation and create another one when + // calling `AutoStream::lock` + state: Box, } impl WinconStream @@ -21,9 +24,15 @@ where pub fn new(console: anstyle_wincon::Console) -> Self { Self { console, - state: Default::default(), + state: Box::default(), } } + + /// Get the wrapped [`RawStream`] + #[inline] + pub fn into_inner(self) -> anstyle_wincon::Console { + self.console + } } impl std::io::Write for WinconStream @@ -31,19 +40,17 @@ where S: RawStream, { fn write(&mut self, buf: &[u8]) -> std::io::Result { - let mut written = 0; - let mut possible = 0; for (style, printable) in self.state.extract_next(buf) { let fg = style.get_fg_color().and_then(cap_wincon_color); let bg = style.get_bg_color().and_then(cap_wincon_color); - written += self.console.write(fg, bg, printable.as_bytes())?; - possible += printable.len(); + let written = self.console.write(fg, bg, printable.as_bytes())?; + let possible = printable.len(); if possible != written { // HACK: Unsupported atm break; } } - Ok(written) + Ok(buf.len()) } #[inline] fn flush(&mut self) -> std::io::Result<()> { @@ -74,3 +81,54 @@ fn cap_wincon_color(color: anstyle::Color) -> Option { anstyle::Color::Rgb(_) => None, } } + +#[cfg(test)] +mod test { + use super::*; + use proptest::prelude::*; + use std::io::Write as _; + + proptest! { + #[test] + #[cfg_attr(miri, ignore)] // See https://github.com/AltSysrq/proptest/issues/253 + fn write_all_no_escapes(s in "\\PC*") { + let buffer = crate::Buffer::new(); + let mut stream = WinconStream::new(anstyle_wincon::Console::new(buffer)); + stream.write_all(s.as_bytes()).unwrap(); + let buffer = stream.into_inner().into_inner(); + let actual = std::str::from_utf8(buffer.as_ref()).unwrap(); + assert_eq!(s, actual); + } + + #[test] + #[cfg_attr(miri, ignore)] // See https://github.com/AltSysrq/proptest/issues/253 + fn write_byte_no_escapes(s in "\\PC*") { + let buffer = crate::Buffer::new(); + let mut stream = WinconStream::new(anstyle_wincon::Console::new(buffer)); + for byte in s.as_bytes() { + stream.write_all(&[*byte]).unwrap(); + } + let buffer = stream.into_inner().into_inner(); + let actual = std::str::from_utf8(buffer.as_ref()).unwrap(); + assert_eq!(s, actual); + } + + #[test] + #[cfg_attr(miri, ignore)] // See https://github.com/AltSysrq/proptest/issues/253 + fn write_all_random(s in any::>()) { + let buffer = crate::Buffer::new(); + let mut stream = WinconStream::new(anstyle_wincon::Console::new(buffer)); + stream.write_all(s.as_slice()).unwrap(); + } + + #[test] + #[cfg_attr(miri, ignore)] // See https://github.com/AltSysrq/proptest/issues/253 + fn write_byte_random(s in any::>()) { + let buffer = crate::Buffer::new(); + let mut stream = WinconStream::new(anstyle_wincon::Console::new(buffer)); + for byte in s.as_slice() { + stream.write_all(&[*byte]).unwrap(); + } + } + } +} diff --git a/crates/anstyle-wincon/src/console.rs b/crates/anstyle-wincon/src/console.rs index 380f6d3e..34154968 100644 --- a/crates/anstyle-wincon/src/console.rs +++ b/crates/anstyle-wincon/src/console.rs @@ -67,6 +67,13 @@ where } } + /// Get the inner writer + #[inline] + pub fn into_inner(mut self) -> S { + let _ = self.reset(); + self.stream.take().unwrap() + } + fn apply( &mut self, fg: Option, diff --git a/crates/anstyle/src/color.rs b/crates/anstyle/src/color.rs index e413e369..a7fe836e 100644 --- a/crates/anstyle/src/color.rs +++ b/crates/anstyle/src/color.rs @@ -122,6 +122,7 @@ impl core::ops::BitOr for Color { /// /// The user's terminal defines the meaning of the each palette code. #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(u8)] pub enum AnsiColor { /// Black: #0 (foreground code `30`, background code `40`). Black,