use super::{WireMetadataChangeGroup, read_wire_name};
use crate::WireError;
use crate::codec::{WireDecode, WireEncode, read_bytes, read_u8, read_u16_le, read_u32_le};
use crate::codec::{read_u64_le, read_u128_le};
use bytes::{BufMut, Bytes, BytesMut};
use std::borrow::Cow;
pub const METADATA_CHANGE_CURSOR_SIZE: usize = 44;
pub const MAX_PAGE_TOKEN_KEY_SIZE: usize = 4 + 1 + 255;
pub const MAX_PAGE_TOKEN_SIZE: usize =
METADATA_CHANGE_CURSOR_SIZE + 8 + 1 + 1 + 2 + MAX_PAGE_TOKEN_KEY_SIZE;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct WireMetadataChangeCursor {
pub log_id: u128,
pub op: u64,
pub checksum: u128,
pub ordinal: u32,
}
impl WireMetadataChangeCursor {
#[must_use]
pub const fn zero(log_id: u128) -> Self {
Self {
log_id,
op: 0,
checksum: 0,
ordinal: 0,
}
}
#[must_use]
pub const fn is_zero(&self) -> bool {
self.op == 0
}
#[must_use]
pub const fn is_canonical(&self) -> bool {
self.op != 0 || (self.checksum == 0 && self.ordinal == 0)
}
}
impl WireEncode for WireMetadataChangeCursor {
fn encoded_size(&self) -> usize {
METADATA_CHANGE_CURSOR_SIZE
}
fn encode(&self, buf: &mut BytesMut) {
buf.put_u128_le(self.log_id);
buf.put_u64_le(self.op);
buf.put_u128_le(self.checksum);
buf.put_u32_le(self.ordinal);
}
}
impl WireDecode for WireMetadataChangeCursor {
fn decode(buf: &[u8]) -> Result<(Self, usize), WireError> {
let cursor = Self {
log_id: read_u128_le(buf, 0)?,
op: read_u64_le(buf, 16)?,
checksum: read_u128_le(buf, 24)?,
ordinal: read_u32_le(buf, 40)?,
};
if !cursor.is_canonical() {
return Err(WireError::Validation(Cow::Borrowed(
"a cursor at op 0 carries no checksum and no ordinal",
)));
}
Ok((cursor, METADATA_CHANGE_CURSOR_SIZE))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WireMetadataCurrentPageToken {
pub anchor: WireMetadataChangeCursor,
pub required_head: u64,
pub groups_mask: u8,
pub group: WireMetadataChangeGroup,
pub last_key: Bytes,
}
impl WireMetadataCurrentPageToken {
const FIXED_SIZE: usize = METADATA_CHANGE_CURSOR_SIZE + 8 + 1 + 1 + 2;
pub fn validate(&self) -> Result<(), WireError> {
if self.groups_mask & self.group.mask_bit() == 0 {
return Err(WireError::Validation(Cow::Borrowed(
"page token group is not in its group mask",
)));
}
if self.required_head < self.anchor.op {
return Err(WireError::Validation(Cow::Borrowed(
"page token head is below its anchor",
)));
}
if !self.anchor.is_canonical() {
return Err(WireError::Validation(Cow::Borrowed(
"page token anchor is not canonical",
)));
}
if self.last_key.len() > MAX_PAGE_TOKEN_KEY_SIZE {
return Err(WireError::Validation(Cow::Borrowed(
"page token key is too long",
)));
}
if !key_fits_group(self.group, &self.last_key) {
return Err(WireError::Validation(Cow::Owned(format!(
"page token key of {} bytes does not fit group {}",
self.last_key.len(),
self.group.as_u8()
))));
}
Ok(())
}
}
impl WireEncode for WireMetadataCurrentPageToken {
fn encoded_size(&self) -> usize {
Self::FIXED_SIZE + self.last_key.len()
}
fn encode(&self, buf: &mut BytesMut) {
self.anchor.encode(buf);
buf.put_u64_le(self.required_head);
buf.put_u8(self.groups_mask);
buf.put_u8(self.group.as_u8());
#[allow(clippy::cast_possible_truncation)]
buf.put_u16_le(self.last_key.len() as u16);
buf.put_slice(&self.last_key);
}
}
impl WireDecode for WireMetadataCurrentPageToken {
fn decode(buf: &[u8]) -> Result<(Self, usize), WireError> {
let (anchor, mut position) = WireMetadataChangeCursor::decode(buf)?;
let required_head = read_u64_le(buf, position)?;
position += 8;
let groups_mask = read_u8(buf, position)?;
position += 1;
let group_offset = position;
let group = WireMetadataChangeGroup::from_code(read_u8(buf, position)?).map_err(|_| {
WireError::Validation(Cow::Borrowed("page token names an unknown group"))
})?;
position += 1;
let key_length = usize::from(read_u16_le(buf, position)?);
position += 2;
if key_length > MAX_PAGE_TOKEN_KEY_SIZE {
return Err(WireError::Validation(Cow::Owned(format!(
"page token key of {key_length} bytes at offset {group_offset} is too long"
))));
}
let last_key = Bytes::copy_from_slice(read_bytes(buf, position, key_length)?);
position += key_length;
let token = Self {
anchor,
required_head,
groups_mask,
group,
last_key,
};
token.validate()?;
Ok((token, position))
}
}
fn key_fits_group(group: WireMetadataChangeGroup, key: &[u8]) -> bool {
match group {
WireMetadataChangeGroup::Streams | WireMetadataChangeGroup::Users => key.len() == 4,
WireMetadataChangeGroup::Topics
| WireMetadataChangeGroup::Partitions
| WireMetadataChangeGroup::Sessions => key.len() == 8,
WireMetadataChangeGroup::ConsumerGroups => key.len() == 12,
WireMetadataChangeGroup::ConsumerGroupMembers => key.len() == 16,
WireMetadataChangeGroup::PersonalAccessTokens => {
key.len() > 4
&& read_wire_name(key, 4).is_ok_and(|(_, consumed)| 4 + consumed == key.len())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cursor() -> WireMetadataChangeCursor {
WireMetadataChangeCursor {
log_id: 0x0d4b_8f2a_6e1c_93d7,
op: 4187,
checksum: 0xf2a4_c6e8_b0d1_f359,
ordinal: 1,
}
}
fn token(group: WireMetadataChangeGroup, last_key: &[u8]) -> WireMetadataCurrentPageToken {
WireMetadataCurrentPageToken {
anchor: cursor(),
required_head: 4200,
groups_mask: u8::MAX,
group,
last_key: Bytes::copy_from_slice(last_key),
}
}
fn token_key(user_id: u32, name: &str) -> Vec<u8> {
let mut key = user_id.to_le_bytes().to_vec();
key.push(u8::try_from(name.len()).unwrap());
key.extend_from_slice(name.as_bytes());
key
}
#[test]
fn given_cursor_when_round_tripping_should_use_44_little_endian_bytes() {
let bytes = cursor().to_bytes();
assert_eq!(bytes.len(), METADATA_CHANGE_CURSOR_SIZE);
assert_eq!(&bytes[16..24], &4187u64.to_le_bytes());
let (decoded, consumed) = WireMetadataChangeCursor::decode(&bytes).unwrap();
assert_eq!(decoded, cursor());
assert_eq!(consumed, METADATA_CHANGE_CURSOR_SIZE);
}
#[test]
fn given_zero_cursor_when_round_tripping_should_keep_its_log_id() {
let zero = WireMetadataChangeCursor::zero(42);
assert!(zero.is_zero());
let (decoded, _) = WireMetadataChangeCursor::decode(&zero.to_bytes()).unwrap();
assert_eq!(decoded, zero);
}
#[test]
fn given_op_zero_with_checksum_or_ordinal_when_decoding_should_refuse() {
for (checksum, ordinal) in [(1u128, 0u32), (0, 1)] {
let cursor = WireMetadataChangeCursor {
log_id: 1,
op: 0,
checksum,
ordinal,
};
assert!(!cursor.is_canonical());
assert!(WireMetadataChangeCursor::decode(&cursor.to_bytes()).is_err());
}
}
#[test]
fn given_truncated_cursor_when_decoding_should_refuse() {
let bytes = cursor().to_bytes();
assert!(WireMetadataChangeCursor::decode(&bytes[..43]).is_err());
}
#[test]
fn given_every_group_key_width_when_round_tripping_page_tokens_should_preserve_them() {
let cases = [
(WireMetadataChangeGroup::Streams, vec![0u8; 4]),
(WireMetadataChangeGroup::Topics, vec![1u8; 8]),
(WireMetadataChangeGroup::Partitions, vec![2u8; 8]),
(WireMetadataChangeGroup::ConsumerGroups, vec![3u8; 12]),
(WireMetadataChangeGroup::ConsumerGroupMembers, vec![4u8; 16]),
(WireMetadataChangeGroup::Users, vec![5u8; 4]),
(
WireMetadataChangeGroup::PersonalAccessTokens,
token_key(9, "ci"),
),
(WireMetadataChangeGroup::Sessions, vec![6u8; 8]),
];
for (group, key) in cases {
let original = token(group, &key);
let bytes = original.to_bytes();
let (decoded, consumed) = WireMetadataCurrentPageToken::decode(&bytes).unwrap();
assert_eq!(decoded, original);
assert_eq!(consumed, bytes.len());
}
}
#[test]
fn given_longest_token_name_when_encoding_should_fit_the_maximum_payload() {
let longest = "n".repeat(255);
let original = token(
WireMetadataChangeGroup::PersonalAccessTokens,
&token_key(1, &longest),
);
assert_eq!(original.to_bytes().len(), MAX_PAGE_TOKEN_SIZE);
assert!(WireMetadataCurrentPageToken::decode(&original.to_bytes()).is_ok());
}
#[test]
fn given_wrong_key_width_when_decoding_page_token_should_refuse() {
for (group, width) in [
(WireMetadataChangeGroup::Streams, 8),
(WireMetadataChangeGroup::Topics, 4),
(WireMetadataChangeGroup::ConsumerGroups, 16),
(WireMetadataChangeGroup::Sessions, 4),
(WireMetadataChangeGroup::PersonalAccessTokens, 4),
] {
let bytes = token(group, &vec![0u8; width]).to_bytes();
assert!(WireMetadataCurrentPageToken::decode(&bytes).is_err());
}
let mut bad_name = token_key(1, "ci");
bad_name.push(0);
let bytes = token(WireMetadataChangeGroup::PersonalAccessTokens, &bad_name).to_bytes();
assert!(WireMetadataCurrentPageToken::decode(&bytes).is_err());
}
#[test]
fn given_group_outside_mask_when_decoding_page_token_should_refuse() {
let mut original = token(WireMetadataChangeGroup::Topics, &[0u8; 8]);
original.groups_mask = WireMetadataChangeGroup::Streams.mask_bit();
assert!(WireMetadataCurrentPageToken::decode(&original.to_bytes()).is_err());
}
#[test]
fn given_head_below_anchor_when_decoding_page_token_should_refuse() {
let mut original = token(WireMetadataChangeGroup::Streams, &[0u8; 4]);
original.required_head = original.anchor.op - 1;
assert!(WireMetadataCurrentPageToken::decode(&original.to_bytes()).is_err());
}
#[test]
fn given_unknown_group_code_when_decoding_page_token_should_refuse() {
let mut bytes = token(WireMetadataChangeGroup::Streams, &[0u8; 4])
.to_bytes()
.to_vec();
bytes[METADATA_CHANGE_CURSOR_SIZE + 9] = 9;
assert!(WireMetadataCurrentPageToken::decode(&bytes).is_err());
}
#[test]
fn given_truncated_page_token_when_decoding_should_refuse_every_prefix() {
let bytes = token(WireMetadataChangeGroup::Users, &[1u8; 4]).to_bytes();
for length in 0..bytes.len() {
assert!(WireMetadataCurrentPageToken::decode(&bytes[..length]).is_err());
}
}
}