1use serde::{Deserialize, Deserializer, Serialize, Serializer};
2use std::{
3 fmt::{Debug, Display},
4 ops::Deref,
5};
6
7use smallvec::SmallVec;
8
9use crate::{ConversionError, types::InstallationId};
10
11type TopicBytes = SmallVec<[u8; 33]>;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16#[repr(u8)]
17#[non_exhaustive]
18pub enum TopicKind {
19 GroupMessagesV1 = 0,
20 WelcomeMessagesV1 = 1,
21 IdentityUpdatesV1 = 2,
22 KeyPackagesV1 = 3,
23 CommitLogEntriesV1 = 4,
24}
25
26impl TryFrom<u8> for TopicKind {
27 type Error = crate::ConversionError;
28
29 fn try_from(value: u8) -> Result<Self, Self::Error> {
30 match value {
31 0 => Ok(TopicKind::GroupMessagesV1),
32 1 => Ok(TopicKind::WelcomeMessagesV1),
33 2 => Ok(TopicKind::IdentityUpdatesV1),
34 3 => Ok(TopicKind::KeyPackagesV1),
35 4 => Ok(TopicKind::CommitLogEntriesV1),
36 i => Err(ConversionError::InvalidValue {
37 item: "u8",
38 expected: "an unsigned integer in the range 0-4",
39 got: i.to_string(),
40 }),
41 }
42 }
43}
44
45impl Display for TopicKind {
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 use TopicKind::*;
48 match self {
49 GroupMessagesV1 => write!(f, "group_message_v1"),
50 WelcomeMessagesV1 => write!(f, "welcome_message_v1"),
51 IdentityUpdatesV1 => write!(f, "identity_updates_v1"),
52 KeyPackagesV1 => write!(f, "key_packages_v1"),
53 CommitLogEntriesV1 => write!(f, "commit_log_entries_v1"),
54 }
55 }
56}
57
58impl TopicKind {
59 fn build<B: AsRef<[u8]>>(&self, bytes: B) -> TopicBytes {
60 let bytes = bytes.as_ref();
61 let mut topic = TopicBytes::new();
62 topic.push(*self as u8);
63 topic.extend_from_slice(bytes);
64 topic
65 }
66
67 pub fn create<B: AsRef<[u8]>>(&self, bytes: B) -> Topic {
68 Topic {
69 inner: self.build(bytes),
70 }
71 }
72}
73
74#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
77#[serde(transparent)]
78pub struct Topic {
79 #[serde(serialize_with = "to_hex", deserialize_with = "from_hex")]
80 inner: TopicBytes,
81}
82
83fn to_hex<S>(bytes: &TopicBytes, serializer: S) -> Result<S::Ok, S::Error>
84where
85 S: Serializer,
86{
87 serializer.serialize_str(&hex::encode(bytes.as_slice()))
88}
89
90fn from_hex<'de, D>(deserializer: D) -> Result<TopicBytes, D::Error>
91where
92 D: Deserializer<'de>,
93{
94 let s: &str = Deserialize::deserialize(deserializer)?;
95 hex::decode(s)
96 .map(SmallVec::from_vec)
97 .map_err(serde::de::Error::custom)
98}
99
100impl Topic {
101 pub fn parse(bytes: &[u8]) -> Result<Self, ConversionError> {
103 use xmtp_configuration::{BACKEND_GROUP_ID_BYTES, BACKEND_INSTALLATION_ID_BYTES};
104
105 let Some((&kind, identifier)) = bytes.split_first() else {
106 return Err(ConversionError::InvalidValue {
107 item: "Topic",
108 expected: "a topic kind followed by its identifier",
109 got: "empty".into(),
110 });
111 };
112 let kind = TopicKind::try_from(kind)?;
113 let expected = match kind {
114 TopicKind::GroupMessagesV1 | TopicKind::CommitLogEntriesV1 => BACKEND_GROUP_ID_BYTES,
115 TopicKind::WelcomeMessagesV1
116 | TopicKind::IdentityUpdatesV1
117 | TopicKind::KeyPackagesV1 => BACKEND_INSTALLATION_ID_BYTES,
118 };
119 if identifier.len() != expected {
120 return Err(ConversionError::InvalidLength {
121 item: "Topic identifier",
122 expected,
123 got: identifier.len(),
124 });
125 }
126 Ok(kind.create(identifier))
127 }
128
129 pub fn new_commit_log(group_id: impl AsRef<[u8]>) -> Self {
131 TopicKind::CommitLogEntriesV1.create(group_id)
132 }
133
134 pub fn new(kind: TopicKind, bytes: Vec<u8>) -> Self {
135 Self {
136 inner: kind.build(bytes),
137 }
138 }
139
140 pub fn new_group_message(group_id: impl AsRef<[u8]>) -> Self {
142 TopicKind::GroupMessagesV1.create(group_id)
143 }
144
145 pub fn new_identity_update(inbox_id: impl AsRef<[u8]>) -> Self {
150 TopicKind::IdentityUpdatesV1.create(inbox_id)
151 }
152
153 pub fn new_welcome_message(installation_id: InstallationId) -> Self {
156 TopicKind::WelcomeMessagesV1.create(installation_id)
157 }
158
159 pub fn new_key_package(installation_id: impl AsRef<[u8]>) -> Self {
162 TopicKind::KeyPackagesV1.create(installation_id.as_ref())
163 }
164
165 pub fn kind(&self) -> TopicKind {
166 self.inner[0]
167 .try_into()
168 .expect("A topic must always be built with a valid `TopicKind`")
169 }
170
171 pub fn identifier(&self) -> &[u8] {
173 &self.inner[1..]
174 }
175
176 pub fn cloned_vec(&self) -> Vec<u8> {
178 self.inner.clone().to_vec()
179 }
180
181 pub fn to_bytes(self) -> TopicBytes {
183 self.inner
184 }
185
186 pub fn identity_updates(&self) -> Option<&Topic> {
191 if self.kind() == TopicKind::IdentityUpdatesV1 {
192 Some(self)
193 } else {
194 None
195 }
196 }
197
198 pub fn group_message_v1(&self) -> Option<&Topic> {
203 if self.kind() == TopicKind::GroupMessagesV1 {
204 Some(self)
205 } else {
206 None
207 }
208 }
209
210 pub fn welcome_message_v1(&self) -> Option<&Topic> {
215 if self.kind() == TopicKind::WelcomeMessagesV1 {
216 Some(self)
217 } else {
218 None
219 }
220 }
221
222 pub fn key_packages_v1(&self) -> Option<&Topic> {
227 if self.kind() == TopicKind::KeyPackagesV1 {
228 Some(self)
229 } else {
230 None
231 }
232 }
233
234 #[cfg(any(feature = "test-utils", test))]
239 pub fn from_bytes(bytes: impl AsRef<[u8]>) -> Self {
240 Self {
241 inner: SmallVec::from_slice(bytes.as_ref()),
242 }
243 }
244}
245
246impl TryFrom<Vec<u8>> for Topic {
247 type Error = ConversionError;
248
249 fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
250 if let Some(byte) = value.first() {
251 let kind = TopicKind::try_from(*byte)?;
252 Ok(Topic::new(kind, value[1..].to_vec()))
253 } else {
254 Err(ConversionError::InvalidValue {
255 item: "Topic",
256 expected: "a byte array where the first byte is a valid TopicKind",
257 got: hex::encode(value),
258 })
259 }
260 }
261}
262
263impl From<Topic> for Vec<u8> {
264 fn from(topic: Topic) -> Vec<u8> {
265 topic.to_bytes().to_vec()
266 }
267}
268
269impl Debug for Topic {
270 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
271 f.debug_struct("Topic")
272 .field("kind", &self.kind())
273 .field("bytes", &hex::encode(self.identifier()))
274 .finish()
275 }
276}
277
278impl Display for Topic {
279 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
280 write!(f, "[{}/{}]", self.kind(), hex::encode(self.identifier()))
281 }
282}
283
284impl Deref for Topic {
285 type Target = [u8];
286
287 fn deref(&self) -> &Self::Target {
288 self.inner.deref()
289 }
290}
291
292impl<T> AsRef<T> for Topic
293where
294 T: ?Sized,
295 <Topic as Deref>::Target: AsRef<T>,
296{
297 fn as_ref(&self) -> &T {
298 self.deref().as_ref()
299 }
300}
301
302impl AsRef<Topic> for Topic {
303 fn as_ref(&self) -> &Topic {
304 self
305 }
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311 use xmtp_configuration::{BACKEND_GROUP_ID_BYTES, BACKEND_INSTALLATION_ID_BYTES};
312
313 #[xmtp_common::test(unwrap_try = true)]
315 fn backend_topic_parser_checks_each_kind_and_identifier_length() {
316 assert!(Topic::parse(&[]).is_err());
317 assert!(Topic::parse(&[u8::MAX]).is_err());
318 for (kind, length) in [
319 (TopicKind::GroupMessagesV1, BACKEND_GROUP_ID_BYTES),
320 (TopicKind::WelcomeMessagesV1, BACKEND_INSTALLATION_ID_BYTES),
321 (TopicKind::IdentityUpdatesV1, BACKEND_INSTALLATION_ID_BYTES),
322 (TopicKind::KeyPackagesV1, BACKEND_INSTALLATION_ID_BYTES),
323 (TopicKind::CommitLogEntriesV1, BACKEND_GROUP_ID_BYTES),
324 ] {
325 let mut bytes = vec![kind as u8];
326 bytes.extend(vec![0; length]);
327 let topic = Topic::parse(&bytes)?;
328 assert_eq!(topic.kind(), kind);
329 assert_eq!(topic.cloned_vec(), bytes);
330 assert!(Topic::parse(&[kind as u8]).is_err());
331 bytes.pop();
332 assert!(Topic::parse(&bytes).is_err());
333 bytes.extend([0, 0]);
334 assert!(Topic::parse(&bytes).is_err());
335 }
336 }
337}