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