//! Presentation only. Never rewrite archives or change internal command output. use crate::{Executed, Result}; use std::io::{Read, Seek, SeekFrom}; pub const DEFAULT_BYTES: u32 = 16 * 1024; const MARKER: &str = "\n... [inline output omitted; see stream archive path for full available output] ...\n"; fn head(text: &str, bytes: usize) -> &str { let mut end = bytes.min(text.len()); while !text.is_char_boundary(end) { end -= 1; } &text[..end] } fn tail(text: &str, bytes: usize) -> &str { let mut start = text.len().saturating_sub(bytes); while !text.is_char_boundary(start) { start += 1; } &text[start..] } fn bounded(text: &str, budget: usize) -> (String, bool) { if text.len() <= budget { return (text.to_owned(), false); } let first = budget.div_ceil(2); ( format!( "{}{MARKER}{}", head(text, first), tail(text, budget - first) ), true, ) } fn from_file(path: &str, budget: usize) -> Result<(String, bool)> { let mut file = std::fs::File::open(path)?; let size = file.metadata()?.len(); if size <= budget as u64 { let mut bytes = Vec::new(); file.take(budget as u64).read_to_end(&mut bytes)?; return Ok(bounded(&String::from_utf8_lossy(&bytes), budget)); } let first = budget.div_ceil(2); let last = budget - first; let mut begin = Vec::new(); (&mut file).take(first as u64).read_to_end(&mut begin)?; file.seek(SeekFrom::Start(size - last as u64))?; let mut end = Vec::new(); file.take(last as u64).read_to_end(&mut end)?; // UTF-8 replacement can expand binary input; bound displayed UTF-8 too. Ok(( format!( "{}{MARKER}{}", head(&String::from_utf8_lossy(&begin), first), tail(&String::from_utf8_lossy(&end), last) ), true, )) } pub fn apply(result: &mut Executed, budget: u32) { let service_cut = result.truncated; result.truncated = result.service_truncated; for (stream, text, path) in [ ("stdout", &mut result.stdout, &result.stdout_path), ("stderr", &mut result.stderr, &result.stderr_path), ] { let excerpt = path.as_deref().map(|p| from_file(p, budget as usize)); let (display, cut) = match excerpt { Some(Ok(value)) => value, Some(Err(e)) => { result .output_notice .get_or_insert_with(String::new) .push_str(&format!( "{stream} excerpt could not read archive: {}; showing service excerpt. ", e.message )); let (display, cut) = bounded(text, budget as usize); (display, cut || service_cut) } None => { let (display, cut) = bounded(text, budget as usize); (display, cut || service_cut) } }; *text = display; result.truncated |= cut; } } #[cfg(test)] mod tests { use super::*; #[test] fn utf8_and_tiny_budgets_are_bounded() { for budget in 0..40 { let text = "🦀é".repeat(20); let (value, cut) = bounded(&text, budget); assert!(cut); assert!(value.len() <= budget + MARKER.len()); assert!(value.contains(MARKER)); } assert_eq!(bounded("", 0), (String::new(), false)); assert_eq!(bounded("small", 5), ("small".into(), false)); } }