aerostream_client/
protocol.rs1use bytes::{BufMut, Bytes, BytesMut};
2use crate::error::AeroError;
3
4pub const MAGIC: [u8; 2] = [0xAE, 0x01];
6
7pub const HEADER_LEN: usize = 7;
9
10pub const STATUS_OK: u8 = 0;
12
13pub const STATUS_EMPTY: u8 = 1;
15
16pub const STATUS_DATA: u8 = 2;
18
19pub const STATUS_AUTH_FAILED: u8 = 3;
21
22pub const STATUS_OUT_OF_ORDER: u8 = 45;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27#[repr(u8)]
28pub enum Command {
29 Auth = 0,
31 Produce = 1,
33 Fetch = 2,
35 ReplicaFetch = 3,
37 FetchMulti = 4,
39}
40
41impl Command {
42 pub fn from_u8(b: u8) -> Result<Self, AeroError> {
44 match b {
45 0 => Ok(Command::Auth),
46 1 => Ok(Command::Produce),
47 2 => Ok(Command::Fetch),
48 3 => Ok(Command::ReplicaFetch),
49 4 => Ok(Command::FetchMulti),
50 other => Err(AeroError::ProtocolError(format!("Unknown command ID: {other}"))),
51 }
52 }
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct Record {
58 pub offset: u64,
60 pub payload: Bytes,
62}
63
64impl Record {
65 pub fn new(offset: u64, payload: Bytes) -> Self {
67 Self { offset, payload }
68 }
69}
70
71#[inline]
73pub fn is_valid_magic(b0: u8, b1: u8) -> bool {
74 b0 == MAGIC[0] && b1 == MAGIC[1]
75}
76
77pub fn encode_header(cmd: Command, body_len: u32, buf: &mut BytesMut) {
79 buf.put_u8(MAGIC[0]);
80 buf.put_u8(MAGIC[1]);
81 buf.put_u8(cmd as u8);
82 buf.put_u32(body_len);
83}
84
85pub fn encode_auth_request(token: &str) -> Bytes {
87 let token_bytes = token.as_bytes();
88 let mut buf = BytesMut::with_capacity(HEADER_LEN + token_bytes.len());
89 encode_header(Command::Auth, token_bytes.len() as u32, &mut buf);
90 buf.put_slice(token_bytes);
91 buf.freeze()
92}
93
94pub fn encode_produce_request(topic: &str, partition: u32, payload: &[u8]) -> Result<Bytes, AeroError> {
97 let topic_bytes = topic.as_bytes();
98 if topic_bytes.len() > u16::MAX as usize {
99 return Err(AeroError::ProtocolError(format!(
100 "Topic length {} exceeds u16::MAX",
101 topic_bytes.len()
102 )));
103 }
104 let body_len = 2 + topic_bytes.len() + 4 + 4 + payload.len();
105 if body_len > u32::MAX as usize {
106 return Err(AeroError::ProtocolError("Produce body length exceeds u32::MAX".into()));
107 }
108
109 let mut buf = BytesMut::with_capacity(HEADER_LEN + body_len);
110 encode_header(Command::Produce, body_len as u32, &mut buf);
111 buf.put_u16(topic_bytes.len() as u16);
112 buf.put_slice(topic_bytes);
113 buf.put_u32(partition);
114 buf.put_u32(payload.len() as u32);
115 buf.put_slice(payload);
116 Ok(buf.freeze())
117}
118
119pub fn encode_fetch_request(
122 topic: &str,
123 partition: u32,
124 start_offset: u64,
125 max_bytes: u32,
126) -> Result<Bytes, AeroError> {
127 let topic_bytes = topic.as_bytes();
128 if topic_bytes.len() > u16::MAX as usize {
129 return Err(AeroError::ProtocolError(format!(
130 "Topic length {} exceeds u16::MAX",
131 topic_bytes.len()
132 )));
133 }
134 let body_len = 2 + topic_bytes.len() + 4 + 8 + 4;
135 let mut buf = BytesMut::with_capacity(HEADER_LEN + body_len);
136 encode_header(Command::Fetch, body_len as u32, &mut buf);
137 buf.put_u16(topic_bytes.len() as u16);
138 buf.put_slice(topic_bytes);
139 buf.put_u32(partition);
140 buf.put_u64(start_offset);
141 buf.put_u32(max_bytes);
142 Ok(buf.freeze())
143}
144
145pub fn encode_fetch_multi_request(
148 topic: &str,
149 partition: u32,
150 start_offset: u64,
151 max_bytes: u32,
152 max_wait_ms: u32,
153) -> Result<Bytes, AeroError> {
154 let topic_bytes = topic.as_bytes();
155 if topic_bytes.len() > u16::MAX as usize {
156 return Err(AeroError::ProtocolError(format!(
157 "Topic length {} exceeds u16::MAX",
158 topic_bytes.len()
159 )));
160 }
161 let body_len = 2 + topic_bytes.len() + 4 + 8 + 4 + 4;
162 let mut buf = BytesMut::with_capacity(HEADER_LEN + body_len);
163 encode_header(Command::FetchMulti, body_len as u32, &mut buf);
164 buf.put_u16(topic_bytes.len() as u16);
165 buf.put_slice(topic_bytes);
166 buf.put_u32(partition);
167 buf.put_u64(start_offset);
168 buf.put_u32(max_bytes);
169 buf.put_u32(max_wait_ms);
170 Ok(buf.freeze())
171}
172
173pub fn decode_status_prefix(bytes: &[u8; 3]) -> Result<u8, AeroError> {
175 if !is_valid_magic(bytes[0], bytes[1]) {
176 return Err(AeroError::ProtocolError(format!(
177 "Invalid magic prefix: [0x{:02X}, 0x{:02X}], expected [0xAE, 0x01]",
178 bytes[0], bytes[1]
179 )));
180 }
181 Ok(bytes[2])
182}
183
184pub fn decode_produce_response(status: u8, offset_bytes: Option<&[u8; 8]>) -> Result<u64, AeroError> {
186 match status {
187 STATUS_OK => {
188 let bytes = offset_bytes.ok_or_else(|| {
189 AeroError::ProtocolError("Produce OK status missing 8-byte offset".into())
190 })?;
191 Ok(u64::from_be_bytes(*bytes))
192 }
193 STATUS_AUTH_FAILED => Err(AeroError::AuthFailed("Produce rejected: not authenticated".into())),
194 STATUS_OUT_OF_ORDER => Err(AeroError::OutOfOrder),
195 other => Err(AeroError::ProtocolError(format!("Produce rejected with status: {other}"))),
196 }
197}
198
199pub fn decode_multi_entries(
201 entry_count: u32,
202 index_data: &[u8],
203 mut payload_bytes: Bytes,
204) -> Result<Vec<Record>, AeroError> {
205 let expected_index_len = (entry_count as usize) * 12;
206 if index_data.len() < expected_index_len {
207 return Err(AeroError::ProtocolError(format!(
208 "Multi-fetch index data too short: got {} bytes, expected {}",
209 index_data.len(),
210 expected_index_len
211 )));
212 }
213
214 let mut records = Vec::with_capacity(entry_count as usize);
215 let mut offset_idx = 0;
216 for _ in 0..entry_count {
217 let off = u64::from_be_bytes(index_data[offset_idx..offset_idx + 8].try_into().unwrap());
218 let len = u32::from_be_bytes(index_data[offset_idx + 8..offset_idx + 12].try_into().unwrap()) as usize;
219 offset_idx += 12;
220
221 if payload_bytes.len() < len {
222 return Err(AeroError::ProtocolError(format!(
223 "Multi-fetch payload truncated: needed {} bytes, remaining {}",
224 len,
225 payload_bytes.len()
226 )));
227 }
228 let record_payload = payload_bytes.split_to(len);
229 records.push(Record::new(off, record_payload));
230 }
231
232 Ok(records)
233}