Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions crates/anstyle-stream/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,7 @@ harness = false
[[bench]]
name = "wincon"
harness = false

[[bench]]
name = "stream"
harness = false
82 changes: 82 additions & 0 deletions crates/anstyle-stream/benches/stream.rs
Original file line number Diff line number Diff line change
@@ -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);
27 changes: 25 additions & 2 deletions crates/anstyle-stream/src/adapter/strip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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())
}
Expand Down Expand Up @@ -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
Expand Down
43 changes: 24 additions & 19 deletions crates/anstyle-stream/src/auto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ enum StreamInner<S: RawStream> {
PassThrough(S),
Strip(StripStream<S>),
#[cfg(feature = "wincon")]
Wincon(Box<WinconStream<S>>),
Wincon(WinconStream<S>),
}

impl<S> AutoStream<S>
Expand Down Expand Up @@ -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)
Expand All @@ -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<S> AutoStream<S>
Expand All @@ -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<S> AutoStream<S>
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<S> std::io::Write for AutoStream<S>
where
S: RawStream,
Expand Down
79 changes: 73 additions & 6 deletions crates/anstyle-stream/src/strip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ where
state: Default::default(),
}
}

/// Get the wrapped [`RawStream`]
#[inline]
pub fn into_inner(self) -> S {
self.raw
}
}

impl<S> std::io::Write for StripStream<S>
Expand All @@ -29,21 +35,19 @@ where
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
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<()> {
Expand Down Expand Up @@ -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::<Vec<u8>>()) {
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::<Vec<u8>>()) {
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);
}
}
}
}
}
Loading