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