Skip to main content

urma_core/
topics.rs

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}