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