1use anyhow::{bail, ensure, Context, Result};
6use prost::Message;
7use std::time::Duration;
8use tokio::io::{AsyncRead, AsyncReadExt};
9
10pub const MAX_FRAME: usize = 5 * 1024 * 1024;
13pub const MAX_COMMAND: usize = 64 * 1024;
15pub const MAX_BATCH: usize = 1000;
17pub const IO_TIMEOUT: Duration = Duration::from_secs(30);
18pub const PROTOCOL_VERSION: i32 = 19;
21const MAGIC_CRC32C: u16 = 0x0e01;
22
23pub mod command_type {
24 pub const CONNECT: i32 = 2;
25 pub const CONNECTED: i32 = 3;
26 pub const SUBSCRIBE: i32 = 4;
27 pub const PRODUCER: i32 = 5;
28 pub const SEND: i32 = 6;
29 pub const SEND_RECEIPT: i32 = 7;
30 pub const SEND_ERROR: i32 = 8;
31 pub const MESSAGE: i32 = 9;
32 pub const ACK: i32 = 10;
33 pub const FLOW: i32 = 11;
34 pub const UNSUBSCRIBE: i32 = 12;
35 pub const SUCCESS: i32 = 13;
36 pub const ERROR: i32 = 14;
37 pub const CLOSE_PRODUCER: i32 = 15;
38 pub const CLOSE_CONSUMER: i32 = 16;
39 pub const PRODUCER_SUCCESS: i32 = 17;
40 pub const PING: i32 = 18;
41 pub const PONG: i32 = 19;
42 pub const REDELIVER_UNACKNOWLEDGED_MESSAGES: i32 = 20;
43 pub const PARTITIONED_METADATA: i32 = 21;
44 pub const PARTITIONED_METADATA_RESPONSE: i32 = 22;
45 pub const LOOKUP: i32 = 23;
46 pub const LOOKUP_RESPONSE: i32 = 24;
47 pub const GET_LAST_MESSAGE_ID: i32 = 29;
48 pub const GET_LAST_MESSAGE_ID_RESPONSE: i32 = 30;
49 pub const GET_SCHEMA: i32 = 34;
50 pub const GET_SCHEMA_RESPONSE: i32 = 35;
51}
52
53pub mod server_error {
55 pub const UNKNOWN_ERROR: i32 = 0;
56 pub const AUTHORIZATION_ERROR: i32 = 4;
57 pub const SERVICE_NOT_READY: i32 = 6;
58 pub const CHECKSUM_ERROR: i32 = 9;
59 pub const UNSUPPORTED_VERSION_ERROR: i32 = 10;
60 pub const TOPIC_NOT_FOUND: i32 = 11;
61 pub const SUBSCRIPTION_NOT_FOUND: i32 = 12;
62 pub const CONSUMER_NOT_FOUND: i32 = 13;
63 pub const TOO_MANY_REQUESTS: i32 = 14;
64 pub const CONSUMER_BUSY: i32 = 5;
65 pub const INVALID_TOPIC_NAME: i32 = 17;
66 pub const NOT_ALLOWED_ERROR: i32 = 22;
67}
68
69pub const SUB_TYPES: [&str; 4] = ["Exclusive", "Shared", "Failover", "Key_Shared"];
70
71#[derive(Clone, PartialEq, Message)]
72pub struct KeyValue {
73 #[prost(string, required, tag = "1")]
74 pub key: String,
75 #[prost(string, required, tag = "2")]
76 pub value: String,
77}
78
79#[derive(Clone, PartialEq, Message)]
80pub struct MessageIdData {
81 #[prost(uint64, required, tag = "1")]
82 pub ledger_id: u64,
83 #[prost(uint64, required, tag = "2")]
84 pub entry_id: u64,
85 #[prost(int32, optional, tag = "3")]
86 pub partition: Option<i32>,
87 #[prost(int32, optional, tag = "4")]
88 pub batch_index: Option<i32>,
89}
90
91#[derive(Clone, PartialEq, Message)]
92pub struct MessageMetadata {
93 #[prost(string, required, tag = "1")]
94 pub producer_name: String,
95 #[prost(uint64, required, tag = "2")]
96 pub sequence_id: u64,
97 #[prost(uint64, required, tag = "3")]
98 pub publish_time: u64,
99 #[prost(message, repeated, tag = "4")]
100 pub properties: Vec<KeyValue>,
101 #[prost(string, optional, tag = "6")]
102 pub partition_key: Option<String>,
103 #[prost(int32, optional, tag = "8")]
104 pub compression: Option<i32>,
105 #[prost(uint32, optional, tag = "9")]
106 pub uncompressed_size: Option<u32>,
107 #[prost(int32, optional, tag = "11")]
108 pub num_messages_in_batch: Option<i32>,
109 #[prost(uint64, optional, tag = "12")]
110 pub event_time: Option<u64>,
111 #[prost(string, optional, tag = "14")]
112 pub encryption_algo: Option<String>,
113 #[prost(bool, optional, tag = "25")]
114 pub null_value: Option<bool>,
115 #[prost(int32, optional, tag = "27")]
116 pub num_chunks_from_msg: Option<i32>,
117}
118
119#[derive(Clone, PartialEq, Message)]
120pub struct SingleMessageMetadata {
121 #[prost(message, repeated, tag = "1")]
122 pub properties: Vec<KeyValue>,
123 #[prost(string, optional, tag = "2")]
124 pub partition_key: Option<String>,
125 #[prost(int32, required, tag = "3")]
126 pub payload_size: i32,
127 #[prost(bool, optional, tag = "4")]
128 pub compacted_out: Option<bool>,
129 #[prost(uint64, optional, tag = "5")]
130 pub event_time: Option<u64>,
131 #[prost(uint64, optional, tag = "8")]
132 pub sequence_id: Option<u64>,
133 #[prost(bool, optional, tag = "9")]
134 pub null_value: Option<bool>,
135}
136
137#[derive(Clone, PartialEq, Message)]
138pub struct CommandConnect {
139 #[prost(string, required, tag = "1")]
140 pub client_version: String,
141 #[prost(bytes = "vec", optional, tag = "3")]
142 pub auth_data: Option<Vec<u8>>,
143 #[prost(int32, optional, tag = "4")]
144 pub protocol_version: Option<i32>,
145 #[prost(string, optional, tag = "5")]
146 pub auth_method_name: Option<String>,
147 #[prost(string, optional, tag = "6")]
148 pub proxy_to_broker_url: Option<String>,
149}
150
151#[derive(Clone, PartialEq, Message)]
152pub struct CommandConnected {
153 #[prost(string, required, tag = "1")]
154 pub server_version: String,
155 #[prost(int32, optional, tag = "2")]
156 pub protocol_version: Option<i32>,
157 #[prost(int32, optional, tag = "3")]
158 pub max_message_size: Option<i32>,
159}
160
161#[derive(Clone, PartialEq, Message)]
162pub struct CommandSubscribe {
163 #[prost(string, required, tag = "1")]
164 pub topic: String,
165 #[prost(string, required, tag = "2")]
166 pub subscription: String,
167 #[prost(int32, required, tag = "3")]
168 pub sub_type: i32,
169 #[prost(uint64, required, tag = "4")]
170 pub consumer_id: u64,
171 #[prost(uint64, required, tag = "5")]
172 pub request_id: u64,
173 #[prost(string, optional, tag = "6")]
174 pub consumer_name: Option<String>,
175 #[prost(bool, optional, tag = "8")]
176 pub durable: Option<bool>,
177 #[prost(int32, optional, tag = "13")]
178 pub initial_position: Option<i32>,
179}
180
181#[derive(Clone, PartialEq, Message)]
182pub struct CommandPartitionedTopicMetadata {
183 #[prost(string, required, tag = "1")]
184 pub topic: String,
185 #[prost(uint64, required, tag = "2")]
186 pub request_id: u64,
187}
188
189#[derive(Clone, PartialEq, Message)]
190pub struct CommandPartitionedTopicMetadataResponse {
191 #[prost(uint32, optional, tag = "1")]
192 pub partitions: Option<u32>,
193 #[prost(uint64, required, tag = "2")]
194 pub request_id: u64,
195 #[prost(int32, optional, tag = "3")]
196 pub response: Option<i32>,
197 #[prost(int32, optional, tag = "4")]
198 pub error: Option<i32>,
199 #[prost(string, optional, tag = "5")]
200 pub message: Option<String>,
201}
202
203#[derive(Clone, PartialEq, Message)]
204pub struct CommandLookupTopic {
205 #[prost(string, required, tag = "1")]
206 pub topic: String,
207 #[prost(uint64, required, tag = "2")]
208 pub request_id: u64,
209 #[prost(bool, optional, tag = "3")]
210 pub authoritative: Option<bool>,
211}
212
213#[derive(Clone, PartialEq, Message)]
214pub struct CommandLookupTopicResponse {
215 #[prost(string, optional, tag = "1")]
216 pub broker_service_url: Option<String>,
217 #[prost(int32, optional, tag = "3")]
218 pub response: Option<i32>,
219 #[prost(uint64, required, tag = "4")]
220 pub request_id: u64,
221 #[prost(bool, optional, tag = "5")]
222 pub authoritative: Option<bool>,
223 #[prost(int32, optional, tag = "6")]
224 pub error: Option<i32>,
225 #[prost(string, optional, tag = "7")]
226 pub message: Option<String>,
227 #[prost(bool, optional, tag = "8")]
228 pub proxy_through_service_url: Option<bool>,
229}
230
231#[derive(Clone, PartialEq, Message)]
232pub struct CommandProducer {
233 #[prost(string, required, tag = "1")]
234 pub topic: String,
235 #[prost(uint64, required, tag = "2")]
236 pub producer_id: u64,
237 #[prost(uint64, required, tag = "3")]
238 pub request_id: u64,
239 #[prost(string, optional, tag = "4")]
240 pub producer_name: Option<String>,
241 #[prost(bool, optional, tag = "5")]
242 pub encrypted: Option<bool>,
243 #[prost(int32, optional, tag = "10")]
244 pub producer_access_mode: Option<i32>,
245}
246
247#[derive(Clone, PartialEq, Message)]
248pub struct CommandSend {
249 #[prost(uint64, required, tag = "1")]
250 pub producer_id: u64,
251 #[prost(uint64, required, tag = "2")]
252 pub sequence_id: u64,
253 #[prost(int32, optional, tag = "3")]
254 pub num_messages: Option<i32>,
255 #[prost(uint64, optional, tag = "4")]
256 pub txnid_least_bits: Option<u64>,
257 #[prost(uint64, optional, tag = "6")]
258 pub highest_sequence_id: Option<u64>,
259 #[prost(bool, optional, tag = "7")]
260 pub is_chunk: Option<bool>,
261}
262
263#[derive(Clone, PartialEq, Message)]
264pub struct CommandSendReceipt {
265 #[prost(uint64, required, tag = "1")]
266 pub producer_id: u64,
267 #[prost(uint64, required, tag = "2")]
268 pub sequence_id: u64,
269 #[prost(message, optional, tag = "3")]
270 pub message_id: Option<MessageIdData>,
271 #[prost(uint64, optional, tag = "4")]
272 pub highest_sequence_id: Option<u64>,
273}
274
275#[derive(Clone, PartialEq, Message)]
276pub struct CommandSendError {
277 #[prost(uint64, required, tag = "1")]
278 pub producer_id: u64,
279 #[prost(uint64, required, tag = "2")]
280 pub sequence_id: u64,
281 #[prost(int32, required, tag = "3")]
282 pub error: i32,
283 #[prost(string, required, tag = "4")]
284 pub message: String,
285}
286
287#[derive(Clone, PartialEq, Message)]
288pub struct CommandMessage {
289 #[prost(uint64, required, tag = "1")]
290 pub consumer_id: u64,
291 #[prost(message, required, tag = "2")]
292 pub message_id: MessageIdData,
293 #[prost(uint32, optional, tag = "3")]
294 pub redelivery_count: Option<u32>,
295}
296
297#[derive(Clone, PartialEq, Message)]
298pub struct CommandAck {
299 #[prost(uint64, required, tag = "1")]
300 pub consumer_id: u64,
301 #[prost(int32, required, tag = "2")]
302 pub ack_type: i32,
303 #[prost(message, repeated, tag = "3")]
304 pub message_id: Vec<MessageIdData>,
305 #[prost(uint64, optional, tag = "8")]
306 pub request_id: Option<u64>,
307}
308
309#[derive(Clone, PartialEq, Message)]
310pub struct CommandAckResponse {
311 #[prost(uint64, required, tag = "1")]
312 pub consumer_id: u64,
313 #[prost(uint64, optional, tag = "6")]
314 pub request_id: Option<u64>,
315}
316
317#[derive(Clone, PartialEq, Message)]
318pub struct CommandFlow {
319 #[prost(uint64, required, tag = "1")]
320 pub consumer_id: u64,
321 #[prost(uint32, required, tag = "2")]
322 pub message_permits: u32,
323}
324
325#[derive(Clone, PartialEq, Message)]
326pub struct CommandUnsubscribe {
327 #[prost(uint64, required, tag = "1")]
328 pub consumer_id: u64,
329 #[prost(uint64, required, tag = "2")]
330 pub request_id: u64,
331}
332
333#[derive(Clone, PartialEq, Message)]
334pub struct CommandCloseProducer {
335 #[prost(uint64, required, tag = "1")]
336 pub producer_id: u64,
337 #[prost(uint64, required, tag = "2")]
338 pub request_id: u64,
339}
340
341#[derive(Clone, PartialEq, Message)]
342pub struct CommandCloseConsumer {
343 #[prost(uint64, required, tag = "1")]
344 pub consumer_id: u64,
345 #[prost(uint64, required, tag = "2")]
346 pub request_id: u64,
347}
348
349#[derive(Clone, PartialEq, Message)]
350pub struct CommandRedeliverUnacknowledgedMessages {
351 #[prost(uint64, required, tag = "1")]
352 pub consumer_id: u64,
353 #[prost(message, repeated, tag = "2")]
354 pub message_ids: Vec<MessageIdData>,
355}
356
357#[derive(Clone, PartialEq, Message)]
358pub struct CommandSuccess {
359 #[prost(uint64, required, tag = "1")]
360 pub request_id: u64,
361}
362
363#[derive(Clone, PartialEq, Message)]
364pub struct CommandProducerSuccess {
365 #[prost(uint64, required, tag = "1")]
366 pub request_id: u64,
367 #[prost(string, required, tag = "2")]
368 pub producer_name: String,
369 #[prost(int64, optional, tag = "3")]
370 pub last_sequence_id: Option<i64>,
371 #[prost(bytes = "vec", optional, tag = "4")]
373 pub schema_version: Option<Vec<u8>>,
374 #[prost(bool, optional, tag = "6")]
375 pub producer_ready: Option<bool>,
376}
377
378#[derive(Clone, PartialEq, Message)]
379pub struct CommandError {
380 #[prost(uint64, required, tag = "1")]
381 pub request_id: u64,
382 #[prost(int32, required, tag = "2")]
383 pub error: i32,
384 #[prost(string, required, tag = "3")]
385 pub message: String,
386}
387
388#[derive(Clone, PartialEq, Message)]
389pub struct CommandPing {}
390
391#[derive(Clone, PartialEq, Message)]
392pub struct CommandPong {}
393
394#[derive(Clone, PartialEq, Message)]
395pub struct CommandGetLastMessageId {
396 #[prost(uint64, required, tag = "1")]
397 pub consumer_id: u64,
398 #[prost(uint64, required, tag = "2")]
399 pub request_id: u64,
400}
401
402#[derive(Clone, PartialEq, Message)]
403pub struct CommandGetLastMessageIdResponse {
404 #[prost(message, required, tag = "1")]
405 pub last_message_id: MessageIdData,
406 #[prost(uint64, required, tag = "2")]
407 pub request_id: u64,
408}
409
410#[derive(Clone, PartialEq, Message)]
411pub struct CommandGetSchema {
412 #[prost(uint64, required, tag = "1")]
413 pub request_id: u64,
414 #[prost(string, required, tag = "2")]
415 pub topic: String,
416}
417
418#[derive(Clone, PartialEq, Message)]
419pub struct CommandGetSchemaResponse {
420 #[prost(uint64, required, tag = "1")]
421 pub request_id: u64,
422 #[prost(int32, optional, tag = "2")]
423 pub error_code: Option<i32>,
424 #[prost(string, optional, tag = "3")]
425 pub error_message: Option<String>,
426}
427
428#[derive(Clone, PartialEq, Message)]
429pub struct BaseCommand {
430 #[prost(int32, required, tag = "1")]
431 pub r#type: i32,
432 #[prost(message, optional, tag = "2")]
433 pub connect: Option<CommandConnect>,
434 #[prost(message, optional, tag = "3")]
435 pub connected: Option<CommandConnected>,
436 #[prost(message, optional, tag = "4")]
437 pub subscribe: Option<CommandSubscribe>,
438 #[prost(message, optional, tag = "5")]
439 pub producer: Option<CommandProducer>,
440 #[prost(message, optional, tag = "6")]
441 pub send: Option<CommandSend>,
442 #[prost(message, optional, tag = "7")]
443 pub send_receipt: Option<CommandSendReceipt>,
444 #[prost(message, optional, tag = "8")]
445 pub send_error: Option<CommandSendError>,
446 #[prost(message, optional, tag = "9")]
447 pub message: Option<CommandMessage>,
448 #[prost(message, optional, tag = "10")]
449 pub ack: Option<CommandAck>,
450 #[prost(message, optional, tag = "11")]
451 pub flow: Option<CommandFlow>,
452 #[prost(message, optional, tag = "12")]
453 pub unsubscribe: Option<CommandUnsubscribe>,
454 #[prost(message, optional, tag = "13")]
455 pub success: Option<CommandSuccess>,
456 #[prost(message, optional, tag = "14")]
457 pub error: Option<CommandError>,
458 #[prost(message, optional, tag = "15")]
459 pub close_producer: Option<CommandCloseProducer>,
460 #[prost(message, optional, tag = "16")]
461 pub close_consumer: Option<CommandCloseConsumer>,
462 #[prost(message, optional, tag = "17")]
463 pub producer_success: Option<CommandProducerSuccess>,
464 #[prost(message, optional, tag = "18")]
465 pub ping: Option<CommandPing>,
466 #[prost(message, optional, tag = "19")]
467 pub pong: Option<CommandPong>,
468 #[prost(message, optional, tag = "20")]
469 pub redeliver_unacknowledged_messages: Option<CommandRedeliverUnacknowledgedMessages>,
470 #[prost(message, optional, tag = "21")]
471 pub partition_metadata: Option<CommandPartitionedTopicMetadata>,
472 #[prost(message, optional, tag = "22")]
473 pub partition_metadata_response: Option<CommandPartitionedTopicMetadataResponse>,
474 #[prost(message, optional, tag = "23")]
475 pub lookup_topic: Option<CommandLookupTopic>,
476 #[prost(message, optional, tag = "24")]
477 pub lookup_topic_response: Option<CommandLookupTopicResponse>,
478 #[prost(message, optional, tag = "29")]
479 pub get_last_message_id: Option<CommandGetLastMessageId>,
480 #[prost(message, optional, tag = "30")]
481 pub get_last_message_id_response: Option<CommandGetLastMessageIdResponse>,
482 #[prost(message, optional, tag = "34")]
483 pub get_schema: Option<CommandGetSchema>,
484 #[prost(message, optional, tag = "35")]
485 pub get_schema_response: Option<CommandGetSchemaResponse>,
486 #[prost(message, optional, tag = "38")]
487 pub ack_response: Option<CommandAckResponse>,
488}
489
490impl BaseCommand {
491 pub fn of(kind: i32) -> Self {
492 BaseCommand {
493 r#type: kind,
494 ..Default::default()
495 }
496 }
497}
498
499pub fn crc32c(data: &[u8]) -> u32 {
501 const TABLE: [u32; 256] = {
502 let mut t = [0u32; 256];
503 let mut i = 0;
504 while i < 256 {
505 let mut c = i as u32;
506 let mut k = 0;
507 while k < 8 {
508 c = if c & 1 != 0 {
509 0x82F6_3B78 ^ (c >> 1)
510 } else {
511 c >> 1
512 };
513 k += 1;
514 }
515 t[i] = c;
516 i += 1;
517 }
518 t
519 };
520 let mut crc = !0u32;
521 for b in data {
522 crc = TABLE[((crc ^ u32::from(*b)) & 0xff) as usize] ^ (crc >> 8);
523 }
524 !crc
525}
526
527pub fn simple(cmd: &BaseCommand) -> Vec<u8> {
529 let body = cmd.encode_to_vec();
530 let mut out = Vec::with_capacity(8 + body.len());
531 out.extend_from_slice(&((4 + body.len()) as u32).to_be_bytes());
532 out.extend_from_slice(&(body.len() as u32).to_be_bytes());
533 out.extend_from_slice(&body);
534 out
535}
536
537pub fn with_payload(cmd: &BaseCommand, metadata: &MessageMetadata, payload: &[u8]) -> Vec<u8> {
539 let body = cmd.encode_to_vec();
540 let meta = metadata.encode_to_vec();
541 let mut checked = Vec::with_capacity(4 + meta.len() + payload.len());
542 checked.extend_from_slice(&(meta.len() as u32).to_be_bytes());
543 checked.extend_from_slice(&meta);
544 checked.extend_from_slice(payload);
545 let total = 4 + body.len() + 2 + 4 + checked.len();
546 let mut out = Vec::with_capacity(4 + total);
547 out.extend_from_slice(&(total as u32).to_be_bytes());
548 out.extend_from_slice(&(body.len() as u32).to_be_bytes());
549 out.extend_from_slice(&body);
550 out.extend_from_slice(&MAGIC_CRC32C.to_be_bytes());
551 out.extend_from_slice(&crc32c(&checked).to_be_bytes());
552 out.extend_from_slice(&checked);
553 out
554}
555
556pub struct Frame {
558 pub command: BaseCommand,
559 pub payload: Option<(MessageMetadata, Vec<u8>)>,
561}
562
563pub type Payload = Result<Option<(MessageMetadata, Vec<u8>)>, PayloadError>;
565
566#[derive(Debug)]
568pub enum PayloadError {
569 Checksum,
570 Malformed(String),
571}
572
573pub async fn read_frame<R: AsyncRead + Unpin>(
577 r: &mut R,
578 idle: Duration,
579) -> Result<Option<(BaseCommand, Payload)>> {
580 let mut size = [0u8; 4];
581 match tokio::time::timeout(idle, r.read_exact(&mut size)).await {
582 Err(_) => bail!("Pulsar peer idle"),
583 Ok(Err(e)) if e.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(None),
584 Ok(r) => {
585 r?;
586 }
587 }
588 let total = u32::from_be_bytes(size) as usize;
589 ensure!(
590 (4..=MAX_FRAME).contains(&total),
591 "Pulsar frame of {total} bytes (at most {MAX_FRAME})"
592 );
593 let mut frame = vec![0u8; total];
594 tokio::time::timeout(IO_TIMEOUT, r.read_exact(&mut frame))
595 .await
596 .context("Pulsar frame read deadline")??;
597 let cmd_len = u32::from_be_bytes(frame[0..4].try_into().unwrap_or_default()) as usize;
598 ensure!(
599 cmd_len <= MAX_COMMAND && 4 + cmd_len <= total,
600 "Pulsar command of {cmd_len} bytes does not fit"
601 );
602 let command = BaseCommand::decode(&frame[4..4 + cmd_len])
603 .context("Pulsar command is not a BaseCommand")?;
604 let rest = &frame[4 + cmd_len..];
605 if rest.is_empty() {
606 return Ok(Some((command, Ok(None))));
607 }
608 Ok(Some((command, parse_payload(rest))))
609}
610
611fn parse_payload(rest: &[u8]) -> Payload {
612 let bad = |m: &str| PayloadError::Malformed(m.to_string());
613 let checked = if rest.len() >= 6 && u16::from_be_bytes([rest[0], rest[1]]) == MAGIC_CRC32C {
614 let want = u32::from_be_bytes([rest[2], rest[3], rest[4], rest[5]]);
615 let checked = &rest[6..];
616 if crc32c(checked) != want {
617 return Err(PayloadError::Checksum);
618 }
619 checked
620 } else {
621 rest
622 };
623 if checked.len() < 4 {
624 return Err(bad("payload section too short"));
625 }
626 let meta_len = u32::from_be_bytes([checked[0], checked[1], checked[2], checked[3]]) as usize;
627 if 4 + meta_len > checked.len() {
628 return Err(bad("metadata size past the frame"));
629 }
630 let metadata = MessageMetadata::decode(&checked[4..4 + meta_len])
631 .map_err(|e| bad(&format!("metadata: {e}")))?;
632 Ok(Some((metadata, checked[4 + meta_len..].to_vec())))
633}
634
635#[derive(Clone, Debug)]
637pub struct Entry {
638 pub properties: Vec<KeyValue>,
639 pub key: Option<String>,
640 pub event_time: Option<u64>,
641 pub sequence_id: u64,
642 pub payload: Vec<u8>,
643}
644
645pub fn entries(meta: &MessageMetadata, payload: &[u8]) -> Result<Vec<Entry>> {
648 ensure!(
649 meta.compression.unwrap_or(0) == 0,
650 "compressed payloads are not supported"
651 );
652 ensure!(
653 meta.encryption_algo.is_none(),
654 "encrypted payloads are not supported"
655 );
656 ensure!(
657 meta.num_chunks_from_msg.unwrap_or(1) <= 1,
658 "chunked messages are not supported"
659 );
660 let Some(n) = meta.num_messages_in_batch.filter(|_| batched(meta)) else {
661 return Ok(vec![Entry {
662 properties: meta.properties.clone(),
663 key: meta.partition_key.clone(),
664 event_time: meta.event_time.filter(|t| *t > 0),
665 sequence_id: meta.sequence_id,
666 payload: payload.to_vec(),
667 }]);
668 };
669 ensure!(
670 (1..=MAX_BATCH as i32).contains(&n),
671 "a batch of {n} messages"
672 );
673 let mut out = Vec::new();
674 let mut rest = payload;
675 for i in 0..n as u64 {
676 ensure!(rest.len() >= 4, "batch entry {i} is cut short");
677 let len = u32::from_be_bytes([rest[0], rest[1], rest[2], rest[3]]) as usize;
678 ensure!(
679 4 + len <= rest.len(),
680 "batch entry {i} metadata past the payload"
681 );
682 let single =
683 SingleMessageMetadata::decode(&rest[4..4 + len]).context("batch entry metadata")?;
684 rest = &rest[4 + len..];
685 let size = usize::try_from(single.payload_size).context("negative payload size")?;
686 ensure!(
687 size <= rest.len(),
688 "batch entry {i} payload past the payload"
689 );
690 let (body, tail) = rest.split_at(size);
691 rest = tail;
692 if single.compacted_out == Some(true) {
693 continue;
694 }
695 out.push(Entry {
696 properties: single.properties,
697 key: single.partition_key,
698 event_time: single.event_time.filter(|t| *t > 0),
699 sequence_id: single.sequence_id.unwrap_or(meta.sequence_id + i),
700 payload: body.to_vec(),
701 });
702 }
703 Ok(out)
704}
705
706fn batched(meta: &MessageMetadata) -> bool {
708 meta.num_messages_in_batch.is_some()
709}
710
711pub fn full_topic(t: &str) -> Result<String> {
713 ensure!(
714 !t.is_empty() && t.len() <= 512,
715 "topic name must be 1-512 bytes"
716 );
717 if t.starts_with("persistent://") || t.starts_with("non-persistent://") {
718 let rest = t.split_once("://").map(|(_, r)| r).unwrap_or_default();
719 ensure!(
720 rest.split('/').count() == 3 && rest.split('/').all(|p| !p.is_empty()),
721 "topic must be tenant/namespace/name"
722 );
723 return Ok(t.to_string());
724 }
725 ensure!(!t.contains("://"), "unknown topic domain");
726 Ok(match t.split('/').count() {
727 1 => format!("persistent://public/default/{t}"),
728 3 => format!("persistent://{t}"),
729 _ => bail!("topic must be a name or tenant/namespace/name"),
730 })
731}
732
733pub fn show(payload: &[u8]) -> (String, &'static str) {
735 match std::str::from_utf8(payload) {
736 Ok(s)
737 if !s
738 .chars()
739 .any(|c| c.is_control() && !matches!(c, '\n' | '\r' | '\t')) =>
740 {
741 (s.to_string(), "utf8")
742 }
743 _ => (hex::encode(payload), "hex"),
744 }
745}
746
747pub fn bytes(payload: &str, encoding: Option<&str>) -> Result<Vec<u8>> {
749 match encoding.unwrap_or("utf8") {
750 "utf8" => Ok(payload.as_bytes().to_vec()),
751 "hex" => hex::decode(payload).context("payload is not hex"),
752 other => bail!("encoding must be utf8 or hex, not {other}"),
753 }
754}
755
756pub fn now_millis() -> u64 {
757 crate::utils::clock::SystemTime::now()
758 .duration_since(crate::utils::clock::UNIX_EPOCH)
759 .map(|d| d.as_millis() as u64)
760 .unwrap_or(0)
761}