use crate::Error;
pub struct WalCursorReader<'a> {
buf: &'a [u8],
pos: usize,
committed: usize,
}
impl<'a> WalCursorReader<'a> {
pub fn new(buf: &'a [u8]) -> Self {
Self {
buf,
pos: 0,
committed: 0,
}
}
pub fn with_committed(buf: &'a [u8], committed: usize) -> Self {
Self {
buf,
pos: 0,
committed: committed.min(buf.len()),
}
}
pub fn set_committed(&mut self, offset: usize) {
let clamped = offset.min(self.buf.len());
if clamped > self.committed {
self.committed = clamped;
}
}
pub fn committed(&self) -> usize {
self.committed
}
pub fn position(&self) -> usize {
self.pos
}
pub fn read_committed(&mut self) -> Result<Option<&[u8]>, Error> {
if self.pos == self.buf.len() {
return Ok(None);
}
if self.pos + 4 > self.buf.len() {
return Err(Error::TruncatedFrame);
}
let len = u32::from_be_bytes(self.buf[self.pos..self.pos + 4].try_into().unwrap()) as usize;
let payload_start = self.pos + 4;
let payload_end = payload_start + len;
if payload_end > self.buf.len() {
return Err(Error::TruncatedFrame);
}
if payload_end > self.committed {
return Ok(None);
}
self.pos = payload_end;
Ok(Some(&self.buf[payload_start..payload_end]))
}
pub fn next_record(&mut self) -> Result<Option<&[u8]>, Error> {
if self.pos == self.buf.len() {
return Ok(None);
}
if self.pos + 4 > self.buf.len() {
return Err(Error::TruncatedFrame);
}
let len = u32::from_be_bytes(self.buf[self.pos..self.pos + 4].try_into().unwrap()) as usize;
let payload_start = self.pos + 4;
let payload_end = payload_start + len;
if payload_end > self.buf.len() {
return Err(Error::TruncatedFrame);
}
self.pos = payload_end;
Ok(Some(&self.buf[payload_start..payload_end]))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::SegmentWriter;
fn build(records: &[&[u8]]) -> Vec<u8> {
let mut buf = Vec::new();
let mut w = SegmentWriter::new(&mut buf);
for r in records {
w.write(r).unwrap();
}
buf
}
fn end_of_block(buf: &[u8], n: usize) -> usize {
let mut pos = 0usize;
for _ in 0..=n {
let len = u32::from_be_bytes(buf[pos..pos + 4].try_into().unwrap()) as usize;
pos += 4 + len;
}
pos
}
#[test]
fn nothing_visible_until_watermark_advances() {
let buf = build(&[b"a", b"b", b"c"]);
let mut r = WalCursorReader::new(&buf);
assert!(
r.read_committed().unwrap().is_none(),
"watermark at 0 -> nothing"
);
}
#[test]
fn advance_to_first_block_exposes_first_block_only() {
let buf = build(&[b"a", b"b", b"c"]);
let mut r = WalCursorReader::new(&buf);
r.set_committed(end_of_block(&buf, 0));
assert_eq!(r.read_committed().unwrap().unwrap(), b"a");
assert!(
r.read_committed().unwrap().is_none(),
"second block not yet committed"
);
}
#[test]
fn advance_again_exposes_more_blocks() {
let buf = build(&[b"a", b"b", b"c"]);
let mut r = WalCursorReader::new(&buf);
r.set_committed(end_of_block(&buf, 0));
let _ = r.read_committed().unwrap();
r.set_committed(end_of_block(&buf, 2));
assert_eq!(r.read_committed().unwrap().unwrap(), b"b");
assert_eq!(r.read_committed().unwrap().unwrap(), b"c");
assert!(r.read_committed().unwrap().is_none());
}
#[test]
fn watermark_is_monotonic() {
let buf = build(&[b"a", b"b"]);
let mut r = WalCursorReader::new(&buf);
r.set_committed(end_of_block(&buf, 1));
assert_eq!(r.committed(), end_of_block(&buf, 1));
r.set_committed(0); assert_eq!(
r.committed(),
end_of_block(&buf, 1),
"backward moves rejected"
);
}
#[test]
fn watermark_clamps_to_buffer_length() {
let buf = build(&[b"a"]);
let mut r = WalCursorReader::new(&buf);
r.set_committed(9999);
assert_eq!(r.committed(), buf.len());
}
#[test]
fn with_committed_seeds_watermark_at_open() {
let buf = build(&[b"a", b"b"]);
let mut r = WalCursorReader::with_committed(&buf, end_of_block(&buf, 0));
assert_eq!(r.read_committed().unwrap().unwrap(), b"a");
assert!(r.read_committed().unwrap().is_none());
}
#[test]
fn dirty_next_record_ignores_watermark() {
let buf = build(&[b"a", b"b"]);
let mut r = WalCursorReader::new(&buf); assert_eq!(r.next_record().unwrap().unwrap(), b"a");
assert_eq!(r.next_record().unwrap().unwrap(), b"b");
assert!(r.next_record().unwrap().is_none());
}
#[test]
fn truncated_tail_surfaces_typed_error() {
let mut buf = build(&[b"first"]);
buf.extend_from_slice(&[0, 0, 0, 10]);
buf.extend_from_slice(b"abc"); let mut r = WalCursorReader::new(&buf);
r.set_committed(buf.len());
assert_eq!(r.read_committed().unwrap().unwrap(), b"first");
assert!(matches!(r.read_committed(), Err(Error::TruncatedFrame)));
}
#[test]
fn position_advances_with_reads() {
let buf = build(&[b"a", b"bb"]);
let mut r = WalCursorReader::new(&buf);
r.set_committed(buf.len());
assert_eq!(r.position(), 0);
let _ = r.read_committed().unwrap();
assert_eq!(r.position(), end_of_block(&buf, 0));
}
}