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 try_from::TryFrom;
8use std::str::FromStr;
9
10use minidom::{Element, IntoAttributeValue};
11use jid::Jid;
12
13use error::Error;
14use ns;
15
16generate_attribute!(Action, "action", {
17 ContentAccept => "content-accept",
18 ContentAdd => "content-add",
19 ContentModify => "content-modify",
20 ContentReject => "content-reject",
21 ContentRemove => "content-remove",
22 DescriptionInfo => "description-info",
23 SecurityInfo => "security-info",
24 SessionAccept => "session-accept",
25 SessionInfo => "session-info",
26 SessionInitiate => "session-initiate",
27 SessionTerminate => "session-terminate",
28 TransportAccept => "transport-accept",
29 TransportInfo => "transport-info",
30 TransportReject => "transport-reject",
31 TransportReplace => "transport-replace",
32});
33
34generate_attribute!(Creator, "creator", {
35 Initiator => "initiator",
36 Responder => "responder",
37});
38
39generate_attribute!(Senders, "senders", {
40 Both => "both",
41 Initiator => "initiator",
42 None => "none",
43 Responder => "responder",
44}, Default = Both);
45
46generate_id!(ContentId);
47
48// TODO: the list of values is defined, use an enum!
49generate_id!(Disposition);
50
51impl Default for Disposition {
52 fn default() -> Disposition {
53 Disposition(String::from("session"))
54 }
55}
56
57#[derive(Debug, Clone)]
58pub struct Content {
59 pub creator: Creator,
60 pub disposition: Disposition,
61 pub name: ContentId,
62 pub senders: Senders,
63 pub description: Option<Element>,
64 pub transport: Option<Element>,
65 pub security: Option<Element>,
66}
67
68impl TryFrom<Element> for Content {
69 type Err = Error;
70
71 fn try_from(elem: Element) -> Result<Content, Error> {
72 if !elem.is("content", ns::JINGLE) {
73 return Err(Error::ParseError("This is not a content element."));
74 }
75
76 let mut content = Content {
77 creator: get_attr!(elem, "creator", required),
78 disposition: get_attr!(elem, "disposition", default),
79 name: get_attr!(elem, "name", required),
80 senders: get_attr!(elem, "senders", default),
81 description: None,
82 transport: None,
83 security: None,
84 };
85 for child in elem.children() {
86 if child.name() == "description" {
87 if content.description.is_some() {
88 return Err(Error::ParseError("Content must not have more than one description."));
89 }
90 content.description = Some(child.clone());
91 } else if child.name() == "transport" {
92 if content.transport.is_some() {
93 return Err(Error::ParseError("Content must not have more than one transport."));
94 }
95 content.transport = Some(child.clone());
96 } else if child.name() == "security" {
97 if content.security.is_some() {
98 return Err(Error::ParseError("Content must not have more than one security."));
99 }
100 content.security = Some(child.clone());
101 }
102 }
103 Ok(content)
104 }
105}
106
107impl From<Content> for Element {
108 fn from(content: Content) -> Element {
109 Element::builder("content")
110 .ns(ns::JINGLE)
111 .attr("creator", content.creator)
112 .attr("disposition", content.disposition)
113 .attr("name", content.name)
114 .attr("senders", content.senders)
115 .append(content.description)
116 .append(content.transport)
117 .append(content.security)
118 .build()
119 }
120}
121
122#[derive(Debug, Clone, PartialEq)]
123pub enum Reason {
124 AlternativeSession, //(String),
125 Busy,
126 Cancel,
127 ConnectivityError,
128 Decline,
129 Expired,
130 FailedApplication,
131 FailedTransport,
132 GeneralError,
133 Gone,
134 IncompatibleParameters,
135 MediaError,
136 SecurityError,
137 Success,
138 Timeout,
139 UnsupportedApplications,
140 UnsupportedTransports,
141}
142
143impl FromStr for Reason {
144 type Err = Error;
145
146 fn from_str(s: &str) -> Result<Reason, Error> {
147 Ok(match s {
148 "alternative-session" => Reason::AlternativeSession,
149 "busy" => Reason::Busy,
150 "cancel" => Reason::Cancel,
151 "connectivity-error" => Reason::ConnectivityError,
152 "decline" => Reason::Decline,
153 "expired" => Reason::Expired,
154 "failed-application" => Reason::FailedApplication,
155 "failed-transport" => Reason::FailedTransport,
156 "general-error" => Reason::GeneralError,
157 "gone" => Reason::Gone,
158 "incompatible-parameters" => Reason::IncompatibleParameters,
159 "media-error" => Reason::MediaError,
160 "security-error" => Reason::SecurityError,
161 "success" => Reason::Success,
162 "timeout" => Reason::Timeout,
163 "unsupported-applications" => Reason::UnsupportedApplications,
164 "unsupported-transports" => Reason::UnsupportedTransports,
165
166 _ => return Err(Error::ParseError("Unknown reason.")),
167 })
168 }
169}
170
171impl From<Reason> for Element {
172 fn from(reason: Reason) -> Element {
173 Element::builder(match reason {
174 Reason::AlternativeSession => "alternative-session",
175 Reason::Busy => "busy",
176 Reason::Cancel => "cancel",
177 Reason::ConnectivityError => "connectivity-error",
178 Reason::Decline => "decline",
179 Reason::Expired => "expired",
180 Reason::FailedApplication => "failed-application",
181 Reason::FailedTransport => "failed-transport",
182 Reason::GeneralError => "general-error",
183 Reason::Gone => "gone",
184 Reason::IncompatibleParameters => "incompatible-parameters",
185 Reason::MediaError => "media-error",
186 Reason::SecurityError => "security-error",
187 Reason::Success => "success",
188 Reason::Timeout => "timeout",
189 Reason::UnsupportedApplications => "unsupported-applications",
190 Reason::UnsupportedTransports => "unsupported-transports",
191 }).build()
192 }
193}
194
195#[derive(Debug, Clone)]
196pub struct ReasonElement {
197 pub reason: Reason,
198 pub text: Option<String>,
199}
200
201impl TryFrom<Element> for ReasonElement {
202 type Err = Error;
203
204 fn try_from(elem: Element) -> Result<ReasonElement, Error> {
205 if !elem.is("reason", ns::JINGLE) {
206 return Err(Error::ParseError("This is not a reason element."));
207 }
208 let mut reason = None;
209 let mut text = None;
210 for child in elem.children() {
211 if !child.has_ns(ns::JINGLE) {
212 return Err(Error::ParseError("Reason contains a foreign element."));
213 }
214 match child.name() {
215 "text" => {
216 if text.is_some() {
217 return Err(Error::ParseError("Reason must not have more than one text."));
218 }
219 text = Some(child.text());
220 },
221 name => {
222 if reason.is_some() {
223 return Err(Error::ParseError("Reason must not have more than one reason."));
224 }
225 reason = Some(name.parse()?);
226 },
227 }
228 }
229 let reason = reason.ok_or(Error::ParseError("Reason doesn’t contain a valid reason."))?;
230 Ok(ReasonElement {
231 reason: reason,
232 text: text,
233 })
234 }
235}
236
237impl From<ReasonElement> for Element {
238 fn from(reason: ReasonElement) -> Element {
239 Element::builder("reason")
240 .append(Element::from(reason.reason))
241 .append(reason.text)
242 .build()
243 }
244}
245
246generate_id!(SessionId);
247
248#[derive(Debug, Clone)]
249pub struct Jingle {
250 pub action: Action,
251 pub initiator: Option<Jid>,
252 pub responder: Option<Jid>,
253 pub sid: SessionId,
254 pub contents: Vec<Content>,
255 pub reason: Option<ReasonElement>,
256 pub other: Vec<Element>,
257}
258
259impl TryFrom<Element> for Jingle {
260 type Err = Error;
261
262 fn try_from(root: Element) -> Result<Jingle, Error> {
263 if !root.is("jingle", ns::JINGLE) {
264 return Err(Error::ParseError("This is not a Jingle element."));
265 }
266
267 let mut jingle = Jingle {
268 action: get_attr!(root, "action", required),
269 initiator: get_attr!(root, "initiator", optional),
270 responder: get_attr!(root, "responder", optional),
271 sid: get_attr!(root, "sid", required),
272 contents: vec!(),
273 reason: None,
274 other: vec!(),
275 };
276
277 for child in root.children().cloned() {
278 if child.is("content", ns::JINGLE) {
279 let content = Content::try_from(child)?;
280 jingle.contents.push(content);
281 } else if child.is("reason", ns::JINGLE) {
282 if jingle.reason.is_some() {
283 return Err(Error::ParseError("Jingle must not have more than one reason."));
284 }
285 let reason = ReasonElement::try_from(child)?;
286 jingle.reason = Some(reason);
287 } else {
288 jingle.other.push(child);
289 }
290 }
291
292 Ok(jingle)
293 }
294}
295
296impl From<Jingle> for Element {
297 fn from(jingle: Jingle) -> Element {
298 Element::builder("jingle")
299 .ns(ns::JINGLE)
300 .attr("action", jingle.action)
301 .attr("initiator", jingle.initiator)
302 .attr("responder", jingle.responder)
303 .attr("sid", jingle.sid)
304 .append(jingle.contents)
305 .append(jingle.reason)
306 .build()
307 }
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313
314 #[test]
315 fn test_simple() {
316 let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'/>".parse().unwrap();
317 let jingle = Jingle::try_from(elem).unwrap();
318 assert_eq!(jingle.action, Action::SessionInitiate);
319 assert_eq!(jingle.sid, SessionId(String::from("coucou")));
320 }
321
322 #[test]
323 fn test_invalid_jingle() {
324 let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1'/>".parse().unwrap();
325 let error = Jingle::try_from(elem).unwrap_err();
326 let message = match error {
327 Error::ParseError(string) => string,
328 _ => panic!(),
329 };
330 assert_eq!(message, "Required attribute 'action' missing.");
331
332 let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-info'/>".parse().unwrap();
333 let error = Jingle::try_from(elem).unwrap_err();
334 let message = match error {
335 Error::ParseError(string) => string,
336 _ => panic!(),
337 };
338 assert_eq!(message, "Required attribute 'sid' missing.");
339
340 let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='coucou' sid='coucou'/>".parse().unwrap();
341 let error = Jingle::try_from(elem).unwrap_err();
342 let message = match error {
343 Error::ParseError(string) => string,
344 _ => panic!(),
345 };
346 assert_eq!(message, "Unknown value for 'action' attribute.");
347 }
348
349 #[test]
350 fn test_content() {
351 let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='initiator' name='coucou'><description/><transport/></content></jingle>".parse().unwrap();
352 let jingle = Jingle::try_from(elem).unwrap();
353 assert_eq!(jingle.contents[0].creator, Creator::Initiator);
354 assert_eq!(jingle.contents[0].name, ContentId(String::from("coucou")));
355 assert_eq!(jingle.contents[0].senders, Senders::Both);
356 assert_eq!(jingle.contents[0].disposition, Disposition(String::from("session")));
357
358 let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='initiator' name='coucou' senders='both'><description/><transport/></content></jingle>".parse().unwrap();
359 let jingle = Jingle::try_from(elem).unwrap();
360 assert_eq!(jingle.contents[0].senders, Senders::Both);
361
362 let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='initiator' name='coucou' disposition='early-session'><description/><transport/></content></jingle>".parse().unwrap();
363 let jingle = Jingle::try_from(elem).unwrap();
364 assert_eq!(jingle.contents[0].disposition, Disposition(String::from("early-session")));
365 }
366
367 #[test]
368 fn test_invalid_content() {
369 let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content/></jingle>".parse().unwrap();
370 let error = Jingle::try_from(elem).unwrap_err();
371 let message = match error {
372 Error::ParseError(string) => string,
373 _ => panic!(),
374 };
375 assert_eq!(message, "Required attribute 'creator' missing.");
376
377 let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='initiator'/></jingle>".parse().unwrap();
378 let error = Jingle::try_from(elem).unwrap_err();
379 let message = match error {
380 Error::ParseError(string) => string,
381 _ => panic!(),
382 };
383 assert_eq!(message, "Required attribute 'name' missing.");
384
385 let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='coucou' name='coucou'/></jingle>".parse().unwrap();
386 let error = Jingle::try_from(elem).unwrap_err();
387 let message = match error {
388 Error::ParseError(string) => string,
389 _ => panic!(),
390 };
391 assert_eq!(message, "Unknown value for 'creator' attribute.");
392
393 let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='initiator' name='coucou' senders='coucou'/></jingle>".parse().unwrap();
394 let error = Jingle::try_from(elem).unwrap_err();
395 let message = match error {
396 Error::ParseError(string) => string,
397 _ => panic!(),
398 };
399 assert_eq!(message, "Unknown value for 'senders' attribute.");
400
401 let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='initiator' name='coucou' senders=''/></jingle>".parse().unwrap();
402 let error = Jingle::try_from(elem).unwrap_err();
403 let message = match error {
404 Error::ParseError(string) => string,
405 _ => panic!(),
406 };
407 assert_eq!(message, "Unknown value for 'senders' attribute.");
408 }
409
410 #[test]
411 fn test_reason() {
412 let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><success/></reason></jingle>".parse().unwrap();
413 let jingle = Jingle::try_from(elem).unwrap();
414 let reason = jingle.reason.unwrap();
415 assert_eq!(reason.reason, Reason::Success);
416 assert_eq!(reason.text, None);
417
418 let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><success/><text>coucou</text></reason></jingle>".parse().unwrap();
419 let jingle = Jingle::try_from(elem).unwrap();
420 let reason = jingle.reason.unwrap();
421 assert_eq!(reason.reason, Reason::Success);
422 assert_eq!(reason.text, Some(String::from("coucou")));
423 }
424
425 #[test]
426 fn test_invalid_reason() {
427 let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason/></jingle>".parse().unwrap();
428 let error = Jingle::try_from(elem).unwrap_err();
429 let message = match error {
430 Error::ParseError(string) => string,
431 _ => panic!(),
432 };
433 assert_eq!(message, "Reason doesn’t contain a valid reason.");
434
435 let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><a/></reason></jingle>".parse().unwrap();
436 let error = Jingle::try_from(elem).unwrap_err();
437 let message = match error {
438 Error::ParseError(string) => string,
439 _ => panic!(),
440 };
441 assert_eq!(message, "Unknown reason.");
442
443 let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><a xmlns='http://www.w3.org/1999/xhtml'/></reason></jingle>".parse().unwrap();
444 let error = Jingle::try_from(elem).unwrap_err();
445 let message = match error {
446 Error::ParseError(string) => string,
447 _ => panic!(),
448 };
449 assert_eq!(message, "Reason contains a foreign element.");
450
451 let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><decline/></reason><reason/></jingle>".parse().unwrap();
452 let error = Jingle::try_from(elem).unwrap_err();
453 let message = match error {
454 Error::ParseError(string) => string,
455 _ => panic!(),
456 };
457 assert_eq!(message, "Jingle must not have more than one reason.");
458
459 let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><decline/><text/><text/></reason></jingle>".parse().unwrap();
460 let error = Jingle::try_from(elem).unwrap_err();
461 let message = match error {
462 Error::ParseError(string) => string,
463 _ => panic!(),
464 };
465 assert_eq!(message, "Reason must not have more than one text.");
466 }
467}