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