1use bytes::{Buf, BufMut, Bytes, BytesMut};
16
17use super::{MAX_RECORDS_PER_BATCH, Record, RecordError, RecordErrorKind, Result};
18use crate::{
19 compression::Compression,
20 crc,
21 primitives::{PrimitiveError, PrimitiveErrorKind},
22};
23
24const MAGIC_V2: i8 = 2;
25const LOG_OVERHEAD: usize = 12;
26const BATCH_HEADER_SIZE: i32 = 49;
27const BATCH_HEADER_SIZE_USIZE: usize = 49;
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31#[non_exhaustive]
32pub enum TimestampType {
33 CreateTime,
35 LogAppendTime,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct RecordBatch {
45 pub base_offset: i64,
47 pub partition_leader_epoch: i32,
49 pub magic: i8,
51 pub attributes: i16,
54 pub last_offset_delta: i32,
56 pub first_timestamp: i64,
58 pub max_timestamp: i64,
60 pub producer_id: i64,
62 pub producer_epoch: i16,
64 pub base_sequence: i32,
66 pub records: Vec<Record>,
68}
69
70impl RecordBatch {
71 pub fn encode(&self, buf: &mut BytesMut) -> Result<()> {
74 self.encode_with_compression_level(buf, None)
75 }
76
77 pub fn encode_with_compression_level(
79 &self,
80 buf: &mut BytesMut,
81 compression_level: Option<i32>,
82 ) -> Result<()> {
83 let codec =
84 Compression::from_attributes(self.attributes).map_err(|e| self.lift_compression(e))?;
85 if codec == Compression::None {
86 let records_payload_len = self.records_encoded_len()?;
87 return self.write_batch(buf, records_payload_len, |buf| self.write_records(buf));
88 }
89
90 let mut records_buf = BytesMut::with_capacity(self.records_encoded_len()?);
91 self.write_records(&mut records_buf)?;
92 let records_payload = Bytes::from(
93 codec
94 .compress_with_level(&records_buf, compression_level)
95 .map_err(|e| self.lift_compression(e))?,
96 );
97 self.write_batch(buf, records_payload.len(), |buf| {
98 buf.extend_from_slice(&records_payload);
99 Ok(())
100 })
101 }
102
103 pub fn uncompressed_encoded_len(&self) -> Result<usize> {
106 let len = super::add_encoded_len("record batch", LOG_OVERHEAD, BATCH_HEADER_SIZE_USIZE)?;
107 super::add_encoded_len("record batch", len, self.records_encoded_len()?)
108 }
109
110 fn records_encoded_len(&self) -> Result<usize> {
111 let mut len = 0;
112 for record in &self.records {
113 len = super::add_encoded_len(
114 "record batch records",
115 len,
116 record
117 .encoded_len()
118 .map_err(|e| RecordError::at_offset(self.base_offset, e.kind))?,
119 )?;
120 }
121 Ok(len)
122 }
123
124 fn write_records(&self, buf: &mut BytesMut) -> Result<()> {
125 for record in &self.records {
126 record
127 .encode(buf)
128 .map_err(|e| RecordError::at_offset(self.base_offset, e.kind))?;
129 }
130 Ok(())
131 }
132
133 fn write_batch<F>(
134 &self,
135 buf: &mut BytesMut,
136 records_payload_len: usize,
137 write_records_payload: F,
138 ) -> Result<()>
139 where
140 F: FnOnce(&mut BytesMut) -> Result<()>,
141 {
142 buf.put_i64(self.base_offset);
143
144 buf.put_i32(self.batch_length(records_payload_len)?);
145
146 buf.put_i32(self.partition_leader_epoch);
147 buf.put_i8(self.magic);
148
149 let crc_field_pos = buf.len();
150 buf.put_u32(0);
151
152 let crc_start = buf.len();
153 buf.put_i16(self.attributes);
154 buf.put_i32(self.last_offset_delta);
155 buf.put_i64(self.first_timestamp);
156 buf.put_i64(self.max_timestamp);
157 buf.put_i64(self.producer_id);
158 buf.put_i16(self.producer_epoch);
159 buf.put_i32(self.base_sequence);
160 buf.put_i32(self.record_count()?);
161 write_records_payload(buf)?;
162
163 let crc = crc::crc32c(buf.get(crc_start..).unwrap_or(&[]));
164 let crc_bytes = crc.to_be_bytes();
165 let crc_field_end = crc_field_pos.checked_add(4).ok_or_else(|| {
166 RecordError::at_offset(
167 self.base_offset,
168 RecordErrorKind::LengthOverflow {
169 field: "crc field",
170 got: crc_field_pos,
171 remaining: buf.len(),
172 },
173 )
174 })?;
175 let Some(slot) = buf.get_mut(crc_field_pos..crc_field_end) else {
176 return Err(RecordError::at_offset(
177 self.base_offset,
178 RecordErrorKind::LengthOverflow {
179 field: "crc field",
180 got: crc_field_end,
181 remaining: buf.len(),
182 },
183 ));
184 };
185 slot.copy_from_slice(&crc_bytes);
186
187 Ok(())
188 }
189
190 fn batch_length(&self, records_payload_len: usize) -> Result<i32> {
191 let payload_len = i32::try_from(records_payload_len).map_err(|_| {
192 RecordError::at_offset(
193 self.base_offset,
194 RecordErrorKind::LengthOverflow {
195 field: "compressed records",
196 got: records_payload_len,
197 remaining: usize::try_from(i32::MAX).unwrap_or(usize::MAX),
198 },
199 )
200 })?;
201 BATCH_HEADER_SIZE.checked_add(payload_len).ok_or_else(|| {
202 RecordError::at_offset(
203 self.base_offset,
204 RecordErrorKind::LengthOverflow {
205 field: "batch length",
206 got: records_payload_len,
207 remaining: usize::try_from(i32::MAX).unwrap_or(usize::MAX),
208 },
209 )
210 })
211 }
212
213 fn record_count(&self) -> Result<i32> {
214 i32::try_from(self.records.len()).map_err(|_| {
215 RecordError::at_offset(
216 self.base_offset,
217 RecordErrorKind::RecordCountTooLarge {
218 got: i32::MAX,
219 max: MAX_RECORDS_PER_BATCH,
220 },
221 )
222 })
223 }
224
225 #[expect(
228 clippy::too_many_lines,
229 reason = "Record-batch decoding mirrors the Kafka wire layout step-by-step; splitting it \
230 before generated protocol decode is stable would obscure validation order."
231 )]
232 pub fn decode(buf: &mut Bytes) -> Result<Self> {
233 let available = buf.remaining();
234 if available < LOG_OVERHEAD {
235 return Err(RecordError::unknown_offset(RecordErrorKind::Primitive(
236 PrimitiveError::from(PrimitiveErrorKind::InsufficientData {
237 needed: LOG_OVERHEAD,
238 available,
239 }),
240 )));
241 }
242 let base_offset = buf.get_i64();
243 let batch_length = buf.get_i32();
244
245 if batch_length < BATCH_HEADER_SIZE {
246 return Err(RecordError::at_offset(
247 base_offset,
248 RecordErrorKind::BatchTooSmall {
249 got: batch_length,
250 min: BATCH_HEADER_SIZE,
251 },
252 ));
253 }
254 let batch_len = usize::try_from(batch_length).map_err(|_| {
255 RecordError::at_offset(
256 base_offset,
257 RecordErrorKind::LengthOverflow {
258 field: "batch payload",
259 got: usize::MAX,
260 remaining: buf.remaining(),
261 },
262 )
263 })?;
264 let remaining = buf.remaining();
265 if batch_len > remaining {
266 return Err(RecordError::at_offset(
267 base_offset,
268 RecordErrorKind::LengthOverflow {
269 field: "batch payload",
270 got: batch_len,
271 remaining,
272 },
273 ));
274 }
275
276 let mut batch_data = buf.split_to(batch_len);
277
278 let crc_slice = batch_data.get(5..9).ok_or_else(|| {
279 RecordError::at_offset(
280 base_offset,
281 RecordErrorKind::LengthOverflow {
282 field: "crc field",
283 got: 9,
284 remaining: batch_data.len(),
285 },
286 )
287 })?;
288 let crc_bytes: [u8; 4] = crc_slice.try_into().map_err(|_| {
289 RecordError::at_offset(
290 base_offset,
291 RecordErrorKind::LengthOverflow {
292 field: "crc field",
293 got: 4,
294 remaining: crc_slice.len(),
295 },
296 )
297 })?;
298 let stored_crc = u32::from_be_bytes(crc_bytes);
299 let crc_payload = batch_data.get(9..).unwrap_or(&[]);
300 crc::validate_crc32c(crc_payload, stored_crc)
301 .map_err(|e| RecordError::at_offset(base_offset, RecordErrorKind::Crc(e)))?;
302
303 let partition_leader_epoch = batch_data.get_i32();
304 let magic = batch_data.get_i8();
305 if magic != MAGIC_V2 {
306 return Err(RecordError::at_offset(
307 base_offset,
308 RecordErrorKind::UnsupportedMagic(magic),
309 ));
310 }
311 let _crc = batch_data.get_u32();
312 let attributes = batch_data.get_i16();
313 let last_offset_delta = batch_data.get_i32();
314 let first_timestamp = batch_data.get_i64();
315 let max_timestamp = batch_data.get_i64();
316 let producer_id = batch_data.get_i64();
317 let producer_epoch = batch_data.get_i16();
318 let base_sequence = batch_data.get_i32();
319 let record_count = batch_data.get_i32();
320
321 if record_count < 0 {
322 return Err(RecordError::at_offset(
323 base_offset,
324 RecordErrorKind::NegativeLength {
325 field: "record count",
326 length: record_count,
327 },
328 ));
329 }
330 let record_count_usize = usize::try_from(record_count).map_err(|_| {
331 RecordError::at_offset(
332 base_offset,
333 RecordErrorKind::LengthOverflow {
334 field: "record count",
335 got: usize::MAX,
336 remaining: MAX_RECORDS_PER_BATCH,
337 },
338 )
339 })?;
340 if record_count_usize > MAX_RECORDS_PER_BATCH {
341 return Err(RecordError::at_offset(
342 base_offset,
343 RecordErrorKind::RecordCountTooLarge {
344 got: record_count,
345 max: MAX_RECORDS_PER_BATCH,
346 },
347 ));
348 }
349
350 let codec = Compression::from_attributes(attributes)
351 .map_err(|e| RecordError::at_offset(base_offset, RecordErrorKind::Compression(e)))?;
352 let mut records_data = if codec == Compression::None {
353 batch_data
354 } else {
355 let decompressed = codec.decompress(&batch_data).map_err(|e| {
356 RecordError::at_offset(base_offset, RecordErrorKind::Compression(e))
357 })?;
358 Bytes::from(decompressed)
359 };
360
361 let mut records = Vec::with_capacity(record_count_usize);
362 for _ in 0..record_count {
363 let rec = Record::decode(&mut records_data)
364 .map_err(|e| RecordError::at_offset(base_offset, e.kind))?;
365 records.push(rec);
366 }
367
368 Ok(Self {
369 base_offset,
370 partition_leader_epoch,
371 magic,
372 attributes,
373 last_offset_delta,
374 first_timestamp,
375 max_timestamp,
376 producer_id,
377 producer_epoch,
378 base_sequence,
379 records,
380 })
381 }
382
383 pub fn compression(&self) -> Result<Compression> {
385 Compression::from_attributes(self.attributes).map_err(|e| self.lift_compression(e))
386 }
387
388 #[must_use]
390 pub const fn timestamp_type(&self) -> TimestampType {
391 if self.attributes & 0x08 != 0 {
392 TimestampType::LogAppendTime
393 } else {
394 TimestampType::CreateTime
395 }
396 }
397
398 #[must_use]
400 pub const fn is_transactional(&self) -> bool {
401 self.attributes & 0x10 != 0
402 }
403
404 #[must_use]
406 pub const fn is_control_batch(&self) -> bool {
407 self.attributes & 0x20 != 0
408 }
409
410 const fn lift_compression(&self, err: crate::compression::CompressionError) -> RecordError {
411 RecordError::at_offset(self.base_offset, RecordErrorKind::Compression(err))
412 }
413}
414
415pub fn decode_next_batch(buf: &mut Bytes) -> Result<Option<RecordBatch>> {
422 if buf.remaining() < LOG_OVERHEAD {
423 return Ok(None);
424 }
425 let Some(len_slice) = buf.get(8..12) else {
426 return Ok(None);
427 };
428 let len_bytes: [u8; 4] = match len_slice.try_into() {
429 Ok(arr) => arr,
430 Err(_) => return Ok(None),
431 };
432 let batch_length = i32::from_be_bytes(len_bytes);
433 if batch_length < 0 {
434 return Ok(None);
435 }
436 let Ok(batch_len) = usize::try_from(batch_length) else {
437 return Ok(None);
438 };
439 let needed = LOG_OVERHEAD.saturating_add(batch_len);
440 if buf.remaining() < needed {
441 return Ok(None);
442 }
443 RecordBatch::decode(buf).map(Some)
444}
445
446pub fn decode_batches(buf: &mut Bytes) -> Result<Vec<RecordBatch>> {
451 let mut batches = Vec::new();
452 while let Some(batch) = decode_next_batch(buf)? {
453 batches.push(batch);
454 }
455 Ok(batches)
456}
457
458#[cfg(test)]
459mod tests {
460 #![allow(
461 clippy::expect_used,
462 clippy::missing_assert_message,
463 reason = "Record-batch encoding tests fail fastest with contextual expect calls."
464 )]
465
466 use bytes::{Bytes, BytesMut};
467
468 use super::{Record, RecordBatch};
469 use crate::record::RecordHeader;
470
471 #[test]
472 fn batch_uncompressed_encoded_len_matches_encoded_bytes() {
473 let batch = RecordBatch {
474 base_offset: 0,
475 partition_leader_epoch: -1,
476 magic: 2,
477 attributes: 0,
478 last_offset_delta: 1,
479 first_timestamp: 10,
480 max_timestamp: 11,
481 producer_id: -1,
482 producer_epoch: -1,
483 base_sequence: -1,
484 records: vec![
485 Record {
486 attributes: 0,
487 timestamp_delta: 0,
488 offset_delta: 0,
489 key: Some(Bytes::from_static(b"key-0")),
490 value: Some(Bytes::from_static(b"value-0")),
491 headers: Vec::new(),
492 },
493 Record {
494 attributes: 0,
495 timestamp_delta: 1,
496 offset_delta: 1,
497 key: None,
498 value: Some(Bytes::from_static(b"value-1")),
499 headers: vec![RecordHeader {
500 key: Bytes::from_static(b"h"),
501 value: None,
502 }],
503 },
504 ],
505 };
506 let encoded_len = batch.uncompressed_encoded_len().expect("batch encoded len");
507 let mut bytes = BytesMut::with_capacity(encoded_len);
508
509 batch.encode(&mut bytes).expect("batch encode");
510
511 assert_eq!(encoded_len, bytes.len());
512 assert_eq!(encoded_len, bytes.capacity());
513 let decoded = RecordBatch::decode(&mut bytes.freeze()).expect("record batch decode");
514 assert_eq!(decoded.records.len(), 2);
515 }
516}