109 lines
3.5 KiB
Rust
109 lines
3.5 KiB
Rust
//! 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));
|
|
}
|
|
}
|