Skip to main content

p2panda_core/
topic.rs

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