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