ruma_identifiers/
event_id.rs

1//! Matrix event identifiers.
2
3use crate::ServerName;
4
5/// A Matrix [event ID].
6///
7/// An `EventId` is generated randomly or converted from a string slice, and can be converted back
8/// into a string as needed.
9///
10/// # Room versions
11///
12/// Matrix specifies multiple [room versions](https://spec.matrix.org/v1.2/#room-versions) and the
13/// format of event identifiers differ between them. The original format used by room versions 1 and
14/// 2 uses a short pseudorandom "localpart" followed by the hostname and port of the originating
15/// homeserver. Later room versions change event identifiers to be a hash of the event encoded with
16/// Base64. Some of the methods provided by `EventId` are only relevant to the original event
17/// format.
18///
19/// ```
20/// # use std::convert::TryFrom;
21/// # use ruma_identifiers::EventId;
22/// // Original format
23/// assert_eq!(<&EventId>::try_from("$h29iv0s8:example.com").unwrap(), "$h29iv0s8:example.com");
24/// // Room version 3 format
25/// assert_eq!(
26///     <&EventId>::try_from("$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk").unwrap(),
27///     "$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk"
28/// );
29/// // Room version 4 format
30/// assert_eq!(
31///     <&EventId>::try_from("$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg").unwrap(),
32///     "$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg"
33/// );
34/// ```
35///
36/// [event ID]: https://spec.matrix.org/v1.2/appendices/#room-ids-and-event-ids
37#[repr(transparent)]
38#[derive(PartialEq, Eq, PartialOrd, Ord, Hash)]
39pub struct EventId(str);
40
41opaque_identifier_validated!(EventId, ruma_identifiers_validation::event_id::validate);
42
43impl EventId {
44    /// Attempts to generate an `EventId` for the given origin server with a localpart consisting
45    /// of 18 random ASCII characters.
46    ///
47    /// This should only be used for events in the original format  as used by Matrix room versions
48    /// 1 and 2.
49    #[cfg(feature = "rand")]
50    pub fn new(server_name: &ServerName) -> Box<Self> {
51        Self::from_owned(format!("${}:{}", crate::generate_localpart(18), server_name).into())
52    }
53
54    /// Returns the event's unique ID.
55    ///
56    /// For the original event format as used by Matrix room versions 1 and 2, this is the
57    /// "localpart" that precedes the homeserver. For later formats, this is the entire ID without
58    /// the leading `$` sigil.
59    pub fn localpart(&self) -> &str {
60        let idx = self.colon_idx().unwrap_or_else(|| self.as_str().len());
61        &self.as_str()[1..idx]
62    }
63
64    /// Returns the server name of the event ID.
65    ///
66    /// Only applicable to events in the original format as used by Matrix room versions 1 and 2.
67    pub fn server_name(&self) -> Option<&ServerName> {
68        self.colon_idx().map(|idx| ServerName::from_borrowed(&self.as_str()[idx + 1..]))
69    }
70
71    fn colon_idx(&self) -> Option<usize> {
72        self.as_str().find(':')
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use std::convert::TryFrom;
79
80    use super::EventId;
81    use crate::Error;
82
83    #[test]
84    fn valid_original_event_id() {
85        assert_eq!(
86            <&EventId>::try_from("$39hvsi03hlne:example.com").expect("Failed to create EventId."),
87            "$39hvsi03hlne:example.com"
88        );
89    }
90
91    #[test]
92    fn valid_base64_event_id() {
93        assert_eq!(
94            <&EventId>::try_from("$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk")
95                .expect("Failed to create EventId."),
96            "$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk"
97        )
98    }
99
100    #[test]
101    fn valid_url_safe_base64_event_id() {
102        assert_eq!(
103            <&EventId>::try_from("$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg")
104                .expect("Failed to create EventId."),
105            "$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg"
106        )
107    }
108
109    #[cfg(feature = "rand")]
110    #[test]
111    fn generate_random_valid_event_id() {
112        use crate::server_name;
113
114        let event_id = EventId::new(server_name!("example.com"));
115        let id_str = event_id.as_str();
116
117        assert!(id_str.starts_with('$'));
118        assert_eq!(id_str.len(), 31);
119    }
120
121    #[cfg(feature = "serde")]
122    #[test]
123    fn serialize_valid_original_event_id() {
124        assert_eq!(
125            serde_json::to_string(
126                <&EventId>::try_from("$39hvsi03hlne:example.com")
127                    .expect("Failed to create EventId.")
128            )
129            .expect("Failed to convert EventId to JSON."),
130            r#""$39hvsi03hlne:example.com""#
131        );
132    }
133
134    #[cfg(feature = "serde")]
135    #[test]
136    fn serialize_valid_base64_event_id() {
137        assert_eq!(
138            serde_json::to_string(
139                <&EventId>::try_from("$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk")
140                    .expect("Failed to create EventId.")
141            )
142            .expect("Failed to convert EventId to JSON."),
143            r#""$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk""#
144        );
145    }
146
147    #[cfg(feature = "serde")]
148    #[test]
149    fn serialize_valid_url_safe_base64_event_id() {
150        assert_eq!(
151            serde_json::to_string(
152                <&EventId>::try_from("$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg")
153                    .expect("Failed to create EventId.")
154            )
155            .expect("Failed to convert EventId to JSON."),
156            r#""$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg""#
157        );
158    }
159
160    #[cfg(feature = "serde")]
161    #[test]
162    fn deserialize_valid_original_event_id() {
163        assert_eq!(
164            serde_json::from_str::<Box<EventId>>(r#""$39hvsi03hlne:example.com""#)
165                .expect("Failed to convert JSON to EventId"),
166            <&EventId>::try_from("$39hvsi03hlne:example.com").expect("Failed to create EventId.")
167        );
168    }
169
170    #[cfg(feature = "serde")]
171    #[test]
172    fn deserialize_valid_base64_event_id() {
173        assert_eq!(
174            serde_json::from_str::<Box<EventId>>(
175                r#""$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk""#
176            )
177            .expect("Failed to convert JSON to EventId"),
178            <&EventId>::try_from("$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk")
179                .expect("Failed to create EventId.")
180        );
181    }
182
183    #[cfg(feature = "serde")]
184    #[test]
185    fn deserialize_valid_url_safe_base64_event_id() {
186        assert_eq!(
187            serde_json::from_str::<Box<EventId>>(
188                r#""$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg""#
189            )
190            .expect("Failed to convert JSON to EventId"),
191            <&EventId>::try_from("$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg")
192                .expect("Failed to create EventId.")
193        );
194    }
195
196    #[test]
197    fn valid_original_event_id_with_explicit_standard_port() {
198        assert_eq!(
199            <&EventId>::try_from("$39hvsi03hlne:example.com:443")
200                .expect("Failed to create EventId."),
201            "$39hvsi03hlne:example.com:443"
202        );
203    }
204
205    #[test]
206    fn valid_original_event_id_with_non_standard_port() {
207        assert_eq!(
208            <&EventId>::try_from("$39hvsi03hlne:example.com:5000")
209                .expect("Failed to create EventId."),
210            "$39hvsi03hlne:example.com:5000"
211        );
212    }
213
214    #[test]
215    fn missing_original_event_id_sigil() {
216        assert_eq!(
217            <&EventId>::try_from("39hvsi03hlne:example.com").unwrap_err(),
218            Error::MissingLeadingSigil
219        );
220    }
221
222    #[test]
223    fn missing_base64_event_id_sigil() {
224        assert_eq!(
225            <&EventId>::try_from("acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk").unwrap_err(),
226            Error::MissingLeadingSigil
227        );
228    }
229
230    #[test]
231    fn missing_url_safe_base64_event_id_sigil() {
232        assert_eq!(
233            <&EventId>::try_from("Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg").unwrap_err(),
234            Error::MissingLeadingSigil
235        );
236    }
237
238    #[test]
239    fn invalid_event_id_host() {
240        assert_eq!(<&EventId>::try_from("$39hvsi03hlne:/").unwrap_err(), Error::InvalidServerName);
241    }
242
243    #[test]
244    fn invalid_event_id_port() {
245        assert_eq!(
246            <&EventId>::try_from("$39hvsi03hlne:example.com:notaport").unwrap_err(),
247            Error::InvalidServerName
248        );
249    }
250}