use super::{
MAX_MEMBERS, MAX_NAME_BYTES, MAX_PARTITIONS, Reader, SOURCE_TAG, Start, check, command,
decode_authority, decode_member, encode_authority, member_bytes, nonzero, reserve,
validate_scope,
};
use crate::{
ConsumerGroupId, ConsumerMemberId, ENVELOPE_BYTES, Envelope, EnvelopeLimits, NodeId, Opcode,
OperationId, Packet, PartitionIncarnation, append::Authority, data::CodecError,
};
pub const INSPECT_FIXED_BYTES: usize = 50;
pub const INSPECTED_FIXED_BYTES: usize = 90;
pub const INSPECTED_MEMBER_BYTES: usize = 48;
pub const INSPECTED_TARGET_BYTES: usize = 44;
pub const DELETE_FIXED_BYTES: usize = 66;
pub const DELETED_BYTES: usize = 66;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct InspectRequest {
pub authority: Authority,
pub coordinator: PartitionIncarnation,
pub name: String,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct InspectedMember {
pub id: ConsumerMemberId,
pub node: NodeId,
pub join: OperationId,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct InspectedTarget {
pub partition: PartitionIncarnation,
pub number: u32,
pub plan_revision: u64,
pub member: Option<ConsumerMemberId>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Inspection {
pub authority: Authority,
pub coordinator: PartitionIncarnation,
pub group: Option<ConsumerGroupId>,
pub revision: u64,
pub initial: Start,
pub members: Vec<InspectedMember>,
pub targets: Vec<InspectedTarget>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DeleteRequest {
pub authority: Authority,
pub coordinator: PartitionIncarnation,
pub group: ConsumerGroupId,
pub name: String,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct DeleteReply {
pub authority: Authority,
pub coordinator: PartitionIncarnation,
pub group: ConsumerGroupId,
pub deleted: bool,
}
pub fn inspect_bytes(name: &str) -> Result<usize, CodecError> {
name_bytes(name).and_then(|name| crate::data::add(INSPECT_FIXED_BYTES, name))
}
#[expect(
clippy::arithmetic_side_effects,
reason = "both counts are capped first, far below overflow"
)]
pub fn inspected_bytes(members: usize, targets: usize) -> Result<usize, CodecError> {
if members > MAX_MEMBERS || targets > MAX_PARTITIONS {
return Err(CodecError::Limit);
}
Ok(INSPECTED_FIXED_BYTES + members * INSPECTED_MEMBER_BYTES + targets * INSPECTED_TARGET_BYTES)
}
pub fn delete_bytes(name: &str) -> Result<usize, CodecError> {
name_bytes(name).and_then(|name| crate::data::add(DELETE_FIXED_BYTES, name))
}
pub fn encode_inspect(
envelope: Envelope,
request: &InspectRequest,
metadata: &mut Vec<u8>,
limits: EnvelopeLimits,
) -> Result<[u8; ENVELOPE_BYTES], CodecError> {
command(envelope, Opcode::GroupInspect, false)?;
validate_scope(request.authority, request.coordinator)?;
let length = inspect_bytes(&request.name)?;
let header = envelope.encode_header(length, 0, limits)?;
reserve(metadata, length)?;
metadata.push(SOURCE_TAG);
encode_authority(metadata, request.authority);
metadata.extend_from_slice(request.coordinator.as_bytes());
encode_name(metadata, &request.name);
debug_assert_eq!(metadata.len(), length);
Ok(header)
}
pub fn decode_inspect(
packet: Packet<'_>,
limits: EnvelopeLimits,
) -> Result<InspectRequest, CodecError> {
check(packet, Opcode::GroupInspect, false, limits)?;
let mut cursor = Reader::new(packet.metadata);
let (authority, coordinator) = decode_scope(&mut cursor)?;
let name = decode_name(&mut cursor)?;
if !cursor.rest().is_empty() {
return Err(CodecError::Length);
}
Ok(InspectRequest {
authority,
coordinator,
name,
})
}
pub fn encode_inspected(
envelope: Envelope,
reply: &Inspection,
metadata: &mut Vec<u8>,
limits: EnvelopeLimits,
) -> Result<[u8; ENVELOPE_BYTES], CodecError> {
command(envelope, Opcode::GroupInspected, true)?;
validate_scope(reply.authority, reply.coordinator)?;
validate_inspection(reply)?;
let length = inspected_bytes(reply.members.len(), reply.targets.len())?;
let header = envelope.encode_header(length, 0, limits)?;
reserve(metadata, length)?;
metadata.push(SOURCE_TAG);
encode_authority(metadata, reply.authority);
metadata.extend_from_slice(reply.coordinator.as_bytes());
metadata.extend_from_slice(&reply.group.map_or([0; 16], |group| *group.as_bytes()));
metadata.extend_from_slice(&reply.revision.to_be_bytes());
reply.initial.encode(metadata);
metadata.extend_from_slice(&(reply.members.len() as u32).to_be_bytes());
for member in &reply.members {
metadata.extend_from_slice(member.id.as_bytes());
metadata.extend_from_slice(member.node.as_bytes());
metadata.extend_from_slice(member.join.as_bytes());
}
metadata.extend_from_slice(&(reply.targets.len() as u32).to_be_bytes());
for target in &reply.targets {
metadata.extend_from_slice(target.partition.as_bytes());
metadata.extend_from_slice(&target.number.to_be_bytes());
metadata.extend_from_slice(&target.plan_revision.to_be_bytes());
metadata.extend_from_slice(&member_bytes(target.member));
}
debug_assert_eq!(metadata.len(), length);
Ok(header)
}
pub fn decode_inspected(
packet: Packet<'_>,
limits: EnvelopeLimits,
) -> Result<Inspection, CodecError> {
check(packet, Opcode::GroupInspected, true, limits)?;
let mut cursor = Reader::new(packet.metadata);
let (authority, coordinator) = decode_scope(&mut cursor)?;
let group = cursor.array::<16>()?;
let revision = cursor.u64()?;
let initial = Start::decode(&mut cursor)?;
let count = cursor.u32()? as usize;
if count > MAX_MEMBERS || cursor.rest().len() < count * INSPECTED_MEMBER_BYTES {
return Err(CodecError::Length);
}
let mut members = Vec::with_capacity(count);
for _ in 0..count {
members.push(InspectedMember {
id: ConsumerMemberId::from_bytes(cursor.array()?),
node: NodeId::from_bytes(cursor.array()?),
join: OperationId::from_bytes(cursor.array()?),
});
}
let count = cursor.u32()? as usize;
if count > MAX_PARTITIONS || cursor.rest().len() != count * INSPECTED_TARGET_BYTES {
return Err(CodecError::Length);
}
let mut targets = Vec::with_capacity(count);
for _ in 0..count {
targets.push(InspectedTarget {
partition: PartitionIncarnation::from_bytes(cursor.array()?),
number: cursor.u32()?,
plan_revision: cursor.u64()?,
member: decode_member(&mut cursor)?,
});
}
let reply = Inspection {
authority,
coordinator,
group: (group != [0; 16]).then(|| ConsumerGroupId::from_bytes(group)),
revision,
initial,
members,
targets,
};
validate_inspection(&reply)?;
Ok(reply)
}
pub fn encode_delete(
envelope: Envelope,
request: &DeleteRequest,
metadata: &mut Vec<u8>,
limits: EnvelopeLimits,
) -> Result<[u8; ENVELOPE_BYTES], CodecError> {
command(envelope, Opcode::GroupDelete, false)?;
validate_scope(request.authority, request.coordinator)?;
nonzero(request.group.as_bytes())?;
let length = delete_bytes(&request.name)?;
let header = envelope.encode_header(length, 0, limits)?;
reserve(metadata, length)?;
metadata.push(SOURCE_TAG);
encode_authority(metadata, request.authority);
metadata.extend_from_slice(request.coordinator.as_bytes());
metadata.extend_from_slice(request.group.as_bytes());
encode_name(metadata, &request.name);
debug_assert_eq!(metadata.len(), length);
Ok(header)
}
pub fn decode_delete(
packet: Packet<'_>,
limits: EnvelopeLimits,
) -> Result<DeleteRequest, CodecError> {
check(packet, Opcode::GroupDelete, false, limits)?;
let mut cursor = Reader::new(packet.metadata);
let (authority, coordinator) = decode_scope(&mut cursor)?;
let group = ConsumerGroupId::from_bytes(cursor.array()?);
nonzero(group.as_bytes())?;
let name = decode_name(&mut cursor)?;
if !cursor.rest().is_empty() {
return Err(CodecError::Length);
}
Ok(DeleteRequest {
authority,
coordinator,
group,
name,
})
}
pub fn encode_deleted(
envelope: Envelope,
reply: DeleteReply,
metadata: &mut Vec<u8>,
limits: EnvelopeLimits,
) -> Result<[u8; ENVELOPE_BYTES], CodecError> {
command(envelope, Opcode::GroupDeleted, true)?;
validate_scope(reply.authority, reply.coordinator)?;
nonzero(reply.group.as_bytes())?;
let header = envelope.encode_header(DELETED_BYTES, 0, limits)?;
reserve(metadata, DELETED_BYTES)?;
metadata.push(SOURCE_TAG);
encode_authority(metadata, reply.authority);
metadata.extend_from_slice(reply.coordinator.as_bytes());
metadata.extend_from_slice(reply.group.as_bytes());
metadata.push(u8::from(!reply.deleted));
debug_assert_eq!(metadata.len(), DELETED_BYTES);
Ok(header)
}
pub fn decode_deleted(
packet: Packet<'_>,
limits: EnvelopeLimits,
) -> Result<DeleteReply, CodecError> {
check(packet, Opcode::GroupDeleted, true, limits)?;
if packet.metadata.len() != DELETED_BYTES {
return Err(CodecError::Length);
}
let mut cursor = Reader::new(packet.metadata);
let (authority, coordinator) = decode_scope(&mut cursor)?;
let group = ConsumerGroupId::from_bytes(cursor.array()?);
nonzero(group.as_bytes())?;
let deleted = match cursor.u8()? {
0 => true,
1 => false,
_ => return Err(CodecError::Profile),
};
Ok(DeleteReply {
authority,
coordinator,
group,
deleted,
})
}
fn name_bytes(name: &str) -> Result<usize, CodecError> {
if name.is_empty() || name.len() > MAX_NAME_BYTES {
return Err(CodecError::Limit);
}
Ok(name.len())
}
fn encode_name(metadata: &mut Vec<u8>, name: &str) {
metadata.push(name.len() as u8);
metadata.extend_from_slice(name.as_bytes());
}
fn decode_name(cursor: &mut Reader<'_>) -> Result<String, CodecError> {
let length = cursor.u8()? as usize;
if length == 0 {
return Err(CodecError::Limit);
}
std::str::from_utf8(cursor.take(length)?)
.map(str::to_owned)
.map_err(|_| CodecError::Profile)
}
fn decode_scope(cursor: &mut Reader<'_>) -> Result<(Authority, PartitionIncarnation), CodecError> {
if cursor.u8()? != SOURCE_TAG {
return Err(CodecError::Profile);
}
let authority = decode_authority(cursor)?;
let coordinator = PartitionIncarnation::from_bytes(cursor.array()?);
validate_scope(authority, coordinator)?;
Ok((authority, coordinator))
}
fn validate_inspection(reply: &Inspection) -> Result<(), CodecError> {
if reply.group.is_some() != (reply.revision != 0)
|| (reply.group.is_none() && (!reply.members.is_empty() || !reply.targets.is_empty()))
|| reply.members.len() > MAX_MEMBERS
|| reply.targets.len() > MAX_PARTITIONS
{
return Err(CodecError::Profile);
}
for member in &reply.members {
nonzero(member.id.as_bytes())?;
nonzero(member.node.as_bytes())?;
nonzero(member.join.as_bytes())?;
}
for target in &reply.targets {
nonzero(target.partition.as_bytes())?;
if target.plan_revision == 0 || target.plan_revision > reply.revision {
return Err(CodecError::Position);
}
}
Ok(())
}