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