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::util::error::Error;
  9use crate::iq::{IqGetPayload, IqResultPayload};
 10use crate::ns;
 11use jid::Jid;
 12use minidom::Element;
 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/// Structure representing a `<feature xmlns='http://jabber.org/protocol/disco#info'/>` element.
 30#[derive(PartialEq)]
 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 {
 41            var: var.into(),
 42        }
 43    }
 44}
 45
 46generate_element!(
 47    /// Structure representing an `<identity xmlns='http://jabber.org/protocol/disco#info'/>` element.
 48    Identity, "identity", DISCO_INFO,
 49    attributes: [
 50        /// Category of this identity.
 51        // TODO: use an enum here.
 52        category: RequiredNonEmpty<String> = "category",
 53
 54        /// Type of this identity.
 55        // TODO: use an enum here.
 56        type_: RequiredNonEmpty<String> = "type",
 57
 58        /// Lang of the name of this identity.
 59        lang: Option<String> = "xml:lang",
 60
 61        /// Name of this identity.
 62        name: Option<String> = "name",
 63    ]
 64);
 65
 66impl Identity {
 67    /// Create a new `<identity/>`.
 68    pub fn new<C, T, L, N>(category: C, type_: T, lang: L, name: N) -> Identity
 69    where C: Into<String>,
 70          T: Into<String>,
 71          L: Into<String>,
 72          N: Into<String>,
 73    {
 74        Identity {
 75            category: category.into(),
 76            type_: type_.into(),
 77            lang: Some(lang.into()),
 78            name: Some(name.into()),
 79        }
 80    }
 81
 82    /// Create a new `<identity/>` without a name.
 83    pub fn new_anonymous<C, T, L, N>(category: C, type_: T) -> Identity
 84    where 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")
179            .ns(ns::DISCO_INFO)
180            .attr("node", disco.node)
181            .append(disco.identities)
182            .append(disco.features)
183            .append(disco.extensions)
184            .build()
185    }
186}
187
188generate_element!(
189/// Structure representing a `<query xmlns='http://jabber.org/protocol/disco#items'/>` element.
190///
191/// It should only be used in an `<iq type='get'/>`, as it can only represent
192/// the request, and not a result.
193DiscoItemsQuery, "query", DISCO_ITEMS,
194attributes: [
195    /// Node on which we are doing the discovery.
196    node: Option<String> = "node",
197]);
198
199impl IqGetPayload for DiscoItemsQuery {}
200
201generate_element!(
202/// Structure representing an `<item xmlns='http://jabber.org/protocol/disco#items'/>` element.
203Item, "item", DISCO_ITEMS,
204attributes: [
205    /// JID of the entity pointed by this item.
206    jid: Required<Jid> = "jid",
207    /// Node of the entity pointed by this item.
208    node: Option<String> = "node",
209    /// Name of the entity pointed by this item.
210    name: Option<String> = "name",
211]);
212
213generate_element!(
214    /// Structure representing a `<query
215    /// xmlns='http://jabber.org/protocol/disco#items'/>` element.
216    ///
217    /// It should only be used in an `<iq type='result'/>`, as it can only
218    /// represent the result, and not a request.
219    DiscoItemsResult, "query", DISCO_ITEMS,
220    attributes: [
221        /// Node on which we have done this discovery.
222        node: Option<String> = "node"
223    ],
224    children: [
225        /// List of items pointed by this entity.
226        items: Vec<Item> = ("item", DISCO_ITEMS) => Item
227    ]
228);
229
230impl IqResultPayload for DiscoItemsResult {}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235    use crate::util::compare_elements::NamespaceAwareCompare;
236    use std::str::FromStr;
237
238    #[cfg(target_pointer_width = "32")]
239    #[test]
240    fn test_size() {
241        assert_size!(Identity, 48);
242        assert_size!(Feature, 12);
243        assert_size!(DiscoInfoQuery, 12);
244        assert_size!(DiscoInfoResult, 48);
245
246        assert_size!(Item, 64);
247        assert_size!(DiscoItemsQuery, 12);
248        assert_size!(DiscoItemsResult, 24);
249    }
250
251    #[cfg(target_pointer_width = "64")]
252    #[test]
253    fn test_size() {
254        assert_size!(Identity, 96);
255        assert_size!(Feature, 24);
256        assert_size!(DiscoInfoQuery, 24);
257        assert_size!(DiscoInfoResult, 96);
258
259        assert_size!(Item, 128);
260        assert_size!(DiscoItemsQuery, 24);
261        assert_size!(DiscoItemsResult, 48);
262    }
263
264    #[test]
265    fn test_simple() {
266        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();
267        let query = DiscoInfoResult::try_from(elem).unwrap();
268        assert!(query.node.is_none());
269        assert_eq!(query.identities.len(), 1);
270        assert_eq!(query.features.len(), 1);
271        assert!(query.extensions.is_empty());
272    }
273
274    #[test]
275    fn test_identity_after_feature() {
276        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();
277        let query = DiscoInfoResult::try_from(elem).unwrap();
278        assert_eq!(query.identities.len(), 1);
279        assert_eq!(query.features.len(), 1);
280        assert!(query.extensions.is_empty());
281    }
282
283    #[test]
284    fn test_feature_after_dataform() {
285        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();
286        let query = DiscoInfoResult::try_from(elem).unwrap();
287        assert_eq!(query.identities.len(), 1);
288        assert_eq!(query.features.len(), 1);
289        assert_eq!(query.extensions.len(), 1);
290    }
291
292    #[test]
293    fn test_extension() {
294        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();
295        let elem1 = elem.clone();
296        let query = DiscoInfoResult::try_from(elem).unwrap();
297        assert!(query.node.is_none());
298        assert_eq!(query.identities.len(), 1);
299        assert_eq!(query.features.len(), 1);
300        assert_eq!(query.extensions.len(), 1);
301        assert_eq!(query.extensions[0].form_type, Some(String::from("example")));
302
303        let elem2 = query.into();
304        assert!(elem1.compare_to(&elem2));
305    }
306
307    #[test]
308    fn test_invalid() {
309        let elem: Element =
310            "<query xmlns='http://jabber.org/protocol/disco#info'><coucou/></query>"
311                .parse()
312                .unwrap();
313        let error = DiscoInfoResult::try_from(elem).unwrap_err();
314        let message = match error {
315            Error::ParseError(string) => string,
316            _ => panic!(),
317        };
318        assert_eq!(message, "Unknown element in disco#info.");
319    }
320
321    #[test]
322    fn test_invalid_identity() {
323        let elem: Element =
324            "<query xmlns='http://jabber.org/protocol/disco#info'><identity/></query>"
325                .parse()
326                .unwrap();
327        let error = DiscoInfoResult::try_from(elem).unwrap_err();
328        let message = match error {
329            Error::ParseError(string) => string,
330            _ => panic!(),
331        };
332        assert_eq!(message, "Required attribute 'category' missing.");
333
334        let elem: Element =
335            "<query xmlns='http://jabber.org/protocol/disco#info'><identity category=''/></query>"
336                .parse()
337                .unwrap();
338        let error = DiscoInfoResult::try_from(elem).unwrap_err();
339        let message = match error {
340            Error::ParseError(string) => string,
341            _ => panic!(),
342        };
343        assert_eq!(
344            message,
345            "Required attribute 'category' must not be empty."
346        );
347
348        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='coucou'/></query>".parse().unwrap();
349        let error = DiscoInfoResult::try_from(elem).unwrap_err();
350        let message = match error {
351            Error::ParseError(string) => string,
352            _ => panic!(),
353        };
354        assert_eq!(message, "Required attribute 'type' missing.");
355
356        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='coucou' type=''/></query>".parse().unwrap();
357        let error = DiscoInfoResult::try_from(elem).unwrap_err();
358        let message = match error {
359            Error::ParseError(string) => string,
360            _ => panic!(),
361        };
362        assert_eq!(message, "Required attribute 'type' must not be empty.");
363    }
364
365    #[test]
366    fn test_invalid_feature() {
367        let elem: Element =
368            "<query xmlns='http://jabber.org/protocol/disco#info'><feature/></query>"
369                .parse()
370                .unwrap();
371        let error = DiscoInfoResult::try_from(elem).unwrap_err();
372        let message = match error {
373            Error::ParseError(string) => string,
374            _ => panic!(),
375        };
376        assert_eq!(message, "Required attribute 'var' missing.");
377    }
378
379    #[test]
380    fn test_invalid_result() {
381        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'/>"
382            .parse()
383            .unwrap();
384        let error = DiscoInfoResult::try_from(elem).unwrap_err();
385        let message = match error {
386            Error::ParseError(string) => string,
387            _ => panic!(),
388        };
389        assert_eq!(
390            message,
391            "There must be at least one identity in disco#info."
392        );
393
394        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='client' type='pc'/></query>".parse().unwrap();
395        let error = DiscoInfoResult::try_from(elem).unwrap_err();
396        let message = match error {
397            Error::ParseError(string) => string,
398            _ => panic!(),
399        };
400        assert_eq!(message, "There must be at least one feature in disco#info.");
401
402        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();
403        let error = DiscoInfoResult::try_from(elem).unwrap_err();
404        let message = match error {
405            Error::ParseError(string) => string,
406            _ => panic!(),
407        };
408        assert_eq!(message, "disco#info feature not present in disco#info.");
409    }
410
411    #[test]
412    fn test_simple_items() {
413        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#items'/>"
414            .parse()
415            .unwrap();
416        let query = DiscoItemsQuery::try_from(elem).unwrap();
417        assert!(query.node.is_none());
418
419        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#items' node='coucou'/>"
420            .parse()
421            .unwrap();
422        let query = DiscoItemsQuery::try_from(elem).unwrap();
423        assert_eq!(query.node, Some(String::from("coucou")));
424    }
425
426    #[test]
427    fn test_simple_items_result() {
428        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#items'/>"
429            .parse()
430            .unwrap();
431        let query = DiscoItemsResult::try_from(elem).unwrap();
432        assert!(query.node.is_none());
433        assert!(query.items.is_empty());
434
435        let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#items' node='coucou'/>"
436            .parse()
437            .unwrap();
438        let query = DiscoItemsResult::try_from(elem).unwrap();
439        assert_eq!(query.node, Some(String::from("coucou")));
440        assert!(query.items.is_empty());
441    }
442
443    #[test]
444    fn test_answers_items_result() {
445        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();
446        let query = DiscoItemsResult::try_from(elem).unwrap();
447        let elem2 = Element::from(query);
448        let query = DiscoItemsResult::try_from(elem2).unwrap();
449        assert_eq!(query.items.len(), 2);
450        assert_eq!(query.items[0].jid, Jid::from_str("component").unwrap());
451        assert_eq!(query.items[0].node, None);
452        assert_eq!(query.items[0].name, None);
453        assert_eq!(query.items[1].jid, Jid::from_str("component2").unwrap());
454        assert_eq!(query.items[1].node, Some(String::from("test")));
455        assert_eq!(query.items[1].name, Some(String::from("A component")));
456    }
457}