#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FrameIndexEntry {
pub byte_offset: u64,
pub byte_len: u32,
}
pub trait FrameIndexBuilder: Send {
fn feed(&mut self, chunk: &[u8], global_offset: u64);
fn drain(&mut self) -> Vec<FrameIndexEntry>;
fn finish(self: Box<Self>) -> std::io::Result<Vec<FrameIndexEntry>>;
fn bytes_seen(&self) -> u64;
}
#[derive(Default)]
pub(crate) struct LineAccumulator {
carry: Vec<u8>,
carry_offset: u64,
bytes_seen: u64,
finished: bool,
}
impl LineAccumulator {
pub fn new() -> Self {
Self::default()
}
pub fn bytes_seen(&self) -> u64 {
self.bytes_seen
}
pub fn feed<F>(&mut self, chunk: &[u8], global_offset: u64, mut f: F)
where
F: FnMut(&str, u64, u32),
{
if self.finished {
panic!("LineAccumulator::feed called after finish");
}
self.bytes_seen = global_offset.saturating_add(chunk.len() as u64);
if chunk.is_empty() {
return;
}
let mut start_in_chunk: usize = 0;
for (i, &b) in chunk.iter().enumerate() {
if b != b'\n' {
continue;
}
let line_end_excl_in_chunk = i + 1;
let line_byte_len_in_chunk = line_end_excl_in_chunk - start_in_chunk;
if !self.carry.is_empty() && start_in_chunk == 0 {
let mut combined = std::mem::take(&mut self.carry);
combined.extend_from_slice(&chunk[..line_end_excl_in_chunk]);
let line_offset = self.carry_offset;
let total_len = combined.len() as u32;
let trimmed = trim_trailing_newline(&combined);
let s = std::str::from_utf8(trimmed).unwrap_or("");
f(s, line_offset, total_len);
} else {
let slice = &chunk[start_in_chunk..line_end_excl_in_chunk];
let line_offset = global_offset + start_in_chunk as u64;
let total_len = line_byte_len_in_chunk as u32;
let trimmed = trim_trailing_newline(slice);
let s = std::str::from_utf8(trimmed).unwrap_or("");
f(s, line_offset, total_len);
}
start_in_chunk = line_end_excl_in_chunk;
}
if start_in_chunk < chunk.len() {
if self.carry.is_empty() {
self.carry_offset = global_offset + start_in_chunk as u64;
}
self.carry.extend_from_slice(&chunk[start_in_chunk..]);
} else if !self.carry.is_empty() && start_in_chunk == 0 {
self.carry.extend_from_slice(chunk);
}
}
pub fn finish<F>(&mut self, mut f: F)
where
F: FnMut(&str, u64, u32),
{
if self.finished {
return;
}
self.finished = true;
if !self.carry.is_empty() {
let line_offset = self.carry_offset;
let total_len = self.carry.len() as u32;
let s = std::str::from_utf8(&self.carry).unwrap_or("");
f(s, line_offset, total_len);
self.carry.clear();
}
}
}
fn trim_trailing_newline(s: &[u8]) -> &[u8] {
if let Some((&b'\n', rest)) = s.split_last() {
if let Some((&b'\r', rest2)) = rest.split_last() {
rest2
} else {
rest
}
} else {
s
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn line_accumulator_byte_by_byte_matches_single_shot() {
let s: &[u8] = b"abc\ndef\r\nghi\n";
let mut want: Vec<(String, u64, u32)> = Vec::new();
let mut single = LineAccumulator::new();
single.feed(s, 0, |line, off, len| {
want.push((line.to_string(), off, len));
});
single.finish(|line, off, len| want.push((line.to_string(), off, len)));
let mut got: Vec<(String, u64, u32)> = Vec::new();
let mut acc = LineAccumulator::new();
for (i, &b) in s.iter().enumerate() {
acc.feed(&[b], i as u64, |line, off, len| {
got.push((line.to_string(), off, len));
});
}
acc.finish(|line, off, len| got.push((line.to_string(), off, len)));
assert_eq!(got, want);
}
#[test]
fn line_accumulator_handles_no_trailing_newline() {
let s: &[u8] = b"abc\ndef";
let mut acc = LineAccumulator::new();
let mut got: Vec<(String, u64, u32)> = Vec::new();
acc.feed(s, 0, |line, off, len| {
got.push((line.to_string(), off, len));
});
acc.finish(|line, off, len| got.push((line.to_string(), off, len)));
assert_eq!(got, vec![("abc".into(), 0, 4), ("def".into(), 4, 3)]);
}
#[test]
fn line_accumulator_handles_crlf_split_across_chunks() {
let s: &[u8] = b"abc\r\ndef\n";
let mut acc = LineAccumulator::new();
let mut got: Vec<(String, u64, u32)> = Vec::new();
acc.feed(&s[..4], 0, |line, off, len| {
got.push((line.to_string(), off, len));
});
acc.feed(&s[4..], 4, |line, off, len| {
got.push((line.to_string(), off, len));
});
acc.finish(|line, off, len| got.push((line.to_string(), off, len)));
assert_eq!(got, vec![("abc".into(), 0, 5), ("def".into(), 5, 4)]);
}
}