1use bytes::{Bytes, BytesMut};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum AutoOffsetReset {
11 Earliest,
15 Latest,
18 None,
23 ByDuration(std::time::Duration),
27}
28
29impl std::str::FromStr for AutoOffsetReset {
30 type Err = String;
31
32 fn from_str(value: &str) -> Result<Self, Self::Err> {
36 match value {
37 "earliest" => Ok(Self::Earliest),
38 "latest" => Ok(Self::Latest),
39 "none" => Ok(Self::None),
40 "by_duration" => {
41 Err("<:duration> part is missing in by_duration auto offset reset strategy.".into())
42 }
43 _ => match value.strip_prefix("by_duration:") {
44 Some(duration) => parse_iso_duration(duration).map(Self::ByDuration),
45 None => Err(format!("Unknown auto offset reset strategy: {value}")),
46 },
47 }
48 }
49}
50
51fn duration_component(part: &str, unit: char) -> (Option<&str>, &str) {
53 match part.find(unit) {
54 Some(end) => (Some(&part[..end]), &part[end + 1..]),
55 None => (None, part),
56 }
57}
58
59fn duration_integer(number: &str) -> Option<i128> {
62 let digits = number.strip_prefix(['-', '+']).unwrap_or(number);
63 if digits.is_empty() || !digits.bytes().all(|byte| byte.is_ascii_digit()) {
64 return None;
65 }
66 number.parse::<i64>().ok().map(i128::from)
67}
68
69fn add_duration_part(nanos: i128, value: i128, unit_seconds: i128) -> Option<i128> {
72 const NANOS_PER_SECOND: i128 = 1_000_000_000;
73 value
74 .checked_mul(unit_seconds)?
75 .checked_mul(NANOS_PER_SECOND)
76 .and_then(|part| nanos.checked_add(part))
77}
78
79fn parse_iso_duration(text: &str) -> Result<std::time::Duration, String> {
83 const NANOS_PER_SECOND: i128 = 1_000_000_000;
84 let invalid =
85 || "Unable to parse duration string in by_duration offset reset strategy.".to_owned();
86 let upper = text.to_ascii_uppercase();
87 let (negative, rest) = match upper.as_bytes().first() {
88 Some(b'-') => (true, &upper[1..]),
89 Some(b'+') => (false, &upper[1..]),
90 _ => (false, upper.as_str()),
91 };
92 let rest = rest.strip_prefix('P').ok_or_else(invalid)?;
93 let (date, time) = match rest.split_once('T') {
94 Some((date, time)) => (date, Some(time)),
95 None => (rest, None),
96 };
97 let mut nanos: i128 = 0;
98 let mut components = 0;
99 let (days, date_rest) = duration_component(date, 'D');
100 if !date_rest.is_empty() {
101 return Err(invalid());
102 }
103 if let Some(days) = days {
104 nanos = add_duration_part(nanos, duration_integer(days).ok_or_else(invalid)?, 86_400)
105 .ok_or_else(invalid)?;
106 components += 1;
107 }
108 if let Some(time) = time {
109 let (hours, time) = duration_component(time, 'H');
110 let (minutes, time) = duration_component(time, 'M');
111 let (seconds, time) = duration_component(time, 'S');
112 if !time.is_empty() || (hours.is_none() && minutes.is_none() && seconds.is_none()) {
113 return Err(invalid());
114 }
115 if let Some(hours) = hours {
116 nanos = add_duration_part(nanos, duration_integer(hours).ok_or_else(invalid)?, 3_600)
117 .ok_or_else(invalid)?;
118 }
119 if let Some(minutes) = minutes {
120 nanos = add_duration_part(nanos, duration_integer(minutes).ok_or_else(invalid)?, 60)
121 .ok_or_else(invalid)?;
122 }
123 if let Some(seconds) = seconds {
124 let (whole, fraction) = match seconds.split_once(['.', ',']) {
125 Some((whole, fraction)) => (whole, fraction),
126 None => (seconds, ""),
127 };
128 if fraction.len() > 9 || !fraction.bytes().all(|byte| byte.is_ascii_digit()) {
129 return Err(invalid());
130 }
131 let whole_value = duration_integer(whole).ok_or_else(invalid)?;
132 let fraction_value = if fraction.is_empty() {
133 0
134 } else {
135 format!("{fraction:0<9}")
136 .parse::<i128>()
137 .map_err(|_| invalid())?
138 };
139 let sign = if whole.starts_with('-') { -1 } else { 1 };
140 nanos = add_duration_part(nanos, whole_value, 1)
141 .and_then(|nanos| nanos.checked_add(sign * fraction_value))
142 .ok_or_else(invalid)?;
143 }
144 components += 1;
145 }
146 if components == 0 {
147 return Err(invalid());
148 }
149 if negative {
150 nanos = -nanos;
151 }
152 if nanos < 0 {
153 return Err(
154 "Negative duration is not supported in by_duration offset reset strategy.".into(),
155 );
156 }
157 let seconds = i64::try_from(nanos / NANOS_PER_SECOND).map_err(|_| invalid())?;
159 let seconds = u64::try_from(seconds).map_err(|_| invalid())?;
160 let subsec = u32::try_from(nanos % NANOS_PER_SECOND).map_err(|_| invalid())?;
161 Ok(std::time::Duration::new(seconds, subsec))
162}
163
164#[derive(Debug, Clone, Copy, PartialEq, Eq)]
172pub enum IsolationLevel {
173 ReadUncommitted,
176 ReadCommitted,
179}
180
181impl std::str::FromStr for IsolationLevel {
182 type Err = String;
183
184 fn from_str(value: &str) -> Result<Self, Self::Err> {
185 match value {
188 "read_uncommitted" => Ok(Self::ReadUncommitted),
189 "read_committed" => Ok(Self::ReadCommitted),
190 _ => Err(format!("invalid isolation level: {value}")),
191 }
192 }
193}
194
195impl IsolationLevel {
196 pub(crate) fn wire(self) -> i8 {
198 match self {
199 IsolationLevel::ReadUncommitted => 0,
200 IsolationLevel::ReadCommitted => 1,
201 }
202 }
203}
204
205use krabka_protocol::{
208 Decode, Encode, UnknownTaggedFields,
209 owned::{
210 consumer_protocol_assignment::{
211 ConsumerProtocolAssignment, TopicPartition as AssignTopicPartition,
212 },
213 consumer_protocol_subscription::{
214 ConsumerProtocolSubscription, TopicPartition as SubTopicPartition,
215 },
216 },
217};
218
219const SUBSCRIPTION_WIRE_VERSION: i16 = 3;
220const ASSIGNMENT_WIRE_VERSION: i16 = 3;
221
222#[derive(Debug, PartialEq)]
223pub(crate) struct DecodedSubscription {
224 pub topics: Vec<String>,
225 pub owned: Vec<(String, i32)>,
226 pub generation_id: i32,
227 pub rack_id: Option<String>,
230 pub user_data: Option<Bytes>,
232}
233
234fn group_by_topic(pairs: &[(String, i32)]) -> std::collections::BTreeMap<&str, Vec<i32>> {
235 let mut by_topic: std::collections::BTreeMap<&str, Vec<i32>> =
236 std::collections::BTreeMap::new();
237 for (t, p) in pairs {
238 by_topic.entry(t.as_str()).or_default().push(*p);
239 }
240 by_topic
241}
242
243fn peek_version(bytes: &[u8]) -> i16 {
244 if bytes.len() < 2 {
245 return 0;
246 }
247 i16::from_be_bytes([bytes[0], bytes[1]])
248}
249
250fn empty_subscription() -> DecodedSubscription {
251 DecodedSubscription {
252 topics: Vec::new(),
253 owned: Vec::new(),
254 generation_id: -1,
255 rack_id: None,
256 user_data: None,
257 }
258}
259
260pub(crate) fn encode_subscription(
264 topics: &[String],
265 owned: &[(String, i32)],
266 generation_id: i32,
267 rack_id: Option<&str>,
268 user_data: Option<Bytes>,
269) -> Bytes {
270 use bytes::BufMut;
271 let owned_partitions: Vec<SubTopicPartition> = group_by_topic(owned)
272 .into_iter()
273 .map(|(topic, mut partitions)| {
274 partitions.sort_unstable();
275 SubTopicPartition {
276 topic: topic.to_string(),
277 partitions,
278 unknown_tagged_fields: UnknownTaggedFields::default(),
279 }
280 })
281 .collect();
282 let mut sorted_topics = topics.to_vec();
283 sorted_topics.sort();
284 let msg = ConsumerProtocolSubscription {
285 topics: sorted_topics,
286 user_data,
287 owned_partitions,
288 generation_id,
289 rack_id: rack_id.map(str::to_string),
290 unknown_tagged_fields: UnknownTaggedFields::default(),
291 };
292 let mut buf = BytesMut::with_capacity(2 + msg.encoded_len(SUBSCRIPTION_WIRE_VERSION));
293 buf.put_i16(SUBSCRIPTION_WIRE_VERSION);
294 msg.encode(&mut buf, SUBSCRIPTION_WIRE_VERSION)
295 .expect("ConsumerProtocolSubscription encode");
296 buf.freeze()
297}
298
299pub(crate) fn decode_subscription(bytes: &[u8]) -> DecodedSubscription {
300 let Some(payload) = bytes.get(2..) else {
301 return empty_subscription();
302 };
303 let version = peek_version(bytes).clamp(0, SUBSCRIPTION_WIRE_VERSION);
304 let mut cur = payload;
305 let Ok(msg) = ConsumerProtocolSubscription::decode(&mut cur, version) else {
306 return empty_subscription();
307 };
308 let mut owned = Vec::new();
309 for tp in msg.owned_partitions {
310 for p in tp.partitions {
311 owned.push((tp.topic.clone(), p));
312 }
313 }
314 DecodedSubscription {
315 topics: msg.topics,
316 owned,
317 generation_id: msg.generation_id,
318 rack_id: msg.rack_id,
319 user_data: msg.user_data,
320 }
321}
322
323pub(crate) fn encode_assignment(partitions: &[(String, i32)]) -> Bytes {
324 use bytes::BufMut;
325 let assigned_partitions: Vec<AssignTopicPartition> = group_by_topic(partitions)
326 .into_iter()
327 .map(|(topic, partitions)| AssignTopicPartition {
328 topic: topic.to_string(),
329 partitions,
330 unknown_tagged_fields: UnknownTaggedFields::default(),
331 })
332 .collect();
333 let msg = ConsumerProtocolAssignment {
334 assigned_partitions,
335 user_data: None,
336 unknown_tagged_fields: UnknownTaggedFields::default(),
337 };
338 let mut buf = BytesMut::with_capacity(2 + msg.encoded_len(ASSIGNMENT_WIRE_VERSION));
339 buf.put_i16(ASSIGNMENT_WIRE_VERSION);
340 msg.encode(&mut buf, ASSIGNMENT_WIRE_VERSION)
341 .expect("ConsumerProtocolAssignment encode");
342 buf.freeze()
343}
344
345pub(crate) fn decode_assignment(bytes: &[u8]) -> Vec<(String, i32)> {
346 let Some(payload) = bytes.get(2..) else {
347 return Vec::new();
348 };
349 let version = peek_version(bytes).clamp(0, ASSIGNMENT_WIRE_VERSION);
350 let mut cur = payload;
351 let Ok(msg) = ConsumerProtocolAssignment::decode(&mut cur, version) else {
352 return Vec::new();
353 };
354 let mut out = Vec::new();
355 for tp in msg.assigned_partitions {
356 for p in tp.partitions {
357 out.push((tp.topic.clone(), p));
358 }
359 }
360 out
361}
362
363#[cfg(test)]
364mod tests {
365
366 use super::*;
367
368 #[test]
369 fn consumer_behavior_values_parse_kafka_spellings() {
370 use std::time::Duration;
371 let reset = |value: &str| value.parse::<AutoOffsetReset>();
372 let isolation = |value: &str| value.parse::<IsolationLevel>().map_err(|_| ());
373 let actual = (
374 [
375 reset("earliest"),
376 reset("latest"),
377 reset("none"),
378 reset("by_duration:PT1H"),
379 reset("by_duration:P1DT2H3M4.5S"),
380 reset("by_duration:pt0.000000001s"),
381 reset("by_duration:PT1H-30M"),
382 reset("by_duration:+P2D"),
383 reset("by_duration"),
384 reset("by_duration:-PT1H"),
385 reset("by_duration:PT"),
386 reset("by_duration:P"),
387 reset("by_duration:1H"),
388 reset("by_duration:PT1.0000000001S"),
389 reset("by_duration:P9223372036854775807D"),
390 reset("by_duration:PT9223372036854775808H"),
391 reset("by_duration:P106751991167300DT24H"),
392 reset("EARLIEST"),
393 reset("unknown"),
394 ],
395 [
396 isolation("read_committed"),
397 isolation("read_uncommitted"),
398 isolation("read-committed"),
399 isolation("READ_COMMITTED"),
400 ],
401 );
402 let unparsable =
403 Err("Unable to parse duration string in by_duration offset reset strategy.".to_owned());
404 let expected = (
405 [
406 Ok(AutoOffsetReset::Earliest),
407 Ok(AutoOffsetReset::Latest),
408 Ok(AutoOffsetReset::None),
409 Ok(AutoOffsetReset::ByDuration(Duration::from_hours(1))),
410 Ok(AutoOffsetReset::ByDuration(Duration::from_millis(
411 ((24 + 2) * 3600 + 3 * 60 + 4) * 1000 + 500,
412 ))),
413 Ok(AutoOffsetReset::ByDuration(Duration::from_nanos(1))),
414 Ok(AutoOffsetReset::ByDuration(Duration::from_mins(30))),
415 Ok(AutoOffsetReset::ByDuration(Duration::from_hours(48))),
416 Err(
417 "<:duration> part is missing in by_duration auto offset reset strategy."
418 .to_owned(),
419 ),
420 Err(
421 "Negative duration is not supported in by_duration offset reset strategy."
422 .to_owned(),
423 ),
424 unparsable.clone(),
425 unparsable.clone(),
426 unparsable.clone(),
427 unparsable.clone(),
428 unparsable.clone(),
429 unparsable.clone(),
430 unparsable,
431 Err("Unknown auto offset reset strategy: EARLIEST".to_owned()),
432 Err("Unknown auto offset reset strategy: unknown".to_owned()),
433 ],
434 [
435 Ok(IsolationLevel::ReadCommitted),
436 Ok(IsolationLevel::ReadUncommitted),
437 Err(()),
438 Err(()),
439 ],
440 );
441 assert2::assert!(actual == expected);
442 }
443
444 #[test]
445 fn isolation_level_wire_values_match_fetch_request_encoding() {
446 for (_name, isolation, expected) in [
447 ("read uncommitted", IsolationLevel::ReadUncommitted, 0),
448 ("read committed", IsolationLevel::ReadCommitted, 1),
449 ] {
450 assert2::assert!(isolation.wire() == expected);
451 }
452 }
453
454 #[test]
455 fn peek_version_requires_two_bytes() {
456 for (_name, bytes, want) in [
457 ("empty", &[][..], 0),
458 ("truncated", &[0x7f][..], 0),
459 ("two byte version", &[0, 3][..], 3),
460 ] {
461 assert2::assert!(peek_version(bytes) == want);
462 }
463 }
464
465 #[test]
466 fn decode_subscription_short_or_malformed_payload_uses_empty_fallback() {
467 for (_name, payload) in [
468 ("empty", &[][..]),
469 ("truncated version", &[0x7f][..]),
470 ("version only", &[0, 3][..]),
471 ] {
472 let decoded = decode_subscription(payload);
473 assert2::assert!(
474 decoded
475 == DecodedSubscription {
476 topics: Vec::new(),
477 owned: Vec::new(),
478 generation_id: -1,
479 rack_id: None,
480 user_data: None,
481 }
482 );
483 }
484 }
485
486 #[test]
487 fn decode_assignment_short_or_malformed_payload_uses_empty_fallback() {
488 for (_name, payload) in [
489 ("empty", &[][..]),
490 ("truncated version", &[0x7f][..]),
491 ("version only", &[0, 3][..]),
492 ] {
493 assert2::assert!(decode_assignment(payload).is_empty());
494 }
495 }
496
497 #[test]
500 fn subscription_sorts_topics_and_owned_partitions_and_keeps_user_data() {
501 let s = encode_subscription(
502 &["t2".into(), "t1".into()],
503 &[("t2".into(), 1), ("t1".into(), 3), ("t2".into(), 0)],
504 5,
505 None,
506 Some(Bytes::from_static(&[0, 0, 0, 5])),
507 );
508 assert2::assert!(
509 decode_subscription(&s)
510 == DecodedSubscription {
511 topics: vec!["t1".into(), "t2".into()],
512 owned: vec![("t1".into(), 3), ("t2".into(), 0), ("t2".into(), 1)],
513 generation_id: 5,
514 rack_id: None,
515 user_data: Some(Bytes::from_static(&[0, 0, 0, 5])),
516 }
517 );
518 }
519
520 #[test]
521 fn subscription_round_trip() {
522 let s = encode_subscription(&["t1".into(), "t2".into()], &[], -1, None, None);
523 let decoded = decode_subscription(&s);
524 assert2::assert!(decoded.topics == vec!["t1", "t2"]);
525 }
526
527 #[test]
528 fn subscription_empty_round_trip() {
529 let s = encode_subscription(&[], &[], -1, None, None);
530 let decoded = decode_subscription(&s);
531 assert2::assert!(
532 decoded
533 == DecodedSubscription {
534 topics: Vec::new(),
535 owned: Vec::new(),
536 generation_id: -1,
537 rack_id: None,
538 user_data: None,
539 }
540 );
541 }
542
543 #[test]
544 fn subscription_v3_owned_partitions_round_trip() {
545 let owned = vec![("t".into(), 0), ("t".into(), 1), ("u".into(), 0)];
546 let s = encode_subscription(&["t".into(), "u".into()], &owned, -1, None, None);
547 let decoded = decode_subscription(&s);
548 let mut got = decoded.owned.clone();
549 got.sort();
550 let mut want = owned.clone();
551 want.sort();
552 assert2::assert!(got == want);
553 }
554
555 #[test]
556 fn subscription_v3_generation_and_rack_round_trip() {
557 let s = encode_subscription(&["t".into()], &[], 42, Some("rack-a"), None);
558 let decoded = decode_subscription(&s);
559 assert2::assert!(
560 decoded
561 == DecodedSubscription {
562 topics: vec!["t".into()],
563 owned: Vec::new(),
564 generation_id: 42,
565 rack_id: Some("rack-a".into()),
566 user_data: None,
567 }
568 );
569 }
570
571 #[test]
572 fn subscription_decodes_v1_payload() {
573 use bytes::BufMut;
574 let mut buf = BytesMut::new();
575 buf.put_i16(1);
576 buf.put_i32(1);
577 let t = "t1";
578 buf.put_i16(i16::try_from(t.len()).unwrap());
579 buf.put_slice(t.as_bytes());
580 buf.put_i32(-1); buf.put_i32(0); let payload = buf.freeze();
583 let decoded = decode_subscription(&payload);
584 assert2::assert!(
585 decoded
586 == DecodedSubscription {
587 topics: vec!["t1".to_string()],
588 owned: Vec::new(),
589 generation_id: -1,
590 rack_id: None,
591 user_data: None,
592 }
593 );
594 }
595
596 #[test]
597 fn assignment_round_trip() {
598 let s = encode_assignment(&[("t".into(), 0), ("t".into(), 1), ("u".into(), 0)]);
599 let mut decoded = decode_assignment(&s);
600 decoded.sort();
601 assert2::assert!(
602 decoded
603 == vec![
604 ("t".to_string(), 0),
605 ("t".to_string(), 1),
606 ("u".to_string(), 0)
607 ]
608 );
609 }
610
611 #[test]
612 fn assignment_empty_round_trip() {
613 let s = encode_assignment(&[]);
614 let decoded = decode_assignment(&s);
615 assert2::assert!(decoded.is_empty());
616 }
617}