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}