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