use crate::WireError;
use crate::codec::{WireDecode, WireEncode, read_bytes, read_u8, read_u16_le, read_u32_le};
use crate::primitives::metadata_changes::{
METADATA_CHANGE_CURSOR_SIZE, WireMetadataChangeCursor, WireMetadataChangeStartMode,
WireMetadataCurrentPageToken,
};
use crate::primitives::metadata_changes::{put_optional_u64, read_optional_u64};
use bytes::{BufMut, BytesMut};
use std::borrow::Cow;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WireMetadataChangeStart {
Earliest,
Latest,
After(WireMetadataChangeCursor),
Current(Option<WireMetadataCurrentPageToken>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GetMetadataChangesRequest {
pub start: WireMetadataChangeStart,
pub since: Option<u64>,
pub until: Option<u64>,
pub limit: u32,
pub groups: Vec<u8>,
}
impl GetMetadataChangesRequest {
const fn start_mode(&self) -> WireMetadataChangeStartMode {
match self.start {
WireMetadataChangeStart::Earliest => WireMetadataChangeStartMode::Earliest,
WireMetadataChangeStart::Latest => WireMetadataChangeStartMode::Latest,
WireMetadataChangeStart::After(_) => WireMetadataChangeStartMode::After,
WireMetadataChangeStart::Current(_) => WireMetadataChangeStartMode::Current,
}
}
fn encoded_groups(&self) -> &[u8] {
&self.groups[..self.groups.len().min(usize::from(u8::MAX))]
}
const fn token(&self) -> Option<&WireMetadataCurrentPageToken> {
match &self.start {
WireMetadataChangeStart::Current(Some(token)) => Some(token),
_ => None,
}
}
}
impl WireEncode for GetMetadataChangesRequest {
fn encoded_size(&self) -> usize {
1 + METADATA_CHANGE_CURSOR_SIZE
+ if self.since.is_some() { 9 } else { 1 }
+ if self.until.is_some() { 9 } else { 1 }
+ 4
+ 1
+ self.encoded_groups().len()
+ 2
+ self.token().map_or(0, WireEncode::encoded_size)
}
fn encode(&self, buf: &mut BytesMut) {
buf.put_u8(self.start_mode().as_u8());
let cursor = match &self.start {
WireMetadataChangeStart::After(cursor) => *cursor,
_ => WireMetadataChangeCursor::default(),
};
cursor.encode(buf);
put_optional_u64(buf, self.since);
put_optional_u64(buf, self.until);
buf.put_u32_le(self.limit);
let groups = self.encoded_groups();
#[allow(clippy::cast_possible_truncation)]
buf.put_u8(groups.len() as u8);
buf.put_slice(groups);
match self.token() {
Some(token) => {
#[allow(clippy::cast_possible_truncation)]
buf.put_u16_le(token.encoded_size() as u16);
token.encode(buf);
}
None => buf.put_u16_le(0),
}
}
}
impl WireDecode for GetMetadataChangesRequest {
fn decode(buf: &[u8]) -> Result<(Self, usize), WireError> {
let mode = WireMetadataChangeStartMode::from_code(read_u8(buf, 0)?)?;
let cursor_bytes = read_bytes(buf, 1, METADATA_CHANGE_CURSOR_SIZE)?;
let mut position = 1 + METADATA_CHANGE_CURSOR_SIZE;
let (since, consumed) = read_optional_u64(buf, position)?;
position += consumed;
let (until, consumed) = read_optional_u64(buf, position)?;
position += consumed;
let limit = read_u32_le(buf, position)?;
position += 4;
let groups_count = usize::from(read_u8(buf, position)?);
position += 1;
let groups = read_bytes(buf, position, groups_count)?.to_vec();
position += groups_count;
let token_length = usize::from(read_u16_le(buf, position)?);
position += 2;
let token_bytes = read_bytes(buf, position, token_length)?;
position += token_length;
if position != buf.len() {
return Err(WireError::Validation(Cow::Owned(format!(
"{} trailing bytes after a metadata changes request",
buf.len() - position
))));
}
if mode != WireMetadataChangeStartMode::After && cursor_bytes.iter().any(|&byte| byte != 0)
{
return Err(WireError::Validation(Cow::Borrowed(
"a cursor is allowed only when starting after it",
)));
}
if mode != WireMetadataChangeStartMode::Current && token_length != 0 {
return Err(WireError::Validation(Cow::Borrowed(
"a page token is allowed only in current mode",
)));
}
let start = match mode {
WireMetadataChangeStartMode::Earliest => WireMetadataChangeStart::Earliest,
WireMetadataChangeStartMode::Latest => WireMetadataChangeStart::Latest,
WireMetadataChangeStartMode::After => {
WireMetadataChangeStart::After(WireMetadataChangeCursor::decode(cursor_bytes)?.0)
}
WireMetadataChangeStartMode::Current if token_length == 0 => {
WireMetadataChangeStart::Current(None)
}
WireMetadataChangeStartMode::Current => {
let (token, consumed) = WireMetadataCurrentPageToken::decode(token_bytes)?;
if consumed != token_length {
return Err(WireError::Validation(Cow::Borrowed(
"trailing bytes after a page token",
)));
}
WireMetadataChangeStart::Current(Some(token))
}
};
Ok((
Self {
start,
since,
until,
limit,
groups,
},
position,
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::primitives::metadata_changes::WireMetadataChangeGroup;
use bytes::Bytes;
fn cursor() -> WireMetadataChangeCursor {
WireMetadataChangeCursor {
log_id: 7,
op: 9,
checksum: 11,
ordinal: 2,
}
}
fn token() -> WireMetadataCurrentPageToken {
WireMetadataCurrentPageToken {
anchor: cursor(),
required_head: 12,
groups_mask: 0b11,
group: WireMetadataChangeGroup::Topics,
last_key: Bytes::from_static(&[1, 0, 0, 0, 2, 0, 0, 0]),
}
}
fn request(start: WireMetadataChangeStart) -> GetMetadataChangesRequest {
GetMetadataChangesRequest {
start,
since: Some(1_760_353_200_000_000),
until: None,
limit: 500,
groups: vec![1, 2],
}
}
#[test]
fn given_every_start_mode_when_round_tripping_should_preserve_every_field() {
for start in [
WireMetadataChangeStart::Earliest,
WireMetadataChangeStart::Latest,
WireMetadataChangeStart::After(cursor()),
WireMetadataChangeStart::Current(None),
WireMetadataChangeStart::Current(Some(token())),
] {
let original = request(start);
let bytes = original.to_bytes();
assert_eq!(bytes.len(), original.encoded_size());
let (decoded, consumed) = GetMetadataChangesRequest::decode(&bytes).unwrap();
assert_eq!(decoded, original);
assert_eq!(consumed, bytes.len());
}
}
#[test]
fn given_no_groups_and_no_bounds_when_round_tripping_should_preserve_them() {
let original = GetMetadataChangesRequest {
start: WireMetadataChangeStart::Latest,
since: None,
until: None,
limit: 1,
groups: vec![],
};
let bytes = original.to_bytes();
assert_eq!(bytes.len(), 1 + 44 + 1 + 1 + 4 + 1 + 2);
assert_eq!(
GetMetadataChangesRequest::decode(&bytes).unwrap().0,
original
);
}
#[test]
fn given_trailing_bytes_when_decoding_request_should_refuse() {
let mut bytes = request(WireMetadataChangeStart::Earliest)
.to_bytes()
.to_vec();
bytes.push(0);
assert!(GetMetadataChangesRequest::decode(&bytes).is_err());
}
#[test]
fn given_cursor_bytes_without_after_when_decoding_request_should_refuse() {
let mut bytes = request(WireMetadataChangeStart::Latest).to_bytes().to_vec();
bytes[5] = 1;
assert!(GetMetadataChangesRequest::decode(&bytes).is_err());
}
#[test]
fn given_non_canonical_cursor_when_decoding_request_should_refuse() {
let original = request(WireMetadataChangeStart::After(WireMetadataChangeCursor {
log_id: 1,
op: 0,
checksum: 3,
ordinal: 0,
}));
assert!(GetMetadataChangesRequest::decode(&original.to_bytes()).is_err());
}
#[test]
fn given_page_token_outside_current_mode_when_decoding_request_should_refuse() {
let mut bytes = request(WireMetadataChangeStart::Current(Some(token())))
.to_bytes()
.to_vec();
bytes[0] = WireMetadataChangeStartMode::Earliest.as_u8();
assert!(GetMetadataChangesRequest::decode(&bytes).is_err());
}
#[test]
fn given_unknown_start_or_presence_byte_when_decoding_request_should_refuse() {
let bytes = request(WireMetadataChangeStart::Earliest)
.to_bytes()
.to_vec();
let mut unknown_start = bytes.clone();
unknown_start[0] = 4;
assert!(GetMetadataChangesRequest::decode(&unknown_start).is_err());
let mut bad_presence = bytes;
bad_presence[1 + 44] = 2;
assert!(GetMetadataChangesRequest::decode(&bad_presence).is_err());
}
#[test]
fn given_truncated_request_when_decoding_should_refuse_every_prefix() {
let bytes = request(WireMetadataChangeStart::Current(Some(token()))).to_bytes();
for length in 0..bytes.len() {
assert!(GetMetadataChangesRequest::decode(&bytes[..length]).is_err());
}
}
}