Skip to main content

p2panda_core/
topic.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3use std::fmt::Display;
4use std::hash::Hash as StdHash;
5use std::str::FromStr;
6
7use rand::Rng;
8use rand::rngs::OsRng;
9use thiserror::Error;
10
11use crate::{Hash, VerifyingKey};
12
13pub const TOPIC_LENGTH: usize = 32;
14
15/// Identifier for a gossip- or sync topic.
16///
17/// A topic identifier is required when subscribing or publishing to a stream.
18///
19/// Topics usually describe concrete data which nodes want to exchange over, for example a document
20/// id or chat group id and so forth. Applications usually want to share topics via a secure side
21/// channel.
22///
23/// **WARNING:** Sensitive topics have to be treated like secret values and generated using a
24/// cryptographically secure pseudorandom number generator (CSPRNG). Otherwise they can be easily
25/// guessed by third parties or leaked during discovery.
26#[derive(Clone, Copy, Debug, Ord, PartialOrd, PartialEq, Eq, StdHash)]
27pub struct Topic(pub(crate) [u8; TOPIC_LENGTH]);
28
29impl Topic {
30    pub fn random() -> Self {
31        let mut rng = OsRng;
32        Self::from_rng(&mut rng)
33    }
34
35    pub fn from_rng<R: Rng>(rng: &mut R) -> Self {
36        Self(rng.r#gen())
37    }
38
39    pub fn from_bytes(&self, bytes: &[u8]) -> Result<Self, TopicError> {
40        Self::try_from(bytes)
41    }
42
43    pub fn as_bytes(&self) -> &[u8; TOPIC_LENGTH] {
44        &self.0
45    }
46
47    pub fn to_bytes(self) -> [u8; TOPIC_LENGTH] {
48        self.0
49    }
50
51    pub fn to_hex(&self) -> String {
52        hex::encode(self.0)
53    }
54}
55
56impl Default for Topic {
57    fn default() -> Self {
58        Self::random()
59    }
60}
61
62impl From<[u8; TOPIC_LENGTH]> for Topic {
63    fn from(topic: [u8; TOPIC_LENGTH]) -> Self {
64        Self(topic)
65    }
66}
67
68impl From<Topic> for [u8; TOPIC_LENGTH] {
69    fn from(topic: Topic) -> Self {
70        topic.0
71    }
72}
73
74impl From<Hash> for Topic {
75    fn from(value: Hash) -> Self {
76        Self(*value.as_bytes())
77    }
78}
79
80impl From<Topic> for Hash {
81    fn from(topic: Topic) -> Self {
82        Hash::from_bytes(topic.0)
83    }
84}
85
86impl From<VerifyingKey> for Topic {
87    fn from(value: VerifyingKey) -> Self {
88        Self(*value.as_bytes())
89    }
90}
91
92impl Display for Topic {
93    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94        write!(f, "{}", hex::encode(self.0))
95    }
96}
97
98impl FromStr for Topic {
99    type Err = TopicError;
100
101    fn from_str(value: &str) -> Result<Self, Self::Err> {
102        Self::try_from(hex::decode(value)?.as_slice())
103    }
104}
105
106impl TryFrom<&[u8]> for Topic {
107    type Error = TopicError;
108
109    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
110        let value_len = value.len();
111
112        let checked_value: [u8; TOPIC_LENGTH] = value
113            .try_into()
114            .map_err(|_| TopicError::InvalidLength(value_len, TOPIC_LENGTH))?;
115
116        Ok(Self::from(checked_value))
117    }
118}
119
120impl TryFrom<Vec<u8>> for Topic {
121    type Error = TopicError;
122
123    fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
124        let value_len = value.len();
125
126        let checked_value: [u8; TOPIC_LENGTH] = value
127            .try_into()
128            .map_err(|_| TopicError::InvalidLength(value_len, TOPIC_LENGTH))?;
129
130        Ok(Self::from(checked_value))
131    }
132}
133
134#[derive(Debug, Error)]
135pub enum TopicError {
136    /// Invalid number of bytes.
137    #[error("invalid bytes length of {0}, expected {1} bytes")]
138    InvalidLength(usize, usize),
139
140    /// String contains invalid hexadecimal characters.
141    #[error("invalid hex encoding in string")]
142    InvalidHexEncoding(#[from] hex::FromHexError),
143}