use base64::Engine;
use base64::engine::general_purpose::STANDARD as BASE64;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub(super) struct FilePosition {
pub(super) generation: u64,
pub(super) offset: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
pub struct LogCursor {
pub(super) positions: Vec<FilePosition>,
}
const POSITION_BYTES: usize = 8 + 8;
impl LogCursor {
pub fn empty() -> Self {
Self::default()
}
#[allow(dead_code)] pub(crate) fn exec(&self) -> (u64, u64) {
self.positions
.first()
.map(|p| (p.generation, p.offset))
.unwrap_or((0, 0))
}
#[allow(dead_code)] pub(crate) fn runtime(&self) -> u64 {
self.positions.get(1).map(|p| p.offset).unwrap_or(0)
}
#[allow(dead_code)] pub(crate) fn kernel(&self) -> u64 {
self.positions.get(2).map(|p| p.offset).unwrap_or(0)
}
}
impl std::fmt::Display for LogCursor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let count = self.positions.len();
let mut buf = vec![0u8; 1 + count * POSITION_BYTES];
buf[0] = count as u8;
for (i, pos) in self.positions.iter().enumerate() {
let off = 1 + i * POSITION_BYTES;
buf[off..off + 8].copy_from_slice(&pos.generation.to_le_bytes());
buf[off + 8..off + 16].copy_from_slice(&pos.offset.to_le_bytes());
}
f.write_str(&BASE64.encode(buf))
}
}
impl std::str::FromStr for LogCursor {
type Err = LogCursorParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let bytes = BASE64
.decode(s.as_bytes())
.map_err(|_| LogCursorParseError::BadBase64)?;
if bytes.is_empty() {
return Err(LogCursorParseError::WrongLength(0));
}
let count = bytes[0] as usize;
let expected = 1 + count * POSITION_BYTES;
if bytes.len() != expected {
return Err(LogCursorParseError::WrongLength(bytes.len()));
}
let mut positions = Vec::with_capacity(count);
for i in 0..count {
let off = 1 + i * POSITION_BYTES;
positions.push(FilePosition {
generation: u64::from_le_bytes(bytes[off..off + 8].try_into().unwrap()),
offset: u64::from_le_bytes(bytes[off + 8..off + 16].try_into().unwrap()),
});
}
Ok(Self { positions })
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum LogCursorParseError {
#[error("cursor is not valid base64")]
BadBase64,
#[error("cursor payload has unexpected length ({0} bytes)")]
WrongLength(usize),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trip_three_positions() {
let c = LogCursor {
positions: vec![
FilePosition {
generation: 0xdead_beef_1234_5678,
offset: 42,
},
FilePosition {
generation: 0,
offset: 100,
},
FilePosition {
generation: 0,
offset: 200,
},
],
};
let s = c.to_string();
let parsed: LogCursor = s.parse().unwrap();
assert_eq!(parsed, c);
}
#[test]
fn round_trip_empty() {
let c = LogCursor::empty();
let s = c.to_string();
let parsed: LogCursor = s.parse().unwrap();
assert_eq!(parsed, c);
}
#[test]
fn round_trip_single_position() {
let c = LogCursor {
positions: vec![FilePosition {
generation: 42,
offset: 100,
}],
};
let s = c.to_string();
let parsed: LogCursor = s.parse().unwrap();
assert_eq!(parsed, c);
}
#[test]
fn parse_rejects_wrong_length() {
let mut payload = vec![0u8; 6];
payload[0] = 2;
let s = BASE64.encode(&payload);
assert!(matches!(
s.parse::<LogCursor>(),
Err(LogCursorParseError::WrongLength(6))
));
}
#[test]
fn parse_rejects_garbage() {
assert!(matches!(
"not-base64!".parse::<LogCursor>(),
Err(LogCursorParseError::BadBase64)
));
}
#[test]
fn sdk_accessors_handle_short_cursors() {
let c = LogCursor::empty();
assert_eq!(c.exec(), (0, 0));
assert_eq!(c.runtime(), 0);
assert_eq!(c.kernel(), 0);
let c = LogCursor {
positions: vec![FilePosition {
generation: 5,
offset: 9,
}],
};
assert_eq!(c.exec(), (5, 9));
assert_eq!(c.runtime(), 0);
assert_eq!(c.kernel(), 0);
}
}