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