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 crate::data_forms::{DataForm, DataFormType};
  8use crate::iq::{IqGetPayload, IqResultPayload};
  9use crate::ns;
 10use crate::util::error::Error;
 11use crate::Element;
 12use jid::Jid;
 13use std::convert::TryFrom;
 14
 15generate_element!(
 16/// Structure representing a `<query xmlns='http://jabber.org/protocol/disco#info'/>` element.
 17///
 18/// It should only be used in an `<iq type='get'/>`, as it can only represent
 19/// the request, and not a result.
 20DiscoInfoQuery, "query", DISCO_INFO,
 21attributes: [
 22    /// Node on which we are doing the discovery.
 23    node: Option<String> = "node",
 24]);
 25
 26impl IqGetPayload for DiscoInfoQuery {}
 27
 28generate_element!(
 29#[derive(Eq, Hash)]
 30/// Structure representing a `<feature xmlns='http://jabber.org/protocol/disco#info'/>` element.
 31Feature, "feature", DISCO_INFO,
 32attributes: [
 33    /// Namespace of the feature we want to represent.
 34    var: Required<String> = "var",
 35]);
 36
 37impl Feature {
 38    /// Create a new `<feature/>` with the according `@var`.
 39    pub fn new<S: Into<String>>(var: S) -> Feature {
 40        Feature { var: var.into() }
 41    }
 42}
 43
 44generate_element!(
 45    /// Structure representing an `<identity xmlns='http://jabber.org/protocol/disco#info'/>` element.
 46    Identity, "identity", DISCO_INFO,
 47    attributes: [
 48        /// Category of this identity.
 49        // TODO: use an enum here.
 50        category: RequiredNonEmpty<String> = "category",
 51
 52        /// Type of this identity.
 53        // TODO: use an enum here.
 54        type_: RequiredNonEmpty<String> = "type",
 55
 56        /// Lang of the name of this identity.
 57        lang: Option<String> = "xml:lang",
 58
 59        /// Name of this identity.
 60        name: Option<String> = "name",
 61    ]
 62);
 63
 64impl Identity {
 65    /// Create a new `<identity/>`.
 66    pub fn new<C, T, L, N>(category: C, type_: T, lang: L, name: N) -> Identity
 67    where
 68        C: Into<String>,
 69        T: Into<String>,
 70        L: Into<String>,
 71        N: Into<String>,
 72    {
 73        Identity {
 74            category: category.into(),
 75            type_: type_.into(),
 76            lang: Some(lang.into()),
 77            name: Some(name.into()),
 78        }
 79    }
 80
 81    /// Create a new `<identity/>` without a name.
 82    pub fn new_anonymous<C, T, L, N>(category: C, type_: T) -> Identity
 83    where
 84        C: Into<String>,
 85        T: Into<String>,
 86    {
 87        Identity {
 88            category: category.into(),
 89            type_: type_.into(),
 90            lang: None,
 91            name: None,
 92        }
 93    }
 94}
 95
 96/// Structure representing a `<query xmlns='http://jabber.org/protocol/disco#info'/>` element.
 97///
 98/// It should only be used in an `<iq type='result'/>`, as it can only
 99/// represent the result, and not a request.
100#[derive(Debug, Clone)]
101pub struct DiscoInfoResult {
102    /// Node on which we have done this discovery.
103    pub node: Option<String>,
104
105    /// List of identities exposed by this entity.
106    pub identities: Vec<Identity>,
107
108    /// List of features supported by this entity.
109    pub features: Vec<Feature>,
110
111    /// List of extensions reported by this entity.
112    pub extensions: Vec<DataForm>,
113}
114
115impl IqResultPayload for DiscoInfoResult {}
116
117impl TryFrom<Element> for DiscoInfoResult {
118    type Error = Error;
119
120    fn try_from(elem: Element) -> Result<DiscoInfoResult, Error> {
121        check_self!(elem, "query", DISCO_INFO, "disco#info result");
122        check_no_unknown_attributes!(elem, "disco#info result", ["node"]);
123
124        let mut result = DiscoInfoResult {
125            node: get_attr!(elem, "node", Option),
126            identities: vec![],
127            features: vec![],
128            extensions: vec![],
129        };
130
131        for child in elem.children() {
132            if child.is("identity", ns::DISCO_INFO) {
133                let identity = Identity::try_from(child.clone())?;
134                result.identities.push(identity);
135            } else if child.is("feature", ns::DISCO_INFO) {
136                let feature = Feature::try_from(child.clone())?;
137                result.features.push(feature);
138            } else if child.is("x", ns::DATA_FORMS) {
139                let data_form = DataForm::try_from(child.clone())?;
140                if data_form.type_ != DataFormType::Result_ {
141                    return Err(Error::ParseError(
142                        "Data form must have a 'result' type in disco#info.",
143                    ));
144                }
145                if data_form.form_type.is_none() {
146                    return Err(Error::ParseError("Data form found without a FORM_TYPE."));
147                }
148                result.extensions.push(data_form);
149            } else {
150                return Err(Error::ParseError("Unknown element in disco#info."));
151            }
152        }
153
154        if result.identities.is_empty() {
155            return Err(Error::ParseError(
156                "There must be at least one identity in disco#info.",
157            ));
158        }
159        if result.features.is_empty() {
160            return Err(Error::ParseError(
161                "There must be at least one feature in disco#info.",
162            ));
163        }
164        if !result.features.contains(&Feature {
165            var: ns::DISCO_INFO.to_owned(),
166        }) {
167            return Err(Error::ParseError(
168                "disco#info feature not present in disco#info.",
169            ));
170        }
171
172        Ok(result)
173    }
174}
175
176impl From<DiscoInfoResult> for Element {
177    fn from(disco: DiscoInfoResult) -> Element {
178        Element::builder("query", ns::DISCO_INFO)
179            .attr("node", disco.node)
180            .append_all(disco.identities.into_iter())
181            .append_all(disco.features.into_iter())
182            .append_all(disco.extensions.iter().cloned().map(Element::from))
183            .build()
184    }
185}
186
187generate_element!(
188/// Structure representing a `<query xmlns='http://jabber.org/protocol/disco#items'/>` element.
189///
190/// It should only be used in an `<iq type='get'/>`, as it can only represent
191/// the request, and not a result.
192DiscoItemsQuery, "query", DISCO_ITEMS,
193attributes: [
194    /// Node on which we are doing the discovery.
195    node: Option<String> = "node",
196]);
197
198impl IqGetPayload for DiscoItemsQuery {}
199
200generate_element!(
201/// Structure representing an `<item xmlns='http://jabber.org/protocol/disco#items'/>` element.
202Item, "item", DISCO_ITEMS,
203attributes: [
204    /// JID of the entity pointed by this item.
205    jid: Required<Jid> = "jid",
206    /// Node of the entity pointed by this item.
207    node: Option<String> = "node",
208    /// Name of the entity pointed by this item.
209    name: Option<String> = "name",
210]);
211
212generate_element!(
213    /// Structure representing a `<query
214    /// xmlns='http://jabber.org/protocol/disco#items'/>` element.
215    ///
216    /// It should only be used in an `<iq type='result'/>`, as it can only
217    /// represent the result, and not a request.
218    DiscoItemsResult, "query", DISCO_ITEMS,
219    attributes: [
220        /// Node on which we have done this discovery.
221        node: Option<String> = "node"
222    ],
223    children: [
224        /// List of items pointed by this entity.
225        items: Vec<Item> = ("item", DISCO_ITEMS) => Item
226    ]
227);
228
229impl IqResultPayload for DiscoItemsResult {}
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234    use jid::BareJid;
235
236    #[cfg(target_pointer_width = "32")]
237    #[test]
238    fn test_size() {
239        assert_size!(Identity, 48);
240        assert_size!(Feature, 12);
241        assert_size!(DiscoInfoQuery, 12);
242        assert_size!(DiscoInfoResult, 48);
243
244        assert_size!(Item, 64);
245        assert_size!(DiscoItemsQuery, 12);
246        assert_size!(DiscoItemsResult, 24);
247    }
248
249    #[cfg(target_pointer_width = "64")]
250    #[test]
251    fn test_size() {
252        assert_size!(Identity, 96);
253        assert_size!(Feature, 24);
254        assert_size!(DiscoInfoQuery, 24);
255        assert_size!(DiscoInfoResult, 96);
256
257        assert_size!(Item, 128);
258        assert_size!(DiscoItemsQuery, 24);
259        assert_size!(DiscoItemsResult, 48);
260    }
261
262    #[test]
263    fn test_simple() {
264        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();
265        let query = DiscoInfoResult::try_from(elem).unwrap();
266        assert!(query.node.is_none());
267        assert_eq!(query.identities.len(), 1);
268        assert_eq!(query.features.len(), 1);
269        assert!(query.extensions.is_empty());
270    }
271
272    #[test]
273    fn test_identity_after_feature() {
274        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();
275        let query = DiscoInfoResult::try_from(elem).unwrap();
276        assert_eq!(query.identities.len(), 1);
277        assert_eq!(query.features.len(), 1);
278        assert!(query.extensions.is_empty());
279    }
280
281    #[test]
282    fn test_feature_after_dataform() {
283        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();
284        let query = DiscoInfoResult::try_from(elem).unwrap();
285        assert_eq!(query.identities.len(), 1);
286        assert_eq!(query.features.len(), 1);
287        assert_eq!(query.extensions.len(), 1);
288    }
289
290    #[test]
291    fn test_extension() {
292        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();
293        let elem1 = elem.clone();
294        let query = DiscoInfoResult::try_from(elem).unwrap();
295        assert!(query.node.is_none());
296        assert_eq!(query.identities.len(), 1);
297        assert_eq!(query.features.len(), 1);
298        assert_eq!(query.extensions.len(), 1);
299        assert_eq!(query.extensions[0].form_type, Some(String::from("example")));
300
301        let elem2 = query.into();
302        assert_eq!(elem1, elem2);
303    }
304
305    #[test]
306    fn test_invalid() {
307        let elem: Element =
308            "<query xmlns='http://jabber.org/protocol/disco#info'><coucou/></query>"
309                .parse()
310                .unwrap();
311        let error = DiscoInfoResult::try_from(elem).unwrap_err();
312        let message = match error {
313            Error::ParseError(string) => string,
314            _ => panic!(),
315        };
316        assert_eq!(message, "Unknown element in disco#info.");
317    }
318
319    #[test]
320    fn test_invalid_identity() {
321        let elem: Element =
322            "<query xmlns='http://jabber.org/protocol/disco#info'><identity/></query>"
323                .parse()
324                .unwrap();
325        let error = DiscoInfoResult::try_from(elem).unwrap_err();
326        let message = match error {
327            Error::ParseError(string) => string,
328            _ => panic!(),
329        };
330        assert_eq!(message, "Required attribute 'category' missing.");
331
332        let elem: Element =
333            "<query xmlns='http://jabber.org/protocol/disco#info'><identity category=''/></query>"
334                .parse()
335                .unwrap();
336        let error = DiscoInfoResult::try_from(elem).unwrap_err();
337        let message = match error {
338            Error::ParseError(string) => string,
339            _ => panic!(),
340        };
341        assert_eq!(message, "Required attribute 'category' must not be empty.");
342
343        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='coucou'/></query>".parse().unwrap();
344        let error = DiscoInfoResult::try_from(elem).unwrap_err();
345        let message = match error {
346            Error::ParseError(string) => string,
347            _ => panic!(),
348        };
349        assert_eq!(message, "Required attribute 'type' missing.");
350
351        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='coucou' type=''/></query>".parse().unwrap();
352        let error = DiscoInfoResult::try_from(elem).unwrap_err();
353        let message = match error {
354            Error::ParseError(string) => string,
355            _ => panic!(),
356        };
357        assert_eq!(message, "Required attribute 'type' must not be empty.");
358    }
359
360    #[test]
361    fn test_invalid_feature() {
362        let elem: Element =
363            "<query xmlns='http://jabber.org/protocol/disco#info'><feature/></query>"
364                .parse()
365                .unwrap();
366        let error = DiscoInfoResult::try_from(elem).unwrap_err();
367        let message = match error {
368            Error::ParseError(string) => string,
369            _ => panic!(),
370        };
371        assert_eq!(message, "Required attribute 'var' missing.");
372    }
373
374    #[test]
375    fn test_invalid_result() {
376        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'/>"
377            .parse()
378            .unwrap();
379        let error = DiscoInfoResult::try_from(elem).unwrap_err();
380        let message = match error {
381            Error::ParseError(string) => string,
382            _ => panic!(),
383        };
384        assert_eq!(
385            message,
386            "There must be at least one identity in disco#info."
387        );
388
389        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='client' type='pc'/></query>".parse().unwrap();
390        let error = DiscoInfoResult::try_from(elem).unwrap_err();
391        let message = match error {
392            Error::ParseError(string) => string,
393            _ => panic!(),
394        };
395        assert_eq!(message, "There must be at least one feature in disco#info.");
396
397        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='client' type='pc'/><feature var='http://jabber.org/protocol/disco#items'/></query>".parse().unwrap();
398        let error = DiscoInfoResult::try_from(elem).unwrap_err();
399        let message = match error {
400            Error::ParseError(string) => string,
401            _ => panic!(),
402        };
403        assert_eq!(message, "disco#info feature not present in disco#info.");
404    }
405
406    #[test]
407    fn test_simple_items() {
408        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#items'/>"
409            .parse()
410            .unwrap();
411        let query = DiscoItemsQuery::try_from(elem).unwrap();
412        assert!(query.node.is_none());
413
414        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#items' node='coucou'/>"
415            .parse()
416            .unwrap();
417        let query = DiscoItemsQuery::try_from(elem).unwrap();
418        assert_eq!(query.node, Some(String::from("coucou")));
419    }
420
421    #[test]
422    fn test_simple_items_result() {
423        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#items'/>"
424            .parse()
425            .unwrap();
426        let query = DiscoItemsResult::try_from(elem).unwrap();
427        assert!(query.node.is_none());
428        assert!(query.items.is_empty());
429
430        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#items' node='coucou'/>"
431            .parse()
432            .unwrap();
433        let query = DiscoItemsResult::try_from(elem).unwrap();
434        assert_eq!(query.node, Some(String::from("coucou")));
435        assert!(query.items.is_empty());
436    }
437
438    #[test]
439    fn test_answers_items_result() {
440        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();
441        let query = DiscoItemsResult::try_from(elem).unwrap();
442        let elem2 = Element::from(query);
443        let query = DiscoItemsResult::try_from(elem2).unwrap();
444        assert_eq!(query.items.len(), 2);
445        assert_eq!(query.items[0].jid, BareJid::domain("component"));
446        assert_eq!(query.items[0].node, None);
447        assert_eq!(query.items[0].name, None);
448        assert_eq!(query.items[1].jid, BareJid::domain("component2"));
449        assert_eq!(query.items[1].node, Some(String::from("test")));
450        assert_eq!(query.items[1].name, Some(String::from("A component")));
451    }
452}