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