use tokio::io::{AsyncRead, AsyncReadExt};
pub(crate) const MAX_STDERR_BYTES: usize = 8 * 1024;
const READ_CHUNK_BYTES: usize = 8 * 1024;
#[derive(Debug, Default)]
pub(crate) struct StderrTail {
bytes: Vec<u8>,
elided: bool,
}
impl StderrTail {
pub(crate) async fn capture<R>(stderr: Option<R>) -> Self
where
R: AsyncRead + Unpin,
{
let mut tail = Self::default();
let Some(mut stderr) = stderr else {
return tail;
};
let mut chunk = vec![0_u8; READ_CHUNK_BYTES];
loop {
match stderr.read(&mut chunk).await {
Ok(0) | Err(_) => return tail,
Ok(count) => tail.push(&chunk[..count]),
}
}
}
pub(crate) fn push(&mut self, chunk: &[u8]) {
self.bytes.extend_from_slice(chunk);
if self.bytes.len() <= MAX_STDERR_BYTES {
return;
}
let cut = ceil_utf8_boundary(&self.bytes, self.bytes.len() - MAX_STDERR_BYTES);
self.bytes.drain(..cut);
self.elided = true;
}
pub(crate) fn elided(&self) -> bool {
self.elided
}
pub(crate) fn finish(self) -> String {
let text = String::from_utf8_lossy(&self.bytes);
let trimmed = text.trim();
if self.elided {
format!("{}{trimmed}", rho_sdk::ELLIPSIS)
} else {
trimmed.to_string()
}
}
}
fn ceil_utf8_boundary(bytes: &[u8], index: usize) -> usize {
let mut index = index.min(bytes.len());
while index < bytes.len() && bytes[index] & 0b1100_0000 == 0b1000_0000 {
index += 1;
}
index
}
#[cfg(test)]
#[path = "stderr_tail_tests.rs"]
mod tests;