iggy_binary_protocol 0.11.0-edge.7

Wire protocol types and codec for the Iggy binary protocol. Shared between server and SDK.
Documentation
// 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);
    }
}