blob: dc1f5ed0cc517a2c562e9693d546fd9dd091438b [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//! Sans-IO frame codec for the Iggy binary protocol.
//!
//! Encodes and decodes complete request/response frames without any I/O.
//! The transport layer (TCP, QUIC, WebSocket) reads bytes into a buffer,
//! then hands the buffer to these types for zero-copy parsing.
use crate::codec::{read_bytes, read_u32_le, read_u64_le};
use crate::error::WireError;
use bytes::{BufMut, BytesMut};
use std::borrow::Cow;
use std::num::NonZeroU32;
/// Status code for a successful response.
pub const STATUS_OK: u32 = 0;
/// Decoded request frame with request ID for request-response correlation
/// and consensus-level duplicate detection (the server framing).
///
/// Wire format: `[length:4 LE][code:4 LE][request_id:8 LE][payload:N]`
/// where `length` = 4 (code) + 8 (`request_id`) + N (payload).
#[derive(Debug)]
pub struct RequestFrame<'a> {
pub code: u32,
pub request_id: u64,
pub payload: &'a [u8],
}
impl<'a> RequestFrame<'a> {
/// Size of the frame header: `[length:4][code:4][request_id:8]`.
pub const HEADER_SIZE: usize = 16;
/// Validate a frame length field and return the payload size.
///
/// # Errors
/// Returns `WireError::Validation` if `frame_length < 12` (must contain
/// code + `request_id`).
pub fn payload_length(frame_length: u32) -> Result<u32, WireError> {
frame_length
.checked_sub(12)
.ok_or(WireError::Validation(Cow::Borrowed(
"request frame length must be at least 12 (code + request_id)",
)))
}
/// Construct a frame from pre-parsed header fields and a payload slice.
#[must_use]
pub const fn from_parts(code: u32, request_id: u64, payload: &'a [u8]) -> Self {
Self {
code,
request_id,
payload,
}
}
/// Decode a request frame from a complete buffer.
///
/// # Errors
/// Returns `WireError::UnexpectedEof` if the buffer is too short.
pub fn decode(buf: &'a [u8]) -> Result<(Self, usize), WireError> {
let frame_length = read_u32_le(buf, 0)?;
let payload_len = Self::payload_length(frame_length)? as usize;
let code = read_u32_le(buf, 4)?;
let request_id = read_u64_le(buf, 8)?;
let payload = read_bytes(buf, Self::HEADER_SIZE, payload_len)?;
let total = Self::HEADER_SIZE + payload_len;
Ok((
Self {
code,
request_id,
payload,
},
total,
))
}
/// Encode a request frame into `out`.
///
/// Writes `[length:4 LE][code:4 LE][request_id:8 LE][payload]` where
/// length = 4 (code) + 8 (`request_id`) + `payload.len()`.
///
/// # Errors
/// Returns `WireError::PayloadTooLarge` if payload exceeds u32 capacity.
pub fn encode(
code: u32,
request_id: u64,
payload: &[u8],
out: &mut BytesMut,
) -> Result<(), WireError> {
let length = payload
.len()
.checked_add(12)
.and_then(|n| u32::try_from(n).ok())
.ok_or(WireError::PayloadTooLarge {
size: payload.len(),
max: u32::MAX as usize - 12,
})?;
out.reserve(Self::HEADER_SIZE + payload.len());
out.put_u32_le(length);
out.put_u32_le(code);
out.put_u64_le(request_id);
out.put_slice(payload);
Ok(())
}
/// Total encoded size for a given payload length.
///
/// Returns `None` if `HEADER_SIZE + payload_len` overflows `usize`.
#[must_use]
pub const fn encoded_size(payload_len: usize) -> Option<usize> {
Self::HEADER_SIZE.checked_add(payload_len)
}
}
/// Decoded response frame with request ID for request-response correlation
/// (the server framing).
///
/// Wire format: `[status:4 LE][length:4 LE][request_id:8 LE][payload:N]`
/// where `status` = 0 for success, non-zero for error code.
#[derive(Debug)]
pub struct ResponseFrame<'a> {
pub status: u32,
pub request_id: u64,
pub payload: &'a [u8],
}
impl<'a> ResponseFrame<'a> {
/// Size of the frame header: `[status:4][length:4][request_id:8]`.
pub const HEADER_SIZE: usize = 16;
/// Decode a response frame from a complete buffer.
///
/// The `length` field covers `request_id(8) + payload(N)`.
///
/// # Errors
/// Returns `WireError::UnexpectedEof` if the buffer is too short.
pub fn decode(buf: &'a [u8]) -> Result<(Self, usize), WireError> {
let status = read_u32_le(buf, 0)?;
let length = read_u32_le(buf, 4)? as usize;
if length < 8 {
return Err(WireError::Validation(Cow::Borrowed(
"response frame length must be at least 8 (request_id)",
)));
}
let request_id = read_u64_le(buf, 8)?;
let payload_len = length - 8;
let payload = read_bytes(buf, Self::HEADER_SIZE, payload_len)?;
let total = Self::HEADER_SIZE + payload_len;
Ok((
Self {
status,
request_id,
payload,
},
total,
))
}
/// Encode a successful response with payload.
///
/// The `length` field = 8 (`request_id`) + `payload.len()`.
///
/// # Errors
/// Returns `WireError::PayloadTooLarge` if payload exceeds u32 capacity.
pub fn encode_ok(request_id: u64, payload: &[u8], out: &mut BytesMut) -> Result<(), WireError> {
let length = payload
.len()
.checked_add(8)
.and_then(|n| u32::try_from(n).ok())
.ok_or(WireError::PayloadTooLarge {
size: payload.len(),
max: u32::MAX as usize - 8,
})?;
out.reserve(Self::HEADER_SIZE + payload.len());
out.put_u32_le(STATUS_OK);
out.put_u32_le(length);
out.put_u64_le(request_id);
out.put_slice(payload);
Ok(())
}
/// Encode an error response (status code, no payload, preserves `request_id`).
pub fn encode_error(status: NonZeroU32, request_id: u64, out: &mut BytesMut) {
out.reserve(Self::HEADER_SIZE);
out.put_u32_le(status.get());
out.put_u32_le(8); // length = request_id only
out.put_u64_le(request_id);
}
/// Returns `true` if this is a success response.
#[must_use]
pub const fn is_ok(&self) -> bool {
self.status == STATUS_OK
}
/// Total encoded size for a given payload length.
///
/// Returns `None` if `HEADER_SIZE + payload_len` overflows `usize`.
#[must_use]
pub const fn encoded_size(payload_len: usize) -> Option<usize> {
Self::HEADER_SIZE.checked_add(payload_len)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn request2_roundtrip() {
let payload = b"hello world";
let mut buf = BytesMut::with_capacity(RequestFrame::encoded_size(payload.len()).unwrap());
RequestFrame::encode(42, 7, payload, &mut buf).unwrap();
let (frame, consumed) = RequestFrame::decode(&buf).unwrap();
assert_eq!(consumed, buf.len());
assert_eq!(frame.code, 42);
assert_eq!(frame.request_id, 7);
assert_eq!(frame.payload, payload);
}
#[test]
fn request2_empty_payload() {
let mut buf = BytesMut::with_capacity(RequestFrame::HEADER_SIZE);
RequestFrame::encode(1, 99, &[], &mut buf).unwrap();
let (frame, consumed) = RequestFrame::decode(&buf).unwrap();
assert_eq!(consumed, 16);
assert_eq!(frame.code, 1);
assert_eq!(frame.request_id, 99);
assert!(frame.payload.is_empty());
}
#[test]
fn request2_length_field_includes_code_and_request_id() {
let payload = b"test";
let mut buf = BytesMut::new();
RequestFrame::encode(99, 1, payload, &mut buf).unwrap();
let length = u32::from_le_bytes(buf[0..4].try_into().unwrap());
assert_eq!(length, 4 + 8 + 4); // code(4) + request_id(8) + payload(4)
}
#[test]
fn request2_truncated_header() {
let buf = [0u8; 15]; // less than HEADER_SIZE (16)
assert!(RequestFrame::decode(&buf).is_err());
}
#[test]
fn request2_truncated_payload() {
let mut buf = BytesMut::new();
buf.put_u32_le(112); // length = 112 (code + request_id + 100 bytes payload)
buf.put_u32_le(1); // code
buf.put_u64_le(1); // request_id
buf.put_slice(&[0u8; 50]); // only 50 of 100 bytes
assert!(RequestFrame::decode(&buf).is_err());
}
#[test]
fn request2_length_too_small() {
let mut buf = BytesMut::new();
buf.put_u32_le(11); // length < 12 (must include code + request_id)
buf.put_u32_le(1);
buf.put_u64_le(1);
assert!(RequestFrame::decode(&buf).is_err());
}
#[test]
fn request2_payload_length_valid() {
assert_eq!(RequestFrame::payload_length(12).unwrap(), 0);
assert_eq!(RequestFrame::payload_length(112).unwrap(), 100);
}
#[test]
fn request2_payload_length_too_small() {
assert!(RequestFrame::payload_length(0).is_err());
assert!(RequestFrame::payload_length(11).is_err());
}
#[test]
fn request2_encoded_size() {
assert_eq!(RequestFrame::encoded_size(0), Some(16));
assert_eq!(RequestFrame::encoded_size(100), Some(116));
assert_eq!(RequestFrame::encoded_size(usize::MAX), None);
}
#[test]
fn request2_from_parts() {
let payload = b"data";
let frame = RequestFrame::from_parts(5, 42, payload);
assert_eq!(frame.code, 5);
assert_eq!(frame.request_id, 42);
assert_eq!(frame.payload, payload);
}
// ResponseFrame tests
#[test]
fn response2_ok_roundtrip() {
let payload = b"response data";
let mut buf = BytesMut::with_capacity(ResponseFrame::encoded_size(payload.len()).unwrap());
ResponseFrame::encode_ok(7, payload, &mut buf).unwrap();
let (frame, consumed) = ResponseFrame::decode(&buf).unwrap();
assert_eq!(consumed, buf.len());
assert!(frame.is_ok());
assert_eq!(frame.request_id, 7);
assert_eq!(frame.payload, payload);
}
#[test]
fn response2_ok_empty_payload() {
let mut buf = BytesMut::new();
ResponseFrame::encode_ok(42, &[], &mut buf).unwrap();
let (frame, consumed) = ResponseFrame::decode(&buf).unwrap();
assert_eq!(consumed, 16);
assert!(frame.is_ok());
assert_eq!(frame.request_id, 42);
assert!(frame.payload.is_empty());
}
#[test]
fn response2_error_roundtrip() {
let mut buf = BytesMut::new();
ResponseFrame::encode_error(NonZeroU32::new(1001).unwrap(), 55, &mut buf);
let (frame, consumed) = ResponseFrame::decode(&buf).unwrap();
assert_eq!(consumed, 16);
assert!(!frame.is_ok());
assert_eq!(frame.status, 1001);
assert_eq!(frame.request_id, 55);
assert!(frame.payload.is_empty());
}
#[test]
fn response2_truncated_header() {
let buf = [0u8; 15];
assert!(ResponseFrame::decode(&buf).is_err());
}
#[test]
fn response2_length_too_small() {
let mut buf = BytesMut::new();
buf.put_u32_le(0); // status
buf.put_u32_le(7); // length < 8 (must include request_id)
buf.put_u64_le(1); // request_id
assert!(ResponseFrame::decode(&buf).is_err());
}
#[test]
fn response2_encoded_size() {
assert_eq!(ResponseFrame::encoded_size(0), Some(16));
assert_eq!(ResponseFrame::encoded_size(256), Some(272));
assert_eq!(ResponseFrame::encoded_size(usize::MAX), None);
}
}