use std::io::Write;
const MAX_PENDING: usize = 64 * 1024;
pub(super) struct LinePrefixer {
label: String,
pending: Vec<u8>,
}
impl LinePrefixer {
pub(super) fn new(label: &str, prefix: bool, allow_color: bool) -> Self {
if !prefix {
return Self {
label: String::new(),
pending: Vec::new(),
};
}
let plain = format!("{label} | ");
let label = crate::ui::paint(
crate::ui::service_style(label),
&plain,
allow_color && crate::ui::stdout_colored(),
);
Self {
label,
pending: Vec::new(),
}
}
pub(super) fn write(&mut self, out: &mut impl Write, chunk: &[u8]) -> std::io::Result<()> {
self.pending.extend_from_slice(chunk);
while let Some(nl) = self.pending.iter().position(|&b| b == b'\n') {
out.write_all(self.label.as_bytes())?;
out.write_all(&self.pending[..=nl])?;
self.pending.drain(..=nl);
}
if self.pending.len() >= MAX_PENDING {
out.write_all(self.label.as_bytes())?;
out.write_all(&self.pending)?;
out.write_all(b"\n")?;
self.pending.clear();
}
out.flush()
}
pub(super) fn flush_tail(&mut self, out: &mut impl Write) {
if !self.pending.is_empty() {
let _ = out.write_all(self.label.as_bytes());
let _ = out.write_all(&self.pending);
let _ = out.write_all(b"\n");
let _ = out.flush();
self.pending.clear();
}
}
}
#[cfg(test)]
mod tests {
use super::LinePrefixer;
#[test]
fn line_prefixer_tags_lines_and_buffers_partials() {
let mut p = LinePrefixer::new("web", true, false);
let mut out: Vec<u8> = Vec::new();
p.write(&mut out, b"hello\nwor").unwrap();
assert_eq!(out, b"web | hello\n");
p.write(&mut out, b"ld\n").unwrap();
assert_eq!(out, b"web | hello\nweb | world\n");
}
#[test]
fn line_prefixer_flush_tail_emits_unterminated_line() {
let mut p = LinePrefixer::new("db", true, false);
let mut out: Vec<u8> = Vec::new();
p.write(&mut out, b"partial").unwrap();
assert!(out.is_empty(), "a line with no newline is held back");
p.flush_tail(&mut out);
assert_eq!(out, b"db | partial\n");
}
#[test]
fn line_prefixer_bounds_a_newlineless_flood() {
use super::MAX_PENDING;
let mut p = LinePrefixer::new("web", true, false);
let mut out: Vec<u8> = Vec::new();
let chunk = vec![b'x'; 4096];
for _ in 0..((MAX_PENDING / chunk.len()) + 2) {
p.write(&mut out, &chunk).unwrap();
}
assert!(
!out.is_empty(),
"the over-long partial was emitted, not held"
);
assert!(
p.pending.len() < MAX_PENDING,
"pending stays bounded under the cap, was {}",
p.pending.len()
);
assert!(
out.starts_with(b"web | "),
"the flushed partial is prefixed"
);
}
#[test]
fn line_prefixer_no_prefix_emits_bare_lines() {
let mut p = LinePrefixer::new("web", false, false);
let mut out: Vec<u8> = Vec::new();
p.write(&mut out, b"hello\n").unwrap();
assert_eq!(out, b"hello\n");
p.write(&mut out, b"tail").unwrap();
p.flush_tail(&mut out);
assert_eq!(out, b"hello\ntail\n");
}
#[test]
fn line_prefixer_surfaces_a_broken_pipe() {
struct ClosedPipe;
impl std::io::Write for ClosedPipe {
fn write(&mut self, _: &[u8]) -> std::io::Result<usize> {
Err(std::io::Error::new(
std::io::ErrorKind::BrokenPipe,
"broken pipe",
))
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
let mut p = LinePrefixer::new("web", true, false);
let err = p
.write(&mut ClosedPipe, b"hello\n")
.expect_err("a closed sink must be reported");
assert_eq!(err.kind(), std::io::ErrorKind::BrokenPipe);
}
}