use crate::session::ConsensusSession;
use bytes::{BufMut, Bytes, BytesMut};
use iggy_binary_protocol::codes::{
LOGIN_REGISTER_CODE, LOGIN_REGISTER_WITH_PAT_CODE, LOGOUT_USER_CODE,
};
use iggy_binary_protocol::consensus::{
Command, EvictionHeader, EvictionReason, GenericHeader, HEADER_SIZE, Operation, ReplyHeader,
RequestHeader, read_size_field, result_code, result_section_len,
};
use iggy_common::{IggyError, calculate_checksum, eviction_reason_to_error};
const NON_REPLICATED_CODE_RANGE: std::ops::Range<usize> = 0..4;
pub(crate) fn encode_contiguous_request(
session: &mut ConsensusSession,
code: u32,
payload: &Bytes,
) -> Result<Bytes, IggyError> {
let (header, total_size) = encode_request_header(session, code, payload)?;
let mut request = BytesMut::with_capacity(total_size);
request.put_slice(bytemuck::bytes_of(&header));
request.put_slice(payload);
Ok(request.freeze())
}
pub(crate) fn encode_request_header(
session: &mut ConsensusSession,
code: u32,
payload: &Bytes,
) -> Result<(RequestHeader, usize), IggyError> {
let (operation, request_id, session_id) = match code {
LOGIN_REGISTER_CODE | LOGIN_REGISTER_WITH_PAT_CODE => {
(Operation::Register, session.begin_register(), 0)
}
_ => {
let operation = operation_for_code(code);
if operation == Operation::NonReplicated {
(
operation,
session.current_request_id(),
session.session().unwrap_or(0),
)
} else if operation.is_partition() {
let session_id = session.session().ok_or(IggyError::Unauthenticated)?;
(operation, session.current_request_id(), session_id)
} else {
let session_id = session.session().ok_or(IggyError::Unauthenticated)?;
(operation, session.next_request_id(), session_id)
}
}
};
let request_checksum = if operation.is_partition() || operation == Operation::NonReplicated {
0
} else {
u128::from(calculate_checksum(payload))
};
let total_size = HEADER_SIZE
.checked_add(payload.len())
.ok_or(IggyError::InvalidConfiguration)?;
let size = u32::try_from(total_size).map_err(|_| IggyError::InvalidConfiguration)?;
let mut reserved = [0; 60];
if operation == Operation::NonReplicated {
reserved[NON_REPLICATED_CODE_RANGE].copy_from_slice(&code.to_le_bytes());
}
let header = RequestHeader {
command: Command::Request,
operation,
size,
client: session.client_id(),
request: request_id,
session: session_id,
request_checksum,
timestamp: 0,
reserved,
..Default::default()
};
Ok((header, total_size))
}
fn operation_for_code(code: u32) -> Operation {
if code == LOGOUT_USER_CODE {
return Operation::Logout;
}
Operation::from_command_code(code).unwrap_or(Operation::NonReplicated)
}
pub(crate) fn response_size(header: &[u8]) -> Result<usize, IggyError> {
let size = read_size_field(header).ok_or(IggyError::InvalidCommand)? as usize;
if size < HEADER_SIZE {
return Err(IggyError::InvalidCommand);
}
Ok(size)
}
pub(crate) fn decode_response(response: Bytes) -> Result<Bytes, IggyError> {
if response.len() < HEADER_SIZE {
return Err(IggyError::EmptyResponse);
}
let header_bytes: &[u8; HEADER_SIZE] = response[..HEADER_SIZE]
.try_into()
.map_err(|_| IggyError::InvalidCommand)?;
match peek_command(header_bytes) {
Command::Eviction => Err(decode_eviction(header_bytes)),
Command::Reply => {
let total_size = response_size(header_bytes)?;
if response.len() < total_size {
return Err(IggyError::InvalidCommand);
}
if let Some(error) = read_reply_status(header_bytes) {
return Err(error);
}
let operation = read_operation(header_bytes)?;
split_metadata_result(operation, response.slice(HEADER_SIZE..total_size))
}
_ => Err(IggyError::InvalidCommand),
}
}
pub(crate) fn decode_response_split(
header_bytes: &[u8; HEADER_SIZE],
body: Bytes,
) -> Result<Bytes, IggyError> {
match peek_command(header_bytes) {
Command::Eviction => Err(decode_eviction(header_bytes)),
Command::Reply => {
let expected_body = response_size(header_bytes)? - HEADER_SIZE;
if body.len() < expected_body {
return Err(IggyError::InvalidCommand);
}
if let Some(error) = read_reply_status(header_bytes) {
return Err(error);
}
let operation = read_operation(header_bytes)?;
split_metadata_result(operation, body.slice(..expected_body))
}
_ => Err(IggyError::InvalidCommand),
}
}
fn read_reply_status(header_bytes: &[u8; HEADER_SIZE]) -> Option<IggyError> {
const STATUS_OFFSET: usize = std::mem::offset_of!(ReplyHeader, status);
let mut bytes = [0u8; 4];
bytes.copy_from_slice(&header_bytes[STATUS_OFFSET..STATUS_OFFSET + 4]);
let status = u32::from_le_bytes(bytes);
(status != 0).then(|| IggyError::from_code(status))
}
fn read_operation(header_bytes: &[u8; HEADER_SIZE]) -> Result<Operation, IggyError> {
const OPERATION_OFFSET: usize = std::mem::offset_of!(ReplyHeader, operation);
bytemuck::checked::try_from_bytes::<Operation>(
&header_bytes[OPERATION_OFFSET..=OPERATION_OFFSET],
)
.copied()
.map_err(|_| IggyError::InvalidCommand)
}
fn split_metadata_result(operation: Operation, body: Bytes) -> Result<Bytes, IggyError> {
let result_framed =
operation.is_result_framed() || (operation == Operation::Register && !body.is_empty());
if !result_framed {
return Ok(body);
}
match result_code(&body) {
Some(0) => {
let payload_start = result_section_len(&body).ok_or(IggyError::InvalidCommand)?;
Ok(body.slice(payload_start..))
}
Some(code) => Err(IggyError::from_code(code)),
None => Err(IggyError::InvalidCommand),
}
}
fn peek_command(header_bytes: &[u8; HEADER_SIZE]) -> Command {
const COMMAND_OFFSET: usize = std::mem::offset_of!(GenericHeader, command);
match header_bytes[COMMAND_OFFSET] {
x if x == Command::Reply as u8 => Command::Reply,
x if x == Command::Eviction as u8 => Command::Eviction,
_ => Command::Reserved,
}
}
fn decode_eviction(header_bytes: &[u8; HEADER_SIZE]) -> IggyError {
const REASON_OFFSET: usize = std::mem::offset_of!(EvictionHeader, reason);
const VERSION_OFFSET: usize = std::mem::offset_of!(EvictionHeader, server_protocol_version);
const VERSION_MIN_OFFSET: usize =
std::mem::offset_of!(EvictionHeader, server_protocol_version_min);
let Ok(&reason) = bytemuck::checked::try_from_bytes::<EvictionReason>(
&header_bytes[REASON_OFFSET..=REASON_OFFSET],
) else {
return IggyError::Unauthenticated;
};
eviction_reason_to_error(
reason,
read_window_field(header_bytes, VERSION_OFFSET),
read_window_field(header_bytes, VERSION_MIN_OFFSET),
)
}
fn read_window_field(header_bytes: &[u8; HEADER_SIZE], offset: usize) -> u32 {
let mut value = [0u8; 4];
value.copy_from_slice(&header_bytes[offset..offset + 4]);
u32::from_le_bytes(value)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::session::ConsensusSession;
use iggy_binary_protocol::codes::{CREATE_STREAM_CODE, GET_STREAM_CODE, PING_CODE};
use iggy_binary_protocol::requests::streams::CreateStreamRequest;
use iggy_binary_protocol::requests::users::LoginRegisterRequest;
use iggy_binary_protocol::version::IGGY_PROTOCOL_VERSION;
use iggy_binary_protocol::{ClientVersionInfo, WireEncode, WireName, WireOptions};
use secrecy::SecretString;
fn decode_request_header(bytes: &Bytes) -> RequestHeader {
*bytemuck::checked::try_from_bytes::<RequestHeader>(&bytes[..HEADER_SIZE]).unwrap()
}
#[test]
fn second_register_on_bound_session_re_arms_instead_of_panicking() {
let request = LoginRegisterRequest {
version_info: ClientVersionInfo {
protocol_version: IGGY_PROTOCOL_VERSION,
sdk_name: WireName::new("rust-sdk").unwrap(),
sdk_version: WireName::new("1.0.0").unwrap(),
},
username: WireName::new("admin").unwrap(),
password: SecretString::from("secret"),
client_context: None,
};
let mut session = ConsensusSession::with_client_id(7);
encode_contiguous_request(&mut session, LOGIN_REGISTER_CODE, &request.to_bytes()).unwrap();
session.bind(42);
let bytes =
encode_contiguous_request(&mut session, LOGIN_REGISTER_CODE, &request.to_bytes())
.unwrap();
let header = decode_request_header(&bytes);
assert_eq!(header.operation, Operation::Register);
assert_eq!(header.request, 0);
assert_eq!(header.session, 0);
assert!(!session.is_bound());
}
#[test]
fn eviction_incompatible_protocol_decodes_to_typed_error() {
use iggy_binary_protocol::version::IGGY_PROTOCOL_VERSION_MIN;
#[repr(C, align(16))]
struct Misaligner([u8; HEADER_SIZE + 1]);
let header = EvictionHeader::incompatible_protocol(
0,
0,
0,
0xCAFE,
IGGY_PROTOCOL_VERSION,
IGGY_PROTOCOL_VERSION_MIN,
);
let mut raw = Misaligner([0; HEADER_SIZE + 1]);
raw.0[1..].copy_from_slice(bytemuck::bytes_of(&header));
let shifted: &[u8; HEADER_SIZE] = raw.0[1..].try_into().unwrap();
let result = decode_response_split(shifted, Bytes::new());
assert!(matches!(
result,
Err(IggyError::IncompatibleProtocolVersion(client, min, max))
if client == IGGY_PROTOCOL_VERSION
&& min == IGGY_PROTOCOL_VERSION_MIN
&& max == IGGY_PROTOCOL_VERSION
));
}
#[test]
fn eviction_with_invalid_window_degrades_to_unauthenticated() {
for (server_max, server_min) in [(1, 0), (1, 2)] {
let mut header = EvictionHeader::incompatible_protocol(0, 0, 0, 0xCAFE, 1, 1);
header.server_protocol_version = server_max;
header.server_protocol_version_min = server_min;
let mut buf = [0u8; HEADER_SIZE];
buf.copy_from_slice(bytemuck::bytes_of(&header));
let result = decode_response_split(&buf, Bytes::new());
assert!(
matches!(result, Err(IggyError::Unauthenticated)),
"window [{server_min}, {server_max}] must not surface as typed error"
);
}
}
#[test]
fn reply_with_nonzero_status_surfaces_as_typed_error() {
let header = ReplyHeader {
command: Command::Reply,
size: HEADER_SIZE as u32,
status: IggyError::Unauthorized.as_code(),
..Default::default()
};
let mut buf = [0u8; HEADER_SIZE];
buf.copy_from_slice(bytemuck::bytes_of(&header));
let result = decode_response_split(&buf, Bytes::new());
assert!(matches!(result, Err(IggyError::Unauthorized)));
}
#[test]
fn reply_with_zero_status_passes_body_through() {
let header = ReplyHeader {
command: Command::Reply,
operation: Operation::NonReplicated,
size: (HEADER_SIZE + 3) as u32,
..Default::default()
};
let mut buf = [0u8; HEADER_SIZE];
buf.copy_from_slice(bytemuck::bytes_of(&header));
let out = decode_response_split(&buf, Bytes::from_static(b"abc")).unwrap();
assert_eq!(&out[..], b"abc");
}
#[test]
fn replicated_request_increments_request_counter() {
let mut session = ConsensusSession::with_client_id(42);
let _ = session.register_request_id();
session.bind(99);
let payload = CreateStreamRequest {
name: WireName::new("stream").unwrap(),
options: WireOptions::empty(),
}
.to_bytes();
let first = encode_contiguous_request(&mut session, CREATE_STREAM_CODE, &payload).unwrap();
let second = encode_contiguous_request(&mut session, CREATE_STREAM_CODE, &payload).unwrap();
assert_eq!(decode_request_header(&first).request, 1);
assert_eq!(decode_request_header(&second).request, 2);
assert_eq!(decode_request_header(&second).session, 99);
}
#[test]
fn request_checksum_is_stamped_only_for_deduped_operations() {
let mut session = ConsensusSession::with_client_id(42);
session.bind(99);
let payload = Bytes::from_static(b"payload");
let deduped =
encode_contiguous_request(&mut session, CREATE_STREAM_CODE, &payload).unwrap();
assert_eq!(
decode_request_header(&deduped).request_checksum,
u128::from(calculate_checksum(&payload)),
);
let ping = encode_contiguous_request(&mut session, PING_CODE, &Bytes::new()).unwrap();
assert_eq!(decode_request_header(&ping).request_checksum, 0);
}
#[test]
fn ping_uses_non_replicated_operation() {
let mut session = ConsensusSession::with_client_id(42);
session.bind(99);
let bytes = encode_contiguous_request(&mut session, PING_CODE, &Bytes::new()).unwrap();
let header = decode_request_header(&bytes);
assert_eq!(header.operation, Operation::NonReplicated);
assert_eq!(
u32::from_le_bytes(
header.reserved[NON_REPLICATED_CODE_RANGE]
.try_into()
.unwrap()
),
PING_CODE
);
assert_eq!(header.session, 99);
}
#[test]
fn logout_uses_replicated_logout_operation() {
let mut session = ConsensusSession::with_client_id(42);
session.bind(99);
let bytes =
encode_contiguous_request(&mut session, LOGOUT_USER_CODE, &Bytes::new()).unwrap();
let header = decode_request_header(&bytes);
assert_eq!(header.operation, Operation::Logout);
assert_eq!(header.request, 1);
assert_eq!(header.session, 99);
}
#[test]
fn read_only_request_uses_non_replicated_operation() {
let mut session = ConsensusSession::with_client_id(42);
session.bind(99);
let bytes =
encode_contiguous_request(&mut session, GET_STREAM_CODE, &Bytes::new()).unwrap();
let header = decode_request_header(&bytes);
assert_eq!(header.operation, Operation::NonReplicated);
assert_eq!(
u32::from_le_bytes(
header.reserved[NON_REPLICATED_CODE_RANGE]
.try_into()
.unwrap()
),
GET_STREAM_CODE
);
assert_eq!(header.session, 99);
}
#[test]
fn unknown_code_encodes_as_non_replicated_and_carries_the_code() {
const UNKNOWN_CODE: u32 = 60_000;
assert!(
iggy_binary_protocol::dispatch::lookup_command(UNKNOWN_CODE).is_none(),
"test needs a code absent from COMMAND_TABLE"
);
let mut session = ConsensusSession::with_client_id(42);
session.bind(99);
let bytes = encode_contiguous_request(&mut session, UNKNOWN_CODE, &Bytes::new()).unwrap();
let header = decode_request_header(&bytes);
assert_eq!(header.operation, Operation::NonReplicated);
assert_eq!(
u32::from_le_bytes(
header.reserved[NON_REPLICATED_CODE_RANGE]
.try_into()
.unwrap()
),
UNKNOWN_CODE
);
}
#[test]
fn no_replicated_command_ever_resolves_to_non_replicated() {
for meta in iggy_binary_protocol::dispatch::COMMAND_TABLE {
if !meta.is_replicated() {
continue;
}
assert_ne!(
operation_for_code(meta.code),
Operation::NonReplicated,
"replicated command {} ({}) must never encode as NonReplicated",
meta.name,
meta.code
);
}
}
#[test]
fn metadata_success_reply_strips_result_section_and_returns_payload() {
let mut body = BytesMut::new();
body.put_u32_le(0); body.put_slice(b"payload");
let payload = split_metadata_result(Operation::CreateStream, body.freeze()).unwrap();
assert_eq!(&payload[..], b"payload");
}
#[test]
fn metadata_rejection_reply_maps_committed_code_to_iggy_error() {
let mut body = BytesMut::new();
body.put_u32_le(1); body.put_u32_le(0); body.put_u32_le(IggyError::StreamIdNotFound(Default::default()).as_code());
let err = split_metadata_result(Operation::DeleteStream, body.freeze()).unwrap_err();
assert_eq!(
err.as_code(),
IggyError::StreamIdNotFound(Default::default()).as_code()
);
}
#[test]
fn metadata_reply_with_truncated_section_is_invalid_command_never_ok() {
let mut body = BytesMut::new();
body.put_u32_le(1);
let err = split_metadata_result(Operation::CreateStream, body.freeze()).unwrap_err();
assert!(matches!(err, IggyError::InvalidCommand));
}
#[test]
fn non_metadata_reply_passes_through_without_a_result_section() {
let body = Bytes::from_static(b"raw-non-metadata-body");
let out = split_metadata_result(Operation::NonReplicated, body.clone()).unwrap();
assert_eq!(out, body);
}
fn rejection_body(code: u32) -> Bytes {
let mut body = Vec::with_capacity(12);
body.extend_from_slice(&1u32.to_le_bytes());
body.extend_from_slice(&0u32.to_le_bytes());
body.extend_from_slice(&code.to_le_bytes());
Bytes::from(body)
}
fn success_body(payload: &[u8]) -> Bytes {
let mut body = Vec::with_capacity(4 + payload.len());
body.extend_from_slice(&0u32.to_le_bytes());
body.extend_from_slice(payload);
Bytes::from(body)
}
#[test]
fn delete_consumer_offset_rejection_decodes_to_terminal_error() {
let code = IggyError::ConsumerOffsetNotFound(0).as_code();
let result = split_metadata_result(Operation::DeleteConsumerOffset, rejection_body(code));
assert_eq!(
result.unwrap_err().as_code(),
code,
"delete rejection must surface as the typed error, not decode as Ok"
);
}
#[test]
fn store_consumer_offset_rejection_decodes_to_terminal_error() {
let code = IggyError::InvalidOffset(42).as_code();
let result = split_metadata_result(Operation::StoreConsumerOffset, rejection_body(code));
assert_eq!(result.unwrap_err().as_code(), code);
}
#[test]
fn consumer_offset_success_strips_the_empty_result_section() {
let out = split_metadata_result(Operation::StoreConsumerOffset, success_body(b"")).unwrap();
assert!(out.is_empty());
let out =
split_metadata_result(Operation::DeleteConsumerOffset, success_body(b"")).unwrap();
assert!(out.is_empty());
}
#[test]
fn metadata_transient_code_decodes_to_transient_not_committed() {
let code = IggyError::TransientNotCommitted.as_code();
let result = split_metadata_result(Operation::CreateStream, rejection_body(code));
assert!(matches!(
result.unwrap_err(),
IggyError::TransientNotCommitted
));
}
#[test]
fn metadata_success_returns_payload_after_result_section() {
let out = split_metadata_result(Operation::CreateStream, success_body(b"payload")).unwrap();
assert_eq!(out.as_ref(), b"payload");
}
#[test]
fn send_messages_body_is_never_interpreted_as_a_result_section() {
let body = rejection_body(IggyError::InvalidOffset(1).as_code());
let out = split_metadata_result(Operation::SendMessages, body.clone()).unwrap();
assert_eq!(out, body);
}
}