1use bytes::Bytes;
4use zerocopy::FromBytes as _;
5
6use crate::primitives::varint::{get_varint, get_varlong};
7use crate::records::RecordsError;
8use crate::records::crc::{crc32c, crc32c_append};
9use crate::records::header::{Attributes, HEADER_LEN, RecordBatchHeader};
10
11const HEADER_TAIL_LEN: i32 = 49;
17
18pub struct RecordBatch<'a> {
19 pub(crate) header: &'a RecordBatchHeader,
20 pub(crate) body: RecordBody<'a>,
21}
22
23pub(crate) enum RecordBody<'a> {
24 Borrowed(&'a [u8]),
25 Owned(Bytes),
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct Record<'a> {
30 pub attributes: i8,
31 pub timestamp_delta: i64,
32 pub offset_delta: i32,
33 pub key: Option<&'a [u8]>,
34 pub value: Option<&'a [u8]>,
35 pub headers: Vec<RecordHeader<'a>>,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct RecordHeader<'a> {
40 pub key: &'a str,
41 pub value: Option<&'a [u8]>,
42}
43
44impl RecordBatch<'_> {
45 #[must_use]
46 pub fn header(&self) -> &RecordBatchHeader {
47 self.header
48 }
49
50 #[must_use]
51 pub fn attributes(&self) -> Attributes {
52 Attributes(self.header.attributes.get())
53 }
54}
55
56impl<'a> Default for RecordBatch<'a> {
57 fn default() -> Self {
61 use zerocopy::FromZeros as _;
62 let header: &'a RecordBatchHeader = Box::leak(Box::new(RecordBatchHeader::new_zeroed()));
64 Self {
65 header,
66 body: RecordBody::Owned(bytes::Bytes::new()),
67 }
68 }
69}
70
71impl<'de> crate::DecodeBorrow<'de> for RecordBatch<'de> {
74 fn decode_borrow(buf: &mut &'de [u8], _version: i16) -> Result<Self, crate::ProtocolError> {
75 decode_borrow_impl(buf).map_err(Into::into)
76 }
77}
78
79fn decode_borrow_impl<'de>(buf: &mut &'de [u8]) -> Result<RecordBatch<'de>, RecordsError> {
80 if buf.len() < HEADER_LEN {
81 return Err(RecordsError::HeaderTooShort {
82 needed: HEADER_LEN - buf.len(),
83 });
84 }
85 let (hdr_slice, rest) = buf.split_at(HEADER_LEN);
87 let hdr: &'de RecordBatchHeader =
88 RecordBatchHeader::ref_from_bytes(hdr_slice).map_err(|_| RecordsError::ZerocopyFailure)?;
89 if hdr.magic != 2 {
90 return Err(RecordsError::UnsupportedMagic { found: hdr.magic });
91 }
92
93 let body_len = i32::checked_sub(hdr.batch_length.get(), HEADER_TAIL_LEN)
94 .and_then(|n| usize::try_from(n).ok())
95 .ok_or_else(|| RecordsError::RecordParse("negative or oversized batch_length".into()))?;
96
97 if rest.len() < body_len {
98 return Err(RecordsError::BodyTooShort {
99 needed: body_len - rest.len(),
100 });
101 }
102 let (raw_body, after) = rest.split_at(body_len);
103 *buf = after;
104
105 let expected = hdr.crc.get();
108 let mut computed = crc32c(&hdr_slice[21..HEADER_LEN]);
109 computed = crc32c_append(computed, raw_body);
110 if computed != expected {
111 return Err(RecordsError::CrcMismatch { expected, computed });
112 }
113
114 let attributes = Attributes(hdr.attributes.get());
115 let codec = attributes.compression();
116 let body = if codec == crabka_compression::CompressionType::None {
117 RecordBody::Borrowed(raw_body)
118 } else {
119 const DECOMPRESS_MIN_CAP: usize = 16 * 1024 * 1024; const DECOMPRESS_MAX_RATIO: usize = 100; const DECOMPRESS_ABSOLUTE_CEILING: usize = 1024 * 1024 * 1024; let max_output = raw_body
126 .len()
127 .saturating_mul(DECOMPRESS_MAX_RATIO)
128 .clamp(DECOMPRESS_MIN_CAP, DECOMPRESS_ABSOLUTE_CEILING);
129 let decompressed = crabka_compression::decompress(codec, raw_body, max_output)?;
130 RecordBody::Owned(decompressed)
131 };
132
133 Ok(RecordBatch { header: hdr, body })
134}
135
136#[derive(Debug)]
140pub struct ValidatedBatch<'a> {
141 pub header: &'a RecordBatchHeader,
143 pub total_len: usize,
146}
147
148pub fn validate_one_v2_batch(buf: &[u8]) -> Result<ValidatedBatch<'_>, RecordsError> {
169 if buf.len() < HEADER_LEN {
170 return Err(RecordsError::HeaderTooShort {
171 needed: HEADER_LEN - buf.len(),
172 });
173 }
174 let (hdr_slice, rest) = buf.split_at(HEADER_LEN);
175 let hdr: &RecordBatchHeader =
176 RecordBatchHeader::ref_from_bytes(hdr_slice).map_err(|_| RecordsError::ZerocopyFailure)?;
177 if hdr.magic != 2 {
178 return Err(RecordsError::UnsupportedMagic { found: hdr.magic });
179 }
180 let body_len = i32::checked_sub(hdr.batch_length.get(), HEADER_TAIL_LEN)
181 .and_then(|n| usize::try_from(n).ok())
182 .ok_or_else(|| RecordsError::RecordParse("negative or oversized batch_length".into()))?;
183 if rest.len() < body_len {
184 return Err(RecordsError::BodyTooShort {
185 needed: body_len - rest.len(),
186 });
187 }
188 let raw_body = &rest[..body_len];
189
190 let expected = hdr.crc.get();
191 let mut computed = crc32c(&hdr_slice[21..HEADER_LEN]);
192 computed = crc32c_append(computed, raw_body);
193 if computed != expected {
194 return Err(RecordsError::CrcMismatch { expected, computed });
195 }
196
197 Ok(ValidatedBatch {
198 header: hdr,
199 total_len: HEADER_LEN + body_len,
200 })
201}
202
203impl RecordBatch<'_> {
206 pub fn iter(&self) -> RecordIter<'_> {
213 let body: &[u8] = match &self.body {
214 RecordBody::Borrowed(s) => s,
215 RecordBody::Owned(b) => b.as_ref(),
216 };
217 #[allow(clippy::cast_sign_loss)] let count = self.header.records_count.get().max(0) as usize;
219 RecordIter {
220 remaining: body,
221 count,
222 index: 0,
223 }
224 }
225}
226
227impl<'a> IntoIterator for &'a RecordBatch<'_> {
228 type Item = Result<Record<'a>, RecordsError>;
229 type IntoIter = RecordIter<'a>;
230
231 fn into_iter(self) -> Self::IntoIter {
232 self.iter()
233 }
234}
235
236pub struct RecordIter<'a> {
237 remaining: &'a [u8],
238 count: usize,
239 index: usize,
240}
241
242impl<'a> Iterator for RecordIter<'a> {
243 type Item = Result<Record<'a>, RecordsError>;
244
245 fn next(&mut self) -> Option<Self::Item> {
246 if self.index >= self.count {
247 return None;
248 }
249 self.index += 1;
250 Some(parse_one_record(&mut self.remaining))
251 }
252}
253
254fn parse_one_record<'a>(buf: &mut &'a [u8]) -> Result<Record<'a>, RecordsError> {
255 let body_len =
256 get_varlong(buf).map_err(|e| RecordsError::RecordParse(format!("record length: {e}")))?;
257 let body_len = usize::try_from(body_len).map_err(|_| {
258 RecordsError::RecordParse(format!("record length negative or too large: {body_len}"))
259 })?;
260 if buf.len() < body_len {
261 return Err(RecordsError::BodyTooShort {
262 needed: body_len - buf.len(),
263 });
264 }
265 let (body, rest) = buf.split_at(body_len);
266 *buf = rest;
267 let mut body_cur = body;
268 let r = parse_body(&mut body_cur)?;
269 if !body_cur.is_empty() {
270 return Err(RecordsError::RecordParse(format!(
271 "trailing bytes inside record (left={})",
272 body_cur.len()
273 )));
274 }
275 Ok(r)
276}
277
278fn parse_body<'a>(buf: &mut &'a [u8]) -> Result<Record<'a>, RecordsError> {
279 if buf.is_empty() {
280 return Err(RecordsError::RecordParse("record body empty".into()));
281 }
282 #[allow(clippy::cast_possible_wrap)] let attributes = buf[0] as i8;
284 *buf = &buf[1..];
285 let timestamp_delta =
286 get_varlong(buf).map_err(|e| RecordsError::RecordParse(format!("timestamp_delta: {e}")))?;
287 let offset_delta =
288 get_varint(buf).map_err(|e| RecordsError::RecordParse(format!("offset_delta: {e}")))?;
289
290 let key = read_nullable_slice(buf, "key")?;
291 let value = read_nullable_slice(buf, "value")?;
292
293 let header_count =
294 get_varint(buf).map_err(|e| RecordsError::RecordParse(format!("header_count: {e}")))?;
295 if header_count < 0 {
296 return Err(RecordsError::RecordParse(format!(
297 "negative header count {header_count}"
298 )));
299 }
300 #[allow(clippy::cast_sign_loss)] let mut headers = Vec::with_capacity((header_count as usize).min(buf.len()));
305 for i in 0..header_count {
306 let key_len = get_varint(buf)
307 .map_err(|e| RecordsError::RecordParse(format!("header[{i}] key length: {e}")))?;
308 if key_len < 0 {
309 return Err(RecordsError::RecordParse(format!(
310 "header[{i}] negative key length"
311 )));
312 }
313 #[allow(clippy::cast_sign_loss)] let n = key_len as usize;
315 if buf.len() < n {
316 return Err(RecordsError::BodyTooShort {
317 needed: n - buf.len(),
318 });
319 }
320 let (key_bytes, rest) = buf.split_at(n);
321 *buf = rest;
322 let key_str = std::str::from_utf8(key_bytes)
323 .map_err(|e| RecordsError::RecordParse(format!("header[{i}] key utf-8: {e}")))?;
324
325 let value = read_nullable_slice(buf, &format!("header[{i}] value"))?;
326 headers.push(RecordHeader {
327 key: key_str,
328 value,
329 });
330 }
331
332 Ok(Record {
333 attributes,
334 timestamp_delta,
335 offset_delta,
336 key,
337 value,
338 headers,
339 })
340}
341
342fn read_nullable_slice<'a>(
343 buf: &mut &'a [u8],
344 label: &str,
345) -> Result<Option<&'a [u8]>, RecordsError> {
346 let len =
347 get_varint(buf).map_err(|e| RecordsError::RecordParse(format!("{label} length: {e}")))?;
348 if len < 0 {
349 Ok(None)
350 } else {
351 #[allow(clippy::cast_sign_loss)] let n = len as usize;
353 if buf.len() < n {
354 return Err(RecordsError::BodyTooShort {
355 needed: n - buf.len(),
356 });
357 }
358 let (head, rest) = buf.split_at(n);
359 *buf = rest;
360 Ok(Some(head))
361 }
362}
363
364impl RecordBatch<'_> {
367 pub fn to_owned(&self) -> Result<super::owned::RecordBatch, RecordsError> {
370 let mut records = Vec::new();
371 for r in self {
372 let r = r?;
373 records.push(super::owned::Record {
374 attributes: r.attributes,
375 timestamp_delta: r.timestamp_delta,
376 offset_delta: r.offset_delta,
377 key: r.key.map(Bytes::copy_from_slice),
378 value: r.value.map(Bytes::copy_from_slice),
379 headers: r
380 .headers
381 .into_iter()
382 .map(|h| super::owned::RecordHeader {
383 key: h.key.to_string(),
384 value: h.value.map(Bytes::copy_from_slice),
385 })
386 .collect(),
387 });
388 }
389 Ok(super::owned::RecordBatch {
390 base_offset: self.header.base_offset.get(),
391 partition_leader_epoch: self.header.partition_leader_epoch.get(),
392 attributes: self.attributes(),
393 last_offset_delta: self.header.last_offset_delta.get(),
394 base_timestamp: self.header.base_timestamp.get(),
395 max_timestamp: self.header.max_timestamp.get(),
396 producer_id: self.header.producer_id.get(),
397 producer_epoch: self.header.producer_epoch.get(),
398 base_sequence: self.header.base_sequence.get(),
399 records,
400 })
401 }
402}
403
404impl std::fmt::Debug for RecordBatch<'_> {
410 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
411 match self.to_owned() {
412 Ok(o) => o.fmt(f),
413 Err(e) => write!(f, "RecordBatch(<decode error: {e}>)"),
414 }
415 }
416}
417
418impl Clone for RecordBatch<'_> {
419 fn clone(&self) -> Self {
423 RecordBatch {
424 header: self.header,
425 body: match &self.body {
426 RecordBody::Borrowed(s) => RecordBody::Borrowed(s),
427 RecordBody::Owned(b) => RecordBody::Owned(b.clone()),
428 },
429 }
430 }
431}
432
433impl PartialEq for RecordBatch<'_> {
434 fn eq(&self, other: &Self) -> bool {
435 match (self.to_owned(), other.to_owned()) {
436 (Ok(a), Ok(b)) => a == b,
437 _ => false,
438 }
439 }
440}
441
442impl Eq for RecordBatch<'_> {}
443
444impl crate::Encode for RecordBatch<'_> {
447 fn encode<B: bytes::BufMut>(
448 &self,
449 buf: &mut B,
450 version: i16,
451 ) -> Result<(), crate::ProtocolError> {
452 let owned = self.to_owned().map_err(crate::ProtocolError::from)?;
453 crate::Encode::encode(&owned, buf, version)
454 }
455
456 fn encoded_len(&self, version: i16) -> usize {
457 match self.to_owned() {
458 Ok(o) => crate::Encode::encoded_len(&o, version),
459 Err(_) => 0,
460 }
461 }
462}
463
464#[cfg(test)]
467mod tests {
468 use super::*;
469 use crate::DecodeBorrow;
470 use assert2::assert;
471 use bytes::BytesMut;
472 use crabka_compression::CompressionType;
473
474 fn encode_owned_then_borrow(b: &super::super::owned::RecordBatch) -> Vec<u8> {
475 let mut buf = BytesMut::new();
476 b.encode(&mut buf).unwrap();
477 buf.to_vec()
478 }
479
480 macro_rules! borrowed_roundtrip {
481 ($name:ident, $codec:expr) => {
482 #[test]
483 fn $name() {
484 let mut owned = super::super::owned::RecordBatch::default();
485 owned.attributes = owned.attributes.with_compression($codec);
486 owned.records.push(super::super::owned::Record {
487 key: Some(Bytes::from_static(b"key")),
488 value: Some(Bytes::from_static(b"value")),
489 ..Default::default()
490 });
491
492 let encoded = encode_owned_then_borrow(&owned);
493 let mut cur: &[u8] = &encoded[..];
494 let borrowed = RecordBatch::decode_borrow(&mut cur, 0).unwrap();
495 assert!(cur.is_empty());
496 assert!(borrowed.attributes() == owned.attributes);
497
498 let records: Vec<_> = borrowed.iter().collect::<Result<_, _>>().unwrap();
499 assert!(records.len() == 1);
500 assert!(records[0].key == Some(b"key".as_slice()));
501 assert!(records[0].value == Some(b"value".as_slice()));
502
503 let back_owned = borrowed.to_owned().unwrap();
504 assert!(back_owned == owned);
505 }
506 };
507 }
508
509 borrowed_roundtrip!(roundtrip_none, CompressionType::None);
510 borrowed_roundtrip!(roundtrip_gzip, CompressionType::Gzip);
511 borrowed_roundtrip!(roundtrip_snappy, CompressionType::Snappy);
512 borrowed_roundtrip!(roundtrip_lz4, CompressionType::Lz4);
513 borrowed_roundtrip!(roundtrip_zstd, CompressionType::Zstd);
514
515 #[test]
516 fn zero_copy_for_uncompressed() {
517 let mut owned = super::super::owned::RecordBatch::default();
520 owned.records.push(super::super::owned::Record {
521 key: Some(Bytes::from_static(b"k")),
522 value: Some(Bytes::from_static(b"v")),
523 ..Default::default()
524 });
525 let encoded = encode_owned_then_borrow(&owned);
526 let encoded_start = encoded.as_ptr() as usize;
527 let encoded_end = encoded_start + encoded.len();
528
529 let mut cur: &[u8] = &encoded[..];
530 let borrowed = RecordBatch::decode_borrow(&mut cur, 0).unwrap();
531 let records: Vec<_> = borrowed.iter().collect::<Result<_, _>>().unwrap();
532
533 let v_ptr = records[0].value.unwrap().as_ptr() as usize;
534 assert!(
535 v_ptr >= encoded_start && v_ptr < encoded_end,
536 "value slice does not point into the input buffer: \
537 input range [{encoded_start:#x}, {encoded_end:#x}), value ptr {v_ptr:#x}",
538 );
539 }
540
541 #[test]
542 fn validate_one_v2_batch_reads_header_and_len() {
543 let mut owned = super::super::owned::RecordBatch {
544 base_offset: 7,
545 partition_leader_epoch: 3,
546 last_offset_delta: 0,
547 producer_id: 99,
548 producer_epoch: 1,
549 base_sequence: 5,
550 max_timestamp: 1_234,
551 ..Default::default()
552 };
553 owned.records.push(super::super::owned::Record {
554 value: Some(Bytes::from_static(b"payload")),
555 ..Default::default()
556 });
557 let encoded = encode_owned_then_borrow(&owned);
558
559 let v = validate_one_v2_batch(&encoded).unwrap();
560 assert!(v.total_len == encoded.len());
561 assert!(v.header.base_offset.get() == 7);
562 assert!(v.header.partition_leader_epoch.get() == 3);
563 assert!(v.header.producer_id.get() == 99);
564 assert!(v.header.producer_epoch.get() == 1);
565 assert!(v.header.base_sequence.get() == 5);
566 assert!(v.header.max_timestamp.get() == 1_234);
567 assert!(v.header.magic == 2);
568 }
569
570 #[test]
571 fn validate_one_v2_batch_rejects_corrupt_crc() {
572 let owned = super::super::owned::RecordBatch {
573 records: vec![super::super::owned::Record {
574 value: Some(Bytes::from_static(b"x")),
575 ..Default::default()
576 }],
577 ..Default::default()
578 };
579 let mut encoded = encode_owned_then_borrow(&owned);
580 encoded[HEADER_LEN] ^= 0xFF;
582 let err = validate_one_v2_batch(&encoded).unwrap_err();
583 assert!(matches!(err, RecordsError::CrcMismatch { .. }));
584 }
585
586 #[test]
587 fn validate_one_v2_batch_rejects_truncated() {
588 let owned = super::super::owned::RecordBatch {
589 records: vec![super::super::owned::Record {
590 value: Some(Bytes::from_static(b"value")),
591 ..Default::default()
592 }],
593 ..Default::default()
594 };
595 let encoded = encode_owned_then_borrow(&owned);
596 let err = validate_one_v2_batch(&encoded[..encoded.len() - 2]).unwrap_err();
597 assert!(matches!(err, RecordsError::BodyTooShort { .. }));
598 }
599
600 #[test]
601 fn borrowed_encode_via_trait_roundtrips() {
602 use crate::Encode as _;
603 let owned_in = super::super::owned::RecordBatch {
604 records: vec![super::super::owned::Record {
605 key: Some(Bytes::from_static(b"x")),
606 value: Some(Bytes::from_static(b"y")),
607 ..Default::default()
608 }],
609 ..Default::default()
610 };
611 let bytes_in = encode_owned_then_borrow(&owned_in);
612 let mut cur: &[u8] = &bytes_in[..];
613 let borrowed = RecordBatch::decode_borrow(&mut cur, 0).unwrap();
614
615 let mut out = BytesMut::new();
616 borrowed.encode(&mut out, 0).unwrap();
617 assert!(&out[..] == &bytes_in[..]);
618 }
619}