1use bytes::{Buf, Bytes};
19
20use crate::{
21 ProtocolError,
22 primitives::{
23 array::get_array_len,
24 fixed::{get_i16, get_i32},
25 string_bytes::{get_compact_nullable_string_owned, get_nullable_string_owned},
26 uuid::{Uuid, get_uuid},
27 varint::get_uvarint,
28 },
29 tagged_fields::read_tagged_fields,
30};
31
32const FLEXIBLE_MIN: i16 = 9;
36
37#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct PartitionRecordSlice {
41 pub topic_index: usize,
43 pub partition_index: usize,
45 pub partition: i32,
47 pub records: Option<Bytes>,
50}
51
52#[derive(Debug, Clone, Default, PartialEq, Eq)]
61pub struct ProduceFraming {
62 pub transactional_id: Option<String>,
64 pub acks: i16,
66 pub timeout_ms: i32,
68 pub topics: Vec<ProduceFramingTopic>,
70}
71
72#[derive(Debug, Clone, Default, PartialEq, Eq)]
74pub struct ProduceFramingTopic {
75 pub name: String,
77 pub topic_id: Uuid,
79 pub partitions: Vec<PartitionRecordSlice>,
81}
82
83pub fn produce_record_slices(
97 mut body: Bytes,
98 version: i16,
99) -> Result<Vec<PartitionRecordSlice>, ProtocolError> {
100 let flex = version >= FLEXIBLE_MIN;
101 let buf = &mut body;
102
103 if version >= 3 {
105 if flex {
106 get_compact_nullable_string_owned(buf)?;
107 } else {
108 get_nullable_string_owned(buf)?;
109 }
110 }
111 let _acks = get_i16(buf)?;
113 let _timeout_ms = get_i32(buf)?;
114
115 let mut out = Vec::new();
116
117 let topic_count = get_array_len(buf, flex)?;
118 for topic_index in 0..topic_count {
119 if version <= 12 {
121 if flex {
122 get_compact_nullable_string_owned(buf)?;
123 } else {
124 get_nullable_string_owned(buf)?;
125 }
126 }
127 if version >= 13 {
128 skip(buf, 16)?; }
130
131 let partition_count = get_array_len(buf, flex)?;
132 for partition_index in 0..partition_count {
133 let partition = get_i32(buf)?;
134 let records = read_nullable_bytes_slice(buf, flex)?;
136 out.push(PartitionRecordSlice {
137 topic_index,
138 partition_index,
139 partition,
140 records,
141 });
142 if flex {
144 read_tagged_fields(buf, |_tag, _payload| Ok(false))?;
145 }
146 }
147 if flex {
149 read_tagged_fields(buf, |_tag, _payload| Ok(false))?;
150 }
151 }
152 if flex {
155 read_tagged_fields(buf, |_tag, _payload| Ok(false))?;
156 }
157
158 Ok(out)
159}
160
161pub fn produce_framing(mut body: Bytes, version: i16) -> Result<ProduceFraming, ProtocolError> {
184 let flex = version >= FLEXIBLE_MIN;
185 let buf = &mut body;
186
187 let transactional_id = if version >= 3 {
189 if flex {
190 get_compact_nullable_string_owned(buf)?
191 } else {
192 get_nullable_string_owned(buf)?
193 }
194 } else {
195 None
196 };
197 let acks = get_i16(buf)?;
198 let timeout_ms = get_i32(buf)?;
199
200 let topic_count = get_array_len(buf, flex)?;
201 let mut topics = Vec::with_capacity(topic_count);
202 for topic_index in 0..topic_count {
203 let mut name = String::new();
205 if version <= 12 {
206 name = if flex {
207 get_compact_nullable_string_owned(buf)?.unwrap_or_default()
208 } else {
209 get_nullable_string_owned(buf)?.unwrap_or_default()
210 };
211 }
212 let mut topic_id = Uuid::ZERO;
213 if version >= 13 {
214 topic_id = get_uuid(buf)?;
215 }
216
217 let partition_count = get_array_len(buf, flex)?;
218 let mut partitions = Vec::with_capacity(partition_count);
219 for partition_index in 0..partition_count {
220 let partition = get_i32(buf)?;
221 let records = read_nullable_bytes_slice(buf, flex)?;
222 partitions.push(PartitionRecordSlice {
223 topic_index,
224 partition_index,
225 partition,
226 records,
227 });
228 if flex {
230 read_tagged_fields(buf, |_tag, _payload| Ok(false))?;
231 }
232 }
233 if flex {
235 read_tagged_fields(buf, |_tag, _payload| Ok(false))?;
236 }
237 topics.push(ProduceFramingTopic {
238 name,
239 topic_id,
240 partitions,
241 });
242 }
243 if flex {
245 read_tagged_fields(buf, |_tag, _payload| Ok(false))?;
246 }
247
248 Ok(ProduceFraming {
249 transactional_id,
250 acks,
251 timeout_ms,
252 topics,
253 })
254}
255
256fn read_nullable_bytes_slice(buf: &mut Bytes, flex: bool) -> Result<Option<Bytes>, ProtocolError> {
259 let len = if flex {
260 let raw = get_uvarint(buf)?;
261 if raw == 0 {
262 return Ok(None);
263 }
264 (raw - 1) as usize
265 } else {
266 let n = get_i32(buf)?;
267 if n < 0 {
268 return Ok(None);
269 }
270 usize::try_from(n).expect("non-negative i32 fits usize")
271 };
272 if buf.remaining() < len {
273 return Err(ProtocolError::UnexpectedEof {
274 needed: len - buf.remaining(),
275 });
276 }
277 Ok(Some(buf.copy_to_bytes(len)))
279}
280
281fn skip(buf: &mut Bytes, n: usize) -> Result<(), ProtocolError> {
282 if buf.remaining() < n {
283 return Err(ProtocolError::UnexpectedEof {
284 needed: n - buf.remaining(),
285 });
286 }
287 buf.advance(n);
288 Ok(())
289}
290
291#[cfg(test)]
292mod tests {
293
294 use bytes::BytesMut;
295
296 use super::*;
297 use crate::{
298 Encode,
299 owned::produce_request::{PartitionProduceData, ProduceRequest, TopicProduceData},
300 records::{Record, RecordBatch, RecordsPayload},
301 };
302
303 fn batch_with_value(v: &[u8], base_offset: i64) -> RecordBatch {
304 RecordBatch {
305 base_offset,
306 records: vec![Record {
307 value: Some(Bytes::copy_from_slice(v)),
308 ..Default::default()
309 }],
310 ..Default::default()
311 }
312 }
313
314 fn check_roundtrip(_case: &str, req: &ProduceRequest, version: i16) {
318 let mut buf = BytesMut::new();
319 req.encode(&mut buf, version).unwrap();
320 let body = buf.freeze();
321
322 let slices = produce_record_slices(body.clone(), version).unwrap();
323
324 let mut expected: Vec<Option<Bytes>> = Vec::new();
326 for t in &req.topic_data {
327 for p in &t.partition_data {
328 let enc = p.records.as_ref().map(|rp| {
329 let mut b = BytesMut::new();
330 <RecordsPayload as Encode>::encode(rp, &mut b, version).unwrap();
331 b.freeze()
332 });
333 expected.push(enc);
334 }
335 }
336 assert2::assert!(slices.len() == expected.len());
337 for (got, want) in slices.iter().zip(expected.iter()) {
338 match (&got.records, want) {
339 (Some(g), Some(w)) => {
340 assert2::assert!(&g[..] == &w[..]);
341 }
342 (None, None) => {}
343 _ => panic!("nullability mismatch"),
344 }
345 }
346 }
347
348 fn multi_partition_request(version: i16) -> ProduceRequest {
349 let topic = |name: &str, parts: usize| TopicProduceData {
350 name: if version <= 12 {
351 name.to_string()
352 } else {
353 String::new()
354 },
355 topic_id: if version >= 13 {
356 crate::primitives::uuid::Uuid([7u8; 16])
357 } else {
358 crate::primitives::uuid::Uuid::ZERO
359 },
360 partition_data: (0..parts)
361 .map(|i| PartitionProduceData {
362 index: i32::try_from(i).unwrap(),
363 records: Some(RecordsPayload::V2(vec![batch_with_value(
364 format!("topic-{name}-part-{i}").as_bytes(),
365 i64::try_from(i).unwrap(),
366 )])),
367 ..Default::default()
368 })
369 .collect(),
370 ..Default::default()
371 };
372 ProduceRequest {
373 transactional_id: None,
374 acks: -1,
375 timeout_ms: 1000,
376 topic_data: vec![topic("alpha", 2), topic("beta", 1)],
377 ..Default::default()
378 }
379 }
380
381 #[test]
382 fn captures_match_decoder_encoding_cases() {
383 for (case, versions) in [("non-flexible", 3..=8), ("flexible", 9..=13)] {
384 for version in versions {
385 check_roundtrip(case, &multi_partition_request(version), version);
386 }
387 }
388 }
389
390 #[test]
391 fn handles_null_records_field() {
392 let version = 9;
393 let req = ProduceRequest {
394 transactional_id: Some("txn".to_string()),
395 acks: 1,
396 timeout_ms: 0,
397 topic_data: vec![TopicProduceData {
398 name: "t".to_string(),
399 partition_data: vec![PartitionProduceData {
400 index: 0,
401 records: None,
402 ..Default::default()
403 }],
404 ..Default::default()
405 }],
406 ..Default::default()
407 };
408 let mut buf = BytesMut::new();
409 req.encode(&mut buf, version).unwrap();
410 let slices = produce_record_slices(buf.freeze(), version).unwrap();
411 assert2::assert!(slices.len() == 1);
412 assert2::assert!(slices[0].records.is_none());
413 }
414
415 #[test]
416 fn captured_slice_is_zero_copy_view() {
417 let version = 9;
418 let req = multi_partition_request(version);
419 let mut buf = BytesMut::new();
420 req.encode(&mut buf, version).unwrap();
421 let body = buf.freeze();
422 let body_start = body.as_ptr() as usize;
423 let body_end = body_start + body.len();
424
425 let slices = produce_record_slices(body.clone(), version).unwrap();
426 let first = slices[0].records.as_ref().unwrap();
427 let ptr = first.as_ptr() as usize;
428 assert2::assert!(ptr >= body_start && ptr < body_end);
429 }
430
431 fn check_framing_roundtrip(req: &ProduceRequest, version: i16) {
436 let mut buf = BytesMut::new();
437 req.encode(&mut buf, version).unwrap();
438 let body = buf.freeze();
439
440 let framing = produce_framing(body.clone(), version).unwrap();
441
442 let expected = ProduceFraming {
443 transactional_id: req.transactional_id.clone(),
444 acks: req.acks,
445 timeout_ms: req.timeout_ms,
446 topics: req
447 .topic_data
448 .iter()
449 .enumerate()
450 .map(|(topic_index, topic)| ProduceFramingTopic {
451 name: topic.name.clone(),
452 topic_id: topic.topic_id,
453 partitions: topic
454 .partition_data
455 .iter()
456 .enumerate()
457 .map(|(partition_index, partition)| PartitionRecordSlice {
458 topic_index,
459 partition_index,
460 partition: partition.index,
461 records: partition.records.as_ref().map(|payload| {
462 let mut b = BytesMut::new();
463 <RecordsPayload as Encode>::encode(payload, &mut b, version)
464 .unwrap();
465 b.freeze()
466 }),
467 })
468 .collect(),
469 })
470 .collect(),
471 };
472 assert2::assert!(framing == expected);
473 }
474
475 #[test]
476 fn framing_matches_decoder_all_versions() {
477 for version in 3..=13 {
478 check_framing_roundtrip(&multi_partition_request(version), version);
479 }
480 }
481
482 #[test]
483 fn framing_captures_transactional_id_and_acks() {
484 let version = 9;
485 let req = ProduceRequest {
486 transactional_id: Some("my-txn".to_string()),
487 acks: -1,
488 timeout_ms: 7777,
489 topic_data: vec![TopicProduceData {
490 name: "t".to_string(),
491 partition_data: vec![PartitionProduceData {
492 index: 3,
493 records: None,
494 ..Default::default()
495 }],
496 ..Default::default()
497 }],
498 ..Default::default()
499 };
500 let mut buf = BytesMut::new();
501 req.encode(&mut buf, version).unwrap();
502 let framing = produce_framing(buf.freeze(), version).unwrap();
503 let expected = ProduceFraming {
504 transactional_id: Some("my-txn".to_string()),
505 acks: -1,
506 timeout_ms: 7777,
507 topics: vec![ProduceFramingTopic {
508 name: "t".to_string(),
509 topic_id: Uuid::ZERO,
510 partitions: vec![PartitionRecordSlice {
511 topic_index: 0,
512 partition_index: 0,
513 partition: 3,
514 records: None,
515 }],
516 }],
517 };
518 assert2::assert!(framing == expected);
519 }
520
521 #[test]
522 fn empty_topic_list_yields_no_slices() {
523 let version = 9;
524 let req = ProduceRequest {
525 acks: 1,
526 timeout_ms: 0,
527 topic_data: vec![],
528 ..Default::default()
529 };
530 let mut buf = BytesMut::new();
531 req.encode(&mut buf, version).unwrap();
532 let slices = produce_record_slices(buf.freeze(), version).unwrap();
533 assert2::assert!(slices.is_empty());
534 }
535}