pub(crate) const MAX_LOG_TAIL_BYTES: u64 = 2 * 1024 * 1024;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct LogWithMeta {
pub(crate) content: String,
pub(crate) total_bytes: u64,
pub(crate) truncated: bool,
}
impl LogWithMeta {
pub(crate) const fn empty() -> Self {
Self {
content: String::new(),
total_bytes: 0,
truncated: false,
}
}
}
pub(crate) fn read_log_tail_with_meta(path: &std::path::Path) -> std::io::Result<LogWithMeta> {
let total_bytes = std::fs::metadata(path)?.len();
let content = read_log_tail_of_len(path, total_bytes)?;
Ok(LogWithMeta {
content,
total_bytes,
truncated: total_bytes > MAX_LOG_TAIL_BYTES,
})
}
pub(crate) fn read_log_tail(path: &std::path::Path) -> std::io::Result<String> {
let len = std::fs::metadata(path)?.len();
read_log_tail_of_len(path, len)
}
fn read_log_tail_of_len(path: &std::path::Path, len: u64) -> std::io::Result<String> {
use std::io::{Read, Seek, SeekFrom};
if len <= MAX_LOG_TAIL_BYTES {
let bytes = std::fs::read(path)?;
return Ok(strip_ansi_noise(&String::from_utf8_lossy(&bytes)));
}
let omitted = len - MAX_LOG_TAIL_BYTES;
let mut file = std::fs::File::open(path)?;
file.seek(SeekFrom::Start(omitted))?;
let mut buf = Vec::with_capacity(MAX_LOG_TAIL_BYTES as usize);
#[allow(
clippy::verbose_file_reads,
reason = "reads from the sought offset to end-of-file for the log's tail, not the whole \
file `fs::read` would read"
)]
file.read_to_end(&mut buf)?;
let utf8_start = buf
.iter()
.take(4)
.position(|&byte| !(0x80..0xC0).contains(&byte))
.unwrap_or(0);
let start = buf[utf8_start..]
.iter()
.position(|&byte| byte == b'\n')
.map_or(utf8_start, |offset| utf8_start + offset + 1);
let tail = String::from_utf8_lossy(&buf[start..]);
Ok(format!(
"... [{omitted} bytes omitted; showing the last {MAX_LOG_TAIL_BYTES} bytes] ...\n{}",
strip_ansi_noise(&tail)
))
}
pub(crate) fn strip_ansi_noise(input: &str) -> String {
let without_escapes = strip_escape_sequences(input);
collapse_carriage_returns(&without_escapes)
}
fn strip_escape_sequences(input: &str) -> String {
let mut out = String::with_capacity(input.len());
let mut chars = input.chars().peekable();
while let Some(current) = chars.next() {
if current != '\u{1B}' {
out.push(current);
continue;
}
match chars.peek() {
Some('[') => {
chars.next();
for pc in chars.by_ref() {
if ('@'..='~').contains(&pc) {
break;
}
}
}
Some(']') => {
chars.next();
loop {
match chars.next() {
Some('\u{7}') | None => break,
Some('\u{1B}') => {
if chars.peek() == Some(&'\\') {
chars.next();
}
break;
}
Some(_) => {}
}
}
}
Some(_) => {
chars.next();
}
None => {}
}
}
out
}
fn collapse_carriage_returns(input: &str) -> String {
input
.split('\n')
.map(|line| line.rsplit('\r').next().unwrap_or(line))
.collect::<Vec<_>>()
.join("\n")
}