// Copyright (c) 2017 Emmanuel Gil Peyrot // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. use std::convert::TryFrom; use std::str::FromStr; use minidom::{Element, IntoElements, IntoAttributeValue, ElementEmitter}; use jid::Jid; use error::Error; use ns; generate_attribute!(Action, "action", { ContentAccept => "content-accept", ContentAdd => "content-add", ContentModify => "content-modify", ContentReject => "content-reject", ContentRemove => "content-remove", DescriptionInfo => "description-info", SecurityInfo => "security-info", SessionAccept => "session-accept", SessionInfo => "session-info", SessionInitiate => "session-initiate", SessionTerminate => "session-terminate", TransportAccept => "transport-accept", TransportInfo => "transport-info", TransportReject => "transport-reject", TransportReplace => "transport-replace", }); generate_attribute!(Creator, "creator", { Initiator => "initiator", Responder => "responder", }); generate_attribute!(Senders, "senders", { Both => "both", Initiator => "initiator", None => "none", Responder => "responder", }, Default = Both); #[derive(Debug, Clone)] pub struct Content { pub creator: Creator, pub disposition: String, // TODO: the list of values is defined, use an enum! pub name: String, pub senders: Senders, pub description: Option, pub transport: Option, pub security: Option, } impl TryFrom for Content { type Error = Error; fn try_from(elem: Element) -> Result { if !elem.is("content", ns::JINGLE) { return Err(Error::ParseError("This is not a content element.")); } let mut content = Content { creator: get_attr!(elem, "creator", required), disposition: get_attr!(elem, "disposition", optional).unwrap_or(String::from("session")), name: get_attr!(elem, "name", required), senders: get_attr!(elem, "senders", default), description: None, transport: None, security: None, }; for child in elem.children() { if child.name() == "description" { if content.description.is_some() { return Err(Error::ParseError("Content must not have more than one description.")); } content.description = Some(child.clone()); } else if child.name() == "transport" { if content.transport.is_some() { return Err(Error::ParseError("Content must not have more than one transport.")); } content.transport = Some(child.clone()); } else if child.name() == "security" { if content.security.is_some() { return Err(Error::ParseError("Content must not have more than one security.")); } content.security = Some(child.clone()); } } Ok(content) } } impl Into for Content { fn into(self) -> Element { Element::builder("content") .ns(ns::JINGLE) .attr("creator", self.creator) .attr("disposition", self.disposition) .attr("name", self.name) .attr("senders", self.senders) .append(self.description) .append(self.transport) .append(self.security) .build() } } impl IntoElements for Content { fn into_elements(self, emitter: &mut ElementEmitter) { emitter.append_child(self.into()); } } #[derive(Debug, Clone, PartialEq)] pub enum Reason { AlternativeSession, //(String), Busy, Cancel, ConnectivityError, Decline, Expired, FailedApplication, FailedTransport, GeneralError, Gone, IncompatibleParameters, MediaError, SecurityError, Success, Timeout, UnsupportedApplications, UnsupportedTransports, } impl FromStr for Reason { type Err = Error; fn from_str(s: &str) -> Result { Ok(match s { "alternative-session" => Reason::AlternativeSession, "busy" => Reason::Busy, "cancel" => Reason::Cancel, "connectivity-error" => Reason::ConnectivityError, "decline" => Reason::Decline, "expired" => Reason::Expired, "failed-application" => Reason::FailedApplication, "failed-transport" => Reason::FailedTransport, "general-error" => Reason::GeneralError, "gone" => Reason::Gone, "incompatible-parameters" => Reason::IncompatibleParameters, "media-error" => Reason::MediaError, "security-error" => Reason::SecurityError, "success" => Reason::Success, "timeout" => Reason::Timeout, "unsupported-applications" => Reason::UnsupportedApplications, "unsupported-transports" => Reason::UnsupportedTransports, _ => return Err(Error::ParseError("Unknown reason.")), }) } } impl Into for Reason { fn into(self) -> Element { Element::builder(match self { Reason::AlternativeSession => "alternative-session", Reason::Busy => "busy", Reason::Cancel => "cancel", Reason::ConnectivityError => "connectivity-error", Reason::Decline => "decline", Reason::Expired => "expired", Reason::FailedApplication => "failed-application", Reason::FailedTransport => "failed-transport", Reason::GeneralError => "general-error", Reason::Gone => "gone", Reason::IncompatibleParameters => "incompatible-parameters", Reason::MediaError => "media-error", Reason::SecurityError => "security-error", Reason::Success => "success", Reason::Timeout => "timeout", Reason::UnsupportedApplications => "unsupported-applications", Reason::UnsupportedTransports => "unsupported-transports", }).build() } } #[derive(Debug, Clone)] pub struct ReasonElement { pub reason: Reason, pub text: Option, } impl TryFrom for ReasonElement { type Error = Error; fn try_from(elem: Element) -> Result { if !elem.is("reason", ns::JINGLE) { return Err(Error::ParseError("This is not a reason element.")); } let mut reason = None; let mut text = None; for child in elem.children() { if child.ns() != Some(ns::JINGLE) { return Err(Error::ParseError("Reason contains a foreign element.")); } match child.name() { "text" => { if text.is_some() { return Err(Error::ParseError("Reason must not have more than one text.")); } text = Some(child.text()); }, name => { if reason.is_some() { return Err(Error::ParseError("Reason must not have more than one reason.")); } reason = Some(name.parse()?); }, } } let reason = reason.ok_or(Error::ParseError("Reason doesn’t contain a valid reason."))?; Ok(ReasonElement { reason: reason, text: text, }) } } impl Into for ReasonElement { fn into(self) -> Element { let reason: Element = self.reason.into(); Element::builder("reason") .append(reason) .append(self.text) .build() } } impl IntoElements for ReasonElement { fn into_elements(self, emitter: &mut ElementEmitter) { emitter.append_child(self.into()); } } #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Sid(String); impl FromStr for Sid { type Err = Error; fn from_str(s: &str) -> Result { // TODO: implement the NMTOKEN restrictions: https://www.w3.org/TR/2000/WD-xml-2e-20000814#NT-Nmtoken Ok(Sid(String::from(s))) } } impl IntoAttributeValue for Sid { fn into_attribute_value(self) -> Option { return Some(self.0); } } #[derive(Debug, Clone)] pub struct Jingle { pub action: Action, pub initiator: Option, pub responder: Option, pub sid: Sid, pub contents: Vec, pub reason: Option, pub other: Vec, } impl TryFrom for Jingle { type Error = Error; fn try_from(root: Element) -> Result { if !root.is("jingle", ns::JINGLE) { return Err(Error::ParseError("This is not a Jingle element.")); } let mut jingle = Jingle { action: get_attr!(root, "action", required), initiator: get_attr!(root, "initiator", optional), responder: get_attr!(root, "responder", optional), sid: get_attr!(root, "sid", required), contents: vec!(), reason: None, other: vec!(), }; for child in root.children().cloned() { if child.is("content", ns::JINGLE) { let content = Content::try_from(child)?; jingle.contents.push(content); } else if child.is("reason", ns::JINGLE) { if jingle.reason.is_some() { return Err(Error::ParseError("Jingle must not have more than one reason.")); } let reason = ReasonElement::try_from(child)?; jingle.reason = Some(reason); } else { jingle.other.push(child); } } Ok(jingle) } } impl Into for Jingle { fn into(self) -> Element { Element::builder("jingle") .ns(ns::JINGLE) .attr("action", self.action) .attr("initiator", match self.initiator { Some(initiator) => Some(String::from(initiator)), None => None }) .attr("responder", match self.responder { Some(responder) => Some(String::from(responder)), None => None }) .attr("sid", self.sid) .append(self.contents) .append(self.reason) .build() } } #[cfg(test)] mod tests { use super::*; #[test] fn test_simple() { let elem: Element = "".parse().unwrap(); let jingle = Jingle::try_from(elem).unwrap(); assert_eq!(jingle.action, Action::SessionInitiate); assert_eq!(jingle.sid, "coucou"); } #[test] fn test_invalid_jingle() { let elem: Element = "".parse().unwrap(); let error = Jingle::try_from(elem).unwrap_err(); let message = match error { Error::ParseError(string) => string, _ => panic!(), }; assert_eq!(message, "Required attribute 'action' missing."); let elem: Element = "".parse().unwrap(); let error = Jingle::try_from(elem).unwrap_err(); let message = match error { Error::ParseError(string) => string, _ => panic!(), }; assert_eq!(message, "Required attribute 'sid' missing."); let elem: Element = "".parse().unwrap(); let error = Jingle::try_from(elem).unwrap_err(); let message = match error { Error::ParseError(string) => string, _ => panic!(), }; assert_eq!(message, "Unknown value for 'action' attribute."); } #[test] fn test_content() { let elem: Element = "".parse().unwrap(); let jingle = Jingle::try_from(elem).unwrap(); assert_eq!(jingle.contents[0].creator, Creator::Initiator); assert_eq!(jingle.contents[0].name, "coucou"); assert_eq!(jingle.contents[0].senders, Senders::Both); assert_eq!(jingle.contents[0].disposition, "session"); let elem: Element = "".parse().unwrap(); let jingle = Jingle::try_from(elem).unwrap(); assert_eq!(jingle.contents[0].senders, Senders::Both); let elem: Element = "".parse().unwrap(); let jingle = Jingle::try_from(elem).unwrap(); assert_eq!(jingle.contents[0].disposition, "early-session"); } #[test] fn test_invalid_content() { let elem: Element = "".parse().unwrap(); let error = Jingle::try_from(elem).unwrap_err(); let message = match error { Error::ParseError(string) => string, _ => panic!(), }; assert_eq!(message, "Required attribute 'creator' missing."); let elem: Element = "".parse().unwrap(); let error = Jingle::try_from(elem).unwrap_err(); let message = match error { Error::ParseError(string) => string, _ => panic!(), }; assert_eq!(message, "Required attribute 'name' missing."); let elem: Element = "".parse().unwrap(); let error = Jingle::try_from(elem).unwrap_err(); let message = match error { Error::ParseError(string) => string, _ => panic!(), }; assert_eq!(message, "Unknown value for 'creator' attribute."); let elem: Element = "".parse().unwrap(); let error = Jingle::try_from(elem).unwrap_err(); let message = match error { Error::ParseError(string) => string, _ => panic!(), }; assert_eq!(message, "Unknown value for 'senders' attribute."); let elem: Element = "".parse().unwrap(); let error = Jingle::try_from(elem).unwrap_err(); let message = match error { Error::ParseError(string) => string, _ => panic!(), }; assert_eq!(message, "Unknown value for 'senders' attribute."); } #[test] fn test_reason() { let elem: Element = "".parse().unwrap(); let jingle = Jingle::try_from(elem).unwrap(); let reason = jingle.reason.unwrap(); assert_eq!(reason.reason, Reason::Success); assert_eq!(reason.text, None); let elem: Element = "coucou".parse().unwrap(); let jingle = Jingle::try_from(elem).unwrap(); let reason = jingle.reason.unwrap(); assert_eq!(reason.reason, Reason::Success); assert_eq!(reason.text, Some(String::from("coucou"))); } #[test] fn test_invalid_reason() { let elem: Element = "".parse().unwrap(); let error = Jingle::try_from(elem).unwrap_err(); let message = match error { Error::ParseError(string) => string, _ => panic!(), }; assert_eq!(message, "Reason doesn’t contain a valid reason."); let elem: Element = "".parse().unwrap(); let error = Jingle::try_from(elem).unwrap_err(); let message = match error { Error::ParseError(string) => string, _ => panic!(), }; assert_eq!(message, "Unknown reason."); let elem: Element = "".parse().unwrap(); let error = Jingle::try_from(elem).unwrap_err(); let message = match error { Error::ParseError(string) => string, _ => panic!(), }; assert_eq!(message, "Reason contains a foreign element."); let elem: Element = "".parse().unwrap(); let error = Jingle::try_from(elem).unwrap_err(); let message = match error { Error::ParseError(string) => string, _ => panic!(), }; assert_eq!(message, "Jingle must not have more than one reason."); let elem: Element = "".parse().unwrap(); let error = Jingle::try_from(elem).unwrap_err(); let message = match error { Error::ParseError(string) => string, _ => panic!(), }; assert_eq!(message, "Reason must not have more than one text."); } }