use crate::wire::error::WireError;
pub const SIDE_BUY: u8 = 0;
pub const SIDE_SELL: u8 = 1;
pub const BOOK_UPDATE_SIZE: usize = 32;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BookUpdateWire {
pub engine_seq: u64,
pub side: u8,
pub price: i64,
pub qty: u64,
}
#[inline]
pub fn encode_book_update(update: &BookUpdateWire, out: &mut Vec<u8>) {
out.reserve(BOOK_UPDATE_SIZE);
out.extend_from_slice(&update.engine_seq.to_le_bytes());
out.push(update.side);
out.extend_from_slice(&update.price.to_le_bytes());
out.extend_from_slice(&update.qty.to_le_bytes());
out.extend_from_slice(&[0u8; 7]);
}
#[inline]
pub fn decode_book_update(payload: &[u8]) -> Result<BookUpdateWire, WireError> {
if payload.len() != BOOK_UPDATE_SIZE {
return Err(WireError::InvalidPayload(
"BookUpdate: payload size mismatch",
));
}
let read_u64 = |offset: usize| -> Result<u64, WireError> {
let slot = payload
.get(offset..offset + 8)
.ok_or(WireError::Truncated)?;
let mut arr = [0u8; 8];
arr.copy_from_slice(slot);
Ok(u64::from_le_bytes(arr))
};
let read_i64 = |offset: usize| -> Result<i64, WireError> {
let slot = payload
.get(offset..offset + 8)
.ok_or(WireError::Truncated)?;
let mut arr = [0u8; 8];
arr.copy_from_slice(slot);
Ok(i64::from_le_bytes(arr))
};
let engine_seq = read_u64(0)?;
let side = *payload.get(8).ok_or(WireError::Truncated)?;
if side != SIDE_BUY && side != SIDE_SELL {
return Err(WireError::InvalidPayload("BookUpdate: unknown side"));
}
let price = read_i64(9)?;
let qty = read_u64(17)?;
let pad = payload.get(25..32).ok_or(WireError::Truncated)?;
if pad.iter().any(|&byte| byte != 0) {
return Err(WireError::InvalidPayload(
"BookUpdate: non-zero reserved padding",
));
}
Ok(BookUpdateWire {
engine_seq,
side,
price,
qty,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::wire::framing::{decode_frame, encode_frame};
use proptest::prelude::*;
#[test]
fn payload_size_constant() {
let upd = BookUpdateWire {
engine_seq: 0,
side: SIDE_BUY,
price: 0,
qty: 0,
};
let mut buf = Vec::new();
encode_book_update(&upd, &mut buf);
assert_eq!(buf.len(), BOOK_UPDATE_SIZE);
}
proptest! {
#[test]
fn roundtrip_through_frame(
engine_seq in any::<u64>(),
side in 0u8..=1u8,
price in any::<i64>(),
qty in any::<u64>(),
) {
let original = BookUpdateWire {
engine_seq,
side,
price,
qty,
};
let mut payload = Vec::new();
encode_book_update(&original, &mut payload);
let mut framed = Vec::new();
encode_frame(0x83, &payload, &mut framed).expect("encode_frame");
let (kind, decoded_payload, _) = decode_frame(&framed).expect("decode_frame");
prop_assert_eq!(kind, 0x83u8);
let decoded = decode_book_update(decoded_payload).expect("decode_book_update");
prop_assert_eq!(decoded, original);
}
}
#[test]
fn rejects_wrong_size() {
let buf = [0u8; BOOK_UPDATE_SIZE - 1];
assert!(matches!(
decode_book_update(&buf),
Err(WireError::InvalidPayload(_))
));
}
}