use std::collections::BTreeSet;
use std::net::SocketAddr;
use crate::{
ENVELOPE_BYTES, Envelope, EnvelopeLimits, GroupId, NodeId, Opcode, Packet,
PartitionIncarnation, TopicId, append::Policy, data::CodecError, layout::Reader,
};
use super::{Limits, control, prepare};
const TOPIC_TAG: u8 = 0;
const MAX_NAME: usize = 128;
const MAX_ENDPOINT: usize = 1024;
const PARTITION_BASE: usize = 45;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TopicRequest {
pub name: String,
pub first: u32,
pub maximum: u16,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct BrokerEndpoint {
pub node: NodeId,
pub peer: String,
pub reader_pub: String,
pub follower_pub: Option<String>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TopicPartition {
pub number: u32,
pub group: GroupId,
pub config_epoch: u64,
pub incarnation: PartitionIncarnation,
pub members: Vec<NodeId>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TopicPage {
pub id: TopicId,
pub name: String,
pub partitioner_seed: u64,
pub total: u32,
pub policy: Policy,
pub brokers: Vec<BrokerEndpoint>,
pub first: u32,
pub partitions: Vec<TopicPartition>,
}
impl TopicPage {
pub fn valid(&self, limits: Limits) -> bool {
if !limits.valid()
|| self.id.as_bytes() == &[0; 16]
|| !name_valid(&self.name)
|| self.total == 0
|| self.partitions.is_empty()
|| self.partitions.len() > limits.partitions
|| !matches!(self.brokers.len(), 1 | 3)
|| self.brokers.len() > limits.members
|| self
.first
.checked_add(u32::try_from(self.partitions.len()).unwrap_or(u32::MAX))
.is_none_or(|end| end > self.total)
|| !matches!(
(self.brokers.len(), self.policy),
(1, Policy::LocalDurable) | (3, Policy::QuorumDurable)
)
{
return false;
}
let mut brokers = BTreeSet::new();
for broker in &self.brokers {
if broker.node.as_bytes() == &[0; 16]
|| !brokers.insert(broker.node)
|| !endpoint_valid(&broker.peer)
|| !endpoint_valid(&broker.reader_pub)
|| broker
.follower_pub
.as_ref()
.is_some_and(|value| !endpoint_valid(value))
{
return false;
}
}
let mut groups = BTreeSet::new();
let mut incarnations = BTreeSet::new();
for (index, partition) in self.partitions.iter().enumerate() {
if u32::try_from(index)
.ok()
.and_then(|index| self.first.checked_add(index))
!= Some(partition.number)
|| partition.group.as_bytes() == &[0; 16]
|| partition.config_epoch == 0
|| partition.incarnation.as_bytes() == &[0; 16]
|| !groups.insert(partition.group)
|| !incarnations.insert(partition.incarnation)
|| partition.members.len() != brokers.len()
|| partition.members.iter().copied().collect::<BTreeSet<_>>() != brokers
{
return false;
}
}
true
}
pub fn metadata_bytes(&self) -> Result<usize, CodecError> {
let mut size = 39_usize
.checked_add(self.name.len())
.ok_or(CodecError::Length)?;
for broker in &self.brokers {
size = crate::data::sum(&[
size,
22,
broker.peer.len(),
broker.reader_pub.len(),
broker.follower_pub.as_ref().map_or(0, String::len),
])?;
}
for partition in &self.partitions {
size = size
.checked_add(
partition
.members
.len()
.checked_mul(16)
.and_then(|members| PARTITION_BASE.checked_add(members))
.ok_or(CodecError::Length)?,
)
.ok_or(CodecError::Length)?;
}
Ok(size)
}
}
pub fn encode_topic_request(
envelope: Envelope,
request: &TopicRequest,
out: &mut Vec<u8>,
frame: EnvelopeLimits,
limits: Limits,
) -> Result<[u8; ENVELOPE_BYTES], CodecError> {
if !limits.valid()
|| !name_valid(&request.name)
|| request.maximum == 0
|| usize::from(request.maximum) > limits.partitions
{
return Err(CodecError::Profile);
}
let header = prepare(
envelope,
Opcode::StateSnapshotRequest,
crate::data::add(8, request.name.len())?,
out,
frame,
)?;
out.push(TOPIC_TAG);
write_name(out, &request.name);
out.extend_from_slice(&request.first.to_be_bytes());
out.extend_from_slice(&request.maximum.to_be_bytes());
Ok(header)
}
pub fn decode_topic_request(
packet: Packet<'_>,
frame: EnvelopeLimits,
limits: Limits,
) -> Result<TopicRequest, CodecError> {
let mut cursor = control(packet, Opcode::StateSnapshotRequest, frame, limits)?;
if cursor.u8()? != TOPIC_TAG {
return Err(CodecError::Profile);
}
let name = read_name(&mut cursor)?;
let first = cursor.u32()?;
let maximum = u16::from_be_bytes(cursor.array()?);
if !cursor.rest().is_empty() || maximum == 0 || usize::from(maximum) > limits.partitions {
return Err(CodecError::Profile);
}
Ok(TopicRequest {
name,
first,
maximum,
})
}
pub fn encode_topic_page(
envelope: Envelope,
page: &TopicPage,
out: &mut Vec<u8>,
frame: EnvelopeLimits,
limits: Limits,
) -> Result<[u8; ENVELOPE_BYTES], CodecError> {
if !page.valid(limits) {
return Err(CodecError::Profile);
}
let header = prepare(
envelope,
Opcode::StateSnapshot,
page.metadata_bytes()?,
out,
frame,
)?;
out.push(TOPIC_TAG);
out.extend_from_slice(page.id.as_bytes());
write_name(out, &page.name);
out.push(1); out.extend_from_slice(&page.partitioner_seed.to_be_bytes());
out.extend_from_slice(&page.total.to_be_bytes());
out.extend_from_slice(&page.first.to_be_bytes());
out.push(page.policy as u8);
out.push(page.brokers.len() as u8);
for broker in &page.brokers {
out.extend_from_slice(broker.node.as_bytes());
write_endpoint(out, &broker.peer);
write_endpoint(out, &broker.reader_pub);
write_endpoint(out, broker.follower_pub.as_deref().unwrap_or(""));
}
out.extend_from_slice(&(page.partitions.len() as u16).to_be_bytes());
for partition in &page.partitions {
out.extend_from_slice(&partition.number.to_be_bytes());
out.extend_from_slice(partition.group.as_bytes());
out.extend_from_slice(&partition.config_epoch.to_be_bytes());
out.extend_from_slice(partition.incarnation.as_bytes());
out.push(partition.members.len() as u8);
for member in &partition.members {
out.extend_from_slice(member.as_bytes());
}
}
Ok(header)
}
pub fn decode_topic_page(
packet: Packet<'_>,
frame: EnvelopeLimits,
limits: Limits,
) -> Result<TopicPage, CodecError> {
let mut cursor = control(packet, Opcode::StateSnapshot, frame, limits)?;
if cursor.u8()? != TOPIC_TAG {
return Err(CodecError::Profile);
}
let id = TopicId::from_bytes(cursor.array()?);
let name = read_name(&mut cursor)?;
if cursor.u8()? != 1 {
return Err(CodecError::Profile);
}
let partitioner_seed = cursor.u64()?;
let total = cursor.u32()?;
let first = cursor.u32()?;
let policy = Policy::try_from(cursor.u8()?)?;
let brokers = read_brokers(&mut cursor, limits)?;
let partitions = read_partitions(&mut cursor, brokers.len(), limits)?;
if !cursor.rest().is_empty() {
return Err(CodecError::Length);
}
let page = TopicPage {
id,
name,
partitioner_seed,
total,
policy,
brokers,
first,
partitions,
};
if !page.valid(limits) {
return Err(CodecError::Profile);
}
Ok(page)
}
fn read_brokers(
cursor: &mut Reader<'_>,
limits: Limits,
) -> Result<Vec<BrokerEndpoint>, CodecError> {
let count = usize::from(cursor.u8()?);
if !matches!(count, 1 | 3) || count > limits.members {
return Err(CodecError::Limit);
}
let mut brokers = Vec::with_capacity(count);
for _ in 0..count {
let node = NodeId::from_bytes(cursor.array()?);
let peer = read_endpoint(cursor)?.ok_or(CodecError::Profile)?;
let reader_pub = read_endpoint(cursor)?.ok_or(CodecError::Profile)?;
let follower_pub = read_endpoint(cursor)?;
brokers.push(BrokerEndpoint {
node,
peer,
reader_pub,
follower_pub,
});
}
Ok(brokers)
}
fn read_partitions(
cursor: &mut Reader<'_>,
members: usize,
limits: Limits,
) -> Result<Vec<TopicPartition>, CodecError> {
let count = usize::from(u16::from_be_bytes(cursor.array()?));
if count == 0 || count > limits.partitions || cursor.rest().len() < PARTITION_BASE * count {
return Err(CodecError::Limit);
}
let mut partitions = Vec::with_capacity(count);
for _ in 0..count {
let number = cursor.u32()?;
let group = GroupId::from_bytes(cursor.array()?);
let config_epoch = cursor.u64()?;
let incarnation = PartitionIncarnation::from_bytes(cursor.array()?);
if usize::from(cursor.u8()?) != members {
return Err(CodecError::Profile);
}
let mut ordered = Vec::with_capacity(members);
for _ in 0..members {
ordered.push(NodeId::from_bytes(cursor.array()?));
}
partitions.push(TopicPartition {
number,
group,
config_epoch,
incarnation,
members: ordered,
});
}
Ok(partitions)
}
fn name_valid(name: &str) -> bool {
(1..=MAX_NAME).contains(&name.len())
&& name
.as_bytes()
.first()
.is_some_and(u8::is_ascii_alphanumeric)
&& name.bytes().all(|byte| {
byte.is_ascii_lowercase() || byte.is_ascii_digit() || b"-_.".contains(&byte)
})
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum EndpointScheme {
Tcp,
Lz4Tcp,
ZstdTcp,
Ipc,
Inproc,
}
impl EndpointScheme {
const ALL: [Self; 5] = [
Self::Tcp,
Self::Lz4Tcp,
Self::ZstdTcp,
Self::Ipc,
Self::Inproc,
];
#[must_use]
pub fn parse(endpoint: &str) -> Option<(Self, &str)> {
Self::ALL.into_iter().find_map(|scheme| {
endpoint
.strip_prefix(scheme.as_str())
.and_then(|rest| rest.strip_prefix("://"))
.map(|address| (scheme, address))
})
}
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Tcp => "tcp",
Self::Lz4Tcp => "lz4+tcp",
Self::ZstdTcp => "zstd+tcp",
Self::Ipc => "ipc",
Self::Inproc => "inproc",
}
}
#[must_use]
pub const fn is_tcp(self) -> bool {
matches!(self, Self::Tcp | Self::Lz4Tcp | Self::ZstdTcp)
}
#[must_use]
pub const fn compressed(self) -> bool {
matches!(self, Self::Lz4Tcp | Self::ZstdTcp)
}
}
fn endpoint_valid(endpoint: &str) -> bool {
if !(1..=MAX_ENDPOINT).contains(&endpoint.len())
|| endpoint
.bytes()
.any(|byte| byte.is_ascii_whitespace() || byte.is_ascii_control())
{
return false;
}
match EndpointScheme::parse(endpoint) {
Some((scheme, address)) if scheme.is_tcp() => {
if let Ok(address) = address.parse::<SocketAddr>() {
return address.port() > 0
&& !address.ip().is_unspecified()
&& !address.ip().is_multicast();
}
address.rsplit_once(':').is_some_and(|(host, port)| {
!host.is_empty()
&& host
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'-'))
&& port.parse::<u16>().is_ok_and(|port| port > 0)
})
}
Some((EndpointScheme::Ipc, name)) => {
name.starts_with('/') || (name.starts_with('@') && name.len() > 1)
}
Some((EndpointScheme::Inproc, name)) => !name.is_empty(),
_ => false,
}
}
fn write_name(out: &mut Vec<u8>, name: &str) {
out.push(name.len() as u8);
out.extend_from_slice(name.as_bytes());
}
fn read_name(cursor: &mut Reader<'_>) -> Result<String, CodecError> {
let count = usize::from(cursor.u8()?);
if count == 0 || count > MAX_NAME {
return Err(CodecError::Profile);
}
let name = std::str::from_utf8(cursor.take(count)?).map_err(|_| CodecError::Profile)?;
if !name_valid(name) {
return Err(CodecError::Profile);
}
Ok(name.to_owned())
}
fn write_endpoint(out: &mut Vec<u8>, endpoint: &str) {
out.extend_from_slice(&(endpoint.len() as u16).to_be_bytes());
out.extend_from_slice(endpoint.as_bytes());
}
fn read_endpoint(cursor: &mut Reader<'_>) -> Result<Option<String>, CodecError> {
let count = usize::from(u16::from_be_bytes(cursor.array()?));
if count == 0 {
return Ok(None);
}
if count > MAX_ENDPOINT {
return Err(CodecError::Limit);
}
let endpoint = std::str::from_utf8(cursor.take(count)?).map_err(|_| CodecError::Profile)?;
if !endpoint_valid(endpoint) {
return Err(CodecError::Profile);
}
Ok(Some(endpoint.to_owned()))
}
#[cfg(test)]
mod tests;