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