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