use memchr::memmem;
pub const DEFAULT_RING_CAP: usize = 512 * 1024;
pub struct Ring {
buf: Vec<u8>,
base: u64, cap: usize,
}
impl Ring {
pub fn new(cap: usize) -> Self {
Self {
buf: Vec::new(),
base: 0,
cap: cap.max(1),
}
}
pub fn total(&self) -> u64 {
self.base + self.buf.len() as u64
}
pub fn append(&mut self, bytes: &[u8]) {
self.buf.extend_from_slice(bytes);
if self.buf.len() > self.cap * 2 {
let drop = self.buf.len() - self.cap;
self.buf.drain(..drop);
self.base += drop as u64;
}
}
pub fn slice_from(&self, cursor: u64) -> (u64, &[u8]) {
let start = cursor.clamp(self.base, self.total());
let idx = (start - self.base) as usize;
(start, &self.buf[idx..])
}
pub fn base(&self) -> u64 {
self.base
}
pub fn tail_lines(&self, lines: usize) -> String {
if lines == 0 {
return String::new();
}
let buf = &self.buf;
let end = buf.len() - usize::from(buf.last() == Some(&b'\n'));
let mut count = 0;
let mut start = 0;
for i in (0..end).rev() {
if buf[i] == b'\n' {
count += 1;
if count == lines {
start = i + 1;
break;
}
}
}
let tail = String::from_utf8_lossy(&buf[start..]);
let all: Vec<&str> = tail.lines().collect();
all.join("\n")
}
}
pub enum Matcher {
Substr(Vec<u8>),
Regex(regex::bytes::Regex),
}
impl Matcher {
pub fn build(pattern: &str, regex: bool) -> Result<Self, String> {
if regex {
regex::bytes::Regex::new(pattern).map(Matcher::Regex).map_err(|e| e.to_string())
} else {
Ok(Matcher::Substr(pattern.as_bytes().to_vec()))
}
}
pub fn find_end(&self, hay: &[u8]) -> Option<usize> {
match self {
Matcher::Substr(needle) => memmem::find(hay, needle).map(|i| i + needle.len()),
Matcher::Regex(re) => re.find(hay).map(|m| m.end()),
}
}
pub fn resume_from(&self, start: u64, end: u64) -> u64 {
match self {
Matcher::Substr(needle) => {
let overlap = needle.len().saturating_sub(1) as u64;
end.saturating_sub(overlap).max(start)
}
Matcher::Regex(_) => start,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ring_drops_oldest_and_tracks_offset() {
let mut ring = Ring::new(DEFAULT_RING_CAP);
ring.append(b"hello ");
ring.append(b"world");
assert_eq!(ring.total(), 11);
let (abs, slice) = ring.slice_from(6);
assert_eq!(abs, 6);
assert_eq!(slice, b"world");
}
#[test]
fn ring_cursor_before_window_clamps_to_base() {
let mut ring = Ring::new(DEFAULT_RING_CAP);
ring.append(b"abc");
let (abs, slice) = ring.slice_from(0);
assert_eq!(abs, 0);
assert_eq!(slice, b"abc");
}
#[test]
fn ring_trims_to_cap_and_moves_base() {
let mut ring = Ring::new(4);
ring.append(&[b'x'; 9]);
assert_eq!(ring.total(), 9);
assert_eq!(ring.base(), 5);
let (abs, slice) = ring.slice_from(0);
assert_eq!(abs, 5);
assert_eq!(slice.len(), 4);
}
#[test]
fn tail_lines_returns_last_lines() {
let mut ring = Ring::new(DEFAULT_RING_CAP);
ring.append(b"one\r\ntwo\r\nthree\r\npartial");
assert_eq!(ring.tail_lines(2), "three\npartial");
assert_eq!(ring.tail_lines(10), "one\ntwo\nthree\npartial");
assert_eq!(ring.tail_lines(0), "");
}
#[test]
fn tail_lines_ignores_trailing_newline() {
let mut ring = Ring::new(DEFAULT_RING_CAP);
ring.append(b"a\nb\n");
assert_eq!(ring.tail_lines(1), "b");
assert_eq!(ring.tail_lines(2), "a\nb");
}
#[test]
fn substr_match_returns_offset_past_match() {
let m = Matcher::build("ready> ", false).unwrap();
assert_eq!(m.find_end(b"value = 1\r\nready> "), Some(18));
assert_eq!(m.find_end(b"still running"), None);
}
#[test]
fn regex_match_finds_tagged_line() {
let m = Matcher::build(r"\(T\d\)", true).unwrap();
assert_eq!(m.find_end(b"timeout (T2) 61 sec"), Some(12));
assert_eq!(m.find_end(b"timeout pending"), None);
}
#[test]
fn substr_scan_resumes_with_overlap_and_regex_rescans() {
let sub = Matcher::build("abc", false).unwrap();
assert_eq!(sub.resume_from(0, 100), 98);
assert_eq!(sub.resume_from(99, 100), 99); let re = Matcher::build("a.*b", true).unwrap();
assert_eq!(re.resume_from(5, 100), 5);
}
}