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