1use bytes::{Buf, BufMut, Bytes, BytesMut};
4use krabka_compression::RecordDecompressionPolicy;
5use krabka_units::{ByteSize, convert::ByteSizeExt as _};
6use zerocopy::FromBytes as _;
7
8use crate::{
9 primitives::varint::{
10 get_varint, get_varlong, put_varint, put_varlong, varint_len, varlong_len,
11 },
12 records::{
13 RecordsError,
14 crc::{crc32c, crc32c_append},
15 header::{Attributes, HEADER_LEN},
16 },
17};
18
19#[derive(Debug, Clone, PartialEq, Eq, Default)]
20pub struct RecordHeader {
21 pub key: String,
22 pub value: Option<Bytes>,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq, Default)]
26pub struct Record {
27 pub attributes: i8,
28 pub timestamp_delta: i64,
29 pub offset_delta: i32,
30 pub key: Option<Bytes>,
31 pub value: Option<Bytes>,
32 pub headers: Vec<RecordHeader>,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct RecordBatch {
37 pub base_offset: i64,
38 pub partition_leader_epoch: i32,
39 pub attributes: Attributes,
40 pub last_offset_delta: i32,
41 pub base_timestamp: i64,
42 pub max_timestamp: i64,
43 pub producer_id: i64,
44 pub producer_epoch: i16,
45 pub base_sequence: i32,
46 pub records: Vec<Record>,
47}
48
49impl Default for RecordBatch {
50 fn default() -> Self {
51 Self {
52 base_offset: 0,
53 partition_leader_epoch: 0,
54 attributes: Attributes::default(),
55 last_offset_delta: 0,
56 base_timestamp: 0,
57 max_timestamp: 0,
58 producer_id: -1, producer_epoch: -1,
60 base_sequence: -1,
61 records: Vec::new(),
62 }
63 }
64}
65
66impl Record {
67 pub fn encode<B: BufMut>(&self, buf: &mut B) -> Result<(), RecordsError> {
73 let body_len = self.body_len();
74 put_varlong(
75 buf,
76 i64::try_from(body_len)
77 .map_err(|_| RecordsError::RecordParse("record body length overflow".into()))?,
78 );
79 self.encode_body(buf)
80 }
81
82 pub fn encoded_len(&self) -> usize {
88 let body = self.body_len();
89 let body_i64 = i64::try_from(body).expect("record body length must fit in i64");
90 varlong_len(body_i64) + body
91 }
92
93 #[must_use]
101 pub fn body_len(&self) -> usize {
102 body_len_of(
103 self.timestamp_delta,
104 self.offset_delta,
105 self.key.as_ref().map(Bytes::len),
106 self.value.as_ref().map(Bytes::len),
107 self.headers
108 .iter()
109 .map(|h| (h.key.len(), h.value.as_ref().map(Bytes::len))),
110 )
111 }
112
113 fn encode_body<B: BufMut>(&self, buf: &mut B) -> Result<(), RecordsError> {
114 const SMALL_KEY_LEN: usize = 32;
116 const PREFIX_LEN: usize = 21 + SMALL_KEY_LEN;
117 let mut prefix = [0_u8; PREFIX_LEN];
118 let mut fields = prefix.as_mut_slice();
119 fields.put_i8(self.attributes);
120 put_varlong(&mut fields, self.timestamp_delta);
121 put_varint(&mut fields, self.offset_delta);
122 match &self.key {
123 None => put_varint(&mut fields, -1),
124 Some(k) => {
125 let Ok(len) = i32::try_from(k.len()) else {
126 let used = PREFIX_LEN - fields.len();
128 buf.put_slice(&prefix[..used]);
129 return Err(RecordsError::RecordParse(
130 "record key length overflow".into(),
131 ));
132 };
133 put_varint(&mut fields, len);
134 }
135 }
136 if let Some(key) = &self.key
137 && key.len() <= SMALL_KEY_LEN
138 {
139 fields.put_slice(key);
140 }
141 let used = PREFIX_LEN - fields.len();
142 buf.put_slice(&prefix[..used]);
143 if let Some(key) = &self.key
144 && key.len() > SMALL_KEY_LEN
145 {
146 buf.put_slice(key);
147 }
148 match &self.value {
149 None => put_varint(buf, -1),
150 Some(v) => {
151 put_varint(
152 buf,
153 i32::try_from(v.len()).map_err(|_| {
154 RecordsError::RecordParse("record value length overflow".into())
155 })?,
156 );
157 buf.put_slice(v);
158 }
159 }
160 put_varint(
161 buf,
162 i32::try_from(self.headers.len())
163 .map_err(|_| RecordsError::RecordParse("record header count overflow".into()))?,
164 );
165 for h in &self.headers {
166 let key_bytes = h.key.as_bytes();
167 put_varint(
168 buf,
169 i32::try_from(key_bytes.len())
170 .map_err(|_| RecordsError::RecordParse("header key length overflow".into()))?,
171 );
172 buf.put_slice(key_bytes);
173 match &h.value {
174 None => put_varint(buf, -1),
175 Some(v) => {
176 put_varint(
177 buf,
178 i32::try_from(v.len()).map_err(|_| {
179 RecordsError::RecordParse("header value length overflow".into())
180 })?,
181 );
182 buf.put_slice(v);
183 }
184 }
185 }
186 Ok(())
187 }
188
189 pub fn decode<B: Buf>(buf: &mut B) -> Result<Self, RecordsError> {
197 let body_len = get_varlong(buf)
198 .map_err(|e| RecordsError::RecordParse(format!("record length: {e}")))?;
199 let body_len = usize::try_from(body_len).map_err(|_| {
200 RecordsError::RecordParse(format!("record length negative or too large: {body_len}"))
201 })?;
202 if buf.remaining() < body_len {
203 return Err(RecordsError::BodyTooShort {
204 needed: body_len - buf.remaining(),
205 });
206 }
207 let mut body = buf.take(body_len);
210 let r = Self::decode_body(&mut body)?;
211 if body.has_remaining() {
213 return Err(RecordsError::RecordParse(format!(
214 "trailing bytes inside record (left={})",
215 body.remaining()
216 )));
217 }
218 Ok(r)
219 }
220
221 fn decode_body<B: Buf>(buf: &mut B) -> Result<Self, RecordsError> {
222 if buf.remaining() == 0 {
223 return Err(RecordsError::RecordParse("record body empty".into()));
224 }
225 let attributes = buf.get_i8();
226 let timestamp_delta = get_varlong(buf)
227 .map_err(|e| RecordsError::RecordParse(format!("timestamp_delta: {e}")))?;
228 let offset_delta =
229 get_varint(buf).map_err(|e| RecordsError::RecordParse(format!("offset_delta: {e}")))?;
230
231 let key = decode_nullable_bytes(buf, "key")?;
232 let value = decode_nullable_bytes(buf, "value")?;
233
234 let header_count =
235 get_varint(buf).map_err(|e| RecordsError::RecordParse(format!("header_count: {e}")))?;
236 if header_count < 0 {
237 return Err(RecordsError::RecordParse(format!(
238 "negative header count {header_count}"
239 )));
240 }
241 let header_count_usize =
242 usize::try_from(header_count).expect("non-negative header count must fit in usize");
243 let mut headers = Vec::with_capacity(header_count_usize.min(buf.remaining()));
248 for i in 0..header_count {
249 headers.push(
250 decode_record_header(buf)
251 .map_err(|e| RecordsError::RecordParse(format!("header[{i}]: {e}")))?,
252 );
253 }
254
255 Ok(Self {
256 attributes,
257 timestamp_delta,
258 offset_delta,
259 key,
260 value,
261 headers,
262 })
263 }
264}
265
266pub(crate) fn body_len_of(
270 timestamp_delta: i64,
271 offset_delta: i32,
272 key: Option<usize>,
273 value: Option<usize>,
274 headers: impl ExactSizeIterator<Item = (usize, Option<usize>)>,
275) -> usize {
276 let field = |len: Option<usize>| match len {
277 None => varint_len(-1),
278 Some(len) => varint_len(i32::try_from(len).unwrap_or(i32::MAX)) + len,
279 };
280 let header_count = varint_len(i32::try_from(headers.len()).unwrap_or(i32::MAX));
281 let headers: usize = headers
282 .map(|(key, value)| field(Some(key)) + field(value))
283 .sum();
284 1 + varlong_len(timestamp_delta)
286 + varint_len(offset_delta)
287 + field(key)
288 + field(value)
289 + header_count
290 + headers
291}
292
293fn decode_nullable_bytes<B: Buf>(buf: &mut B, label: &str) -> Result<Option<Bytes>, RecordsError> {
294 let len =
295 get_varint(buf).map_err(|e| RecordsError::RecordParse(format!("{label} length: {e}")))?;
296 if len < 0 {
297 Ok(None)
298 } else {
299 let n = usize::try_from(len).expect("non-negative value length must fit in usize");
300 if buf.remaining() < n {
301 return Err(RecordsError::BodyTooShort {
302 needed: n - buf.remaining(),
303 });
304 }
305 let mut v = vec![0u8; n];
306 buf.copy_to_slice(&mut v);
307 Ok(Some(Bytes::from(v)))
308 }
309}
310
311fn decode_record_header<B: Buf>(buf: &mut B) -> Result<RecordHeader, String> {
312 let key_len = get_varint(buf).map_err(|e| format!("key length: {e}"))?;
313 if key_len < 0 {
314 return Err(format!("non-nullable key has negative length {key_len}"));
315 }
316 let n = usize::try_from(key_len).expect("non-negative key length must fit in usize");
317 if buf.remaining() < n {
318 return Err(format!("key truncated (need {} more)", n - buf.remaining()));
319 }
320 let mut kv = vec![0u8; n];
321 buf.copy_to_slice(&mut kv);
322 let key = String::from_utf8(kv).map_err(|e| format!("key utf-8: {e}"))?;
323
324 let value_len = get_varint(buf).map_err(|e| format!("value length: {e}"))?;
325 let value = if value_len < 0 {
326 None
327 } else {
328 let n = usize::try_from(value_len).expect("non-negative value length must fit in usize");
329 if buf.remaining() < n {
330 return Err(format!(
331 "value truncated (need {} more)",
332 n - buf.remaining()
333 ));
334 }
335 let mut vv = vec![0u8; n];
336 buf.copy_to_slice(&mut vv);
337 Some(Bytes::from(vv))
338 };
339
340 Ok(RecordHeader { key, value })
341}
342
343#[cfg(test)]
344mod record_tests {
345
346 use bytes::BytesMut;
347
348 use super::*;
349
350 fn fixture_minimal_record() -> Record {
351 Record {
352 attributes: 0,
353 timestamp_delta: 0,
354 offset_delta: 0,
355 key: None,
356 value: None,
357 headers: vec![],
358 }
359 }
360
361 fn fixture_keyed_record() -> Record {
362 Record {
363 attributes: 0,
364 timestamp_delta: 17,
365 offset_delta: 2,
366 key: Some(Bytes::from_static(b"the-key")),
367 value: Some(Bytes::from_static(b"hello kafka")),
368 headers: vec![
369 RecordHeader {
370 key: "trace-id".to_string(),
371 value: Some(Bytes::from_static(b"abc")),
372 },
373 RecordHeader {
374 key: "null-val".to_string(),
375 value: None,
376 },
377 ],
378 }
379 }
380
381 fn fixture_large_payload_record() -> Record {
382 Record {
383 attributes: 0,
384 timestamp_delta: 1_000_000,
385 offset_delta: 999,
386 key: Some(Bytes::from(vec![b'k'; 128])),
387 value: Some(Bytes::from(vec![b'v'; 4096])),
388 headers: vec![],
389 }
390 }
391
392 #[test]
393 fn record_prefix_golden_bytes_across_destination_splits() {
394 let mut cases = vec![
395 (Record::default(), vec![0x0c, 0, 0, 0, 1, 1, 0]),
396 (
397 Record {
398 attributes: i8::MIN,
399 timestamp_delta: i64::MIN,
400 offset_delta: i32::MIN,
401 key: Some(Bytes::from_static(b"k")),
402 value: Some(Bytes::from_static(b"v")),
403 headers: vec![],
404 },
405 vec![
406 0x2a, 0x80, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01, 0xff,
407 0xff, 0xff, 0xff, 0x0f, 0x02, b'k', 0x02, b'v', 0,
408 ],
409 ),
410 (
411 Record {
412 offset_delta: 129,
413 key: Some(Bytes::from_static(b"k")),
414 value: Some(Bytes::from_static(b"v")),
415 ..Default::default()
416 },
417 vec![0x12, 0, 0, 0x82, 0x02, 0x02, b'k', 0x02, b'v', 0],
418 ),
419 ];
420 for (len, length_prefix, key_prefix) in [
423 (0, vec![12], vec![0]),
424 (32, vec![76], vec![64]),
425 (33, vec![78], vec![66]),
426 (63, vec![138, 1], vec![126]),
427 (64, vec![142, 1], vec![128, 1]),
428 ] {
429 let mut expected = length_prefix;
430 expected.extend_from_slice(&[0, 0, 0]);
431 expected.extend(key_prefix);
432 expected.extend(std::iter::repeat_n(b'k', len));
433 expected.extend_from_slice(&[1, 0]);
434 cases.push((
435 Record {
436 key: Some(Bytes::from(vec![b'k'; len])),
437 ..Default::default()
438 },
439 expected,
440 ));
441 }
442 for (record, expected) in cases {
443 for split in 0..=expected.len() {
444 let mut encoded = vec![0xcd; expected.len()];
445 let (first, second) = encoded.split_at_mut(split);
446 let mut destination = first.chain_mut(second);
447 record.encode(&mut destination).unwrap();
448 assert2::assert!(destination.remaining_mut() == 0);
449 assert2::assert!(encoded == expected);
450 let mut cursor = encoded.as_slice();
451 assert2::assert!(Record::decode(&mut cursor).unwrap() == record);
452 assert2::assert!(cursor.is_empty());
453 }
454 }
455 }
456
457 #[test]
458 fn record_roundtrip_cases() {
459 type TestCase1<'a> = (&'a str, fn() -> Record);
460 let cases: [TestCase1<'_>; 3] = [
461 ("minimal", fixture_minimal_record),
462 ("keyed with headers", fixture_keyed_record),
463 ("large payload", fixture_large_payload_record),
464 ];
465 for (_case, fixture) in cases {
466 let record = fixture();
467 let mut buf = BytesMut::new();
468 record.encode(&mut buf).unwrap();
469 assert2::assert!(buf.len() == record.encoded_len());
470
471 let mut cur: &[u8] = &buf[..];
472 let decoded = Record::decode(&mut cur).unwrap();
473 assert2::assert!((decoded, cur.is_empty()) == (record, true));
474 }
475 }
476
477 #[test]
486 fn body_len_is_the_size_the_length_prefix_declares() {
487 for (value_len, body, prefix) in [
488 (0, 6, 1),
489 (57, 63, 1),
490 (58, 64, 2),
491 (63, 69, 2),
492 (64, 71, 2),
493 (8_184, 8_191, 2),
494 (8_185, 8_192, 3),
495 (8_192, 8_200, 3),
496 (70_000, 70_008, 3),
497 ] {
498 let record = Record {
499 value: Some(Bytes::from(vec![7_u8; value_len])),
500 ..Default::default()
501 };
502 let mut buf = BytesMut::new();
503 record.encode(&mut buf).unwrap();
504
505 let mut cur: &[u8] = &buf[..];
506 let declared = get_varlong(&mut cur).unwrap();
507 let case = format!("value_len={value_len}");
508 assert2::check!(record.body_len() == body, "{case}");
509 assert2::check!(record.encoded_len() == prefix + body, "{case}");
510 assert2::check!(buf.len() == prefix + body, "{case}");
511 assert2::check!(declared == i64::try_from(body).unwrap(), "{case}");
512 assert2::check!(buf.len() - cur.len() == prefix, "{case}");
513 }
514 }
515
516 #[test]
520 fn body_len_counts_every_field() {
521 type Fixture<'a> = (&'a str, fn() -> Record, usize);
522 let cases: [Fixture<'_>; 3] = [
523 ("minimal", fixture_minimal_record, 6),
525 ("keyed with headers", fixture_keyed_record, 47),
529 ("large payload", fixture_large_payload_record, 4_235),
532 ];
533 for (case, fixture, body) in cases {
534 let record = fixture();
535 let mut buf = BytesMut::new();
536 record.encode(&mut buf).unwrap();
537 let declared = get_varlong(&mut &buf[..]).unwrap();
538 assert2::check!(record.body_len() == body, "{case}");
539 assert2::check!(declared == i64::try_from(body).unwrap(), "{case}");
540 }
541 }
542
543 #[test]
544 fn decode_rejects_negative_header_count() {
545 let mut buf = BytesMut::new();
546 put_varlong(&mut buf, 6); buf.put_i8(0); put_varlong(&mut buf, 0); put_varint(&mut buf, 0); put_varint(&mut buf, -1); put_varint(&mut buf, -1); put_varint(&mut buf, -1); let mut cur: &[u8] = &buf[..];
557 match Record::decode(&mut cur) {
558 Err(RecordsError::RecordParse(msg)) => {
559 assert2::assert!(msg.contains("negative header count"));
560 }
561 other => panic!("expected RecordParse, got {other:?}"),
562 }
563 }
564
565 #[test]
566 fn decode_huge_header_count_does_not_overallocate() {
567 let mut inner = BytesMut::new();
572 inner.put_i8(0); put_varlong(&mut inner, 0); put_varint(&mut inner, 0); put_varint(&mut inner, -1); put_varint(&mut inner, -1); put_varint(&mut inner, 1_000_000_000); let mut buf = BytesMut::new();
580 put_varlong(&mut buf, i64::try_from(inner.len()).unwrap());
581 buf.extend_from_slice(&inner);
582
583 let mut cur: &[u8] = &buf[..];
584 assert2::assert!(Record::decode(&mut cur).is_err());
586 }
587}
588
589impl RecordBatch {
590 #[must_use]
594 pub fn delete_horizon_ms(&self) -> Option<i64> {
595 self.attributes
596 .has_delete_horizon()
597 .then_some(self.base_timestamp)
598 }
599
600 #[must_use]
606 pub fn with_delete_horizon(mut self, horizon_ms: i64) -> Self {
607 let old_base = self.base_timestamp;
608 for r in &mut self.records {
609 let abs = old_base.saturating_add(r.timestamp_delta);
614 r.timestamp_delta = abs.saturating_sub(horizon_ms);
615 }
616 self.base_timestamp = horizon_ms;
617 self.attributes = self.attributes.with_delete_horizon(true);
618 self
619 }
620
621 pub fn decode<B: Buf>(buf: &mut B) -> Result<Self, RecordsError> {
633 Self::decode_with_policy(buf, RecordDecompressionPolicy::default())
634 }
635
636 pub fn decode_with_policy<B: Buf>(
648 buf: &mut B,
649 policy: RecordDecompressionPolicy,
650 ) -> Result<Self, RecordsError> {
651 const HEADER_TAIL_LEN: i32 = 49;
657
658 if buf.remaining() < HEADER_LEN {
660 return Err(RecordsError::HeaderTooShort {
661 needed: HEADER_LEN - buf.remaining(),
662 });
663 }
664 let mut hdr_bytes = [0u8; HEADER_LEN];
666 buf.copy_to_slice(&mut hdr_bytes);
667
668 let hdr = crate::records::header::RecordBatchHeader::ref_from_bytes(&hdr_bytes[..])
669 .map_err(|_| RecordsError::ZerocopyFailure)?;
670
671 if hdr.magic != 2 {
672 return Err(RecordsError::UnsupportedMagic { found: hdr.magic });
673 }
674
675 let body_len = i32::checked_sub(hdr.batch_length.get(), HEADER_TAIL_LEN)
677 .and_then(|n| usize::try_from(n).ok())
678 .ok_or_else(|| {
679 RecordsError::RecordParse("negative or oversized batch_length".into())
680 })?;
681
682 if buf.remaining() < body_len {
683 return Err(RecordsError::BodyTooShort {
684 needed: body_len - buf.remaining(),
685 });
686 }
687
688 let expected_crc = hdr.crc.get();
691 let verify_crc = |body: &[u8]| {
692 let computed = crc32c_append(crc32c(&hdr_bytes[21..HEADER_LEN]), body);
693 if computed == expected_crc {
694 Ok(())
695 } else {
696 Err(RecordsError::CrcMismatch {
697 expected: expected_crc,
698 computed,
699 })
700 }
701 };
702
703 let attributes = Attributes(hdr.attributes.get());
704 let codec = attributes.compression();
705
706 let decompress = |body: &[u8]| -> Result<Bytes, RecordsError> {
707 verify_crc(body)?;
708 Ok(krabka_compression::decompress(
709 codec,
710 body,
711 policy.output_limit(ByteSize::from_bytes(body.len() as u64)),
712 )?)
713 };
714
715 let body_for_records = if codec != krabka_compression::CompressionType::None
721 && buf.chunk().len() >= body_len
722 {
723 let decoded = decompress(&buf.chunk()[..body_len]);
724 buf.advance(body_len);
725 decoded?
726 } else {
727 let mut body = vec![0u8; body_len];
728 buf.copy_to_slice(&mut body);
729 if codec == krabka_compression::CompressionType::None {
730 verify_crc(&body)?;
731 Bytes::from(body)
732 } else {
733 decompress(&body)?
734 }
735 };
736
737 let count = hdr.records_count.get();
739 if count < 0 {
740 return Err(RecordsError::RecordParse(format!(
741 "negative records_count {count}"
742 )));
743 }
744 let mut body_cur: &[u8] = &body_for_records[..];
745 let record_capacity = usize::try_from(count)
749 .expect("non-negative record count must fit in usize")
750 .min(body_for_records.len());
751 let mut records = Vec::with_capacity(record_capacity);
752 for i in 0..count {
753 let r = crate::records::borrowed::parse_one_record(&mut body_cur)
760 .map_err(|e| RecordsError::RecordParse(format!("record[{i}]: {e}")))?;
761 records.push(Record {
762 attributes: r.attributes,
763 timestamp_delta: r.timestamp_delta,
764 offset_delta: r.offset_delta,
765 key: r.key.map(|s| body_for_records.slice_ref(s)),
766 value: r.value.map(|s| body_for_records.slice_ref(s)),
767 headers: r
768 .headers
769 .into_iter()
770 .map(|h| RecordHeader {
771 key: h.key.to_string(),
772 value: h.value.map(|s| body_for_records.slice_ref(s)),
773 })
774 .collect(),
775 });
776 }
777 if !body_cur.is_empty() {
778 return Err(RecordsError::RecordParse(format!(
779 "trailing bytes after records (left={})",
780 body_cur.len()
781 )));
782 }
783
784 Ok(Self {
785 base_offset: hdr.base_offset.get(),
786 partition_leader_epoch: hdr.partition_leader_epoch.get(),
787 attributes,
788 last_offset_delta: hdr.last_offset_delta.get(),
789 base_timestamp: hdr.base_timestamp.get(),
790 max_timestamp: hdr.max_timestamp.get(),
791 producer_id: hdr.producer_id.get(),
792 producer_epoch: hdr.producer_epoch.get(),
793 base_sequence: hdr.base_sequence.get(),
794 records,
795 })
796 }
797
798 pub fn encode<B: BufMut>(&self, buf: &mut B) -> Result<(), RecordsError> {
804 self.encode_with_compression_level(buf, None)
805 }
806
807 pub fn encode_to_bytes(&self) -> Result<Bytes, RecordsError> {
816 let mut wire = BytesMut::with_capacity(self.encoded_len());
817 if self.attributes.compression() == krabka_compression::CompressionType::None {
818 wire.resize(HEADER_LEN, 0);
819 for record in &self.records {
820 record.encode(&mut wire)?;
821 }
822 let (mut header, body) = wire.split_at_mut(HEADER_LEN);
823 self.encode_header(&mut header, body)?;
824 } else {
825 self.encode(&mut wire)?;
826 }
827 Ok(wire.freeze())
828 }
829
830 pub fn encode_with_compression_level<B: BufMut>(
844 &self,
845 buf: &mut B,
846 level: Option<i32>,
847 ) -> Result<(), RecordsError> {
848 let mut raw_body =
850 BytesMut::with_capacity(self.records.iter().map(Record::encoded_len).sum());
851 for r in &self.records {
852 r.encode(&mut raw_body)?;
853 }
854 let raw_body = raw_body.freeze();
855
856 let codec = self.attributes.compression();
858 let body: Bytes = match level {
859 Some(level) => krabka_compression::compress_with_level(codec, &raw_body, level)?,
860 None if codec == krabka_compression::CompressionType::None => raw_body,
861 None => krabka_compression::compress(codec, &raw_body)?,
862 };
863
864 self.encode_header(buf, &body)?;
865 buf.put_slice(&body);
866 Ok(())
867 }
868
869 fn encode_header<B: BufMut>(&self, buf: &mut B, body: &[u8]) -> Result<(), RecordsError> {
870 const HEADER_TAIL_LEN: i32 = 49;
871
872 let batch_length = HEADER_TAIL_LEN
874 + i32::try_from(body.len())
875 .map_err(|_| RecordsError::RecordParse("body length exceeds i32".into()))?;
876
877 let mut covered_head = [0_u8; 40];
879 let mut covered = covered_head.as_mut_slice();
880 covered.put_i16(self.attributes.0);
881 covered.put_i32(self.last_offset_delta);
882 covered.put_i64(self.base_timestamp);
883 covered.put_i64(self.max_timestamp);
884 covered.put_i64(self.producer_id);
885 covered.put_i16(self.producer_epoch);
886 covered.put_i32(self.base_sequence);
887 covered.put_i32(
888 i32::try_from(self.records.len())
889 .map_err(|_| RecordsError::RecordParse("records_count exceeds i32".into()))?,
890 );
891 let mut crc = crc32c(&covered_head);
893 crc = crc32c_append(crc, body);
894
895 buf.put_i64(self.base_offset);
897 buf.put_i32(batch_length);
898 buf.put_i32(self.partition_leader_epoch);
899 buf.put_i8(2); buf.put_u32(crc);
901 buf.put_slice(&covered_head);
902 Ok(())
903 }
904
905 pub fn encoded_len(&self) -> usize {
909 let body: usize = self.records.iter().map(Record::encoded_len).sum();
910 HEADER_LEN + body
911 }
912}
913
914#[cfg(test)]
915mod batch_tests {
916 use assert2::check;
917 use krabka_compression::{CompressionError, CompressionType, RecordDecompressionPolicy};
918 use krabka_units::{bytes, fraction};
919
920 use super::*;
921
922 fn fixture_empty_batch() -> RecordBatch {
923 RecordBatch::default()
924 }
925
926 fn fixture_single_record_batch() -> RecordBatch {
927 RecordBatch {
928 records: vec![Record {
929 key: Some(Bytes::from_static(b"k1")),
930 value: Some(Bytes::from_static(b"v1")),
931 ..Default::default()
932 }],
933 ..RecordBatch::default()
934 }
935 }
936
937 fn fixture_multi_record_batch() -> RecordBatch {
938 RecordBatch {
939 base_offset: 42,
940 partition_leader_epoch: 5,
941 last_offset_delta: 2,
942 base_timestamp: 1_700_000_000,
943 max_timestamp: 1_700_000_500,
944 producer_id: 100,
945 producer_epoch: 3,
946 base_sequence: 7,
947 records: vec![
948 Record {
949 offset_delta: 0,
950 timestamp_delta: 0,
951 key: Some(Bytes::from_static(b"a")),
952 value: Some(Bytes::from_static(b"1")),
953 ..Default::default()
954 },
955 Record {
956 offset_delta: 1,
957 timestamp_delta: 100,
958 key: Some(Bytes::from_static(b"b")),
959 value: Some(Bytes::from_static(b"2")),
960 ..Default::default()
961 },
962 Record {
963 offset_delta: 2,
964 timestamp_delta: 500,
965 key: None,
966 value: Some(Bytes::from_static(b"3")),
967 headers: vec![RecordHeader {
968 key: "h".to_string(),
969 value: Some(Bytes::from_static(b"hv")),
970 }],
971 ..Default::default()
972 },
973 ],
974 ..RecordBatch::default()
975 }
976 }
977
978 #[test]
979 fn uncompressed_roundtrip_cases() {
980 type TestCase2 = (&'static str, fn() -> RecordBatch);
981 let cases: [TestCase2; 3] = [
982 ("empty", fixture_empty_batch),
983 ("single", fixture_single_record_batch),
984 ("multiple", fixture_multi_record_batch),
985 ];
986 for (_case, fixture) in cases {
987 let mut batch = fixture();
988 batch.attributes = batch.attributes.with_compression(CompressionType::None);
989 let mut buf = BytesMut::new();
990 batch.encode(&mut buf).unwrap();
991 assert2::assert!(buf.len() == batch.encoded_len());
992 let mut cur: &[u8] = &buf[..];
993 let decoded = RecordBatch::decode(&mut cur).unwrap();
994 assert2::assert!((decoded, cur.is_empty()) == (batch, true));
995 }
996 }
997
998 #[test]
1003 fn encode_with_compression_level_compresses_at_the_level() {
1004 let value: Vec<u8> = (0..16 * 1024u32)
1005 .map(|i| u8::try_from((i * 7 + i / 13) % 251).unwrap_or(0))
1006 .collect();
1007 let mut batch = RecordBatch {
1008 records: vec![Record {
1009 value: Some(Bytes::from(value)),
1010 ..Default::default()
1011 }],
1012 ..RecordBatch::default()
1013 };
1014 batch.attributes = batch.attributes.with_compression(CompressionType::Gzip);
1015 let encoded = |level: Option<i32>| {
1016 let mut buf = BytesMut::new();
1017 batch
1018 .encode_with_compression_level(&mut buf, level)
1019 .map(|()| buf.freeze())
1020 };
1021 let default = encoded(None).unwrap();
1022 let fastest = encoded(Some(1)).unwrap();
1023 let best = encoded(Some(9)).unwrap();
1024 let decoded =
1025 [&fastest, &best].map(|bytes| RecordBatch::decode(&mut &bytes[..]).unwrap() == batch);
1026
1027 assert2::assert!(
1028 (
1029 encoded(Some(-1)).unwrap() == default,
1030 fastest != best,
1031 decoded,
1032 ) == (true, true, [true, true])
1033 );
1034 assert2::assert!(matches!(
1035 encoded(Some(10)),
1036 Err(RecordsError::Compression(CompressionError::InvalidLevel {
1037 level: 10,
1038 ..
1039 }))
1040 ));
1041 }
1042
1043 #[test]
1044 fn rejects_pre_v2_magic() {
1045 let mut buf = BytesMut::new();
1046 buf.put_i64(0); buf.put_i32(49); buf.put_i32(0); buf.put_i8(1); buf.put_u32(0); for _ in 21..HEADER_LEN {
1052 buf.put_u8(0);
1053 }
1054 let mut cur: &[u8] = &buf[..];
1055 assert2::assert!(matches!(
1056 RecordBatch::decode(&mut cur),
1057 Err(RecordsError::UnsupportedMagic { found: 1 })
1058 ));
1059 }
1060
1061 #[test]
1062 fn rejects_bad_crc() {
1063 let b = fixture_single_record_batch();
1064 let mut buf = BytesMut::new();
1065 b.encode(&mut buf).unwrap();
1066 buf[17] ^= 0xFF;
1068 let mut cur: &[u8] = &buf[..];
1069 assert2::assert!(matches!(
1070 RecordBatch::decode(&mut cur),
1071 Err(RecordsError::CrcMismatch { .. })
1072 ));
1073 }
1074
1075 #[test]
1076 fn compressed_roundtrip_cases() {
1077 for (_case, codec) in [
1078 ("gzip", CompressionType::Gzip),
1079 ("snappy", CompressionType::Snappy),
1080 ("lz4", CompressionType::Lz4),
1081 ("zstd", CompressionType::Zstd),
1082 ] {
1083 let mut batch = fixture_multi_record_batch();
1084 batch.attributes = batch.attributes.with_compression(codec);
1085 let mut buf = BytesMut::new();
1086 batch.encode(&mut buf).unwrap();
1087 let mut cur: &[u8] = &buf[..];
1088 let decoded = RecordBatch::decode(&mut cur).unwrap();
1089 assert2::assert!((decoded, cur.is_empty()) == (batch, true));
1090 }
1091 }
1092
1093 #[test]
1094 fn decompression_policy_limits_owned_decode() {
1095 let mut batch = fixture_single_record_batch();
1096 batch.records[0].value = Some(Bytes::from(vec![b'x'; 4096]));
1097 batch.attributes = batch.attributes.with_compression(CompressionType::Lz4);
1098 let mut wire = BytesMut::new();
1099 batch.encode(&mut wire).unwrap();
1100
1101 let mut default_cur = &wire[..];
1102 RecordBatch::decode(&mut default_cur).unwrap();
1103
1104 let policy = RecordDecompressionPolicy::new(fraction(1.0), bytes(1), bytes(32)).unwrap();
1105 let mut limited_cur = &wire[..];
1106 assert2::assert!(matches!(
1107 RecordBatch::decode_with_policy(&mut limited_cur, policy),
1108 Err(RecordsError::Compression(CompressionError::TooLarge {
1109 limit: 32
1110 }))
1111 ));
1112 }
1113
1114 #[test]
1115 fn with_delete_horizon_stamps_and_preserves_record_timestamps() {
1116 let b = RecordBatch {
1118 base_timestamp: 1000,
1119 records: vec![
1120 Record {
1121 timestamp_delta: 0,
1122 ..Default::default()
1123 },
1124 Record {
1125 timestamp_delta: 5,
1126 ..Default::default()
1127 },
1128 ],
1129 ..RecordBatch::default()
1130 };
1131
1132 let stamped = b.with_delete_horizon(9999);
1133
1134 check!(stamped.attributes.has_delete_horizon());
1135 check!(stamped.base_timestamp == 9999);
1136 check!(stamped.delete_horizon_ms() == Some(9999));
1137
1138 let absolutes: Vec<i64> = stamped
1140 .records
1141 .iter()
1142 .map(|r| stamped.base_timestamp + r.timestamp_delta)
1143 .collect();
1144 assert2::assert!(absolutes == vec![1000, 1005]);
1145 }
1146
1147 #[test]
1148 fn delete_horizon_round_trips_through_encode_decode() {
1149 let b = RecordBatch {
1151 base_timestamp: 1000,
1152 last_offset_delta: 1,
1153 records: vec![
1154 Record {
1155 timestamp_delta: 0,
1156 offset_delta: 0,
1157 key: Some(Bytes::from_static(b"k1")),
1158 value: Some(Bytes::from_static(b"v1")),
1159 ..Default::default()
1160 },
1161 Record {
1162 timestamp_delta: 5,
1163 offset_delta: 1,
1164 key: Some(Bytes::from_static(b"k2")),
1165 value: Some(Bytes::from_static(b"v2")),
1166 ..Default::default()
1167 },
1168 ],
1169 ..RecordBatch::default()
1170 }
1171 .with_delete_horizon(9999);
1172
1173 let mut buf = BytesMut::new();
1174 b.encode(&mut buf).unwrap();
1175
1176 let mut cur: &[u8] = &buf[..];
1177 let decoded = RecordBatch::decode(&mut cur).unwrap();
1178 assert2::assert!(cur.is_empty());
1179
1180 assert2::assert!(decoded.delete_horizon_ms() == Some(9999));
1181
1182 let absolutes: Vec<i64> = decoded
1183 .records
1184 .iter()
1185 .map(|r| decoded.base_timestamp + r.timestamp_delta)
1186 .collect();
1187 assert2::assert!(absolutes == vec![1000, 1005]);
1188 }
1189
1190 #[test]
1191 fn decode_huge_records_count_does_not_overallocate() {
1192 let mut b = fixture_empty_batch();
1197 b.attributes = b.attributes.with_compression(CompressionType::None);
1198 let mut buf = BytesMut::new();
1199 b.encode(&mut buf).unwrap();
1200
1201 let rc_off = HEADER_LEN - 4;
1203 buf[rc_off..HEADER_LEN].copy_from_slice(&1_000_000_000i32.to_be_bytes());
1204
1205 let body = &buf[HEADER_LEN..];
1207 let mut computed = crc32c(&buf[21..HEADER_LEN]);
1208 computed = crc32c_append(computed, body);
1209 buf[17..21].copy_from_slice(&computed.to_be_bytes());
1210
1211 let mut cur: &[u8] = &buf[..];
1212 assert2::assert!(RecordBatch::decode(&mut cur).is_err());
1214 }
1215}
1216
1217impl crate::Encode for RecordBatch {
1218 fn encode<B: BufMut>(&self, buf: &mut B, _version: i16) -> Result<(), crate::ProtocolError> {
1219 RecordBatch::encode(self, buf).map_err(Into::into)
1220 }
1221
1222 fn encoded_len(&self, _version: i16) -> usize {
1223 RecordBatch::encoded_len(self)
1224 }
1225}
1226
1227impl crate::Decode<'_> for RecordBatch {
1228 fn decode<B: Buf>(buf: &mut B, _version: i16) -> Result<Self, crate::ProtocolError> {
1229 RecordBatch::decode(buf).map_err(Into::into)
1230 }
1231}