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> {
106 use xmtp_configuration::{BACKEND_INSTALLATION_ID_BYTES, GROUP_ID_BYTES};
107
108 let Some((&kind, identifier)) = bytes.split_first() else {
109 return Err(ConversionError::InvalidValue {
110 item: "Topic",
111 expected: "a topic kind followed by its identifier",
112 got: "empty".into(),
113 });
114 };
115 let kind = TopicKind::try_from(kind)?;
116 let expected = match kind {
117 TopicKind::GroupMessagesV1 | TopicKind::CommitLogEntriesV1 => GROUP_ID_BYTES,
118 TopicKind::WelcomeMessagesV1
119 | TopicKind::IdentityUpdatesV1
120 | TopicKind::KeyPackagesV1 => BACKEND_INSTALLATION_ID_BYTES,
121 };
122 if identifier.len() != expected {
123 return Err(ConversionError::InvalidLength {
124 item: "Topic identifier",
125 expected,
126 got: identifier.len(),
127 });
128 }
129 Ok(kind.create(identifier))
130 }
131
132 pub fn new_commit_log(group_id: impl AsRef<[u8]>) -> Self {
134 TopicKind::CommitLogEntriesV1.create(group_id)
135 }
136
137 pub fn new(kind: TopicKind, bytes: Vec<u8>) -> Self {
138 Self {
139 inner: kind.build(bytes),
140 }
141 }
142
143 pub fn new_group_message(group_id: impl AsRef<[u8]>) -> Self {
145 TopicKind::GroupMessagesV1.create(group_id)
146 }
147
148 pub fn new_identity_update(inbox_id: impl AsRef<[u8]>) -> Self {
153 TopicKind::IdentityUpdatesV1.create(inbox_id)
154 }
155
156 pub fn new_welcome_message(installation_id: InstallationId) -> Self {
159 TopicKind::WelcomeMessagesV1.create(installation_id)
160 }
161
162 pub fn new_key_package(installation_id: impl AsRef<[u8]>) -> Self {
165 TopicKind::KeyPackagesV1.create(installation_id.as_ref())
166 }
167
168 pub fn kind(&self) -> TopicKind {
169 self.inner[0]
170 .try_into()
171 .expect("A topic must always be built with a valid `TopicKind`")
172 }
173
174 pub fn identifier(&self) -> &[u8] {
176 &self.inner[1..]
177 }
178
179 pub fn cloned_vec(&self) -> Vec<u8> {
181 self.inner.clone().to_vec()
182 }
183
184 pub fn to_bytes(self) -> TopicBytes {
186 self.inner
187 }
188
189 pub fn identity_updates(&self) -> Option<&Topic> {
194 if self.kind() == TopicKind::IdentityUpdatesV1 {
195 Some(self)
196 } else {
197 None
198 }
199 }
200
201 pub fn group_message_v1(&self) -> Option<&Topic> {
206 if self.kind() == TopicKind::GroupMessagesV1 {
207 Some(self)
208 } else {
209 None
210 }
211 }
212
213 pub fn welcome_message_v1(&self) -> Option<&Topic> {
218 if self.kind() == TopicKind::WelcomeMessagesV1 {
219 Some(self)
220 } else {
221 None
222 }
223 }
224
225 pub fn key_packages_v1(&self) -> Option<&Topic> {
230 if self.kind() == TopicKind::KeyPackagesV1 {
231 Some(self)
232 } else {
233 None
234 }
235 }
236
237 #[cfg(any(feature = "test-utils", test))]
242 pub fn from_bytes(bytes: impl AsRef<[u8]>) -> Self {
243 Self {
244 inner: SmallVec::from_slice(bytes.as_ref()),
245 }
246 }
247}
248
249impl TryFrom<Vec<u8>> for Topic {
250 type Error = ConversionError;
251
252 fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
253 if let Some(byte) = value.first() {
254 let kind = TopicKind::try_from(*byte)?;
255 Ok(Topic::new(kind, value[1..].to_vec()))
256 } else {
257 Err(ConversionError::InvalidValue {
258 item: "Topic",
259 expected: "a byte array where the first byte is a valid TopicKind",
260 got: hex::encode(value),
261 })
262 }
263 }
264}
265
266impl From<Topic> for Vec<u8> {
267 fn from(topic: Topic) -> Vec<u8> {
268 topic.to_bytes().to_vec()
269 }
270}
271
272impl Debug for Topic {
273 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
274 f.debug_struct("Topic")
275 .field("kind", &self.kind())
276 .field("bytes", &hex::encode(self.identifier()))
277 .finish()
278 }
279}
280
281impl Display for Topic {
282 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
283 write!(f, "[{}/{}]", self.kind(), hex::encode(self.identifier()))
284 }
285}
286
287impl Deref for Topic {
288 type Target = [u8];
289
290 fn deref(&self) -> &Self::Target {
291 self.inner.deref()
292 }
293}
294
295impl<T> AsRef<T> for Topic
296where
297 T: ?Sized,
298 <Topic as Deref>::Target: AsRef<T>,
299{
300 fn as_ref(&self) -> &T {
301 self.deref().as_ref()
302 }
303}
304
305impl AsRef<Topic> for Topic {
306 fn as_ref(&self) -> &Topic {
307 self
308 }
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314 use xmtp_configuration::{
315 BACKEND_INSTALLATION_ID_BYTES, GROUP_ID_BYTES, LEGACY_GROUP_ID_BYTES,
316 };
317
318 fn topic_bytes(kind: TopicKind, length: usize) -> Vec<u8> {
319 let mut bytes = vec![kind as u8];
320 bytes.extend(vec![0; length]);
321 bytes
322 }
323
324 #[xmtp_common::test(unwrap_try = true)]
326 fn backend_topic_parser_checks_each_kind_and_identifier_length() {
327 assert!(Topic::parse(&[]).is_err());
328 assert!(Topic::parse(&[u8::MAX]).is_err());
329 for (kind, length) in [
330 (TopicKind::GroupMessagesV1, GROUP_ID_BYTES),
331 (TopicKind::WelcomeMessagesV1, BACKEND_INSTALLATION_ID_BYTES),
332 (TopicKind::IdentityUpdatesV1, BACKEND_INSTALLATION_ID_BYTES),
333 (TopicKind::KeyPackagesV1, BACKEND_INSTALLATION_ID_BYTES),
334 (TopicKind::CommitLogEntriesV1, GROUP_ID_BYTES),
335 ] {
336 let mut bytes = topic_bytes(kind, length);
337 let topic = Topic::parse(&bytes)?;
338 assert_eq!(topic.kind(), kind);
339 assert_eq!(topic.cloned_vec(), bytes);
340 assert!(Topic::parse(&[kind as u8]).is_err());
341 bytes.pop();
342 assert!(Topic::parse(&bytes).is_err());
343 bytes.extend([0, 0]);
344 assert!(Topic::parse(&bytes).is_err());
345 }
346 }
347
348 #[xmtp_common::test(unwrap_try = true)]
352 fn group_and_commit_log_topics_accept_only_32_byte_identifiers() {
353 for kind in [TopicKind::GroupMessagesV1, TopicKind::CommitLogEntriesV1] {
354 assert!(
355 Topic::parse(&topic_bytes(kind, LEGACY_GROUP_ID_BYTES)).is_err(),
356 "{kind} with a legacy 16-byte group id"
357 );
358 for length in 0..=40 {
359 let accepted = length == GROUP_ID_BYTES;
360 assert_eq!(
361 Topic::parse(&topic_bytes(kind, length)).is_ok(),
362 accepted,
363 "{kind} with a {length}-byte identifier"
364 );
365 }
366 }
367 for kind in [
368 TopicKind::WelcomeMessagesV1,
369 TopicKind::IdentityUpdatesV1,
370 TopicKind::KeyPackagesV1,
371 ] {
372 assert!(Topic::parse(&topic_bytes(kind, LEGACY_GROUP_ID_BYTES)).is_err());
373 }
374 }
375}