use rupg_common::SqlState;
use crate::binary::Recv;
use crate::error::TypeError;
use crate::number::is_space;
const MAX_NXIP: usize = (0x3fff_ffff - 20) / 8;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Snapshot {
pub xmin: u64,
pub xmax: u64,
pub xip: Vec<u64>,
}
impl Snapshot {
#[must_use]
pub fn to_bytes(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(20 + 8 * self.xip.len());
let nxip = i32::try_from(self.xip.len()).unwrap_or(i32::MAX);
out.extend_from_slice(&nxip.to_be_bytes());
out.extend_from_slice(&self.xmin.to_be_bytes());
out.extend_from_slice(&self.xmax.to_be_bytes());
for xid in &self.xip {
out.extend_from_slice(&xid.to_be_bytes());
}
out
}
#[must_use]
pub fn from_bytes(bytes: &[u8]) -> Option<Snapshot> {
let mut recv = Recv::new(bytes);
let nxip = usize::try_from(recv.i32().ok()?).ok()?;
let xmin = recv.i64().ok()?.cast_unsigned();
let xmax = recv.i64().ok()?.cast_unsigned();
if recv.remaining() != nxip * 8 {
return None;
}
let xip =
(0..nxip).map(|_| recv.i64().map(i64::cast_unsigned)).collect::<Result<_, _>>().ok()?;
Some(Snapshot { xmin, xmax, xip })
}
#[must_use]
pub fn is_visible(&self, xid: u64) -> bool {
if xid < self.xmin {
return true;
}
xid < self.xmax && self.xip.binary_search(&xid).is_err()
}
}
fn strtou64(b: &[u8]) -> (u64, usize) {
let mut i = 0;
while b.get(i).is_some_and(|&c| is_space(c)) {
i += 1;
}
let neg = b.get(i) == Some(&b'-');
if neg || b.get(i) == Some(&b'+') {
i += 1;
}
let first = i;
let mut value = Some(0u64);
while let Some(&c) = b.get(i).filter(|c| c.is_ascii_digit()) {
value =
value.and_then(|v| v.checked_mul(10)).and_then(|v| v.checked_add(u64::from(c - b'0')));
i += 1;
}
if i == first {
return (0, 0);
}
match value {
None => (u64::MAX, i),
Some(v) if neg => (v.wrapping_neg(), i),
Some(v) => (v, i),
}
}
pub fn snapshot_in(s: &str) -> Result<Snapshot, TypeError> {
let syntax = || TypeError::syntax("pg_snapshot", s);
let b = s.as_bytes();
let (xmin, end) = strtou64(b);
if b.get(end) != Some(&b':') {
return Err(syntax());
}
let mut at = end + 1;
let (xmax, end) = strtou64(&b[at..]);
if b.get(at + end) != Some(&b':') {
return Err(syntax());
}
at += end + 1;
if xmin == 0 || xmax == 0 || xmax < xmin {
return Err(syntax());
}
let mut xip = Vec::new();
let mut last = 0;
while at < b.len() {
let (xid, end) = strtou64(&b[at..]);
at += end;
if xid < xmin || xid >= xmax || xid < last {
return Err(syntax());
}
if xid != last {
xip.push(xid);
}
last = xid;
match b.get(at) {
Some(b',') => at += 1,
Some(_) => return Err(syntax()),
None => {}
}
}
Ok(Snapshot { xmin, xmax, xip })
}
pub fn snapshot_out(snapshot: &Snapshot, out: &mut Vec<u8>) {
let mut text = format!("{}:{}:", snapshot.xmin, snapshot.xmax);
for (i, xid) in snapshot.xip.iter().enumerate() {
if i > 0 {
text.push(',');
}
text.push_str(&xid.to_string());
}
out.extend_from_slice(text.as_bytes());
}
pub fn snapshot_recv(recv: &mut Recv<'_>) -> Result<Snapshot, TypeError> {
let bad = || {
TypeError::new(
SqlState::INVALID_BINARY_REPRESENTATION,
"invalid external pg_snapshot data".to_string(),
)
};
let Some(nxip) = usize::try_from(recv.i32()?).ok().filter(|&n| n <= MAX_NXIP) else {
return Err(bad());
};
let xmin = recv.i64()?.cast_unsigned();
let xmax = recv.i64()?.cast_unsigned();
if xmin == 0 || xmax == 0 || xmax < xmin {
return Err(bad());
}
let mut xip = Vec::new();
let mut last = 0;
for _ in 0..nxip {
let xid = recv.i64()?.cast_unsigned();
if xid < last || xid < xmin || xmax < xid {
return Err(bad());
}
if xid != last {
xip.push(xid);
}
last = xid;
}
Ok(Snapshot { xmin, xmax, xip })
}
#[cfg(test)]
mod tests {
use super::*;
fn text(s: &str) -> String {
let mut out = Vec::new();
snapshot_out(&snapshot_in(s).unwrap(), &mut out);
String::from_utf8(out).unwrap()
}
#[test]
fn text_forms() {
assert_eq!(text("10:20:10,14,15"), "10:20:10,14,15");
assert_eq!(text("10:20:10,10,14"), "10:20:10,14");
assert_eq!(text("10:20:10,"), "10:20:10");
assert_eq!(text(" 10:20:"), "10:20:");
assert_eq!(text("00010:020:"), "10:20:");
assert_eq!(text("10:10:"), "10:10:");
assert_eq!(
text("18446744073709551616:18446744073709551616:"),
"18446744073709551615:18446744073709551615:"
);
for bad in [
"20:10:",
"10:20:9",
"10:20:20",
"10:20:14,12",
"0:20:",
"a:20:",
"10:20: ",
"10 :20:",
"10:20",
"-1:20:",
] {
let error = snapshot_in(bad).unwrap_err();
assert_eq!(
error.message,
format!("invalid input syntax for type pg_snapshot: \"{bad}\"")
);
}
}
#[test]
fn binary_forms() {
let snapshot = snapshot_in("10:20:10,14,15").unwrap();
let bytes = snapshot.to_bytes();
assert_eq!(bytes.len(), 44);
assert_eq!(&bytes[..4], &[0, 0, 0, 3]);
assert_eq!(snapshot_recv(&mut Recv::new(&bytes)).unwrap(), snapshot);
assert_eq!(Snapshot::from_bytes(&bytes).unwrap(), snapshot);
let mut bad = bytes.clone();
bad[4..12].copy_from_slice(&30u64.to_be_bytes());
assert_eq!(
snapshot_recv(&mut Recv::new(&bad)).unwrap_err().message,
"invalid external pg_snapshot data"
);
}
#[test]
fn visibility() {
let snapshot = snapshot_in("10:20:10,14,15").unwrap();
let visible: Vec<bool> =
[9, 10, 11, 14, 19, 20].iter().map(|&xid| snapshot.is_visible(xid)).collect();
assert_eq!(visible, [true, false, true, false, true, false]);
}
}