#![allow(clippy::cast_possible_truncation)]
use std::collections::VecDeque;
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone)]
pub struct TranscriptEntry {
pub timestamp: u64,
pub data: Vec<u8>,
}
pub struct Transcript {
max_size: usize,
current_size: usize,
entries: VecDeque<TranscriptEntry>,
}
impl Transcript {
#[must_use]
pub const fn new(max_size: usize) -> Self {
Self {
max_size,
current_size: 0,
entries: VecDeque::new(),
}
}
fn now_millis() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64
}
pub fn append(&mut self, data: &[u8]) {
if data.is_empty() {
return;
}
let entry = TranscriptEntry {
timestamp: Self::now_millis(),
data: data.to_vec(),
};
let entry_size = entry.data.len();
while self.current_size + entry_size > self.max_size && !self.entries.is_empty() {
if let Some(old) = self.entries.pop_front() {
self.current_size -= old.data.len();
}
}
self.current_size += entry_size;
self.entries.push_back(entry);
}
#[must_use]
pub fn since(&self, timestamp: u64) -> Vec<&TranscriptEntry> {
self.entries
.iter()
.filter(|e| e.timestamp >= timestamp)
.collect()
}
#[must_use]
pub fn tail_bytes(&self, n: usize) -> Vec<u8> {
let mut result = Vec::new();
for entry in self.entries.iter().rev() {
if result.len() >= n {
break;
}
let remaining = n - result.len();
let take = entry.data.len().min(remaining);
result.splice(0..0, entry.data[entry.data.len() - take..].iter().copied());
}
result
}
#[must_use]
pub fn tail_lines(&self, n: usize) -> Vec<u8> {
if n == 0 {
return self.all_bytes();
}
let all = self.all_bytes();
if all.is_empty() {
return all;
}
let bytes = &all[..];
let mut lines_found = 0;
let mut pos = bytes.len();
if pos > 0 && bytes[pos - 1] == b'\n' {
pos -= 1;
}
while pos > 0 {
pos -= 1;
if bytes[pos] == b'\n' {
lines_found += 1;
if lines_found == n {
return bytes[pos + 1..].to_vec();
}
}
}
all
}
pub fn all(&self) -> impl Iterator<Item = &TranscriptEntry> {
self.entries.iter()
}
#[must_use]
pub const fn size(&self) -> usize {
self.current_size
}
#[must_use]
pub fn all_bytes(&self) -> Vec<u8> {
let mut result = Vec::with_capacity(self.current_size);
for entry in &self.entries {
result.extend_from_slice(&entry.data);
}
result
}
pub fn clear(&mut self) {
self.entries.clear();
self.current_size = 0;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_append_and_size() {
let mut t = Transcript::new(1024);
t.append(b"hello");
t.append(b"world");
assert_eq!(t.size(), 10);
}
#[test]
fn test_ring_buffer_eviction() {
let mut t = Transcript::new(10);
t.append(b"hello"); t.append(b"world"); t.append(b"!");
let all: Vec<_> = t.all().collect();
assert_eq!(all.len(), 2);
assert_eq!(all[0].data, b"world");
assert_eq!(all[1].data, b"!");
}
#[test]
fn test_tail_bytes() {
let mut t = Transcript::new(1024);
t.append(b"hello");
t.append(b"world");
let tail = t.tail_bytes(5);
assert_eq!(tail, b"world");
let tail = t.tail_bytes(7);
assert_eq!(tail, b"loworld");
}
#[test]
fn test_tail_lines() {
let mut t = Transcript::new(4096);
t.append(b"line1\nline2\nline3\nline4\nline5\n");
let tail = t.tail_lines(2);
assert_eq!(tail, b"line4\nline5\n");
let tail = t.tail_lines(1);
assert_eq!(tail, b"line5\n");
let tail = t.tail_lines(100);
assert_eq!(tail, b"line1\nline2\nline3\nline4\nline5\n");
let tail = t.tail_lines(0);
assert_eq!(tail, b"line1\nline2\nline3\nline4\nline5\n");
}
#[test]
fn test_tail_lines_no_trailing_newline() {
let mut t = Transcript::new(4096);
t.append(b"line1\nline2\nline3");
let tail = t.tail_lines(2);
assert_eq!(tail, b"line2\nline3");
let tail = t.tail_lines(1);
assert_eq!(tail, b"line3");
}
#[test]
fn test_tail_lines_across_entries() {
let mut t = Transcript::new(4096);
t.append(b"line1\nline2\n");
t.append(b"line3\nline4\n");
let tail = t.tail_lines(2);
assert_eq!(tail, b"line3\nline4\n");
}
#[test]
fn test_tail_lines_empty() {
let t = Transcript::new(4096);
let tail = t.tail_lines(10);
assert!(tail.is_empty());
}
}