1use bytes::Bytes;
32
33use crate::{Error, Result};
34
35mod field {
37 pub const BASE_OFFSET: usize = 0;
38 pub const LENGTH: usize = 8;
39 pub const MAGIC: usize = 16;
40 pub const CRC: usize = 17;
41 pub const ATTRIBUTES: usize = 21;
42 pub const BASE_TIMESTAMP: usize = 27;
43 pub const PRODUCER_ID: usize = 43;
44 pub const RECORD_COUNT: usize = 57;
45 pub const HEADER_LEN: usize = 61;
47 pub const CRC_FROM: usize = 21;
49}
50
51#[derive(Debug, Clone, Copy)]
53pub struct LeanRecord {
54 pub offset: i64,
55 pub timestamp: i64,
56 key: (u32, u32),
57 value: (u32, u32),
58 headers: (u32, u32),
65 header_count: u32,
66}
67
68#[derive(Debug, Clone)]
70pub struct LeanBatch {
71 buffer: Bytes,
73 pub base_offset: i64,
74 pub producer_id: i64,
75 pub transactional: bool,
76 pub control: bool,
79 pub records: Vec<LeanRecord>,
80}
81
82impl LeanBatch {
83 #[must_use]
85 pub fn key(&self, record: &LeanRecord) -> Option<Bytes> {
86 self.slice(record.key)
87 }
88
89 #[must_use]
98 pub fn value(&self, record: &LeanRecord) -> Option<Bytes> {
99 self.slice(record.value)
100 }
101
102 fn slice(&self, (at, len): (u32, u32)) -> Option<Bytes> {
105 if at == u32::MAX {
106 return None;
107 }
108 let at = at as usize;
109 Some(self.buffer.slice(at..at + len as usize))
110 }
111
112 pub fn headers(&self, record: &LeanRecord) -> Result<Vec<(Bytes, Option<Bytes>)>> {
120 if record.header_count == 0 {
121 return Ok(Vec::new());
122 }
123 let at = record.headers.0 as usize;
124 let end = at + record.headers.1 as usize;
125 let block = self
126 .buffer
127 .get(at..end)
128 .ok_or_else(|| Error::Codec("header block".to_owned()))?;
129
130 let mut out = Vec::with_capacity(record.header_count as usize);
131 let mut pos = 0usize;
132 for _ in 0..record.header_count {
133 let key_len = varint(block, &mut pos)
134 .ok_or_else(|| Error::Codec("header key length".to_owned()))?;
135 let key_at = at + pos;
136 let key_len = key_len.max(0) as usize;
137 pos += key_len;
138
139 let value_len = varint(block, &mut pos)
140 .ok_or_else(|| Error::Codec("header value length".to_owned()))?;
141 let value = if value_len >= 0 {
142 let value_at = at + pos;
143 pos += value_len as usize;
144 Some(self.buffer.slice(value_at..value_at + value_len as usize))
145 } else {
146 None
147 };
148 out.push((self.buffer.slice(key_at..key_at + key_len), value));
149 }
150 Ok(out)
151 }
152
153 #[must_use]
157 pub fn control_type(&self, record: &LeanRecord) -> Option<i16> {
158 let key = self.slice(record.key)?;
159 if key.len() < 4 {
160 return None;
161 }
162 Some(i16::from_be_bytes([key[2], key[3]]))
163 }
164}
165
166#[inline]
172fn varint(buf: &[u8], pos: &mut usize) -> Option<i64> {
173 let mut raw: u64 = 0;
174 let mut shift = 0;
175 loop {
176 if shift > 63 {
177 return None;
178 }
179 let byte = *buf.get(*pos)?;
180 *pos += 1;
181 raw |= u64::from(byte & 0x7f) << shift;
182 if byte & 0x80 == 0 {
183 break;
184 }
185 shift += 7;
186 }
187 Some(((raw >> 1) as i64) ^ -((raw & 1) as i64))
188}
189
190fn i16_at(buf: &[u8], at: usize) -> Option<i16> {
191 Some(i16::from_be_bytes(buf.get(at..at + 2)?.try_into().ok()?))
192}
193
194fn i32_at(buf: &[u8], at: usize) -> Option<i32> {
195 Some(i32::from_be_bytes(buf.get(at..at + 4)?.try_into().ok()?))
196}
197
198fn i64_at(buf: &[u8], at: usize) -> Option<i64> {
199 Some(i64::from_be_bytes(buf.get(at..at + 8)?.try_into().ok()?))
200}
201
202pub fn decode_lean(buffer: &Bytes) -> Result<Option<Vec<LeanBatch>>> {
212 let mut batches = Vec::new();
213 let mut at = 0usize;
214
215 while at < buffer.len() {
216 let Some(length) = i32_at(buffer, at + field::LENGTH) else {
219 break;
220 };
221 let end = at + field::LENGTH + 4 + length.max(0) as usize;
222 if length <= 0 || end > buffer.len() {
223 break;
224 }
225 let batch = &buffer[at..end];
226 if batch.len() < field::HEADER_LEN {
227 break;
228 }
229
230 if batch[field::MAGIC] != 2 {
231 return Ok(None);
232 }
233 let attributes = i16_at(batch, field::ATTRIBUTES)
234 .ok_or_else(|| Error::Codec("batch attributes".to_owned()))?;
235
236 let expected =
237 i32_at(batch, field::CRC).ok_or_else(|| Error::Codec("batch crc".to_owned()))? as u32;
238 let actual = crc32c::crc32c(&batch[field::CRC_FROM..]);
239 if expected != actual {
240 return Err(Error::Codec(format!(
241 "record batch crc: expected {expected:#x}, got {actual:#x}"
242 )));
243 }
244
245 let base_offset = i64_at(batch, field::BASE_OFFSET)
246 .ok_or_else(|| Error::Codec("base offset".to_owned()))?;
247 let base_timestamp = i64_at(batch, field::BASE_TIMESTAMP)
248 .ok_or_else(|| Error::Codec("base timestamp".to_owned()))?;
249 let producer_id = i64_at(batch, field::PRODUCER_ID)
250 .ok_or_else(|| Error::Codec("producer id".to_owned()))?;
251 let count = i32_at(batch, field::RECORD_COUNT)
252 .ok_or_else(|| Error::Codec("record count".to_owned()))?
253 .max(0) as usize;
254
255 let body: Bytes = match attributes & 0x07 {
262 0 => buffer.slice(at + field::HEADER_LEN..end),
263 codec => decompress(codec, &batch[field::HEADER_LEN..])?,
264 };
265
266 let mut records = Vec::with_capacity(count);
267 let mut pos = 0usize;
268 for _ in 0..count {
269 let Some(len) = varint(&body, &mut pos) else {
270 return Err(Error::Codec("record length".to_owned()));
271 };
272 let record_end = pos + len.max(0) as usize;
273 if record_end > body.len() {
274 return Err(Error::Codec("record overruns its batch".to_owned()));
275 }
276
277 pos += 1; let timestamp_delta = varint(&body, &mut pos)
279 .ok_or_else(|| Error::Codec("timestamp delta".to_owned()))?;
280 let offset_delta =
281 varint(&body, &mut pos).ok_or_else(|| Error::Codec("offset delta".to_owned()))?;
282
283 let key_len =
284 varint(&body, &mut pos).ok_or_else(|| Error::Codec("key length".to_owned()))?;
285 let key = if key_len >= 0 {
286 let range = (pos as u32, key_len as u32);
287 pos += key_len as usize;
288 range
289 } else {
290 (u32::MAX, 0)
291 };
292
293 let value_len =
294 varint(&body, &mut pos).ok_or_else(|| Error::Codec("value length".to_owned()))?;
295 let value = if value_len >= 0 {
296 let range = (pos as u32, value_len as u32);
297 pos += value_len as usize;
298 range
299 } else {
300 (u32::MAX, 0)
301 };
302
303 let header_count = varint(&body, &mut pos)
306 .ok_or_else(|| Error::Codec("header count".to_owned()))?
307 .max(0) as u32;
308 let headers = (pos as u32, record_end.saturating_sub(pos) as u32);
309
310 records.push(LeanRecord {
311 offset: base_offset + offset_delta,
312 timestamp: base_timestamp + timestamp_delta,
313 key,
314 value,
315 headers,
316 header_count,
317 });
318 pos = record_end;
319 }
320
321 batches.push(LeanBatch {
322 buffer: body,
323 base_offset,
324 producer_id,
325 transactional: attributes & 0x10 != 0,
326 control: attributes & 0x20 != 0,
327 records,
328 });
329 at = end;
330 }
331
332 Ok(Some(batches))
333}
334
335const SNAPPY_MAGIC: &[u8; 16] = b"\x82SNAPPY\x00\x00\x00\x00\x01\x00\x00\x00\x01";
339
340fn snappy(compressed: &[u8]) -> Result<Vec<u8>> {
341 let raw = |bytes: &[u8]| {
342 snap::raw::Decoder::new()
343 .decompress_vec(bytes)
344 .map_err(|e| Error::Codec(format!("snappy: {e}")))
345 };
346
347 if compressed.len() < SNAPPY_MAGIC.len() || &compressed[..SNAPPY_MAGIC.len()] != SNAPPY_MAGIC {
348 return raw(compressed);
349 }
350
351 let mut out = Vec::new();
352 let mut at = SNAPPY_MAGIC.len();
353 while at < compressed.len() {
354 let len = compressed
355 .get(at..at + 4)
356 .and_then(|b| b.try_into().ok())
357 .map(u32::from_be_bytes)
358 .ok_or_else(|| Error::Codec("snappy block length".to_owned()))?
359 as usize;
360 at += 4;
361 let block = compressed
362 .get(at..at + len)
363 .ok_or_else(|| Error::Codec("snappy block overruns".to_owned()))?;
364 out.extend_from_slice(&raw(block)?);
365 at += len;
366 }
367 Ok(out)
368}
369
370fn decompress(codec: i16, compressed: &[u8]) -> Result<Bytes> {
376 use std::io::Read;
377
378 let mut out = Vec::new();
379 match codec {
380 1 => {
381 flate2::read::GzDecoder::new(compressed)
382 .read_to_end(&mut out)
383 .map_err(|e| Error::Codec(format!("gzip: {e}")))?;
384 }
385 2 => out = snappy(compressed)?,
386 3 => {
387 lz4::Decoder::new(compressed)
388 .map_err(|e| Error::Codec(format!("lz4: {e}")))?
389 .read_to_end(&mut out)
390 .map_err(|e| Error::Codec(format!("lz4: {e}")))?;
391 }
392 4 => {
393 zstd::stream::copy_decode(compressed, &mut out)
394 .map_err(|e| Error::Codec(format!("zstd: {e}")))?;
395 }
396 other => return Err(Error::Codec(format!("unknown compression codec {other}"))),
397 }
398 Ok(Bytes::from(out))
399}
400
401#[must_use]
413pub fn filter_batches(
414 batches: Vec<LeanBatch>,
415 aborted: &[crate::consumer::AbortedTransaction],
416 last_stable_offset: i64,
417 isolation: crate::IsolationLevel,
418 fetch_offset: i64,
419) -> (Vec<LeanBatch>, i64) {
420 let read_committed = isolation == crate::IsolationLevel::ReadCommitted;
421
422 let mut sorted: Vec<crate::consumer::AbortedTransaction> = aborted.to_vec();
423 sorted.sort_by_key(|a| a.first_offset);
424 let mut pending = sorted.into_iter().peekable();
425 let mut aborted_producers: std::collections::HashSet<i64> = std::collections::HashSet::new();
426
427 let mut kept = Vec::with_capacity(batches.len());
428 let mut next_offset = fetch_offset;
429
430 for mut batch in batches {
431 let Some(first) = batch.records.first().map(|r| r.offset) else {
432 continue;
433 };
434 if read_committed && first >= last_stable_offset {
437 break;
438 }
439
440 while pending.peek().is_some_and(|a| a.first_offset <= first) {
441 let a = pending.next().expect("peeked");
442 aborted_producers.insert(a.producer_id);
443 }
444
445 let last = batch.records.last().map_or(first, |r| r.offset);
446 next_offset = last + 1;
447
448 if batch.control {
449 for record in &batch.records {
452 if batch.control_type(record) == Some(CONTROL_ABORT) {
453 aborted_producers.remove(&batch.producer_id);
454 }
455 }
456 continue;
457 }
458
459 if read_committed && batch.transactional && aborted_producers.contains(&batch.producer_id) {
460 continue;
461 }
462
463 if first < fetch_offset {
466 batch.records.retain(|r| r.offset >= fetch_offset);
467 }
468 if read_committed {
469 batch.records.retain(|r| r.offset < last_stable_offset);
470 }
471 if !batch.records.is_empty() {
472 kept.push(batch);
473 }
474 }
475
476 (kept, next_offset)
477}
478
479const CONTROL_ABORT: i16 = 0;
481
482#[cfg(test)]
483mod tests {
484 use super::*;
485 use kafka_protocol::records::{
486 Compression, Record, RecordBatchEncoder, RecordEncodeOptions, TimestampType,
487 };
488
489 fn encode(records: &[Record], compression: Compression) -> Bytes {
490 let mut buf = bytes::BytesMut::new();
491 RecordBatchEncoder::encode(
492 &mut buf,
493 records.iter(),
494 &RecordEncodeOptions {
495 version: 2,
496 compression,
497 },
498 )
499 .expect("encode");
500 buf.freeze()
501 }
502
503 fn record(offset: i64, key: Option<&[u8]>, value: Option<&[u8]>) -> Record {
504 Record {
505 transactional: false,
506 control: false,
507 partition_leader_epoch: 0,
508 producer_id: 7,
509 producer_epoch: 0,
510 timestamp_type: TimestampType::Creation,
511 offset,
512 sequence: offset as i32,
513 timestamp: 1_000 + offset,
514 key: key.map(Bytes::copy_from_slice),
515 value: value.map(Bytes::copy_from_slice),
516 headers: Default::default(),
517 }
518 }
519
520 #[test]
522 fn agrees_with_the_reference_decoder() {
523 let records: Vec<Record> = (0..64)
524 .map(|i| {
525 record(
526 i,
527 Some(format!("k{i}").as_bytes()),
528 Some(format!("value-{i}").as_bytes()),
529 )
530 })
531 .collect();
532 let encoded = encode(&records, Compression::None);
533
534 let reference = kafka_protocol::records::RecordBatchDecoder::decode(&mut encoded.clone())
535 .expect("reference decode")
536 .records;
537 let lean = decode_lean(&encoded)
538 .expect("lean decode")
539 .expect("handled");
540
541 let flat: Vec<_> = lean
542 .iter()
543 .flat_map(|batch| batch.records.iter().map(move |r| (batch, r)))
544 .collect();
545 assert_eq!(flat.len(), reference.len());
546
547 for ((batch, lean), reference) in flat.iter().zip(&reference) {
548 assert_eq!(lean.offset, reference.offset, "offset");
549 assert_eq!(lean.timestamp, reference.timestamp, "timestamp");
550 assert_eq!(batch.key(lean), reference.key, "key");
551 assert_eq!(batch.value(lean), reference.value, "value");
552 assert_eq!(batch.producer_id, reference.producer_id, "producer id");
553 }
554 }
555
556 #[test]
557 fn a_null_key_stays_null() {
558 let encoded = encode(&[record(0, None, Some(b"v"))], Compression::None);
559 let lean = decode_lean(&encoded).expect("decode").expect("handled");
560 let batch = &lean[0];
561 assert_eq!(batch.key(&batch.records[0]), None);
562 assert_eq!(
563 batch.value(&batch.records[0]),
564 Some(Bytes::from_static(b"v"))
565 );
566 }
567
568 #[test]
570 fn every_compression_codec_round_trips() {
571 for compression in [
572 Compression::Gzip,
573 Compression::Snappy,
574 Compression::Lz4,
575 Compression::Zstd,
576 ] {
577 let records: Vec<Record> = (0..32)
578 .map(|i| {
579 record(
580 i,
581 Some(format!("k{i}").as_bytes()),
582 Some(format!("value-{i}").as_bytes()),
583 )
584 })
585 .collect();
586 let encoded = encode(&records, compression);
587
588 let lean = decode_lean(&encoded)
589 .unwrap_or_else(|e| panic!("{compression:?}: {e}"))
590 .unwrap_or_else(|| panic!("{compression:?} was handed back"));
591 let flat: Vec<_> = lean
592 .iter()
593 .flat_map(|b| b.records.iter().map(move |r| (b, r)))
594 .collect();
595 assert_eq!(flat.len(), records.len(), "{compression:?}");
596 for ((batch, lean), reference) in flat.iter().zip(&records) {
597 assert_eq!(lean.offset, reference.offset, "{compression:?} offset");
598 assert_eq!(batch.value(lean), reference.value, "{compression:?} value");
599 }
600 }
601 }
602
603 #[test]
605 fn headers_are_read_on_demand() {
606 let mut with_headers = record(0, Some(b"k"), Some(b"v"));
607 with_headers.headers.insert(
608 kafka_protocol::protocol::StrBytes::from_static_str("trace"),
609 Some(Bytes::from_static(b"abc")),
610 );
611 with_headers.headers.insert(
612 kafka_protocol::protocol::StrBytes::from_static_str("empty"),
613 None,
614 );
615 let encoded = encode(&[with_headers], Compression::None);
616
617 let lean = decode_lean(&encoded).expect("decode").expect("handled");
618 let batch = &lean[0];
619 let headers = batch.headers(&batch.records[0]).expect("headers");
620 assert_eq!(headers.len(), 2);
621 assert_eq!(headers[0].0, Bytes::from_static(b"trace"));
622 assert_eq!(headers[0].1, Some(Bytes::from_static(b"abc")));
623 assert_eq!(headers[1].0, Bytes::from_static(b"empty"));
624 assert_eq!(headers[1].1, None);
625 }
626
627 #[test]
629 fn no_headers_is_empty() {
630 let encoded = encode(&[record(0, None, Some(b"v"))], Compression::None);
631 let lean = decode_lean(&encoded).expect("decode").expect("handled");
632 let batch = &lean[0];
633 assert!(batch
634 .headers(&batch.records[0])
635 .expect("headers")
636 .is_empty());
637 }
638
639 #[test]
642 fn a_truncated_trailing_batch_is_ignored() {
643 let encoded = encode(&[record(0, None, Some(b"v"))], Compression::None);
644 let mut truncated = bytes::BytesMut::from(&encoded[..]);
645 truncated.extend_from_slice(&encoded[..encoded.len() / 2]);
646 let lean = decode_lean(&truncated.freeze())
647 .expect("decode")
648 .expect("handled");
649 assert_eq!(
650 lean.len(),
651 1,
652 "the whole batch is kept, the fragment is not"
653 );
654 assert_eq!(lean[0].records.len(), 1);
655 }
656
657 #[test]
659 fn a_bad_crc_is_an_error() {
660 let encoded = encode(&[record(0, None, Some(b"value"))], Compression::None);
661 let mut corrupt = bytes::BytesMut::from(&encoded[..]);
662 let last = corrupt.len() - 1;
663 corrupt[last] ^= 0xff;
664 assert!(decode_lean(&corrupt.freeze()).is_err());
665 }
666}