ecksport_core/
topic.rs

1use std::fmt;
2
3use borsh::{BorshDeserialize, BorshSerialize};
4
5/// Util to help copying arrs since we don't have the ability to use
6/// `.copy_from_slice` in const contexts.
7macro_rules! gen_byte_copy {
8    ($dest:ident, $src:ident, $idx:literal) => {
9        if $src.len() > $idx {
10            $dest[$idx] = $src[$idx];
11        }
12    };
13}
14
15pub const TOPIC_LEN: usize = 8;
16
17/// Inner topic 8 byte array.
18pub type TopicInner = [u8; TOPIC_LEN];
19
20/// Tag type to route messagse to the right handler.  Should be printable 0-terminated ASCII chars.
21#[derive(Copy, Clone, Hash, Eq, PartialEq, Ord, PartialOrd, BorshDeserialize, BorshSerialize)]
22pub struct Topic(TopicInner);
23
24impl Topic {
25    /// Wraps a raw array topic.
26    pub fn new(buf: TopicInner) -> Self {
27        Self(buf)
28    }
29
30    /// Parses a str if valid.
31    ///
32    /// # Panics
33    ///
34    /// If the str has a length greater than `TOPIC_LEN`.
35    pub fn from_str(s: &str) -> Self {
36        if s.as_bytes().len() > TOPIC_LEN {
37            panic!("malformed topic '{}'", s);
38        }
39
40        let mut buf = [0; TOPIC_LEN];
41        buf.copy_from_slice(s.as_bytes());
42
43        Self(buf)
44    }
45
46    pub const fn from_const_str(s: &'static str) -> Self {
47        let sb = s.as_bytes();
48
49        if sb.len() > TOPIC_LEN {
50            panic!("topic str too long");
51        }
52
53        let mut buf = [0u8; TOPIC_LEN];
54
55        // This is horrible because we don't want to require `const_for`.
56        gen_byte_copy!(buf, sb, 0);
57        gen_byte_copy!(buf, sb, 1);
58        gen_byte_copy!(buf, sb, 2);
59        gen_byte_copy!(buf, sb, 3);
60        gen_byte_copy!(buf, sb, 4);
61        gen_byte_copy!(buf, sb, 5);
62        gen_byte_copy!(buf, sb, 6);
63        gen_byte_copy!(buf, sb, 7);
64
65        Self(buf)
66    }
67
68    pub fn inner(&self) -> &TopicInner {
69        &self.0
70    }
71
72    pub fn into_inner(&self) -> TopicInner {
73        self.0
74    }
75
76    /// Returns the length of the "string part".
77    pub fn str_len(&self) -> usize {
78        self.0.iter().position(|v| *v == 0).unwrap_or_default()
79    }
80
81    /// Returns a slice of the string bytes of the topic.
82    pub fn as_str_bytes(&self) -> &[u8] {
83        &self.0[..self.str_len()]
84    }
85
86    /// Returns if it's a standard topic ID.
87    pub fn is_standard(&self) -> bool {
88        let str_bytes = self.str_len();
89
90        // Checks that all "str part" bytes are sensible and all bytes after the first 0 are also 0.
91        self.as_str_bytes().iter().all(|b| {
92            b.is_ascii_alphanumeric() || *b == b'_' || *b == b'-' || *b == b'$' || *b == b'.'
93        }) && self.inner()[str_bytes..].iter().all(|b| *b == 0)
94    }
95
96    /// If the topic is a standard topic, returns it as a str.
97    pub fn as_str(&self) -> Option<&str> {
98        if self.is_standard() {
99            Some(unsafe { std::str::from_utf8_unchecked(self.as_str_bytes()) })
100        } else {
101            None
102        }
103    }
104}
105
106impl fmt::Debug for Topic {
107    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108        if let Some(ts) = self.as_str() {
109            f.write_str(ts)
110        } else {
111            f.write_fmt(format_args!("nonstd{:?}", self.inner()))
112        }
113    }
114}
115
116impl fmt::Display for Topic {
117    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118        <Self as fmt::Debug>::fmt(self, f)
119    }
120}