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