use std::fmt;
use faucet_core::FaucetError;
pub const LSN_BYTES: usize = 10;
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Lsn([u8; LSN_BYTES]);
impl Lsn {
pub const ZERO: Lsn = Lsn([0u8; LSN_BYTES]);
pub fn from_array(bytes: [u8; LSN_BYTES]) -> Self {
Lsn(bytes)
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, FaucetError> {
if bytes.len() != LSN_BYTES {
return Err(FaucetError::Source(format!(
"mssql-cdc: LSN must be exactly {LSN_BYTES} bytes, got {}",
bytes.len()
)));
}
let mut arr = [0u8; LSN_BYTES];
arr.copy_from_slice(bytes);
Ok(Lsn(arr))
}
pub fn from_hex(s: &str) -> Result<Self, FaucetError> {
let hex = s
.strip_prefix("0x")
.or_else(|| s.strip_prefix("0X"))
.unwrap_or(s);
if hex.len() != LSN_BYTES * 2 {
return Err(FaucetError::Source(format!(
"mssql-cdc: LSN hex must be {} digits, got {} ({s:?})",
LSN_BYTES * 2,
hex.len()
)));
}
let mut arr = [0u8; LSN_BYTES];
for (i, byte) in arr.iter_mut().enumerate() {
let pair = &hex[i * 2..i * 2 + 2];
*byte = u8::from_str_radix(pair, 16).map_err(|e| {
FaucetError::Source(format!("mssql-cdc: invalid LSN hex {s:?}: {e}"))
})?;
}
Ok(Lsn(arr))
}
pub fn to_hex(&self) -> String {
let mut out = String::with_capacity(LSN_BYTES * 2);
for b in &self.0 {
out.push_str(&format!("{b:02x}"));
}
out
}
pub fn increment(&self) -> Option<Self> {
let mut arr = self.0;
for byte in arr.iter_mut().rev() {
if *byte == u8::MAX {
*byte = 0;
} else {
*byte += 1;
return Some(Lsn(arr));
}
}
None
}
}
impl fmt::Debug for Lsn {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Lsn({})", self.to_hex())
}
}
impl fmt::Display for Lsn {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.to_hex())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hex_round_trips() {
let hex = "0000002a000000550003";
let lsn = Lsn::from_hex(hex).unwrap();
assert_eq!(lsn.to_hex(), hex);
}
#[test]
fn hex_tolerates_0x_prefix_and_uppercase() {
let a = Lsn::from_hex("0x0000002A000000550003").unwrap();
let b = Lsn::from_hex("0000002a000000550003").unwrap();
assert_eq!(a, b);
}
#[test]
fn hex_rejects_wrong_length_and_non_hex() {
assert!(Lsn::from_hex("00").is_err());
assert!(Lsn::from_hex("").is_err());
assert!(Lsn::from_hex("0000002a000000550zzz").is_err());
}
#[test]
fn from_bytes_requires_exact_width() {
assert!(Lsn::from_bytes(&[0u8; 10]).is_ok());
assert!(Lsn::from_bytes(&[0u8; 9]).is_err());
assert!(Lsn::from_bytes(&[0u8; 11]).is_err());
}
#[test]
fn ordering_is_numeric_big_endian() {
let small = Lsn::from_hex("00000000000000000001").unwrap();
let big = Lsn::from_hex("00000000000000000002").unwrap();
let bigger_high_byte = Lsn::from_hex("01000000000000000000").unwrap();
assert!(small < big);
assert!(big < bigger_high_byte);
assert_eq!(Lsn::ZERO, Lsn::from_hex("00000000000000000000").unwrap());
assert!(Lsn::ZERO < small);
}
#[test]
fn increment_is_the_next_value() {
let lsn = Lsn::from_hex("000000000000000000ff").unwrap();
let next = lsn.increment().unwrap();
assert_eq!(next.to_hex(), "00000000000000000100");
assert!(lsn < next);
assert!(Lsn::from_hex("00000000000000000100").unwrap() <= next);
}
#[test]
fn increment_simple_low_byte() {
let lsn = Lsn::from_hex("00000000000000000001").unwrap();
assert_eq!(lsn.increment().unwrap().to_hex(), "00000000000000000002");
}
#[test]
fn increment_max_saturates_to_none() {
let max = Lsn::from_array([0xFF; LSN_BYTES]);
assert!(max.increment().is_none());
}
#[test]
fn increment_of_zero_is_one() {
assert_eq!(
Lsn::ZERO.increment().unwrap().to_hex(),
"00000000000000000001"
);
}
}