1use bytes::{Buf, BufMut, Bytes, BytesMut};
4use zerocopy::FromBytes as _;
5
6use crate::primitives::varint::{
7 get_varint, get_varlong, put_varint, put_varlong, varint_len, varlong_len,
8};
9use crate::records::RecordsError;
10use crate::records::crc::{crc32c, crc32c_append};
11use crate::records::header::{Attributes, HEADER_LEN};
12
13#[derive(Debug, Clone, PartialEq, Eq, Default)]
14pub struct RecordHeader {
15 pub key: String,
16 pub value: Option<Bytes>,
17}
18
19#[derive(Debug, Clone, PartialEq, Eq, Default)]
20pub struct Record {
21 pub attributes: i8,
22 pub timestamp_delta: i64,
23 pub offset_delta: i32,
24 pub key: Option<Bytes>,
25 pub value: Option<Bytes>,
26 pub headers: Vec<RecordHeader>,
27}
28
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct RecordBatch {
31 pub base_offset: i64,
32 pub partition_leader_epoch: i32,
33 pub attributes: Attributes,
34 pub last_offset_delta: i32,
35 pub base_timestamp: i64,
36 pub max_timestamp: i64,
37 pub producer_id: i64,
38 pub producer_epoch: i16,
39 pub base_sequence: i32,
40 pub records: Vec<Record>,
41}
42
43impl Default for RecordBatch {
44 fn default() -> Self {
45 Self {
46 base_offset: 0,
47 partition_leader_epoch: 0,
48 attributes: Attributes::default(),
49 last_offset_delta: 0,
50 base_timestamp: 0,
51 max_timestamp: 0,
52 producer_id: -1, producer_epoch: -1,
54 base_sequence: -1,
55 records: Vec::new(),
56 }
57 }
58}
59
60impl Record {
61 pub fn encode<B: BufMut>(&self, buf: &mut B) -> Result<(), RecordsError> {
63 let body_len = self.body_len();
64 put_varlong(
65 buf,
66 i64::try_from(body_len)
67 .map_err(|_| RecordsError::RecordParse("record body length overflow".into()))?,
68 );
69 self.encode_body(buf)
70 }
71
72 pub fn encoded_len(&self) -> usize {
74 let body = self.body_len();
75 #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
76 let body_i64 = body as i64;
77 varlong_len(body_i64) + body
78 }
79
80 fn body_len(&self) -> usize {
81 let mut n = 1; n += varlong_len(self.timestamp_delta);
83 n += varint_len(self.offset_delta);
84 n += match &self.key {
85 None => varint_len(-1),
86 Some(k) => varint_len(i32::try_from(k.len()).unwrap_or(i32::MAX)) + k.len(),
87 };
88 n += match &self.value {
89 None => varint_len(-1),
90 Some(v) => varint_len(i32::try_from(v.len()).unwrap_or(i32::MAX)) + v.len(),
91 };
92 n += varint_len(i32::try_from(self.headers.len()).unwrap_or(i32::MAX));
93 for h in &self.headers {
94 let key_bytes = h.key.as_bytes();
95 n += varint_len(i32::try_from(key_bytes.len()).unwrap_or(i32::MAX)) + key_bytes.len();
96 n += match &h.value {
97 None => varint_len(-1),
98 Some(v) => varint_len(i32::try_from(v.len()).unwrap_or(i32::MAX)) + v.len(),
99 };
100 }
101 n
102 }
103
104 fn encode_body<B: BufMut>(&self, buf: &mut B) -> Result<(), RecordsError> {
105 buf.put_i8(self.attributes);
106 put_varlong(buf, self.timestamp_delta);
107 put_varint(buf, self.offset_delta);
108 match &self.key {
109 None => put_varint(buf, -1),
110 Some(k) => {
111 put_varint(
112 buf,
113 i32::try_from(k.len()).map_err(|_| {
114 RecordsError::RecordParse("record key length overflow".into())
115 })?,
116 );
117 buf.put_slice(k);
118 }
119 }
120 match &self.value {
121 None => put_varint(buf, -1),
122 Some(v) => {
123 put_varint(
124 buf,
125 i32::try_from(v.len()).map_err(|_| {
126 RecordsError::RecordParse("record value length overflow".into())
127 })?,
128 );
129 buf.put_slice(v);
130 }
131 }
132 put_varint(
133 buf,
134 i32::try_from(self.headers.len())
135 .map_err(|_| RecordsError::RecordParse("record header count overflow".into()))?,
136 );
137 for h in &self.headers {
138 let key_bytes = h.key.as_bytes();
139 put_varint(
140 buf,
141 i32::try_from(key_bytes.len())
142 .map_err(|_| RecordsError::RecordParse("header key length overflow".into()))?,
143 );
144 buf.put_slice(key_bytes);
145 match &h.value {
146 None => put_varint(buf, -1),
147 Some(v) => {
148 put_varint(
149 buf,
150 i32::try_from(v.len()).map_err(|_| {
151 RecordsError::RecordParse("header value length overflow".into())
152 })?,
153 );
154 buf.put_slice(v);
155 }
156 }
157 }
158 Ok(())
159 }
160
161 pub fn decode<B: Buf>(buf: &mut B) -> Result<Self, RecordsError> {
164 let body_len = get_varlong(buf)
165 .map_err(|e| RecordsError::RecordParse(format!("record length: {e}")))?;
166 let body_len = usize::try_from(body_len).map_err(|_| {
167 RecordsError::RecordParse(format!("record length negative or too large: {body_len}"))
168 })?;
169 if buf.remaining() < body_len {
170 return Err(RecordsError::BodyTooShort {
171 needed: body_len - buf.remaining(),
172 });
173 }
174 let mut body = buf.take(body_len);
177 let r = Self::decode_body(&mut body)?;
178 if body.has_remaining() {
180 return Err(RecordsError::RecordParse(format!(
181 "trailing bytes inside record (left={})",
182 body.remaining()
183 )));
184 }
185 Ok(r)
186 }
187
188 fn decode_body<B: Buf>(buf: &mut B) -> Result<Self, RecordsError> {
189 if buf.remaining() == 0 {
190 return Err(RecordsError::RecordParse("record body empty".into()));
191 }
192 let attributes = buf.get_i8();
193 let timestamp_delta = get_varlong(buf)
194 .map_err(|e| RecordsError::RecordParse(format!("timestamp_delta: {e}")))?;
195 let offset_delta =
196 get_varint(buf).map_err(|e| RecordsError::RecordParse(format!("offset_delta: {e}")))?;
197
198 let key = decode_nullable_bytes(buf, "key")?;
199 let value = decode_nullable_bytes(buf, "value")?;
200
201 let header_count =
202 get_varint(buf).map_err(|e| RecordsError::RecordParse(format!("header_count: {e}")))?;
203 if header_count < 0 {
204 return Err(RecordsError::RecordParse(format!(
205 "negative header count {header_count}"
206 )));
207 }
208 #[allow(clippy::cast_sign_loss)] let header_count_usize = header_count as usize;
210 let mut headers = Vec::with_capacity(header_count_usize.min(buf.remaining()));
215 for i in 0..header_count {
216 headers.push(
217 decode_record_header(buf)
218 .map_err(|e| RecordsError::RecordParse(format!("header[{i}]: {e}")))?,
219 );
220 }
221
222 Ok(Self {
223 attributes,
224 timestamp_delta,
225 offset_delta,
226 key,
227 value,
228 headers,
229 })
230 }
231}
232
233fn decode_nullable_bytes<B: Buf>(buf: &mut B, label: &str) -> Result<Option<Bytes>, RecordsError> {
234 let len =
235 get_varint(buf).map_err(|e| RecordsError::RecordParse(format!("{label} length: {e}")))?;
236 if len < 0 {
237 Ok(None)
238 } else {
239 #[allow(clippy::cast_sign_loss)] let n = len as usize;
241 if buf.remaining() < n {
242 return Err(RecordsError::BodyTooShort {
243 needed: n - buf.remaining(),
244 });
245 }
246 let mut v = vec![0u8; n];
247 buf.copy_to_slice(&mut v);
248 Ok(Some(Bytes::from(v)))
249 }
250}
251
252fn decode_record_header<B: Buf>(buf: &mut B) -> Result<RecordHeader, String> {
253 let key_len = get_varint(buf).map_err(|e| format!("key length: {e}"))?;
254 if key_len < 0 {
255 return Err(format!("non-nullable key has negative length {key_len}"));
256 }
257 #[allow(clippy::cast_sign_loss)] let n = key_len as usize;
259 if buf.remaining() < n {
260 return Err(format!("key truncated (need {} more)", n - buf.remaining()));
261 }
262 let mut kv = vec![0u8; n];
263 buf.copy_to_slice(&mut kv);
264 let key = String::from_utf8(kv).map_err(|e| format!("key utf-8: {e}"))?;
265
266 let value_len = get_varint(buf).map_err(|e| format!("value length: {e}"))?;
267 let value = if value_len < 0 {
268 None
269 } else {
270 #[allow(clippy::cast_sign_loss)] let n = value_len as usize;
272 if buf.remaining() < n {
273 return Err(format!(
274 "value truncated (need {} more)",
275 n - buf.remaining()
276 ));
277 }
278 let mut vv = vec![0u8; n];
279 buf.copy_to_slice(&mut vv);
280 Some(Bytes::from(vv))
281 };
282
283 Ok(RecordHeader { key, value })
284}
285
286#[cfg(test)]
287mod record_tests {
288 use super::*;
289 use assert2::assert;
290 use bytes::BytesMut;
291
292 fn fixture_minimal_record() -> Record {
293 Record {
294 attributes: 0,
295 timestamp_delta: 0,
296 offset_delta: 0,
297 key: None,
298 value: None,
299 headers: vec![],
300 }
301 }
302
303 fn fixture_keyed_record() -> Record {
304 Record {
305 attributes: 0,
306 timestamp_delta: 17,
307 offset_delta: 2,
308 key: Some(Bytes::from_static(b"the-key")),
309 value: Some(Bytes::from_static(b"hello kafka")),
310 headers: vec![
311 RecordHeader {
312 key: "trace-id".to_string(),
313 value: Some(Bytes::from_static(b"abc")),
314 },
315 RecordHeader {
316 key: "null-val".to_string(),
317 value: None,
318 },
319 ],
320 }
321 }
322
323 fn fixture_large_payload_record() -> Record {
324 Record {
325 attributes: 0,
326 timestamp_delta: 1_000_000,
327 offset_delta: 999,
328 key: Some(Bytes::from(vec![b'k'; 128])),
329 value: Some(Bytes::from(vec![b'v'; 4096])),
330 headers: vec![],
331 }
332 }
333
334 macro_rules! roundtrip {
335 ($name:ident, $fixture:ident) => {
336 #[test]
337 fn $name() {
338 let r = $fixture();
339 let mut buf = BytesMut::new();
340 r.encode(&mut buf).unwrap();
341 assert!(buf.len() == r.encoded_len(), "predicted len mismatch");
342
343 let mut cur: &[u8] = &buf[..];
344 let decoded = Record::decode(&mut cur).unwrap();
345 assert!(decoded == r);
346 assert!(cur.is_empty(), "trailing bytes after decode");
347 }
348 };
349 }
350
351 roundtrip!(minimal, fixture_minimal_record);
352 roundtrip!(keyed_with_headers, fixture_keyed_record);
353 roundtrip!(large_payload, fixture_large_payload_record);
354
355 #[test]
356 fn decode_rejects_negative_header_count() {
357 let mut buf = BytesMut::new();
358 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[..];
369 match Record::decode(&mut cur) {
370 Err(RecordsError::RecordParse(msg)) => {
371 assert!(msg.contains("negative header count"), "got: {msg}");
372 }
373 other => panic!("expected RecordParse, got {other:?}"),
374 }
375 }
376
377 #[test]
378 fn decode_huge_header_count_does_not_overallocate() {
379 let mut inner = BytesMut::new();
384 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();
392 put_varlong(&mut buf, i64::try_from(inner.len()).unwrap());
393 buf.extend_from_slice(&inner);
394
395 let mut cur: &[u8] = &buf[..];
396 assert!(Record::decode(&mut cur).is_err());
398 }
399}
400
401impl RecordBatch {
402 #[must_use]
405 pub fn delete_horizon_ms(&self) -> Option<i64> {
406 self.attributes
407 .has_delete_horizon()
408 .then_some(self.base_timestamp)
409 }
410
411 #[must_use]
416 pub fn with_delete_horizon(mut self, horizon_ms: i64) -> Self {
417 let old_base = self.base_timestamp;
418 for r in &mut self.records {
419 let abs = old_base.saturating_add(r.timestamp_delta);
424 r.timestamp_delta = abs.saturating_sub(horizon_ms);
425 }
426 self.base_timestamp = horizon_ms;
427 self.attributes = self.attributes.with_delete_horizon(true);
428 self
429 }
430
431 pub fn decode<B: Buf>(buf: &mut B) -> Result<Self, RecordsError> {
434 const HEADER_TAIL_LEN: i32 = 49;
440
441 if buf.remaining() < HEADER_LEN {
443 return Err(RecordsError::HeaderTooShort {
444 needed: HEADER_LEN - buf.remaining(),
445 });
446 }
447 let mut hdr_bytes = [0u8; HEADER_LEN];
449 buf.copy_to_slice(&mut hdr_bytes);
450
451 let hdr = crate::records::header::RecordBatchHeader::ref_from_bytes(&hdr_bytes[..])
452 .map_err(|_| RecordsError::ZerocopyFailure)?;
453
454 if hdr.magic != 2 {
455 return Err(RecordsError::UnsupportedMagic { found: hdr.magic });
456 }
457
458 let body_len = i32::checked_sub(hdr.batch_length.get(), HEADER_TAIL_LEN)
460 .and_then(|n| usize::try_from(n).ok())
461 .ok_or_else(|| {
462 RecordsError::RecordParse("negative or oversized batch_length".into())
463 })?;
464
465 if buf.remaining() < body_len {
466 return Err(RecordsError::BodyTooShort {
467 needed: body_len - buf.remaining(),
468 });
469 }
470
471 let mut body = vec![0u8; body_len];
473 buf.copy_to_slice(&mut body);
474
475 let expected_crc = hdr.crc.get();
478 let mut computed = crc32c(&hdr_bytes[21..HEADER_LEN]);
479 computed = crc32c_append(computed, &body);
480 if computed != expected_crc {
481 return Err(RecordsError::CrcMismatch {
482 expected: expected_crc,
483 computed,
484 });
485 }
486
487 let attributes = Attributes(hdr.attributes.get());
488 let codec = attributes.compression();
489
490 let body_for_records: Bytes = if codec == crabka_compression::CompressionType::None {
492 Bytes::from(body)
493 } else {
494 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 = body
501 .len()
502 .saturating_mul(DECOMPRESS_MAX_RATIO)
503 .clamp(DECOMPRESS_MIN_CAP, DECOMPRESS_ABSOLUTE_CEILING);
504 crabka_compression::decompress(codec, &body, max_output)?
505 };
506
507 let count = hdr.records_count.get();
509 if count < 0 {
510 return Err(RecordsError::RecordParse(format!(
511 "negative records_count {count}"
512 )));
513 }
514 let mut body_cur: &[u8] = &body_for_records[..];
515 #[allow(clippy::cast_sign_loss)] let mut records = Vec::with_capacity((count as usize).min(body_for_records.len()));
520 for i in 0..count {
521 let r = crate::records::borrowed::parse_one_record(&mut body_cur)
528 .map_err(|e| RecordsError::RecordParse(format!("record[{i}]: {e}")))?;
529 records.push(Record {
530 attributes: r.attributes,
531 timestamp_delta: r.timestamp_delta,
532 offset_delta: r.offset_delta,
533 key: r.key.map(|s| body_for_records.slice_ref(s)),
534 value: r.value.map(|s| body_for_records.slice_ref(s)),
535 headers: r
536 .headers
537 .into_iter()
538 .map(|h| RecordHeader {
539 key: h.key.to_string(),
540 value: h.value.map(|s| body_for_records.slice_ref(s)),
541 })
542 .collect(),
543 });
544 }
545 if !body_cur.is_empty() {
546 return Err(RecordsError::RecordParse(format!(
547 "trailing bytes after records (left={})",
548 body_cur.len()
549 )));
550 }
551
552 Ok(Self {
553 base_offset: hdr.base_offset.get(),
554 partition_leader_epoch: hdr.partition_leader_epoch.get(),
555 attributes,
556 last_offset_delta: hdr.last_offset_delta.get(),
557 base_timestamp: hdr.base_timestamp.get(),
558 max_timestamp: hdr.max_timestamp.get(),
559 producer_id: hdr.producer_id.get(),
560 producer_epoch: hdr.producer_epoch.get(),
561 base_sequence: hdr.base_sequence.get(),
562 records,
563 })
564 }
565
566 pub fn encode<B: BufMut>(&self, buf: &mut B) -> Result<(), RecordsError> {
568 const HEADER_TAIL_LEN: i32 = 49;
569
570 let mut raw_body =
572 BytesMut::with_capacity(self.records.iter().map(Record::encoded_len).sum());
573 for r in &self.records {
574 r.encode(&mut raw_body)?;
575 }
576 let raw_body = raw_body.freeze();
577
578 let codec = self.attributes.compression();
580 let body: Bytes = if codec == crabka_compression::CompressionType::None {
581 raw_body
582 } else {
583 crabka_compression::compress(codec, &raw_body)?
584 };
585
586 let batch_length = HEADER_TAIL_LEN
588 + i32::try_from(body.len())
589 .map_err(|_| RecordsError::RecordParse("body length exceeds i32".into()))?;
590
591 let mut covered = BytesMut::with_capacity(40);
593 covered.put_i16(self.attributes.0);
594 covered.put_i32(self.last_offset_delta);
595 covered.put_i64(self.base_timestamp);
596 covered.put_i64(self.max_timestamp);
597 covered.put_i64(self.producer_id);
598 covered.put_i16(self.producer_epoch);
599 covered.put_i32(self.base_sequence);
600 covered.put_i32(
601 i32::try_from(self.records.len())
602 .map_err(|_| RecordsError::RecordParse("records_count exceeds i32".into()))?,
603 );
604 let covered_head = covered.freeze();
605
606 let mut crc = crc32c(&covered_head);
608 crc = crc32c_append(crc, &body);
609
610 buf.put_i64(self.base_offset);
612 buf.put_i32(batch_length);
613 buf.put_i32(self.partition_leader_epoch);
614 buf.put_i8(2); buf.put_u32(crc);
616 buf.put_slice(&covered_head);
617 buf.put_slice(&body);
618 Ok(())
619 }
620
621 pub fn encoded_len(&self) -> usize {
624 let body: usize = self.records.iter().map(Record::encoded_len).sum();
625 HEADER_LEN + body
626 }
627}
628
629#[cfg(test)]
630mod batch_tests {
631 use super::*;
632 use assert2::assert;
633 use crabka_compression::CompressionType;
634
635 fn fixture_empty_batch() -> RecordBatch {
636 RecordBatch::default()
637 }
638
639 fn fixture_single_record_batch() -> RecordBatch {
640 RecordBatch {
641 records: vec![Record {
642 key: Some(Bytes::from_static(b"k1")),
643 value: Some(Bytes::from_static(b"v1")),
644 ..Default::default()
645 }],
646 ..RecordBatch::default()
647 }
648 }
649
650 fn fixture_multi_record_batch() -> RecordBatch {
651 RecordBatch {
652 base_offset: 42,
653 partition_leader_epoch: 5,
654 last_offset_delta: 2,
655 base_timestamp: 1_700_000_000,
656 max_timestamp: 1_700_000_500,
657 producer_id: 100,
658 producer_epoch: 3,
659 base_sequence: 7,
660 records: vec![
661 Record {
662 offset_delta: 0,
663 timestamp_delta: 0,
664 key: Some(Bytes::from_static(b"a")),
665 value: Some(Bytes::from_static(b"1")),
666 ..Default::default()
667 },
668 Record {
669 offset_delta: 1,
670 timestamp_delta: 100,
671 key: Some(Bytes::from_static(b"b")),
672 value: Some(Bytes::from_static(b"2")),
673 ..Default::default()
674 },
675 Record {
676 offset_delta: 2,
677 timestamp_delta: 500,
678 key: None,
679 value: Some(Bytes::from_static(b"3")),
680 headers: vec![RecordHeader {
681 key: "h".to_string(),
682 value: Some(Bytes::from_static(b"hv")),
683 }],
684 ..Default::default()
685 },
686 ],
687 ..RecordBatch::default()
688 }
689 }
690
691 macro_rules! roundtrip_uncompressed {
692 ($name:ident, $fixture:ident) => {
693 #[test]
694 fn $name() {
695 let mut b = $fixture();
696 b.attributes = b.attributes.with_compression(CompressionType::None);
697
698 let mut buf = BytesMut::new();
699 b.encode(&mut buf).unwrap();
700 assert!(buf.len() == b.encoded_len());
701
702 let mut cur: &[u8] = &buf[..];
703 let decoded = RecordBatch::decode(&mut cur).unwrap();
704 assert!(decoded == b);
705 assert!(cur.is_empty());
706 }
707 };
708 }
709
710 roundtrip_uncompressed!(uncompressed_empty, fixture_empty_batch);
711 roundtrip_uncompressed!(uncompressed_single, fixture_single_record_batch);
712 roundtrip_uncompressed!(uncompressed_multi, fixture_multi_record_batch);
713
714 #[test]
715 fn rejects_pre_v2_magic() {
716 let mut buf = BytesMut::new();
717 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 {
723 buf.put_u8(0);
724 }
725 let mut cur: &[u8] = &buf[..];
726 assert!(matches!(
727 RecordBatch::decode(&mut cur),
728 Err(RecordsError::UnsupportedMagic { found: 1 })
729 ));
730 }
731
732 #[test]
733 fn rejects_bad_crc() {
734 let b = fixture_single_record_batch();
735 let mut buf = BytesMut::new();
736 b.encode(&mut buf).unwrap();
737 buf[17] ^= 0xFF;
739 let mut cur: &[u8] = &buf[..];
740 assert!(matches!(
741 RecordBatch::decode(&mut cur),
742 Err(RecordsError::CrcMismatch { .. })
743 ));
744 }
745
746 macro_rules! roundtrip_compressed {
747 ($name:ident, $codec:expr) => {
748 #[test]
749 fn $name() {
750 let mut b = fixture_multi_record_batch();
751 b.attributes = b.attributes.with_compression($codec);
752
753 let mut buf = BytesMut::new();
754 b.encode(&mut buf).unwrap();
755 let mut cur: &[u8] = &buf[..];
756 let decoded = RecordBatch::decode(&mut cur).unwrap();
757 assert!(decoded == b);
758 assert!(cur.is_empty());
759 }
760 };
761 }
762
763 roundtrip_compressed!(compressed_gzip, CompressionType::Gzip);
764 roundtrip_compressed!(compressed_snappy, CompressionType::Snappy);
765 roundtrip_compressed!(compressed_lz4, CompressionType::Lz4);
766 roundtrip_compressed!(compressed_zstd, CompressionType::Zstd);
767
768 #[test]
769 fn with_delete_horizon_stamps_and_preserves_record_timestamps() {
770 let b = RecordBatch {
772 base_timestamp: 1000,
773 records: vec![
774 Record {
775 timestamp_delta: 0,
776 ..Default::default()
777 },
778 Record {
779 timestamp_delta: 5,
780 ..Default::default()
781 },
782 ],
783 ..RecordBatch::default()
784 };
785
786 let stamped = b.with_delete_horizon(9999);
787
788 assert!(stamped.attributes.has_delete_horizon());
789 assert!(stamped.base_timestamp == 9999);
790 assert!(stamped.delete_horizon_ms() == Some(9999));
791
792 let absolutes: Vec<i64> = stamped
794 .records
795 .iter()
796 .map(|r| stamped.base_timestamp + r.timestamp_delta)
797 .collect();
798 assert!(absolutes == vec![1000, 1005]);
799 }
800
801 #[test]
802 fn delete_horizon_round_trips_through_encode_decode() {
803 let b = RecordBatch {
805 base_timestamp: 1000,
806 last_offset_delta: 1,
807 records: vec![
808 Record {
809 timestamp_delta: 0,
810 offset_delta: 0,
811 key: Some(Bytes::from_static(b"k1")),
812 value: Some(Bytes::from_static(b"v1")),
813 ..Default::default()
814 },
815 Record {
816 timestamp_delta: 5,
817 offset_delta: 1,
818 key: Some(Bytes::from_static(b"k2")),
819 value: Some(Bytes::from_static(b"v2")),
820 ..Default::default()
821 },
822 ],
823 ..RecordBatch::default()
824 }
825 .with_delete_horizon(9999);
826
827 let mut buf = BytesMut::new();
828 b.encode(&mut buf).unwrap();
829
830 let mut cur: &[u8] = &buf[..];
831 let decoded = RecordBatch::decode(&mut cur).unwrap();
832 assert!(cur.is_empty());
833
834 assert!(decoded.delete_horizon_ms() == Some(9999));
835
836 let absolutes: Vec<i64> = decoded
837 .records
838 .iter()
839 .map(|r| decoded.base_timestamp + r.timestamp_delta)
840 .collect();
841 assert!(absolutes == vec![1000, 1005]);
842 }
843
844 #[test]
845 fn decode_huge_records_count_does_not_overallocate() {
846 let mut b = fixture_empty_batch();
851 b.attributes = b.attributes.with_compression(CompressionType::None);
852 let mut buf = BytesMut::new();
853 b.encode(&mut buf).unwrap();
854
855 let rc_off = HEADER_LEN - 4;
857 buf[rc_off..HEADER_LEN].copy_from_slice(&1_000_000_000i32.to_be_bytes());
858
859 let body = &buf[HEADER_LEN..];
861 let mut computed = crc32c(&buf[21..HEADER_LEN]);
862 computed = crc32c_append(computed, body);
863 buf[17..21].copy_from_slice(&computed.to_be_bytes());
864
865 let mut cur: &[u8] = &buf[..];
866 assert!(RecordBatch::decode(&mut cur).is_err());
868 }
869}
870
871impl crate::Encode for RecordBatch {
872 fn encode<B: BufMut>(&self, buf: &mut B, _version: i16) -> Result<(), crate::ProtocolError> {
873 RecordBatch::encode(self, buf).map_err(Into::into)
874 }
875
876 fn encoded_len(&self, _version: i16) -> usize {
877 RecordBatch::encoded_len(self)
878 }
879}
880
881impl crate::Decode<'_> for RecordBatch {
882 fn decode<B: Buf>(buf: &mut B, _version: i16) -> Result<Self, crate::ProtocolError> {
883 RecordBatch::decode(buf).map_err(Into::into)
884 }
885}