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