xmpp_parsers/
disco.rs

1// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
2//
3// This Source Code Form is subject to the terms of the Mozilla Public
4// License, v. 2.0. If a copy of the MPL was not distributed with this
5// file, You can obtain one at http://mozilla.org/MPL/2.0/.
6
7use xso::{AsXml, FromXml};
8
9use crate::data_forms::DataForm;
10use crate::iq::{IqGetPayload, IqResultPayload};
11use crate::ns;
12use crate::rsm::{SetQuery, SetResult};
13use jid::Jid;
14
15/// Structure representing a `<query xmlns='http://jabber.org/protocol/disco#info'/>` element.
16///
17/// It should only be used in an `<iq type='get'/>`, as it can only represent
18/// the request, and not a result.
19#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
20#[xml(namespace = ns::DISCO_INFO, name = "query")]
21pub struct DiscoInfoQuery {
22    /// Node on which we are doing the discovery.
23    #[xml(attribute(default))]
24    pub node: Option<String>,
25}
26
27impl IqGetPayload for DiscoInfoQuery {}
28
29/// Structure representing an `<identity xmlns='http://jabber.org/protocol/disco#info'/>` element.
30#[derive(FromXml, AsXml, Debug, Clone, PartialEq, Eq, Hash)]
31#[xml(namespace = ns::DISCO_INFO, name = "identity")]
32pub struct Identity {
33    /// Category of this identity.
34    // TODO: use an enum here.
35    #[xml(attribute)]
36    pub category: String,
37
38    /// Type of this identity.
39    // TODO: use an enum here.
40    #[xml(attribute = "type")]
41    pub type_: String,
42
43    /// Lang of the name of this identity.
44    #[xml(lang(default))]
45    pub lang: Option<String>,
46
47    /// Name of this identity.
48    #[xml(attribute(default))]
49    pub name: Option<String>,
50}
51
52impl Identity {
53    /// Create a new `<identity/>`.
54    pub fn new<C, T, L, N>(category: C, type_: T, lang: L, name: N) -> Identity
55    where
56        C: Into<String>,
57        T: Into<String>,
58        L: Into<String>,
59        N: Into<String>,
60    {
61        Identity {
62            category: category.into(),
63            type_: type_.into(),
64            lang: Some(lang.into()),
65            name: Some(name.into()),
66        }
67    }
68
69    /// Create a new `<identity/>` without a name.
70    pub fn new_anonymous<C, T, L, N>(category: C, type_: T) -> Identity
71    where
72        C: Into<String>,
73        T: Into<String>,
74    {
75        Identity {
76            category: category.into(),
77            type_: type_.into(),
78            lang: None,
79            name: None,
80        }
81    }
82}
83
84/// Structure representing a `<query xmlns='http://jabber.org/protocol/disco#info'/>` element.
85///
86/// It should only be used in an `<iq type='result'/>`, as it can only
87/// represent the result, and not a request.
88#[derive(FromXml, AsXml, Debug, Clone)]
89#[xml(namespace = ns::DISCO_INFO, name = "query")]
90pub struct DiscoInfoResult {
91    /// Node on which we have done this discovery.
92    #[xml(attribute(default))]
93    pub node: Option<String>,
94
95    /// List of identities exposed by this entity.
96    #[xml(child(n = ..))]
97    pub identities: Vec<Identity>,
98
99    /// List of features supported by this entity.
100    #[xml(extract(n = .., name = "feature", fields(attribute(name = "var", type_ = String))))]
101    pub features: Vec<String>,
102
103    /// List of extensions reported by this entity.
104    #[xml(child(n = ..))]
105    pub extensions: Vec<DataForm>,
106}
107
108impl IqResultPayload for DiscoInfoResult {}
109
110/// Structure representing a `<query xmlns='http://jabber.org/protocol/disco#items'/>` element.
111///
112/// It should only be used in an `<iq type='get'/>`, as it can only represent
113/// the request, and not a result.
114#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
115#[xml(namespace = ns::DISCO_ITEMS, name = "query")]
116pub struct DiscoItemsQuery {
117    /// Node on which we are doing the discovery.
118    #[xml(attribute(default))]
119    pub node: Option<String>,
120
121    /// Optional paging via Result Set Management
122    #[xml(child(default))]
123    pub rsm: Option<SetQuery>,
124}
125
126impl IqGetPayload for DiscoItemsQuery {}
127
128/// Structure representing an `<item xmlns='http://jabber.org/protocol/disco#items'/>` element.
129#[derive(FromXml, AsXml, Debug, Clone, PartialEq)]
130#[xml(namespace = ns::DISCO_ITEMS, name = "item")]
131pub struct Item {
132    /// JID of the entity pointed by this item.
133    #[xml(attribute)]
134    pub jid: Jid,
135
136    /// Node of the entity pointed by this item.
137    #[xml(attribute(default))]
138    pub node: Option<String>,
139
140    /// Name of the entity pointed by this item.
141    #[xml(attribute(default))]
142    pub name: Option<String>,
143}
144
145/// Structure representing a `<query
146/// xmlns='http://jabber.org/protocol/disco#items'/>` element.
147///
148/// It should only be used in an `<iq type='result'/>`, as it can only
149/// represent the result, and not a request.
150#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
151#[xml(namespace = ns::DISCO_ITEMS, name = "query")]
152pub struct DiscoItemsResult {
153    /// Node on which we have done this discovery.
154    #[xml(attribute(default))]
155    pub node: Option<String>,
156
157    /// List of items pointed by this entity.
158    #[xml(child(n = ..))]
159    pub items: Vec<Item>,
160
161    /// Optional paging via Result Set Management
162    #[xml(child(default))]
163    pub rsm: Option<SetResult>,
164}
165
166impl IqResultPayload for DiscoItemsResult {}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171    use jid::BareJid;
172    use minidom::Element;
173    use xso::error::{Error, FromElementError};
174
175    #[cfg(target_pointer_width = "32")]
176    #[test]
177    fn test_size() {
178        assert_size!(Identity, 48);
179        assert_size!(DiscoInfoQuery, 12);
180        assert_size!(DiscoInfoResult, 48);
181
182        assert_size!(Item, 40);
183        assert_size!(DiscoItemsQuery, 52);
184        assert_size!(DiscoItemsResult, 64);
185    }
186
187    #[cfg(target_pointer_width = "64")]
188    #[test]
189    fn test_size() {
190        assert_size!(Identity, 96);
191        assert_size!(DiscoInfoQuery, 24);
192        assert_size!(DiscoInfoResult, 96);
193
194        assert_size!(Item, 80);
195        assert_size!(DiscoItemsQuery, 104);
196        assert_size!(DiscoItemsResult, 128);
197    }
198
199    #[test]
200    fn test_simple() {
201        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='client' type='pc'/><feature var='http://jabber.org/protocol/disco#info'/></query>".parse().unwrap();
202        let query = DiscoInfoResult::try_from(elem).unwrap();
203        assert!(query.node.is_none());
204        assert_eq!(query.identities.len(), 1);
205        assert_eq!(query.features.len(), 1);
206        assert!(query.extensions.is_empty());
207    }
208
209    #[test]
210    fn test_identity_after_feature() {
211        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><feature var='http://jabber.org/protocol/disco#info'/><identity category='client' type='pc'/></query>".parse().unwrap();
212        let query = DiscoInfoResult::try_from(elem).unwrap();
213        assert_eq!(query.identities.len(), 1);
214        assert_eq!(query.features.len(), 1);
215        assert!(query.extensions.is_empty());
216    }
217
218    #[test]
219    fn test_feature_after_dataform() {
220        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='client' type='pc'/><x xmlns='jabber:x:data' type='result'><field var='FORM_TYPE' type='hidden'><value>coucou</value></field></x><feature var='http://jabber.org/protocol/disco#info'/></query>".parse().unwrap();
221        let query = DiscoInfoResult::try_from(elem).unwrap();
222        assert_eq!(query.identities.len(), 1);
223        assert_eq!(query.features.len(), 1);
224        assert_eq!(query.extensions.len(), 1);
225    }
226
227    #[test]
228    fn test_extension() {
229        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='client' type='pc'/><feature var='http://jabber.org/protocol/disco#info'/><x xmlns='jabber:x:data' type='result'><field var='FORM_TYPE' type='hidden'><value>example</value></field></x></query>".parse().unwrap();
230        let elem1 = elem.clone();
231        let query = DiscoInfoResult::try_from(elem).unwrap();
232        assert!(query.node.is_none());
233        assert_eq!(query.identities.len(), 1);
234        assert_eq!(query.features.len(), 1);
235        assert_eq!(query.extensions.len(), 1);
236        assert_eq!(query.extensions[0].form_type(), Some("example"));
237
238        let elem2 = query.into();
239        assert_eq!(elem1, elem2);
240    }
241
242    #[test]
243    #[cfg_attr(feature = "disable-validation", should_panic = "Result::unwrap_err")]
244    fn test_invalid() {
245        let elem: Element =
246            "<query xmlns='http://jabber.org/protocol/disco#info'><coucou/></query>"
247                .parse()
248                .unwrap();
249        let error = DiscoInfoResult::try_from(elem).unwrap_err();
250        let message = match error {
251            FromElementError::Invalid(Error::Other(string)) => string,
252            _ => panic!(),
253        };
254        assert_eq!(message, "Unknown child in DiscoInfoResult element.");
255    }
256
257    #[test]
258    fn test_invalid_identity() {
259        let elem: Element =
260            "<query xmlns='http://jabber.org/protocol/disco#info'><identity/></query>"
261                .parse()
262                .unwrap();
263        let error = DiscoInfoResult::try_from(elem).unwrap_err();
264        let message = match error {
265            FromElementError::Invalid(Error::Other(string)) => string,
266            _ => panic!(),
267        };
268        assert_eq!(
269            message,
270            "Required attribute field 'category' on Identity element missing."
271        );
272
273        let elem: Element =
274            "<query xmlns='http://jabber.org/protocol/disco#info'><identity type='coucou'/></query>"
275                .parse()
276                .unwrap();
277        let error = DiscoInfoResult::try_from(elem).unwrap_err();
278        let message = match error {
279            FromElementError::Invalid(Error::Other(string)) => string,
280            _ => panic!(),
281        };
282        assert_eq!(
283            message,
284            "Required attribute field 'category' on Identity element missing."
285        );
286
287        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='coucou'/></query>".parse().unwrap();
288        let error = DiscoInfoResult::try_from(elem).unwrap_err();
289        let message = match error {
290            FromElementError::Invalid(Error::Other(string)) => string,
291            _ => panic!(),
292        };
293        assert_eq!(
294            message,
295            "Required attribute field 'type_' on Identity element missing."
296        );
297    }
298
299    #[test]
300    fn test_invalid_feature() {
301        let elem: Element =
302            "<query xmlns='http://jabber.org/protocol/disco#info'><feature/></query>"
303                .parse()
304                .unwrap();
305        let error = DiscoInfoResult::try_from(elem).unwrap_err();
306        let message = match error {
307            FromElementError::Invalid(Error::Other(string)) => string,
308            _ => panic!(),
309        };
310        // TODO: Make xso generate a better error message, with s/unnamed field 0/'var'/ for
311        // instance.
312        assert_eq!(
313            message,
314            "Required attribute unnamed field 0 on extraction for field 'features' in DiscoInfoResult element missing."
315        );
316    }
317
318    // TODO: We stopped validating that there are enough identities and features in this result,
319    // this is a limitation of xso which accepts n = .. only, and not n = 1.., so let’s wait until
320    // xso implements this to reenable this test.
321    #[test]
322    #[ignore]
323    fn test_invalid_result() {
324        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'/>"
325            .parse()
326            .unwrap();
327        let error = DiscoInfoResult::try_from(elem).unwrap_err();
328        let message = match error {
329            FromElementError::Invalid(Error::Other(string)) => string,
330            _ => panic!(),
331        };
332        assert_eq!(
333            message,
334            "There must be at least one identity in disco#info."
335        );
336
337        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='client' type='pc'/></query>".parse().unwrap();
338        let error = DiscoInfoResult::try_from(elem).unwrap_err();
339        let message = match error {
340            FromElementError::Invalid(Error::Other(string)) => string,
341            _ => panic!(),
342        };
343        assert_eq!(message, "There must be at least one feature in disco#info.");
344    }
345
346    #[test]
347    fn test_simple_items() {
348        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#items'/>"
349            .parse()
350            .unwrap();
351        let query = DiscoItemsQuery::try_from(elem).unwrap();
352        assert!(query.node.is_none());
353
354        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#items' node='coucou'/>"
355            .parse()
356            .unwrap();
357        let query = DiscoItemsQuery::try_from(elem).unwrap();
358        assert_eq!(query.node, Some(String::from("coucou")));
359    }
360
361    #[test]
362    fn test_simple_items_result() {
363        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#items'/>"
364            .parse()
365            .unwrap();
366        let query = DiscoItemsResult::try_from(elem).unwrap();
367        assert!(query.node.is_none());
368        assert!(query.items.is_empty());
369
370        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#items' node='coucou'/>"
371            .parse()
372            .unwrap();
373        let query = DiscoItemsResult::try_from(elem).unwrap();
374        assert_eq!(query.node, Some(String::from("coucou")));
375        assert!(query.items.is_empty());
376    }
377
378    #[test]
379    fn test_answers_items_result() {
380        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#items'><item jid='component'/><item jid='component2' node='test' name='A component'/></query>".parse().unwrap();
381        let query = DiscoItemsResult::try_from(elem).unwrap();
382        let elem2 = Element::from(query);
383        let query = DiscoItemsResult::try_from(elem2).unwrap();
384        assert_eq!(query.items.len(), 2);
385        assert_eq!(query.items[0].jid, BareJid::new("component").unwrap());
386        assert_eq!(query.items[0].node, None);
387        assert_eq!(query.items[0].name, None);
388        assert_eq!(query.items[1].jid, BareJid::new("component2").unwrap());
389        assert_eq!(query.items[1].node, Some(String::from("test")));
390        assert_eq!(query.items[1].name, Some(String::from("A component")));
391    }
392
393    // WORKAROUND FOR PROSODY BUG 1664, DO NOT REMOVE BEFORE 2028-12-17 (5 YEARS AFTER FIX)
394    // https://issues.prosody.im/1664
395    // See also:
396    // https://gitlab.com/xmpp-rs/xmpp-rs/-/issues/128
397    // https://gitlab.com/xmpp-rs/xmpp-rs/-/merge_requests/302
398    #[test]
399    fn test_missing_disco_info_feature_workaround() {
400        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='client' type='pc'/><feature var='http://jabber.org/protocol/muc#user'/></query>".parse().unwrap();
401        let query = DiscoInfoResult::try_from(elem).unwrap();
402        assert_eq!(query.identities.len(), 1);
403        assert_eq!(query.features.len(), 1);
404        assert!(query.extensions.is_empty());
405    }
406}