use xso::{AsXml, FromXml};
use crate::iq::IqSetPayload;
use crate::jingle_grouping::Group;
use crate::jingle_ibb::Transport as IbbTransport;
use crate::jingle_ice_udp::Transport as IceUdpTransport;
use crate::jingle_rtp::Description as RtpDescription;
use crate::jingle_s5b::Transport as Socks5Transport;
use crate::ns;
use alloc::{collections::BTreeMap, fmt};
use jid::Jid;
use minidom::Element;
use xso::error::Error;
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
);
generate_attribute!(
Disposition, "disposition", {
Inline => "inline",
Attachment => "attachment",
FormData => "form-data",
Signal => "signal",
Alert => "alert",
Icon => "icon",
Render => "render",
RecipientListHistory => "recipient-list-history",
Session => "session",
Aib => "aib",
EarlySession => "early-session",
RecipientList => "recipient-list",
Notification => "notification",
ByReference => "by-reference",
InfoPackage => "info-package",
RecordingSession => "recording-session",
}, Default = Session
);
generate_id!(
ContentId
);
#[derive(AsXml, Debug, Clone, PartialEq)]
#[xml()]
pub enum Description {
#[xml(transparent)]
Rtp(RtpDescription),
#[xml(transparent)]
Unknown(Element),
}
impl TryFrom<Element> for Description {
type Error = Error;
fn try_from(elem: Element) -> Result<Description, Error> {
Ok(if elem.is("description", ns::JINGLE_RTP) {
Description::Rtp(RtpDescription::try_from(elem)?)
} else if elem.name() == "description" {
Description::Unknown(elem)
} else {
return Err(Error::Other("Invalid description."));
})
}
}
impl ::xso::FromXml for Description {
type Builder = ::xso::minidom_compat::FromEventsViaElement<Description>;
fn from_events(
qname: ::xso::exports::rxml::QName,
attrs: ::xso::exports::rxml::AttrMap,
) -> Result<Self::Builder, ::xso::error::FromEventsError> {
if qname.1 != "description" {
return Err(::xso::error::FromEventsError::Mismatch { name: qname, attrs });
}
Self::Builder::new(qname, attrs)
}
}
impl From<RtpDescription> for Description {
fn from(desc: RtpDescription) -> Description {
Description::Rtp(desc)
}
}
#[derive(AsXml, Debug, Clone, PartialEq)]
#[xml()]
pub enum Transport {
#[xml(transparent)]
IceUdp(IceUdpTransport),
#[xml(transparent)]
Ibb(IbbTransport),
#[xml(transparent)]
Socks5(Socks5Transport),
#[xml(transparent)]
Unknown(Element),
}
impl TryFrom<Element> for Transport {
type Error = Error;
fn try_from(elem: Element) -> Result<Transport, Error> {
Ok(if elem.is("transport", ns::JINGLE_ICE_UDP) {
Transport::IceUdp(IceUdpTransport::try_from(elem)?)
} else if elem.is("transport", ns::JINGLE_IBB) {
Transport::Ibb(IbbTransport::try_from(elem)?)
} else if elem.is("transport", ns::JINGLE_S5B) {
Transport::Socks5(Socks5Transport::try_from(elem)?)
} else if elem.name() == "transport" {
Transport::Unknown(elem)
} else {
return Err(Error::Other("Invalid transport."));
})
}
}
impl ::xso::FromXml for Transport {
type Builder = ::xso::minidom_compat::FromEventsViaElement<Transport>;
fn from_events(
qname: ::xso::exports::rxml::QName,
attrs: ::xso::exports::rxml::AttrMap,
) -> Result<Self::Builder, ::xso::error::FromEventsError> {
if qname.1 != "transport" {
return Err(::xso::error::FromEventsError::Mismatch { name: qname, attrs });
}
Self::Builder::new(qname, attrs)
}
}
impl From<IceUdpTransport> for Transport {
fn from(transport: IceUdpTransport) -> Transport {
Transport::IceUdp(transport)
}
}
impl From<IbbTransport> for Transport {
fn from(transport: IbbTransport) -> Transport {
Transport::Ibb(transport)
}
}
impl From<Socks5Transport> for Transport {
fn from(transport: Socks5Transport) -> Transport {
Transport::Socks5(transport)
}
}
#[derive(FromXml, AsXml, Debug, Clone, PartialEq)]
#[xml(namespace = ns::JINGLE, name = "security")]
pub struct Security;
#[derive(FromXml, AsXml, Debug, Clone, PartialEq)]
#[xml(namespace = ns::JINGLE, name = "content")]
pub struct Content {
#[xml(attribute)]
pub creator: Creator,
#[xml(attribute(default))]
pub disposition: Disposition,
#[xml(attribute)]
pub name: ContentId,
#[xml(attribute(default))]
pub senders: Senders,
#[xml(child(default))]
pub description: Option<Description>,
#[xml(child(default))]
pub transport: Option<Transport>,
#[xml(child(default))]
pub security: Option<Security>,
}
impl Content {
pub fn new(creator: Creator, name: ContentId) -> Content {
Content {
creator,
name,
disposition: Disposition::Session,
senders: Senders::Both,
description: None,
transport: None,
security: None,
}
}
pub fn with_disposition(mut self, disposition: Disposition) -> Content {
self.disposition = disposition;
self
}
pub fn with_senders(mut self, senders: Senders) -> Content {
self.senders = senders;
self
}
pub fn with_description<D: Into<Description>>(mut self, description: D) -> Content {
self.description = Some(description.into());
self
}
pub fn with_transport<T: Into<Transport>>(mut self, transport: T) -> Content {
self.transport = Some(transport.into());
self
}
pub fn with_security(mut self, security: Security) -> Content {
self.security = Some(security);
self
}
}
#[derive(FromXml, AsXml, Debug, Clone, PartialEq)]
#[xml(namespace = ns::JINGLE)]
pub enum Reason {
#[xml(name = "alternative-session")]
AlternativeSession {
#[xml(extract(namespace = ns::JINGLE, name = "sid", default, fields(text(type_ = String))))]
sid: Option<String>,
},
#[xml(name = "busy")]
Busy,
#[xml(name = "cancel")]
Cancel,
#[xml(name = "connectivity-error")]
ConnectivityError,
#[xml(name = "decline")]
Decline,
#[xml(name = "expired")]
Expired,
#[xml(name = "failed-application")]
FailedApplication,
#[xml(name = "failed-transport")]
FailedTransport,
#[xml(name = "general-error")]
GeneralError,
#[xml(name = "gone")]
Gone,
#[xml(name = "incompatible-parameters")]
IncompatibleParameters,
#[xml(name = "media-error")]
MediaError,
#[xml(name = "security-error")]
SecurityError,
#[xml(name = "success")]
Success,
#[xml(name = "timeout")]
Timeout,
#[xml(name = "unsupported-applications")]
UnsupportedApplications,
#[xml(name = "unsupported-transports")]
UnsupportedTransports,
}
type Lang = String;
#[derive(FromXml, AsXml, Debug, Clone, PartialEq)]
#[xml(namespace = ns::JINGLE, name = "reason")]
pub struct ReasonElement {
#[xml(child)]
pub reason: Reason,
#[xml(extract(n = .., namespace = ns::JINGLE, name = "text", fields(
attribute(type_ = String, name = "xml:lang", default),
text(type_ = String),
)))]
pub texts: BTreeMap<Lang, String>,
}
impl fmt::Display for ReasonElement {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
write!(fmt, "{}", Element::from(self.reason.clone()).name())?;
if let Some(text) = self.texts.get("en") {
write!(fmt, ": {}", text)?;
} else if let Some(text) = self.texts.get("") {
write!(fmt, ": {}", text)?;
}
Ok(())
}
}
generate_id!(
SessionId
);
#[derive(FromXml, AsXml, Debug, Clone, PartialEq)]
#[xml(namespace = ns::JINGLE, name = "jingle")]
pub struct Jingle {
#[xml(attribute)]
pub action: Action,
#[xml(attribute(default))]
pub initiator: Option<Jid>,
#[xml(attribute(default))]
pub responder: Option<Jid>,
#[xml(attribute)]
pub sid: SessionId,
#[xml(child(n = ..))]
pub contents: Vec<Content>,
#[xml(child(default))]
pub reason: Option<ReasonElement>,
#[xml(child(default))]
pub group: Option<Group>,
#[xml(child(n = ..))]
pub other: Vec<Element>,
}
impl IqSetPayload for Jingle {}
impl Jingle {
pub fn new(action: Action, sid: SessionId) -> Jingle {
Jingle {
action,
sid,
initiator: None,
responder: None,
contents: Vec::new(),
reason: None,
group: None,
other: Vec::new(),
}
}
pub fn with_initiator(mut self, initiator: Jid) -> Jingle {
self.initiator = Some(initiator);
self
}
pub fn with_responder(mut self, responder: Jid) -> Jingle {
self.responder = Some(responder);
self
}
pub fn add_content(mut self, content: Content) -> Jingle {
self.contents.push(content);
self
}
pub fn set_reason(mut self, reason: ReasonElement) -> Jingle {
self.reason = Some(reason);
self
}
pub fn set_group(mut self, group: Group) -> Jingle {
self.group = Some(group);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use xso::error::FromElementError;
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(Action, 1);
assert_size!(Creator, 1);
assert_size!(Senders, 1);
assert_size!(Disposition, 1);
assert_size!(ContentId, 12);
assert_size!(Content, 156);
assert_size!(Reason, 12);
assert_size!(ReasonElement, 24);
assert_size!(SessionId, 12);
assert_size!(Jingle, 112);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(Action, 1);
assert_size!(Creator, 1);
assert_size!(Senders, 1);
assert_size!(Disposition, 1);
assert_size!(ContentId, 24);
assert_size!(Content, 312);
assert_size!(Reason, 24);
assert_size!(ReasonElement, 48);
assert_size!(SessionId, 24);
assert_size!(Jingle, 224);
}
#[test]
fn test_simple() {
let elem: Element =
"<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'/>"
.parse()
.unwrap();
let jingle = Jingle::try_from(elem).unwrap();
assert_eq!(jingle.action, Action::SessionInitiate);
assert_eq!(jingle.sid, SessionId(String::from("coucou")));
}
#[test]
fn test_invalid_jingle() {
let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1'/>".parse().unwrap();
let error = Jingle::try_from(elem).unwrap_err();
let message = match error {
FromElementError::Invalid(Error::Other(string)) => string,
_ => panic!(),
};
assert_eq!(
message,
"Required attribute field 'action' on Jingle element missing."
);
let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-info'/>"
.parse()
.unwrap();
let error = Jingle::try_from(elem).unwrap_err();
let message = match error {
FromElementError::Invalid(Error::Other(string)) => string,
_ => panic!(),
};
assert_eq!(
message,
"Required attribute field 'sid' on Jingle element missing."
);
let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='coucou' sid='coucou'/>"
.parse()
.unwrap();
let error = Jingle::try_from(elem).unwrap_err();
let message = match error {
FromElementError::Invalid(Error::TextParseError(string)) => string,
_ => panic!(),
};
assert_eq!(message.to_string(), "Unknown value for 'action' attribute.");
}
#[test]
fn test_content() {
let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='initiator' name='coucou'><description/><transport xmlns='urn:xmpp:jingle:transports:stub:0'/></content></jingle>".parse().unwrap();
let jingle = Jingle::try_from(elem).unwrap();
assert_eq!(jingle.contents[0].creator, Creator::Initiator);
assert_eq!(jingle.contents[0].name, ContentId(String::from("coucou")));
assert_eq!(jingle.contents[0].senders, Senders::Both);
assert_eq!(jingle.contents[0].disposition, Disposition::Session);
let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='initiator' name='coucou' senders='both'><description/><transport xmlns='urn:xmpp:jingle:transports:stub:0'/></content></jingle>".parse().unwrap();
let jingle = Jingle::try_from(elem).unwrap();
assert_eq!(jingle.contents[0].senders, Senders::Both);
let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='initiator' name='coucou' disposition='early-session'><description/><transport xmlns='urn:xmpp:jingle:transports:stub:0'/></content></jingle>".parse().unwrap();
let jingle = Jingle::try_from(elem).unwrap();
assert_eq!(jingle.contents[0].disposition, Disposition::EarlySession);
}
#[test]
fn test_invalid_content() {
let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content/></jingle>".parse().unwrap();
let error = Jingle::try_from(elem).unwrap_err();
let message = match error {
FromElementError::Invalid(Error::Other(string)) => string,
_ => panic!(),
};
assert_eq!(
message,
"Required attribute field 'creator' on Content element missing."
);
let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='initiator'/></jingle>".parse().unwrap();
let error = Jingle::try_from(elem).unwrap_err();
let message = match error {
FromElementError::Invalid(Error::Other(string)) => string,
_ => panic!(),
};
assert_eq!(
message,
"Required attribute field 'name' on Content element missing."
);
let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='coucou' name='coucou'/></jingle>".parse().unwrap();
let error = Jingle::try_from(elem).unwrap_err();
let message = match error {
FromElementError::Invalid(Error::TextParseError(string)) => string,
other => panic!("unexpected result: {:?}", other),
};
assert_eq!(
message.to_string(),
"Unknown value for 'creator' attribute."
);
let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='initiator' name='coucou' senders='coucou'/></jingle>".parse().unwrap();
let error = Jingle::try_from(elem).unwrap_err();
let message = match error {
FromElementError::Invalid(Error::TextParseError(string)) => string,
_ => panic!(),
};
assert_eq!(
message.to_string(),
"Unknown value for 'senders' attribute."
);
let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='initiator' name='coucou' senders=''/></jingle>".parse().unwrap();
let error = Jingle::try_from(elem).unwrap_err();
let message = match error {
FromElementError::Invalid(Error::TextParseError(string)) => string,
_ => panic!(),
};
assert_eq!(
message.to_string(),
"Unknown value for 'senders' attribute."
);
}
#[test]
fn test_reason() {
let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><success/></reason></jingle>".parse().unwrap();
let jingle = Jingle::try_from(elem).unwrap();
let reason = jingle.reason.unwrap();
assert_eq!(reason.reason, Reason::Success);
assert_eq!(reason.texts, BTreeMap::new());
let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><success/><text>coucou</text></reason></jingle>".parse().unwrap();
let jingle = Jingle::try_from(elem).unwrap();
let reason = jingle.reason.unwrap();
assert_eq!(reason.reason, Reason::Success);
assert_eq!(reason.texts.get(""), Some(&String::from("coucou")));
}
#[test]
fn test_missing_reason_text() {
let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason/></jingle>".parse().unwrap();
let error = Jingle::try_from(elem).unwrap_err();
let message = match error {
FromElementError::Invalid(Error::Other(string)) => string,
_ => panic!(),
};
assert_eq!(
message,
"Missing child field 'reason' in ReasonElement element."
);
}
#[test]
#[cfg_attr(feature = "disable-validation", should_panic = "Result::unwrap_err")]
fn test_invalid_child_in_reason() {
let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><decline/><a/></reason></jingle>".parse().unwrap();
let error = Jingle::try_from(elem).unwrap_err();
let message = match error {
FromElementError::Invalid(Error::Other(string)) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown child in ReasonElement element.");
}
#[test]
fn test_multiple_reason_children() {
let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><decline/></reason><reason/></jingle>".parse().unwrap();
let error = Jingle::try_from(elem).unwrap_err();
let message = match error {
FromElementError::Invalid(Error::Other(string)) => string,
_ => panic!(),
};
assert_eq!(
message,
"Jingle element must not have more than one child in field 'reason'."
);
}
#[test]
fn test_serialize_jingle() {
let reference: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='a73sjjvkla37jfea'><content xmlns='urn:xmpp:jingle:1' creator='initiator' name='this-is-a-stub'><description xmlns='urn:xmpp:jingle:apps:stub:0'/><transport xmlns='urn:xmpp:jingle:transports:stub:0'/></content></jingle>"
.parse()
.unwrap();
let jingle = Jingle {
action: Action::SessionInitiate,
initiator: None,
responder: None,
sid: SessionId(String::from("a73sjjvkla37jfea")),
contents: vec![Content {
creator: Creator::Initiator,
disposition: Disposition::default(),
name: ContentId(String::from("this-is-a-stub")),
senders: Senders::default(),
description: Some(Description::Unknown(
Element::builder("description", "urn:xmpp:jingle:apps:stub:0").build(),
)),
transport: Some(Transport::Unknown(
Element::builder("transport", "urn:xmpp:jingle:transports:stub:0").build(),
)),
security: None,
}],
reason: None,
group: None,
other: vec![],
};
let serialized: Element = jingle.into();
assert_eq!(serialized, reference);
}
}