1use crate::error::{Error, ensure};
2
3#[derive(Clone, Debug, PartialEq, Eq)]
4pub struct Topics {
5 pub wire: String,
6 pub hashtags: Vec<String>,
7}
8
9impl Topics {
10 pub const MAX_LABEL_BYTES: usize = 32;
11 pub const MAX_HASHTAGS: usize = 8;
12
13 pub fn validate(&self) -> Result<(), Error> {
14 if !self.wire.is_empty() {
15 Self::validate_label(&self.wire)?;
16 }
17 ensure!(
18 self.hashtags.len() <= Self::MAX_HASHTAGS,
19 "at most 8 hashtags"
20 );
21 for tag in &self.hashtags {
22 Self::validate_label(tag)?;
23 }
24 ensure!(
25 self.hashtags.windows(2).all(|pair| pair[0] < pair[1]),
26 "hashtags must be unique and sorted"
27 );
28 Ok(())
29 }
30
31 pub fn validate_label(label: &str) -> Result<(), Error> {
32 let bytes = label.as_bytes();
33 ensure!(
34 (1..=Self::MAX_LABEL_BYTES).contains(&bytes.len()),
35 "label must be 1..32 bytes"
36 );
37 ensure!(
38 bytes[0].is_ascii_lowercase() || bytes[0].is_ascii_digit(),
39 "label must start with a lowercase letter or digit"
40 );
41 ensure!(
42 bytes.iter().all(|byte| byte.is_ascii_lowercase()
43 || byte.is_ascii_digit()
44 || *byte == b'-'
45 || *byte == b'_'),
46 "label permits only lowercase ASCII letters, digits, - and _"
47 );
48 Ok(())
49 }
50
51 pub fn encode_body(&self, text: &str) -> Result<Vec<u8>, Error> {
52 self.validate()?;
53 let mut bytes = Vec::new();
54 bytes.push(u8::try_from(self.wire.len())?);
55 bytes.extend_from_slice(self.wire.as_bytes());
56 bytes.push(u8::try_from(self.hashtags.len())?);
57 for tag in &self.hashtags {
58 bytes.push(u8::try_from(tag.len())?);
59 bytes.extend_from_slice(tag.as_bytes());
60 }
61 bytes.extend_from_slice(&u16::try_from(text.len())?.to_le_bytes());
62 bytes.extend_from_slice(text.as_bytes());
63 Ok(bytes)
64 }
65
66 pub fn decode_body(mut bytes: &[u8]) -> Result<(Self, String), Error> {
67 let wire_length = usize::from(take(&mut bytes, 1)?[0]);
68 ensure!(
69 wire_length <= Self::MAX_LABEL_BYTES,
70 "wire exceeds 32 bytes"
71 );
72 let wire = std::str::from_utf8(take(&mut bytes, wire_length)?)?.to_owned();
73 let count = usize::from(take(&mut bytes, 1)?[0]);
74 ensure!(count <= Self::MAX_HASHTAGS, "at most 8 hashtags");
75 let mut hashtags = Vec::with_capacity(count);
76 for _ in 0..count {
77 hashtags.push(read_label(&mut bytes)?);
78 }
79 let length = usize::from(u16::from_le_bytes(take(&mut bytes, 2)?.try_into()?));
80 let text = std::str::from_utf8(take(&mut bytes, length)?)?.to_owned();
81 ensure!(bytes.is_empty(), "trailing structured record bytes");
82 let topics = Self { wire, hashtags };
83 topics.validate()?;
84 Ok((topics, text))
85 }
86}
87
88fn take<'a>(bytes: &mut &'a [u8], length: usize) -> Result<&'a [u8], Error> {
89 ensure!(bytes.len() >= length, "truncated structured record");
90 let (head, tail) = bytes.split_at(length);
91 *bytes = tail;
92 Ok(head)
93}
94
95fn read_label(bytes: &mut &[u8]) -> Result<String, Error> {
96 let length = usize::from(take(bytes, 1)?[0]);
97 ensure!(
98 (1..=Topics::MAX_LABEL_BYTES).contains(&length),
99 "label must be 1..32 bytes"
100 );
101 Ok(std::str::from_utf8(take(bytes, length)?)?.to_owned())
102}