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