Add a Jingle parser.

Emmanuel Gil Peyrot created

Change summary

src/jingle.rs | 491 +++++++++++++++++++++++++++++++++++++++++++++++++++++
src/lib.rs    |   1 
src/ns.rs     |   1 
3 files changed, 493 insertions(+)

Detailed changes

src/jingle.rs 🔗

@@ -0,0 +1,491 @@
+extern crate minidom;
+
+use std::str::FromStr;
+
+use minidom::Element;
+
+use error::Error;
+use ns::{JINGLE_NS};
+
+#[derive(Debug, PartialEq)]
+pub enum Action {
+    ContentAccept,
+    ContentAdd,
+    ContentModify,
+    ContentReject,
+    ContentRemove,
+    DescriptionInfo,
+    SecurityInfo,
+    SessionAccept,
+    SessionInfo,
+    SessionInitiate,
+    SessionTerminate,
+    TransportAccept,
+    TransportInfo,
+    TransportReject,
+    TransportReplace,
+}
+
+impl FromStr for Action {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Action, Error> {
+        if s == "content-accept" {
+            Ok(Action::ContentAccept)
+        } else if s == "content-add" {
+            Ok(Action::ContentAdd)
+        } else if s == "content-modify" {
+            Ok(Action::ContentModify)
+        } else if s == "content-reject" {
+            Ok(Action::ContentReject)
+        } else if s == "content-remove" {
+            Ok(Action::ContentRemove)
+        } else if s == "description-info" {
+            Ok(Action::DescriptionInfo)
+        } else if s == "security-info" {
+            Ok(Action::SecurityInfo)
+        } else if s == "session-accept" {
+            Ok(Action::SessionAccept)
+        } else if s == "session-info" {
+            Ok(Action::SessionInfo)
+        } else if s == "session-initiate" {
+            Ok(Action::SessionInitiate)
+        } else if s == "session-terminate" {
+            Ok(Action::SessionTerminate)
+        } else if s == "transport-accept" {
+            Ok(Action::TransportAccept)
+        } else if s == "transport-info" {
+            Ok(Action::TransportInfo)
+        } else if s == "transport-reject" {
+            Ok(Action::TransportReject)
+        } else if s == "transport-replace" {
+            Ok(Action::TransportReplace)
+        } else {
+            Err(Error::ParseError("Unknown action."))
+        }
+    }
+}
+
+// TODO: use a real JID type.
+type Jid = String;
+
+#[derive(Debug, PartialEq)]
+pub enum Creator {
+    Initiator,
+    Responder,
+}
+
+impl FromStr for Creator {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Creator, Error> {
+        if s == "initiator" {
+            Ok(Creator::Initiator)
+        } else if s == "responder" {
+            Ok(Creator::Responder)
+        } else {
+            Err(Error::ParseError("Unknown creator."))
+        }
+    }
+}
+
+#[derive(Debug, PartialEq)]
+pub enum Senders {
+    Both,
+    Initiator,
+    None_,
+    Responder,
+}
+
+impl FromStr for Senders {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Senders, Error> {
+        if s == "both" {
+            Ok(Senders::Both)
+        } else if s == "initiator" {
+            Ok(Senders::Initiator)
+        } else if s == "none" {
+            Ok(Senders::None_)
+        } else if s == "responder" {
+            Ok(Senders::Responder)
+        } else {
+            Err(Error::ParseError("Unknown senders."))
+        }
+    }
+}
+
+#[derive(Debug)]
+pub struct Content {
+    pub creator: Creator,
+    pub disposition: String,
+    pub name: String,
+    pub senders: Senders,
+    pub description: String,
+    pub transport: String,
+    pub security: Option<String>,
+}
+
+#[derive(Debug, 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<Reason, Error> {
+        if s == "alternative-session" {
+            Ok(Reason::AlternativeSession)
+        } else if s == "busy" {
+            Ok(Reason::Busy)
+        } else if s == "cancel" {
+            Ok(Reason::Cancel)
+        } else if s == "connectivity-error" {
+            Ok(Reason::ConnectivityError)
+        } else if s == "decline" {
+            Ok(Reason::Decline)
+        } else if s == "expired" {
+            Ok(Reason::Expired)
+        } else if s == "failed-application" {
+            Ok(Reason::FailedApplication)
+        } else if s == "failed-transport" {
+            Ok(Reason::FailedTransport)
+        } else if s == "general-error" {
+            Ok(Reason::GeneralError)
+        } else if s == "gone" {
+            Ok(Reason::Gone)
+        } else if s == "incompatible-parameters" {
+            Ok(Reason::IncompatibleParameters)
+        } else if s == "media-error" {
+            Ok(Reason::MediaError)
+        } else if s == "security-error" {
+            Ok(Reason::SecurityError)
+        } else if s == "success" {
+            Ok(Reason::Success)
+        } else if s == "timeout" {
+            Ok(Reason::Timeout)
+        } else if s == "unsupported-applications" {
+            Ok(Reason::UnsupportedApplications)
+        } else if s == "unsupported-transports" {
+            Ok(Reason::UnsupportedTransports)
+        } else {
+            Err(Error::ParseError("Unknown reason."))
+        }
+    }
+}
+
+#[derive(Debug)]
+pub struct ReasonElement {
+    pub reason: Reason,
+    pub text: Option<String>,
+}
+
+#[derive(Debug)]
+pub struct Jingle {
+    pub action: Action,
+    pub initiator: Option<Jid>,
+    pub responder: Option<Jid>,
+    pub sid: String,
+    pub contents: Vec<Content>,
+    pub reason: Option<ReasonElement>,
+    //pub other: Vec<Element>,
+}
+
+pub fn parse_jingle(root: &Element) -> Result<Jingle, Error> {
+    assert!(root.is("jingle", JINGLE_NS));
+    let mut contents: Vec<Content> = vec!();
+
+    let action = root.attr("action")
+                     .ok_or(Error::ParseError("Jingle must have an 'action' attribute."))?
+                     .parse()?;
+    let initiator = root.attr("initiator")
+                        .and_then(|initiator| initiator.parse().ok());
+    let responder = root.attr("responder")
+                        .and_then(|responder| responder.parse().ok());
+    let sid = root.attr("sid")
+                  .ok_or(Error::ParseError("Jingle must have a 'sid' attribute."))?;
+    let mut reason_element = None;
+
+    for child in root.children() {
+        if child.is("content", JINGLE_NS) {
+            let creator = child.attr("creator")
+                               .ok_or(Error::ParseError("Content must have a 'creator' attribute."))?
+                               .parse()?;
+            let disposition = child.attr("disposition")
+                                   .unwrap_or("session");
+            let name = child.attr("name")
+                            .ok_or(Error::ParseError("Content must have a 'name' attribute."))?;
+            let senders = child.attr("senders")
+                               .unwrap_or("both")
+                               .parse()?;
+            let mut description = None;
+            let mut transport = None;
+            let mut security = None;
+            for stuff in child.children() {
+                if stuff.name() == "description" {
+                    if description.is_some() {
+                        return Err(Error::ParseError("Content must not have more than one description."));
+                    }
+                    description = Some(stuff.ns().ok_or(Error::ParseError("Description without a namespace."))?);
+                } else if stuff.name() == "transport" {
+                    if transport.is_some() {
+                        return Err(Error::ParseError("Content must not have more than one transport."));
+                    }
+                    transport = Some(stuff.ns().ok_or(Error::ParseError("Transport without a namespace."))?);
+                } else if stuff.name() == "security" {
+                    if security.is_some() {
+                        return Err(Error::ParseError("Content must not have more than one security."));
+                    }
+                    security = stuff.ns().and_then(|ns| ns.parse().ok());
+                }
+            }
+            if description.is_none() {
+                return Err(Error::ParseError("Content must have one description."));
+            }
+            if transport.is_none() {
+                return Err(Error::ParseError("Content must have one transport."));
+            }
+            let description = description.unwrap().to_owned();
+            let transport = transport.unwrap().to_owned();
+            contents.push(Content {
+                creator: creator,
+                disposition: disposition.to_owned(),
+                name: name.to_owned(),
+                senders: senders,
+                description: description,
+                transport: transport,
+                security: security.to_owned(),
+            });
+        } else if child.is("reason", JINGLE_NS) {
+            if reason_element.is_some() {
+                return Err(Error::ParseError("Jingle must not have more than one reason."));
+            }
+            let mut reason = None;
+            let mut text = None;
+            for stuff in child.children() {
+                if stuff.ns() != Some(JINGLE_NS) {
+                    return Err(Error::ParseError("Reason contains a foreign element."));
+                }
+                let name = stuff.name();
+                if name == "text" {
+                    text = Some(stuff.text());
+                } else {
+                    reason = Some(name.parse()?);
+                }
+            }
+            if reason.is_none() {
+                return Err(Error::ParseError("Reason doesn’t contain a valid reason."));
+            }
+            reason_element = Some(ReasonElement {
+                reason: reason.unwrap(),
+                text: text,
+            });
+        } else {
+            return Err(Error::ParseError("Unknown element in jingle."));
+        }
+    }
+
+    return Ok(Jingle {
+        action: action,
+        initiator: initiator,
+        responder: responder,
+        sid: sid.to_owned(),
+        contents: contents,
+        reason: reason_element,
+    });
+}
+
+#[cfg(test)]
+mod tests {
+    use minidom::Element;
+    use error::Error;
+    use jingle;
+
+    #[test]
+    fn test_simple() {
+        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'/>".parse().unwrap();
+        let jingle = jingle::parse_jingle(&elem).unwrap();
+        assert_eq!(jingle.action, jingle::Action::SessionInitiate);
+        assert_eq!(jingle.sid, "coucou");
+    }
+
+    #[test]
+    fn test_invalid_jingle() {
+        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1'/>".parse().unwrap();
+        let error = jingle::parse_jingle(&elem).unwrap_err();
+        let message = match error {
+            Error::ParseError(string) => string,
+            _ => panic!(),
+        };
+        assert_eq!(message, "Jingle must have an 'action' attribute.");
+
+        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-info'/>".parse().unwrap();
+        let error = jingle::parse_jingle(&elem).unwrap_err();
+        let message = match error {
+            Error::ParseError(string) => string,
+            _ => panic!(),
+        };
+        assert_eq!(message, "Jingle must have a 'sid' attribute.");
+
+        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='coucou' sid='coucou'/>".parse().unwrap();
+        let error = jingle::parse_jingle(&elem).unwrap_err();
+        let message = match error {
+            Error::ParseError(string) => string,
+            _ => panic!(),
+        };
+        assert_eq!(message, "Unknown action.");
+
+        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-accept' sid='coucou'><coucou/></jingle>".parse().unwrap();
+        let error = jingle::parse_jingle(&elem).unwrap_err();
+        let message = match error {
+            Error::ParseError(string) => string,
+            _ => panic!(),
+        };
+        assert_eq!(message, "Unknown element in jingle.");
+    }
+
+    #[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/></content></jingle>".parse().unwrap();
+        let jingle = jingle::parse_jingle(&elem).unwrap();
+        assert_eq!(jingle.contents[0].creator, jingle::Creator::Initiator);
+        assert_eq!(jingle.contents[0].name, "coucou");
+        assert_eq!(jingle.contents[0].senders, jingle::Senders::Both);
+        assert_eq!(jingle.contents[0].disposition, "session");
+        println!("{:#?}", jingle);
+
+        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();
+        let jingle = jingle::parse_jingle(&elem).unwrap();
+        assert_eq!(jingle.contents[0].senders, jingle::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/></content></jingle>".parse().unwrap();
+        let jingle = jingle::parse_jingle(&elem).unwrap();
+        assert_eq!(jingle.contents[0].disposition, "early-session");
+    }
+
+    #[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::parse_jingle(&elem).unwrap_err();
+        let message = match error {
+            Error::ParseError(string) => string,
+            _ => panic!(),
+        };
+        assert_eq!(message, "Content must have a 'creator' attribute.");
+
+        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='initiator'/></jingle>".parse().unwrap();
+        let error = jingle::parse_jingle(&elem).unwrap_err();
+        let message = match error {
+            Error::ParseError(string) => string,
+            _ => panic!(),
+        };
+        assert_eq!(message, "Content must have a 'name' attribute.");
+
+        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::parse_jingle(&elem).unwrap_err();
+        let message = match error {
+            Error::ParseError(string) => string,
+            _ => panic!(),
+        };
+        assert_eq!(message, "Unknown creator.");
+
+        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::parse_jingle(&elem).unwrap_err();
+        let message = match error {
+            Error::ParseError(string) => string,
+            _ => panic!(),
+        };
+        assert_eq!(message, "Unknown senders.");
+
+        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::parse_jingle(&elem).unwrap_err();
+        let message = match error {
+            Error::ParseError(string) => string,
+            _ => panic!(),
+        };
+        assert_eq!(message, "Unknown senders.");
+
+        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='initiator' name='coucou'/></jingle>".parse().unwrap();
+        let error = jingle::parse_jingle(&elem).unwrap_err();
+        let message = match error {
+            Error::ParseError(string) => string,
+            _ => panic!(),
+        };
+        assert_eq!(message, "Content must have one description.");
+
+        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><content creator='initiator' name='coucou'><description/></content></jingle>".parse().unwrap();
+        let error = jingle::parse_jingle(&elem).unwrap_err();
+        let message = match error {
+            Error::ParseError(string) => string,
+            _ => panic!(),
+        };
+        assert_eq!(message, "Content must have one transport.");
+    }
+
+    #[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::parse_jingle(&elem).unwrap();
+        let reason = jingle.reason.unwrap();
+        assert_eq!(reason.reason, jingle::Reason::Success);
+        assert_eq!(reason.text, None);
+
+        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::parse_jingle(&elem).unwrap();
+        let reason = jingle.reason.unwrap();
+        assert_eq!(reason.reason, jingle::Reason::Success);
+        assert_eq!(reason.text, Some(String::from("coucou")));
+    }
+
+    #[test]
+    fn test_invalid_reason() {
+        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason/></jingle>".parse().unwrap();
+        let error = jingle::parse_jingle(&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 = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><a/></reason></jingle>".parse().unwrap();
+        let error = jingle::parse_jingle(&elem).unwrap_err();
+        let message = match error {
+            Error::ParseError(string) => string,
+            _ => panic!(),
+        };
+        assert_eq!(message, "Unknown reason.");
+
+        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();
+        let error = jingle::parse_jingle(&elem).unwrap_err();
+        let message = match error {
+            Error::ParseError(string) => string,
+            _ => panic!(),
+        };
+        assert_eq!(message, "Reason contains a foreign element.");
+
+        let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><decline/></reason><reason/></jingle>".parse().unwrap();
+        let error = jingle::parse_jingle(&elem).unwrap_err();
+        let message = match error {
+            Error::ParseError(string) => string,
+            _ => panic!(),
+        };
+        assert_eq!(message, "Jingle must not have more than one reason.");
+    }
+}

src/lib.rs 🔗

@@ -7,3 +7,4 @@ pub mod disco;
 pub mod data_forms;
 pub mod media_element;
 pub mod ecaps2;
+pub mod jingle;

src/ns.rs 🔗

@@ -1,3 +1,4 @@
 pub const DISCO_INFO_NS: &'static str = "http://jabber.org/protocol/disco#info";
 pub const DATA_FORMS_NS: &'static str = "jabber:x:data";
 pub const MEDIA_ELEMENT_NS: &'static str = "urn:xmpp:media-element";
+pub const JINGLE_NS: &'static str = "urn:xmpp:jingle:1";