use bytes::{Buf, BufMut, Bytes, BytesMut};
use postgres_protocol::message::frontend;
use super::lsn::Lsn;
use crate::error::PgError;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CopyBothResponse {
pub format: i8,
pub column_formats: Vec<i16>,
}
pub fn parse_copy_both_response(buf: &[u8]) -> Result<CopyBothResponse, PgError> {
let mut cursor = buf;
if cursor.remaining() < 1 {
return Err(PgError::Protocol(
"CopyBothResponse truncated: missing format byte".to_string(),
));
}
let format = cursor.get_i8();
if cursor.remaining() < 2 {
return Err(PgError::Protocol(
"CopyBothResponse truncated: missing column count".to_string(),
));
}
let num_columns = cursor.get_u16() as usize;
let mut column_formats = Vec::with_capacity(num_columns);
for i in 0..num_columns {
if cursor.remaining() < 2 {
return Err(PgError::Protocol(format!(
"CopyBothResponse truncated: missing format code for column {i} of {num_columns}"
)));
}
column_formats.push(cursor.get_i16());
}
if cursor.has_remaining() {
return Err(PgError::Protocol(format!(
"CopyBothResponse has {} unexpected trailing byte(s) after {num_columns} column \
format code(s)",
cursor.remaining()
)));
}
Ok(CopyBothResponse {
format,
column_formats,
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct XLogData {
pub wal_start: Lsn,
pub wal_end: Lsn,
pub server_time: i64,
pub data: Bytes,
}
pub fn parse_xlog_data(payload: &[u8]) -> Result<XLogData, PgError> {
let mut cursor = payload;
if cursor.remaining() < 1 {
return Err(PgError::Protocol(
"XLogData truncated: missing sub-message tag byte".to_string(),
));
}
let tag = cursor.get_u8();
if tag != b'w' {
return Err(PgError::Protocol(format!(
"XLogData: expected sub-message tag 'w' (0x77), found {tag:#04x}"
)));
}
if cursor.remaining() < 8 {
return Err(PgError::Protocol(
"XLogData truncated: missing wal_start".to_string(),
));
}
let wal_start = Lsn::from_u64(cursor.get_u64());
if cursor.remaining() < 8 {
return Err(PgError::Protocol(
"XLogData truncated: missing wal_end".to_string(),
));
}
let wal_end = Lsn::from_u64(cursor.get_u64());
if cursor.remaining() < 8 {
return Err(PgError::Protocol(
"XLogData truncated: missing server_time".to_string(),
));
}
let server_time = cursor.get_i64();
let data = Bytes::copy_from_slice(cursor);
Ok(XLogData {
wal_start,
wal_end,
server_time,
data,
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PrimaryKeepAlive {
pub wal_end: Lsn,
pub server_time: i64,
pub reply_requested: bool,
}
pub fn parse_primary_keepalive(payload: &[u8]) -> Result<PrimaryKeepAlive, PgError> {
let mut cursor = payload;
if cursor.remaining() < 1 {
return Err(PgError::Protocol(
"PrimaryKeepAlive truncated: missing sub-message tag byte".to_string(),
));
}
let tag = cursor.get_u8();
if tag != b'k' {
return Err(PgError::Protocol(format!(
"PrimaryKeepAlive: expected sub-message tag 'k' (0x6b), found {tag:#04x}"
)));
}
if cursor.remaining() < 8 {
return Err(PgError::Protocol(
"PrimaryKeepAlive truncated: missing wal_end".to_string(),
));
}
let wal_end = Lsn::from_u64(cursor.get_u64());
if cursor.remaining() < 8 {
return Err(PgError::Protocol(
"PrimaryKeepAlive truncated: missing server_time".to_string(),
));
}
let server_time = cursor.get_i64();
if cursor.remaining() < 1 {
return Err(PgError::Protocol(
"PrimaryKeepAlive truncated: missing reply_requested byte".to_string(),
));
}
let reply_requested = cursor.get_u8() != 0;
if cursor.has_remaining() {
return Err(PgError::Protocol(format!(
"PrimaryKeepAlive has {} unexpected trailing byte(s)",
cursor.remaining()
)));
}
Ok(PrimaryKeepAlive {
wal_end,
server_time,
reply_requested,
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CopyBothMessage {
XLogData(XLogData),
KeepAlive(PrimaryKeepAlive),
}
pub fn parse_copy_both_message(payload: &[u8]) -> Result<CopyBothMessage, PgError> {
match payload.first() {
None => Err(PgError::Protocol(
"CopyData sub-message payload is empty".to_string(),
)),
Some(b'w') => parse_xlog_data(payload).map(CopyBothMessage::XLogData),
Some(b'k') => parse_primary_keepalive(payload).map(CopyBothMessage::KeepAlive),
Some(other) => Err(PgError::Protocol(format!(
"unexpected CopyData sub-message tag: {other:#04x}"
))),
}
}
const STANDBY_STATUS_UPDATE_BODY_LEN: usize = 34;
const STANDBY_STATUS_UPDATE_FRAME_LEN: i32 = 34 + 4;
pub fn encode_standby_status_update(
written_lsn: Lsn,
flushed_lsn: Lsn,
applied_lsn: Lsn,
client_time: i64,
reply_requested: bool,
) -> Bytes {
let mut body_buf = BytesMut::with_capacity(STANDBY_STATUS_UPDATE_BODY_LEN);
body_buf.put_u8(b'r');
body_buf.put_u64(written_lsn.as_u64());
body_buf.put_u64(flushed_lsn.as_u64());
body_buf.put_u64(applied_lsn.as_u64());
body_buf.put_i64(client_time);
body_buf.put_u8(u8::from(reply_requested));
let body = body_buf.freeze();
let mut framed = BytesMut::with_capacity(5 + STANDBY_STATUS_UPDATE_BODY_LEN);
if let Ok(copy_data) = frontend::CopyData::new(body.clone()) {
copy_data.write(&mut framed);
} else {
framed.put_u8(b'd');
framed.put_i32(STANDBY_STATUS_UPDATE_FRAME_LEN);
framed.put_slice(&body);
}
framed.freeze()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn copy_both_response_valid_empty_columns() {
let buf = [0x00, 0x00, 0x00]; let result = parse_copy_both_response(&buf).unwrap();
assert_eq!(
result,
CopyBothResponse {
format: 0,
column_formats: vec![],
}
);
}
#[test]
fn copy_both_response_valid_with_columns() {
let buf = [
0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, ];
let result = parse_copy_both_response(&buf).unwrap();
assert_eq!(
result,
CopyBothResponse {
format: 0,
column_formats: vec![0, 1],
}
);
}
#[test]
fn copy_both_response_binary_format() {
let buf = [0x01, 0x00, 0x00]; let result = parse_copy_both_response(&buf).unwrap();
assert_eq!(result.format, 1);
}
#[test]
fn copy_both_response_truncated_missing_format() {
let buf: [u8; 0] = [];
assert!(matches!(
parse_copy_both_response(&buf),
Err(PgError::Protocol(_))
));
}
#[test]
fn copy_both_response_truncated_missing_column_count() {
let buf = [0x00]; assert!(matches!(
parse_copy_both_response(&buf),
Err(PgError::Protocol(_))
));
}
#[test]
fn copy_both_response_truncated_partial_column_count() {
let buf = [0x00, 0x00]; assert!(matches!(
parse_copy_both_response(&buf),
Err(PgError::Protocol(_))
));
}
#[test]
fn copy_both_response_truncated_missing_column_formats() {
let buf = [0x00, 0x00, 0x02]; assert!(matches!(
parse_copy_both_response(&buf),
Err(PgError::Protocol(_))
));
}
#[test]
fn copy_both_response_truncated_partial_column_formats() {
let buf = [0x00, 0x00, 0x02, 0x00, 0x00, 0x00]; assert!(matches!(
parse_copy_both_response(&buf),
Err(PgError::Protocol(_))
));
}
#[test]
fn copy_both_response_trailing_bytes_errors() {
let buf = [0x00, 0x00, 0x00, 0xFF]; assert!(matches!(
parse_copy_both_response(&buf),
Err(PgError::Protocol(_))
));
}
#[test]
fn xlog_data_valid_fixture() {
let mut buf = vec![b'w'];
buf.extend_from_slice(&0x1000_u64.to_be_bytes());
buf.extend_from_slice(&0x2000_u64.to_be_bytes());
buf.extend_from_slice(&12345_i64.to_be_bytes());
buf.extend_from_slice(b"hello");
let result = parse_xlog_data(&buf).unwrap();
assert_eq!(result.wal_start, Lsn::from_u64(0x1000));
assert_eq!(result.wal_end, Lsn::from_u64(0x2000));
assert_eq!(result.server_time, 12345);
assert_eq!(result.data.as_ref(), b"hello");
}
#[test]
fn xlog_data_wrong_tag() {
let mut buf = vec![b'k']; buf.extend_from_slice(&0_u64.to_be_bytes());
buf.extend_from_slice(&0_u64.to_be_bytes());
buf.extend_from_slice(&0_i64.to_be_bytes());
assert!(matches!(parse_xlog_data(&buf), Err(PgError::Protocol(_))));
}
#[test]
fn xlog_data_truncated_missing_tag() {
let buf: [u8; 0] = [];
assert!(matches!(parse_xlog_data(&buf), Err(PgError::Protocol(_))));
}
#[test]
fn xlog_data_truncated_missing_wal_start() {
let buf = [b'w'];
assert!(matches!(parse_xlog_data(&buf), Err(PgError::Protocol(_))));
}
#[test]
fn xlog_data_truncated_partial_wal_start() {
let mut buf = vec![b'w'];
buf.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); assert!(matches!(parse_xlog_data(&buf), Err(PgError::Protocol(_))));
}
#[test]
fn xlog_data_truncated_missing_wal_end() {
let mut buf = vec![b'w'];
buf.extend_from_slice(&0x1000_u64.to_be_bytes());
assert!(matches!(parse_xlog_data(&buf), Err(PgError::Protocol(_))));
}
#[test]
fn xlog_data_truncated_partial_wal_end() {
let mut buf = vec![b'w'];
buf.extend_from_slice(&0x1000_u64.to_be_bytes());
buf.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); assert!(matches!(parse_xlog_data(&buf), Err(PgError::Protocol(_))));
}
#[test]
fn xlog_data_truncated_missing_server_time() {
let mut buf = vec![b'w'];
buf.extend_from_slice(&0x1000_u64.to_be_bytes());
buf.extend_from_slice(&0x2000_u64.to_be_bytes());
assert!(matches!(parse_xlog_data(&buf), Err(PgError::Protocol(_))));
}
#[test]
fn xlog_data_truncated_partial_server_time() {
let mut buf = vec![b'w'];
buf.extend_from_slice(&0x1000_u64.to_be_bytes());
buf.extend_from_slice(&0x2000_u64.to_be_bytes());
buf.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); assert!(matches!(parse_xlog_data(&buf), Err(PgError::Protocol(_))));
}
#[test]
fn xlog_data_empty_data_payload_is_valid() {
let mut buf = vec![b'w'];
buf.extend_from_slice(&0x1000_u64.to_be_bytes());
buf.extend_from_slice(&0x2000_u64.to_be_bytes());
buf.extend_from_slice(&12345_i64.to_be_bytes());
let result = parse_xlog_data(&buf).unwrap();
assert_eq!(result.wal_start, Lsn::from_u64(0x1000));
assert_eq!(result.wal_end, Lsn::from_u64(0x2000));
assert_eq!(result.server_time, 12345);
assert_eq!(result.data.len(), 0);
}
#[test]
fn primary_keepalive_valid_reply_true() {
let mut buf = vec![b'k'];
buf.extend_from_slice(&0x2000_u64.to_be_bytes());
buf.extend_from_slice(&54321_i64.to_be_bytes());
buf.push(0x01);
let result = parse_primary_keepalive(&buf).unwrap();
assert_eq!(result.wal_end, Lsn::from_u64(0x2000));
assert_eq!(result.server_time, 54321);
assert!(result.reply_requested);
}
#[test]
fn primary_keepalive_valid_reply_false() {
let mut buf = vec![b'k'];
buf.extend_from_slice(&0x2000_u64.to_be_bytes());
buf.extend_from_slice(&54321_i64.to_be_bytes());
buf.push(0x00);
let result = parse_primary_keepalive(&buf).unwrap();
assert!(!result.reply_requested);
}
#[test]
fn primary_keepalive_reply_nonzero_non_one_is_treated_as_true() {
let mut buf = vec![b'k'];
buf.extend_from_slice(&0x2000_u64.to_be_bytes());
buf.extend_from_slice(&54321_i64.to_be_bytes());
buf.push(0x42);
let result = parse_primary_keepalive(&buf).unwrap();
assert!(result.reply_requested);
}
#[test]
fn primary_keepalive_wrong_tag() {
let mut buf = vec![b'w']; buf.extend_from_slice(&0_u64.to_be_bytes());
buf.extend_from_slice(&0_i64.to_be_bytes());
buf.push(0x00);
assert!(matches!(
parse_primary_keepalive(&buf),
Err(PgError::Protocol(_))
));
}
#[test]
fn primary_keepalive_truncated_missing_tag() {
let buf: [u8; 0] = [];
assert!(matches!(
parse_primary_keepalive(&buf),
Err(PgError::Protocol(_))
));
}
#[test]
fn primary_keepalive_truncated_missing_wal_end() {
let buf = [b'k'];
assert!(matches!(
parse_primary_keepalive(&buf),
Err(PgError::Protocol(_))
));
}
#[test]
fn primary_keepalive_truncated_partial_wal_end() {
let mut buf = vec![b'k'];
buf.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); assert!(matches!(
parse_primary_keepalive(&buf),
Err(PgError::Protocol(_))
));
}
#[test]
fn primary_keepalive_truncated_missing_server_time() {
let mut buf = vec![b'k'];
buf.extend_from_slice(&0x2000_u64.to_be_bytes());
assert!(matches!(
parse_primary_keepalive(&buf),
Err(PgError::Protocol(_))
));
}
#[test]
fn primary_keepalive_truncated_partial_server_time() {
let mut buf = vec![b'k'];
buf.extend_from_slice(&0x2000_u64.to_be_bytes());
buf.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); assert!(matches!(
parse_primary_keepalive(&buf),
Err(PgError::Protocol(_))
));
}
#[test]
fn primary_keepalive_truncated_missing_reply_requested() {
let mut buf = vec![b'k'];
buf.extend_from_slice(&0x2000_u64.to_be_bytes());
buf.extend_from_slice(&54321_i64.to_be_bytes());
assert!(matches!(
parse_primary_keepalive(&buf),
Err(PgError::Protocol(_))
));
}
#[test]
fn primary_keepalive_trailing_bytes_errors() {
let mut buf = vec![b'k'];
buf.extend_from_slice(&0x2000_u64.to_be_bytes());
buf.extend_from_slice(&54321_i64.to_be_bytes());
buf.push(0x01);
buf.push(0xFF); assert!(matches!(
parse_primary_keepalive(&buf),
Err(PgError::Protocol(_))
));
}
#[test]
fn copy_both_message_dispatches_xlog_data() {
let mut buf = vec![b'w'];
buf.extend_from_slice(&0x1000_u64.to_be_bytes());
buf.extend_from_slice(&0x2000_u64.to_be_bytes());
buf.extend_from_slice(&12345_i64.to_be_bytes());
buf.extend_from_slice(b"hi");
let Ok(CopyBothMessage::XLogData(x)) = parse_copy_both_message(&buf) else {
panic!("expected Ok(CopyBothMessage::XLogData(_))");
};
assert_eq!(x.wal_start, Lsn::from_u64(0x1000));
assert_eq!(x.data.as_ref(), b"hi");
}
#[test]
fn copy_both_message_dispatches_keepalive() {
let mut buf = vec![b'k'];
buf.extend_from_slice(&0x2000_u64.to_be_bytes());
buf.extend_from_slice(&54321_i64.to_be_bytes());
buf.push(0x01);
let Ok(CopyBothMessage::KeepAlive(k)) = parse_copy_both_message(&buf) else {
panic!("expected Ok(CopyBothMessage::KeepAlive(_))");
};
assert_eq!(k.wal_end, Lsn::from_u64(0x2000));
assert!(k.reply_requested);
}
#[test]
fn copy_both_message_unknown_tag_errors() {
let buf = [b'X', 0x00, 0x00];
assert!(matches!(
parse_copy_both_message(&buf),
Err(PgError::Protocol(_))
));
}
#[test]
fn copy_both_message_empty_payload_errors() {
let buf: [u8; 0] = [];
assert!(matches!(
parse_copy_both_message(&buf),
Err(PgError::Protocol(_))
));
}
#[test]
fn encode_standby_status_update_golden_bytes() {
let actual = encode_standby_status_update(
Lsn::from_u64(0x1000),
Lsn::from_u64(0x2000),
Lsn::from_u64(0x3000),
999,
true,
);
let expected: &[u8] = &[
b'd', 0x00, 0x00, 0x00, 0x26, b'r', 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xE7, 0x01, ];
assert_eq!(actual.len(), 39);
assert_eq!(actual.as_ref(), expected);
}
#[test]
fn encode_standby_status_update_reply_not_requested() {
let actual = encode_standby_status_update(
Lsn::from_u64(0x1000),
Lsn::from_u64(0x1000),
Lsn::from_u64(0x1000),
0,
false,
);
assert_eq!(actual.len(), 39);
assert_eq!(actual.last(), Some(&0x00));
assert_eq!(&actual[0..5], &[b'd', 0x00, 0x00, 0x00, 0x26]);
}
#[test]
fn encode_standby_status_update_body_round_trips_via_manual_reparse() {
let written = Lsn::from_u64(0x1234_5678);
let flushed = Lsn::from_u64(0x1234_5680);
let applied = Lsn::from_u64(0x1234_5690);
let client_time = 1_700_000_000_123_456_i64;
let framed = encode_standby_status_update(written, flushed, applied, client_time, false);
assert_eq!(framed[0], b'd');
let frame_len = i32::from_be_bytes([framed[1], framed[2], framed[3], framed[4]]);
assert_eq!(frame_len, STANDBY_STATUS_UPDATE_FRAME_LEN);
let body = &framed[5..];
assert_eq!(body.len(), STANDBY_STATUS_UPDATE_BODY_LEN);
let mut cursor = body;
assert_eq!(cursor.get_u8(), b'r');
assert_eq!(Lsn::from_u64(cursor.get_u64()), written);
assert_eq!(Lsn::from_u64(cursor.get_u64()), flushed);
assert_eq!(Lsn::from_u64(cursor.get_u64()), applied);
assert_eq!(cursor.get_i64(), client_time);
assert_eq!(cursor.get_u8(), 0x00); assert!(!cursor.has_remaining());
}
}