use std::collections::VecDeque;
use std::sync::Mutex;
pub const MAX_LINE: usize = 4096;
pub const DEFAULT_CAP: usize = 20_000;
pub const MAX_READ: usize = 2_000;
#[derive(Debug)]
struct Ring {
lines: VecDeque<String>,
first: u64,
cap: usize,
}
#[derive(Debug)]
pub struct BootLog {
ring: Mutex<Ring>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Chunk {
pub dropped: u64,
pub lines: Vec<String>,
pub next: u64,
}
impl Default for BootLog {
fn default() -> Self {
BootLog::new(DEFAULT_CAP)
}
}
impl BootLog {
pub const fn new(cap: usize) -> BootLog {
BootLog { ring: Mutex::new(Ring { lines: VecDeque::new(), first: 0, cap }) }
}
pub fn push(&self, text: &str) {
let mut r = self.ring.lock().unwrap_or_else(|p| p.into_inner());
for line in text.split('\n') {
let line = line.strip_suffix('\r').unwrap_or(line);
if line.trim().is_empty() {
continue;
}
let line = if line.len() > MAX_LINE {
let mut cut = MAX_LINE;
while !line.is_char_boundary(cut) {
cut -= 1;
}
format!("{} … (cut at {MAX_LINE} bytes)", &line[..cut])
} else {
line.to_string()
};
if r.cap == 0 {
r.first += 1;
continue;
}
if r.lines.len() == r.cap {
r.lines.pop_front();
r.first += 1;
}
r.lines.push_back(line);
}
}
pub fn total(&self) -> u64 {
let r = self.ring.lock().unwrap_or_else(|p| p.into_inner());
r.first + r.lines.len() as u64
}
pub fn dropped(&self) -> u64 {
self.ring.lock().unwrap_or_else(|p| p.into_inner()).first
}
pub fn since(&self, from: u64, max: usize) -> Chunk {
let r = self.ring.lock().unwrap_or_else(|p| p.into_inner());
let end = r.first + r.lines.len() as u64;
let start = from.max(r.first).min(end);
let dropped = start.saturating_sub(from.min(end));
let skip = (start - r.first) as usize;
let lines: Vec<String> = r.lines.iter().skip(skip).take(max).cloned().collect();
let next = start + lines.len() as u64;
Chunk { dropped, lines, next }
}
}
pub fn dropped_marker(n: u64) -> String {
format!("… {n} lines dropped (the VM's log holds its newest lines only) …")
}
impl Chunk {
pub fn render(&self) -> String {
let mut out = format!("next = {}\ndropped = {}\n\n", self.next, self.dropped);
for l in &self.lines {
out.push_str(l);
out.push('\n');
}
out
}
pub fn parse(text: &str) -> Result<Chunk, String> {
let (head, body) = text.split_once("\n\n").ok_or("a log chunk without its header")?;
let mut next = None;
let mut dropped = 0;
for l in head.lines() {
match l.split_once('=').map(|(k, v)| (k.trim(), v.trim())) {
Some(("next", v)) => next = v.parse().ok(),
Some(("dropped", v)) => dropped = v.parse().map_err(|_| format!("`{l}` is not a count"))?,
_ => {}
}
}
let next = next.ok_or("a log chunk without `next`")?;
Ok(Chunk { dropped, lines: body.lines().map(str::to_string).collect(), next })
}
pub fn stored(&self) -> Vec<String> {
let mut v = Vec::with_capacity(self.lines.len() + 1);
if self.dropped > 0 {
v.push(dropped_marker(self.dropped));
}
v.extend(self.lines.iter().cloned());
v
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_reader_gets_every_line_once_and_continues_where_it_left_off() {
let log = BootLog::new(10);
log.push("one\ntwo\r\n\nthree");
let c = log.since(0, 2);
assert_eq!((c.lines.clone(), c.next, c.dropped), (vec!["one".to_string(), "two".to_string()], 2, 0));
let c = log.since(c.next, 100);
assert_eq!((c.lines, c.next), (vec!["three".to_string()], 3));
assert_eq!(log.since(3, 100).lines.len(), 0, "nothing new");
assert_eq!(log.since(99, 100).next, 3, "a reader ahead of the log is brought back to its end");
}
#[test]
fn a_full_log_drops_the_oldest_and_counts_them_and_the_reader_is_told() {
let log = BootLog::new(3);
for i in 0..8 {
log.push(&format!("line {i}"));
}
assert_eq!((log.total(), log.dropped()), (8, 5));
let c = log.since(0, 100);
assert_eq!(c.dropped, 5);
assert_eq!(c.lines, vec!["line 5", "line 6", "line 7"]);
assert_eq!(c.stored()[0], dropped_marker(5));
assert!(c.stored()[0].contains("5 lines dropped"));
let later = log.since(6, 100);
assert_eq!((later.dropped, later.lines.len()), (0, 2), "a reader that kept up loses nothing");
}
#[test]
fn a_long_line_is_cut_on_a_character_boundary_and_the_wire_round_trips() {
let log = BootLog::new(4);
log.push(&"å".repeat(MAX_LINE));
let c = log.since(0, 10);
assert!(c.lines[0].len() < MAX_LINE + 40 && c.lines[0].ends_with("(cut at 4096 bytes)"));
let wire = c.render();
assert_eq!(Chunk::parse(&wire).unwrap(), c);
assert!(Chunk::parse("garbage").is_err());
}
}