From aae435c4d95d131aecfd9f78fa621f272131bb7b Mon Sep 17 00:00:00 2001 From: Emmanuel Gil Peyrot Date: Sun, 28 May 2017 16:30:43 +0100 Subject: [PATCH] Add a roster parser/serialiser. --- ChangeLog | 6 ++ src/lib.rs | 3 + src/ns.rs | 3 + src/roster.rs | 286 ++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 298 insertions(+) create mode 100644 src/roster.rs diff --git a/ChangeLog b/ChangeLog index 375c06fcb35bd58614f282ff681cc7e2eb50046d..63f21648c286ded9afd6544e967e512fa94914ca 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,9 @@ +Version 0.?.?: +2017-??-?? Emmanuel Gil Peyrot + * New parsers/serialisers: + - Implementation of the roster management protocol defined in + RFC 6121 §2. + Version 0.4.0: 2017-05-28 Emmanuel Gil Peyrot * Incompatible changes: diff --git a/src/lib.rs b/src/lib.rs index 50ed56e67d624c853a8c32ed5c32b33e3c0fcbee..c6f11031aaebd1b5ac05a32f3a2296c61f0b91a0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -62,6 +62,9 @@ pub mod iq; /// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core pub mod stanza_error; +/// RFC 6121: Extensible Messaging and Presence Protocol (XMPP): Instant Messaging and Presence +pub mod roster; + /// XEP-0004: Data Forms pub mod data_forms; diff --git a/src/ns.rs b/src/ns.rs index 99865ae56c929772e5140ff82830de313af969a1..4f563e620f8e99c4fcd2b0269aa091f1c6d355a7 100644 --- a/src/ns.rs +++ b/src/ns.rs @@ -9,6 +9,9 @@ pub const JABBER_CLIENT: &'static str = "jabber:client"; /// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core pub const XMPP_STANZAS: &'static str = "urn:ietf:params:xml:ns:xmpp-stanzas"; +/// RFC 6121: Extensible Messaging and Presence Protocol (XMPP): Instant Messaging and Presence +pub const ROSTER: &'static str = "jabber:iq:roster"; + /// XEP-0004: Data Forms pub const DATA_FORMS: &'static str = "jabber:x:data"; diff --git a/src/roster.rs b/src/roster.rs new file mode 100644 index 0000000000000000000000000000000000000000..aaca6d8ef2582a1a489fbaf9ee3f257c86be078b --- /dev/null +++ b/src/roster.rs @@ -0,0 +1,286 @@ +// 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; + +type Group = String; + +#[derive(Debug, Clone, PartialEq)] +pub enum Subscription { + None, + From, + To, + Both, + Remove, +} + +impl FromStr for Subscription { + type Err = Error; + + fn from_str(s: &str) -> Result { + Ok(match s { + "none" => Subscription::None, + "from" => Subscription::From, + "to" => Subscription::To, + "both" => Subscription::Both, + "remove" => Subscription::Remove, + + _ => return Err(Error::ParseError("Unknown value for attribute 'subscription'.")), + }) + } +} + +impl IntoAttributeValue for Subscription { + fn into_attribute_value(self) -> Option { + Some(String::from(match self { + Subscription::None => "none", + Subscription::From => "from", + Subscription::To => "to", + Subscription::Both => "both", + Subscription::Remove => "remove", + })) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Item { + pub jid: Jid, + pub name: Option, + pub subscription: Option, + pub groups: Vec, +} + +impl TryFrom for Item { + type Error = Error; + + fn try_from(elem: Element) -> Result { + if !elem.is("item", ns::ROSTER) { + return Err(Error::ParseError("This is not a roster item element.")); + } + + let mut item = Item { + jid: get_attr!(elem, "jid", required), + name: get_attr!(elem, "name", optional), + subscription: get_attr!(elem, "subscription", optional), + groups: vec!(), + }; + for child in elem.children() { + if !child.is("group", ns::ROSTER) { + return Err(Error::ParseError("Unknown element in roster item element.")); + } + for _ in child.children() { + return Err(Error::ParseError("Roster item group can’t have children.")); + } + item.groups.push(child.text()); + } + Ok(item) + } +} + +impl Into for Item { + fn into(self) -> Element { + Element::builder("item") + .ns(ns::ROSTER) + .attr("jid", String::from(self.jid)) + .attr("name", self.name) + .attr("subscription", self.subscription) + .append(self.groups) + .build() + } +} + +impl IntoElements for Item { + fn into_elements(self, emitter: &mut ElementEmitter) { + emitter.append_child(self.into()); + } +} + +#[derive(Debug, Clone)] +pub struct Roster { + pub ver: Option, + pub items: Vec, +} + +impl TryFrom for Roster { + type Error = Error; + + fn try_from(elem: Element) -> Result { + if !elem.is("query", ns::ROSTER) { + return Err(Error::ParseError("This is not a roster element.")); + } + for (attr, _) in elem.attrs() { + if attr != "ver" { + return Err(Error::ParseError("Unknown attribute in roster element.")); + } + } + + let mut roster = Roster { + ver: get_attr!(elem, "ver", optional), + items: vec!(), + }; + for child in elem.children() { + if !child.is("item", ns::ROSTER) { + return Err(Error::ParseError("Unknown element in roster element.")); + } + let item = Item::try_from(child.clone())?; + roster.items.push(item); + } + Ok(roster) + } +} + +impl Into for Roster { + fn into(self) -> Element { + Element::builder("query") + .ns(ns::ROSTER) + .attr("ver", self.ver) + .append(self.items) + .build() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_get() { + let elem: Element = "".parse().unwrap(); + let roster = Roster::try_from(elem).unwrap(); + assert!(roster.ver.is_none()); + assert!(roster.items.is_empty()); + } + + #[test] + fn test_result() { + let elem: Element = "".parse().unwrap(); + let roster = Roster::try_from(elem).unwrap(); + assert_eq!(roster.ver, Some(String::from("ver7"))); + assert_eq!(roster.items.len(), 2); + + let elem: Element = "".parse().unwrap(); + let roster = Roster::try_from(elem).unwrap(); + assert_eq!(roster.ver, Some(String::from("ver9"))); + assert!(roster.items.is_empty()); + + let elem: Element = r#" + + + Friends + + + + +"#.parse().unwrap(); + let roster = Roster::try_from(elem).unwrap(); + assert_eq!(roster.ver, Some(String::from("ver11"))); + assert_eq!(roster.items.len(), 3); + assert_eq!(roster.items[0].jid, Jid::from_str("romeo@example.net").unwrap()); + assert_eq!(roster.items[0].name, Some(String::from("Romeo"))); + assert_eq!(roster.items[0].subscription, Some(Subscription::Both)); + assert_eq!(roster.items[0].groups, vec!(String::from("Friends"))); + } + + #[test] + fn test_set() { + let elem: Element = "".parse().unwrap(); + let roster = Roster::try_from(elem).unwrap(); + assert!(roster.ver.is_none()); + assert_eq!(roster.items.len(), 1); + + let elem: Element = r#" + + + Servants + + + +"#.parse().unwrap(); + let roster = Roster::try_from(elem).unwrap(); + assert!(roster.ver.is_none()); + assert_eq!(roster.items.len(), 1); + assert_eq!(roster.items[0].jid, Jid::from_str("nurse@example.com").unwrap()); + assert_eq!(roster.items[0].name, Some(String::from("Nurse"))); + assert_eq!(roster.items[0].groups.len(), 1); + assert_eq!(roster.items[0].groups[0], String::from("Servants")); + + let elem: Element = r#" + + + +"#.parse().unwrap(); + let roster = Roster::try_from(elem).unwrap(); + assert!(roster.ver.is_none()); + assert_eq!(roster.items.len(), 1); + assert_eq!(roster.items[0].jid, Jid::from_str("nurse@example.com").unwrap()); + assert!(roster.items[0].name.is_none()); + assert!(roster.items[0].groups.is_empty()); + assert_eq!(roster.items[0].subscription, Some(Subscription::Remove)); + } + + #[test] + fn test_invalid() { + let elem: Element = "".parse().unwrap(); + let error = Roster::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown element in roster element."); + + let elem: Element = "".parse().unwrap(); + let error = Roster::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown attribute in roster element."); + } + + #[test] + fn test_invalid_item() { + let elem: Element = "".parse().unwrap(); + let error = Roster::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'jid' missing."); + + /* + let elem: Element = "".parse().unwrap(); + let error = Roster::try_from(elem).unwrap_err(); + let error = match error { + Error::JidParseError(error) => error, + _ => panic!(), + }; + assert_eq!(error.description(), "Invalid JID, I guess?"); + */ + + let elem: Element = "".parse().unwrap(); + let error = Roster::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown element in roster item element."); + } +}