Prepare for merge: Move all xmpp-parsers files into xmpp-parsers/

Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
This commit is contained in:
Maxime “pep” Buquet 2019-10-18 13:45:51 +02:00
commit dc76f296de
No known key found for this signature in database
GPG key ID: DEDA74AEECA9D0F2
79 changed files with 0 additions and 0 deletions

View file

@ -0,0 +1,72 @@
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 crate::message::MessagePayload;
generate_empty_element!(
/// Requests the attention of the recipient.
Attention,
"attention",
ATTENTION
);
impl MessagePayload for Attention {}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(not(feature = "disable-validation"))]
use crate::util::error::Error;
use crate::Element;
use std::convert::TryFrom;
#[test]
fn test_size() {
assert_size!(Attention, 0);
}
#[test]
fn test_simple() {
let elem: Element = "<attention xmlns='urn:xmpp:attention:0'/>".parse().unwrap();
Attention::try_from(elem).unwrap();
}
#[cfg(not(feature = "disable-validation"))]
#[test]
fn test_invalid_child() {
let elem: Element = "<attention xmlns='urn:xmpp:attention:0'><coucou/></attention>"
.parse()
.unwrap();
let error = Attention::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown child in attention element.");
}
#[cfg(not(feature = "disable-validation"))]
#[test]
fn test_invalid_attribute() {
let elem: Element = "<attention xmlns='urn:xmpp:attention:0' coucou=''/>"
.parse()
.unwrap();
let error = Attention::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown attribute in attention element.");
}
#[test]
fn test_serialise() {
let elem: Element = "<attention xmlns='urn:xmpp:attention:0'/>".parse().unwrap();
let attention = Attention;
let elem2: Element = attention.into();
assert_eq!(elem, elem2);
}
}

128
xmpp-parsers/src/avatar.rs Normal file
View file

@ -0,0 +1,128 @@
// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 crate::hashes::Sha1HexAttribute;
use crate::pubsub::PubSubPayload;
use crate::util::helpers::WhitespaceAwareBase64;
generate_element!(
/// Communicates information about an avatar.
Metadata, "metadata", AVATAR_METADATA,
children: [
/// List of information elements describing this avatar.
infos: Vec<Info> = ("info", AVATAR_METADATA) => Info
]
);
impl PubSubPayload for Metadata {}
generate_element!(
/// Communicates avatar metadata.
Info, "info", AVATAR_METADATA,
attributes: [
/// The size of the image data in bytes.
bytes: Required<u16> = "bytes",
/// The width of the image in pixels.
width: Option<u16> = "width",
/// The height of the image in pixels.
height: Option<u16> = "height",
/// The SHA-1 hash of the image data for the specified content-type.
id: Required<Sha1HexAttribute> = "id",
/// The IANA-registered content type of the image data.
type_: Required<String> = "type",
/// The http: or https: URL at which the image data file is hosted.
url: Option<String> = "url",
]
);
generate_element!(
/// The actual avatar data.
Data, "data", AVATAR_DATA,
text: (
/// Vector of bytes representing the avatars image.
data: WhitespaceAwareBase64<Vec<u8>>
)
);
impl PubSubPayload for Data {}
#[cfg(test)]
mod tests {
use super::*;
use crate::hashes::Algo;
#[cfg(not(feature = "disable-validation"))]
use crate::util::error::Error;
use crate::Element;
use std::convert::TryFrom;
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(Metadata, 12);
assert_size!(Info, 64);
assert_size!(Data, 12);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(Metadata, 24);
assert_size!(Info, 120);
assert_size!(Data, 24);
}
#[test]
fn test_simple() {
let elem: Element = "<metadata xmlns='urn:xmpp:avatar:metadata'>
<info bytes='12345' width='64' height='64'
id='111f4b3c50d7b0df729d299bc6f8e9ef9066971f'
type='image/png'/>
</metadata>"
.parse()
.unwrap();
let metadata = Metadata::try_from(elem).unwrap();
assert_eq!(metadata.infos.len(), 1);
let info = &metadata.infos[0];
assert_eq!(info.bytes, 12345);
assert_eq!(info.width, Some(64));
assert_eq!(info.height, Some(64));
assert_eq!(info.id.algo, Algo::Sha_1);
assert_eq!(info.type_, "image/png");
assert_eq!(info.url, None);
assert_eq!(
info.id.hash,
[
17, 31, 75, 60, 80, 215, 176, 223, 114, 157, 41, 155, 198, 248, 233, 239, 144, 102,
151, 31
]
);
let elem: Element = "<data xmlns='urn:xmpp:avatar:data'>AAAA</data>"
.parse()
.unwrap();
let data = Data::try_from(elem).unwrap();
assert_eq!(data.data, b"\0\0\0");
}
#[cfg(not(feature = "disable-validation"))]
#[test]
fn test_invalid() {
let elem: Element = "<data xmlns='urn:xmpp:avatar:data' id='coucou'/>"
.parse()
.unwrap();
let error = Data::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown attribute in data element.")
}
}

198
xmpp-parsers/src/bind.rs Normal file
View file

@ -0,0 +1,198 @@
// Copyright (c) 2018 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 crate::util::error::Error;
use crate::iq::{IqResultPayload, IqSetPayload};
use crate::ns;
use jid::{FullJid, Jid};
use crate::Element;
use std::str::FromStr;
use std::convert::TryFrom;
/// The request for resource binding, which is the process by which a client
/// can obtain a full JID and start exchanging on the XMPP network.
///
/// See https://xmpp.org/rfcs/rfc6120.html#bind
#[derive(Debug, Clone, PartialEq)]
pub struct BindQuery {
/// Requests this resource, the server may associate another one though.
///
/// If this is None, we request no particular resource, and a random one
/// will be affected by the server.
resource: Option<String>,
}
impl BindQuery {
/// Creates a resource binding request.
pub fn new(resource: Option<String>) -> BindQuery {
BindQuery { resource }
}
}
impl IqSetPayload for BindQuery {}
impl TryFrom<Element> for BindQuery {
type Error = Error;
fn try_from(elem: Element) -> Result<BindQuery, Error> {
check_self!(elem, "bind", BIND);
check_no_attributes!(elem, "bind");
let mut resource = None;
for child in elem.children() {
if resource.is_some() {
return Err(Error::ParseError("Bind can only have one child."));
}
if child.is("resource", ns::BIND) {
check_no_attributes!(child, "resource");
check_no_children!(child, "resource");
resource = Some(child.text());
} else {
return Err(Error::ParseError("Unknown element in bind request."));
}
}
Ok(BindQuery { resource })
}
}
impl From<BindQuery> for Element {
fn from(bind: BindQuery) -> Element {
Element::builder("bind")
.ns(ns::BIND)
.append_all(bind.resource.map(|resource|
Element::builder("resource")
.ns(ns::BIND)
.append(resource)))
.build()
}
}
/// The response for resource binding, containing the clients full JID.
///
/// See https://xmpp.org/rfcs/rfc6120.html#bind
#[derive(Debug, Clone, PartialEq)]
pub struct BindResponse {
/// The full JID returned by the server for this client.
jid: FullJid,
}
impl IqResultPayload for BindResponse {}
impl From<BindResponse> for FullJid {
fn from(bind: BindResponse) -> FullJid {
bind.jid
}
}
impl From<BindResponse> for Jid {
fn from(bind: BindResponse) -> Jid {
Jid::Full(bind.jid)
}
}
impl TryFrom<Element> for BindResponse {
type Error = Error;
fn try_from(elem: Element) -> Result<BindResponse, Error> {
check_self!(elem, "bind", BIND);
check_no_attributes!(elem, "bind");
let mut jid = None;
for child in elem.children() {
if jid.is_some() {
return Err(Error::ParseError("Bind can only have one child."));
}
if child.is("jid", ns::BIND) {
check_no_attributes!(child, "jid");
check_no_children!(child, "jid");
jid = Some(FullJid::from_str(&child.text())?);
} else {
return Err(Error::ParseError("Unknown element in bind response."));
}
}
Ok(BindResponse { jid: match jid {
None => return Err(Error::ParseError("Bind response must contain a jid element.")),
Some(jid) => jid,
} })
}
}
impl From<BindResponse> for Element {
fn from(bind: BindResponse) -> Element {
Element::builder("bind")
.ns(ns::BIND)
.append(Element::builder("jid").ns(ns::BIND).append(bind.jid))
.build()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(BindQuery, 12);
assert_size!(BindResponse, 36);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(BindQuery, 24);
assert_size!(BindResponse, 72);
}
#[test]
fn test_simple() {
let elem: Element = "<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/>"
.parse()
.unwrap();
let bind = BindQuery::try_from(elem).unwrap();
assert_eq!(bind.resource, None);
let elem: Element = "<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'><resource>Hello™</resource></bind>"
.parse()
.unwrap();
let bind = BindQuery::try_from(elem).unwrap();
// FIXME: “™” should be resourceprepd into “TM” here…
//assert_eq!(bind.resource.unwrap(), "HelloTM");
assert_eq!(bind.resource.unwrap(), "Hello™");
let elem: Element = "<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'><jid>coucou@linkmauve.fr/HelloTM</jid></bind>"
.parse()
.unwrap();
let bind = BindResponse::try_from(elem).unwrap();
assert_eq!(bind.jid, FullJid::new("coucou", "linkmauve.fr", "HelloTM"));
}
#[cfg(not(feature = "disable-validation"))]
#[test]
fn test_invalid_resource() {
let elem: Element = "<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'><resource attr='coucou'>resource</resource></bind>"
.parse()
.unwrap();
let error = BindQuery::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown attribute in resource element.");
let elem: Element = "<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'><resource><hello-world/>resource</resource></bind>"
.parse()
.unwrap();
let error = BindQuery::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown child in resource element.");
}
}

View file

@ -0,0 +1,223 @@
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 crate::util::error::Error;
use crate::iq::{IqGetPayload, IqResultPayload, IqSetPayload};
use crate::ns;
use jid::Jid;
use crate::Element;
use std::convert::TryFrom;
generate_empty_element!(
/// The element requesting the blocklist, the result iq will contain a
/// [BlocklistResult].
BlocklistRequest,
"blocklist",
BLOCKING
);
impl IqGetPayload for BlocklistRequest {}
macro_rules! generate_blocking_element {
($(#[$meta:meta])* $elem:ident, $name:tt) => (
$(#[$meta])*
#[derive(Debug, Clone)]
pub struct $elem {
/// List of JIDs affected by this command.
pub items: Vec<Jid>,
}
impl TryFrom<Element> for $elem {
type Error = Error;
fn try_from(elem: Element) -> Result<$elem, Error> {
check_self!(elem, $name, BLOCKING);
check_no_attributes!(elem, $name);
let mut items = vec!();
for child in elem.children() {
check_self!(child, "item", BLOCKING);
check_no_unknown_attributes!(child, "item", ["jid"]);
check_no_children!(child, "item");
items.push(get_attr!(child, "jid", Required));
}
Ok($elem { items })
}
}
impl From<$elem> for Element {
fn from(elem: $elem) -> Element {
Element::builder($name)
.ns(ns::BLOCKING)
.append_all(elem.items.into_iter().map(|jid| {
Element::builder("item")
.ns(ns::BLOCKING)
.attr("jid", jid)
}))
.build()
}
}
);
}
generate_blocking_element!(
/// The element containing the current blocklist, as a reply from
/// [BlocklistRequest].
BlocklistResult,
"blocklist"
);
impl IqResultPayload for BlocklistResult {}
// TODO: Prevent zero elements from being allowed.
generate_blocking_element!(
/// A query to block one or more JIDs.
Block,
"block"
);
impl IqSetPayload for Block {}
generate_blocking_element!(
/// A query to unblock one or more JIDs, or all of them.
///
/// Warning: not putting any JID there means clearing out the blocklist.
Unblock,
"unblock"
);
impl IqSetPayload for Unblock {}
generate_empty_element!(
/// The application-specific error condition when a message is blocked.
Blocked,
"blocked",
BLOCKING_ERRORS
);
#[cfg(test)]
mod tests {
use super::*;
use jid::BareJid;
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(BlocklistRequest, 0);
assert_size!(BlocklistResult, 12);
assert_size!(Block, 12);
assert_size!(Unblock, 12);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(BlocklistRequest, 0);
assert_size!(BlocklistResult, 24);
assert_size!(Block, 24);
assert_size!(Unblock, 24);
}
#[test]
fn test_simple() {
let elem: Element = "<blocklist xmlns='urn:xmpp:blocking'/>".parse().unwrap();
let request_elem = elem.clone();
BlocklistRequest::try_from(request_elem).unwrap();
let result_elem = elem.clone();
let result = BlocklistResult::try_from(result_elem).unwrap();
assert_eq!(result.items, vec!());
let elem: Element = "<block xmlns='urn:xmpp:blocking'/>".parse().unwrap();
let block = Block::try_from(elem).unwrap();
assert_eq!(block.items, vec!());
let elem: Element = "<unblock xmlns='urn:xmpp:blocking'/>".parse().unwrap();
let unblock = Unblock::try_from(elem).unwrap();
assert_eq!(unblock.items, vec!());
}
#[test]
fn test_items() {
let elem: Element = "<blocklist xmlns='urn:xmpp:blocking'><item jid='coucou@coucou'/><item jid='domain'/></blocklist>".parse().unwrap();
let two_items = vec![
Jid::Bare(BareJid {
node: Some(String::from("coucou")),
domain: String::from("coucou"),
}),
Jid::Bare(BareJid {
node: None,
domain: String::from("domain"),
}),
];
let result_elem = elem.clone();
let result = BlocklistResult::try_from(result_elem).unwrap();
assert_eq!(result.items, two_items);
let elem: Element = "<block xmlns='urn:xmpp:blocking'><item jid='coucou@coucou'/><item jid='domain'/></block>".parse().unwrap();
let block = Block::try_from(elem).unwrap();
assert_eq!(block.items, two_items);
let elem: Element = "<unblock xmlns='urn:xmpp:blocking'><item jid='coucou@coucou'/><item jid='domain'/></unblock>".parse().unwrap();
let unblock = Unblock::try_from(elem).unwrap();
assert_eq!(unblock.items, two_items);
}
#[cfg(not(feature = "disable-validation"))]
#[test]
fn test_invalid() {
let elem: Element = "<blocklist xmlns='urn:xmpp:blocking' coucou=''/>"
.parse()
.unwrap();
let request_elem = elem.clone();
let error = BlocklistRequest::try_from(request_elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown attribute in blocklist element.");
let result_elem = elem.clone();
let error = BlocklistResult::try_from(result_elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown attribute in blocklist element.");
let elem: Element = "<block xmlns='urn:xmpp:blocking' coucou=''/>"
.parse()
.unwrap();
let error = Block::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown attribute in block element.");
let elem: Element = "<unblock xmlns='urn:xmpp:blocking' coucou=''/>"
.parse()
.unwrap();
let error = Unblock::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown attribute in unblock element.");
}
#[cfg(not(feature = "disable-validation"))]
#[test]
fn test_non_empty_blocklist_request() {
let elem: Element = "<blocklist xmlns='urn:xmpp:blocking'><item jid='coucou@coucou'/><item jid='domain'/></blocklist>".parse().unwrap();
let error = BlocklistRequest::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown child in blocklist element.");
}
}

168
xmpp-parsers/src/bob.rs Normal file
View file

@ -0,0 +1,168 @@
// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 crate::hashes::{Hash, Algo};
use crate::util::helpers::Base64;
use crate::util::error::Error;
use minidom::IntoAttributeValue;
use std::str::FromStr;
/// A Content-ID, as defined in RFC2111.
///
/// The text value SHOULD be of the form algo+hash@bob.xmpp.org, this struct
/// enforces that format.
#[derive(Clone, Debug)]
pub struct ContentId {
hash: Hash,
}
impl FromStr for ContentId {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Error> {
let temp: Vec<_> = s.splitn(2, '@').collect();
let temp: Vec<_> = match temp[..] {
[lhs, rhs] => {
if rhs != "bob.xmpp.org" {
return Err(Error::ParseError("Wrong domain for cid URI."))
}
lhs.splitn(2, '+').collect()
},
_ => return Err(Error::ParseError("Missing @ in cid URI."))
};
let (algo, hex) = match temp[..] {
[lhs, rhs] => {
let algo = match lhs {
"sha1" => Algo::Sha_1,
"sha256" => Algo::Sha_256,
_ => unimplemented!(),
};
(algo, rhs)
},
_ => return Err(Error::ParseError("Missing + in cid URI."))
};
let hash = Hash::from_hex(algo, hex)?;
Ok(ContentId { hash })
}
}
impl IntoAttributeValue for ContentId {
fn into_attribute_value(self) -> Option<String> {
let algo = match self.hash.algo {
Algo::Sha_1 => "sha1",
Algo::Sha_256 => "sha256",
_ => unimplemented!(),
};
Some(format!("{}+{}@bob.xmpp.org", algo, self.hash.to_hex()))
}
}
generate_element!(
/// Request for an uncached cid file.
Data, "data", BOB,
attributes: [
/// The cid in question.
cid: Required<ContentId> = "cid",
/// How long to cache it (in seconds).
max_age: Option<usize> = "max-age",
/// The MIME type of the data being transmitted.
///
/// See the [IANA MIME Media Types Registry][1] for a list of
/// registered types, but unregistered or yet-to-be-registered are
/// accepted too.
///
/// [1]: https://www.iana.org/assignments/media-types/media-types.xhtml
type_: Option<String> = "type"
],
text: (
/// The actual data.
data: Base64<Vec<u8>>
)
);
#[cfg(test)]
mod tests {
use super::*;
use crate::Element;
use std::convert::TryFrom;
use std::error::Error as StdError;
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(ContentId, 28);
assert_size!(Data, 60);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(ContentId, 56);
assert_size!(Data, 120);
}
#[test]
fn test_simple() {
let cid: ContentId = "sha1+8f35fef110ffc5df08d579a50083ff9308fb6242@bob.xmpp.org".parse().unwrap();
assert_eq!(cid.hash.algo, Algo::Sha_1);
assert_eq!(cid.hash.hash, b"\x8f\x35\xfe\xf1\x10\xff\xc5\xdf\x08\xd5\x79\xa5\x00\x83\xff\x93\x08\xfb\x62\x42");
assert_eq!(cid.into_attribute_value().unwrap(), "sha1+8f35fef110ffc5df08d579a50083ff9308fb6242@bob.xmpp.org");
let elem: Element = "<data xmlns='urn:xmpp:bob' cid='sha1+8f35fef110ffc5df08d579a50083ff9308fb6242@bob.xmpp.org'/>".parse().unwrap();
let data = Data::try_from(elem).unwrap();
assert_eq!(data.cid.hash.algo, Algo::Sha_1);
assert_eq!(data.cid.hash.hash, b"\x8f\x35\xfe\xf1\x10\xff\xc5\xdf\x08\xd5\x79\xa5\x00\x83\xff\x93\x08\xfb\x62\x42");
assert!(data.max_age.is_none());
assert!(data.type_.is_none());
assert!(data.data.is_empty());
}
#[test]
fn invalid_cid() {
let error = "Hello world!".parse::<ContentId>().unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Missing @ in cid URI.");
let error = "Hello world@bob.xmpp.org".parse::<ContentId>().unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Missing + in cid URI.");
let error = "sha1+1234@coucou.linkmauve.fr".parse::<ContentId>().unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Wrong domain for cid URI.");
let error = "sha1+invalid@bob.xmpp.org".parse::<ContentId>().unwrap_err();
let message = match error {
Error::ParseIntError(error) => error,
_ => panic!(),
};
assert_eq!(message.description(), "invalid digit found in string");
}
#[test]
fn unknown_child() {
let elem: Element = "<data xmlns='urn:xmpp:bob'><coucou/></data>"
.parse()
.unwrap();
let error = Data::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown child in data element.");
}
}

View file

@ -0,0 +1,122 @@
// Copyright (c) 2018 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 jid::BareJid;
generate_attribute!(
/// Whether a conference bookmark should be joined automatically.
Autojoin,
"autojoin",
bool
);
generate_element!(
/// A conference bookmark.
Conference, "conference", BOOKMARKS,
attributes: [
/// Whether a conference bookmark should be joined automatically.
autojoin: Default<Autojoin> = "autojoin",
/// The JID of the conference.
jid: Required<BareJid> = "jid",
/// A user-defined name for this conference.
name: Required<String> = "name",
],
children: [
/// The nick the user will use to join this conference.
nick: Option<String> = ("nick", BOOKMARKS) => String,
/// The password required to join this conference.
password: Option<String> = ("password", BOOKMARKS) => String
]
);
generate_element!(
/// An URL bookmark.
Url, "url", BOOKMARKS,
attributes: [
/// A user-defined name for this URL.
name: Required<String> = "name",
/// The URL of this bookmark.
url: Required<String> = "url",
]
);
generate_element!(
/// Container element for multiple bookmarks.
#[derive(Default)]
Storage, "storage", BOOKMARKS,
children: [
/// Conferences the user has expressed an interest in.
conferences: Vec<Conference> = ("conference", BOOKMARKS) => Conference,
/// URLs the user is interested in.
urls: Vec<Url> = ("url", BOOKMARKS) => Url
]
);
impl Storage {
/// Create an empty bookmarks storage.
pub fn new() -> Storage {
Storage::default()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::util::compare_elements::NamespaceAwareCompare;
use crate::Element;
use std::convert::TryFrom;
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(Conference, 64);
assert_size!(Url, 24);
assert_size!(Storage, 24);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(Conference, 128);
assert_size!(Url, 48);
assert_size!(Storage, 48);
}
#[test]
fn empty() {
let elem: Element = "<storage xmlns='storage:bookmarks'/>".parse().unwrap();
let elem1 = elem.clone();
let storage = Storage::try_from(elem).unwrap();
assert_eq!(storage.conferences.len(), 0);
assert_eq!(storage.urls.len(), 0);
let elem2 = Element::from(Storage::new());
assert!(elem1.compare_to(&elem2));
}
#[test]
fn complete() {
let elem: Element = "<storage xmlns='storage:bookmarks'><url name='Example' url='https://example.org/'/><conference autojoin='true' jid='test-muc@muc.localhost' name='Test MUC'><nick>Coucou</nick><password>secret</password></conference></storage>".parse().unwrap();
let storage = Storage::try_from(elem).unwrap();
assert_eq!(storage.urls.len(), 1);
assert_eq!(storage.urls[0].name, "Example");
assert_eq!(storage.urls[0].url, "https://example.org/");
assert_eq!(storage.conferences.len(), 1);
assert_eq!(storage.conferences[0].autojoin, Autojoin::True);
assert_eq!(
storage.conferences[0].jid,
BareJid::new("test-muc", "muc.localhost")
);
assert_eq!(storage.conferences[0].name, "Test MUC");
assert_eq!(storage.conferences[0].clone().nick.unwrap(), "Coucou");
assert_eq!(storage.conferences[0].clone().password.unwrap(), "secret");
}
}

View file

@ -0,0 +1,119 @@
// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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/.
generate_attribute!(
/// Whether a conference bookmark should be joined automatically.
Autojoin,
"autojoin",
bool
);
generate_element!(
/// A conference bookmark.
Conference, "conference", BOOKMARKS2,
attributes: [
/// Whether a conference bookmark should be joined automatically.
autojoin: Default<Autojoin> = "autojoin",
/// A user-defined name for this conference.
name: Option<String> = "name",
],
children: [
/// The nick the user will use to join this conference.
nick: Option<String> = ("nick", BOOKMARKS2) => String,
/// The password required to join this conference.
password: Option<String> = ("password", BOOKMARKS2) => String
]
);
impl Conference {
/// Create a new conference.
pub fn new() -> Conference {
Conference {
autojoin: Autojoin::False,
name: None,
nick: None,
password: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::util::compare_elements::NamespaceAwareCompare;
use crate::Element;
use std::convert::TryFrom;
use crate::pubsub::pubsub::Item as PubSubItem;
use crate::pubsub::event::PubSubEvent;
use crate::ns;
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(Conference, 40);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(Conference, 80);
}
#[test]
fn simple() {
let elem: Element = "<conference xmlns='urn:xmpp:bookmarks:0'/>".parse().unwrap();
let elem1 = elem.clone();
let conference = Conference::try_from(elem).unwrap();
assert_eq!(conference.autojoin, Autojoin::False);
assert_eq!(conference.name, None);
assert_eq!(conference.nick, None);
assert_eq!(conference.password, None);
let elem2 = Element::from(Conference::new());
assert!(elem1.compare_to(&elem2));
}
#[test]
fn complete() {
let elem: Element = "<conference xmlns='urn:xmpp:bookmarks:0' autojoin='true' name='Test MUC'><nick>Coucou</nick><password>secret</password></conference>".parse().unwrap();
let conference = Conference::try_from(elem).unwrap();
assert_eq!(conference.autojoin, Autojoin::True);
assert_eq!(conference.name, Some(String::from("Test MUC")));
assert_eq!(conference.clone().nick.unwrap(), "Coucou");
assert_eq!(conference.clone().password.unwrap(), "secret");
}
#[test]
fn wrapped() {
let elem: Element = "<item xmlns='http://jabber.org/protocol/pubsub' id='test-muc@muc.localhost'><conference xmlns='urn:xmpp:bookmarks:0' autojoin='true' name='Test MUC'><nick>Coucou</nick><password>secret</password></conference></item>".parse().unwrap();
let item = PubSubItem::try_from(elem).unwrap();
let payload = item.payload.clone().unwrap();
let conference = Conference::try_from(payload).unwrap();
assert_eq!(conference.autojoin, Autojoin::True);
assert_eq!(conference.name, Some(String::from("Test MUC")));
assert_eq!(conference.clone().nick.unwrap(), "Coucou");
assert_eq!(conference.clone().password.unwrap(), "secret");
let elem: Element = "<event xmlns='http://jabber.org/protocol/pubsub#event'><items node='urn:xmpp:bookmarks:0'><item xmlns='http://jabber.org/protocol/pubsub#event' id='test-muc@muc.localhost'><conference xmlns='urn:xmpp:bookmarks:0' autojoin='true' name='Test MUC'><nick>Coucou</nick><password>secret</password></conference></item></items></event>".parse().unwrap();
let mut items = match PubSubEvent::try_from(elem) {
Ok(PubSubEvent::PublishedItems { node, items }) => {
assert_eq!(&node.0, ns::BOOKMARKS2);
items
},
_ => panic!(),
};
assert_eq!(items.len(), 1);
let item = items.pop().unwrap();
let payload = item.payload.clone().unwrap();
let conference = Conference::try_from(payload).unwrap();
assert_eq!(conference.autojoin, Autojoin::True);
assert_eq!(conference.name, Some(String::from("Test MUC")));
assert_eq!(conference.clone().nick.unwrap(), "Coucou");
assert_eq!(conference.clone().password.unwrap(), "secret");
}
}

341
xmpp-parsers/src/caps.rs Normal file
View file

@ -0,0 +1,341 @@
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 crate::data_forms::DataForm;
use crate::disco::{DiscoInfoQuery, DiscoInfoResult, Feature, Identity};
use crate::util::error::Error;
use crate::hashes::{Algo, Hash};
use crate::ns;
use crate::presence::PresencePayload;
use blake2::VarBlake2b;
use digest::{Digest, Input, VariableOutput};
use crate::Element;
use sha1::Sha1;
use sha2::{Sha256, Sha512};
use sha3::{Sha3_256, Sha3_512};
use std::convert::TryFrom;
/// Represents a capability hash for a given client.
#[derive(Debug, Clone)]
pub struct Caps {
/// Deprecated list of additional feature bundles.
pub ext: Option<String>,
/// A URI identifying an XMPP application.
pub node: String,
/// The hash of that applications
/// [disco#info](../disco/struct.DiscoInfoResult.html).
///
/// Warning: This protocol is insecure, you may want to switch to
/// [ecaps2](../ecaps2/index.html) instead, see [this
/// email](https://mail.jabber.org/pipermail/security/2009-July/000812.html).
pub hash: Hash,
}
impl PresencePayload for Caps {}
impl TryFrom<Element> for Caps {
type Error = Error;
fn try_from(elem: Element) -> Result<Caps, Error> {
check_self!(elem, "c", CAPS, "caps");
check_no_children!(elem, "caps");
check_no_unknown_attributes!(elem, "caps", ["hash", "ver", "ext", "node"]);
let ver: String = get_attr!(elem, "ver", Required);
let hash = Hash {
algo: get_attr!(elem, "hash", Required),
hash: base64::decode(&ver)?,
};
Ok(Caps {
ext: get_attr!(elem, "ext", Option),
node: get_attr!(elem, "node", Required),
hash,
})
}
}
impl From<Caps> for Element {
fn from(caps: Caps) -> Element {
Element::builder("c")
.ns(ns::CAPS)
.attr("ext", caps.ext)
.attr("hash", caps.hash.algo)
.attr("node", caps.node)
.attr("ver", base64::encode(&caps.hash.hash))
.build()
}
}
impl Caps {
/// Create a Caps element from its node and hash.
pub fn new<N: Into<String>>(node: N, hash: Hash) -> Caps {
Caps {
ext: None,
node: node.into(),
hash,
}
}
}
fn compute_item(field: &str) -> Vec<u8> {
let mut bytes = field.as_bytes().to_vec();
bytes.push(b'<');
bytes
}
fn compute_items<T, F: Fn(&T) -> Vec<u8>>(things: &[T], encode: F) -> Vec<u8> {
let mut string: Vec<u8> = vec![];
let mut accumulator: Vec<Vec<u8>> = vec![];
for thing in things {
let bytes = encode(thing);
accumulator.push(bytes);
}
// This works using the expected i;octet collation.
accumulator.sort();
for mut bytes in accumulator {
string.append(&mut bytes);
}
string
}
fn compute_features(features: &[Feature]) -> Vec<u8> {
compute_items(features, |feature| compute_item(&feature.var))
}
fn compute_identities(identities: &[Identity]) -> Vec<u8> {
compute_items(identities, |identity| {
let lang = identity.lang.clone().unwrap_or_default();
let name = identity.name.clone().unwrap_or_default();
let string = format!("{}/{}/{}/{}", identity.category, identity.type_, lang, name);
let bytes = string.as_bytes();
let mut vec = Vec::with_capacity(bytes.len());
vec.extend_from_slice(bytes);
vec.push(b'<');
vec
})
}
fn compute_extensions(extensions: &[DataForm]) -> Vec<u8> {
compute_items(extensions, |extension| {
let mut bytes = vec![];
// TODO: maybe handle the error case?
if let Some(ref form_type) = extension.form_type {
bytes.extend_from_slice(form_type.as_bytes());
}
bytes.push(b'<');
for field in extension.fields.clone() {
if field.var == "FORM_TYPE" {
continue;
}
bytes.append(&mut compute_item(&field.var));
bytes.append(&mut compute_items(&field.values, |value| {
compute_item(value)
}));
}
bytes
})
}
/// Applies the caps algorithm on the provided disco#info result, to generate
/// the hash input.
///
/// Warning: This protocol is insecure, you may want to switch to
/// [ecaps2](../ecaps2/index.html) instead, see [this
/// email](https://mail.jabber.org/pipermail/security/2009-July/000812.html).
pub fn compute_disco(disco: &DiscoInfoResult) -> Vec<u8> {
let identities_string = compute_identities(&disco.identities);
let features_string = compute_features(&disco.features);
let extensions_string = compute_extensions(&disco.extensions);
let mut final_string = vec![];
final_string.extend(identities_string);
final_string.extend(features_string);
final_string.extend(extensions_string);
final_string
}
fn get_hash_vec(hash: &[u8]) -> Vec<u8> {
let mut vec = Vec::with_capacity(hash.len());
vec.extend_from_slice(hash);
vec
}
/// Hashes the result of [compute_disco()] with one of the supported [hash
/// algorithms](../hashes/enum.Algo.html).
pub fn hash_caps(data: &[u8], algo: Algo) -> Result<Hash, String> {
Ok(Hash {
hash: match algo {
Algo::Sha_1 => {
let hash = Sha1::digest(data);
get_hash_vec(hash.as_slice())
}
Algo::Sha_256 => {
let hash = Sha256::digest(data);
get_hash_vec(hash.as_slice())
}
Algo::Sha_512 => {
let hash = Sha512::digest(data);
get_hash_vec(hash.as_slice())
}
Algo::Sha3_256 => {
let hash = Sha3_256::digest(data);
get_hash_vec(hash.as_slice())
}
Algo::Sha3_512 => {
let hash = Sha3_512::digest(data);
get_hash_vec(hash.as_slice())
}
Algo::Blake2b_256 => {
let mut hasher = VarBlake2b::new(32).unwrap();
hasher.input(data);
hasher.vec_result()
}
Algo::Blake2b_512 => {
let mut hasher = VarBlake2b::new(64).unwrap();
hasher.input(data);
hasher.vec_result()
}
Algo::Unknown(algo) => return Err(format!("Unknown algorithm: {}.", algo)),
},
algo,
})
}
/// Helper function to create the query for the disco#info corresponding to a
/// caps hash.
pub fn query_caps(caps: Caps) -> DiscoInfoQuery {
DiscoInfoQuery {
node: Some(format!("{}#{}", caps.node, base64::encode(&caps.hash.hash))),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::caps;
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(Caps, 52);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(Caps, 104);
}
#[test]
fn test_parse() {
let elem: Element = "<c xmlns='http://jabber.org/protocol/caps' hash='sha-256' node='coucou' ver='K1Njy3HZBThlo4moOD5gBGhn0U0oK7/CbfLlIUDi6o4='/>".parse().unwrap();
let caps = Caps::try_from(elem).unwrap();
assert_eq!(caps.node, String::from("coucou"));
assert_eq!(caps.hash.algo, Algo::Sha_256);
assert_eq!(
caps.hash.hash,
base64::decode("K1Njy3HZBThlo4moOD5gBGhn0U0oK7/CbfLlIUDi6o4=").unwrap()
);
}
#[cfg(not(feature = "disable-validation"))]
#[test]
fn test_invalid_child() {
let elem: Element = "<c xmlns='http://jabber.org/protocol/caps'><hash xmlns='urn:xmpp:hashes:2' algo='sha-256'>K1Njy3HZBThlo4moOD5gBGhn0U0oK7/CbfLlIUDi6o4=</hash></c>".parse().unwrap();
let error = Caps::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown child in caps element.");
}
#[test]
fn test_simple() {
let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='client' type='pc'/><feature var='http://jabber.org/protocol/disco#info'/></query>".parse().unwrap();
let disco = DiscoInfoResult::try_from(elem).unwrap();
let caps = caps::compute_disco(&disco);
assert_eq!(caps.len(), 50);
}
#[test]
fn test_xep_5_2() {
let elem: Element = r#"
<query xmlns='http://jabber.org/protocol/disco#info'
node='http://psi-im.org#q07IKJEyjvHSyhy//CH0CxmKi8w='>
<identity category='client' name='Exodus 0.9.1' type='pc'/>
<feature var='http://jabber.org/protocol/caps'/>
<feature var='http://jabber.org/protocol/disco#info'/>
<feature var='http://jabber.org/protocol/disco#items'/>
<feature var='http://jabber.org/protocol/muc'/>
</query>
"#
.parse()
.unwrap();
let data = b"client/pc//Exodus 0.9.1<http://jabber.org/protocol/caps<http://jabber.org/protocol/disco#info<http://jabber.org/protocol/disco#items<http://jabber.org/protocol/muc<";
let mut expected = Vec::with_capacity(data.len());
expected.extend_from_slice(data);
let disco = DiscoInfoResult::try_from(elem).unwrap();
let caps = caps::compute_disco(&disco);
assert_eq!(caps, expected);
let sha_1 = caps::hash_caps(&caps, Algo::Sha_1).unwrap();
assert_eq!(
sha_1.hash,
base64::decode("QgayPKawpkPSDYmwT/WM94uAlu0=").unwrap()
);
}
#[test]
fn test_xep_5_3() {
let elem: Element = r#"
<query xmlns='http://jabber.org/protocol/disco#info'
node='http://psi-im.org#q07IKJEyjvHSyhy//CH0CxmKi8w='>
<identity xml:lang='en' category='client' name='Psi 0.11' type='pc'/>
<identity xml:lang='el' category='client' name='Ψ 0.11' type='pc'/>
<feature var='http://jabber.org/protocol/caps'/>
<feature var='http://jabber.org/protocol/disco#info'/>
<feature var='http://jabber.org/protocol/disco#items'/>
<feature var='http://jabber.org/protocol/muc'/>
<x xmlns='jabber:x:data' type='result'>
<field var='FORM_TYPE' type='hidden'>
<value>urn:xmpp:dataforms:softwareinfo</value>
</field>
<field var='ip_version'>
<value>ipv4</value>
<value>ipv6</value>
</field>
<field var='os'>
<value>Mac</value>
</field>
<field var='os_version'>
<value>10.5.1</value>
</field>
<field var='software'>
<value>Psi</value>
</field>
<field var='software_version'>
<value>0.11</value>
</field>
</x>
</query>
"#
.parse()
.unwrap();
let expected = b"client/pc/el/\xce\xa8 0.11<client/pc/en/Psi 0.11<http://jabber.org/protocol/caps<http://jabber.org/protocol/disco#info<http://jabber.org/protocol/disco#items<http://jabber.org/protocol/muc<urn:xmpp:dataforms:softwareinfo<ip_version<ipv4<ipv6<os<Mac<os_version<10.5.1<software<Psi<software_version<0.11<".to_vec();
let disco = DiscoInfoResult::try_from(elem).unwrap();
let caps = caps::compute_disco(&disco);
assert_eq!(caps, expected);
let sha_1 = caps::hash_caps(&caps, Algo::Sha_1).unwrap();
assert_eq!(
sha_1.hash,
base64::decode("q07IKJEyjvHSyhy//CH0CxmKi8w=").unwrap()
);
}
}

127
xmpp-parsers/src/carbons.rs Normal file
View file

@ -0,0 +1,127 @@
// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 crate::forwarding::Forwarded;
use crate::iq::IqSetPayload;
use crate::message::MessagePayload;
generate_empty_element!(
/// Enable carbons for this session.
Enable,
"enable",
CARBONS
);
impl IqSetPayload for Enable {}
generate_empty_element!(
/// Disable a previously-enabled carbons.
Disable,
"disable",
CARBONS
);
impl IqSetPayload for Disable {}
generate_empty_element!(
/// Request the enclosing message to not be copied to other carbons-enabled
/// resources of the user.
Private,
"private",
CARBONS
);
impl MessagePayload for Private {}
generate_element!(
/// Wrapper for a message received on another resource.
Received, "received", CARBONS,
children: [
/// Wrapper for the enclosed message.
forwarded: Required<Forwarded> = ("forwarded", FORWARD) => Forwarded
]
);
impl MessagePayload for Received {}
generate_element!(
/// Wrapper for a message sent from another resource.
Sent, "sent", CARBONS,
children: [
/// Wrapper for the enclosed message.
forwarded: Required<Forwarded> = ("forwarded", FORWARD) => Forwarded
]
);
impl MessagePayload for Sent {}
#[cfg(test)]
mod tests {
use super::*;
use crate::Element;
use std::convert::TryFrom;
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(Enable, 0);
assert_size!(Disable, 0);
assert_size!(Private, 0);
assert_size!(Received, 212);
assert_size!(Sent, 212);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(Enable, 0);
assert_size!(Disable, 0);
assert_size!(Private, 0);
assert_size!(Received, 408);
assert_size!(Sent, 408);
}
#[test]
fn empty_elements() {
let elem: Element = "<enable xmlns='urn:xmpp:carbons:2'/>".parse().unwrap();
Enable::try_from(elem).unwrap();
let elem: Element = "<disable xmlns='urn:xmpp:carbons:2'/>".parse().unwrap();
Disable::try_from(elem).unwrap();
let elem: Element = "<private xmlns='urn:xmpp:carbons:2'/>".parse().unwrap();
Private::try_from(elem).unwrap();
}
#[test]
fn forwarded_elements() {
let elem: Element = "<received xmlns='urn:xmpp:carbons:2'>
<forwarded xmlns='urn:xmpp:forward:0'>
<message xmlns='jabber:client'
to='juliet@capulet.example/balcony'
from='romeo@montague.example/home'/>
</forwarded>
</received>"
.parse()
.unwrap();
let received = Received::try_from(elem).unwrap();
assert!(received.forwarded.stanza.is_some());
let elem: Element = "<sent xmlns='urn:xmpp:carbons:2'>
<forwarded xmlns='urn:xmpp:forward:0'>
<message xmlns='jabber:client'
to='juliet@capulet.example/balcony'
from='romeo@montague.example/home'/>
</forwarded>
</sent>"
.parse()
.unwrap();
let sent = Sent::try_from(elem).unwrap();
assert!(sent.forwarded.stanza.is_some());
}
}

View file

@ -0,0 +1,214 @@
// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 crate::iq::{IqSetPayload, IqGetPayload, IqResultPayload};
use crate::util::helpers::Base64;
generate_elem_id!(
/// The name of a certificate.
Name, "name", SASL_CERT
);
generate_element!(
/// An X.509 certificate.
Cert, "x509cert", SASL_CERT,
text: (
/// The BER X.509 data.
data: Base64<Vec<u8>>
)
);
generate_element!(
/// For the client to upload an X.509 certificate.
Append, "append", SASL_CERT,
children: [
/// The name of this certificate.
name: Required<Name> = ("name", SASL_CERT) => Name,
/// The X.509 certificate to set.
cert: Required<Cert> = ("x509cert", SASL_CERT) => Cert,
/// This client is forbidden from managing certificates.
no_cert_management: Present<_> = ("no-cert-management", SASL_CERT) => bool
]
);
impl IqSetPayload for Append {}
generate_empty_element!(
/// Client requests the current list of X.509 certificates.
ListCertsQuery, "items", SASL_CERT
);
impl IqGetPayload for ListCertsQuery {}
generate_elem_id!(
/// One resource currently using a certificate.
Resource, "resource", SASL_CERT
);
generate_element!(
/// A list of resources currently using this certificate.
Users, "users", SASL_CERT,
children: [
/// Resources currently using this certificate.
resources: Vec<Resource> = ("resource", SASL_CERT) => Resource
]
);
generate_element!(
/// An X.509 certificate being set for this user.
Item, "item", SASL_CERT,
children: [
/// The name of this certificate.
name: Required<Name> = ("name", SASL_CERT) => Name,
/// The X.509 certificate to set.
cert: Required<Cert> = ("x509cert", SASL_CERT) => Cert,
/// This client is forbidden from managing certificates.
no_cert_management: Present<_> = ("no-cert-management", SASL_CERT) => bool,
/// List of resources currently using this certificate.
users: Option<Users> = ("users", SASL_CERT) => Users
]
);
generate_element!(
/// Server answers with the current list of X.509 certificates.
ListCertsResponse, "items", SASL_CERT,
children: [
/// List of certificates.
items: Vec<Item> = ("item", SASL_CERT) => Item
]
);
impl IqResultPayload for ListCertsResponse {}
generate_element!(
/// Client disables an X.509 certificate.
Disable, "disable", SASL_CERT,
children: [
/// Name of the certificate to disable.
name: Required<Name> = ("name", SASL_CERT) => Name
]
);
impl IqSetPayload for Disable {}
generate_element!(
/// Client revokes an X.509 certificate.
Revoke, "revoke", SASL_CERT,
children: [
/// Name of the certificate to revoke.
name: Required<Name> = ("name", SASL_CERT) => Name
]
);
impl IqSetPayload for Revoke {}
#[cfg(test)]
mod tests {
use super::*;
use crate::Element;
use std::convert::TryFrom;
use std::str::FromStr;
use crate::ns;
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(Append, 28);
assert_size!(Disable, 12);
assert_size!(Revoke, 12);
assert_size!(ListCertsQuery, 0);
assert_size!(ListCertsResponse, 12);
assert_size!(Item, 40);
assert_size!(Resource, 12);
assert_size!(Users, 12);
assert_size!(Cert, 12);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(Append, 56);
assert_size!(Disable, 24);
assert_size!(Revoke, 24);
assert_size!(ListCertsQuery, 0);
assert_size!(ListCertsResponse, 24);
assert_size!(Item, 80);
assert_size!(Resource, 24);
assert_size!(Users, 24);
assert_size!(Cert, 24);
}
#[test]
fn simple() {
let elem: Element = "<append xmlns='urn:xmpp:saslcert:1'><name>Mobile Client</name><x509cert>AAAA</x509cert></append>".parse().unwrap();
let append = Append::try_from(elem).unwrap();
assert_eq!(append.name.0, "Mobile Client");
assert_eq!(append.cert.data, b"\0\0\0");
let elem: Element = "<disable xmlns='urn:xmpp:saslcert:1'><name>Mobile Client</name></disable>".parse().unwrap();
let disable = Disable::try_from(elem).unwrap();
assert_eq!(disable.name.0, "Mobile Client");
let elem: Element = "<revoke xmlns='urn:xmpp:saslcert:1'><name>Mobile Client</name></revoke>".parse().unwrap();
let revoke = Revoke::try_from(elem).unwrap();
assert_eq!(revoke.name.0, "Mobile Client");
}
#[test]
fn list() {
let elem: Element = r#"
<items xmlns='urn:xmpp:saslcert:1'>
<item>
<name>Mobile Client</name>
<x509cert>AAAA</x509cert>
<users>
<resource>Phone</resource>
</users>
</item>
<item>
<name>Laptop</name>
<x509cert>BBBB</x509cert>
</item>
</items>"#.parse().unwrap();
let mut list = ListCertsResponse::try_from(elem).unwrap();
assert_eq!(list.items.len(), 2);
let item = list.items.pop().unwrap();
assert_eq!(item.name.0, "Laptop");
assert_eq!(item.cert.data, [4, 16, 65]);
assert!(item.users.is_none());
let item = list.items.pop().unwrap();
assert_eq!(item.name.0, "Mobile Client");
assert_eq!(item.cert.data, b"\0\0\0");
assert_eq!(item.users.unwrap().resources.len(), 1);
}
#[test]
fn test_serialise() {
let append = Append {
name: Name::from_str("Mobile Client").unwrap(),
cert: Cert { data: b"\0\0\0".to_vec() },
no_cert_management: false,
};
let elem: Element = append.into();
assert!(elem.is("append", ns::SASL_CERT));
let disable = Disable {
name: Name::from_str("Mobile Client").unwrap(),
};
let elem: Element = disable.into();
assert!(elem.is("disable", ns::SASL_CERT));
let elem = elem.children().cloned().collect::<Vec<_>>().pop().unwrap();
assert!(elem.is("name", ns::SASL_CERT));
assert_eq!(elem.text(), "Mobile Client");
}
}

View file

@ -0,0 +1,100 @@
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 crate::message::MessagePayload;
generate_element_enum!(
/// Enum representing chatstate elements part of the
/// `http://jabber.org/protocol/chatstates` namespace.
ChatState, "chatstate", CHATSTATES, {
/// `<active xmlns='http://jabber.org/protocol/chatstates'/>`
Active => "active",
/// `<composing xmlns='http://jabber.org/protocol/chatstates'/>`
Composing => "composing",
/// `<gone xmlns='http://jabber.org/protocol/chatstates'/>`
Gone => "gone",
/// `<inactive xmlns='http://jabber.org/protocol/chatstates'/>`
Inactive => "inactive",
/// `<paused xmlns='http://jabber.org/protocol/chatstates'/>`
Paused => "paused",
}
);
impl MessagePayload for ChatState {}
#[cfg(test)]
mod tests {
use super::*;
use crate::util::error::Error;
use crate::ns;
use crate::Element;
use std::convert::TryFrom;
#[test]
fn test_size() {
assert_size!(ChatState, 1);
}
#[test]
fn test_simple() {
let elem: Element = "<active xmlns='http://jabber.org/protocol/chatstates'/>"
.parse()
.unwrap();
ChatState::try_from(elem).unwrap();
}
#[test]
fn test_invalid() {
let elem: Element = "<coucou xmlns='http://jabber.org/protocol/chatstates'/>"
.parse()
.unwrap();
let error = ChatState::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "This is not a chatstate element.");
}
#[cfg(not(feature = "disable-validation"))]
#[test]
fn test_invalid_child() {
let elem: Element = "<gone xmlns='http://jabber.org/protocol/chatstates'><coucou/></gone>"
.parse()
.unwrap();
let error = ChatState::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown child in chatstate element.");
}
#[cfg(not(feature = "disable-validation"))]
#[test]
fn test_invalid_attribute() {
let elem: Element = "<inactive xmlns='http://jabber.org/protocol/chatstates' coucou=''/>"
.parse()
.unwrap();
let error = ChatState::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown attribute in chatstate element.");
}
#[test]
fn test_serialise() {
let chatstate = ChatState::Active;
let elem: Element = chatstate.into();
assert!(elem.is("active", ns::CHATSTATES));
}
}

View file

@ -0,0 +1,87 @@
// Copyright (c) 2018 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 crate::util::helpers::PlainText;
use digest::Digest;
use sha1::Sha1;
generate_element!(
/// The main authentication mechanism for components.
#[derive(Default)]
Handshake, "handshake", COMPONENT,
text: (
/// If Some, contains the hex-encoded SHA-1 of the concatenation of the
/// stream id and the password, and is used to authenticate against the
/// server.
///
/// If None, it is the successful reply from the server, the stream is now
/// fully established and both sides can now exchange stanzas.
data: PlainText<Option<String>>
)
);
impl Handshake {
/// Creates a successful reply from a server.
pub fn new() -> Handshake {
Handshake::default()
}
/// Creates an authentication request from the component.
pub fn from_password_and_stream_id(password: &str, stream_id: &str) -> Handshake {
let input = String::from(stream_id) + password;
let hash = Sha1::digest(input.as_bytes());
let content = format!("{:x}", hash);
Handshake {
data: Some(content),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Element;
use std::convert::TryFrom;
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(Handshake, 12);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(Handshake, 24);
}
#[test]
fn test_simple() {
let elem: Element = "<handshake xmlns='jabber:component:accept'/>"
.parse()
.unwrap();
let handshake = Handshake::try_from(elem).unwrap();
assert_eq!(handshake.data, None);
let elem: Element = "<handshake xmlns='jabber:component:accept'>Coucou</handshake>"
.parse()
.unwrap();
let handshake = Handshake::try_from(elem).unwrap();
assert_eq!(handshake.data, Some(String::from("Coucou")));
}
#[test]
fn test_constructors() {
let handshake = Handshake::new();
assert_eq!(handshake.data, None);
let handshake = Handshake::from_password_and_stream_id("123456", "sid");
assert_eq!(
handshake.data,
Some(String::from("9accec263ab84a43c6037ccf7cd48cb1d3f6df8e"))
);
}
}

59
xmpp-parsers/src/csi.rs Normal file
View file

@ -0,0 +1,59 @@
// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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/.
generate_empty_element!(
/// Stream:feature sent by the server to advertise it supports CSI.
Feature, "csi", CSI
);
generate_empty_element!(
/// Client indicates it is inactive.
Inactive, "inactive", CSI
);
generate_empty_element!(
/// Client indicates it is active again.
Active, "active", CSI
);
#[cfg(test)]
mod tests {
use super::*;
use crate::Element;
use std::convert::TryFrom;
use crate::ns;
#[test]
fn test_size() {
assert_size!(Feature, 0);
assert_size!(Inactive, 0);
assert_size!(Active, 0);
}
#[test]
fn parsing() {
let elem: Element = "<csi xmlns='urn:xmpp:csi:0'/>".parse().unwrap();
Feature::try_from(elem).unwrap();
let elem: Element = "<inactive xmlns='urn:xmpp:csi:0'/>".parse().unwrap();
Inactive::try_from(elem).unwrap();
let elem: Element = "<active xmlns='urn:xmpp:csi:0'/>".parse().unwrap();
Active::try_from(elem).unwrap();
}
#[test]
fn serialising() {
let elem: Element = Feature.into();
assert!(elem.is("csi", ns::CSI));
let elem: Element = Inactive.into();
assert!(elem.is("inactive", ns::CSI));
let elem: Element = Active.into();
assert!(elem.is("active", ns::CSI));
}
}

View file

@ -0,0 +1,395 @@
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 crate::util::error::Error;
use crate::media_element::MediaElement;
use crate::ns;
use crate::Element;
use std::convert::TryFrom;
generate_element!(
/// Represents one of the possible values for a list- field.
Option_, "option", DATA_FORMS,
attributes: [
/// The optional label to be displayed to the user for this option.
label: Option<String> = "label"
],
children: [
/// The value returned to the server when selecting this option.
value: Required<String> = ("value", DATA_FORMS) => String
]
);
generate_attribute!(
/// The type of a [field](struct.Field.html) element.
FieldType, "type", {
/// This field can only take the values "0" or "false" for a false
/// value, and "1" or "true" for a true value.
Boolean => "boolean",
/// This field describes data, it must not be sent back to the
/// requester.
Fixed => "fixed",
/// This field is hidden, it should not be displayed to the user but
/// should be sent back to the requester.
Hidden => "hidden",
/// This field accepts one or more [JIDs](../../jid/struct.Jid.html).
/// A client may want to let the user autocomplete them based on their
/// contacts list for instance.
JidMulti => "jid-multi",
/// This field accepts one [JID](../../jid/struct.Jid.html). A client
/// may want to let the user autocomplete it based on their contacts
/// list for instance.
JidSingle => "jid-single",
/// This field accepts one or more values from the list provided as
/// [options](struct.Option_.html).
ListMulti => "list-multi",
/// This field accepts one value from the list provided as
/// [options](struct.Option_.html).
ListSingle => "list-single",
/// This field accepts one or more free form text lines.
TextMulti => "text-multi",
/// This field accepts one free form password, a client should hide it
/// in its user interface.
TextPrivate => "text-private",
/// This field accepts one free form text line.
TextSingle => "text-single",
}, Default = TextSingle
);
/// Represents a field in a [data form](struct.DataForm.html).
#[derive(Debug, Clone)]
pub struct Field {
/// The unique identifier for this field, in the form.
pub var: String,
/// The type of this field.
pub type_: FieldType,
/// The label to be possibly displayed to the user for this field.
pub label: Option<String>,
/// The form will be rejected if this field isnt present.
pub required: bool,
/// A list of allowed values.
pub options: Vec<Option_>,
/// The values provided for this field.
pub values: Vec<String>,
/// A list of media related to this field.
pub media: Vec<MediaElement>,
}
impl Field {
fn is_list(&self) -> bool {
self.type_ == FieldType::ListSingle || self.type_ == FieldType::ListMulti
}
}
impl TryFrom<Element> for Field {
type Error = Error;
fn try_from(elem: Element) -> Result<Field, Error> {
check_self!(elem, "field", DATA_FORMS);
check_no_unknown_attributes!(elem, "field", ["label", "type", "var"]);
let mut field = Field {
var: get_attr!(elem, "var", Required),
type_: get_attr!(elem, "type", Default),
label: get_attr!(elem, "label", Option),
required: false,
options: vec![],
values: vec![],
media: vec![],
};
for element in elem.children() {
if element.is("value", ns::DATA_FORMS) {
check_no_children!(element, "value");
check_no_attributes!(element, "value");
field.values.push(element.text());
} else if element.is("required", ns::DATA_FORMS) {
if field.required {
return Err(Error::ParseError("More than one required element."));
}
check_no_children!(element, "required");
check_no_attributes!(element, "required");
field.required = true;
} else if element.is("option", ns::DATA_FORMS) {
if !field.is_list() {
return Err(Error::ParseError("Option element found in non-list field."));
}
let option = Option_::try_from(element.clone())?;
field.options.push(option);
} else if element.is("media", ns::MEDIA_ELEMENT) {
let media_element = MediaElement::try_from(element.clone())?;
field.media.push(media_element);
} else {
return Err(Error::ParseError(
"Field child isnt a value, option or media element.",
));
}
}
Ok(field)
}
}
impl From<Field> for Element {
fn from(field: Field) -> Element {
Element::builder("field")
.ns(ns::DATA_FORMS)
.attr("var", field.var)
.attr("type", field.type_)
.attr("label", field.label)
.append_all(if field.required {
Some(Element::builder("required").ns(ns::DATA_FORMS))
} else {
None
})
.append_all(field.options.iter().cloned().map(Element::from))
.append_all(
field
.values
.into_iter()
.map(|value| {
Element::builder("value")
.ns(ns::DATA_FORMS)
.append(value)
})
)
.append_all(field.media.iter().cloned().map(Element::from))
.build()
}
}
generate_attribute!(
/// Represents the type of a [data form](struct.DataForm.html).
DataFormType, "type", {
/// This is a cancel request for a prior type="form" data form.
Cancel => "cancel",
/// This is a request for the recipient to fill this form and send it
/// back as type="submit".
Form => "form",
/// This is a result form, which contains what the requester asked for.
Result_ => "result",
/// This is a complete response to a form received before.
Submit => "submit",
}
);
/// This is a form to be sent to another entity for filling.
#[derive(Debug, Clone)]
pub struct DataForm {
/// The type of this form, telling the other party which action to execute.
pub type_: DataFormType,
/// An easy accessor for the FORM_TYPE of this form, see
/// [XEP-0068](https://xmpp.org/extensions/xep-0068.html) for more
/// information.
pub form_type: Option<String>,
/// The title of this form.
pub title: Option<String>,
/// The instructions given with this form.
pub instructions: Option<String>,
/// A list of fields comprising this form.
pub fields: Vec<Field>,
}
impl TryFrom<Element> for DataForm {
type Error = Error;
fn try_from(elem: Element) -> Result<DataForm, Error> {
check_self!(elem, "x", DATA_FORMS);
check_no_unknown_attributes!(elem, "x", ["type"]);
let type_ = get_attr!(elem, "type", Required);
let mut form = DataForm {
type_,
form_type: None,
title: None,
instructions: None,
fields: vec![],
};
for child in elem.children() {
if child.is("title", ns::DATA_FORMS) {
if form.title.is_some() {
return Err(Error::ParseError("More than one title in form element."));
}
check_no_children!(child, "title");
check_no_attributes!(child, "title");
form.title = Some(child.text());
} else if child.is("instructions", ns::DATA_FORMS) {
if form.instructions.is_some() {
return Err(Error::ParseError(
"More than one instructions in form element.",
));
}
check_no_children!(child, "instructions");
check_no_attributes!(child, "instructions");
form.instructions = Some(child.text());
} else if child.is("field", ns::DATA_FORMS) {
let field = Field::try_from(child.clone())?;
if field.var == "FORM_TYPE" {
let mut field = field;
if form.form_type.is_some() {
return Err(Error::ParseError("More than one FORM_TYPE in a data form."));
}
if field.type_ != FieldType::Hidden {
return Err(Error::ParseError("Invalid field type for FORM_TYPE."));
}
if field.values.len() != 1 {
return Err(Error::ParseError("Wrong number of values in FORM_TYPE."));
}
form.form_type = field.values.pop();
} else {
form.fields.push(field);
}
} else {
return Err(Error::ParseError("Unknown child in data form element."));
}
}
Ok(form)
}
}
impl From<DataForm> for Element {
fn from(form: DataForm) -> Element {
Element::builder("x")
.ns(ns::DATA_FORMS)
.attr("type", form.type_)
.append_all(
form.title
.map(|title| Element::builder("title").ns(ns::DATA_FORMS).append(title)),
)
.append_all(form.instructions.map(|text| {
Element::builder("instructions")
.ns(ns::DATA_FORMS)
.append(text)
}))
.append_all(form.form_type.map(|form_type| {
Element::builder("field")
.ns(ns::DATA_FORMS)
.attr("var", "FORM_TYPE")
.attr("type", "hidden")
.append(Element::builder("value")
.ns(ns::DATA_FORMS)
.append(form_type))
}))
.append_all(form.fields.iter().cloned().map(Element::from))
.build()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(Option_, 24);
assert_size!(FieldType, 1);
assert_size!(Field, 64);
assert_size!(DataFormType, 1);
assert_size!(DataForm, 52);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(Option_, 48);
assert_size!(FieldType, 1);
assert_size!(Field, 128);
assert_size!(DataFormType, 1);
assert_size!(DataForm, 104);
}
#[test]
fn test_simple() {
let elem: Element = "<x xmlns='jabber:x:data' type='result'/>".parse().unwrap();
let form = DataForm::try_from(elem).unwrap();
assert_eq!(form.type_, DataFormType::Result_);
assert!(form.form_type.is_none());
assert!(form.fields.is_empty());
}
#[test]
fn test_invalid() {
let elem: Element = "<x xmlns='jabber:x:data'/>".parse().unwrap();
let error = DataForm::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Required attribute 'type' missing.");
let elem: Element = "<x xmlns='jabber:x:data' type='coucou'/>".parse().unwrap();
let error = DataForm::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown value for 'type' attribute.");
}
#[test]
fn test_wrong_child() {
let elem: Element = "<x xmlns='jabber:x:data' type='cancel'><coucou/></x>"
.parse()
.unwrap();
let error = DataForm::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown child in data form element.");
}
#[test]
fn option() {
let elem: Element =
"<option xmlns='jabber:x:data' label='Coucou!'><value>coucou</value></option>"
.parse()
.unwrap();
let option = Option_::try_from(elem).unwrap();
assert_eq!(&option.label.unwrap(), "Coucou!");
assert_eq!(&option.value, "coucou");
let elem: Element = "<option xmlns='jabber:x:data' label='Coucou!'/>"
.parse()
.unwrap();
let error = Option_::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Missing child value in option element.");
let elem: Element = "<option xmlns='jabber:x:data' label='Coucou!'><value>coucou</value><value>error</value></option>".parse().unwrap();
let error = Option_::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(
message,
"Element option must not have more than one value child."
);
}
}

138
xmpp-parsers/src/date.rs Normal file
View file

@ -0,0 +1,138 @@
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 crate::util::error::Error;
use chrono::{DateTime as ChronoDateTime, FixedOffset};
use minidom::{IntoAttributeValue, Node};
use std::str::FromStr;
/// Implements the DateTime profile of XEP-0082, which represents a
/// non-recurring moment in time, with an accuracy of seconds or fraction of
/// seconds, and includes a timezone.
#[derive(Debug, Clone, PartialEq)]
pub struct DateTime(ChronoDateTime<FixedOffset>);
impl DateTime {
/// Retrieves the associated timezone.
pub fn timezone(&self) -> FixedOffset {
self.0.timezone()
}
/// Returns a new `DateTime` with a different timezone.
pub fn with_timezone(&self, tz: FixedOffset) -> DateTime {
DateTime(self.0.with_timezone(&tz))
}
/// Formats this `DateTime` with the specified format string.
pub fn format(&self, fmt: &str) -> String {
format!("{}", self.0.format(fmt))
}
}
impl FromStr for DateTime {
type Err = Error;
fn from_str(s: &str) -> Result<DateTime, Error> {
Ok(DateTime(ChronoDateTime::parse_from_rfc3339(s)?))
}
}
impl IntoAttributeValue for DateTime {
fn into_attribute_value(self) -> Option<String> {
Some(self.0.to_rfc3339())
}
}
impl Into<Node> for DateTime {
fn into(self) -> Node {
Node::Text(self.0.to_rfc3339())
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::{Datelike, Timelike};
use std::error::Error as StdError;
// DateTimes size doesnt depend on the architecture.
#[test]
fn test_size() {
assert_size!(DateTime, 16);
}
#[test]
fn test_simple() {
let date: DateTime = "2002-09-10T23:08:25Z".parse().unwrap();
assert_eq!(date.0.year(), 2002);
assert_eq!(date.0.month(), 9);
assert_eq!(date.0.day(), 10);
assert_eq!(date.0.hour(), 23);
assert_eq!(date.0.minute(), 08);
assert_eq!(date.0.second(), 25);
assert_eq!(date.0.nanosecond(), 0);
assert_eq!(date.0.timezone(), FixedOffset::east(0));
}
#[test]
fn test_invalid_date() {
// There is no thirteenth month.
let error = DateTime::from_str("2017-13-01T12:23:34Z").unwrap_err();
let message = match error {
Error::ChronoParseError(string) => string,
_ => panic!(),
};
assert_eq!(message.description(), "input is out of range");
// Timezone ≥24:00 arent allowed.
let error = DateTime::from_str("2017-05-27T12:11:02+25:00").unwrap_err();
let message = match error {
Error::ChronoParseError(string) => string,
_ => panic!(),
};
assert_eq!(message.description(), "input is out of range");
// Timezone without the : separator arent allowed.
let error = DateTime::from_str("2017-05-27T12:11:02+0100").unwrap_err();
let message = match error {
Error::ChronoParseError(string) => string,
_ => panic!(),
};
assert_eq!(message.description(), "input contains invalid characters");
// No seconds, error message could be improved.
let error = DateTime::from_str("2017-05-27T12:11+01:00").unwrap_err();
let message = match error {
Error::ChronoParseError(string) => string,
_ => panic!(),
};
assert_eq!(message.description(), "input contains invalid characters");
// TODO: maybe well want to support this one, as per XEP-0082 §4.
let error = DateTime::from_str("20170527T12:11:02+01:00").unwrap_err();
let message = match error {
Error::ChronoParseError(string) => string,
_ => panic!(),
};
assert_eq!(message.description(), "input contains invalid characters");
// No timezone.
let error = DateTime::from_str("2017-05-27T12:11:02").unwrap_err();
let message = match error {
Error::ChronoParseError(string) => string,
_ => panic!(),
};
assert_eq!(message.description(), "premature end of input");
}
#[test]
fn test_serialise() {
let date =
DateTime(ChronoDateTime::parse_from_rfc3339("2017-05-21T20:19:55+01:00").unwrap());
let attr = date.into_attribute_value();
assert_eq!(attr, Some(String::from("2017-05-21T20:19:55+01:00")));
}
}

118
xmpp-parsers/src/delay.rs Normal file
View file

@ -0,0 +1,118 @@
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 crate::date::DateTime;
use crate::util::helpers::PlainText;
use crate::message::MessagePayload;
use crate::presence::PresencePayload;
use jid::Jid;
generate_element!(
/// Notes when and by whom a message got stored for later delivery.
Delay, "delay", DELAY,
attributes: [
/// The entity which delayed this message.
from: Option<Jid> = "from",
/// The time at which this message got stored.
stamp: Required<DateTime> = "stamp"
],
text: (
/// The optional reason this message got delayed.
data: PlainText<Option<String>>
)
);
impl MessagePayload for Delay {}
impl PresencePayload for Delay {}
#[cfg(test)]
mod tests {
use super::*;
use crate::util::error::Error;
use crate::Element;
use std::str::FromStr;
use std::convert::TryFrom;
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(Delay, 68);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(Delay, 120);
}
#[test]
fn test_simple() {
let elem: Element =
"<delay xmlns='urn:xmpp:delay' from='capulet.com' stamp='2002-09-10T23:08:25Z'/>"
.parse()
.unwrap();
let delay = Delay::try_from(elem).unwrap();
assert_eq!(delay.from, Some(Jid::from_str("capulet.com").unwrap()));
assert_eq!(
delay.stamp,
DateTime::from_str("2002-09-10T23:08:25Z").unwrap()
);
assert_eq!(delay.data, None);
}
#[test]
fn test_unknown() {
let elem: Element = "<replace xmlns='urn:xmpp:message-correct:0'/>"
.parse()
.unwrap();
let error = Delay::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "This is not a delay element.");
}
#[test]
fn test_invalid_child() {
let elem: Element = "<delay xmlns='urn:xmpp:delay'><coucou/></delay>"
.parse()
.unwrap();
let error = Delay::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown child in delay element.");
}
#[test]
fn test_serialise() {
let elem: Element = "<delay xmlns='urn:xmpp:delay' stamp='2002-09-10T23:08:25+00:00'/>"
.parse()
.unwrap();
let delay = Delay {
from: None,
stamp: DateTime::from_str("2002-09-10T23:08:25Z").unwrap(),
data: None,
};
let elem2 = delay.into();
assert_eq!(elem, elem2);
}
#[test]
fn test_serialise_data() {
let elem: Element = "<delay xmlns='urn:xmpp:delay' from='juliet@example.org' stamp='2002-09-10T23:08:25+00:00'>Reason</delay>".parse().unwrap();
let delay = Delay {
from: Some(Jid::from_str("juliet@example.org").unwrap()),
stamp: DateTime::from_str("2002-09-10T23:08:25Z").unwrap(),
data: Some(String::from("Reason")),
};
let elem2 = delay.into();
assert_eq!(elem, elem2);
}
}

457
xmpp-parsers/src/disco.rs Normal file
View file

@ -0,0 +1,457 @@
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 crate::data_forms::{DataForm, DataFormType};
use crate::util::error::Error;
use crate::iq::{IqGetPayload, IqResultPayload};
use crate::ns;
use jid::Jid;
use crate::Element;
use std::convert::TryFrom;
generate_element!(
/// Structure representing a `<query xmlns='http://jabber.org/protocol/disco#info'/>` element.
///
/// It should only be used in an `<iq type='get'/>`, as it can only represent
/// the request, and not a result.
DiscoInfoQuery, "query", DISCO_INFO,
attributes: [
/// Node on which we are doing the discovery.
node: Option<String> = "node",
]);
impl IqGetPayload for DiscoInfoQuery {}
generate_element!(
/// Structure representing a `<feature xmlns='http://jabber.org/protocol/disco#info'/>` element.
#[derive(PartialEq)]
Feature, "feature", DISCO_INFO,
attributes: [
/// Namespace of the feature we want to represent.
var: Required<String> = "var",
]);
impl Feature {
/// Create a new `<feature/>` with the according `@var`.
pub fn new<S: Into<String>>(var: S) -> Feature {
Feature {
var: var.into(),
}
}
}
generate_element!(
/// Structure representing an `<identity xmlns='http://jabber.org/protocol/disco#info'/>` element.
Identity, "identity", DISCO_INFO,
attributes: [
/// Category of this identity.
// TODO: use an enum here.
category: RequiredNonEmpty<String> = "category",
/// Type of this identity.
// TODO: use an enum here.
type_: RequiredNonEmpty<String> = "type",
/// Lang of the name of this identity.
lang: Option<String> = "xml:lang",
/// Name of this identity.
name: Option<String> = "name",
]
);
impl Identity {
/// Create a new `<identity/>`.
pub fn new<C, T, L, N>(category: C, type_: T, lang: L, name: N) -> Identity
where C: Into<String>,
T: Into<String>,
L: Into<String>,
N: Into<String>,
{
Identity {
category: category.into(),
type_: type_.into(),
lang: Some(lang.into()),
name: Some(name.into()),
}
}
/// Create a new `<identity/>` without a name.
pub fn new_anonymous<C, T, L, N>(category: C, type_: T) -> Identity
where C: Into<String>,
T: Into<String>,
{
Identity {
category: category.into(),
type_: type_.into(),
lang: None,
name: None,
}
}
}
/// Structure representing a `<query xmlns='http://jabber.org/protocol/disco#info'/>` element.
///
/// It should only be used in an `<iq type='result'/>`, as it can only
/// represent the result, and not a request.
#[derive(Debug, Clone)]
pub struct DiscoInfoResult {
/// Node on which we have done this discovery.
pub node: Option<String>,
/// List of identities exposed by this entity.
pub identities: Vec<Identity>,
/// List of features supported by this entity.
pub features: Vec<Feature>,
/// List of extensions reported by this entity.
pub extensions: Vec<DataForm>,
}
impl IqResultPayload for DiscoInfoResult {}
impl TryFrom<Element> for DiscoInfoResult {
type Error = Error;
fn try_from(elem: Element) -> Result<DiscoInfoResult, Error> {
check_self!(elem, "query", DISCO_INFO, "disco#info result");
check_no_unknown_attributes!(elem, "disco#info result", ["node"]);
let mut result = DiscoInfoResult {
node: get_attr!(elem, "node", Option),
identities: vec![],
features: vec![],
extensions: vec![],
};
for child in elem.children() {
if child.is("identity", ns::DISCO_INFO) {
let identity = Identity::try_from(child.clone())?;
result.identities.push(identity);
} else if child.is("feature", ns::DISCO_INFO) {
let feature = Feature::try_from(child.clone())?;
result.features.push(feature);
} else if child.is("x", ns::DATA_FORMS) {
let data_form = DataForm::try_from(child.clone())?;
if data_form.type_ != DataFormType::Result_ {
return Err(Error::ParseError(
"Data form must have a 'result' type in disco#info.",
));
}
if data_form.form_type.is_none() {
return Err(Error::ParseError("Data form found without a FORM_TYPE."));
}
result.extensions.push(data_form);
} else {
return Err(Error::ParseError("Unknown element in disco#info."));
}
}
if result.identities.is_empty() {
return Err(Error::ParseError(
"There must be at least one identity in disco#info.",
));
}
if result.features.is_empty() {
return Err(Error::ParseError(
"There must be at least one feature in disco#info.",
));
}
if !result.features.contains(&Feature {
var: ns::DISCO_INFO.to_owned(),
}) {
return Err(Error::ParseError(
"disco#info feature not present in disco#info.",
));
}
Ok(result)
}
}
impl From<DiscoInfoResult> for Element {
fn from(disco: DiscoInfoResult) -> Element {
Element::builder("query")
.ns(ns::DISCO_INFO)
.attr("node", disco.node)
.append_all(disco.identities.into_iter())
.append_all(disco.features.into_iter())
.append_all(disco.extensions.iter().cloned().map(Element::from))
.build()
}
}
generate_element!(
/// Structure representing a `<query xmlns='http://jabber.org/protocol/disco#items'/>` element.
///
/// It should only be used in an `<iq type='get'/>`, as it can only represent
/// the request, and not a result.
DiscoItemsQuery, "query", DISCO_ITEMS,
attributes: [
/// Node on which we are doing the discovery.
node: Option<String> = "node",
]);
impl IqGetPayload for DiscoItemsQuery {}
generate_element!(
/// Structure representing an `<item xmlns='http://jabber.org/protocol/disco#items'/>` element.
Item, "item", DISCO_ITEMS,
attributes: [
/// JID of the entity pointed by this item.
jid: Required<Jid> = "jid",
/// Node of the entity pointed by this item.
node: Option<String> = "node",
/// Name of the entity pointed by this item.
name: Option<String> = "name",
]);
generate_element!(
/// Structure representing a `<query
/// xmlns='http://jabber.org/protocol/disco#items'/>` element.
///
/// It should only be used in an `<iq type='result'/>`, as it can only
/// represent the result, and not a request.
DiscoItemsResult, "query", DISCO_ITEMS,
attributes: [
/// Node on which we have done this discovery.
node: Option<String> = "node"
],
children: [
/// List of items pointed by this entity.
items: Vec<Item> = ("item", DISCO_ITEMS) => Item
]
);
impl IqResultPayload for DiscoItemsResult {}
#[cfg(test)]
mod tests {
use super::*;
use crate::util::compare_elements::NamespaceAwareCompare;
use std::str::FromStr;
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(Identity, 48);
assert_size!(Feature, 12);
assert_size!(DiscoInfoQuery, 12);
assert_size!(DiscoInfoResult, 48);
assert_size!(Item, 64);
assert_size!(DiscoItemsQuery, 12);
assert_size!(DiscoItemsResult, 24);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(Identity, 96);
assert_size!(Feature, 24);
assert_size!(DiscoInfoQuery, 24);
assert_size!(DiscoInfoResult, 96);
assert_size!(Item, 128);
assert_size!(DiscoItemsQuery, 24);
assert_size!(DiscoItemsResult, 48);
}
#[test]
fn test_simple() {
let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='client' type='pc'/><feature var='http://jabber.org/protocol/disco#info'/></query>".parse().unwrap();
let query = DiscoInfoResult::try_from(elem).unwrap();
assert!(query.node.is_none());
assert_eq!(query.identities.len(), 1);
assert_eq!(query.features.len(), 1);
assert!(query.extensions.is_empty());
}
#[test]
fn test_identity_after_feature() {
let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><feature var='http://jabber.org/protocol/disco#info'/><identity category='client' type='pc'/></query>".parse().unwrap();
let query = DiscoInfoResult::try_from(elem).unwrap();
assert_eq!(query.identities.len(), 1);
assert_eq!(query.features.len(), 1);
assert!(query.extensions.is_empty());
}
#[test]
fn test_feature_after_dataform() {
let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='client' type='pc'/><x xmlns='jabber:x:data' type='result'><field var='FORM_TYPE' type='hidden'><value>coucou</value></field></x><feature var='http://jabber.org/protocol/disco#info'/></query>".parse().unwrap();
let query = DiscoInfoResult::try_from(elem).unwrap();
assert_eq!(query.identities.len(), 1);
assert_eq!(query.features.len(), 1);
assert_eq!(query.extensions.len(), 1);
}
#[test]
fn test_extension() {
let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='client' type='pc'/><feature var='http://jabber.org/protocol/disco#info'/><x xmlns='jabber:x:data' type='result'><field var='FORM_TYPE' type='hidden'><value>example</value></field></x></query>".parse().unwrap();
let elem1 = elem.clone();
let query = DiscoInfoResult::try_from(elem).unwrap();
assert!(query.node.is_none());
assert_eq!(query.identities.len(), 1);
assert_eq!(query.features.len(), 1);
assert_eq!(query.extensions.len(), 1);
assert_eq!(query.extensions[0].form_type, Some(String::from("example")));
let elem2 = query.into();
assert!(elem1.compare_to(&elem2));
}
#[test]
fn test_invalid() {
let elem: Element =
"<query xmlns='http://jabber.org/protocol/disco#info'><coucou/></query>"
.parse()
.unwrap();
let error = DiscoInfoResult::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown element in disco#info.");
}
#[test]
fn test_invalid_identity() {
let elem: Element =
"<query xmlns='http://jabber.org/protocol/disco#info'><identity/></query>"
.parse()
.unwrap();
let error = DiscoInfoResult::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Required attribute 'category' missing.");
let elem: Element =
"<query xmlns='http://jabber.org/protocol/disco#info'><identity category=''/></query>"
.parse()
.unwrap();
let error = DiscoInfoResult::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(
message,
"Required attribute 'category' must not be empty."
);
let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='coucou'/></query>".parse().unwrap();
let error = DiscoInfoResult::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Required attribute 'type' missing.");
let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='coucou' type=''/></query>".parse().unwrap();
let error = DiscoInfoResult::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Required attribute 'type' must not be empty.");
}
#[test]
fn test_invalid_feature() {
let elem: Element =
"<query xmlns='http://jabber.org/protocol/disco#info'><feature/></query>"
.parse()
.unwrap();
let error = DiscoInfoResult::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Required attribute 'var' missing.");
}
#[test]
fn test_invalid_result() {
let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'/>"
.parse()
.unwrap();
let error = DiscoInfoResult::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(
message,
"There must be at least one identity in disco#info."
);
let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='client' type='pc'/></query>".parse().unwrap();
let error = DiscoInfoResult::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "There must be at least one feature in disco#info.");
let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='client' type='pc'/><feature var='http://jabber.org/protocol/disco#items'/></query>".parse().unwrap();
let error = DiscoInfoResult::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "disco#info feature not present in disco#info.");
}
#[test]
fn test_simple_items() {
let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#items'/>"
.parse()
.unwrap();
let query = DiscoItemsQuery::try_from(elem).unwrap();
assert!(query.node.is_none());
let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#items' node='coucou'/>"
.parse()
.unwrap();
let query = DiscoItemsQuery::try_from(elem).unwrap();
assert_eq!(query.node, Some(String::from("coucou")));
}
#[test]
fn test_simple_items_result() {
let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#items'/>"
.parse()
.unwrap();
let query = DiscoItemsResult::try_from(elem).unwrap();
assert!(query.node.is_none());
assert!(query.items.is_empty());
let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#items' node='coucou'/>"
.parse()
.unwrap();
let query = DiscoItemsResult::try_from(elem).unwrap();
assert_eq!(query.node, Some(String::from("coucou")));
assert!(query.items.is_empty());
}
#[test]
fn test_answers_items_result() {
let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#items'><item jid='component'/><item jid='component2' node='test' name='A component'/></query>".parse().unwrap();
let query = DiscoItemsResult::try_from(elem).unwrap();
let elem2 = Element::from(query);
let query = DiscoItemsResult::try_from(elem2).unwrap();
assert_eq!(query.items.len(), 2);
assert_eq!(query.items[0].jid, Jid::from_str("component").unwrap());
assert_eq!(query.items[0].node, None);
assert_eq!(query.items[0].name, None);
assert_eq!(query.items[1].jid, Jid::from_str("component2").unwrap());
assert_eq!(query.items[1].node, Some(String::from("test")));
assert_eq!(query.items[1].name, Some(String::from("A component")));
}
}

472
xmpp-parsers/src/ecaps2.rs Normal file
View file

@ -0,0 +1,472 @@
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 crate::data_forms::DataForm;
use crate::disco::{DiscoInfoQuery, DiscoInfoResult, Feature, Identity};
use crate::hashes::{Algo, Hash};
use crate::ns;
use crate::presence::PresencePayload;
use blake2::VarBlake2b;
use digest::{Digest, Input, VariableOutput};
use sha2::{Sha256, Sha512};
use sha3::{Sha3_256, Sha3_512};
generate_element!(
/// Represents a set of capability hashes, all of them must correspond to
/// the same input [disco#info](../disco/struct.DiscoInfoResult.html),
/// using different [algorithms](../hashes/enum.Algo.html).
ECaps2, "c", ECAPS2,
children: [
/// Hashes of the [disco#info](../disco/struct.DiscoInfoResult.html).
hashes: Vec<Hash> = ("hash", HASHES) => Hash
]
);
impl PresencePayload for ECaps2 {}
impl ECaps2 {
/// Create an ECaps2 element from a list of hashes.
pub fn new(hashes: Vec<Hash>) -> ECaps2 {
ECaps2 {
hashes,
}
}
}
fn compute_item(field: &str) -> Vec<u8> {
let mut bytes = field.as_bytes().to_vec();
bytes.push(0x1f);
bytes
}
fn compute_items<T, F: Fn(&T) -> Vec<u8>>(things: &[T], separator: u8, encode: F) -> Vec<u8> {
let mut string: Vec<u8> = vec![];
let mut accumulator: Vec<Vec<u8>> = vec![];
for thing in things {
let bytes = encode(thing);
accumulator.push(bytes);
}
// This works using the expected i;octet collation.
accumulator.sort();
for mut bytes in accumulator {
string.append(&mut bytes);
}
string.push(separator);
string
}
fn compute_features(features: &[Feature]) -> Vec<u8> {
compute_items(features, 0x1c, |feature| compute_item(&feature.var))
}
fn compute_identities(identities: &[Identity]) -> Vec<u8> {
compute_items(identities, 0x1c, |identity| {
let mut bytes = compute_item(&identity.category);
bytes.append(&mut compute_item(&identity.type_));
bytes.append(&mut compute_item(
&identity.lang.clone().unwrap_or_default(),
));
bytes.append(&mut compute_item(
&identity.name.clone().unwrap_or_default(),
));
bytes.push(0x1e);
bytes
})
}
fn compute_extensions(extensions: &[DataForm]) -> Result<Vec<u8>, ()> {
for extension in extensions {
if extension.form_type.is_none() {
return Err(());
}
}
Ok(compute_items(extensions, 0x1c, |extension| {
let mut bytes = compute_item("FORM_TYPE");
bytes.append(&mut compute_item(if let Some(ref form_type) = extension.form_type { form_type } else { unreachable!() }));
bytes.push(0x1e);
bytes.append(&mut compute_items(&extension.fields, 0x1d, |field| {
let mut bytes = compute_item(&field.var);
bytes.append(&mut compute_items(&field.values, 0x1e, |value| {
compute_item(value)
}));
bytes
}));
bytes
}))
}
/// Applies the [algorithm from
/// XEP-0390](https://xmpp.org/extensions/xep-0390.html#algorithm-input) on a
/// [disco#info query element](../disco/struct.DiscoInfoResult.html).
pub fn compute_disco(disco: &DiscoInfoResult) -> Result<Vec<u8>, ()> {
let features_string = compute_features(&disco.features);
let identities_string = compute_identities(&disco.identities);
let extensions_string = compute_extensions(&disco.extensions)?;
let mut final_string = vec![];
final_string.extend(features_string);
final_string.extend(identities_string);
final_string.extend(extensions_string);
Ok(final_string)
}
fn get_hash_vec(hash: &[u8]) -> Vec<u8> {
let mut vec = Vec::with_capacity(hash.len());
vec.extend_from_slice(hash);
vec
}
/// Hashes the result of [compute_disco()] with one of the supported [hash
/// algorithms](../hashes/enum.Algo.html).
pub fn hash_ecaps2(data: &[u8], algo: Algo) -> Result<Hash, String> {
Ok(Hash {
hash: match algo {
Algo::Sha_256 => {
let hash = Sha256::digest(data);
get_hash_vec(hash.as_slice())
}
Algo::Sha_512 => {
let hash = Sha512::digest(data);
get_hash_vec(hash.as_slice())
}
Algo::Sha3_256 => {
let hash = Sha3_256::digest(data);
get_hash_vec(hash.as_slice())
}
Algo::Sha3_512 => {
let hash = Sha3_512::digest(data);
get_hash_vec(hash.as_slice())
}
Algo::Blake2b_256 => {
let mut hasher = VarBlake2b::new(32).unwrap();
hasher.input(data);
hasher.vec_result()
}
Algo::Blake2b_512 => {
let mut hasher = VarBlake2b::new(64).unwrap();
hasher.input(data);
hasher.vec_result()
}
Algo::Sha_1 => return Err(String::from("Disabled algorithm sha-1: unsafe.")),
Algo::Unknown(algo) => return Err(format!("Unknown algorithm: {}.", algo)),
},
algo,
})
}
/// Helper function to create the query for the disco#info corresponding to an
/// ecaps2 hash.
pub fn query_ecaps2(hash: Hash) -> DiscoInfoQuery {
DiscoInfoQuery {
node: Some(format!(
"{}#{}.{}",
ns::ECAPS2,
String::from(hash.algo),
base64::encode(&hash.hash)
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::util::error::Error;
use crate::Element;
use std::convert::TryFrom;
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(ECaps2, 12);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(ECaps2, 24);
}
#[test]
fn test_parse() {
let elem: Element = "<c xmlns='urn:xmpp:caps'><hash xmlns='urn:xmpp:hashes:2' algo='sha-256'>K1Njy3HZBThlo4moOD5gBGhn0U0oK7/CbfLlIUDi6o4=</hash><hash xmlns='urn:xmpp:hashes:2' algo='sha3-256'>+sDTQqBmX6iG/X3zjt06fjZMBBqL/723knFIyRf0sg8=</hash></c>".parse().unwrap();
let ecaps2 = ECaps2::try_from(elem).unwrap();
assert_eq!(ecaps2.hashes.len(), 2);
assert_eq!(ecaps2.hashes[0].algo, Algo::Sha_256);
assert_eq!(
ecaps2.hashes[0].hash,
base64::decode("K1Njy3HZBThlo4moOD5gBGhn0U0oK7/CbfLlIUDi6o4=").unwrap()
);
assert_eq!(ecaps2.hashes[1].algo, Algo::Sha3_256);
assert_eq!(
ecaps2.hashes[1].hash,
base64::decode("+sDTQqBmX6iG/X3zjt06fjZMBBqL/723knFIyRf0sg8=").unwrap()
);
}
#[test]
fn test_invalid_child() {
let elem: Element = "<c xmlns='urn:xmpp:caps'><hash xmlns='urn:xmpp:hashes:2' algo='sha-256'>K1Njy3HZBThlo4moOD5gBGhn0U0oK7/CbfLlIUDi6o4=</hash><hash xmlns='urn:xmpp:hashes:1' algo='sha3-256'>+sDTQqBmX6iG/X3zjt06fjZMBBqL/723knFIyRf0sg8=</hash></c>".parse().unwrap();
let error = ECaps2::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown child in c element.");
}
#[test]
fn test_simple() {
let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='client' type='pc'/><feature var='http://jabber.org/protocol/disco#info'/></query>".parse().unwrap();
let disco = DiscoInfoResult::try_from(elem).unwrap();
let ecaps2 = compute_disco(&disco).unwrap();
assert_eq!(ecaps2.len(), 54);
}
#[test]
fn test_xep_ex1() {
let elem: Element = r#"
<query xmlns="http://jabber.org/protocol/disco#info">
<identity category="client" name="BombusMod" type="mobile"/>
<feature var="http://jabber.org/protocol/si"/>
<feature var="http://jabber.org/protocol/bytestreams"/>
<feature var="http://jabber.org/protocol/chatstates"/>
<feature var="http://jabber.org/protocol/disco#info"/>
<feature var="http://jabber.org/protocol/disco#items"/>
<feature var="urn:xmpp:ping"/>
<feature var="jabber:iq:time"/>
<feature var="jabber:iq:privacy"/>
<feature var="jabber:iq:version"/>
<feature var="http://jabber.org/protocol/rosterx"/>
<feature var="urn:xmpp:time"/>
<feature var="jabber:x:oob"/>
<feature var="http://jabber.org/protocol/ibb"/>
<feature var="http://jabber.org/protocol/si/profile/file-transfer"/>
<feature var="urn:xmpp:receipts"/>
<feature var="jabber:iq:roster"/>
<feature var="jabber:iq:last"/>
</query>
"#
.parse()
.unwrap();
let expected = vec![
104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112,
114, 111, 116, 111, 99, 111, 108, 47, 98, 121, 116, 101, 115, 116, 114, 101, 97, 109,
115, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103,
47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 99, 104, 97, 116, 115, 116, 97, 116,
101, 115, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114,
103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 100, 105, 115, 99, 111, 35, 105,
110, 102, 111, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111,
114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 100, 105, 115, 99, 111, 35,
105, 116, 101, 109, 115, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114,
46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 105, 98, 98, 31, 104,
116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114,
111, 116, 111, 99, 111, 108, 47, 114, 111, 115, 116, 101, 114, 120, 31, 104, 116, 116,
112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116,
111, 99, 111, 108, 47, 115, 105, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98,
101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 115, 105,
47, 112, 114, 111, 102, 105, 108, 101, 47, 102, 105, 108, 101, 45, 116, 114, 97, 110,
115, 102, 101, 114, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 108, 97, 115, 116,
31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 112, 114, 105, 118, 97, 99, 121, 31,
106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 114, 111, 115, 116, 101, 114, 31, 106, 97,
98, 98, 101, 114, 58, 105, 113, 58, 116, 105, 109, 101, 31, 106, 97, 98, 98, 101, 114,
58, 105, 113, 58, 118, 101, 114, 115, 105, 111, 110, 31, 106, 97, 98, 98, 101, 114, 58,
120, 58, 111, 111, 98, 31, 117, 114, 110, 58, 120, 109, 112, 112, 58, 112, 105, 110,
103, 31, 117, 114, 110, 58, 120, 109, 112, 112, 58, 114, 101, 99, 101, 105, 112, 116,
115, 31, 117, 114, 110, 58, 120, 109, 112, 112, 58, 116, 105, 109, 101, 31, 28, 99,
108, 105, 101, 110, 116, 31, 109, 111, 98, 105, 108, 101, 31, 31, 66, 111, 109, 98,
117, 115, 77, 111, 100, 31, 30, 28, 28,
];
let disco = DiscoInfoResult::try_from(elem).unwrap();
let ecaps2 = compute_disco(&disco).unwrap();
assert_eq!(ecaps2.len(), 0x1d9);
assert_eq!(ecaps2, expected);
let sha_256 = hash_ecaps2(&ecaps2, Algo::Sha_256).unwrap();
assert_eq!(
sha_256.hash,
base64::decode("kzBZbkqJ3ADrj7v08reD1qcWUwNGHaidNUgD7nHpiw8=").unwrap()
);
let sha3_256 = hash_ecaps2(&ecaps2, Algo::Sha3_256).unwrap();
assert_eq!(
sha3_256.hash,
base64::decode("79mdYAfU9rEdTOcWDO7UEAt6E56SUzk/g6TnqUeuD9Q=").unwrap()
);
}
#[test]
fn test_xep_ex2() {
let elem: Element = r#"
<query xmlns="http://jabber.org/protocol/disco#info">
<identity category="client" name="Tkabber" type="pc" xml:lang="en"/>
<identity category="client" name="Ткаббер" type="pc" xml:lang="ru"/>
<feature var="games:board"/>
<feature var="http://jabber.org/protocol/activity"/>
<feature var="http://jabber.org/protocol/activity+notify"/>
<feature var="http://jabber.org/protocol/bytestreams"/>
<feature var="http://jabber.org/protocol/chatstates"/>
<feature var="http://jabber.org/protocol/commands"/>
<feature var="http://jabber.org/protocol/disco#info"/>
<feature var="http://jabber.org/protocol/disco#items"/>
<feature var="http://jabber.org/protocol/evil"/>
<feature var="http://jabber.org/protocol/feature-neg"/>
<feature var="http://jabber.org/protocol/geoloc"/>
<feature var="http://jabber.org/protocol/geoloc+notify"/>
<feature var="http://jabber.org/protocol/ibb"/>
<feature var="http://jabber.org/protocol/iqibb"/>
<feature var="http://jabber.org/protocol/mood"/>
<feature var="http://jabber.org/protocol/mood+notify"/>
<feature var="http://jabber.org/protocol/rosterx"/>
<feature var="http://jabber.org/protocol/si"/>
<feature var="http://jabber.org/protocol/si/profile/file-transfer"/>
<feature var="http://jabber.org/protocol/tune"/>
<feature var="http://www.facebook.com/xmpp/messages"/>
<feature var="http://www.xmpp.org/extensions/xep-0084.html#ns-metadata+notify"/>
<feature var="jabber:iq:avatar"/>
<feature var="jabber:iq:browse"/>
<feature var="jabber:iq:dtcp"/>
<feature var="jabber:iq:filexfer"/>
<feature var="jabber:iq:ibb"/>
<feature var="jabber:iq:inband"/>
<feature var="jabber:iq:jidlink"/>
<feature var="jabber:iq:last"/>
<feature var="jabber:iq:oob"/>
<feature var="jabber:iq:privacy"/>
<feature var="jabber:iq:roster"/>
<feature var="jabber:iq:time"/>
<feature var="jabber:iq:version"/>
<feature var="jabber:x:data"/>
<feature var="jabber:x:event"/>
<feature var="jabber:x:oob"/>
<feature var="urn:xmpp:avatar:metadata+notify"/>
<feature var="urn:xmpp:ping"/>
<feature var="urn:xmpp:receipts"/>
<feature var="urn:xmpp:time"/>
<x xmlns="jabber:x:data" type="result">
<field type="hidden" var="FORM_TYPE">
<value>urn:xmpp:dataforms:softwareinfo</value>
</field>
<field var="software">
<value>Tkabber</value>
</field>
<field var="software_version">
<value>0.11.1-svn-20111216-mod (Tcl/Tk 8.6b2)</value>
</field>
<field var="os">
<value>Windows</value>
</field>
<field var="os_version">
<value>XP</value>
</field>
</x>
</query>
"#
.parse()
.unwrap();
let expected = vec![
103, 97, 109, 101, 115, 58, 98, 111, 97, 114, 100, 31, 104, 116, 116, 112, 58, 47, 47,
106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111,
108, 47, 97, 99, 116, 105, 118, 105, 116, 121, 31, 104, 116, 116, 112, 58, 47, 47, 106,
97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47,
97, 99, 116, 105, 118, 105, 116, 121, 43, 110, 111, 116, 105, 102, 121, 31, 104, 116,
116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111,
116, 111, 99, 111, 108, 47, 98, 121, 116, 101, 115, 116, 114, 101, 97, 109, 115, 31,
104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112,
114, 111, 116, 111, 99, 111, 108, 47, 99, 104, 97, 116, 115, 116, 97, 116, 101, 115,
31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47,
112, 114, 111, 116, 111, 99, 111, 108, 47, 99, 111, 109, 109, 97, 110, 100, 115, 31,
104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112,
114, 111, 116, 111, 99, 111, 108, 47, 100, 105, 115, 99, 111, 35, 105, 110, 102, 111,
31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47,
112, 114, 111, 116, 111, 99, 111, 108, 47, 100, 105, 115, 99, 111, 35, 105, 116, 101,
109, 115, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114,
103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 101, 118, 105, 108, 31, 104, 116,
116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111,
116, 111, 99, 111, 108, 47, 102, 101, 97, 116, 117, 114, 101, 45, 110, 101, 103, 31,
104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112,
114, 111, 116, 111, 99, 111, 108, 47, 103, 101, 111, 108, 111, 99, 31, 104, 116, 116,
112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116,
111, 99, 111, 108, 47, 103, 101, 111, 108, 111, 99, 43, 110, 111, 116, 105, 102, 121,
31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47,
112, 114, 111, 116, 111, 99, 111, 108, 47, 105, 98, 98, 31, 104, 116, 116, 112, 58, 47,
47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111,
108, 47, 105, 113, 105, 98, 98, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98,
101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 109, 111,
111, 100, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114,
103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 109, 111, 111, 100, 43, 110, 111,
116, 105, 102, 121, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46,
111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 114, 111, 115, 116, 101,
114, 120, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114,
103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 115, 105, 31, 104, 116, 116, 112,
58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111,
99, 111, 108, 47, 115, 105, 47, 112, 114, 111, 102, 105, 108, 101, 47, 102, 105, 108,
101, 45, 116, 114, 97, 110, 115, 102, 101, 114, 31, 104, 116, 116, 112, 58, 47, 47,
106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111,
108, 47, 116, 117, 110, 101, 31, 104, 116, 116, 112, 58, 47, 47, 119, 119, 119, 46,
102, 97, 99, 101, 98, 111, 111, 107, 46, 99, 111, 109, 47, 120, 109, 112, 112, 47, 109,
101, 115, 115, 97, 103, 101, 115, 31, 104, 116, 116, 112, 58, 47, 47, 119, 119, 119,
46, 120, 109, 112, 112, 46, 111, 114, 103, 47, 101, 120, 116, 101, 110, 115, 105, 111,
110, 115, 47, 120, 101, 112, 45, 48, 48, 56, 52, 46, 104, 116, 109, 108, 35, 110, 115,
45, 109, 101, 116, 97, 100, 97, 116, 97, 43, 110, 111, 116, 105, 102, 121, 31, 106, 97,
98, 98, 101, 114, 58, 105, 113, 58, 97, 118, 97, 116, 97, 114, 31, 106, 97, 98, 98,
101, 114, 58, 105, 113, 58, 98, 114, 111, 119, 115, 101, 31, 106, 97, 98, 98, 101, 114,
58, 105, 113, 58, 100, 116, 99, 112, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58,
102, 105, 108, 101, 120, 102, 101, 114, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113,
58, 105, 98, 98, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 105, 110, 98, 97,
110, 100, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 106, 105, 100, 108, 105,
110, 107, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 108, 97, 115, 116, 31, 106,
97, 98, 98, 101, 114, 58, 105, 113, 58, 111, 111, 98, 31, 106, 97, 98, 98, 101, 114,
58, 105, 113, 58, 112, 114, 105, 118, 97, 99, 121, 31, 106, 97, 98, 98, 101, 114, 58,
105, 113, 58, 114, 111, 115, 116, 101, 114, 31, 106, 97, 98, 98, 101, 114, 58, 105,
113, 58, 116, 105, 109, 101, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 118, 101,
114, 115, 105, 111, 110, 31, 106, 97, 98, 98, 101, 114, 58, 120, 58, 100, 97, 116, 97,
31, 106, 97, 98, 98, 101, 114, 58, 120, 58, 101, 118, 101, 110, 116, 31, 106, 97, 98,
98, 101, 114, 58, 120, 58, 111, 111, 98, 31, 117, 114, 110, 58, 120, 109, 112, 112, 58,
97, 118, 97, 116, 97, 114, 58, 109, 101, 116, 97, 100, 97, 116, 97, 43, 110, 111, 116,
105, 102, 121, 31, 117, 114, 110, 58, 120, 109, 112, 112, 58, 112, 105, 110, 103, 31,
117, 114, 110, 58, 120, 109, 112, 112, 58, 114, 101, 99, 101, 105, 112, 116, 115, 31,
117, 114, 110, 58, 120, 109, 112, 112, 58, 116, 105, 109, 101, 31, 28, 99, 108, 105,
101, 110, 116, 31, 112, 99, 31, 101, 110, 31, 84, 107, 97, 98, 98, 101, 114, 31, 30,
99, 108, 105, 101, 110, 116, 31, 112, 99, 31, 114, 117, 31, 208, 162, 208, 186, 208,
176, 208, 177, 208, 177, 208, 181, 209, 128, 31, 30, 28, 70, 79, 82, 77, 95, 84, 89,
80, 69, 31, 117, 114, 110, 58, 120, 109, 112, 112, 58, 100, 97, 116, 97, 102, 111, 114,
109, 115, 58, 115, 111, 102, 116, 119, 97, 114, 101, 105, 110, 102, 111, 31, 30, 111,
115, 31, 87, 105, 110, 100, 111, 119, 115, 31, 30, 111, 115, 95, 118, 101, 114, 115,
105, 111, 110, 31, 88, 80, 31, 30, 115, 111, 102, 116, 119, 97, 114, 101, 31, 84, 107,
97, 98, 98, 101, 114, 31, 30, 115, 111, 102, 116, 119, 97, 114, 101, 95, 118, 101, 114,
115, 105, 111, 110, 31, 48, 46, 49, 49, 46, 49, 45, 115, 118, 110, 45, 50, 48, 49, 49,
49, 50, 49, 54, 45, 109, 111, 100, 32, 40, 84, 99, 108, 47, 84, 107, 32, 56, 46, 54,
98, 50, 41, 31, 30, 29, 28,
];
let disco = DiscoInfoResult::try_from(elem).unwrap();
let ecaps2 = compute_disco(&disco).unwrap();
assert_eq!(ecaps2.len(), 0x543);
assert_eq!(ecaps2, expected);
let sha_256 = hash_ecaps2(&ecaps2, Algo::Sha_256).unwrap();
assert_eq!(
sha_256.hash,
base64::decode("u79ZroNJbdSWhdSp311mddz44oHHPsEBntQ5b1jqBSY=").unwrap()
);
let sha3_256 = hash_ecaps2(&ecaps2, Algo::Sha3_256).unwrap();
assert_eq!(
sha3_256.hash,
base64::decode("XpUJzLAc93258sMECZ3FJpebkzuyNXDzRNwQog8eycg=").unwrap()
);
}
#[test]
fn test_blake2b_512() {
let hash = hash_ecaps2("abc".as_bytes(), Algo::Blake2b_512).unwrap();
let known_hash: Vec<u8> = vec![
0xBA, 0x80, 0xA5, 0x3F, 0x98, 0x1C, 0x4D, 0x0D, 0x6A, 0x27, 0x97, 0xB6, 0x9F, 0x12,
0xF6, 0xE9, 0x4C, 0x21, 0x2F, 0x14, 0x68, 0x5A, 0xC4, 0xB7, 0x4B, 0x12, 0xBB, 0x6F,
0xDB, 0xFF, 0xA2, 0xD1, 0x7D, 0x87, 0xC5, 0x39, 0x2A, 0xAB, 0x79, 0x2D, 0xC2, 0x52,
0xD5, 0xDE, 0x45, 0x33, 0xCC, 0x95, 0x18, 0xD3, 0x8A, 0xA8, 0xDB, 0xF1, 0x92, 0x5A,
0xB9, 0x23, 0x86, 0xED, 0xD4, 0x00, 0x99, 0x23,
];
assert_eq!(hash.hash, known_hash);
}
}

96
xmpp-parsers/src/eme.rs Normal file
View file

@ -0,0 +1,96 @@
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 crate::message::MessagePayload;
generate_element!(
/// Structure representing an `<encryption xmlns='urn:xmpp:eme:0'/>` element.
ExplicitMessageEncryption, "encryption", EME,
attributes: [
/// Namespace of the encryption scheme used.
namespace: Required<String> = "namespace",
/// User-friendly name for the encryption scheme, should be `None` for OTR,
/// legacy OpenPGP and OX.
name: Option<String> = "name",
]
);
impl MessagePayload for ExplicitMessageEncryption {}
#[cfg(test)]
mod tests {
use super::*;
use crate::util::error::Error;
use crate::Element;
use std::convert::TryFrom;
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(ExplicitMessageEncryption, 24);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(ExplicitMessageEncryption, 48);
}
#[test]
fn test_simple() {
let elem: Element = "<encryption xmlns='urn:xmpp:eme:0' namespace='urn:xmpp:otr:0'/>"
.parse()
.unwrap();
let encryption = ExplicitMessageEncryption::try_from(elem).unwrap();
assert_eq!(encryption.namespace, "urn:xmpp:otr:0");
assert_eq!(encryption.name, None);
let elem: Element = "<encryption xmlns='urn:xmpp:eme:0' namespace='some.unknown.mechanism' name='SuperMechanism'/>".parse().unwrap();
let encryption = ExplicitMessageEncryption::try_from(elem).unwrap();
assert_eq!(encryption.namespace, "some.unknown.mechanism");
assert_eq!(encryption.name, Some(String::from("SuperMechanism")));
}
#[test]
fn test_unknown() {
let elem: Element = "<replace xmlns='urn:xmpp:message-correct:0'/>"
.parse()
.unwrap();
let error = ExplicitMessageEncryption::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "This is not a encryption element.");
}
#[test]
fn test_invalid_child() {
let elem: Element = "<encryption xmlns='urn:xmpp:eme:0'><coucou/></encryption>"
.parse()
.unwrap();
let error = ExplicitMessageEncryption::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown child in encryption element.");
}
#[test]
fn test_serialise() {
let elem: Element = "<encryption xmlns='urn:xmpp:eme:0' namespace='coucou'/>"
.parse()
.unwrap();
let eme = ExplicitMessageEncryption {
namespace: String::from("coucou"),
name: None,
};
let elem2 = eme.into();
assert_eq!(elem, elem2);
}
}

View file

@ -0,0 +1,74 @@
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 crate::delay::Delay;
use crate::message::Message;
generate_element!(
/// Contains a forwarded stanza, either standalone or part of another
/// extension (such as carbons).
Forwarded, "forwarded", FORWARD,
children: [
/// When the stanza originally got sent.
delay: Option<Delay> = ("delay", DELAY) => Delay,
// XXX: really? Option?
/// The stanza being forwarded.
stanza: Option<Message> = ("message", DEFAULT_NS) => Message
// TODO: also handle the two other stanza possibilities.
]
);
#[cfg(test)]
mod tests {
use super::*;
use crate::util::error::Error;
use crate::Element;
use std::convert::TryFrom;
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(Forwarded, 212);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(Forwarded, 408);
}
#[test]
fn test_simple() {
let elem: Element = "<forwarded xmlns='urn:xmpp:forward:0'/>".parse().unwrap();
Forwarded::try_from(elem).unwrap();
}
#[test]
fn test_invalid_child() {
let elem: Element = "<forwarded xmlns='urn:xmpp:forward:0'><coucou/></forwarded>"
.parse()
.unwrap();
let error = Forwarded::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown child in forwarded element.");
}
#[test]
fn test_serialise() {
let elem: Element = "<forwarded xmlns='urn:xmpp:forward:0'/>".parse().unwrap();
let forwarded = Forwarded {
delay: None,
stanza: None,
};
let elem2 = forwarded.into();
assert_eq!(elem, elem2);
}
}

269
xmpp-parsers/src/hashes.rs Normal file
View file

@ -0,0 +1,269 @@
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 crate::util::error::Error;
use crate::util::helpers::Base64;
use minidom::IntoAttributeValue;
use std::num::ParseIntError;
use std::ops::{Deref, DerefMut};
use std::str::FromStr;
/// List of the algorithms we support, or Unknown.
#[allow(non_camel_case_types)]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Algo {
/// The Secure Hash Algorithm 1, with known vulnerabilities, do not use it.
///
/// See https://tools.ietf.org/html/rfc3174
Sha_1,
/// The Secure Hash Algorithm 2, in its 256-bit version.
///
/// See https://tools.ietf.org/html/rfc6234
Sha_256,
/// The Secure Hash Algorithm 2, in its 512-bit version.
///
/// See https://tools.ietf.org/html/rfc6234
Sha_512,
/// The Secure Hash Algorithm 3, based on Keccak, in its 256-bit version.
///
/// See https://keccak.team/files/Keccak-submission-3.pdf
Sha3_256,
/// The Secure Hash Algorithm 3, based on Keccak, in its 512-bit version.
///
/// See https://keccak.team/files/Keccak-submission-3.pdf
Sha3_512,
/// The BLAKE2 hash algorithm, for a 256-bit output.
///
/// See https://tools.ietf.org/html/rfc7693
Blake2b_256,
/// The BLAKE2 hash algorithm, for a 512-bit output.
///
/// See https://tools.ietf.org/html/rfc7693
Blake2b_512,
/// An unknown hash not in this list, you can probably reject it.
Unknown(String),
}
impl FromStr for Algo {
type Err = Error;
fn from_str(s: &str) -> Result<Algo, Error> {
Ok(match s {
"" => return Err(Error::ParseError("'algo' argument cant be empty.")),
"sha-1" => Algo::Sha_1,
"sha-256" => Algo::Sha_256,
"sha-512" => Algo::Sha_512,
"sha3-256" => Algo::Sha3_256,
"sha3-512" => Algo::Sha3_512,
"blake2b-256" => Algo::Blake2b_256,
"blake2b-512" => Algo::Blake2b_512,
value => Algo::Unknown(value.to_owned()),
})
}
}
impl From<Algo> for String {
fn from(algo: Algo) -> String {
String::from(match algo {
Algo::Sha_1 => "sha-1",
Algo::Sha_256 => "sha-256",
Algo::Sha_512 => "sha-512",
Algo::Sha3_256 => "sha3-256",
Algo::Sha3_512 => "sha3-512",
Algo::Blake2b_256 => "blake2b-256",
Algo::Blake2b_512 => "blake2b-512",
Algo::Unknown(text) => return text,
})
}
}
impl IntoAttributeValue for Algo {
fn into_attribute_value(self) -> Option<String> {
Some(String::from(self))
}
}
generate_element!(
/// This element represents a hash of some data, defined by the hash
/// algorithm used and the computed value.
#[derive(PartialEq)]
Hash, "hash", HASHES,
attributes: [
/// The algorithm used to create this hash.
algo: Required<Algo> = "algo"
],
text: (
/// The hash value, as a vector of bytes.
hash: Base64<Vec<u8>>
)
);
impl Hash {
/// Creates a [Hash] element with the given algo and data.
pub fn new(algo: Algo, hash: Vec<u8>) -> Hash {
Hash { algo, hash }
}
/// Like [new](#method.new) but takes base64-encoded data before decoding
/// it.
pub fn from_base64(algo: Algo, hash: &str) -> Result<Hash, Error> {
Ok(Hash::new(algo, base64::decode(hash)?))
}
/// Like [new](#method.new) but takes hex-encoded data before decoding it.
pub fn from_hex(algo: Algo, hex: &str) -> Result<Hash, ParseIntError> {
let mut bytes = vec![];
for i in 0..hex.len() / 2 {
let byte = u8::from_str_radix(&hex[2 * i..2 * i + 2], 16)?;
bytes.push(byte);
}
Ok(Hash::new(algo, bytes))
}
/// Like [new](#method.new) but takes hex-encoded data before decoding it.
pub fn from_colon_separated_hex(algo: Algo, hex: &str) -> Result<Hash, ParseIntError> {
let mut bytes = vec![];
for i in 0..(1 + hex.len()) / 3 {
let byte = u8::from_str_radix(&hex[3 * i..3 * i + 2], 16)?;
if 3 * i + 2 < hex.len() {
assert_eq!(&hex[3 * i + 2..3 * i + 3], ":");
}
bytes.push(byte);
}
Ok(Hash::new(algo, bytes))
}
/// Formats this hash into base64.
pub fn to_base64(&self) -> String {
base64::encode(&self.hash[..])
}
/// Formats this hash into hexadecimal.
pub fn to_hex(&self) -> String {
let mut bytes = vec![];
for byte in self.hash.iter() {
bytes.push(format!("{:02x}", byte));
}
bytes.join("")
}
/// Formats this hash into colon-separated hexadecimal.
pub fn to_colon_separated_hex(&self) -> String {
let mut bytes = vec![];
for byte in self.hash.iter() {
bytes.push(format!("{:02x}", byte));
}
bytes.join(":")
}
}
/// Helper for parsing and serialising a SHA-1 attribute.
#[derive(Debug, Clone, PartialEq)]
pub struct Sha1HexAttribute(Hash);
impl FromStr for Sha1HexAttribute {
type Err = ParseIntError;
fn from_str(hex: &str) -> Result<Self, Self::Err> {
let hash = Hash::from_hex(Algo::Sha_1, hex)?;
Ok(Sha1HexAttribute(hash))
}
}
impl IntoAttributeValue for Sha1HexAttribute {
fn into_attribute_value(self) -> Option<String> {
Some(self.to_hex())
}
}
impl DerefMut for Sha1HexAttribute {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl Deref for Sha1HexAttribute {
type Target = Hash;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Element;
use std::convert::TryFrom;
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(Algo, 16);
assert_size!(Hash, 28);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(Algo, 32);
assert_size!(Hash, 56);
}
#[test]
fn test_simple() {
let elem: Element = "<hash xmlns='urn:xmpp:hashes:2' algo='sha-256'>2XarmwTlNxDAMkvymloX3S5+VbylNrJt/l5QyPa+YoU=</hash>".parse().unwrap();
let hash = Hash::try_from(elem).unwrap();
assert_eq!(hash.algo, Algo::Sha_256);
assert_eq!(
hash.hash,
base64::decode("2XarmwTlNxDAMkvymloX3S5+VbylNrJt/l5QyPa+YoU=").unwrap()
);
}
#[test]
fn value_serialisation() {
let elem: Element = "<hash xmlns='urn:xmpp:hashes:2' algo='sha-256'>2XarmwTlNxDAMkvymloX3S5+VbylNrJt/l5QyPa+YoU=</hash>".parse().unwrap();
let hash = Hash::try_from(elem).unwrap();
assert_eq!(hash.to_base64(), "2XarmwTlNxDAMkvymloX3S5+VbylNrJt/l5QyPa+YoU=");
assert_eq!(hash.to_hex(), "d976ab9b04e53710c0324bf29a5a17dd2e7e55bca536b26dfe5e50c8f6be6285");
assert_eq!(hash.to_colon_separated_hex(), "d9:76:ab:9b:04:e5:37:10:c0:32:4b:f2:9a:5a:17:dd:2e:7e:55:bc:a5:36:b2:6d:fe:5e:50:c8:f6:be:62:85");
}
#[test]
fn test_unknown() {
let elem: Element = "<replace xmlns='urn:xmpp:message-correct:0'/>"
.parse()
.unwrap();
let error = Hash::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "This is not a hash element.");
}
#[test]
fn test_invalid_child() {
let elem: Element = "<hash xmlns='urn:xmpp:hashes:2'><coucou/></hash>"
.parse()
.unwrap();
let error = Hash::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown child in hash element.");
}
}

172
xmpp-parsers/src/ibb.rs Normal file
View file

@ -0,0 +1,172 @@
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 crate::util::helpers::Base64;
use crate::iq::IqSetPayload;
generate_id!(
/// An identifier matching a stream.
StreamId
);
generate_attribute!(
/// Which stanza type to use to exchange data.
Stanza, "stanza", {
/// `<iq/>` gives a feedback on whether the chunk has been received or not,
/// which is useful in the case the recipient might not receive them in a
/// timely manner, or to do your own throttling based on the results.
Iq => "iq",
/// `<message/>` can be faster, since it doesnt require any feedback, but in
/// practice it will be throttled by the servers on the way.
Message => "message",
}, Default = Iq);
generate_element!(
/// Starts an In-Band Bytestream session with the given parameters.
Open, "open", IBB,
attributes: [
/// Maximum size in bytes for each chunk.
block_size: Required<u16> = "block-size",
/// The identifier to be used to create a stream.
sid: Required<StreamId> = "sid",
/// Which stanza type to use to exchange data.
stanza: Default<Stanza> = "stanza",
]);
impl IqSetPayload for Open {}
generate_element!(
/// Exchange a chunk of data in an open stream.
Data, "data", IBB,
attributes: [
/// Sequence number of this chunk, must wraparound after 65535.
seq: Required<u16> = "seq",
/// The identifier of the stream on which data is being exchanged.
sid: Required<StreamId> = "sid"
],
text: (
/// Vector of bytes to be exchanged.
data: Base64<Vec<u8>>
)
);
impl IqSetPayload for Data {}
generate_element!(
/// Close an open stream.
Close, "close", IBB,
attributes: [
/// The identifier of the stream to be closed.
sid: Required<StreamId> = "sid",
]);
impl IqSetPayload for Close {}
#[cfg(test)]
mod tests {
use super::*;
use crate::util::error::Error;
use crate::Element;
use std::error::Error as StdError;
use std::convert::TryFrom;
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(StreamId, 12);
assert_size!(Stanza, 1);
assert_size!(Open, 16);
assert_size!(Data, 28);
assert_size!(Close, 12);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(StreamId, 24);
assert_size!(Stanza, 1);
assert_size!(Open, 32);
assert_size!(Data, 56);
assert_size!(Close, 24);
}
#[test]
fn test_simple() {
let sid = StreamId(String::from("coucou"));
let elem: Element =
"<open xmlns='http://jabber.org/protocol/ibb' block-size='3' sid='coucou'/>"
.parse()
.unwrap();
let open = Open::try_from(elem).unwrap();
assert_eq!(open.block_size, 3);
assert_eq!(open.sid, sid);
assert_eq!(open.stanza, Stanza::Iq);
let elem: Element =
"<data xmlns='http://jabber.org/protocol/ibb' seq='0' sid='coucou'>AAAA</data>"
.parse()
.unwrap();
let data = Data::try_from(elem).unwrap();
assert_eq!(data.seq, 0);
assert_eq!(data.sid, sid);
assert_eq!(data.data, vec!(0, 0, 0));
let elem: Element = "<close xmlns='http://jabber.org/protocol/ibb' sid='coucou'/>"
.parse()
.unwrap();
let close = Close::try_from(elem).unwrap();
assert_eq!(close.sid, sid);
}
#[test]
fn test_invalid() {
let elem: Element = "<open xmlns='http://jabber.org/protocol/ibb'/>"
.parse()
.unwrap();
let error = Open::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Required attribute 'block-size' missing.");
let elem: Element = "<open xmlns='http://jabber.org/protocol/ibb' block-size='-5'/>"
.parse()
.unwrap();
let error = Open::try_from(elem).unwrap_err();
let message = match error {
Error::ParseIntError(error) => error,
_ => panic!(),
};
assert_eq!(message.description(), "invalid digit found in string");
let elem: Element = "<open xmlns='http://jabber.org/protocol/ibb' block-size='128'/>"
.parse()
.unwrap();
let error = Open::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(error) => error,
_ => panic!(),
};
assert_eq!(message, "Required attribute 'sid' missing.");
}
#[test]
fn test_invalid_stanza() {
let elem: Element = "<open xmlns='http://jabber.org/protocol/ibb' block-size='128' sid='coucou' stanza='fdsq'/>".parse().unwrap();
let error = Open::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown value for 'stanza' attribute.");
}
}

247
xmpp-parsers/src/ibr.rs Normal file
View file

@ -0,0 +1,247 @@
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 crate::data_forms::DataForm;
use crate::util::error::Error;
use crate::iq::{IqGetPayload, IqResultPayload, IqSetPayload};
use crate::ns;
use crate::Element;
use std::collections::HashMap;
use std::convert::TryFrom;
/// Query for registering against a service.
#[derive(Debug, Clone)]
pub struct Query {
/// Deprecated fixed list of possible fields to fill before the user can
/// register.
pub fields: HashMap<String, String>,
/// Whether this account is already registered.
pub registered: bool,
/// Whether to remove this account.
pub remove: bool,
/// A data form the user must fill before being allowed to register.
pub form: Option<DataForm>,
// Not yet implemented.
//pub oob: Option<Oob>,
}
impl IqGetPayload for Query {}
impl IqSetPayload for Query {}
impl IqResultPayload for Query {}
impl TryFrom<Element> for Query {
type Error = Error;
fn try_from(elem: Element) -> Result<Query, Error> {
check_self!(elem, "query", REGISTER, "IBR query");
let mut query = Query {
registered: false,
fields: HashMap::new(),
remove: false,
form: None,
};
for child in elem.children() {
let namespace = child.ns().unwrap();
if namespace == ns::REGISTER {
let name = child.name();
let fields = vec![
"address",
"city",
"date",
"email",
"first",
"instructions",
"key",
"last",
"misc",
"name",
"nick",
"password",
"phone",
"state",
"text",
"url",
"username",
"zip",
];
if fields.binary_search(&name).is_ok() {
query.fields.insert(name.to_owned(), child.text());
} else if name == "registered" {
query.registered = true;
} else if name == "remove" {
query.remove = true;
} else {
return Err(Error::ParseError("Wrong field in ibr element."));
}
} else if child.is("x", ns::DATA_FORMS) {
query.form = Some(DataForm::try_from(child.clone())?);
} else {
return Err(Error::ParseError("Unknown child in ibr element."));
}
}
Ok(query)
}
}
impl From<Query> for Element {
fn from(query: Query) -> Element {
Element::builder("query")
.ns(ns::REGISTER)
.append_all(if query.registered {
Some(Element::builder("registered").ns(ns::REGISTER))
} else {
None
})
.append_all(
query
.fields
.into_iter()
.map(|(name, value)| Element::builder(name).ns(ns::REGISTER).append(value))
)
.append_all(if query.remove {
Some(Element::builder("remove").ns(ns::REGISTER))
} else {
None
})
.append_all(query.form.map(Element::from))
.build()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::util::compare_elements::NamespaceAwareCompare;
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(Query, 96);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(Query, 168);
}
#[test]
fn test_simple() {
let elem: Element = "<query xmlns='jabber:iq:register'/>".parse().unwrap();
Query::try_from(elem).unwrap();
}
#[test]
fn test_ex2() {
let elem: Element = r#"
<query xmlns='jabber:iq:register'>
<instructions>
Choose a username and password for use with this service.
Please also provide your email address.
</instructions>
<username/>
<password/>
<email/>
</query>
"#
.parse()
.unwrap();
let query = Query::try_from(elem).unwrap();
assert_eq!(query.registered, false);
assert_eq!(query.fields["instructions"], "\n Choose a username and password for use with this service.\n Please also provide your email address.\n ");
assert_eq!(query.fields["username"], "");
assert_eq!(query.fields["password"], "");
assert_eq!(query.fields["email"], "");
assert_eq!(query.fields.contains_key("name"), false);
// FIXME: HashMap doesnt keep the order right.
//let elem2 = query.into();
//assert_eq!(elem, elem2);
}
#[test]
fn test_ex9() {
let elem: Element = r#"
<query xmlns='jabber:iq:register'>
<instructions>
Use the enclosed form to register. If your Jabber client does not
support Data Forms, visit http://www.shakespeare.lit/contests.php
</instructions>
<x xmlns='jabber:x:data' type='form'>
<title>Contest Registration</title>
<instructions>
Please provide the following information
to sign up for our special contests!
</instructions>
<field type='hidden' var='FORM_TYPE'>
<value>jabber:iq:register</value>
</field>
<field label='Given Name' var='first'>
<required/>
</field>
<field label='Family Name' var='last'>
<required/>
</field>
<field label='Email Address' var='email'>
<required/>
</field>
<field type='list-single' label='Gender' var='x-gender'>
<option label='Male'><value>M</value></option>
<option label='Female'><value>F</value></option>
</field>
</x>
</query>
"#
.parse()
.unwrap();
let elem1 = elem.clone();
let query = Query::try_from(elem).unwrap();
assert_eq!(query.registered, false);
assert!(!query.fields["instructions"].is_empty());
let form = query.form.clone().unwrap();
assert!(!form.instructions.unwrap().is_empty());
let elem2 = query.into();
assert!(elem1.compare_to(&elem2));
}
#[test]
fn test_ex10() {
let elem: Element = r#"
<query xmlns='jabber:iq:register'>
<x xmlns='jabber:x:data' type='submit'>
<field type='hidden' var='FORM_TYPE'>
<value>jabber:iq:register</value>
</field>
<field label='Given Name' var='first'>
<value>Juliet</value>
</field>
<field label='Family Name' var='last'>
<value>Capulet</value>
</field>
<field label='Email Address' var='email'>
<value>juliet@capulet.com</value>
</field>
<field type='list-single' label='Gender' var='x-gender'>
<value>F</value>
</field>
</x>
</query>
"#
.parse()
.unwrap();
let elem1 = elem.clone();
let query = Query::try_from(elem).unwrap();
assert_eq!(query.registered, false);
for _ in &query.fields {
panic!();
}
let elem2 = query.into();
assert!(elem1.compare_to(&elem2));
}
}

147
xmpp-parsers/src/idle.rs Normal file
View file

@ -0,0 +1,147 @@
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 crate::date::DateTime;
use crate::presence::PresencePayload;
generate_element!(
/// Represents the last time the user interacted with their system.
Idle, "idle", IDLE,
attributes: [
/// The time at which the user stopped interacting.
since: Required<DateTime> = "since",
]
);
impl PresencePayload for Idle {}
#[cfg(test)]
mod tests {
use super::*;
use crate::util::error::Error;
use crate::Element;
use std::error::Error as StdError;
use std::str::FromStr;
use std::convert::TryFrom;
#[test]
fn test_size() {
assert_size!(Idle, 16);
}
#[test]
fn test_simple() {
let elem: Element = "<idle xmlns='urn:xmpp:idle:1' since='2017-05-21T20:19:55+01:00'/>"
.parse()
.unwrap();
Idle::try_from(elem).unwrap();
}
#[test]
fn test_invalid_child() {
let elem: Element = "<idle xmlns='urn:xmpp:idle:1'><coucou/></idle>"
.parse()
.unwrap();
let error = Idle::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown child in idle element.");
}
#[test]
fn test_invalid_id() {
let elem: Element = "<idle xmlns='urn:xmpp:idle:1'/>".parse().unwrap();
let error = Idle::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Required attribute 'since' missing.");
}
#[test]
fn test_invalid_date() {
// There is no thirteenth month.
let elem: Element = "<idle xmlns='urn:xmpp:idle:1' since='2017-13-01T12:23:34Z'/>"
.parse()
.unwrap();
let error = Idle::try_from(elem).unwrap_err();
let message = match error {
Error::ChronoParseError(string) => string,
_ => panic!(),
};
assert_eq!(message.description(), "input is out of range");
// Timezone ≥24:00 arent allowed.
let elem: Element = "<idle xmlns='urn:xmpp:idle:1' since='2017-05-27T12:11:02+25:00'/>"
.parse()
.unwrap();
let error = Idle::try_from(elem).unwrap_err();
let message = match error {
Error::ChronoParseError(string) => string,
_ => panic!(),
};
assert_eq!(message.description(), "input is out of range");
// Timezone without the : separator arent allowed.
let elem: Element = "<idle xmlns='urn:xmpp:idle:1' since='2017-05-27T12:11:02+0100'/>"
.parse()
.unwrap();
let error = Idle::try_from(elem).unwrap_err();
let message = match error {
Error::ChronoParseError(string) => string,
_ => panic!(),
};
assert_eq!(message.description(), "input contains invalid characters");
// No seconds, error message could be improved.
let elem: Element = "<idle xmlns='urn:xmpp:idle:1' since='2017-05-27T12:11+01:00'/>"
.parse()
.unwrap();
let error = Idle::try_from(elem).unwrap_err();
let message = match error {
Error::ChronoParseError(string) => string,
_ => panic!(),
};
assert_eq!(message.description(), "input contains invalid characters");
// TODO: maybe well want to support this one, as per XEP-0082 §4.
let elem: Element = "<idle xmlns='urn:xmpp:idle:1' since='20170527T12:11:02+01:00'/>"
.parse()
.unwrap();
let error = Idle::try_from(elem).unwrap_err();
let message = match error {
Error::ChronoParseError(string) => string,
_ => panic!(),
};
assert_eq!(message.description(), "input contains invalid characters");
// No timezone.
let elem: Element = "<idle xmlns='urn:xmpp:idle:1' since='2017-05-27T12:11:02'/>"
.parse()
.unwrap();
let error = Idle::try_from(elem).unwrap_err();
let message = match error {
Error::ChronoParseError(string) => string,
_ => panic!(),
};
assert_eq!(message.description(), "premature end of input");
}
#[test]
fn test_serialise() {
let elem: Element = "<idle xmlns='urn:xmpp:idle:1' since='2017-05-21T20:19:55+01:00'/>"
.parse()
.unwrap();
let idle = Idle {
since: DateTime::from_str("2017-05-21T20:19:55+01:00").unwrap(),
};
let elem2 = idle.into();
assert_eq!(elem, elem2);
}
}

447
xmpp-parsers/src/iq.rs Normal file
View file

@ -0,0 +1,447 @@
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
// Copyright (c) 2017 Maxime “pep” Buquet <pep@bouah.net>
//
// 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 crate::util::error::Error;
use crate::ns;
use crate::stanza_error::StanzaError;
use jid::Jid;
use crate::Element;
use minidom::IntoAttributeValue;
use std::convert::TryFrom;
/// Should be implemented on every known payload of an `<iq type='get'/>`.
pub trait IqGetPayload: TryFrom<Element> + Into<Element> {}
/// Should be implemented on every known payload of an `<iq type='set'/>`.
pub trait IqSetPayload: TryFrom<Element> + Into<Element> {}
/// Should be implemented on every known payload of an `<iq type='result'/>`.
pub trait IqResultPayload: TryFrom<Element> + Into<Element> {}
/// Represents one of the four possible iq types.
#[derive(Debug, Clone)]
pub enum IqType {
/// This is a request for accessing some data.
Get(Element),
/// This is a request for modifying some data.
Set(Element),
/// This is a result containing some data.
Result(Option<Element>),
/// A get or set request failed.
Error(StanzaError),
}
impl<'a> IntoAttributeValue for &'a IqType {
fn into_attribute_value(self) -> Option<String> {
Some(
match *self {
IqType::Get(_) => "get",
IqType::Set(_) => "set",
IqType::Result(_) => "result",
IqType::Error(_) => "error",
}
.to_owned(),
)
}
}
/// The main structure representing the `<iq/>` stanza.
#[derive(Debug, Clone)]
pub struct Iq {
/// The JID emitting this stanza.
pub from: Option<Jid>,
/// The recipient of this stanza.
pub to: Option<Jid>,
/// The @id attribute of this stanza, which is required in order to match a
/// request with its result/error.
pub id: String,
/// The payload content of this stanza.
pub payload: IqType,
}
impl Iq {
/// Creates an `<iq/>` stanza containing a get request.
pub fn from_get<S: Into<String>>(id: S, payload: impl IqGetPayload) -> Iq {
Iq {
from: None,
to: None,
id: id.into(),
payload: IqType::Get(payload.into()),
}
}
/// Creates an `<iq/>` stanza containing a set request.
pub fn from_set<S: Into<String>>(id: S, payload: impl IqSetPayload) -> Iq {
Iq {
from: None,
to: None,
id: id.into(),
payload: IqType::Set(payload.into()),
}
}
/// Creates an `<iq/>` stanza containing a result.
pub fn from_result<S: Into<String>>(id: S, payload: Option<impl IqResultPayload>) -> Iq {
Iq {
from: None,
to: None,
id: id.into(),
payload: IqType::Result(payload.map(Into::into)),
}
}
/// Creates an `<iq/>` stanza containing an error.
pub fn from_error<S: Into<String>>(id: S, payload: StanzaError) -> Iq {
Iq {
from: None,
to: None,
id: id.into(),
payload: IqType::Error(payload),
}
}
/// Sets the recipient of this stanza.
pub fn with_to(mut self, to: Jid) -> Iq {
self.to = Some(to);
self
}
/// Sets the emitter of this stanza.
pub fn with_from(mut self, from: Jid) -> Iq {
self.from = Some(from);
self
}
/// Sets the id of this stanza, in order to later match its response.
pub fn with_id(mut self, id: String) -> Iq {
self.id = id;
self
}
}
impl TryFrom<Element> for Iq {
type Error = Error;
fn try_from(root: Element) -> Result<Iq, Error> {
check_self!(root, "iq", DEFAULT_NS);
let from = get_attr!(root, "from", Option);
let to = get_attr!(root, "to", Option);
let id = get_attr!(root, "id", Required);
let type_: String = get_attr!(root, "type", Required);
let mut payload = None;
let mut error_payload = None;
for elem in root.children() {
if payload.is_some() {
return Err(Error::ParseError("Wrong number of children in iq element."));
}
if type_ == "error" {
if elem.is("error", ns::DEFAULT_NS) {
if error_payload.is_some() {
return Err(Error::ParseError("Wrong number of children in iq element."));
}
error_payload = Some(StanzaError::try_from(elem.clone())?);
} else if root.children().count() != 2 {
return Err(Error::ParseError("Wrong number of children in iq element."));
}
} else {
payload = Some(elem.clone());
}
}
let type_ = if type_ == "get" {
if let Some(payload) = payload {
IqType::Get(payload)
} else {
return Err(Error::ParseError("Wrong number of children in iq element."));
}
} else if type_ == "set" {
if let Some(payload) = payload {
IqType::Set(payload)
} else {
return Err(Error::ParseError("Wrong number of children in iq element."));
}
} else if type_ == "result" {
if let Some(payload) = payload {
IqType::Result(Some(payload))
} else {
IqType::Result(None)
}
} else if type_ == "error" {
if let Some(payload) = error_payload {
IqType::Error(payload)
} else {
return Err(Error::ParseError("Wrong number of children in iq element."));
}
} else {
return Err(Error::ParseError("Unknown iq type."));
};
Ok(Iq {
from,
to,
id,
payload: type_,
})
}
}
impl From<Iq> for Element {
fn from(iq: Iq) -> Element {
let mut stanza = Element::builder("iq")
.ns(ns::DEFAULT_NS)
.attr("from", iq.from)
.attr("to", iq.to)
.attr("id", iq.id)
.attr("type", &iq.payload)
.build();
let elem = match iq.payload {
IqType::Get(elem) | IqType::Set(elem) | IqType::Result(Some(elem)) => elem,
IqType::Error(error) => error.into(),
IqType::Result(None) => return stanza,
};
stanza.append_child(elem);
stanza
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::util::compare_elements::NamespaceAwareCompare;
use crate::disco::DiscoInfoQuery;
use crate::stanza_error::{DefinedCondition, ErrorType};
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(IqType, 112);
assert_size!(Iq, 204);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(IqType, 224);
assert_size!(Iq, 408);
}
#[test]
fn test_require_type() {
#[cfg(not(feature = "component"))]
let elem: Element = "<iq xmlns='jabber:client'/>".parse().unwrap();
#[cfg(feature = "component")]
let elem: Element = "<iq xmlns='jabber:component:accept'/>".parse().unwrap();
let error = Iq::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Required attribute 'id' missing.");
#[cfg(not(feature = "component"))]
let elem: Element = "<iq xmlns='jabber:client' id='coucou'/>".parse().unwrap();
#[cfg(feature = "component")]
let elem: Element = "<iq xmlns='jabber:component:accept' id='coucou'/>".parse().unwrap();
let error = Iq::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Required attribute 'type' missing.");
}
#[test]
fn test_get() {
#[cfg(not(feature = "component"))]
let elem: Element = "<iq xmlns='jabber:client' type='get' id='foo'>
<foo xmlns='bar'/>
</iq>"
.parse()
.unwrap();
#[cfg(feature = "component")]
let elem: Element = "<iq xmlns='jabber:component:accept' type='get' id='foo'>
<foo xmlns='bar'/>
</iq>"
.parse()
.unwrap();
let iq = Iq::try_from(elem).unwrap();
let query: Element = "<foo xmlns='bar'/>".parse().unwrap();
assert_eq!(iq.from, None);
assert_eq!(iq.to, None);
assert_eq!(&iq.id, "foo");
assert!(match iq.payload {
IqType::Get(element) => element.compare_to(&query),
_ => false,
});
}
#[test]
fn test_set() {
#[cfg(not(feature = "component"))]
let elem: Element = "<iq xmlns='jabber:client' type='set' id='vcard'>
<vCard xmlns='vcard-temp'/>
</iq>"
.parse()
.unwrap();
#[cfg(feature = "component")]
let elem: Element = "<iq xmlns='jabber:component:accept' type='set' id='vcard'>
<vCard xmlns='vcard-temp'/>
</iq>"
.parse()
.unwrap();
let iq = Iq::try_from(elem).unwrap();
let vcard: Element = "<vCard xmlns='vcard-temp'/>".parse().unwrap();
assert_eq!(iq.from, None);
assert_eq!(iq.to, None);
assert_eq!(&iq.id, "vcard");
assert!(match iq.payload {
IqType::Set(element) => element.compare_to(&vcard),
_ => false,
});
}
#[test]
fn test_result_empty() {
#[cfg(not(feature = "component"))]
let elem: Element = "<iq xmlns='jabber:client' type='result' id='res'/>".parse().unwrap();
#[cfg(feature = "component")]
let elem: Element = "<iq xmlns='jabber:component:accept' type='result' id='res'/>"
.parse()
.unwrap();
let iq = Iq::try_from(elem).unwrap();
assert_eq!(iq.from, None);
assert_eq!(iq.to, None);
assert_eq!(&iq.id, "res");
assert!(match iq.payload {
IqType::Result(None) => true,
_ => false,
});
}
#[test]
fn test_result() {
#[cfg(not(feature = "component"))]
let elem: Element = "<iq xmlns='jabber:client' type='result' id='res'>
<query xmlns='http://jabber.org/protocol/disco#items'/>
</iq>"
.parse()
.unwrap();
#[cfg(feature = "component")]
let elem: Element = "<iq xmlns='jabber:component:accept' type='result' id='res'>
<query xmlns='http://jabber.org/protocol/disco#items'/>
</iq>"
.parse()
.unwrap();
let iq = Iq::try_from(elem).unwrap();
let query: Element = "<query xmlns='http://jabber.org/protocol/disco#items'/>"
.parse()
.unwrap();
assert_eq!(iq.from, None);
assert_eq!(iq.to, None);
assert_eq!(&iq.id, "res");
assert!(match iq.payload {
IqType::Result(Some(element)) => element.compare_to(&query),
_ => false,
});
}
#[test]
fn test_error() {
#[cfg(not(feature = "component"))]
let elem: Element = "<iq xmlns='jabber:client' type='error' id='err1'>
<ping xmlns='urn:xmpp:ping'/>
<error type='cancel'>
<service-unavailable xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
</error>
</iq>"
.parse()
.unwrap();
#[cfg(feature = "component")]
let elem: Element = "<iq xmlns='jabber:component:accept' type='error' id='err1'>
<ping xmlns='urn:xmpp:ping'/>
<error type='cancel'>
<service-unavailable xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
</error>
</iq>"
.parse()
.unwrap();
let iq = Iq::try_from(elem).unwrap();
assert_eq!(iq.from, None);
assert_eq!(iq.to, None);
assert_eq!(iq.id, "err1");
match iq.payload {
IqType::Error(error) => {
assert_eq!(error.type_, ErrorType::Cancel);
assert_eq!(error.by, None);
assert_eq!(
error.defined_condition,
DefinedCondition::ServiceUnavailable
);
assert_eq!(error.texts.len(), 0);
assert_eq!(error.other, None);
}
_ => panic!(),
}
}
#[test]
fn test_children_invalid() {
#[cfg(not(feature = "component"))]
let elem: Element = "<iq xmlns='jabber:client' type='error' id='error'/>"
.parse()
.unwrap();
#[cfg(feature = "component")]
let elem: Element = "<iq xmlns='jabber:component:accept' type='error' id='error'/>"
.parse()
.unwrap();
let error = Iq::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Wrong number of children in iq element.");
}
#[test]
fn test_serialise() {
#[cfg(not(feature = "component"))]
let elem: Element = "<iq xmlns='jabber:client' type='result' id='res'/>".parse().unwrap();
#[cfg(feature = "component")]
let elem: Element = "<iq xmlns='jabber:component:accept' type='result' id='res'/>"
.parse()
.unwrap();
let iq2 = Iq {
from: None,
to: None,
id: String::from("res"),
payload: IqType::Result(None),
};
let elem2 = iq2.into();
assert_eq!(elem, elem2);
}
#[test]
fn test_disco() {
#[cfg(not(feature = "component"))]
let elem: Element = "<iq xmlns='jabber:client' type='get' id='disco'><query xmlns='http://jabber.org/protocol/disco#info'/></iq>".parse().unwrap();
#[cfg(feature = "component")]
let elem: Element = "<iq xmlns='jabber:component:accept' type='get' id='disco'><query xmlns='http://jabber.org/protocol/disco#info'/></iq>".parse().unwrap();
let iq = Iq::try_from(elem).unwrap();
let disco_info = match iq.payload {
IqType::Get(payload) => DiscoInfoQuery::try_from(payload).unwrap(),
_ => panic!(),
};
assert!(disco_info.node.is_none());
}
}

View file

@ -0,0 +1,73 @@
// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 crate::iq::{IqGetPayload, IqResultPayload};
use crate::util::helpers::{Text, JidCodec};
use jid::Jid;
generate_element!(
/// Request from a client to stringprep/PRECIS a string into a JID.
JidPrepQuery, "jid", JID_PREP,
text: (
/// The potential JID.
data: Text<String>
)
);
impl IqGetPayload for JidPrepQuery {}
impl JidPrepQuery {
/// Create a new JID Prep query.
pub fn new<J: Into<String>>(jid: J) -> JidPrepQuery {
JidPrepQuery {
data: jid.into(),
}
}
}
generate_element!(
/// Response from the server with the stringprepd/PRECISd JID.
JidPrepResponse, "jid", JID_PREP,
text: (
/// The JID.
jid: JidCodec<Jid>
)
);
impl IqResultPayload for JidPrepResponse {}
#[cfg(test)]
mod tests {
use super::*;
use crate::Element;
use std::convert::TryFrom;
use std::str::FromStr;
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(JidPrepQuery, 12);
assert_size!(JidPrepResponse, 40);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(JidPrepQuery, 24);
assert_size!(JidPrepResponse, 80);
}
#[test]
fn simple() {
let elem: Element = "<jid xmlns='urn:xmpp:jidprep:0'>ROMeo@montague.lit/orchard</jid>".parse().unwrap();
let query = JidPrepQuery::try_from(elem).unwrap();
assert_eq!(query.data, "ROMeo@montague.lit/orchard");
let elem: Element = "<jid xmlns='urn:xmpp:jidprep:0'>romeo@montague.lit/orchard</jid>".parse().unwrap();
let response = JidPrepResponse::try_from(elem).unwrap();
assert_eq!(response.jid, Jid::from_str("romeo@montague.lit/orchard").unwrap());
}
}

867
xmpp-parsers/src/jingle.rs Normal file
View file

@ -0,0 +1,867 @@
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 crate::util::error::Error;
use crate::iq::IqSetPayload;
use crate::jingle_rtp::Description as RtpDescription;
use crate::jingle_ice_udp::Transport as IceUdpTransport;
use crate::jingle_ibb::Transport as IbbTransport;
use crate::jingle_s5b::Transport as Socks5Transport;
use crate::ns;
use jid::Jid;
use crate::Element;
use std::collections::BTreeMap;
use std::str::FromStr;
use std::convert::TryFrom;
generate_attribute!(
/// The action attribute.
Action, "action", {
/// Accept a content-add action received from another party.
ContentAccept => "content-accept",
/// Add one or more new content definitions to the session.
ContentAdd => "content-add",
/// Change the directionality of media sending.
ContentModify => "content-modify",
/// Reject a content-add action received from another party.
ContentReject => "content-reject",
/// Remove one or more content definitions from the session.
ContentRemove => "content-remove",
/// Exchange information about parameters for an application type.
DescriptionInfo => "description-info",
/// Exchange information about security preconditions.
SecurityInfo => "security-info",
/// Definitively accept a session negotiation.
SessionAccept => "session-accept",
/// Send session-level information, such as a ping or a ringing message.
SessionInfo => "session-info",
/// Request negotiation of a new Jingle session.
SessionInitiate => "session-initiate",
/// End an existing session.
SessionTerminate => "session-terminate",
/// Accept a transport-replace action received from another party.
TransportAccept => "transport-accept",
/// Exchange transport candidates.
TransportInfo => "transport-info",
/// Reject a transport-replace action received from another party.
TransportReject => "transport-reject",
/// Redefine a transport method or replace it with a different method.
TransportReplace => "transport-replace",
}
);
generate_attribute!(
/// Which party originally generated the content type.
Creator, "creator", {
/// This content was created by the initiator of this session.
Initiator => "initiator",
/// This content was created by the responder of this session.
Responder => "responder",
}
);
generate_attribute!(
/// Which parties in the session will be generating content.
Senders, "senders", {
/// Both parties can send for this content.
Both => "both",
/// Only the initiator can send for this content.
Initiator => "initiator",
/// No one can send for this content.
None => "none",
/// Only the responder can send for this content.
Responder => "responder",
}, Default = Both
);
generate_attribute!(
/// How the content definition is to be interpreted by the recipient. The
/// meaning of this attribute matches the "Content-Disposition" header as
/// defined in RFC 2183 and applied to SIP by RFC 3261.
///
/// Possible values are defined here:
/// https://www.iana.org/assignments/cont-disp/cont-disp.xhtml
Disposition, "disposition", {
/// Displayed automatically.
Inline => "inline",
/// User controlled display.
Attachment => "attachment",
/// Process as form response.
FormData => "form-data",
/// Tunneled content to be processed silently.
Signal => "signal",
/// The body is a custom ring tone to alert the user.
Alert => "alert",
/// The body is displayed as an icon to the user.
Icon => "icon",
/// The body should be displayed to the user.
Render => "render",
/// The body contains a list of URIs that indicates the recipients of
/// the request.
RecipientListHistory => "recipient-list-history",
/// The body describes a communications session, for example, an
/// RFC2327 SDP body.
Session => "session",
/// Authenticated Identity Body.
Aib => "aib",
/// The body describes an early communications session, for example,
/// and [RFC2327] SDP body.
EarlySession => "early-session",
/// The body includes a list of URIs to which URI-list services are to
/// be applied.
RecipientList => "recipient-list",
/// The payload of the message carrying this Content-Disposition header
/// field value is an Instant Message Disposition Notification as
/// requested in the corresponding Instant Message.
Notification => "notification",
/// The body needs to be handled according to a reference to the body
/// that is located in the same SIP message as the body.
ByReference => "by-reference",
/// The body contains information associated with an Info Package.
InfoPackage => "info-package",
/// The body describes either metadata about the RS or the reason for
/// the metadata snapshot request as determined by the MIME value
/// indicated in the Content-Type.
RecordingSession => "recording-session",
}, Default = Session
);
generate_id!(
/// An unique identifier in a session, referencing a
/// [struct.Content.html](Content element).
ContentId
);
/// Enum wrapping all of the various supported descriptions of a Content.
#[derive(Debug, Clone)]
pub enum Description {
/// Jingle RTP Sessions (XEP-0167) description.
Rtp(RtpDescription),
/// To be used for any description that isnt known at compile-time.
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 {
Description::Unknown(elem)
})
}
}
impl From<RtpDescription> for Description {
fn from(desc: RtpDescription) -> Description {
Description::Rtp(desc)
}
}
impl From<Description> for Element {
fn from(desc: Description) -> Element {
match desc {
Description::Rtp(desc) => desc.into(),
Description::Unknown(elem) => elem,
}
}
}
impl Description {
fn get_ns(&self) -> String {
match self {
Description::Rtp(_) => String::from(ns::JINGLE_RTP),
Description::Unknown(elem) => elem.ns().unwrap_or_else(|| String::new()),
}
}
}
/// Enum wrapping all of the various supported transports of a Content.
#[derive(Debug, Clone)]
pub enum Transport {
/// Jingle ICE-UDP Bytestreams (XEP-0176) transport.
IceUdp(IceUdpTransport),
/// Jingle In-Band Bytestreams (XEP-0261) transport.
Ibb(IbbTransport),
/// Jingle SOCKS5 Bytestreams (XEP-0260) transport.
Socks5(Socks5Transport),
/// To be used for any transport that isnt known at compile-time.
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 {
Transport::Unknown(elem)
})
}
}
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)
}
}
impl From<Transport> for Element {
fn from(transport: Transport) -> Element {
match transport {
Transport::IceUdp(transport) => transport.into(),
Transport::Ibb(transport) => transport.into(),
Transport::Socks5(transport) => transport.into(),
Transport::Unknown(elem) => elem,
}
}
}
impl Transport {
fn get_ns(&self) -> String {
match self {
Transport::IceUdp(_) => String::from(ns::JINGLE_ICE_UDP),
Transport::Ibb(_) => String::from(ns::JINGLE_IBB),
Transport::Socks5(_) => String::from(ns::JINGLE_S5B),
Transport::Unknown(elem) => elem.ns().unwrap_or_else(|| String::new()),
}
}
}
generate_element!(
/// Describes a sessions content, there can be multiple content in one
/// session.
Content, "content", JINGLE,
attributes: [
/// Who created this content.
creator: Required<Creator> = "creator",
/// How the content definition is to be interpreted by the recipient.
disposition: Default<Disposition> = "disposition",
/// A per-session unique identifier for this content.
name: Required<ContentId> = "name",
/// Who can send data for this content.
senders: Default<Senders> = "senders",
],
children: [
/// What to send.
description: Option<Description> = ("description", *) => Description,
/// How to send it.
transport: Option<Transport> = ("transport", *) => Transport,
/// With which security.
security: Option<Element> = ("security", JINGLE) => Element
]
);
impl Content {
/// Create a new content.
pub fn new(creator: Creator, name: ContentId) -> Content {
Content {
creator,
name,
disposition: Disposition::Session,
senders: Senders::Both,
description: None,
transport: None,
security: None,
}
}
/// Set how the content is to be interpreted by the recipient.
pub fn with_disposition(mut self, disposition: Disposition) -> Content {
self.disposition = disposition;
self
}
/// Specify who can send data for this content.
pub fn with_senders(mut self, senders: Senders) -> Content {
self.senders = senders;
self
}
/// Set the description of this content.
pub fn with_description<D: Into<Description>>(mut self, description: D) -> Content {
self.description = Some(description.into());
self
}
/// Set the transport of this content.
pub fn with_transport<T: Into<Transport>>(mut self, transport: T) -> Content {
self.transport = Some(transport.into());
self
}
/// Set the security of this content.
pub fn with_security(mut self, security: Element) -> Content {
self.security = Some(security);
self
}
}
/// Lists the possible reasons to be included in a Jingle iq.
#[derive(Debug, Clone, PartialEq)]
pub enum Reason {
/// The party prefers to use an existing session with the peer rather than
/// initiate a new session; the Jingle session ID of the alternative
/// session SHOULD be provided as the XML character data of the <sid/>
/// child.
AlternativeSession, //(String),
/// The party is busy and cannot accept a session.
Busy,
/// The initiator wishes to formally cancel the session initiation request.
Cancel,
/// The action is related to connectivity problems.
ConnectivityError,
/// The party wishes to formally decline the session.
Decline,
/// The session length has exceeded a pre-defined time limit (e.g., a
/// meeting hosted at a conference service).
Expired,
/// The party has been unable to initialize processing related to the
/// application type.
FailedApplication,
/// The party has been unable to establish connectivity for the transport
/// method.
FailedTransport,
/// The action is related to a non-specific application error.
GeneralError,
/// The entity is going offline or is no longer available.
Gone,
/// The party supports the offered application type but does not support
/// the offered or negotiated parameters.
IncompatibleParameters,
/// The action is related to media processing problems.
MediaError,
/// The action is related to a violation of local security policies.
SecurityError,
/// The action is generated during the normal course of state management
/// and does not reflect any error.
Success,
/// A request has not been answered so the sender is timing out the
/// request.
Timeout,
/// The party supports none of the offered application types.
UnsupportedApplications,
/// The party supports none of the offered transport methods.
UnsupportedTransports,
}
impl FromStr for Reason {
type Err = Error;
fn from_str(s: &str) -> Result<Reason, Error> {
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 From<Reason> for Element {
fn from(reason: Reason) -> Element {
Element::builder(match reason {
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",
})
.ns(ns::JINGLE)
.build()
}
}
type Lang = String;
/// Informs the recipient of something.
#[derive(Debug, Clone)]
pub struct ReasonElement {
/// The list of possible reasons to be included in a Jingle iq.
pub reason: Reason,
/// A human-readable description of this reason.
pub texts: BTreeMap<Lang, String>,
}
impl TryFrom<Element> for ReasonElement {
type Error = Error;
fn try_from(elem: Element) -> Result<ReasonElement, Error> {
check_self!(elem, "reason", JINGLE);
check_no_attributes!(elem, "reason");
let mut reason = None;
let mut texts = BTreeMap::new();
for child in elem.children() {
if child.is("text", ns::JINGLE) {
check_no_children!(child, "text");
check_no_unknown_attributes!(child, "text", ["xml:lang"]);
let lang = get_attr!(elem, "xml:lang", Default);
if texts.insert(lang, child.text()).is_some() {
return Err(Error::ParseError(
"Text element present twice for the same xml:lang.",
));
}
} else if child.has_ns(ns::JINGLE) {
if reason.is_some() {
return Err(Error::ParseError(
"Reason must not have more than one reason.",
));
}
check_no_children!(child, "reason");
check_no_attributes!(child, "reason");
reason = Some(child.name().parse()?);
} else {
return Err(Error::ParseError("Reason contains a foreign element."));
}
}
let reason = reason.ok_or(Error::ParseError(
"Reason doesnt contain a valid reason.",
))?;
Ok(ReasonElement {
reason,
texts,
})
}
}
impl From<ReasonElement> for Element {
fn from(reason: ReasonElement) -> Element {
Element::builder("reason")
.ns(ns::JINGLE)
.append(Element::from(reason.reason))
.append_all(
reason.texts.into_iter().map(|(lang, text)| {
Element::builder("text")
.ns(ns::JINGLE)
.attr("xml:lang", lang)
.append(text)
}))
.build()
}
}
generate_id!(
/// Unique identifier for a session between two JIDs.
SessionId
);
/// The main Jingle container, to be included in an iq stanza.
#[derive(Debug, Clone)]
pub struct Jingle {
/// The action to execute on both ends.
pub action: Action,
/// Who the initiator is.
pub initiator: Option<Jid>,
/// Who the responder is.
pub responder: Option<Jid>,
/// Unique session identifier between two entities.
pub sid: SessionId,
/// A list of contents to be negotiated in this session.
pub contents: Vec<Content>,
/// An optional reason.
pub reason: Option<ReasonElement>,
/// Payloads to be included.
pub other: Vec<Element>,
}
impl IqSetPayload for Jingle {}
impl Jingle {
/// Create a new Jingle element.
pub fn new(action: Action, sid: SessionId) -> Jingle {
Jingle {
action,
sid,
initiator: None,
responder: None,
contents: Vec::new(),
reason: None,
other: Vec::new(),
}
}
/// Set the initiators JID.
pub fn with_initiator(mut self, initiator: Jid) -> Jingle {
self.initiator = Some(initiator);
self
}
/// Set the responders JID.
pub fn with_responder(mut self, responder: Jid) -> Jingle {
self.responder = Some(responder);
self
}
/// Add a content to this Jingle container.
pub fn add_content(mut self, content: Content) -> Jingle {
self.contents.push(content);
self
}
/// Set the reason in this Jingle container.
pub fn set_reason(mut self, content: Content) -> Jingle {
self.contents.push(content);
self
}
}
impl TryFrom<Element> for Jingle {
type Error = Error;
fn try_from(root: Element) -> Result<Jingle, Error> {
check_self!(root, "jingle", JINGLE, "Jingle");
check_no_unknown_attributes!(root, "Jingle", ["action", "initiator", "responder", "sid"]);
let mut jingle = Jingle {
action: get_attr!(root, "action", Required),
initiator: get_attr!(root, "initiator", Option),
responder: get_attr!(root, "responder", Option),
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 From<Jingle> for Element {
fn from(jingle: Jingle) -> Element {
Element::builder("jingle")
.ns(ns::JINGLE)
.attr("action", jingle.action)
.attr("initiator", jingle.initiator)
.attr("responder", jingle.responder)
.attr("sid", jingle.sid)
.append_all(jingle.contents)
.append_all(jingle.reason.map(Element::from))
.build()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[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, 172);
assert_size!(Reason, 1);
assert_size!(ReasonElement, 16);
assert_size!(SessionId, 12);
assert_size!(Jingle, 136);
}
#[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, 384);
assert_size!(Reason, 1);
assert_size!(ReasonElement, 32);
assert_size!(SessionId, 24);
assert_size!(Jingle, 272);
}
#[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 {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Required attribute 'action' 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 {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Required attribute 'sid' 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 {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "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 {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Required attribute 'creator' 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 {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Required attribute 'name' 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 {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "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 {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "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 {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "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_invalid_reason() {
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 {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Reason doesnt 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::try_from(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::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 = "<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 {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Jingle must not have more than one reason.");
let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><decline/><text/><text/></reason></jingle>".parse().unwrap();
let error = Jingle::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Text element present twice for the same xml:lang.");
}
}

View file

@ -0,0 +1,98 @@
// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 crate::util::helpers::ColonSeparatedHex;
use crate::util::error::Error;
use crate::hashes::{Hash, Algo};
generate_attribute!(
/// Indicates which of the end points should initiate the TCP connection establishment.
Setup, "setup", {
/// The endpoint will initiate an outgoing connection.
Active => "active",
/// The endpoint will accept an incoming connection.
Passive => "passive",
/// The endpoint is willing to accept an incoming connection or to initiate an outgoing
/// connection.
Actpass => "actpass",
/*
/// The endpoint does not want the connection to be established for the time being.
///
/// Note that this value isnt used, as per the XEP.
Holdconn => "holdconn",
*/
}
);
// TODO: use a hashes::Hash instead of two different fields here.
generate_element!(
/// Fingerprint of the key used for a DTLS handshake.
Fingerprint, "fingerprint", JINGLE_DTLS,
attributes: [
/// The hash algorithm used for this fingerprint.
hash: Required<Algo> = "hash",
/// Indicates which of the end points should initiate the TCP connection establishment.
setup: Required<Setup> = "setup"
],
text: (
/// Hash value of this fingerprint.
value: ColonSeparatedHex<Vec<u8>>
)
);
impl Fingerprint {
/// Create a new Fingerprint from a Setup and a Hash.
pub fn from_hash(setup: Setup, hash: Hash) -> Fingerprint {
Fingerprint {
hash: hash.algo,
setup,
value: hash.hash,
}
}
/// Create a new Fingerprint from a Setup and parsing the hash.
pub fn from_colon_separated_hex(setup: Setup, algo: &str, hash: &str) -> Result<Fingerprint, Error> {
let algo = algo.parse()?;
let hash = Hash::from_colon_separated_hex(algo, hash)?;
Ok(Fingerprint::from_hash(setup, hash))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Element;
use std::convert::TryFrom;
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(Setup, 1);
assert_size!(Fingerprint, 32);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(Setup, 1);
assert_size!(Fingerprint, 64);
}
#[test]
fn test_ex1() {
let elem: Element = "<fingerprint xmlns='urn:xmpp:jingle:apps:dtls:0' hash='sha-256' setup='actpass'>02:1A:CC:54:27:AB:EB:9C:53:3F:3E:4B:65:2E:7D:46:3F:54:42:CD:54:F1:7A:03:A2:7D:F9:B0:7F:46:19:B2</fingerprint>"
.parse()
.unwrap();
let fingerprint = Fingerprint::try_from(elem).unwrap();
assert_eq!(fingerprint.setup, Setup::Actpass);
assert_eq!(fingerprint.hash, Algo::Sha_256);
assert_eq!(fingerprint.value, [2, 26, 204, 84, 39, 171, 235, 156, 83, 63, 62, 75, 101, 46, 125, 70, 63, 84, 66, 205, 84, 241, 122, 3, 162, 125, 249, 176, 127, 70, 25, 178]);
}
}

View file

@ -0,0 +1,616 @@
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 crate::date::DateTime;
use crate::util::error::Error;
use crate::hashes::Hash;
use crate::jingle::{ContentId, Creator};
use crate::ns;
use minidom::{Element, Node};
use std::collections::BTreeMap;
use std::str::FromStr;
use std::convert::TryFrom;
generate_element!(
/// Represents a range in a file.
#[derive(PartialEq, Default)]
Range, "range", JINGLE_FT,
attributes: [
/// The offset in bytes from the beginning of the file.
offset: Default<u64> = "offset",
/// The length in bytes of the range, or None to be the entire
/// remaining of the file.
length: Option<u64> = "length"
],
children: [
/// List of hashes for this range.
hashes: Vec<Hash> = ("hash", HASHES) => Hash
]
);
impl Range {
/// Creates a new range.
pub fn new() -> Range {
Default::default()
}
}
type Lang = String;
generate_id!(
/// Wrapper for a file description.
Desc
);
/// Represents a file to be transferred.
#[derive(Debug, Clone, Default)]
pub struct File {
/// The date of last modification of this file.
pub date: Option<DateTime>,
/// The MIME type of this file.
pub media_type: Option<String>,
/// The name of this file.
pub name: Option<String>,
/// The description of this file, possibly localised.
pub descs: BTreeMap<Lang, Desc>,
/// The size of this file, in bytes.
pub size: Option<u64>,
/// Used to request only a part of this file.
pub range: Option<Range>,
/// A list of hashes matching this entire file.
pub hashes: Vec<Hash>,
}
impl File {
/// Creates a new file descriptor.
pub fn new() -> File {
File::default()
}
/// Sets the date of last modification on this file.
pub fn with_date(mut self, date: DateTime) -> File {
self.date = Some(date);
self
}
/// Sets the date of last modification on this file from an ISO-8601
/// string.
pub fn with_date_str(mut self, date: &str) -> Result<File, Error> {
self.date = Some(DateTime::from_str(date)?);
Ok(self)
}
/// Sets the MIME type of this file.
pub fn with_media_type(mut self, media_type: String) -> File {
self.media_type = Some(media_type);
self
}
/// Sets the name of this file.
pub fn with_name(mut self, name: String) -> File {
self.name = Some(name);
self
}
/// Sets a description for this file.
pub fn add_desc(mut self, lang: &str, desc: Desc) -> File {
self.descs.insert(Lang::from(lang), desc);
self
}
/// Sets the file size of this file, in bytes.
pub fn with_size(mut self, size: u64) -> File {
self.size = Some(size);
self
}
/// Request only a range of this file.
pub fn with_range(mut self, range: Range) -> File {
self.range = Some(range);
self
}
/// Add a hash on this file.
pub fn add_hash(mut self, hash: Hash) -> File {
self.hashes.push(hash);
self
}
}
impl TryFrom<Element> for File {
type Error = Error;
fn try_from(elem: Element) -> Result<File, Error> {
check_self!(elem, "file", JINGLE_FT);
check_no_attributes!(elem, "file");
let mut file = File {
date: None,
media_type: None,
name: None,
descs: BTreeMap::new(),
size: None,
range: None,
hashes: vec![],
};
for child in elem.children() {
if child.is("date", ns::JINGLE_FT) {
if file.date.is_some() {
return Err(Error::ParseError("File must not have more than one date."));
}
file.date = Some(child.text().parse()?);
} else if child.is("media-type", ns::JINGLE_FT) {
if file.media_type.is_some() {
return Err(Error::ParseError(
"File must not have more than one media-type.",
));
}
file.media_type = Some(child.text());
} else if child.is("name", ns::JINGLE_FT) {
if file.name.is_some() {
return Err(Error::ParseError("File must not have more than one name."));
}
file.name = Some(child.text());
} else if child.is("desc", ns::JINGLE_FT) {
let lang = get_attr!(child, "xml:lang", Default);
let desc = Desc(child.text());
if file.descs.insert(lang, desc).is_some() {
return Err(Error::ParseError(
"Desc element present twice for the same xml:lang.",
));
}
} else if child.is("size", ns::JINGLE_FT) {
if file.size.is_some() {
return Err(Error::ParseError("File must not have more than one size."));
}
file.size = Some(child.text().parse()?);
} else if child.is("range", ns::JINGLE_FT) {
if file.range.is_some() {
return Err(Error::ParseError("File must not have more than one range."));
}
file.range = Some(Range::try_from(child.clone())?);
} else if child.is("hash", ns::HASHES) {
file.hashes.push(Hash::try_from(child.clone())?);
} else {
return Err(Error::ParseError("Unknown element in JingleFT file."));
}
}
Ok(file)
}
}
impl From<File> for Element {
fn from(file: File) -> Element {
Element::builder("file")
.ns(ns::JINGLE_FT)
.append_all(file.date.map(|date|
Element::builder("date")
.append(date)))
.append_all(file.media_type.map(|media_type|
Element::builder("media-type")
.append(media_type)))
.append_all(file.name.map(|name|
Element::builder("name")
.append(name)))
.append_all(file.descs.into_iter().map(|(lang, desc)|
Element::builder("desc")
.attr("xml:lang", lang)
.append(desc.0)))
.append_all(file.size.map(|size|
Element::builder("size")
.append(format!("{}", size))))
.append_all(file.range)
.append_all(file.hashes)
.build()
}
}
/// A wrapper element for a file.
#[derive(Debug, Clone)]
pub struct Description {
/// The actual file descriptor.
pub file: File,
}
impl TryFrom<Element> for Description {
type Error = Error;
fn try_from(elem: Element) -> Result<Description, Error> {
check_self!(elem, "description", JINGLE_FT, "JingleFT description");
check_no_attributes!(elem, "JingleFT description");
let mut file = None;
for child in elem.children() {
if file.is_some() {
return Err(Error::ParseError(
"JingleFT description element must have exactly one child.",
));
}
file = Some(File::try_from(child.clone())?);
}
if file.is_none() {
return Err(Error::ParseError(
"JingleFT description element must have exactly one child.",
));
}
Ok(Description {
file: file.unwrap(),
})
}
}
impl From<Description> for Element {
fn from(description: Description) -> Element {
Element::builder("description")
.ns(ns::JINGLE_FT)
.append(Node::Element(description.file.into()))
.build()
}
}
/// A checksum for checking that the file has been transferred correctly.
#[derive(Debug, Clone)]
pub struct Checksum {
/// The identifier of the file transfer content.
pub name: ContentId,
/// The creator of this file transfer.
pub creator: Creator,
/// The file being checksummed.
pub file: File,
}
impl TryFrom<Element> for Checksum {
type Error = Error;
fn try_from(elem: Element) -> Result<Checksum, Error> {
check_self!(elem, "checksum", JINGLE_FT);
check_no_unknown_attributes!(elem, "checksum", ["name", "creator"]);
let mut file = None;
for child in elem.children() {
if file.is_some() {
return Err(Error::ParseError(
"JingleFT checksum element must have exactly one child.",
));
}
file = Some(File::try_from(child.clone())?);
}
if file.is_none() {
return Err(Error::ParseError(
"JingleFT checksum element must have exactly one child.",
));
}
Ok(Checksum {
name: get_attr!(elem, "name", Required),
creator: get_attr!(elem, "creator", Required),
file: file.unwrap(),
})
}
}
impl From<Checksum> for Element {
fn from(checksum: Checksum) -> Element {
Element::builder("checksum")
.ns(ns::JINGLE_FT)
.attr("name", checksum.name)
.attr("creator", checksum.creator)
.append(Node::Element(checksum.file.into()))
.build()
}
}
generate_element!(
/// A notice that the file transfer has been completed.
Received, "received", JINGLE_FT,
attributes: [
/// The content identifier of this Jingle session.
name: Required<ContentId> = "name",
/// The creator of this file transfer.
creator: Required<Creator> = "creator",
]
);
#[cfg(test)]
mod tests {
use super::*;
use crate::hashes::Algo;
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(Range, 40);
assert_size!(File, 128);
assert_size!(Description, 128);
assert_size!(Checksum, 144);
assert_size!(Received, 16);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(Range, 48);
assert_size!(File, 184);
assert_size!(Description, 184);
assert_size!(Checksum, 216);
assert_size!(Received, 32);
}
#[test]
fn test_description() {
let elem: Element = r#"
<description xmlns='urn:xmpp:jingle:apps:file-transfer:5'>
<file>
<media-type>text/plain</media-type>
<name>test.txt</name>
<date>2015-07-26T21:46:00+01:00</date>
<size>6144</size>
<hash xmlns='urn:xmpp:hashes:2'
algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash>
</file>
</description>
"#
.parse()
.unwrap();
let desc = Description::try_from(elem).unwrap();
assert_eq!(desc.file.media_type, Some(String::from("text/plain")));
assert_eq!(desc.file.name, Some(String::from("test.txt")));
assert_eq!(desc.file.descs, BTreeMap::new());
assert_eq!(
desc.file.date,
DateTime::from_str("2015-07-26T21:46:00+01:00").ok()
);
assert_eq!(desc.file.size, Some(6144u64));
assert_eq!(desc.file.range, None);
assert_eq!(desc.file.hashes[0].algo, Algo::Sha_1);
assert_eq!(
desc.file.hashes[0].hash,
base64::decode("w0mcJylzCn+AfvuGdqkty2+KP48=").unwrap()
);
}
#[test]
fn test_request() {
let elem: Element = r#"
<description xmlns='urn:xmpp:jingle:apps:file-transfer:5'>
<file>
<hash xmlns='urn:xmpp:hashes:2'
algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash>
</file>
</description>
"#
.parse()
.unwrap();
let desc = Description::try_from(elem).unwrap();
assert_eq!(desc.file.media_type, None);
assert_eq!(desc.file.name, None);
assert_eq!(desc.file.descs, BTreeMap::new());
assert_eq!(desc.file.date, None);
assert_eq!(desc.file.size, None);
assert_eq!(desc.file.range, None);
assert_eq!(desc.file.hashes[0].algo, Algo::Sha_1);
assert_eq!(
desc.file.hashes[0].hash,
base64::decode("w0mcJylzCn+AfvuGdqkty2+KP48=").unwrap()
);
}
#[test]
fn test_descs() {
let elem: Element = r#"
<description xmlns='urn:xmpp:jingle:apps:file-transfer:5'>
<file>
<media-type>text/plain</media-type>
<desc xml:lang='fr'>Fichier secret!</desc>
<desc xml:lang='en'>Secret file!</desc>
<hash xmlns='urn:xmpp:hashes:2'
algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash>
</file>
</description>
"#
.parse()
.unwrap();
let desc = Description::try_from(elem).unwrap();
assert_eq!(
desc.file.descs.keys().cloned().collect::<Vec<_>>(),
["en", "fr"]
);
assert_eq!(desc.file.descs["en"], Desc(String::from("Secret file!")));
assert_eq!(
desc.file.descs["fr"],
Desc(String::from("Fichier secret!"))
);
let elem: Element = r#"
<description xmlns='urn:xmpp:jingle:apps:file-transfer:5'>
<file>
<media-type>text/plain</media-type>
<desc xml:lang='fr'>Fichier secret!</desc>
<desc xml:lang='fr'>Secret file!</desc>
<hash xmlns='urn:xmpp:hashes:2'
algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash>
</file>
</description>
"#
.parse()
.unwrap();
let error = Description::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Desc element present twice for the same xml:lang.");
}
#[test]
fn test_received() {
let elem: Element = "<received xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='initiator'/>".parse().unwrap();
let received = Received::try_from(elem).unwrap();
assert_eq!(received.name, ContentId(String::from("coucou")));
assert_eq!(received.creator, Creator::Initiator);
let elem2 = Element::from(received.clone());
let received2 = Received::try_from(elem2).unwrap();
assert_eq!(received2.name, ContentId(String::from("coucou")));
assert_eq!(received2.creator, Creator::Initiator);
let elem: Element = "<received xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='initiator'><coucou/></received>".parse().unwrap();
let error = Received::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown child in received element.");
let elem: Element =
"<received xmlns='urn:xmpp:jingle:apps:file-transfer:5' creator='initiator'/>"
.parse()
.unwrap();
let error = Received::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Required attribute 'name' missing.");
let elem: Element = "<received xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='coucou'/>".parse().unwrap();
let error = Received::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown value for 'creator' attribute.");
}
#[cfg(not(feature = "disable-validation"))]
#[test]
fn test_invalid_received() {
let elem: Element = "<received xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='initiator' coucou=''/>".parse().unwrap();
let error = Received::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown attribute in received element.");
}
#[test]
fn test_checksum() {
let elem: Element = "<checksum xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='initiator'><file><hash xmlns='urn:xmpp:hashes:2' algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash></file></checksum>".parse().unwrap();
let hash = vec![
195, 73, 156, 39, 41, 115, 10, 127, 128, 126, 251, 134, 118, 169, 45, 203, 111, 138,
63, 143,
];
let checksum = Checksum::try_from(elem).unwrap();
assert_eq!(checksum.name, ContentId(String::from("coucou")));
assert_eq!(checksum.creator, Creator::Initiator);
assert_eq!(
checksum.file.hashes,
vec!(Hash {
algo: Algo::Sha_1,
hash: hash.clone()
})
);
let elem2 = Element::from(checksum);
let checksum2 = Checksum::try_from(elem2).unwrap();
assert_eq!(checksum2.name, ContentId(String::from("coucou")));
assert_eq!(checksum2.creator, Creator::Initiator);
assert_eq!(
checksum2.file.hashes,
vec!(Hash {
algo: Algo::Sha_1,
hash: hash.clone()
})
);
let elem: Element = "<checksum xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='initiator'><coucou/></checksum>".parse().unwrap();
let error = Checksum::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "This is not a file element.");
let elem: Element = "<checksum xmlns='urn:xmpp:jingle:apps:file-transfer:5' creator='initiator'><file><hash xmlns='urn:xmpp:hashes:2' algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash></file></checksum>".parse().unwrap();
let error = Checksum::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Required attribute 'name' missing.");
let elem: Element = "<checksum xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='coucou'><file><hash xmlns='urn:xmpp:hashes:2' algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash></file></checksum>".parse().unwrap();
let error = Checksum::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown value for 'creator' attribute.");
}
#[cfg(not(feature = "disable-validation"))]
#[test]
fn test_invalid_checksum() {
let elem: Element = "<checksum xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='initiator' coucou=''><file><hash xmlns='urn:xmpp:hashes:2' algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash></file></checksum>".parse().unwrap();
let error = Checksum::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown attribute in checksum element.");
}
#[test]
fn test_range() {
let elem: Element = "<range xmlns='urn:xmpp:jingle:apps:file-transfer:5'/>"
.parse()
.unwrap();
let range = Range::try_from(elem).unwrap();
assert_eq!(range.offset, 0);
assert_eq!(range.length, None);
assert_eq!(range.hashes, vec!());
let elem: Element = "<range xmlns='urn:xmpp:jingle:apps:file-transfer:5' offset='2048' length='1024'><hash xmlns='urn:xmpp:hashes:2' algo='sha-1'>kHp5RSzW/h7Gm1etSf90Mr5PC/k=</hash></range>".parse().unwrap();
let hashes = vec![Hash {
algo: Algo::Sha_1,
hash: vec![
144, 122, 121, 69, 44, 214, 254, 30, 198, 155, 87, 173, 73, 255, 116, 50, 190, 79,
11, 249,
],
}];
let range = Range::try_from(elem).unwrap();
assert_eq!(range.offset, 2048);
assert_eq!(range.length, Some(1024));
assert_eq!(range.hashes, hashes);
let elem2 = Element::from(range);
let range2 = Range::try_from(elem2).unwrap();
assert_eq!(range2.offset, 2048);
assert_eq!(range2.length, Some(1024));
assert_eq!(range2.hashes, hashes);
}
#[cfg(not(feature = "disable-validation"))]
#[test]
fn test_invalid_range() {
let elem: Element = "<range xmlns='urn:xmpp:jingle:apps:file-transfer:5' coucou=''/>"
.parse()
.unwrap();
let error = Range::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown attribute in range element.");
}
}

View file

@ -0,0 +1,114 @@
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 crate::ibb::{Stanza, StreamId};
generate_element!(
/// Describes an [In-Band Bytestream](https://xmpp.org/extensions/xep-0047.html)
/// Jingle transport, see also the [IBB module](../ibb.rs).
Transport, "transport", JINGLE_IBB,
attributes: [
/// Maximum size in bytes for each chunk.
block_size: Required<u16> = "block-size",
/// The identifier to be used to create a stream.
sid: Required<StreamId> = "sid",
/// Which stanza type to use to exchange data.
stanza: Default<Stanza> = "stanza",
]);
#[cfg(test)]
mod tests {
use super::*;
use crate::util::error::Error;
use crate::Element;
use std::error::Error as StdError;
use std::convert::TryFrom;
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(Transport, 16);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(Transport, 32);
}
#[test]
fn test_simple() {
let elem: Element =
"<transport xmlns='urn:xmpp:jingle:transports:ibb:1' block-size='3' sid='coucou'/>"
.parse()
.unwrap();
let transport = Transport::try_from(elem).unwrap();
assert_eq!(transport.block_size, 3);
assert_eq!(transport.sid, StreamId(String::from("coucou")));
assert_eq!(transport.stanza, Stanza::Iq);
}
#[test]
fn test_invalid() {
let elem: Element = "<transport xmlns='urn:xmpp:jingle:transports:ibb:1'/>"
.parse()
.unwrap();
let error = Transport::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Required attribute 'block-size' missing.");
let elem: Element =
"<transport xmlns='urn:xmpp:jingle:transports:ibb:1' block-size='65536'/>"
.parse()
.unwrap();
let error = Transport::try_from(elem).unwrap_err();
let message = match error {
Error::ParseIntError(error) => error,
_ => panic!(),
};
assert_eq!(
message.description(),
"number too large to fit in target type"
);
let elem: Element = "<transport xmlns='urn:xmpp:jingle:transports:ibb:1' block-size='-5'/>"
.parse()
.unwrap();
let error = Transport::try_from(elem).unwrap_err();
let message = match error {
Error::ParseIntError(error) => error,
_ => panic!(),
};
assert_eq!(message.description(), "invalid digit found in string");
let elem: Element =
"<transport xmlns='urn:xmpp:jingle:transports:ibb:1' block-size='128'/>"
.parse()
.unwrap();
let error = Transport::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Required attribute 'sid' missing.");
}
#[test]
fn test_invalid_stanza() {
let elem: Element = "<transport xmlns='urn:xmpp:jingle:transports:ibb:1' block-size='128' sid='coucou' stanza='fdsq'/>".parse().unwrap();
let error = Transport::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown value for 'stanza' attribute.");
}
}

View file

@ -0,0 +1,189 @@
// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 crate::jingle_dtls_srtp::Fingerprint;
use std::net::IpAddr;
generate_element!(
/// Wrapper element for an ICE-UDP transport.
Transport, "transport", JINGLE_ICE_UDP,
attributes: [
/// A Password as defined in ICE-CORE.
pwd: Option<String> = "pwd",
/// A User Fragment as defined in ICE-CORE.
ufrag: Option<String> = "ufrag",
],
children: [
/// List of candidates for this ICE-UDP session.
candidates: Vec<Candidate> = ("candidate", JINGLE_ICE_UDP) => Candidate,
/// Fingerprint of the key used for the DTLS handshake.
fingerprint: Option<Fingerprint> = ("fingerprint", JINGLE_DTLS) => Fingerprint
]
);
impl Transport {
/// Create a new ICE-UDP transport.
pub fn new() -> Transport {
Transport {
pwd: None,
ufrag: None,
candidates: Vec::new(),
fingerprint: None,
}
}
/// Add a candidate to this transport.
pub fn add_candidate(mut self, candidate: Candidate) -> Self {
self.candidates.push(candidate);
self
}
/// Set the DTLS-SRTP fingerprint of this transport.
pub fn with_fingerprint(mut self, fingerprint: Fingerprint) -> Self {
self.fingerprint = Some(fingerprint);
self
}
}
generate_attribute!(
/// A Candidate Type as defined in ICE-CORE.
Type, "type", {
/// Host candidate.
Host => "host",
/// Peer reflexive candidate.
Prflx => "prflx",
/// Relayed candidate.
Relay => "relay",
/// Server reflexive candidate.
Srflx => "srflx",
}
);
generate_element!(
/// A candidate for an ICE-UDP session.
Candidate, "candidate", JINGLE_ICE_UDP,
attributes: [
/// A Component ID as defined in ICE-CORE.
component: Required<u8> = "component",
/// A Foundation as defined in ICE-CORE.
foundation: Required<u8> = "foundation",
/// An index, starting at 0, that enables the parties to keep track of updates to the
/// candidate throughout the life of the session.
generation: Required<u8> = "generation",
/// A unique identifier for the candidate.
id: Required<String> = "id",
/// The Internet Protocol (IP) address for the candidate transport mechanism; this can be
/// either an IPv4 address or an IPv6 address.
ip: Required<IpAddr> = "ip",
/// An index, starting at 0, referencing which network this candidate is on for a given
/// peer.
network: Required<u8> = "network",
/// The port at the candidate IP address.
port: Required<u16> = "port",
/// A Priority as defined in ICE-CORE.
priority: Required<u32> = "priority",
/// The protocol to be used. The only value defined by this specification is "udp".
protocol: Required<String> = "protocol",
/// A related address as defined in ICE-CORE.
rel_addr: Option<IpAddr> = "rel-addr",
/// A related port as defined in ICE-CORE.
rel_port: Option<u16> = "rel-port",
/// A Candidate Type as defined in ICE-CORE.
type_: Required<Type> = "type",
]
);
#[cfg(test)]
mod tests {
use super::*;
use crate::Element;
use std::convert::TryFrom;
use crate::hashes::Algo;
use crate::jingle_dtls_srtp::Setup;
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(Transport, 68);
assert_size!(Type, 1);
assert_size!(Candidate, 80);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(Transport, 136);
assert_size!(Type, 1);
assert_size!(Candidate, 104);
}
#[test]
fn test_gajim() {
let elem: Element = "
<transport xmlns='urn:xmpp:jingle:transports:ice-udp:1' pwd='wakMJ8Ydd5rqnPaFerws5o' ufrag='aeXX'>
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='2' foundation='1' generation='0' id='11b72719-6a1b-4c51-8ae6-9f1538047568' ip='192.168.0.12' network='0' port='56715' priority='1010828030' protocol='tcp' type='host'/>
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='2' foundation='1' generation='0' id='7e07b22d-db50-4e17-9ed9-eafeb96f4f63' ip='192.168.0.12' network='0' port='0' priority='1015022334' protocol='tcp' type='host'/>
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='2' foundation='1' generation='0' id='431de362-c45f-40a8-bf10-9ed898a71d86' ip='192.168.0.12' network='0' port='36480' priority='2013266428' protocol='udp' type='host'/>
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='1' foundation='1' generation='0' id='b1197df3-abca-413b-99ee-3660d91bcfa7' ip='192.168.0.12' network='0' port='50387' priority='1010828031' protocol='tcp' type='host'/>
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='1' foundation='1' generation='0' id='adaf3a85-3a57-4df0-a2d8-0c7d28d3ca01' ip='192.168.0.12' network='0' port='0' priority='1015022335' protocol='tcp' type='host'/>
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='1' foundation='1' generation='0' id='ef4e0a62-81f2-4fe3-87ae-46cb5d1d1e1d' ip='192.168.0.12' network='0' port='43132' priority='2013266429' protocol='udp' type='host'/>
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='1' foundation='1' generation='0' id='51891e8a-4c1e-4540-b173-8637aeb0143c' ip='fe80::24eb:646f:7d78:cb6' network='0' port='38881' priority='2013266431' protocol='udp' type='host'/>
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='1' foundation='1' generation='0' id='73f82655-eb84-4fa1-b05c-1ea76f695d32' ip='fe80::24eb:646f:7d78:cb6' network='0' port='0' priority='1015023103' protocol='tcp' type='host'/>
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='1' foundation='1' generation='0' id='a2a8fa62-6f2e-41e8-b218-ba095540d60f' ip='fe80::24eb:646f:7d78:cb6' network='0' port='55819' priority='1010828799' protocol='tcp' type='host'/>
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='1' foundation='1' generation='0' id='23e66735-9515-414c-81ad-2455569a57f8' ip='2a01:e35:2e2f:fbb0:43aa:33b5:5535:8905' network='0' port='39967' priority='2013266430' protocol='udp' type='host'/>
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='1' foundation='1' generation='0' id='9a8dff18-e138-4fb2-a956-89d71216da84' ip='2a01:e35:2e2f:fbb0:43aa:33b5:5535:8905' network='0' port='0' priority='1015022079' protocol='tcp' type='host'/>
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='1' foundation='1' generation='0' id='f0c73ac3-9b7d-4032-abe3-6dd9a57d0f03' ip='2a01:e35:2e2f:fbb0:43aa:33b5:5535:8905' network='0' port='37487' priority='1010827775' protocol='tcp' type='host'/>
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='2' foundation='1' generation='0' id='a6199a00-34df-46f5-a608-847b75c5250e' ip='fe80::24eb:646f:7d78:cb6' network='0' port='43521' priority='2013266430' protocol='udp' type='host'/>
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='2' foundation='1' generation='0' id='83bc2600-39ce-4c9e-8b0b-cc7aa7e6a293' ip='fe80::24eb:646f:7d78:cb6' network='0' port='0' priority='1015023102' protocol='tcp' type='host'/>
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='2' foundation='1' generation='0' id='7e3606ca-46de-4de8-8802-068dd69ef01a' ip='fe80::24eb:646f:7d78:cb6' network='0' port='52279' priority='1010828798' protocol='tcp' type='host'/>
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='2' foundation='1' generation='0' id='a7c2472a-8462-412c-a64c-d3528f0abfa4' ip='2a01:e35:2e2f:fbb0:43aa:33b5:5535:8905' network='0' port='34088' priority='2013266429' protocol='udp' type='host'/>
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='2' foundation='1' generation='0' id='5a12c345-9643-4d2c-b770-695ec6affcaf' ip='2a01:e35:2e2f:fbb0:43aa:33b5:5535:8905' network='0' port='0' priority='1015022078' protocol='tcp' type='host'/>
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='2' foundation='1' generation='0' id='67f65b0b-8cee-421a-9f37-1f2ca2211c87' ip='2a01:e35:2e2f:fbb0:43aa:33b5:5535:8905' network='0' port='39431' priority='1010827774' protocol='tcp' type='host'/>
</transport>"
.parse()
.unwrap();
let transport = Transport::try_from(elem).unwrap();
assert_eq!(transport.pwd.unwrap(), "wakMJ8Ydd5rqnPaFerws5o");
assert_eq!(transport.ufrag.unwrap(), "aeXX");
}
#[test]
fn test_jitsi_meet() {
let elem: Element = "
<transport ufrag='2acq51d4p07v2m' pwd='7lk9uul39gckit6t02oavv2r9j' xmlns='urn:xmpp:jingle:transports:ice-udp:1'>
<fingerprint hash='sha-1' setup='actpass' xmlns='urn:xmpp:jingle:apps:dtls:0'>97:F2:B5:BE:DB:A6:00:B1:3E:40:B2:41:3C:0D:FC:E0:BD:B2:A0:E8</fingerprint>
<candidate type='host' protocol='udp' id='186cb069513c2bbe546192c93cc4ab3b05ab0d426' ip='2a05:d014:fc7:54a1:8bfc:7248:3d1c:51a4' component='1' port='10000' foundation='1' generation='0' priority='2130706431' network='0'/>
<candidate type='host' protocol='udp' id='186cb069513c2bbe546192c93cc4ab3b063daeefd' ip='10.15.1.120' component='1' port='10000' foundation='2' generation='0' priority='2130706431' network='0'/>
<candidate rel-port='10000' type='srflx' protocol='udp' id='186cb069513c2bbe546192c93cc4ab3b05d449db8' ip='3.120.176.51' component='1' port='10000' foundation='3' generation='0' network='0' priority='1677724415' rel-addr='10.15.1.120'/>
</transport>"
.parse()
.unwrap();
let transport = Transport::try_from(elem).unwrap();
assert_eq!(transport.pwd.unwrap(), "7lk9uul39gckit6t02oavv2r9j");
assert_eq!(transport.ufrag.unwrap(), "2acq51d4p07v2m");
let fingerprint = transport.fingerprint.unwrap();
assert_eq!(fingerprint.hash, Algo::Sha_1);
assert_eq!(fingerprint.setup, Setup::Actpass);
assert_eq!(fingerprint.value, [151, 242, 181, 190, 219, 166, 0, 177, 62, 64, 178, 65, 60, 13, 252, 224, 189, 178, 160, 232]);
}
}

View file

@ -0,0 +1,146 @@
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 crate::util::error::Error;
use crate::jingle::SessionId;
use crate::ns;
use crate::Element;
use std::convert::TryFrom;
/// Defines a protocol for broadcasting Jingle requests to all of the clients
/// of a user.
#[derive(Debug, Clone)]
pub enum JingleMI {
/// Indicates we want to start a Jingle session.
Propose {
/// The generated session identifier, must be unique between two users.
sid: SessionId,
/// The application description of the proposed session.
// TODO: Use a more specialised type here.
description: Element,
},
/// Cancels a previously proposed session.
Retract(SessionId),
/// Accepts a session proposed by the other party.
Accept(SessionId),
/// Proceed with a previously proposed session.
Proceed(SessionId),
/// Rejects a session proposed by the other party.
Reject(SessionId),
}
fn get_sid(elem: Element) -> Result<SessionId, Error> {
check_no_unknown_attributes!(elem, "Jingle message", ["id"]);
Ok(SessionId(get_attr!(elem, "id", Required)))
}
fn check_empty_and_get_sid(elem: Element) -> Result<SessionId, Error> {
check_no_children!(elem, "Jingle message");
get_sid(elem)
}
impl TryFrom<Element> for JingleMI {
type Error = Error;
fn try_from(elem: Element) -> Result<JingleMI, Error> {
if !elem.has_ns(ns::JINGLE_MESSAGE) {
return Err(Error::ParseError("This is not a Jingle message element."));
}
Ok(match elem.name() {
"propose" => {
let mut description = None;
for child in elem.children() {
if child.name() != "description" {
return Err(Error::ParseError("Unknown child in propose element."));
}
if description.is_some() {
return Err(Error::ParseError("Too many children in propose element."));
}
description = Some(child.clone());
}
JingleMI::Propose {
sid: get_sid(elem)?,
description: description.ok_or(Error::ParseError(
"Propose element doesnt contain a description.",
))?,
}
}
"retract" => JingleMI::Retract(check_empty_and_get_sid(elem)?),
"accept" => JingleMI::Accept(check_empty_and_get_sid(elem)?),
"proceed" => JingleMI::Proceed(check_empty_and_get_sid(elem)?),
"reject" => JingleMI::Reject(check_empty_and_get_sid(elem)?),
_ => return Err(Error::ParseError("This is not a Jingle message element.")),
})
}
}
impl From<JingleMI> for Element {
fn from(jingle_mi: JingleMI) -> Element {
match jingle_mi {
JingleMI::Propose { sid, description } => Element::builder("propose")
.ns(ns::JINGLE_MESSAGE)
.attr("id", sid)
.append(description),
JingleMI::Retract(sid) => Element::builder("retract")
.ns(ns::JINGLE_MESSAGE)
.attr("id", sid),
JingleMI::Accept(sid) => Element::builder("accept")
.ns(ns::JINGLE_MESSAGE)
.attr("id", sid),
JingleMI::Proceed(sid) => Element::builder("proceed")
.ns(ns::JINGLE_MESSAGE)
.attr("id", sid),
JingleMI::Reject(sid) => Element::builder("reject")
.ns(ns::JINGLE_MESSAGE)
.attr("id", sid),
}
.build()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(JingleMI, 68);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(JingleMI, 136);
}
#[test]
fn test_simple() {
let elem: Element = "<accept xmlns='urn:xmpp:jingle-message:0' id='coucou'/>"
.parse()
.unwrap();
JingleMI::try_from(elem).unwrap();
}
#[test]
fn test_invalid_child() {
let elem: Element =
"<propose xmlns='urn:xmpp:jingle-message:0' id='coucou'><coucou/></propose>"
.parse()
.unwrap();
let error = JingleMI::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown child in propose element.");
}
}

View file

@ -0,0 +1,46 @@
// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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/.
generate_element!(
/// Wrapper element for a rtcp-fb.
RtcpFb, "rtcp-fb", JINGLE_RTCP_FB,
attributes: [
/// Type of this rtcp-fb.
type_: Required<String> = "type",
/// Subtype of this rtcp-fb, if relevant.
subtype: Option<String> = "subtype",
]
);
#[cfg(test)]
mod tests {
use super::*;
use crate::Element;
use std::convert::TryFrom;
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(RtcpFb, 24);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(RtcpFb, 48);
}
#[test]
fn parse_simple() {
let elem: Element = "<rtcp-fb xmlns='urn:xmpp:jingle:apps:rtp:rtcp-fb:0' type='nack' subtype='sli'/>"
.parse()
.unwrap();
let rtcp_fb = RtcpFb::try_from(elem).unwrap();
assert_eq!(rtcp_fb.type_, "nack");
assert_eq!(rtcp_fb.subtype.unwrap(), "sli");
}
}

View file

@ -0,0 +1,173 @@
// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 crate::jingle_ssma::{Source, Group};
use crate::jingle_rtcp_fb::RtcpFb;
generate_element!(
/// Wrapper element describing an RTP session.
Description, "description", JINGLE_RTP,
attributes: [
/// Namespace of the encryption scheme used.
media: Required<String> = "media",
/// User-friendly name for the encryption scheme, should be `None` for OTR,
/// legacy OpenPGP and OX.
// XXX: is this a String or an u32?! Refer to RFC 3550.
ssrc: Option<String> = "ssrc",
],
children: [
/// List of encodings that can be used for this RTP stream.
payload_types: Vec<PayloadType> = ("payload-type", JINGLE_RTP) => PayloadType,
/// List of ssrc-group.
ssrc_groups: Vec<Group> = ("ssrc-group", JINGLE_SSMA) => Group,
/// List of ssrc.
ssrcs: Vec<Source> = ("source", JINGLE_SSMA) => Source
// TODO: Add support for <encryption/> and <bandwidth/>.
]
);
impl Description {
/// Create a new RTP description.
pub fn new(media: String) -> Description {
Description {
media,
ssrc: None,
payload_types: Vec::new(),
ssrc_groups: Vec::new(),
ssrcs: Vec::new(),
}
}
}
generate_attribute!(
/// The number of channels.
Channels, "channels", u8, Default = 1
);
generate_element!(
/// An encoding that can be used for an RTP stream.
PayloadType, "payload-type", JINGLE_RTP,
attributes: [
/// The number of channels.
channels: Default<Channels> = "channels",
/// The sampling frequency in Hertz.
clockrate: Option<u32> = "clockrate",
/// The payload identifier.
id: Required<u8> = "id",
/// Maximum packet time as specified in RFC 4566.
maxptime: Option<u32> = "maxptime",
/// The appropriate subtype of the MIME type.
name: Option<String> = "name",
/// Packet time as specified in RFC 4566.
ptime: Option<u32> = "ptime",
],
children: [
/// List of parameters specifying this payload-type.
///
/// Their order MUST be ignored.
parameters: Vec<Parameter> = ("parameter", JINGLE_RTP) => Parameter,
/// List of rtcp-fb parameters from XEP-0293.
rtcp_fbs: Vec<RtcpFb> = ("rtcp-fb", JINGLE_RTCP_FB) => RtcpFb
]
);
impl PayloadType {
/// Create a new RTP payload-type.
pub fn new(id: u8, name: String) -> PayloadType {
PayloadType {
channels: Default::default(),
clockrate: None,
id,
maxptime: None,
name: Some(name),
ptime: None,
parameters: Vec::new(),
rtcp_fbs: Vec::new(),
}
}
}
generate_element!(
/// Parameter related to a payload.
Parameter, "parameter", JINGLE_RTP,
attributes: [
/// The name of the parameter, from the list at
/// https://www.iana.org/assignments/sdp-parameters/sdp-parameters.xhtml
name: Required<String> = "name",
/// The value of this parameter.
value: Required<String> = "value",
]
);
#[cfg(test)]
mod tests {
use super::*;
use crate::Element;
use std::convert::TryFrom;
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(Description, 36);
assert_size!(Channels, 1);
assert_size!(PayloadType, 52);
assert_size!(Parameter, 24);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(Description, 120);
assert_size!(Channels, 1);
assert_size!(PayloadType, 104);
assert_size!(Parameter, 48);
}
#[test]
fn test_simple() {
let elem: Element = "
<description xmlns='urn:xmpp:jingle:apps:rtp:1' media='audio'>
<payload-type xmlns='urn:xmpp:jingle:apps:rtp:1' channels='2' clockrate='48000' id='96' name='OPUS'/>
<payload-type xmlns='urn:xmpp:jingle:apps:rtp:1' channels='1' clockrate='32000' id='105' name='SPEEX'/>
<payload-type xmlns='urn:xmpp:jingle:apps:rtp:1' channels='1' clockrate='8000' id='9' name='G722'/>
<payload-type xmlns='urn:xmpp:jingle:apps:rtp:1' channels='1' clockrate='16000' id='106' name='SPEEX'/>
<payload-type xmlns='urn:xmpp:jingle:apps:rtp:1' clockrate='8000' id='8' name='PCMA'/>
<payload-type xmlns='urn:xmpp:jingle:apps:rtp:1' clockrate='8000' id='0' name='PCMU'/>
<payload-type xmlns='urn:xmpp:jingle:apps:rtp:1' channels='1' clockrate='8000' id='107' name='SPEEX'/>
<payload-type xmlns='urn:xmpp:jingle:apps:rtp:1' channels='1' clockrate='8000' id='99' name='AMR'>
<parameter xmlns='urn:xmpp:jingle:apps:rtp:1' name='octet-align' value='1'/>
<parameter xmlns='urn:xmpp:jingle:apps:rtp:1' name='crc' value='0'/>
<parameter xmlns='urn:xmpp:jingle:apps:rtp:1' name='robust-sorting' value='0'/>
<parameter xmlns='urn:xmpp:jingle:apps:rtp:1' name='interleaving' value='0'/>
</payload-type>
<payload-type xmlns='urn:xmpp:jingle:apps:rtp:1' clockrate='48000' id='100' name='telephone-event'>
<parameter xmlns='urn:xmpp:jingle:apps:rtp:1' name='events' value='0-15'/>
</payload-type>
<payload-type xmlns='urn:xmpp:jingle:apps:rtp:1' clockrate='16000' id='101' name='telephone-event'>
<parameter xmlns='urn:xmpp:jingle:apps:rtp:1' name='events' value='0-15'/>
</payload-type>
<payload-type xmlns='urn:xmpp:jingle:apps:rtp:1' clockrate='8000' id='102' name='telephone-event'>
<parameter xmlns='urn:xmpp:jingle:apps:rtp:1' name='events' value='0-15'/>
</payload-type>
</description>"
.parse()
.unwrap();
let desc = Description::try_from(elem).unwrap();
assert_eq!(desc.media, "audio");
assert_eq!(desc.ssrc, None);
}
}

View file

@ -0,0 +1,352 @@
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 crate::util::error::Error;
use crate::ns;
use jid::Jid;
use crate::Element;
use std::net::IpAddr;
use std::convert::TryFrom;
generate_attribute!(
/// The type of the connection being proposed by this candidate.
Type, "type", {
/// Direct connection using NAT assisting technologies like NAT-PMP or
/// UPnP-IGD.
Assisted => "assisted",
/// Direct connection using the given interface.
Direct => "direct",
/// SOCKS5 relay.
Proxy => "proxy",
/// Tunnel protocol such as Teredo.
Tunnel => "tunnel",
}, Default = Direct
);
generate_attribute!(
/// Which mode to use for the connection.
Mode, "mode", {
/// Use TCP, which is the default.
Tcp => "tcp",
/// Use UDP.
Udp => "udp",
}, Default = Tcp
);
generate_id!(
/// An identifier for a candidate.
CandidateId
);
generate_id!(
/// An identifier for a stream.
StreamId
);
generate_element!(
/// A candidate for a connection.
Candidate, "candidate", JINGLE_S5B,
attributes: [
/// The identifier for this candidate.
cid: Required<CandidateId> = "cid",
/// The host to connect to.
host: Required<IpAddr> = "host",
/// The JID to request at the given end.
jid: Required<Jid> = "jid",
/// The port to connect to.
port: Option<u16> = "port",
/// The priority of this candidate, computed using this formula:
/// priority = (2^16)*(type preference) + (local preference)
priority: Required<u32> = "priority",
/// The type of the connection being proposed by this candidate.
type_: Default<Type> = "type",
]
);
impl Candidate {
/// Creates a new candidate with the given parameters.
pub fn new(cid: CandidateId, host: IpAddr, jid: Jid, priority: u32) -> Candidate {
Candidate {
cid,
host,
jid,
priority,
port: Default::default(),
type_: Default::default(),
}
}
/// Sets the port of this candidate.
pub fn with_port(mut self, port: u16) -> Candidate {
self.port = Some(port);
self
}
/// Sets the type of this candidate.
pub fn with_type(mut self, type_: Type) -> Candidate {
self.type_ = type_;
self
}
}
/// The payload of a transport.
#[derive(Debug, Clone)]
pub enum TransportPayload {
/// The responder informs the initiator that the bytestream pointed by this
/// candidate has been activated.
Activated(CandidateId),
/// A list of suggested candidates.
Candidates(Vec<Candidate>),
/// Both parties failed to use a candidate, they should fallback to another
/// transport.
CandidateError,
/// The candidate pointed here should be used by both parties.
CandidateUsed(CandidateId),
/// This entity cant connect to the SOCKS5 proxy.
ProxyError,
/// XXX: Invalid, should not be found in the wild.
None,
}
/// Describes a Jingle transport using a direct or proxied connection.
#[derive(Debug, Clone)]
pub struct Transport {
/// The stream identifier for this transport.
pub sid: StreamId,
/// The destination address.
pub dstaddr: Option<String>,
/// The mode to be used for the transfer.
pub mode: Mode,
/// The payload of this transport.
pub payload: TransportPayload,
}
impl Transport {
/// Creates a new transport element.
pub fn new(sid: StreamId) -> Transport {
Transport {
sid,
dstaddr: None,
mode: Default::default(),
payload: TransportPayload::None,
}
}
/// Sets the destination address of this transport.
pub fn with_dstaddr(mut self, dstaddr: String) -> Transport {
self.dstaddr = Some(dstaddr);
self
}
/// Sets the mode of this transport.
pub fn with_mode(mut self, mode: Mode) -> Transport {
self.mode = mode;
self
}
/// Sets the payload of this transport.
pub fn with_payload(mut self, payload: TransportPayload) -> Transport {
self.payload = payload;
self
}
}
impl TryFrom<Element> for Transport {
type Error = Error;
fn try_from(elem: Element) -> Result<Transport, Error> {
check_self!(elem, "transport", JINGLE_S5B);
check_no_unknown_attributes!(elem, "transport", ["sid", "dstaddr", "mode"]);
let sid = get_attr!(elem, "sid", Required);
let dstaddr = get_attr!(elem, "dstaddr", Option);
let mode = get_attr!(elem, "mode", Default);
let mut payload = None;
for child in elem.children() {
payload = Some(if child.is("candidate", ns::JINGLE_S5B) {
let mut candidates =
match payload {
Some(TransportPayload::Candidates(candidates)) => candidates,
Some(_) => return Err(Error::ParseError(
"Non-candidate child already present in JingleS5B transport element.",
)),
None => vec![],
};
candidates.push(Candidate::try_from(child.clone())?);
TransportPayload::Candidates(candidates)
} else if child.is("activated", ns::JINGLE_S5B) {
if payload.is_some() {
return Err(Error::ParseError(
"Non-activated child already present in JingleS5B transport element.",
));
}
let cid = get_attr!(child, "cid", Required);
TransportPayload::Activated(cid)
} else if child.is("candidate-error", ns::JINGLE_S5B) {
if payload.is_some() {
return Err(Error::ParseError(
"Non-candidate-error child already present in JingleS5B transport element.",
));
}
TransportPayload::CandidateError
} else if child.is("candidate-used", ns::JINGLE_S5B) {
if payload.is_some() {
return Err(Error::ParseError(
"Non-candidate-used child already present in JingleS5B transport element.",
));
}
let cid = get_attr!(child, "cid", Required);
TransportPayload::CandidateUsed(cid)
} else if child.is("proxy-error", ns::JINGLE_S5B) {
if payload.is_some() {
return Err(Error::ParseError(
"Non-proxy-error child already present in JingleS5B transport element.",
));
}
TransportPayload::ProxyError
} else {
return Err(Error::ParseError(
"Unknown child in JingleS5B transport element.",
));
});
}
let payload = payload.unwrap_or(TransportPayload::None);
Ok(Transport {
sid,
dstaddr,
mode,
payload,
})
}
}
impl From<Transport> for Element {
fn from(transport: Transport) -> Element {
Element::builder("transport")
.ns(ns::JINGLE_S5B)
.attr("sid", transport.sid)
.attr("dstaddr", transport.dstaddr)
.attr("mode", transport.mode)
.append_all(match transport.payload {
TransportPayload::Candidates(candidates) => candidates
.into_iter()
.map(Element::from)
.collect::<Vec<_>>(),
TransportPayload::Activated(cid) => vec![Element::builder("activated")
.ns(ns::JINGLE_S5B)
.attr("cid", cid)
.build()],
TransportPayload::CandidateError => vec![Element::builder("candidate-error")
.ns(ns::JINGLE_S5B)
.build()],
TransportPayload::CandidateUsed(cid) => vec![Element::builder("candidate-used")
.ns(ns::JINGLE_S5B)
.attr("cid", cid)
.build()],
TransportPayload::ProxyError => vec![Element::builder("proxy-error")
.ns(ns::JINGLE_S5B)
.build()],
TransportPayload::None => vec![],
})
.build()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::util::compare_elements::NamespaceAwareCompare;
use std::str::FromStr;
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(Type, 1);
assert_size!(Mode, 1);
assert_size!(CandidateId, 12);
assert_size!(StreamId, 12);
assert_size!(Candidate, 84);
assert_size!(TransportPayload, 16);
assert_size!(Transport, 44);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(Type, 1);
assert_size!(Mode, 1);
assert_size!(CandidateId, 24);
assert_size!(StreamId, 24);
assert_size!(Candidate, 136);
assert_size!(TransportPayload, 32);
assert_size!(Transport, 88);
}
#[test]
fn test_simple() {
let elem: Element = "<transport xmlns='urn:xmpp:jingle:transports:s5b:1' sid='coucou'/>"
.parse()
.unwrap();
let transport = Transport::try_from(elem).unwrap();
assert_eq!(transport.sid, StreamId(String::from("coucou")));
assert_eq!(transport.dstaddr, None);
assert_eq!(transport.mode, Mode::Tcp);
match transport.payload {
TransportPayload::None => (),
_ => panic!("Wrong element inside transport!"),
}
}
#[test]
fn test_serialise_activated() {
let elem: Element = "<transport xmlns='urn:xmpp:jingle:transports:s5b:1' sid='coucou'><activated cid='coucou'/></transport>".parse().unwrap();
let transport = Transport {
sid: StreamId(String::from("coucou")),
dstaddr: None,
mode: Mode::Tcp,
payload: TransportPayload::Activated(CandidateId(String::from("coucou"))),
};
let elem2: Element = transport.into();
assert!(elem.compare_to(&elem2));
}
#[test]
fn test_serialise_candidate() {
let elem: Element = "<transport xmlns='urn:xmpp:jingle:transports:s5b:1' sid='coucou'><candidate cid='coucou' host='127.0.0.1' jid='coucou@coucou' priority='0'/></transport>".parse().unwrap();
let transport = Transport {
sid: StreamId(String::from("coucou")),
dstaddr: None,
mode: Mode::Tcp,
payload: TransportPayload::Candidates(vec![Candidate {
cid: CandidateId(String::from("coucou")),
host: IpAddr::from_str("127.0.0.1").unwrap(),
jid: Jid::from_str("coucou@coucou").unwrap(),
port: None,
priority: 0u32,
type_: Type::Direct,
}]),
};
let elem2: Element = transport.into();
assert!(elem.compare_to(&elem2));
}
}

View file

@ -0,0 +1,114 @@
// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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/.
generate_element!(
/// Source element for the ssrc SDP attribute.
Source, "source", JINGLE_SSMA,
attributes: [
/// Maps to the ssrc-id parameter.
id: Required<String> = "ssrc",
],
children: [
/// List of attributes for this source.
parameters: Vec<Parameter> = ("parameter", JINGLE_SSMA) => Parameter
]
);
impl Source {
/// Create a new SSMA Source element.
pub fn new(id: String) -> Source {
Source {
id,
parameters: Vec::new(),
}
}
}
generate_element!(
/// Parameter associated with a ssrc.
Parameter, "parameter", JINGLE_SSMA,
attributes: [
/// The name of the parameter.
name: Required<String> = "name",
/// The optional value of the parameter.
value: Option<String> = "value",
]
);
generate_element!(
/// Element grouping multiple ssrc.
Group, "ssrc-group", JINGLE_SSMA,
attributes: [
/// The semantics of this group.
semantics: Required<String> = "semantics",
],
children: [
/// The various ssrc concerned by this group.
sources: Vec<Source> = ("source", JINGLE_SSMA) => Source
]
);
#[cfg(test)]
mod tests {
use super::*;
use crate::Element;
use std::convert::TryFrom;
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(Source, 24);
assert_size!(Parameter, 24);
assert_size!(Group, 24);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(Source, 48);
assert_size!(Parameter, 48);
assert_size!(Group, 48);
}
#[test]
fn parse_source() {
let elem: Element = "
<source ssrc='1656081975' xmlns='urn:xmpp:jingle:apps:rtp:ssma:0'>
<parameter name='cname' value='Yv/wvbCdsDW2Prgd'/>
<parameter name='msid' value='MLTJKIHilGn71fNQoszkQ4jlPTuS5vJyKVIv MLTJKIHilGn71fNQoszkQ4jlPTuS5vJyKVIva0'/>
</source>"
.parse()
.unwrap();
let mut ssrc = Source::try_from(elem).unwrap();
assert_eq!(ssrc.id, "1656081975");
assert_eq!(ssrc.parameters.len(), 2);
let parameter = ssrc.parameters.pop().unwrap();
assert_eq!(parameter.name, "msid");
assert_eq!(parameter.value.unwrap(), "MLTJKIHilGn71fNQoszkQ4jlPTuS5vJyKVIv MLTJKIHilGn71fNQoszkQ4jlPTuS5vJyKVIva0");
let parameter = ssrc.parameters.pop().unwrap();
assert_eq!(parameter.name, "cname");
assert_eq!(parameter.value.unwrap(), "Yv/wvbCdsDW2Prgd");
}
#[test]
fn parse_source_group() {
let elem: Element = "
<ssrc-group semantics='FID' xmlns='urn:xmpp:jingle:apps:rtp:ssma:0'>
<source ssrc='2301230316'/>
<source ssrc='386328120'/>
</ssrc-group>"
.parse()
.unwrap();
let mut group = Group::try_from(elem).unwrap();
assert_eq!(group.semantics, "FID");
assert_eq!(group.sources.len(), 2);
let source = group.sources.pop().unwrap();
assert_eq!(source.id, "386328120");
let source = group.sources.pop().unwrap();
assert_eq!(source.id, "2301230316");
}
}

214
xmpp-parsers/src/lib.rs Normal file
View file

@ -0,0 +1,214 @@
//! A crate parsing common XMPP elements into Rust structures.
//!
//! Each module implements the `TryFrom<Element>` trait, which takes a
//! minidom [`Element`] and returns a `Result` whose value is `Ok` if the
//! element parsed correctly, `Err(error::Error)` otherwise.
//!
//! The returned structure can be manipuled as any Rust structure, with each
//! field being public. You can also create the same structure manually, with
//! some having `new()` and `with_*()` helper methods to create them.
//!
//! Once you are happy with your structure, you can serialise it back to an
//! [`Element`], using either `From` or `Into<Element>`, which give you what
//! you want to be sending on the wire.
//!
//! [`Element`]: ../minidom/element/struct.Element.html
// Copyright (c) 2017-2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
// Copyright (c) 2017-2019 Maxime “pep” Buquet <pep@bouah.net>
//
// 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/.
#![deny(missing_docs)]
pub use minidom::Element;
pub use jid::{BareJid, FullJid, Jid, JidParseError};
pub use crate::util::error::Error;
/// XML namespace definitions used through XMPP.
pub mod ns;
#[macro_use]
mod util;
/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core
pub mod bind;
/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core
pub mod iq;
/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core
pub mod message;
/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core
pub mod presence;
/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core
pub mod sasl;
/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core
pub mod stanza_error;
/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core
pub mod stream;
/// RFC 6121: Extensible Messaging and Presence Protocol (XMPP): Instant Messaging and Presence
pub mod roster;
/// RFC 7395: An Extensible Messaging and Presence Protocol (XMPP) Subprotocol for WebSocket
pub mod websocket;
/// XEP-0004: Data Forms
pub mod data_forms;
/// XEP-0030: Service Discovery
pub mod disco;
/// XEP-0045: Multi-User Chat
pub mod muc;
/// XEP-0047: In-Band Bytestreams
pub mod ibb;
/// XEP-0048: Bookmarks
pub mod bookmarks;
/// XEP-0059: Result Set Management
pub mod rsm;
/// XEP-0060: Publish-Subscribe
pub mod pubsub;
/// XEP-0071: XHTML-IM
pub mod xhtml;
/// XEP-0077: In-Band Registration
pub mod ibr;
/// XEP-0082: XMPP Date and Time Profiles
pub mod date;
/// XEP-0084: User Avatar
pub mod avatar;
/// XEP-0085: Chat State Notifications
pub mod chatstates;
/// XEP-0092: Software Version
pub mod version;
/// XEP-0107: User Mood
pub mod mood;
/// XEP-0114: Jabber Component Protocol
pub mod component;
/// XEP-0115: Entity Capabilities
pub mod caps;
/// XEP-0118: User Tune
pub mod tune;
/// XEP-0157: Contact Addresses for XMPP Services
pub mod server_info;
/// XEP-0166: Jingle
pub mod jingle;
/// XEP-0167: Jingle RTP Sessions
pub mod jingle_rtp;
/// XEP-0172: User Nickname
pub mod nick;
/// XEP-0176: Jingle ICE-UDP Transport Method
pub mod jingle_ice_udp;
/// XEP-0184: Message Delivery Receipts
pub mod receipts;
/// XEP-0191: Blocking Command
pub mod blocking;
/// XEP-0198: Stream Management
pub mod sm;
/// XEP-0199: XMPP Ping
pub mod ping;
/// XEP-0202: Entity Time
pub mod time;
/// XEP-0203: Delayed Delivery
pub mod delay;
/// XEP-0221: Data Forms Media Element
pub mod media_element;
/// XEP-0224: Attention
pub mod attention;
/// XEP-0231: Bits of Binary
pub mod bob;
/// XEP-0234: Jingle File Transfer
pub mod jingle_ft;
/// XEP-0257: Client Certificate Management for SASL EXTERNAL
pub mod cert_management;
/// XEP-0260: Jingle SOCKS5 Bytestreams Transport Method
pub mod jingle_s5b;
/// XEP-0261: Jingle In-Band Bytestreams Transport Method
pub mod jingle_ibb;
/// XEP-0280: Message Carbons
pub mod carbons;
/// XEP-0293: Jingle RTP Feedback Negotiation
pub mod jingle_rtcp_fb;
/// XEP-0297: Stanza Forwarding
pub mod forwarding;
/// XEP-0300: Use of Cryptographic Hash Functions in XMPP
pub mod hashes;
/// XEP-0308: Last Message Correction
pub mod message_correct;
/// XEP-0313: Message Archive Management
pub mod mam;
/// XEP-0319: Last User Interaction in Presence
pub mod idle;
/// XEP-0320: Use of DTLS-SRTP in Jingle Sessions
pub mod jingle_dtls_srtp;
/// XEP-0328: JID Prep
pub mod jid_prep;
/// XEP-0339: Source-Specific Media Attributes in Jingle
pub mod jingle_ssma;
/// XEP-0352: Client State Indication
pub mod csi;
/// XEP-0353: Jingle Message Initiation
pub mod jingle_message;
/// XEP-0359: Unique and Stable Stanza IDs
pub mod stanza_id;
/// XEP-0373: OpenPGP for XMPP
pub mod openpgp;
/// XEP-0380: Explicit Message Encryption
pub mod eme;
/// XEP-0390: Entity Capabilities 2.0
pub mod ecaps2;
/// XEP-0402: Bookmarks 2 (This Time it's Serious)
pub mod bookmarks2;
/// XEP-0421: Anonymous unique occupant identifiers for MUCs
pub mod occupant_id;

392
xmpp-parsers/src/mam.rs Normal file
View file

@ -0,0 +1,392 @@
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 crate::data_forms::DataForm;
use crate::util::error::Error;
use crate::forwarding::Forwarded;
use crate::iq::{IqGetPayload, IqResultPayload, IqSetPayload};
use crate::message::MessagePayload;
use crate::ns;
use crate::pubsub::NodeName;
use crate::rsm::{SetQuery, SetResult};
use jid::Jid;
use minidom::{Element, Node};
use std::convert::TryFrom;
generate_id!(
/// An identifier matching a result message to the query requesting it.
QueryId
);
generate_element!(
/// Starts a query to the archive.
Query, "query", MAM,
attributes: [
/// An optional identifier for matching forwarded messages to this
/// query.
queryid: Option<QueryId> = "queryid",
/// Must be set to Some when querying a PubSub nodes archive.
node: Option<NodeName> = "node"
],
children: [
/// Used for filtering the results.
form: Option<DataForm> = ("x", DATA_FORMS) => DataForm,
/// Used for paging through results.
set: Option<SetQuery> = ("set", RSM) => SetQuery
]
);
impl IqGetPayload for Query {}
impl IqSetPayload for Query {}
impl IqResultPayload for Query {}
generate_element!(
/// The wrapper around forwarded stanzas.
Result_, "result", MAM,
attributes: [
/// The stanza-id under which the archive stored this stanza.
id: Required<String> = "id",
/// The same queryid as the one requested in the
/// [query](struct.Query.html).
queryid: Option<QueryId> = "queryid",
],
children: [
/// The actual stanza being forwarded.
forwarded: Required<Forwarded> = ("forwarded", FORWARD) => Forwarded
]
);
impl MessagePayload for Result_ {}
generate_attribute!(
/// True when the end of a MAM query has been reached.
Complete,
"complete",
bool
);
generate_element!(
/// Notes the end of a page in a query.
Fin, "fin", MAM,
attributes: [
/// True when the end of a MAM query has been reached.
complete: Default<Complete> = "complete",
],
children: [
/// Describes the current page, it should contain at least [first]
/// (with an [index]) and [last], and generally [count].
///
/// [first]: ../rsm/struct.SetResult.html#structfield.first
/// [index]: ../rsm/struct.SetResult.html#structfield.first_index
/// [last]: ../rsm/struct.SetResult.html#structfield.last
/// [count]: ../rsm/struct.SetResult.html#structfield.count
set: Required<SetResult> = ("set", RSM) => SetResult
]
);
impl IqResultPayload for Fin {}
generate_attribute!(
/// Notes the default archiving preference for the user.
DefaultPrefs, "default", {
/// The default is to always log messages in the archive.
Always => "always",
/// The default is to never log messages in the archive.
Never => "never",
/// The default is to log messages in the archive only for contacts
/// present in the users [roster](../roster/index.html).
Roster => "roster",
}
);
/// Controls the archiving preferences of the user.
#[derive(Debug, Clone)]
pub struct Prefs {
/// The default preference for JIDs in neither
/// [always](#structfield.always) or [never](#structfield.never) lists.
pub default_: DefaultPrefs,
/// The set of JIDs for which to always store messages in the archive.
pub always: Vec<Jid>,
/// The set of JIDs for which to never store messages in the archive.
pub never: Vec<Jid>,
}
impl IqGetPayload for Prefs {}
impl IqSetPayload for Prefs {}
impl IqResultPayload for Prefs {}
impl TryFrom<Element> for Prefs {
type Error = Error;
fn try_from(elem: Element) -> Result<Prefs, Error> {
check_self!(elem, "prefs", MAM);
check_no_unknown_attributes!(elem, "prefs", ["default"]);
let mut always = vec![];
let mut never = vec![];
for child in elem.children() {
if child.is("always", ns::MAM) {
for jid_elem in child.children() {
if !jid_elem.is("jid", ns::MAM) {
return Err(Error::ParseError("Invalid jid element in always."));
}
always.push(jid_elem.text().parse()?);
}
} else if child.is("never", ns::MAM) {
for jid_elem in child.children() {
if !jid_elem.is("jid", ns::MAM) {
return Err(Error::ParseError("Invalid jid element in never."));
}
never.push(jid_elem.text().parse()?);
}
} else {
return Err(Error::ParseError("Unknown child in prefs element."));
}
}
let default_ = get_attr!(elem, "default", Required);
Ok(Prefs {
default_,
always,
never,
})
}
}
fn serialise_jid_list(name: &str, jids: Vec<Jid>) -> ::std::option::IntoIter<Node> {
if jids.is_empty() {
None.into_iter()
} else {
Some(
Element::builder(name)
.ns(ns::MAM)
.append_all(
jids.into_iter()
.map(|jid|
Element::builder("jid")
.ns(ns::MAM)
.append(String::from(jid))))
.into(),
).into_iter()
}
}
impl From<Prefs> for Element {
fn from(prefs: Prefs) -> Element {
Element::builder("prefs")
.ns(ns::MAM)
.attr("default", prefs.default_)
.append_all(serialise_jid_list("always", prefs.always))
.append_all(serialise_jid_list("never", prefs.never))
.build()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(QueryId, 12);
assert_size!(Query, 116);
assert_size!(Result_, 236);
assert_size!(Complete, 1);
assert_size!(Fin, 44);
assert_size!(DefaultPrefs, 1);
assert_size!(Prefs, 28);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(QueryId, 24);
assert_size!(Query, 232);
assert_size!(Result_, 456);
assert_size!(Complete, 1);
assert_size!(Fin, 88);
assert_size!(DefaultPrefs, 1);
assert_size!(Prefs, 56);
}
#[test]
fn test_query() {
let elem: Element = "<query xmlns='urn:xmpp:mam:2'/>".parse().unwrap();
Query::try_from(elem).unwrap();
}
#[test]
fn test_result() {
#[cfg(not(feature = "component"))]
let elem: Element = r#"
<result xmlns='urn:xmpp:mam:2' queryid='f27' id='28482-98726-73623'>
<forwarded xmlns='urn:xmpp:forward:0'>
<delay xmlns='urn:xmpp:delay' stamp='2010-07-10T23:08:25Z'/>
<message xmlns='jabber:client' from="witch@shakespeare.lit" to="macbeth@shakespeare.lit">
<body>Hail to thee</body>
</message>
</forwarded>
</result>
"#
.parse()
.unwrap();
#[cfg(feature = "component")]
let elem: Element = r#"
<result xmlns='urn:xmpp:mam:2' queryid='f27' id='28482-98726-73623'>
<forwarded xmlns='urn:xmpp:forward:0'>
<delay xmlns='urn:xmpp:delay' stamp='2010-07-10T23:08:25Z'/>
<message xmlns='jabber:component:accept' from="witch@shakespeare.lit" to="macbeth@shakespeare.lit">
<body>Hail to thee</body>
</message>
</forwarded>
</result>
"#.parse().unwrap();
Result_::try_from(elem).unwrap();
}
#[test]
fn test_fin() {
let elem: Element = r#"
<fin xmlns='urn:xmpp:mam:2'>
<set xmlns='http://jabber.org/protocol/rsm'>
<first index='0'>28482-98726-73623</first>
<last>09af3-cc343-b409f</last>
</set>
</fin>
"#
.parse()
.unwrap();
Fin::try_from(elem).unwrap();
}
#[test]
fn test_query_x() {
let elem: Element = r#"
<query xmlns='urn:xmpp:mam:2'>
<x xmlns='jabber:x:data' type='submit'>
<field var='FORM_TYPE' type='hidden'>
<value>urn:xmpp:mam:2</value>
</field>
<field var='with'>
<value>juliet@capulet.lit</value>
</field>
</x>
</query>
"#
.parse()
.unwrap();
Query::try_from(elem).unwrap();
}
#[test]
fn test_query_x_set() {
let elem: Element = r#"
<query xmlns='urn:xmpp:mam:2'>
<x xmlns='jabber:x:data' type='submit'>
<field var='FORM_TYPE' type='hidden'>
<value>urn:xmpp:mam:2</value>
</field>
<field var='start'>
<value>2010-08-07T00:00:00Z</value>
</field>
</x>
<set xmlns='http://jabber.org/protocol/rsm'>
<max>10</max>
</set>
</query>
"#
.parse()
.unwrap();
Query::try_from(elem).unwrap();
}
#[test]
fn test_prefs_get() {
let elem: Element = "<prefs xmlns='urn:xmpp:mam:2' default='always'/>"
.parse()
.unwrap();
let prefs = Prefs::try_from(elem).unwrap();
assert_eq!(prefs.always, vec!());
assert_eq!(prefs.never, vec!());
let elem: Element = r#"
<prefs xmlns='urn:xmpp:mam:2' default='roster'>
<always/>
<never/>
</prefs>
"#
.parse()
.unwrap();
let prefs = Prefs::try_from(elem).unwrap();
assert_eq!(prefs.always, vec!());
assert_eq!(prefs.never, vec!());
}
#[test]
fn test_prefs_result() {
let elem: Element = r#"
<prefs xmlns='urn:xmpp:mam:2' default='roster'>
<always>
<jid>romeo@montague.lit</jid>
</always>
<never>
<jid>montague@montague.lit</jid>
</never>
</prefs>
"#
.parse()
.unwrap();
let prefs = Prefs::try_from(elem).unwrap();
assert_eq!(
prefs.always,
vec!(Jid::from_str("romeo@montague.lit").unwrap())
);
assert_eq!(
prefs.never,
vec!(Jid::from_str("montague@montague.lit").unwrap())
);
let elem2 = Element::from(prefs.clone());
println!("{:?}", elem2);
let prefs2 = Prefs::try_from(elem2).unwrap();
assert_eq!(prefs.default_, prefs2.default_);
assert_eq!(prefs.always, prefs2.always);
assert_eq!(prefs.never, prefs2.never);
}
#[test]
fn test_invalid_child() {
let elem: Element = "<query xmlns='urn:xmpp:mam:2'><coucou/></query>"
.parse()
.unwrap();
let error = Query::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown child in query element.");
}
#[test]
fn test_serialise() {
let elem: Element = "<query xmlns='urn:xmpp:mam:2'/>".parse().unwrap();
let replace = Query {
queryid: None,
node: None,
form: None,
set: None,
};
let elem2 = replace.into();
assert_eq!(elem, elem2);
}
}

View file

@ -0,0 +1,256 @@
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 crate::util::helpers::TrimmedPlainText;
generate_element!(
/// Represents an URI used in a media element.
URI, "uri", MEDIA_ELEMENT,
attributes: [
/// The MIME type of the URI referenced.
///
/// See the [IANA MIME Media Types Registry][1] for a list of
/// registered types, but unregistered or yet-to-be-registered are
/// accepted too.
///
/// [1]: https://www.iana.org/assignments/media-types/media-types.xhtml
type_: Required<String> = "type"
],
text: (
/// The actual URI contained.
uri: TrimmedPlainText<String>
)
);
generate_element!(
/// References a media element, to be used in [data
/// forms](../data_forms/index.html).
MediaElement, "media", MEDIA_ELEMENT,
attributes: [
/// The recommended display width in pixels.
width: Option<usize> = "width",
/// The recommended display height in pixels.
height: Option<usize> = "height"
],
children: [
/// A list of URIs referencing this media.
uris: Vec<URI> = ("uri", MEDIA_ELEMENT) => URI
]
);
#[cfg(test)]
mod tests {
use super::*;
use crate::data_forms::DataForm;
use crate::util::error::Error;
use crate::Element;
use std::error::Error as StdError;
use std::convert::TryFrom;
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(URI, 24);
assert_size!(MediaElement, 28);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(URI, 48);
assert_size!(MediaElement, 56);
}
#[test]
fn test_simple() {
let elem: Element = "<media xmlns='urn:xmpp:media-element'/>".parse().unwrap();
let media = MediaElement::try_from(elem).unwrap();
assert!(media.width.is_none());
assert!(media.height.is_none());
assert!(media.uris.is_empty());
}
#[test]
fn test_width_height() {
let elem: Element = "<media xmlns='urn:xmpp:media-element' width='32' height='32'/>"
.parse()
.unwrap();
let media = MediaElement::try_from(elem).unwrap();
assert_eq!(media.width.unwrap(), 32);
assert_eq!(media.height.unwrap(), 32);
}
#[test]
fn test_uri() {
let elem: Element = "<media xmlns='urn:xmpp:media-element'><uri type='text/html'>https://example.org/</uri></media>".parse().unwrap();
let media = MediaElement::try_from(elem).unwrap();
assert_eq!(media.uris.len(), 1);
assert_eq!(media.uris[0].type_, "text/html");
assert_eq!(media.uris[0].uri, "https://example.org/");
}
#[test]
fn test_invalid_width_height() {
let elem: Element = "<media xmlns='urn:xmpp:media-element' width=''/>"
.parse()
.unwrap();
let error = MediaElement::try_from(elem).unwrap_err();
let error = match error {
Error::ParseIntError(error) => error,
_ => panic!(),
};
assert_eq!(
error.description(),
"cannot parse integer from empty string"
);
let elem: Element = "<media xmlns='urn:xmpp:media-element' width='coucou'/>"
.parse()
.unwrap();
let error = MediaElement::try_from(elem).unwrap_err();
let error = match error {
Error::ParseIntError(error) => error,
_ => panic!(),
};
assert_eq!(error.description(), "invalid digit found in string");
let elem: Element = "<media xmlns='urn:xmpp:media-element' height=''/>"
.parse()
.unwrap();
let error = MediaElement::try_from(elem).unwrap_err();
let error = match error {
Error::ParseIntError(error) => error,
_ => panic!(),
};
assert_eq!(
error.description(),
"cannot parse integer from empty string"
);
let elem: Element = "<media xmlns='urn:xmpp:media-element' height='-10'/>"
.parse()
.unwrap();
let error = MediaElement::try_from(elem).unwrap_err();
let error = match error {
Error::ParseIntError(error) => error,
_ => panic!(),
};
assert_eq!(error.description(), "invalid digit found in string");
}
#[test]
fn test_unknown_child() {
let elem: Element = "<media xmlns='urn:xmpp:media-element'><coucou/></media>"
.parse()
.unwrap();
let error = MediaElement::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown child in media element.");
}
#[test]
fn test_bad_uri() {
let elem: Element =
"<media xmlns='urn:xmpp:media-element'><uri>https://example.org/</uri></media>"
.parse()
.unwrap();
let error = MediaElement::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Required attribute 'type' missing.");
let elem: Element = "<media xmlns='urn:xmpp:media-element'><uri type='text/html'/></media>"
.parse()
.unwrap();
let error = MediaElement::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "URI missing in uri.");
}
#[test]
fn test_xep_ex1() {
let elem: Element = r#"
<media xmlns='urn:xmpp:media-element'>
<uri type='audio/x-wav'>
http://victim.example.com/challenges/speech.wav?F3A6292C
</uri>
<uri type='audio/ogg; codecs=speex'>
cid:sha1+a15a505e360702b79c75a5f67773072ed392f52a@bob.xmpp.org
</uri>
<uri type='audio/mpeg'>
http://victim.example.com/challenges/speech.mp3?F3A6292C
</uri>
</media>"#
.parse()
.unwrap();
let media = MediaElement::try_from(elem).unwrap();
assert!(media.width.is_none());
assert!(media.height.is_none());
assert_eq!(media.uris.len(), 3);
assert_eq!(media.uris[0].type_, "audio/x-wav");
assert_eq!(
media.uris[0].uri,
"http://victim.example.com/challenges/speech.wav?F3A6292C"
);
assert_eq!(media.uris[1].type_, "audio/ogg; codecs=speex");
assert_eq!(
media.uris[1].uri,
"cid:sha1+a15a505e360702b79c75a5f67773072ed392f52a@bob.xmpp.org"
);
assert_eq!(media.uris[2].type_, "audio/mpeg");
assert_eq!(
media.uris[2].uri,
"http://victim.example.com/challenges/speech.mp3?F3A6292C"
);
}
#[test]
fn test_xep_ex2() {
let elem: Element = r#"
<x xmlns='jabber:x:data' type='form'>
[ ... ]
<field var='ocr'>
<media xmlns='urn:xmpp:media-element'
height='80'
width='290'>
<uri type='image/jpeg'>
http://www.victim.com/challenges/ocr.jpeg?F3A6292C
</uri>
<uri type='image/jpeg'>
cid:sha1+f24030b8d91d233bac14777be5ab531ca3b9f102@bob.xmpp.org
</uri>
</media>
</field>
[ ... ]
</x>"#
.parse()
.unwrap();
let form = DataForm::try_from(elem).unwrap();
assert_eq!(form.fields.len(), 1);
assert_eq!(form.fields[0].var, "ocr");
assert_eq!(form.fields[0].media[0].width, Some(290));
assert_eq!(form.fields[0].media[0].height, Some(80));
assert_eq!(form.fields[0].media[0].uris[0].type_, "image/jpeg");
assert_eq!(
form.fields[0].media[0].uris[0].uri,
"http://www.victim.com/challenges/ocr.jpeg?F3A6292C"
);
assert_eq!(form.fields[0].media[0].uris[1].type_, "image/jpeg");
assert_eq!(
form.fields[0].media[0].uris[1].uri,
"cid:sha1+f24030b8d91d233bac14777be5ab531ca3b9f102@bob.xmpp.org"
);
}
}

418
xmpp-parsers/src/message.rs Normal file
View file

@ -0,0 +1,418 @@
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 crate::util::error::Error;
use crate::ns;
use jid::Jid;
use crate::Element;
use std::collections::BTreeMap;
use std::convert::TryFrom;
/// Should be implemented on every known payload of a `<message/>`.
pub trait MessagePayload: TryFrom<Element> + Into<Element> {}
generate_attribute!(
/// The type of a message.
MessageType, "type", {
/// Standard instant messaging message.
Chat => "chat",
/// Notifies that an error happened.
Error => "error",
/// Standard group instant messaging message.
Groupchat => "groupchat",
/// Used by servers to notify users when things happen.
Headline => "headline",
/// This is an email-like message, it usually contains a
/// [subject](struct.Subject.html).
Normal => "normal",
}, Default = Normal
);
type Lang = String;
generate_elem_id!(
/// Represents one `<body/>` element, that is the free form text content of
/// a message.
Body,
"body",
DEFAULT_NS
);
generate_elem_id!(
/// Defines the subject of a room, or of an email-like normal message.
Subject,
"subject",
DEFAULT_NS
);
generate_elem_id!(
/// A thread identifier, so that other people can specify to which message
/// they are replying.
Thread,
"thread",
DEFAULT_NS
);
/// The main structure representing the `<message/>` stanza.
#[derive(Debug, Clone)]
pub struct Message {
/// The JID emitting this stanza.
pub from: Option<Jid>,
/// The recipient of this stanza.
pub to: Option<Jid>,
/// The @id attribute of this stanza, which is required in order to match a
/// request with its response.
pub id: Option<String>,
/// The type of this message.
pub type_: MessageType,
/// A list of bodies, sorted per language. Use
/// [get_best_body()](#method.get_best_body) to access them on reception.
pub bodies: BTreeMap<Lang, Body>,
/// A list of subjects, sorted per language. Use
/// [get_best_subject()](#method.get_best_subject) to access them on
/// reception.
pub subjects: BTreeMap<Lang, Subject>,
/// An optional thread identifier, so that other people can reply directly
/// to this message.
pub thread: Option<Thread>,
/// A list of the extension payloads contained in this stanza.
pub payloads: Vec<Element>,
}
impl Message {
/// Creates a new `<message/>` stanza for the given recipient.
pub fn new(to: Option<Jid>) -> Message {
Message {
from: None,
to,
id: None,
type_: MessageType::Chat,
bodies: BTreeMap::new(),
subjects: BTreeMap::new(),
thread: None,
payloads: vec![],
}
}
fn get_best<'a, T>(
map: &'a BTreeMap<Lang, T>,
preferred_langs: Vec<&str>,
) -> Option<(Lang, &'a T)> {
if map.is_empty() {
return None;
}
for lang in preferred_langs {
if let Some(value) = map.get(lang) {
return Some((Lang::from(lang), value));
}
}
if let Some(value) = map.get("") {
return Some((Lang::new(), value));
}
map.iter().map(|(lang, value)| (lang.clone(), value)).next()
}
/// Returns the best matching body from a list of languages.
///
/// For instance, if a message contains both an xml:lang='de', an xml:lang='fr' and an English
/// body without an xml:lang attribute, and you pass ["fr", "en"] as your preferred languages,
/// `Some(("fr", the_second_body))` will be returned.
///
/// If no body matches, an undefined body will be returned.
pub fn get_best_body(&self, preferred_langs: Vec<&str>) -> Option<(Lang, &Body)> {
Message::get_best::<Body>(&self.bodies, preferred_langs)
}
/// Returns the best matching subject from a list of languages.
///
/// For instance, if a message contains both an xml:lang='de', an xml:lang='fr' and an English
/// subject without an xml:lang attribute, and you pass ["fr", "en"] as your preferred
/// languages, `Some(("fr", the_second_subject))` will be returned.
///
/// If no subject matches, an undefined subject will be returned.
pub fn get_best_subject(&self, preferred_langs: Vec<&str>) -> Option<(Lang, &Subject)> {
Message::get_best::<Subject>(&self.subjects, preferred_langs)
}
}
impl TryFrom<Element> for Message {
type Error = Error;
fn try_from(root: Element) -> Result<Message, Error> {
check_self!(root, "message", DEFAULT_NS);
let from = get_attr!(root, "from", Option);
let to = get_attr!(root, "to", Option);
let id = get_attr!(root, "id", Option);
let type_ = get_attr!(root, "type", Default);
let mut bodies = BTreeMap::new();
let mut subjects = BTreeMap::new();
let mut thread = None;
let mut payloads = vec![];
for elem in root.children() {
if elem.is("body", ns::DEFAULT_NS) {
check_no_children!(elem, "body");
let lang = get_attr!(elem, "xml:lang", Default);
let body = Body(elem.text());
if bodies.insert(lang, body).is_some() {
return Err(Error::ParseError(
"Body element present twice for the same xml:lang.",
));
}
} else if elem.is("subject", ns::DEFAULT_NS) {
check_no_children!(elem, "subject");
let lang = get_attr!(elem, "xml:lang", Default);
let subject = Subject(elem.text());
if subjects.insert(lang, subject).is_some() {
return Err(Error::ParseError(
"Subject element present twice for the same xml:lang.",
));
}
} else if elem.is("thread", ns::DEFAULT_NS) {
if thread.is_some() {
return Err(Error::ParseError("Thread element present twice."));
}
check_no_children!(elem, "thread");
thread = Some(Thread(elem.text()));
} else {
payloads.push(elem.clone())
}
}
Ok(Message {
from,
to,
id,
type_,
bodies,
subjects,
thread,
payloads,
})
}
}
impl From<Message> for Element {
fn from(message: Message) -> Element {
Element::builder("message")
.ns(ns::DEFAULT_NS)
.attr("from", message.from)
.attr("to", message.to)
.attr("id", message.id)
.attr("type", message.type_)
.append_all(
message
.subjects
.into_iter()
.map(|(lang, subject)| {
let mut subject = Element::from(subject);
subject.set_attr(
"xml:lang",
match lang.as_ref() {
"" => None,
lang => Some(lang),
},
);
subject
})
)
.append_all(
message
.bodies
.into_iter()
.map(|(lang, body)| {
let mut body = Element::from(body);
body.set_attr(
"xml:lang",
match lang.as_ref() {
"" => None,
lang => Some(lang),
},
);
body
})
)
.append_all(message.payloads.into_iter())
.build()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::util::compare_elements::NamespaceAwareCompare;
use std::str::FromStr;
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(MessageType, 1);
assert_size!(Body, 12);
assert_size!(Subject, 12);
assert_size!(Thread, 12);
assert_size!(Message, 144);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(MessageType, 1);
assert_size!(Body, 24);
assert_size!(Subject, 24);
assert_size!(Thread, 24);
assert_size!(Message, 288);
}
#[test]
fn test_simple() {
#[cfg(not(feature = "component"))]
let elem: Element = "<message xmlns='jabber:client'/>".parse().unwrap();
#[cfg(feature = "component")]
let elem: Element = "<message xmlns='jabber:component:accept'/>"
.parse()
.unwrap();
let message = Message::try_from(elem).unwrap();
assert_eq!(message.from, None);
assert_eq!(message.to, None);
assert_eq!(message.id, None);
assert_eq!(message.type_, MessageType::Normal);
assert!(message.payloads.is_empty());
}
#[test]
fn test_serialise() {
#[cfg(not(feature = "component"))]
let elem: Element = "<message xmlns='jabber:client'/>".parse().unwrap();
#[cfg(feature = "component")]
let elem: Element = "<message xmlns='jabber:component:accept'/>"
.parse()
.unwrap();
let mut message = Message::new(None);
message.type_ = MessageType::Normal;
let elem2 = message.into();
assert_eq!(elem, elem2);
}
#[test]
fn test_body() {
#[cfg(not(feature = "component"))]
let elem: Element = "<message xmlns='jabber:client' to='coucou@example.org' type='chat'><body>Hello world!</body></message>".parse().unwrap();
#[cfg(feature = "component")]
let elem: Element = "<message xmlns='jabber:component:accept' to='coucou@example.org' type='chat'><body>Hello world!</body></message>".parse().unwrap();
let elem1 = elem.clone();
let message = Message::try_from(elem).unwrap();
assert_eq!(message.bodies[""], Body::from_str("Hello world!").unwrap());
{
let (lang, body) = message.get_best_body(vec!["en"]).unwrap();
assert_eq!(lang, "");
assert_eq!(body, &Body::from_str("Hello world!").unwrap());
}
let elem2 = message.into();
assert!(elem1.compare_to(&elem2));
}
#[test]
fn test_serialise_body() {
#[cfg(not(feature = "component"))]
let elem: Element = "<message xmlns='jabber:client' to='coucou@example.org' type='chat'><body>Hello world!</body></message>".parse().unwrap();
#[cfg(feature = "component")]
let elem: Element = "<message xmlns='jabber:component:accept' to='coucou@example.org' type='chat'><body>Hello world!</body></message>".parse().unwrap();
let mut message = Message::new(Some(Jid::from_str("coucou@example.org").unwrap()));
message
.bodies
.insert(String::from(""), Body::from_str("Hello world!").unwrap());
let elem2 = message.into();
assert!(elem.compare_to(&elem2));
}
#[test]
fn test_subject() {
#[cfg(not(feature = "component"))]
let elem: Element = "<message xmlns='jabber:client' to='coucou@example.org' type='chat'><subject>Hello world!</subject></message>".parse().unwrap();
#[cfg(feature = "component")]
let elem: Element = "<message xmlns='jabber:component:accept' to='coucou@example.org' type='chat'><subject>Hello world!</subject></message>".parse().unwrap();
let elem1 = elem.clone();
let message = Message::try_from(elem).unwrap();
assert_eq!(
message.subjects[""],
Subject::from_str("Hello world!").unwrap()
);
{
let (lang, subject) = message.get_best_subject(vec!["en"]).unwrap();
assert_eq!(lang, "");
assert_eq!(subject, &Subject::from_str("Hello world!").unwrap());
}
let elem2 = message.into();
assert!(elem1.compare_to(&elem2));
}
#[test]
fn get_best_body() {
#[cfg(not(feature = "component"))]
let elem: Element = "<message xmlns='jabber:client' to='coucou@example.org' type='chat'><body xml:lang='de'>Hallo Welt!</body><body xml:lang='fr'>Salut le monde!</body><body>Hello world!</body></message>".parse().unwrap();
#[cfg(feature = "component")]
let elem: Element = "<message xmlns='jabber:component:accept' to='coucou@example.org' type='chat'><body>Hello world!</body></message>".parse().unwrap();
let message = Message::try_from(elem).unwrap();
// Tests basic feature.
{
let (lang, body) = message.get_best_body(vec!["fr"]).unwrap();
assert_eq!(lang, "fr");
assert_eq!(body, &Body::from_str("Salut le monde!").unwrap());
}
// Tests order.
{
let (lang, body) = message.get_best_body(vec!["en", "de"]).unwrap();
assert_eq!(lang, "de");
assert_eq!(body, &Body::from_str("Hallo Welt!").unwrap());
}
// Tests fallback.
{
let (lang, body) = message.get_best_body(vec![]).unwrap();
assert_eq!(lang, "");
assert_eq!(body, &Body::from_str("Hello world!").unwrap());
}
// Tests fallback.
{
let (lang, body) = message.get_best_body(vec!["ja"]).unwrap();
assert_eq!(lang, "");
assert_eq!(body, &Body::from_str("Hello world!").unwrap());
}
let message = Message::new(None);
// Tests without a body.
assert_eq!(message.get_best_body(vec!("ja")), None);
}
#[test]
fn test_attention() {
#[cfg(not(feature = "component"))]
let elem: Element = "<message xmlns='jabber:client' to='coucou@example.org' type='chat'><attention xmlns='urn:xmpp:attention:0'/></message>".parse().unwrap();
#[cfg(feature = "component")]
let elem: Element = "<message xmlns='jabber:component:accept' to='coucou@example.org' type='chat'><attention xmlns='urn:xmpp:attention:0'/></message>".parse().unwrap();
let elem1 = elem.clone();
let message = Message::try_from(elem).unwrap();
let elem2 = message.into();
assert_eq!(elem1, elem2);
}
}

View file

@ -0,0 +1,99 @@
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 crate::message::MessagePayload;
generate_element!(
/// Defines that the message containing this payload should replace a
/// previous message, identified by the id.
Replace, "replace", MESSAGE_CORRECT,
attributes: [
/// The 'id' attribute of the message getting corrected.
id: Required<String> = "id",
]
);
impl MessagePayload for Replace {}
#[cfg(test)]
mod tests {
use super::*;
use crate::util::error::Error;
use crate::Element;
use std::convert::TryFrom;
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(Replace, 12);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(Replace, 24);
}
#[test]
fn test_simple() {
let elem: Element = "<replace xmlns='urn:xmpp:message-correct:0' id='coucou'/>"
.parse()
.unwrap();
Replace::try_from(elem).unwrap();
}
#[cfg(not(feature = "disable-validation"))]
#[test]
fn test_invalid_attribute() {
let elem: Element = "<replace xmlns='urn:xmpp:message-correct:0' coucou=''/>"
.parse()
.unwrap();
let error = Replace::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown attribute in replace element.");
}
#[test]
fn test_invalid_child() {
let elem: Element = "<replace xmlns='urn:xmpp:message-correct:0'><coucou/></replace>"
.parse()
.unwrap();
let error = Replace::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown child in replace element.");
}
#[test]
fn test_invalid_id() {
let elem: Element = "<replace xmlns='urn:xmpp:message-correct:0'/>"
.parse()
.unwrap();
let error = Replace::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Required attribute 'id' missing.");
}
#[test]
fn test_serialise() {
let elem: Element = "<replace xmlns='urn:xmpp:message-correct:0' id='coucou'/>"
.parse()
.unwrap();
let replace = Replace {
id: String::from("coucou"),
};
let elem2 = replace.into();
assert_eq!(elem, elem2);
}
}

312
xmpp-parsers/src/mood.rs Normal file
View file

@ -0,0 +1,312 @@
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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/.
generate_element_enum!(
/// Enum representing all of the possible values of the XEP-0107 moods.
MoodEnum, "mood", MOOD, {
/// Impressed with fear or apprehension; in fear; apprehensive.
Afraid => "afraid",
/// Astonished; confounded with fear, surprise or wonder.
Amazed => "amazed",
/// Inclined to love; having a propensity to love, or to sexual enjoyment; loving, fond, affectionate, passionate, lustful, sexual, etc.
Amorous => "amorous",
/// Displaying or feeling anger, i.e., a strong feeling of displeasure, hostility or antagonism towards someone or something, usually combined with an urge to harm.
Angry => "angry",
/// To be disturbed or irritated, especially by continued or repeated acts.
Annoyed => "annoyed",
/// Full of anxiety or disquietude; greatly concerned or solicitous, esp. respecting something future or unknown; being in painful suspense.
Anxious => "anxious",
/// To be stimulated in one's feelings, especially to be sexually stimulated.
Aroused => "aroused",
/// Feeling shame or guilt.
Ashamed => "ashamed",
/// Suffering from boredom; uninterested, without attention.
Bored => "bored",
/// Strong in the face of fear; courageous.
Brave => "brave",
/// Peaceful, quiet.
Calm => "calm",
/// Taking care or caution; tentative.
Cautious => "cautious",
/// Feeling the sensation of coldness, especially to the point of discomfort.
Cold => "cold",
/// Feeling very sure of or positive about something, especially about one's own capabilities.
Confident => "confident",
/// Chaotic, jumbled or muddled.
Confused => "confused",
/// Feeling introspective or thoughtful.
Contemplative => "contemplative",
/// Pleased at the satisfaction of a want or desire; satisfied.
Contented => "contented",
/// Grouchy, irritable; easily upset.
Cranky => "cranky",
/// Feeling out of control; feeling overly excited or enthusiastic.
Crazy => "crazy",
/// Feeling original, expressive, or imaginative.
Creative => "creative",
/// Inquisitive; tending to ask questions, investigate, or explore.
Curious => "curious",
/// Feeling sad and dispirited.
Dejected => "dejected",
/// Severely despondent and unhappy.
Depressed => "depressed",
/// Defeated of expectation or hope; let down.
Disappointed => "disappointed",
/// Filled with disgust; irritated and out of patience.
Disgusted => "disgusted",
/// Feeling a sudden or complete loss of courage in the face of trouble or danger.
Dismayed => "dismayed",
/// Having one's attention diverted; preoccupied.
Distracted => "distracted",
/// Having a feeling of shameful discomfort.
Embarrassed => "embarrassed",
/// Feeling pain by the excellence or good fortune of another.
Envious => "envious",
/// Having great enthusiasm.
Excited => "excited",
/// In the mood for flirting.
Flirtatious => "flirtatious",
/// Suffering from frustration; dissatisfied, agitated, or discontented because one is unable to perform an action or fulfill a desire.
Frustrated => "frustrated",
/// Feeling appreciation or thanks.
Grateful => "grateful",
/// Feeling very sad about something, especially something lost; mournful; sorrowful.
Grieving => "grieving",
/// Unhappy and irritable.
Grumpy => "grumpy",
/// Feeling responsible for wrongdoing; feeling blameworthy.
Guilty => "guilty",
/// Experiencing the effect of favourable fortune; having the feeling arising from the consciousness of well-being or of enjoyment; enjoying good of any kind, as peace, tranquillity, comfort; contented; joyous.
Happy => "happy",
/// Having a positive feeling, belief, or expectation that something wished for can or will happen.
Hopeful => "hopeful",
/// Feeling the sensation of heat, especially to the point of discomfort.
Hot => "hot",
/// Having or showing a modest or low estimate of one's own importance; feeling lowered in dignity or importance.
Humbled => "humbled",
/// Feeling deprived of dignity or self-respect.
Humiliated => "humiliated",
/// Having a physical need for food.
Hungry => "hungry",
/// Wounded, injured, or pained, whether physically or emotionally.
Hurt => "hurt",
/// Favourably affected by something or someone.
Impressed => "impressed",
/// Feeling amazement at something or someone; or feeling a combination of fear and reverence.
InAwe => "in_awe",
/// Feeling strong affection, care, liking, or attraction..
InLove => "in_love",
/// Showing anger or indignation, especially at something unjust or wrong.
Indignant => "indignant",
/// Showing great attention to something or someone; having or showing interest.
Interested => "interested",
/// Under the influence of alcohol; drunk.
Intoxicated => "intoxicated",
/// Feeling as if one cannot be defeated, overcome or denied.
Invincible => "invincible",
/// Fearful of being replaced in position or affection.
Jealous => "jealous",
/// Feeling isolated, empty, or abandoned.
Lonely => "lonely",
/// Unable to find one's way, either physically or emotionally.
Lost => "lost",
/// Feeling as if one will be favored by luck.
Lucky => "lucky",
/// Causing or intending to cause intentional harm; bearing ill will towards another; cruel; malicious.
Mean => "mean",
/// Given to sudden or frequent changes of mind or feeling; temperamental.
Moody => "moody",
/// Easily agitated or alarmed; apprehensive or anxious.
Nervous => "nervous",
/// Not having a strong mood or emotional state.
Neutral => "neutral",
/// Feeling emotionally hurt, displeased, or insulted.
Offended => "offended",
/// Feeling resentful anger caused by an extremely violent or vicious attack, or by an offensive, immoral, or indecent act.
Outraged => "outraged",
/// Interested in play; fun, recreational, unserious, lighthearted; joking, silly.
Playful => "playful",
/// Feeling a sense of one's own worth or accomplishment.
Proud => "proud",
/// Having an easy-going mood; not stressed; calm.
Relaxed => "relaxed",
/// Feeling uplifted because of the removal of stress or discomfort.
Relieved => "relieved",
/// Feeling regret or sadness for doing something wrong.
Remorseful => "remorseful",
/// Without rest; unable to be still or quiet; uneasy; continually moving.
Restless => "restless",
/// Feeling sorrow; sorrowful, mournful.
Sad => "sad",
/// Mocking and ironical.
Sarcastic => "sarcastic",
/// Pleased at the fulfillment of a need or desire.
Satisfied => "satisfied",
/// Without humor or expression of happiness; grave in manner or disposition; earnest; thoughtful; solemn.
Serious => "serious",
/// Surprised, startled, confused, or taken aback.
Shocked => "shocked",
/// Feeling easily frightened or scared; timid; reserved or coy.
Shy => "shy",
/// Feeling in poor health; ill.
Sick => "sick",
/// Feeling the need for sleep.
Sleepy => "sleepy",
/// Acting without planning; natural; impulsive.
Spontaneous => "spontaneous",
/// Suffering emotional pressure.
Stressed => "stressed",
/// Capable of producing great physical force; or, emotionally forceful, able, determined, unyielding.
Strong => "strong",
/// Experiencing a feeling caused by something unexpected.
Surprised => "surprised",
/// Showing appreciation or gratitude.
Thankful => "thankful",
/// Feeling the need to drink.
Thirsty => "thirsty",
/// In need of rest or sleep.
Tired => "tired",
/// [Feeling any emotion not defined here.]
Undefined => "undefined",
/// Lacking in force or ability, either physical or emotional.
Weak => "weak",
/// Thinking about unpleasant things that have happened or that might happen; feeling afraid and unhappy.
Worried => "worried",
}
);
generate_elem_id!(
/// Free-form text description of the mood.
Text,
"text",
MOOD
);
#[cfg(test)]
mod tests {
use super::*;
use crate::Element;
use std::convert::TryFrom;
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(MoodEnum, 1);
assert_size!(Text, 12);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(MoodEnum, 1);
assert_size!(Text, 24);
}
#[test]
fn test_simple() {
let elem: Element = "<happy xmlns='http://jabber.org/protocol/mood'/>"
.parse()
.unwrap();
let mood = MoodEnum::try_from(elem).unwrap();
assert_eq!(mood, MoodEnum::Happy);
}
#[test]
fn test_text() {
let elem: Element = "<text xmlns='http://jabber.org/protocol/mood'>Yay!</text>"
.parse()
.unwrap();
let elem2 = elem.clone();
let text = Text::try_from(elem).unwrap();
assert_eq!(text.0, String::from("Yay!"));
let elem3 = text.into();
assert_eq!(elem2, elem3);
}
}

View file

@ -0,0 +1,14 @@
// Copyright (c) 2017 Maxime “pep” Buquet <pep@bouah.net>
//
// 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/.
/// The http://jabber.org/protocol/muc protocol.
pub mod muc;
/// The http://jabber.org/protocol/muc#user protocol.
pub mod user;
pub use self::muc::Muc;
pub use self::user::MucUser;

197
xmpp-parsers/src/muc/muc.rs Normal file
View file

@ -0,0 +1,197 @@
// Copyright (c) 2017 Maxime “pep” Buquet <pep@bouah.net>
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 crate::date::DateTime;
use crate::presence::PresencePayload;
generate_element!(
/// Represents the query for messages before our join.
#[derive(PartialEq, Default)]
History, "history", MUC,
attributes: [
/// How many characters of history to send, in XML characters.
maxchars: Option<u32> = "maxchars",
/// How many messages to send.
maxstanzas: Option<u32> = "maxstanzas",
/// Only send messages received in these last seconds.
seconds: Option<u32> = "seconds",
/// Only send messages after this date.
since: Option<DateTime> = "since",
]
);
impl History {
/// Create a new empty history element.
pub fn new() -> Self {
History::default()
}
/// Set how many characters of history to send.
pub fn with_maxchars(mut self, maxchars: u32) -> Self {
self.maxchars = Some(maxchars);
self
}
/// Set how many messages to send.
pub fn with_maxstanzas(mut self, maxstanzas: u32) -> Self {
self.maxstanzas = Some(maxstanzas);
self
}
/// Only send messages received in these last seconds.
pub fn with_seconds(mut self, seconds: u32) -> Self {
self.seconds = Some(seconds);
self
}
/// Only send messages received since this date.
pub fn with_since(mut self, since: DateTime) -> Self {
self.since = Some(since);
self
}
}
generate_element!(
/// Represents a room join request.
#[derive(PartialEq, Default)]
Muc, "x", MUC, children: [
/// Password to use when the room is protected by a password.
password: Option<String> = ("password", MUC) => String,
/// Controls how much and how old we want to receive history on join.
history: Option<History> = ("history", MUC) => History
]
);
impl PresencePayload for Muc {}
impl Muc {
/// Create a new MUC join element.
pub fn new() -> Self {
Muc::default()
}
/// Join a room with this password.
pub fn with_password(mut self, password: String) -> Self {
self.password = Some(password);
self
}
/// Join a room with only that much history.
pub fn with_history(mut self, history: History) -> Self {
self.history = Some(history);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::util::compare_elements::NamespaceAwareCompare;
use crate::util::error::Error;
use crate::Element;
use std::str::FromStr;
use std::convert::TryFrom;
#[test]
fn test_muc_simple() {
let elem: Element = "<x xmlns='http://jabber.org/protocol/muc'/>"
.parse()
.unwrap();
Muc::try_from(elem).unwrap();
}
#[test]
fn test_muc_invalid_child() {
let elem: Element = "<x xmlns='http://jabber.org/protocol/muc'><coucou/></x>"
.parse()
.unwrap();
let error = Muc::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown child in x element.");
}
#[test]
fn test_muc_serialise() {
let elem: Element = "<x xmlns='http://jabber.org/protocol/muc'/>"
.parse()
.unwrap();
let muc = Muc {
password: None,
history: None,
};
let elem2 = muc.into();
assert_eq!(elem, elem2);
}
#[cfg(not(feature = "disable-validation"))]
#[test]
fn test_muc_invalid_attribute() {
let elem: Element = "<x xmlns='http://jabber.org/protocol/muc' coucou=''/>"
.parse()
.unwrap();
let error = Muc::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown attribute in x element.");
}
#[test]
fn test_muc_simple_password() {
let elem: Element = "
<x xmlns='http://jabber.org/protocol/muc'>
<password>coucou</password>
</x>"
.parse()
.unwrap();
let elem1 = elem.clone();
let muc = Muc::try_from(elem).unwrap();
assert_eq!(muc.password, Some("coucou".to_owned()));
let elem2 = Element::from(muc);
assert!(elem1.compare_to(&elem2));
}
#[test]
fn history() {
let elem: Element = "
<x xmlns='http://jabber.org/protocol/muc'>
<history maxstanzas='0'/>
</x>"
.parse()
.unwrap();
let muc = Muc::try_from(elem).unwrap();
let muc2 = Muc::new().with_history(History::new().with_maxstanzas(0));
assert_eq!(muc, muc2);
let history = muc.history.unwrap();
assert_eq!(history.maxstanzas, Some(0));
assert_eq!(history.maxchars, None);
assert_eq!(history.seconds, None);
assert_eq!(history.since, None);
let elem: Element = "
<x xmlns='http://jabber.org/protocol/muc'>
<history since='1970-01-01T00:00:00Z'/>
</x>"
.parse()
.unwrap();
let muc = Muc::try_from(elem).unwrap();
assert_eq!(
muc.history.unwrap().since.unwrap(),
DateTime::from_str("1970-01-01T00:00:00+00:00").unwrap()
);
}
}

View file

@ -0,0 +1,693 @@
// Copyright (c) 2017 Maxime “pep” Buquet <pep@bouah.net>
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 crate::util::error::Error;
use crate::ns;
use jid::FullJid;
use crate::Element;
use std::convert::TryFrom;
generate_attribute_enum!(
/// Lists all of the possible status codes used in MUC presences.
Status, "status", MUC_USER, "code", {
/// Inform user that any occupant is allowed to see the user's full JID
NonAnonymousRoom => 100,
/// Inform user that his or her affiliation changed while not in the room
AffiliationChange => 101,
/// Inform occupants that room now shows unavailable members
ConfigShowsUnavailableMembers => 102,
/// Inform occupants that room now does not show unavailable members
ConfigHidesUnavailableMembers => 103,
/// Inform occupants that a non-privacy-related room configuration change has occurred
ConfigNonPrivacyRelated => 104,
/// Inform user that presence refers to itself
SelfPresence => 110,
/// Inform occupants that room logging is now enabled
ConfigRoomLoggingEnabled => 170,
/// Inform occupants that room logging is now disabled
ConfigRoomLoggingDisabled => 171,
/// Inform occupants that the room is now non-anonymous
ConfigRoomNonAnonymous => 172,
/// Inform occupants that the room is now semi-anonymous
ConfigRoomSemiAnonymous => 173,
/// Inform user that a new room has been created
RoomHasBeenCreated => 201,
/// Inform user that service has assigned or modified occupant's roomnick
AssignedNick => 210,
/// Inform user that he or she has been banned from the room
Banned => 301,
/// Inform all occupants of new room nickname
NewNick => 303,
/// Inform user that he or she has been kicked from the room
Kicked => 307,
/// Inform user that he or she is being removed from the room
/// because of an affiliation change
RemovalFromRoom => 321,
/// Inform user that he or she is being removed from the room
/// because the room has been changed to members-only and the
/// user is not a member
ConfigMembersOnly => 322,
/// Inform user that he or she is being removed from the room
/// because the MUC service is being shut down
ServiceShutdown => 332,
});
/// Optional <actor/> element used in <item/> elements inside presence stanzas of type
/// "unavailable" that are sent to users who are kick or banned, as well as within IQs for tracking
/// purposes. -- CHANGELOG 0.17 (2002-10-23)
///
/// Possesses a 'jid' and a 'nick' attribute, so that an action can be attributed either to a real
/// JID or to a roomnick. -- CHANGELOG 1.25 (2012-02-08)
#[derive(Debug, Clone, PartialEq)]
pub enum Actor {
/// The full JID associated with this user.
Jid(FullJid),
/// The nickname of this user.
Nick(String),
}
impl TryFrom<Element> for Actor {
type Error = Error;
fn try_from(elem: Element) -> Result<Actor, Error> {
check_self!(elem, "actor", MUC_USER);
check_no_unknown_attributes!(elem, "actor", ["jid", "nick"]);
check_no_children!(elem, "actor");
let jid: Option<FullJid> = get_attr!(elem, "jid", Option);
let nick = get_attr!(elem, "nick", Option);
match (jid, nick) {
(Some(_), Some(_)) | (None, None) => {
Err(Error::ParseError(
"Either 'jid' or 'nick' attribute is required.",
))
}
(Some(jid), _) => Ok(Actor::Jid(jid)),
(_, Some(nick)) => Ok(Actor::Nick(nick)),
}
}
}
impl From<Actor> for Element {
fn from(actor: Actor) -> Element {
let elem = Element::builder("actor").ns(ns::MUC_USER);
(match actor {
Actor::Jid(jid) => elem.attr("jid", jid),
Actor::Nick(nick) => elem.attr("nick", nick),
})
.build()
}
}
generate_element!(
/// Used to continue a one-to-one discussion in a room, with more than one
/// participant.
Continue, "continue", MUC_USER,
attributes: [
/// The thread to continue in this room.
thread: Option<String> = "thread",
]
);
generate_elem_id!(
/// A reason for inviting, declining, etc. a request.
Reason,
"reason",
MUC_USER
);
generate_attribute!(
/// The affiliation of an entity with a room, which isnt tied to its
/// presence in it.
Affiliation, "affiliation", {
/// The user who created the room, or who got appointed by its creator
/// to be their equal.
Owner => "owner",
/// A user who has been empowered by an owner to do administrative
/// operations.
Admin => "admin",
/// A user who is whitelisted to speak in moderated rooms, or to join a
/// member-only room.
Member => "member",
/// A user who has been banned from this room.
Outcast => "outcast",
/// A normal participant.
None => "none",
}, Default = None
);
generate_attribute!(
/// The current role of an entity in a room, it can be changed by an owner
/// or an administrator but will be lost once they leave the room.
Role, "role", {
/// This user can kick other participants, as well as grant and revoke
/// them voice.
Moderator => "moderator",
/// A user who can speak in this room.
Participant => "participant",
/// A user who cannot speak in this room, and must request voice before
/// doing so.
Visitor => "visitor",
/// A user who is absent from the room.
None => "none",
}, Default = None
);
generate_element!(
/// An item representing a user in a room.
Item, "item", MUC_USER, attributes: [
/// The affiliation of this user with the room.
affiliation: Required<Affiliation> = "affiliation",
/// The real JID of this user, if you are allowed to see it.
jid: Option<FullJid> = "jid",
/// The current nickname of this user.
nick: Option<String> = "nick",
/// The current role of this user.
role: Required<Role> = "role",
], children: [
/// The actor affected by this item.
actor: Option<Actor> = ("actor", MUC_USER) => Actor,
/// Whether this continues a one-to-one discussion.
continue_: Option<Continue> = ("continue", MUC_USER) => Continue,
/// A reason for this item.
reason: Option<Reason> = ("reason", MUC_USER) => Reason
]
);
impl Item {
/// Creates a new item with the given affiliation and role.
pub fn new(affiliation: Affiliation, role: Role) -> Item {
Item {
affiliation,
role,
jid: None,
nick: None,
actor: None,
continue_: None,
reason: None,
}
}
}
generate_element!(
/// The main muc#user element.
MucUser, "x", MUC_USER, children: [
/// List of statuses applying to this item.
status: Vec<Status> = ("status", MUC_USER) => Status,
/// List of items.
items: Vec<Item> = ("item", MUC_USER) => Item
]
);
#[cfg(test)]
mod tests {
use super::*;
use crate::util::compare_elements::NamespaceAwareCompare;
use std::error::Error as StdError;
#[test]
fn test_simple() {
let elem: Element = "
<x xmlns='http://jabber.org/protocol/muc#user'/>
"
.parse()
.unwrap();
MucUser::try_from(elem).unwrap();
}
#[test]
fn statuses_and_items() {
let elem: Element = "
<x xmlns='http://jabber.org/protocol/muc#user'>
<status code='101'/>
<status code='102'/>
<item affiliation='member' role='moderator'/>
</x>
"
.parse()
.unwrap();
let muc_user = MucUser::try_from(elem).unwrap();
assert_eq!(muc_user.status.len(), 2);
assert_eq!(muc_user.status[0], Status::AffiliationChange);
assert_eq!(muc_user.status[1], Status::ConfigShowsUnavailableMembers);
assert_eq!(muc_user.items.len(), 1);
assert_eq!(muc_user.items[0].affiliation, Affiliation::Member);
assert_eq!(muc_user.items[0].role, Role::Moderator);
}
#[test]
fn test_invalid_child() {
let elem: Element = "
<x xmlns='http://jabber.org/protocol/muc#user'>
<coucou/>
</x>
"
.parse()
.unwrap();
let error = MucUser::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown child in x element.");
}
#[test]
fn test_serialise() {
let elem: Element = "
<x xmlns='http://jabber.org/protocol/muc#user'/>
"
.parse()
.unwrap();
let muc = MucUser {
status: vec![],
items: vec![],
};
let elem2 = muc.into();
assert!(elem.compare_to(&elem2));
}
#[cfg(not(feature = "disable-validation"))]
#[test]
fn test_invalid_attribute() {
let elem: Element = "
<x xmlns='http://jabber.org/protocol/muc#user' coucou=''/>
"
.parse()
.unwrap();
let error = MucUser::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown attribute in x element.");
}
#[test]
fn test_status_simple() {
let elem: Element = "
<status xmlns='http://jabber.org/protocol/muc#user' code='110'/>
"
.parse()
.unwrap();
Status::try_from(elem).unwrap();
}
#[test]
fn test_status_invalid() {
let elem: Element = "
<status xmlns='http://jabber.org/protocol/muc#user'/>
"
.parse()
.unwrap();
let error = Status::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Required attribute 'code' missing.");
}
#[cfg(not(feature = "disable-validation"))]
#[test]
fn test_status_invalid_child() {
let elem: Element = "
<status xmlns='http://jabber.org/protocol/muc#user' code='110'>
<foo/>
</status>
"
.parse()
.unwrap();
let error = Status::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown child in status element.");
}
#[test]
fn test_status_simple_code() {
let elem: Element = "
<status xmlns='http://jabber.org/protocol/muc#user' code='307'/>
"
.parse()
.unwrap();
let status = Status::try_from(elem).unwrap();
assert_eq!(status, Status::Kicked);
}
#[test]
fn test_status_invalid_code() {
let elem: Element = "
<status xmlns='http://jabber.org/protocol/muc#user' code='666'/>
"
.parse()
.unwrap();
let error = Status::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Invalid status code value.");
}
#[test]
fn test_status_invalid_code2() {
let elem: Element = "
<status xmlns='http://jabber.org/protocol/muc#user' code='coucou'/>
"
.parse()
.unwrap();
let error = Status::try_from(elem).unwrap_err();
let error = match error {
Error::ParseIntError(error) => error,
_ => panic!(),
};
assert_eq!(error.description(), "invalid digit found in string");
}
#[test]
fn test_actor_required_attributes() {
let elem: Element = "
<actor xmlns='http://jabber.org/protocol/muc#user'/>
"
.parse()
.unwrap();
let error = Actor::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Either 'jid' or 'nick' attribute is required.");
}
#[test]
fn test_actor_required_attributes2() {
let elem: Element = "
<actor xmlns='http://jabber.org/protocol/muc#user'
jid='foo@bar/baz'
nick='baz'/>
"
.parse()
.unwrap();
let error = Actor::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Either 'jid' or 'nick' attribute is required.");
}
#[test]
fn test_actor_jid() {
let elem: Element = "
<actor xmlns='http://jabber.org/protocol/muc#user'
jid='foo@bar/baz'/>
"
.parse()
.unwrap();
let actor = Actor::try_from(elem).unwrap();
let jid = match actor {
Actor::Jid(jid) => jid,
_ => panic!(),
};
assert_eq!(jid, "foo@bar/baz".parse::<FullJid>().unwrap());
}
#[test]
fn test_actor_nick() {
let elem: Element = "
<actor xmlns='http://jabber.org/protocol/muc#user' nick='baz'/>
"
.parse()
.unwrap();
let actor = Actor::try_from(elem).unwrap();
let nick = match actor {
Actor::Nick(nick) => nick,
_ => panic!(),
};
assert_eq!(nick, "baz".to_owned());
}
#[test]
fn test_continue_simple() {
let elem: Element = "
<continue xmlns='http://jabber.org/protocol/muc#user'/>
"
.parse()
.unwrap();
Continue::try_from(elem).unwrap();
}
#[test]
fn test_continue_thread_attribute() {
let elem: Element = "
<continue xmlns='http://jabber.org/protocol/muc#user'
thread='foo'/>
"
.parse()
.unwrap();
let continue_ = Continue::try_from(elem).unwrap();
assert_eq!(continue_.thread, Some("foo".to_owned()));
}
#[test]
fn test_continue_invalid() {
let elem: Element = "
<continue xmlns='http://jabber.org/protocol/muc#user'>
<foobar/>
</continue>
"
.parse()
.unwrap();
let continue_ = Continue::try_from(elem).unwrap_err();
let message = match continue_ {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown child in continue element.".to_owned());
}
#[test]
fn test_reason_simple() {
let elem: Element = "
<reason xmlns='http://jabber.org/protocol/muc#user'>Reason</reason>"
.parse()
.unwrap();
let elem2 = elem.clone();
let reason = Reason::try_from(elem).unwrap();
assert_eq!(reason.0, "Reason".to_owned());
let elem3 = reason.into();
assert_eq!(elem2, elem3);
}
#[cfg(not(feature = "disable-validation"))]
#[test]
fn test_reason_invalid_attribute() {
let elem: Element = "
<reason xmlns='http://jabber.org/protocol/muc#user' foo='bar'/>
"
.parse()
.unwrap();
let error = Reason::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown attribute in reason element.".to_owned());
}
#[cfg(not(feature = "disable-validation"))]
#[test]
fn test_reason_invalid() {
let elem: Element = "
<reason xmlns='http://jabber.org/protocol/muc#user'>
<foobar/>
</reason>
"
.parse()
.unwrap();
let error = Reason::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown child in reason element.".to_owned());
}
#[cfg(not(feature = "disable-validation"))]
#[test]
fn test_item_invalid_attr() {
let elem: Element = "
<item xmlns='http://jabber.org/protocol/muc#user'
foo='bar'/>
"
.parse()
.unwrap();
let error = Item::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown attribute in item element.".to_owned());
}
#[test]
fn test_item_affiliation_role_attr() {
let elem: Element = "
<item xmlns='http://jabber.org/protocol/muc#user'
affiliation='member'
role='moderator'/>
"
.parse()
.unwrap();
Item::try_from(elem).unwrap();
}
#[test]
fn test_item_affiliation_role_invalid_attr() {
let elem: Element = "
<item xmlns='http://jabber.org/protocol/muc#user'
affiliation='member'/>
"
.parse()
.unwrap();
let error = Item::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Required attribute 'role' missing.".to_owned());
}
#[test]
fn test_item_nick_attr() {
let elem: Element = "
<item xmlns='http://jabber.org/protocol/muc#user'
affiliation='member'
role='moderator'
nick='foobar'/>
"
.parse()
.unwrap();
let item = Item::try_from(elem).unwrap();
match item {
Item { nick, .. } => assert_eq!(nick, Some("foobar".to_owned())),
}
}
#[test]
fn test_item_affiliation_role_invalid_attr2() {
let elem: Element = "
<item xmlns='http://jabber.org/protocol/muc#user'
role='moderator'/>
"
.parse()
.unwrap();
let error = Item::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(
message,
"Required attribute 'affiliation' missing.".to_owned()
);
}
#[test]
fn test_item_role_actor_child() {
let elem: Element = "
<item xmlns='http://jabber.org/protocol/muc#user'
affiliation='member'
role='moderator'>
<actor nick='foobar'/>
</item>
"
.parse()
.unwrap();
let item = Item::try_from(elem).unwrap();
match item {
Item { actor, .. } => assert_eq!(actor, Some(Actor::Nick("foobar".to_owned()))),
}
}
#[test]
fn test_item_role_continue_child() {
let elem: Element = "
<item xmlns='http://jabber.org/protocol/muc#user'
affiliation='member'
role='moderator'>
<continue thread='foobar'/>
</item>
"
.parse()
.unwrap();
let item = Item::try_from(elem).unwrap();
let continue_1 = Continue {
thread: Some("foobar".to_owned()),
};
match item {
Item {
continue_: Some(continue_2),
..
} => assert_eq!(continue_2.thread, continue_1.thread),
_ => panic!(),
}
}
#[test]
fn test_item_role_reason_child() {
let elem: Element = "
<item xmlns='http://jabber.org/protocol/muc#user'
affiliation='member'
role='moderator'>
<reason>foobar</reason>
</item>
"
.parse()
.unwrap();
let item = Item::try_from(elem).unwrap();
match item {
Item { reason, .. } => assert_eq!(reason, Some(Reason("foobar".to_owned()))),
}
}
}

79
xmpp-parsers/src/nick.rs Normal file
View file

@ -0,0 +1,79 @@
// Copyright (c) 2018 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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/.
generate_elem_id!(
/// Represents a global, memorable, friendly or informal name chosen by a user.
Nick,
"nick",
NICK
);
#[cfg(test)]
mod tests {
use super::*;
#[cfg(not(feature = "disable-validation"))]
use crate::util::error::Error;
use crate::Element;
use std::convert::TryFrom;
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(Nick, 12);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(Nick, 24);
}
#[test]
fn test_simple() {
let elem: Element = "<nick xmlns='http://jabber.org/protocol/nick'>Link Mauve</nick>"
.parse()
.unwrap();
let nick = Nick::try_from(elem).unwrap();
assert_eq!(&nick.0, "Link Mauve");
}
#[test]
fn test_serialise() {
let elem1 = Element::from(Nick(String::from("Link Mauve")));
let elem2: Element = "<nick xmlns='http://jabber.org/protocol/nick'>Link Mauve</nick>"
.parse()
.unwrap();
assert_eq!(elem1, elem2);
}
#[cfg(not(feature = "disable-validation"))]
#[test]
fn test_invalid() {
let elem: Element = "<nick xmlns='http://jabber.org/protocol/nick'><coucou/></nick>"
.parse()
.unwrap();
let error = Nick::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown child in nick element.");
}
#[cfg(not(feature = "disable-validation"))]
#[test]
fn test_invalid_attribute() {
let elem: Element = "<nick xmlns='http://jabber.org/protocol/nick' coucou=''/>"
.parse()
.unwrap();
let error = Nick::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown attribute in nick element.");
}
}

235
xmpp-parsers/src/ns.rs Normal file
View file

@ -0,0 +1,235 @@
// Copyright (c) 2017-2018 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
// Copyright (c) 2017 Maxime “pep” Buquet <pep@bouah.net>
//
// 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/.
/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core
pub const JABBER_CLIENT: &str = "jabber:client";
/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core
pub const XMPP_STANZAS: &str = "urn:ietf:params:xml:ns:xmpp-stanzas";
/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core
pub const STREAM: &str = "http://etherx.jabber.org/streams";
/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core
pub const SASL: &str = "urn:ietf:params:xml:ns:xmpp-sasl";
/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core
pub const BIND: &str = "urn:ietf:params:xml:ns:xmpp-bind";
/// RFC 6121: Extensible Messaging and Presence Protocol (XMPP): Instant Messaging and Presence
pub const ROSTER: &str = "jabber:iq:roster";
/// RFC 7395: An Extensible Messaging and Presence Protocol (XMPP) Subprotocol for WebSocket
pub const WEBSOCKET: &str = "urn:ietf:params:xml:ns:xmpp-framing";
/// XEP-0004: Data Forms
pub const DATA_FORMS: &str = "jabber:x:data";
/// XEP-0030: Service Discovery
pub const DISCO_INFO: &str = "http://jabber.org/protocol/disco#info";
/// XEP-0030: Service Discovery
pub const DISCO_ITEMS: &str = "http://jabber.org/protocol/disco#items";
/// XEP-0045: Multi-User Chat
pub const MUC: &str = "http://jabber.org/protocol/muc";
/// XEP-0045: Multi-User Chat
pub const MUC_USER: &str = "http://jabber.org/protocol/muc#user";
/// XEP-0047: In-Band Bytestreams
pub const IBB: &str = "http://jabber.org/protocol/ibb";
/// XEP-0048: Bookmarks
pub const BOOKMARKS: &str = "storage:bookmarks";
/// XEP-0059: Result Set Management
pub const RSM: &str = "http://jabber.org/protocol/rsm";
/// XEP-0060: Publish-Subscribe
pub const PUBSUB: &str = "http://jabber.org/protocol/pubsub";
/// XEP-0060: Publish-Subscribe
pub const PUBSUB_ERRORS: &str = "http://jabber.org/protocol/pubsub#errors";
/// XEP-0060: Publish-Subscribe
pub const PUBSUB_EVENT: &str = "http://jabber.org/protocol/pubsub#event";
/// XEP-0060: Publish-Subscribe
pub const PUBSUB_OWNER: &str = "http://jabber.org/protocol/pubsub#owner";
/// XEP-0071: XHTML-IM
pub const XHTML_IM: &str = "http://jabber.org/protocol/xhtml-im";
/// XEP-0071: XHTML-IM
pub const XHTML: &str = "http://www.w3.org/1999/xhtml";
/// XEP-0077: In-Band Registration
pub const REGISTER: &str = "jabber:iq:register";
/// XEP-0084: User Avatar
pub const AVATAR_DATA: &str = "urn:xmpp:avatar:data";
/// XEP-0084: User Avatar
pub const AVATAR_METADATA: &str = "urn:xmpp:avatar:metadata";
/// XEP-0085: Chat State Notifications
pub const CHATSTATES: &str = "http://jabber.org/protocol/chatstates";
/// XEP-0092: Software Version
pub const VERSION: &str = "jabber:iq:version";
/// XEP-0107: User Mood
pub const MOOD: &str = "http://jabber.org/protocol/mood";
/// XEP-0114: Jabber Component Protocol
pub const COMPONENT_ACCEPT: &str = "jabber:component:accept";
/// XEP-0114: Jabber Component Protocol
pub const COMPONENT: &str = "jabber:component:accept";
/// XEP-0115: Entity Capabilities
pub const CAPS: &str = "http://jabber.org/protocol/caps";
/// XEP-0118: User Tune
pub const TUNE: &str = "http://jabber.org/protocol/tune";
/// XEP-0157: Contact Addresses for XMPP Services
pub const SERVER_INFO: &str = "http://jabber.org/network/serverinfo";
/// XEP-0166: Jingle
pub const JINGLE: &str = "urn:xmpp:jingle:1";
/// XEP-0167: Jingle RTP Sessions
pub const JINGLE_RTP: &str = "urn:xmpp:jingle:apps:rtp:1";
/// XEP-0167: Jingle RTP Sessions
pub const JINGLE_RTP_AUDIO: &str = "urn:xmpp:jingle:apps:rtp:audio";
/// XEP-0167: Jingle RTP Sessions
pub const JINGLE_RTP_VIDEO: &str = "urn:xmpp:jingle:apps:rtp:video";
/// XEP-0172: User Nickname
pub const NICK: &str = "http://jabber.org/protocol/nick";
/// XEP-0176: Jingle ICE-UDP Transport Method
pub const JINGLE_ICE_UDP: &str = "urn:xmpp:jingle:transports:ice-udp:1";
/// XEP-0184: Message Delivery Receipts
pub const RECEIPTS: &str = "urn:xmpp:receipts";
/// XEP-0191: Blocking Command
pub const BLOCKING: &str = "urn:xmpp:blocking";
/// XEP-0191: Blocking Command
pub const BLOCKING_ERRORS: &str = "urn:xmpp:blocking:errors";
/// XEP-0198: Stream Management
pub const SM: &str = "urn:xmpp:sm:3";
/// XEP-0199: XMPP Ping
pub const PING: &str = "urn:xmpp:ping";
/// XEP-0202: Entity Time
pub const TIME: &str = "urn:xmpp:time";
/// XEP-0203: Delayed Delivery
pub const DELAY: &str = "urn:xmpp:delay";
/// XEP-0221: Data Forms Media Element
pub const MEDIA_ELEMENT: &str = "urn:xmpp:media-element";
/// XEP-0224: Attention
pub const ATTENTION: &str = "urn:xmpp:attention:0";
/// XEP-0231: Bits of Binary
pub const BOB: &str = "urn:xmpp:bob";
/// XEP-0234: Jingle File Transfer
pub const JINGLE_FT: &str = "urn:xmpp:jingle:apps:file-transfer:5";
/// XEP-0234: Jingle File Transfer
pub const JINGLE_FT_ERROR: &str = "urn:xmpp:jingle:apps:file-transfer:errors:0";
/// XEP-0257: Client Certificate Management for SASL EXTERNAL
pub const SASL_CERT: &str = "urn:xmpp:saslcert:1";
/// XEP-0260: Jingle SOCKS5 Bytestreams Transport Method
pub const JINGLE_S5B: &str = "urn:xmpp:jingle:transports:s5b:1";
/// XEP-0261: Jingle In-Band Bytestreams Transport Method
pub const JINGLE_IBB: &str = "urn:xmpp:jingle:transports:ibb:1";
/// XEP-0277: Microblogging over XMPP
pub const MICROBLOG: &str = "urn:xmpp:microblog:0";
/// XEP-0280: Message Carbons
pub const CARBONS: &str = "urn:xmpp:carbons:2";
/// XEP-0293: Jingle RTP Feedback Negotiation
pub const JINGLE_RTCP_FB: &str = "urn:xmpp:jingle:apps:rtp:rtcp-fb:0";
/// XEP-0297: Stanza Forwarding
pub const FORWARD: &str = "urn:xmpp:forward:0";
/// XEP-0300: Use of Cryptographic Hash Functions in XMPP
pub const HASHES: &str = "urn:xmpp:hashes:2";
/// XEP-0300: Use of Cryptographic Hash Functions in XMPP
pub const HASH_ALGO_SHA_256: &str = "urn:xmpp:hash-function-text-names:sha-256";
/// XEP-0300: Use of Cryptographic Hash Functions in XMPP
pub const HASH_ALGO_SHA_512: &str = "urn:xmpp:hash-function-text-names:sha-512";
/// XEP-0300: Use of Cryptographic Hash Functions in XMPP
pub const HASH_ALGO_SHA3_256: &str = "urn:xmpp:hash-function-text-names:sha3-256";
/// XEP-0300: Use of Cryptographic Hash Functions in XMPP
pub const HASH_ALGO_SHA3_512: &str = "urn:xmpp:hash-function-text-names:sha3-512";
/// XEP-0300: Use of Cryptographic Hash Functions in XMPP
pub const HASH_ALGO_BLAKE2B_256: &str = "urn:xmpp:hash-function-text-names:id-blake2b256";
/// XEP-0300: Use of Cryptographic Hash Functions in XMPP
pub const HASH_ALGO_BLAKE2B_512: &str = "urn:xmpp:hash-function-text-names:id-blake2b512";
/// XEP-0308: Last Message Correction
pub const MESSAGE_CORRECT: &str = "urn:xmpp:message-correct:0";
/// XEP-0313: Message Archive Management
pub const MAM: &str = "urn:xmpp:mam:2";
/// XEP-0319: Last User Interaction in Presence
pub const IDLE: &str = "urn:xmpp:idle:1";
/// XEP-0320: Use of DTLS-SRTP in Jingle Sessions
pub const JINGLE_DTLS: &str = "urn:xmpp:jingle:apps:dtls:0";
/// XEP-0328: JID Prep
pub const JID_PREP: &str = "urn:xmpp:jidprep:0";
/// XEP-0339: Source-Specific Media Attributes in Jingle
pub const JINGLE_SSMA: &str = "urn:xmpp:jingle:apps:rtp:ssma:0";
/// XEP-0352: Client State Indication
pub const CSI: &str = "urn:xmpp:csi:0";
/// XEP-0353: Jingle Message Initiation
pub const JINGLE_MESSAGE: &str = "urn:xmpp:jingle-message:0";
/// XEP-0359: Unique and Stable Stanza IDs
pub const SID: &str = "urn:xmpp:sid:0";
/// XEP-0373: OpenPGP for XMPP
pub const OX: &str = "urn:xmpp:openpgp:0";
/// XEP-0373: OpenPGP for XMPP
pub const OX_PUBKEYS: &str = "urn:xmpp:openpgp:0:public-keys";
/// XEP-0380: Explicit Message Encryption
pub const EME: &str = "urn:xmpp:eme:0";
/// XEP-0390: Entity Capabilities 2.0
pub const ECAPS2: &str = "urn:xmpp:caps";
/// XEP-0390: Entity Capabilities 2.0
pub const ECAPS2_OPTIMIZE: &str = "urn:xmpp:caps:optimize";
/// XEP-0402: Bookmarks 2 (This Time it's Serious)
pub const BOOKMARKS2: &str = "urn:xmpp:bookmarks:0";
/// XEP-0402: Bookmarks 2 (This Time it's Serious)
pub const BOOKMARKS2_COMPAT: &str = "urn:xmpp:bookmarks:0#compat";
/// XEP-0421: Anonymous unique occupant identifiers for MUCs
pub const OID: &str = "urn:xmpp:occupant-id:0";
/// Alias for the main namespace of the stream, that is "jabber:client" when
/// the component feature isnt enabled.
#[cfg(not(feature = "component"))]
pub const DEFAULT_NS: &str = JABBER_CLIENT;
/// Alias for the main namespace of the stream, that is
/// "jabber:component:accept" when the component feature is enabled.
#[cfg(feature = "component")]
pub const DEFAULT_NS: &str = COMPONENT_ACCEPT;

View file

@ -0,0 +1,89 @@
// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 crate::message::MessagePayload;
use crate::presence::PresencePayload;
generate_element!(
/// Unique identifier given to a MUC participant.
///
/// It allows clients to identify a MUC participant across reconnects and
/// renames. It thus prevents impersonification of anonymous users.
OccupantId, "occupant-id", OID,
attributes: [
/// The id associated to the sending user by the MUC service.
id: Required<String> = "id",
]
);
impl MessagePayload for OccupantId {}
impl PresencePayload for OccupantId {}
#[cfg(test)]
mod tests {
use super::*;
use crate::util::error::Error;
use crate::Element;
use std::convert::TryFrom;
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(OccupantId, 12);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(OccupantId, 24);
}
#[test]
fn test_simple() {
let elem: Element = "<occupant-id xmlns='urn:xmpp:occupant-id:0' id='coucou'/>"
.parse()
.unwrap();
let origin_id = OccupantId::try_from(elem).unwrap();
assert_eq!(origin_id.id, "coucou");
}
#[test]
fn test_invalid_child() {
let elem: Element = "<occupant-id xmlns='urn:xmpp:occupant-id:0'><coucou/></occupant-id>"
.parse()
.unwrap();
let error = OccupantId::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown child in occupant-id element.");
}
#[test]
fn test_invalid_id() {
let elem: Element = "<occupant-id xmlns='urn:xmpp:occupant-id:0'/>".parse().unwrap();
let error = OccupantId::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Required attribute 'id' missing.");
}
#[test]
fn test_serialise() {
let elem: Element = "<occupant-id xmlns='urn:xmpp:occupant-id:0' id='coucou'/>"
.parse()
.unwrap();
let occupant_id = OccupantId {
id: String::from("coucou"),
};
let elem2 = occupant_id.into();
assert_eq!(elem, elem2);
}
}

104
xmpp-parsers/src/openpgp.rs Normal file
View file

@ -0,0 +1,104 @@
// Copyright (c) 2019 Maxime “pep” Buquet <pep@bouah.net>
//
// 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 crate::date::DateTime;
use crate::util::helpers::Base64;
use crate::pubsub::PubSubPayload;
// TODO: Merge this container with the PubKey struct
generate_element!(
/// Data contained in the PubKey element
PubKeyData, "data", OX,
text: (
/// Base64 data
data: Base64<Vec<u8>>
)
);
generate_element!(
/// Pubkey element to be used in PubSub publish payloads.
PubKey, "pubkey", OX,
attributes: [
/// Last updated date
date: Option<DateTime> = "date"
],
children: [
/// Public key as base64 data
data: Required<PubKeyData> = ("data", OX) => PubKeyData
]
);
impl PubSubPayload for PubKey {}
generate_element!(
/// Public key metadata
PubKeyMeta, "pubkey-metadata", OX,
attributes: [
/// OpenPGP v4 fingerprint
v4fingerprint: Required<String> = "v4-fingerprint",
/// Time the key was published or updated
date: Required<DateTime> = "date",
]
);
generate_element!(
/// List of public key metadata
PubKeysMeta, "public-key-list", OX,
children: [
/// Public keys
pubkeys: Vec<PubKeyMeta> = ("pubkey-metadata", OX) => PubKeyMeta
]
);
impl PubSubPayload for PubKeysMeta {}
#[cfg(test)]
mod tests {
use super::*;
use crate::ns;
use std::str::FromStr;
use crate::pubsub::{NodeName, Item, pubsub::{Item as PubSubItem, Publish}};
#[test]
fn pubsub_publish_pubkey_data() {
let pubkey = PubKey {
date: None,
data: PubKeyData {
data: (&"Foo").as_bytes().to_vec(),
}
};
println!("Foo1: {:?}", pubkey);
let pubsub = Publish {
node: NodeName(format!("{}:{}", ns::OX_PUBKEYS, "some-fingerprint")),
items: vec![
PubSubItem(Item::new(None, None, Some(pubkey))),
],
};
println!("Foo2: {:?}", pubsub);
}
#[test]
fn pubsub_publish_pubkey_meta() {
let pubkeymeta = PubKeysMeta {
pubkeys: vec![
PubKeyMeta {
v4fingerprint: "some-fingerprint".to_owned(),
date: DateTime::from_str("2019-03-30T18:30:25Z").unwrap(),
},
],
};
println!("Foo1: {:?}", pubkeymeta);
let pubsub = Publish {
node: NodeName("foo".to_owned()),
items: vec![
PubSubItem(Item::new(None, None, Some(pubkeymeta))),
],
};
println!("Foo2: {:?}", pubsub);
}
}

71
xmpp-parsers/src/ping.rs Normal file
View file

@ -0,0 +1,71 @@
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
// Copyright (c) 2017 Maxime “pep” Buquet <pep@bouah.net>
//
// 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 crate::iq::IqGetPayload;
generate_empty_element!(
/// Represents a ping to the recipient, which must be answered with an
/// empty `<iq/>` or with an error.
Ping,
"ping",
PING
);
impl IqGetPayload for Ping {}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(not(feature = "disable-validation"))]
use crate::util::error::Error;
use crate::Element;
use std::convert::TryFrom;
#[test]
fn test_size() {
assert_size!(Ping, 0);
}
#[test]
fn test_simple() {
let elem: Element = "<ping xmlns='urn:xmpp:ping'/>".parse().unwrap();
Ping::try_from(elem).unwrap();
}
#[test]
fn test_serialise() {
let elem1 = Element::from(Ping);
let elem2: Element = "<ping xmlns='urn:xmpp:ping'/>".parse().unwrap();
assert_eq!(elem1, elem2);
}
#[cfg(not(feature = "disable-validation"))]
#[test]
fn test_invalid() {
let elem: Element = "<ping xmlns='urn:xmpp:ping'><coucou/></ping>"
.parse()
.unwrap();
let error = Ping::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown child in ping element.");
}
#[cfg(not(feature = "disable-validation"))]
#[test]
fn test_invalid_attribute() {
let elem: Element = "<ping xmlns='urn:xmpp:ping' coucou=''/>".parse().unwrap();
let error = Ping::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown attribute in ping element.");
}
}

View file

@ -0,0 +1,661 @@
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
// Copyright (c) 2017 Maxime “pep” Buquet <pep@bouah.net>
//
// 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 crate::util::error::Error;
use crate::ns;
use jid::Jid;
use minidom::{Element, IntoAttributeValue, Node};
use std::collections::BTreeMap;
use std::str::FromStr;
use std::convert::TryFrom;
/// Should be implemented on every known payload of a `<presence/>`.
pub trait PresencePayload: TryFrom<Element> + Into<Element> {}
/// Specifies the availability of an entity or resource.
#[derive(Debug, Clone, PartialEq)]
pub enum Show {
/// The entity or resource is temporarily away.
Away,
/// The entity or resource is actively interested in chatting.
Chat,
/// The entity or resource is busy (dnd = "Do Not Disturb").
Dnd,
/// The entity or resource is away for an extended period (xa = "eXtended
/// Away").
Xa,
}
impl FromStr for Show {
type Err = Error;
fn from_str(s: &str) -> Result<Show, Error> {
Ok(match s {
"away" => Show::Away,
"chat" => Show::Chat,
"dnd" => Show::Dnd,
"xa" => Show::Xa,
_ => return Err(Error::ParseError("Invalid value for show.")),
})
}
}
impl Into<Node> for Show {
fn into(self) -> Node {
Element::builder("show")
.append(match self {
Show::Away => "away",
Show::Chat => "chat",
Show::Dnd => "dnd",
Show::Xa => "xa",
})
.build()
.into()
}
}
type Lang = String;
type Status = String;
type Priority = i8;
///
#[derive(Debug, Clone, PartialEq)]
pub enum Type {
/// This value is not an acceptable 'type' attribute, it is only used
/// internally to signal the absence of 'type'.
None,
/// An error has occurred regarding processing of a previously sent
/// presence stanza; if the presence stanza is of type "error", it MUST
/// include an <error/> child element (refer to [XMPPCORE]).
Error,
/// A request for an entity's current presence; SHOULD be generated only by
/// a server on behalf of a user.
Probe,
/// The sender wishes to subscribe to the recipient's presence.
Subscribe,
/// The sender has allowed the recipient to receive their presence.
Subscribed,
/// The sender is no longer available for communication.
Unavailable,
/// The sender is unsubscribing from the receiver's presence.
Unsubscribe,
/// The subscription request has been denied or a previously granted
/// subscription has been canceled.
Unsubscribed,
}
impl Default for Type {
fn default() -> Type {
Type::None
}
}
impl FromStr for Type {
type Err = Error;
fn from_str(s: &str) -> Result<Type, Error> {
Ok(match s {
"error" => Type::Error,
"probe" => Type::Probe,
"subscribe" => Type::Subscribe,
"subscribed" => Type::Subscribed,
"unavailable" => Type::Unavailable,
"unsubscribe" => Type::Unsubscribe,
"unsubscribed" => Type::Unsubscribed,
_ => {
return Err(Error::ParseError(
"Invalid 'type' attribute on presence element.",
));
}
})
}
}
impl IntoAttributeValue for Type {
fn into_attribute_value(self) -> Option<String> {
Some(
match self {
Type::None => return None,
Type::Error => "error",
Type::Probe => "probe",
Type::Subscribe => "subscribe",
Type::Subscribed => "subscribed",
Type::Unavailable => "unavailable",
Type::Unsubscribe => "unsubscribe",
Type::Unsubscribed => "unsubscribed",
}
.to_owned(),
)
}
}
/// The main structure representing the `<presence/>` stanza.
#[derive(Debug, Clone)]
pub struct Presence {
/// The sender of this presence.
pub from: Option<Jid>,
/// The recipient of this presence.
pub to: Option<Jid>,
/// The identifier, unique on this stream, of this stanza.
pub id: Option<String>,
/// The type of this presence stanza.
pub type_: Type,
/// The availability of the sender of this presence.
pub show: Option<Show>,
/// A localised list of statuses defined in this presence.
pub statuses: BTreeMap<Lang, Status>,
/// The senders resource priority, if negative it wont receive messages
/// that havent been directed to it.
pub priority: Priority,
/// A list of payloads contained in this presence.
pub payloads: Vec<Element>,
}
impl Presence {
/// Create a new presence of this type.
pub fn new(type_: Type) -> Presence {
Presence {
from: None,
to: None,
id: None,
type_,
show: None,
statuses: BTreeMap::new(),
priority: 0i8,
payloads: vec![],
}
}
/// Set the emitter of this presence, this should only be useful for
/// servers and components, as clients can only send presences from their
/// own resource (which is implicit).
pub fn with_from<J: Into<Jid>>(mut self, from: J) -> Presence {
self.from = Some(from.into());
self
}
/// Set the recipient of this presence, this is only useful for directed
/// presences.
pub fn with_to<J: Into<Jid>>(mut self, to: J) -> Presence {
self.to = Some(to.into());
self
}
/// Set the identifier for this presence.
pub fn with_id(mut self, id: String) -> Presence {
self.id = Some(id);
self
}
/// Set the availability information of this presence.
pub fn with_show(mut self, show: Show) -> Presence {
self.show = Some(show);
self
}
/// Set the priority of this presence.
pub fn with_priority(mut self, priority: i8) -> Presence {
self.priority = priority;
self
}
/// Set the payloads of this presence.
pub fn with_payloads(mut self, payloads: Vec<Element>) -> Presence {
self.payloads = payloads;
self
}
/// Set the availability information of this presence.
pub fn set_status<L, S>(&mut self, lang: L, status: S)
where L: Into<Lang>,
S: Into<Status>,
{
self.statuses.insert(lang.into(), status.into());
}
/// Add a payload to this presence.
pub fn add_payload<P: PresencePayload>(&mut self, payload: P) {
self.payloads.push(payload.into());
}
}
impl TryFrom<Element> for Presence {
type Error = Error;
fn try_from(root: Element) -> Result<Presence, Error> {
check_self!(root, "presence", DEFAULT_NS);
let mut show = None;
let mut priority = None;
let mut presence = Presence {
from: get_attr!(root, "from", Option),
to: get_attr!(root, "to", Option),
id: get_attr!(root, "id", Option),
type_: get_attr!(root, "type", Default),
show: None,
statuses: BTreeMap::new(),
priority: 0i8,
payloads: vec![],
};
for elem in root.children() {
if elem.is("show", ns::DEFAULT_NS) {
if show.is_some() {
return Err(Error::ParseError(
"More than one show element in a presence.",
));
}
check_no_attributes!(elem, "show");
check_no_children!(elem, "show");
show = Some(Show::from_str(elem.text().as_ref())?);
} else if elem.is("status", ns::DEFAULT_NS) {
check_no_unknown_attributes!(elem, "status", ["xml:lang"]);
check_no_children!(elem, "status");
let lang = get_attr!(elem, "xml:lang", Default);
if presence.statuses.insert(lang, elem.text()).is_some() {
return Err(Error::ParseError(
"Status element present twice for the same xml:lang.",
));
}
} else if elem.is("priority", ns::DEFAULT_NS) {
if priority.is_some() {
return Err(Error::ParseError(
"More than one priority element in a presence.",
));
}
check_no_attributes!(elem, "priority");
check_no_children!(elem, "priority");
priority = Some(Priority::from_str(elem.text().as_ref())?);
} else {
presence.payloads.push(elem.clone());
}
}
presence.show = show;
if let Some(priority) = priority {
presence.priority = priority;
}
Ok(presence)
}
}
impl From<Presence> for Element {
fn from(presence: Presence) -> Element {
Element::builder("presence")
.ns(ns::DEFAULT_NS)
.attr("from", presence.from)
.attr("to", presence.to)
.attr("id", presence.id)
.attr("type", presence.type_)
.append_all(presence.show.into_iter())
.append_all(
presence
.statuses
.into_iter()
.map(|(lang, status)| {
Element::builder("status")
.attr(
"xml:lang",
match lang.as_ref() {
"" => None,
lang => Some(lang),
},
)
.append(status)
})
)
.append_all(if presence.priority == 0 {
None
} else {
Some(Element::builder("priority")
.append(format!("{}", presence.priority)))
})
.append_all(presence.payloads.into_iter())
.build()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::util::compare_elements::NamespaceAwareCompare;
use jid::{BareJid, FullJid};
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(Show, 1);
assert_size!(Type, 1);
assert_size!(Presence, 120);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(Show, 1);
assert_size!(Type, 1);
assert_size!(Presence, 240);
}
#[test]
fn test_simple() {
#[cfg(not(feature = "component"))]
let elem: Element = "<presence xmlns='jabber:client'/>".parse().unwrap();
#[cfg(feature = "component")]
let elem: Element = "<presence xmlns='jabber:component:accept'/>"
.parse()
.unwrap();
let presence = Presence::try_from(elem).unwrap();
assert_eq!(presence.from, None);
assert_eq!(presence.to, None);
assert_eq!(presence.id, None);
assert_eq!(presence.type_, Type::None);
assert!(presence.payloads.is_empty());
}
#[test]
fn test_serialise() {
#[cfg(not(feature = "component"))]
let elem: Element = "<presence xmlns='jabber:client' type='unavailable'/>/>"
.parse()
.unwrap();
#[cfg(feature = "component")]
let elem: Element = "<presence xmlns='jabber:component:accept' type='unavailable'/>/>"
.parse()
.unwrap();
let presence = Presence::new(Type::Unavailable);
let elem2 = presence.into();
assert!(elem.compare_to(&elem2));
}
#[test]
fn test_show() {
#[cfg(not(feature = "component"))]
let elem: Element = "<presence xmlns='jabber:client'><show>chat</show></presence>"
.parse()
.unwrap();
#[cfg(feature = "component")]
let elem: Element =
"<presence xmlns='jabber:component:accept'><show>chat</show></presence>"
.parse()
.unwrap();
let presence = Presence::try_from(elem).unwrap();
assert_eq!(presence.payloads.len(), 0);
assert_eq!(presence.show, Some(Show::Chat));
}
#[test]
fn test_empty_show_value() {
#[cfg(not(feature = "component"))]
let elem: Element = "<presence xmlns='jabber:client'/>"
.parse()
.unwrap();
#[cfg(feature = "component")]
let elem: Element = "<presence xmlns='jabber:component:accept'/>"
.parse()
.unwrap();
let presence = Presence::try_from(elem).unwrap();
assert_eq!(presence.show, None);
}
#[test]
fn test_missing_show_value() {
#[cfg(not(feature = "component"))]
let elem: Element = "<presence xmlns='jabber:client'><show/></presence>"
.parse()
.unwrap();
#[cfg(feature = "component")]
let elem: Element = "<presence xmlns='jabber:component:accept'><show/></presence>"
.parse()
.unwrap();
let error = Presence::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Invalid value for show.");
}
#[test]
fn test_invalid_show() {
// "online" used to be a pretty common mistake.
#[cfg(not(feature = "component"))]
let elem: Element = "<presence xmlns='jabber:client'><show>online</show></presence>"
.parse()
.unwrap();
#[cfg(feature = "component")]
let elem: Element =
"<presence xmlns='jabber:component:accept'><show>online</show></presence>"
.parse()
.unwrap();
let error = Presence::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Invalid value for show.");
}
#[test]
fn test_empty_status() {
#[cfg(not(feature = "component"))]
let elem: Element = "<presence xmlns='jabber:client'><status/></presence>"
.parse()
.unwrap();
#[cfg(feature = "component")]
let elem: Element = "<presence xmlns='jabber:component:accept'><status/></presence>"
.parse()
.unwrap();
let presence = Presence::try_from(elem).unwrap();
assert_eq!(presence.payloads.len(), 0);
assert_eq!(presence.statuses.len(), 1);
assert_eq!(presence.statuses[""], "");
}
#[test]
fn test_status() {
#[cfg(not(feature = "component"))]
let elem: Element = "<presence xmlns='jabber:client'><status>Here!</status></presence>"
.parse()
.unwrap();
#[cfg(feature = "component")]
let elem: Element =
"<presence xmlns='jabber:component:accept'><status>Here!</status></presence>"
.parse()
.unwrap();
let presence = Presence::try_from(elem).unwrap();
assert_eq!(presence.payloads.len(), 0);
assert_eq!(presence.statuses.len(), 1);
assert_eq!(presence.statuses[""], "Here!");
}
#[test]
fn test_multiple_statuses() {
#[cfg(not(feature = "component"))]
let elem: Element = "<presence xmlns='jabber:client'><status>Here!</status><status xml:lang='fr'>Là!</status></presence>".parse().unwrap();
#[cfg(feature = "component")]
let elem: Element = "<presence xmlns='jabber:component:accept'><status>Here!</status><status xml:lang='fr'>Là!</status></presence>".parse().unwrap();
let presence = Presence::try_from(elem).unwrap();
assert_eq!(presence.payloads.len(), 0);
assert_eq!(presence.statuses.len(), 2);
assert_eq!(presence.statuses[""], "Here!");
assert_eq!(presence.statuses["fr"], "Là!");
}
#[test]
fn test_invalid_multiple_statuses() {
#[cfg(not(feature = "component"))]
let elem: Element = "<presence xmlns='jabber:client'><status xml:lang='fr'>Here!</status><status xml:lang='fr'>Là!</status></presence>".parse().unwrap();
#[cfg(feature = "component")]
let elem: Element = "<presence xmlns='jabber:component:accept'><status xml:lang='fr'>Here!</status><status xml:lang='fr'>Là!</status></presence>".parse().unwrap();
let error = Presence::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(
message,
"Status element present twice for the same xml:lang."
);
}
#[test]
fn test_priority() {
#[cfg(not(feature = "component"))]
let elem: Element = "<presence xmlns='jabber:client'><priority>-1</priority></presence>"
.parse()
.unwrap();
#[cfg(feature = "component")]
let elem: Element =
"<presence xmlns='jabber:component:accept'><priority>-1</priority></presence>"
.parse()
.unwrap();
let presence = Presence::try_from(elem).unwrap();
assert_eq!(presence.payloads.len(), 0);
assert_eq!(presence.priority, -1i8);
}
#[test]
fn test_invalid_priority() {
#[cfg(not(feature = "component"))]
let elem: Element = "<presence xmlns='jabber:client'><priority>128</priority></presence>"
.parse()
.unwrap();
#[cfg(feature = "component")]
let elem: Element =
"<presence xmlns='jabber:component:accept'><priority>128</priority></presence>"
.parse()
.unwrap();
let error = Presence::try_from(elem).unwrap_err();
match error {
Error::ParseIntError(_) => (),
_ => panic!(),
};
}
#[test]
fn test_unknown_child() {
#[cfg(not(feature = "component"))]
let elem: Element = "<presence xmlns='jabber:client'><test xmlns='invalid'/></presence>"
.parse()
.unwrap();
#[cfg(feature = "component")]
let elem: Element =
"<presence xmlns='jabber:component:accept'><test xmlns='invalid'/></presence>"
.parse()
.unwrap();
let presence = Presence::try_from(elem).unwrap();
let payload = &presence.payloads[0];
assert!(payload.is("test", "invalid"));
}
#[cfg(not(feature = "disable-validation"))]
#[test]
fn test_invalid_status_child() {
#[cfg(not(feature = "component"))]
let elem: Element = "<presence xmlns='jabber:client'><status><coucou/></status></presence>"
.parse()
.unwrap();
#[cfg(feature = "component")]
let elem: Element =
"<presence xmlns='jabber:component:accept'><status><coucou/></status></presence>"
.parse()
.unwrap();
let error = Presence::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown child in status element.");
}
#[cfg(not(feature = "disable-validation"))]
#[test]
fn test_invalid_attribute() {
#[cfg(not(feature = "component"))]
let elem: Element = "<presence xmlns='jabber:client'><status coucou=''/></presence>"
.parse()
.unwrap();
#[cfg(feature = "component")]
let elem: Element =
"<presence xmlns='jabber:component:accept'><status coucou=''/></presence>"
.parse()
.unwrap();
let error = Presence::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown attribute in status element.");
}
#[test]
fn test_serialise_status() {
let status = Status::from("Hello world!");
let mut presence = Presence::new(Type::Unavailable);
presence.statuses.insert(String::from(""), status);
let elem: Element = presence.into();
assert!(elem.is("presence", ns::DEFAULT_NS));
assert!(elem.children().next().unwrap().is("status", ns::DEFAULT_NS));
}
#[test]
fn test_serialise_priority() {
let presence = Presence::new(Type::None)
.with_priority(42);
let elem: Element = presence.into();
assert!(elem.is("presence", ns::DEFAULT_NS));
let priority = elem.children().next().unwrap();
assert!(priority.is("priority", ns::DEFAULT_NS));
assert_eq!(priority.text(), "42");
}
#[test]
fn presence_with_to() {
let presence = Presence::new(Type::None);
let elem: Element = presence.into();
assert_eq!(elem.attr("to"), None);
let presence = Presence::new(Type::None)
.with_to(Jid::Bare(BareJid::domain("localhost")));
let elem: Element = presence.into();
assert_eq!(elem.attr("to"), Some("localhost"));
let presence = Presence::new(Type::None)
.with_to(BareJid::domain("localhost"));
let elem: Element = presence.into();
assert_eq!(elem.attr("to"), Some("localhost"));
let presence = Presence::new(Type::None)
.with_to(Jid::Full(FullJid::new("test", "localhost", "coucou")));
let elem: Element = presence.into();
assert_eq!(elem.attr("to"), Some("test@localhost/coucou"));
let presence = Presence::new(Type::None)
.with_to(FullJid::new("test", "localhost", "coucou"));
let elem: Element = presence.into();
assert_eq!(elem.attr("to"), Some("test@localhost/coucou"));
}
}

View file

@ -0,0 +1,429 @@
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 crate::data_forms::DataForm;
use crate::date::DateTime;
use crate::util::error::Error;
use crate::ns;
use crate::pubsub::{ItemId, NodeName, Subscription, SubscriptionId, Item as PubSubItem};
use jid::Jid;
use crate::Element;
use std::convert::TryFrom;
/// Event wrapper for a PubSub `<item/>`.
#[derive(Debug, Clone)]
pub struct Item(pub PubSubItem);
impl_pubsub_item!(Item, PUBSUB_EVENT);
/// Represents an event happening to a PubSub node.
#[derive(Debug, Clone)]
pub enum PubSubEvent {
/*
Collection {
},
*/
/// This nodes configuration changed.
Configuration {
/// The node affected.
node: NodeName,
/// The new configuration of this node.
form: Option<DataForm>,
},
/// This node has been deleted, with an optional redirect to another node.
Delete {
/// The node affected.
node: NodeName,
/// The xmpp: URI of another node replacing this one.
redirect: Option<String>,
},
/// Some items have been published on this node.
PublishedItems {
/// The node affected.
node: NodeName,
/// The list of published items.
items: Vec<Item>,
},
/// Some items have been removed from this node.
RetractedItems {
/// The node affected.
node: NodeName,
/// The list of retracted items.
items: Vec<ItemId>,
},
/// All items of this node just got removed at once.
Purge {
/// The node affected.
node: NodeName,
},
/// The users subscription to this node has changed.
Subscription {
/// The node affected.
node: NodeName,
/// The time at which this subscription will expire.
expiry: Option<DateTime>,
/// The JID of the user affected.
jid: Option<Jid>,
/// An identifier for this subscription.
subid: Option<SubscriptionId>,
/// The state of this subscription.
subscription: Option<Subscription>,
},
}
fn parse_items(elem: Element, node: NodeName) -> Result<PubSubEvent, Error> {
let mut is_retract = None;
let mut items = vec![];
let mut retracts = vec![];
for child in elem.children() {
if child.is("item", ns::PUBSUB_EVENT) {
match is_retract {
None => is_retract = Some(false),
Some(false) => (),
Some(true) => {
return Err(Error::ParseError(
"Mix of item and retract in items element.",
));
}
}
items.push(Item::try_from(child.clone())?);
} else if child.is("retract", ns::PUBSUB_EVENT) {
match is_retract {
None => is_retract = Some(true),
Some(true) => (),
Some(false) => {
return Err(Error::ParseError(
"Mix of item and retract in items element.",
));
}
}
check_no_children!(child, "retract");
check_no_unknown_attributes!(child, "retract", ["id"]);
let id = get_attr!(child, "id", Required);
retracts.push(id);
} else {
return Err(Error::ParseError("Invalid child in items element."));
}
}
Ok(match is_retract {
Some(false) => PubSubEvent::PublishedItems { node, items },
Some(true) => PubSubEvent::RetractedItems {
node,
items: retracts,
},
None => return Err(Error::ParseError("Missing children in items element.")),
})
}
impl TryFrom<Element> for PubSubEvent {
type Error = Error;
fn try_from(elem: Element) -> Result<PubSubEvent, Error> {
check_self!(elem, "event", PUBSUB_EVENT);
check_no_attributes!(elem, "event");
let mut payload = None;
for child in elem.children() {
let node = get_attr!(child, "node", Required);
if child.is("configuration", ns::PUBSUB_EVENT) {
let mut payloads = child.children().cloned().collect::<Vec<_>>();
let item = payloads.pop();
if !payloads.is_empty() {
return Err(Error::ParseError(
"More than a single payload in configuration element.",
));
}
let form = match item {
None => None,
Some(payload) => Some(DataForm::try_from(payload)?),
};
payload = Some(PubSubEvent::Configuration { node, form });
} else if child.is("delete", ns::PUBSUB_EVENT) {
let mut redirect = None;
for item in child.children() {
if item.is("redirect", ns::PUBSUB_EVENT) {
if redirect.is_some() {
return Err(Error::ParseError(
"More than one redirect in delete element.",
));
}
let uri = get_attr!(item, "uri", Required);
redirect = Some(uri);
} else {
return Err(Error::ParseError("Unknown child in delete element."));
}
}
payload = Some(PubSubEvent::Delete { node, redirect });
} else if child.is("items", ns::PUBSUB_EVENT) {
payload = Some(parse_items(child.clone(), node)?);
} else if child.is("purge", ns::PUBSUB_EVENT) {
check_no_children!(child, "purge");
payload = Some(PubSubEvent::Purge { node });
} else if child.is("subscription", ns::PUBSUB_EVENT) {
check_no_children!(child, "subscription");
payload = Some(PubSubEvent::Subscription {
node,
expiry: get_attr!(child, "expiry", Option),
jid: get_attr!(child, "jid", Option),
subid: get_attr!(child, "subid", Option),
subscription: get_attr!(child, "subscription", Option),
});
} else {
return Err(Error::ParseError("Unknown child in event element."));
}
}
Ok(payload.ok_or(Error::ParseError("No payload in event element."))?)
}
}
impl From<PubSubEvent> for Element {
fn from(event: PubSubEvent) -> Element {
let payload = match event {
PubSubEvent::Configuration { node, form } => Element::builder("configuration")
.ns(ns::PUBSUB_EVENT)
.attr("node", node)
.append_all(form.map(Element::from)),
PubSubEvent::Delete { node, redirect } => Element::builder("purge")
.ns(ns::PUBSUB_EVENT)
.attr("node", node)
.append_all(redirect.map(|redirect| {
Element::builder("redirect")
.ns(ns::PUBSUB_EVENT)
.attr("uri", redirect)
})),
PubSubEvent::PublishedItems { node, items } => Element::builder("items")
.ns(ns::PUBSUB_EVENT)
.attr("node", node)
.append_all(items.into_iter()),
PubSubEvent::RetractedItems { node, items } => Element::builder("items")
.ns(ns::PUBSUB_EVENT)
.attr("node", node)
.append_all(
items
.into_iter()
.map(|id| {
Element::builder("retract")
.ns(ns::PUBSUB_EVENT)
.attr("id", id)
})
),
PubSubEvent::Purge { node } => Element::builder("purge")
.ns(ns::PUBSUB_EVENT)
.attr("node", node),
PubSubEvent::Subscription {
node,
expiry,
jid,
subid,
subscription,
} => Element::builder("subscription")
.ns(ns::PUBSUB_EVENT)
.attr("node", node)
.attr("expiry", expiry)
.attr("jid", jid)
.attr("subid", subid)
.attr("subscription", subscription),
};
Element::builder("event")
.ns(ns::PUBSUB_EVENT)
.append(payload)
.build()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::util::compare_elements::NamespaceAwareCompare;
use std::str::FromStr;
#[test]
fn missing_items() {
let elem: Element =
"<event xmlns='http://jabber.org/protocol/pubsub#event'><items node='coucou'/></event>"
.parse()
.unwrap();
let error = PubSubEvent::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Missing children in items element.");
}
#[test]
fn test_simple_items() {
let elem: Element = "<event xmlns='http://jabber.org/protocol/pubsub#event'><items node='coucou'><item id='test' publisher='test@coucou'/></items></event>".parse().unwrap();
let event = PubSubEvent::try_from(elem).unwrap();
match event {
PubSubEvent::PublishedItems { node, items } => {
assert_eq!(node, NodeName(String::from("coucou")));
assert_eq!(items[0].id, Some(ItemId(String::from("test"))));
assert_eq!(
items[0].publisher,
Some(Jid::from_str("test@coucou").unwrap())
);
assert_eq!(items[0].payload, None);
}
_ => panic!(),
}
}
#[test]
fn test_simple_pep() {
let elem: Element = "<event xmlns='http://jabber.org/protocol/pubsub#event'><items node='something'><item><foreign xmlns='example:namespace'/></item></items></event>".parse().unwrap();
let event = PubSubEvent::try_from(elem).unwrap();
match event {
PubSubEvent::PublishedItems { node, items } => {
assert_eq!(node, NodeName(String::from("something")));
assert_eq!(items[0].id, None);
assert_eq!(items[0].publisher, None);
match items[0].payload {
Some(ref elem) => assert!(elem.is("foreign", "example:namespace")),
_ => panic!(),
}
}
_ => panic!(),
}
}
#[test]
fn test_simple_retract() {
let elem: Element = "<event xmlns='http://jabber.org/protocol/pubsub#event'><items node='something'><retract id='coucou'/><retract id='test'/></items></event>".parse().unwrap();
let event = PubSubEvent::try_from(elem).unwrap();
match event {
PubSubEvent::RetractedItems { node, items } => {
assert_eq!(node, NodeName(String::from("something")));
assert_eq!(items[0], ItemId(String::from("coucou")));
assert_eq!(items[1], ItemId(String::from("test")));
}
_ => panic!(),
}
}
#[test]
fn test_simple_delete() {
let elem: Element = "<event xmlns='http://jabber.org/protocol/pubsub#event'><delete node='coucou'><redirect uri='hello'/></delete></event>".parse().unwrap();
let event = PubSubEvent::try_from(elem).unwrap();
match event {
PubSubEvent::Delete { node, redirect } => {
assert_eq!(node, NodeName(String::from("coucou")));
assert_eq!(redirect, Some(String::from("hello")));
}
_ => panic!(),
}
}
#[test]
fn test_simple_purge() {
let elem: Element =
"<event xmlns='http://jabber.org/protocol/pubsub#event'><purge node='coucou'/></event>"
.parse()
.unwrap();
let event = PubSubEvent::try_from(elem).unwrap();
match event {
PubSubEvent::Purge { node } => {
assert_eq!(node, NodeName(String::from("coucou")));
}
_ => panic!(),
}
}
#[test]
fn test_simple_configure() {
let elem: Element = "<event xmlns='http://jabber.org/protocol/pubsub#event'><configuration node='coucou'><x xmlns='jabber:x:data' type='result'><field var='FORM_TYPE' type='hidden'><value>http://jabber.org/protocol/pubsub#node_config</value></field></x></configuration></event>".parse().unwrap();
let event = PubSubEvent::try_from(elem).unwrap();
match event {
PubSubEvent::Configuration { node, form: _ } => {
assert_eq!(node, NodeName(String::from("coucou")));
//assert_eq!(form.type_, Result_);
}
_ => panic!(),
}
}
#[test]
fn test_invalid() {
let elem: Element =
"<event xmlns='http://jabber.org/protocol/pubsub#event'><coucou node='test'/></event>"
.parse()
.unwrap();
let error = PubSubEvent::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown child in event element.");
}
#[cfg(not(feature = "disable-validation"))]
#[test]
fn test_invalid_attribute() {
let elem: Element = "<event xmlns='http://jabber.org/protocol/pubsub#event' coucou=''/>"
.parse()
.unwrap();
let error = PubSubEvent::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown attribute in event element.");
}
#[test]
fn test_ex221_subscription() {
let elem: Element = r#"
<event xmlns='http://jabber.org/protocol/pubsub#event'>
<subscription
expiry='2006-02-28T23:59:59+00:00'
jid='francisco@denmark.lit'
node='princely_musings'
subid='ba49252aaa4f5d320c24d3766f0bdcade78c78d3'
subscription='subscribed'/>
</event>
"#
.parse()
.unwrap();
let event = PubSubEvent::try_from(elem.clone()).unwrap();
match event.clone() {
PubSubEvent::Subscription {
node,
expiry,
jid,
subid,
subscription,
} => {
assert_eq!(node, NodeName(String::from("princely_musings")));
assert_eq!(
subid,
Some(SubscriptionId(String::from(
"ba49252aaa4f5d320c24d3766f0bdcade78c78d3"
)))
);
assert_eq!(subscription, Some(Subscription::Subscribed));
assert_eq!(jid, Some(Jid::from_str("francisco@denmark.lit").unwrap()));
assert_eq!(expiry, Some("2006-02-28T23:59:59Z".parse().unwrap()));
}
_ => panic!(),
}
let elem2: Element = event.into();
assert!(elem.compare_to(&elem2));
}
}

View file

@ -0,0 +1,76 @@
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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/.
/// The `http://jabber.org/protocol/pubsub#event` protocol.
pub mod event;
/// The `http://jabber.org/protocol/pubsub` protocol.
pub mod pubsub;
pub use self::event::PubSubEvent;
pub use self::pubsub::PubSub;
use crate::{Jid, Element};
generate_id!(
/// The name of a PubSub node, used to identify it on a JID.
NodeName
);
generate_id!(
/// The identifier of an item, which is unique per node.
ItemId
);
generate_id!(
/// The identifier of a subscription to a PubSub node.
SubscriptionId
);
generate_attribute!(
/// The state of a subscription to a node.
Subscription, "subscription", {
/// The user is not subscribed to this node.
None => "none",
/// The users subscription to this node is still pending.
Pending => "pending",
/// The user is subscribed to this node.
Subscribed => "subscribed",
/// The users subscription to this node will only be valid once
/// configured.
Unconfigured => "unconfigured",
}, Default = None
);
/// An item from a PubSub node.
#[derive(Debug, Clone)]
pub struct Item {
/// The identifier for this item, unique per node.
pub id: Option<ItemId>,
/// The JID of the entity who published this item.
pub publisher: Option<Jid>,
/// The payload of this item, in an arbitrary namespace.
pub payload: Option<Element>,
}
impl Item {
/// Create a new item, accepting only payloads implementing `PubSubPayload`.
pub fn new<P: PubSubPayload>(id: Option<ItemId>, publisher: Option<Jid>, payload: Option<P>) -> Item {
Item {
id,
publisher,
payload: payload.map(Into::into),
}
}
}
/// This trait should be implemented on any element which can be included as a PubSub payload.
pub trait PubSubPayload: ::std::convert::TryFrom<crate::Element> + Into<crate::Element> {}

View file

@ -0,0 +1,657 @@
// Copyright (c) 2018 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 crate::data_forms::DataForm;
use crate::util::error::Error;
use crate::iq::{IqGetPayload, IqResultPayload, IqSetPayload};
use crate::ns;
use crate::pubsub::{NodeName, Subscription, SubscriptionId, Item as PubSubItem};
use jid::Jid;
use crate::Element;
use std::convert::TryFrom;
// TODO: a better solution would be to split this into a query and a result elements, like for
// XEP-0030.
generate_element!(
/// A list of affiliations you have on a service, or on a node.
Affiliations, "affiliations", PUBSUB,
attributes: [
/// The optional node name this request pertains to.
node: Option<NodeName> = "node",
],
children: [
/// The actual list of affiliation elements.
affiliations: Vec<Affiliation> = ("affiliation", PUBSUB) => Affiliation
]
);
generate_attribute!(
/// A list of possible affiliations to a node.
AffiliationAttribute, "affiliation", {
/// You are a member of this node, you can subscribe and retrieve items.
Member => "member",
/// You dont have a specific affiliation with this node, you can only subscribe to it.
None => "none",
/// You are banned from this node.
Outcast => "outcast",
/// You are an owner of this node, and can do anything with it.
Owner => "owner",
/// You are a publisher on this node, you can publish and retract items to it.
Publisher => "publisher",
/// You can publish and retract items on this node, but not subscribe or retrieve items.
PublishOnly => "publish-only",
}
);
generate_element!(
/// An affiliation element.
Affiliation, "affiliation", PUBSUB,
attributes: [
/// The node this affiliation pertains to.
node: Required<NodeName> = "node",
/// The affiliation you currently have on this node.
affiliation: Required<AffiliationAttribute> = "affiliation",
]
);
generate_element!(
/// Request to configure a new node.
Configure, "configure", PUBSUB,
children: [
/// The form to configure it.
form: Option<DataForm> = ("x", DATA_FORMS) => DataForm
]
);
generate_element!(
/// Request to create a new node.
Create, "create", PUBSUB,
attributes: [
/// The node name to create, if `None` the service will generate one.
node: Option<NodeName> = "node",
]
);
generate_element!(
/// Request for a default node configuration.
Default, "default", PUBSUB,
attributes: [
/// The node targeted by this request, otherwise the entire service.
node: Option<NodeName> = "node",
// TODO: do we really want to support collection nodes?
// type: Option<String> = "type",
]
);
generate_element!(
/// A request for a list of items.
Items, "items", PUBSUB,
attributes: [
// TODO: should be an xs:positiveInteger, that is, an unbounded int ≥ 1.
/// Maximum number of items returned.
max_items: Option<u32> = "max_items",
/// The node queried by this request.
node: Required<NodeName> = "node",
/// The subscription identifier related to this request.
subid: Option<SubscriptionId> = "subid",
],
children: [
/// The actual list of items returned.
items: Vec<Item> = ("item", PUBSUB) => Item
]
);
impl Items {
/// Create a new items request.
pub fn new(node: &str) -> Items {
Items {
node: NodeName(String::from(node)),
max_items: None,
subid: None,
items: Vec::new(),
}
}
}
/// Response wrapper for a PubSub `<item/>`.
#[derive(Debug, Clone)]
pub struct Item(pub PubSubItem);
impl_pubsub_item!(Item, PUBSUB);
generate_element!(
/// The options associated to a subscription request.
Options, "options", PUBSUB,
attributes: [
/// The JID affected by this request.
jid: Required<Jid> = "jid",
/// The node affected by this request.
node: Option<NodeName> = "node",
/// The subscription identifier affected by this request.
subid: Option<SubscriptionId> = "subid",
],
children: [
/// The form describing the subscription.
form: Option<DataForm> = ("x", DATA_FORMS) => DataForm
]
);
generate_element!(
/// Request to publish items to a node.
Publish, "publish", PUBSUB,
attributes: [
/// The target node for this operation.
node: Required<NodeName> = "node",
],
children: [
/// The items you want to publish.
items: Vec<Item> = ("item", PUBSUB) => Item
]
);
generate_element!(
/// The options associated to a publish request.
PublishOptions, "publish-options", PUBSUB,
children: [
/// The form describing these options.
form: Option<DataForm> = ("x", DATA_FORMS) => DataForm
]
);
generate_attribute!(
/// Whether a retract request should notify subscribers or not.
Notify,
"notify",
bool
);
generate_element!(
/// A request to retract some items from a node.
Retract, "retract", PUBSUB,
attributes: [
/// The node affected by this request.
node: Required<NodeName> = "node",
/// Whether a retract request should notify subscribers or not.
notify: Default<Notify> = "notify",
],
children: [
/// The items affected by this request.
items: Vec<Item> = ("item", PUBSUB) => Item
]
);
/// Indicate that the subscription can be configured.
#[derive(Debug, Clone)]
pub struct SubscribeOptions {
/// If `true`, the configuration is actually required.
required: bool,
}
impl TryFrom<Element> for SubscribeOptions {
type Error = Error;
fn try_from(elem: Element) -> Result<Self, Error> {
check_self!(elem, "subscribe-options", PUBSUB);
check_no_attributes!(elem, "subscribe-options");
let mut required = false;
for child in elem.children() {
if child.is("required", ns::PUBSUB) {
if required {
return Err(Error::ParseError(
"More than one required element in subscribe-options.",
));
}
required = true;
} else {
return Err(Error::ParseError(
"Unknown child in subscribe-options element.",
));
}
}
Ok(SubscribeOptions { required })
}
}
impl From<SubscribeOptions> for Element {
fn from(subscribe_options: SubscribeOptions) -> Element {
Element::builder("subscribe-options")
.ns(ns::PUBSUB)
.append_all(if subscribe_options.required {
Some(Element::builder("required").ns(ns::PUBSUB))
} else {
None
})
.build()
}
}
generate_element!(
/// A request to subscribe a JID to a node.
Subscribe, "subscribe", PUBSUB,
attributes: [
/// The JID being subscribed.
jid: Required<Jid> = "jid",
/// The node to subscribe to.
node: Option<NodeName> = "node",
]
);
generate_element!(
/// A request for current subscriptions.
Subscriptions, "subscriptions", PUBSUB,
attributes: [
/// The node to query.
node: Option<NodeName> = "node",
],
children: [
/// The list of subscription elements returned.
subscription: Vec<SubscriptionElem> = ("subscription", PUBSUB) => SubscriptionElem
]
);
generate_element!(
/// A subscription element, describing the state of a subscription.
SubscriptionElem, "subscription", PUBSUB,
attributes: [
/// The JID affected by this subscription.
jid: Required<Jid> = "jid",
/// The node affected by this subscription.
node: Option<NodeName> = "node",
/// The subscription identifier for this subscription.
subid: Option<SubscriptionId> = "subid",
/// The state of the subscription.
subscription: Option<Subscription> = "subscription",
],
children: [
/// The options related to this subscription.
subscribe_options: Option<SubscribeOptions> = ("subscribe-options", PUBSUB) => SubscribeOptions
]
);
generate_element!(
/// An unsubscribe request.
Unsubscribe, "unsubscribe", PUBSUB,
attributes: [
/// The JID affected by this request.
jid: Required<Jid> = "jid",
/// The node affected by this request.
node: Option<NodeName> = "node",
/// The subscription identifier for this subscription.
subid: Option<SubscriptionId> = "subid",
]
);
/// Main payload used to communicate with a PubSub service.
///
/// `<pubsub xmlns="http://jabber.org/protocol/pubsub"/>`
#[derive(Debug, Clone)]
pub enum PubSub {
/// Request to create a new node, with optional suggested name and suggested configuration.
Create {
/// The create request.
create: Create,
/// The configure request for the new node.
configure: Option<Configure>,
},
/// Request to publish items to a node, with optional options.
Publish {
/// The publish request.
publish: Publish,
/// The options related to this publish request.
publish_options: Option<PublishOptions>,
},
/// A list of affiliations you have on a service, or on a node.
Affiliations(Affiliations),
/// Request for a default node configuration.
Default(Default),
/// A request for a list of items.
Items(Items),
/// A request to retract some items from a node.
Retract(Retract),
/// A request about a subscription.
Subscription(SubscriptionElem),
/// A request for current subscriptions.
Subscriptions(Subscriptions),
/// An unsubscribe request.
Unsubscribe(Unsubscribe),
}
impl IqGetPayload for PubSub {}
impl IqSetPayload for PubSub {}
impl IqResultPayload for PubSub {}
impl TryFrom<Element> for PubSub {
type Error = Error;
fn try_from(elem: Element) -> Result<PubSub, Error> {
check_self!(elem, "pubsub", PUBSUB);
check_no_attributes!(elem, "pubsub");
let mut payload = None;
for child in elem.children() {
if child.is("create", ns::PUBSUB) {
if payload.is_some() {
return Err(Error::ParseError(
"Payload is already defined in pubsub element.",
));
}
let create = Create::try_from(child.clone())?;
payload = Some(PubSub::Create {
create,
configure: None,
});
} else if child.is("configure", ns::PUBSUB) {
if let Some(PubSub::Create { create, configure }) = payload {
if configure.is_some() {
return Err(Error::ParseError(
"Configure is already defined in pubsub element.",
));
}
let configure = Some(Configure::try_from(child.clone())?);
payload = Some(PubSub::Create { create, configure });
} else {
return Err(Error::ParseError(
"Payload is already defined in pubsub element.",
));
}
} else if child.is("publish", ns::PUBSUB) {
if payload.is_some() {
return Err(Error::ParseError(
"Payload is already defined in pubsub element.",
));
}
let publish = Publish::try_from(child.clone())?;
payload = Some(PubSub::Publish {
publish,
publish_options: None,
});
} else if child.is("publish-options", ns::PUBSUB) {
if let Some(PubSub::Publish {
publish,
publish_options,
}) = payload
{
if publish_options.is_some() {
return Err(Error::ParseError(
"Publish-options are already defined in pubsub element.",
));
}
let publish_options = Some(PublishOptions::try_from(child.clone())?);
payload = Some(PubSub::Publish {
publish,
publish_options,
});
} else {
return Err(Error::ParseError(
"Payload is already defined in pubsub element.",
));
}
} else if child.is("affiliations", ns::PUBSUB) {
if payload.is_some() {
return Err(Error::ParseError(
"Payload is already defined in pubsub element.",
));
}
let affiliations = Affiliations::try_from(child.clone())?;
payload = Some(PubSub::Affiliations(affiliations));
} else if child.is("default", ns::PUBSUB) {
if payload.is_some() {
return Err(Error::ParseError(
"Payload is already defined in pubsub element.",
));
}
let default = Default::try_from(child.clone())?;
payload = Some(PubSub::Default(default));
} else if child.is("items", ns::PUBSUB) {
if payload.is_some() {
return Err(Error::ParseError(
"Payload is already defined in pubsub element.",
));
}
let items = Items::try_from(child.clone())?;
payload = Some(PubSub::Items(items));
} else if child.is("retract", ns::PUBSUB) {
if payload.is_some() {
return Err(Error::ParseError(
"Payload is already defined in pubsub element.",
));
}
let retract = Retract::try_from(child.clone())?;
payload = Some(PubSub::Retract(retract));
} else if child.is("subscription", ns::PUBSUB) {
if payload.is_some() {
return Err(Error::ParseError(
"Payload is already defined in pubsub element.",
));
}
let subscription = SubscriptionElem::try_from(child.clone())?;
payload = Some(PubSub::Subscription(subscription));
} else if child.is("subscriptions", ns::PUBSUB) {
if payload.is_some() {
return Err(Error::ParseError(
"Payload is already defined in pubsub element.",
));
}
let subscriptions = Subscriptions::try_from(child.clone())?;
payload = Some(PubSub::Subscriptions(subscriptions));
} else if child.is("unsubscribe", ns::PUBSUB) {
if payload.is_some() {
return Err(Error::ParseError(
"Payload is already defined in pubsub element.",
));
}
let unsubscribe = Unsubscribe::try_from(child.clone())?;
payload = Some(PubSub::Unsubscribe(unsubscribe));
} else {
return Err(Error::ParseError("Unknown child in pubsub element."));
}
}
Ok(payload.ok_or(Error::ParseError("No payload in pubsub element."))?)
}
}
impl From<PubSub> for Element {
fn from(pubsub: PubSub) -> Element {
Element::builder("pubsub")
.ns(ns::PUBSUB)
.append_all(match pubsub {
PubSub::Create { create, configure } => {
let mut elems = vec![Element::from(create)];
if let Some(configure) = configure {
elems.push(Element::from(configure));
}
elems
}
PubSub::Publish {
publish,
publish_options,
} => {
let mut elems = vec![Element::from(publish)];
if let Some(publish_options) = publish_options {
elems.push(Element::from(publish_options));
}
elems
}
PubSub::Affiliations(affiliations) => vec![Element::from(affiliations)],
PubSub::Default(default) => vec![Element::from(default)],
PubSub::Items(items) => vec![Element::from(items)],
PubSub::Retract(retract) => vec![Element::from(retract)],
PubSub::Subscription(subscription) => vec![Element::from(subscription)],
PubSub::Subscriptions(subscriptions) => vec![Element::from(subscriptions)],
PubSub::Unsubscribe(unsubscribe) => vec![Element::from(unsubscribe)],
})
.build()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::util::compare_elements::NamespaceAwareCompare;
#[test]
fn create() {
let elem: Element = "<pubsub xmlns='http://jabber.org/protocol/pubsub'><create/></pubsub>"
.parse()
.unwrap();
let elem1 = elem.clone();
let pubsub = PubSub::try_from(elem).unwrap();
match pubsub.clone() {
PubSub::Create { create, configure } => {
assert!(create.node.is_none());
assert!(configure.is_none());
}
_ => panic!(),
}
let elem2 = Element::from(pubsub);
assert!(elem1.compare_to(&elem2));
let elem: Element =
"<pubsub xmlns='http://jabber.org/protocol/pubsub'><create node='coucou'/></pubsub>"
.parse()
.unwrap();
let elem1 = elem.clone();
let pubsub = PubSub::try_from(elem).unwrap();
match pubsub.clone() {
PubSub::Create { create, configure } => {
assert_eq!(&create.node.unwrap().0, "coucou");
assert!(configure.is_none());
}
_ => panic!(),
}
let elem2 = Element::from(pubsub);
assert!(elem1.compare_to(&elem2));
}
#[test]
fn create_and_configure() {
let elem: Element =
"<pubsub xmlns='http://jabber.org/protocol/pubsub'><create/><configure/></pubsub>"
.parse()
.unwrap();
let elem1 = elem.clone();
let pubsub = PubSub::try_from(elem).unwrap();
match pubsub.clone() {
PubSub::Create { create, configure } => {
assert!(create.node.is_none());
assert!(configure.unwrap().form.is_none());
}
_ => panic!(),
}
let elem2 = Element::from(pubsub);
assert!(elem1.compare_to(&elem2));
}
#[test]
fn publish() {
let elem: Element =
"<pubsub xmlns='http://jabber.org/protocol/pubsub'><publish node='coucou'/></pubsub>"
.parse()
.unwrap();
let elem1 = elem.clone();
let pubsub = PubSub::try_from(elem).unwrap();
match pubsub.clone() {
PubSub::Publish {
publish,
publish_options,
} => {
assert_eq!(&publish.node.0, "coucou");
assert!(publish_options.is_none());
}
_ => panic!(),
}
let elem2 = Element::from(pubsub);
assert!(elem1.compare_to(&elem2));
}
#[test]
fn publish_with_publish_options() {
let elem: Element = "<pubsub xmlns='http://jabber.org/protocol/pubsub'><publish node='coucou'/><publish-options/></pubsub>".parse().unwrap();
let elem1 = elem.clone();
let pubsub = PubSub::try_from(elem).unwrap();
match pubsub.clone() {
PubSub::Publish {
publish,
publish_options,
} => {
assert_eq!(&publish.node.0, "coucou");
assert!(publish_options.unwrap().form.is_none());
}
_ => panic!(),
}
let elem2 = Element::from(pubsub);
assert!(elem1.compare_to(&elem2));
}
#[test]
fn invalid_empty_pubsub() {
let elem: Element = "<pubsub xmlns='http://jabber.org/protocol/pubsub'/>"
.parse()
.unwrap();
let error = PubSub::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "No payload in pubsub element.");
}
#[test]
fn publish_option() {
let elem: Element = "<publish-options xmlns='http://jabber.org/protocol/pubsub'><x xmlns='jabber:x:data' type='submit'><field var='FORM_TYPE' type='hidden'><value>http://jabber.org/protocol/pubsub#publish-options</value></field></x></publish-options>".parse().unwrap();
let publish_options = PublishOptions::try_from(elem).unwrap();
assert_eq!(
&publish_options.form.unwrap().form_type.unwrap(),
"http://jabber.org/protocol/pubsub#publish-options"
);
}
#[test]
fn subscribe_options() {
let elem1: Element = "<subscribe-options xmlns='http://jabber.org/protocol/pubsub'/>"
.parse()
.unwrap();
let subscribe_options1 = SubscribeOptions::try_from(elem1).unwrap();
assert_eq!(subscribe_options1.required, false);
let elem2: Element = "<subscribe-options xmlns='http://jabber.org/protocol/pubsub'><required/></subscribe-options>".parse().unwrap();
let subscribe_options2 = SubscribeOptions::try_from(elem2).unwrap();
assert_eq!(subscribe_options2.required, true);
}
}

View file

@ -0,0 +1,89 @@
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 crate::message::MessagePayload;
generate_empty_element!(
/// Requests that this message is acked by the final recipient once
/// received.
Request,
"request",
RECEIPTS
);
impl MessagePayload for Request {}
generate_element!(
/// Notes that a previous message has correctly been received, it is
/// referenced by its 'id' attribute.
Received, "received", RECEIPTS,
attributes: [
/// The 'id' attribute of the received message.
id: Required<String> = "id",
]
);
impl MessagePayload for Received {}
#[cfg(test)]
mod tests {
use super::*;
use crate::ns;
use crate::Element;
use std::convert::TryFrom;
use crate::util::error::Error;
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(Request, 0);
assert_size!(Received, 12);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(Request, 0);
assert_size!(Received, 24);
}
#[test]
fn test_simple() {
let elem: Element = "<request xmlns='urn:xmpp:receipts'/>".parse().unwrap();
Request::try_from(elem).unwrap();
let elem: Element = "<received xmlns='urn:xmpp:receipts' id='coucou'/>"
.parse()
.unwrap();
Received::try_from(elem).unwrap();
}
#[test]
fn test_missing_id() {
let elem: Element = "<received xmlns='urn:xmpp:receipts'/>".parse().unwrap();
let error = Received::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Required attribute 'id' missing.");
}
#[test]
fn test_serialise() {
let receipt = Request;
let elem: Element = receipt.into();
assert!(elem.is("request", ns::RECEIPTS));
assert_eq!(elem.attrs().count(), 0);
let receipt = Received {
id: String::from("coucou"),
};
let elem: Element = receipt.into();
assert!(elem.is("received", ns::RECEIPTS));
assert_eq!(elem.attr("id"), Some("coucou"));
}
}

337
xmpp-parsers/src/roster.rs Normal file
View file

@ -0,0 +1,337 @@
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 crate::iq::{IqGetPayload, IqResultPayload, IqSetPayload};
use jid::BareJid;
generate_elem_id!(
/// Represents a group a contact is part of.
Group,
"group",
ROSTER
);
generate_attribute!(
/// The state of your mutual subscription with a contact.
Subscription, "subscription", {
/// The user doesnt have any subscription to this contacts presence,
/// and neither does this contact.
None => "none",
/// Only this contact has a subscription with you, not the opposite.
From => "from",
/// Only you have a subscription with this contact, not the opposite.
To => "to",
/// Both you and your contact are subscribed to each others presence.
Both => "both",
/// In a roster set, this asks the server to remove this contact item
/// from your roster.
Remove => "remove",
}, Default = None
);
generate_attribute!(
/// The sub-state of subscription with a contact.
Ask, "ask", (
/// Pending sub-state of the 'none' subscription state.
Subscribe => "subscribe"
)
);
generate_element!(
/// Contact from the users contact list.
#[derive(PartialEq)]
Item, "item", ROSTER,
attributes: [
/// JID of this contact.
jid: Required<BareJid> = "jid",
/// Name of this contact.
name: OptionEmpty<String> = "name",
/// Subscription status of this contact.
subscription: Default<Subscription> = "subscription",
/// Indicates “Pending Out” sub-states for this contact.
ask: Default<Ask> = "ask",
],
children: [
/// Groups this contact is part of.
groups: Vec<Group> = ("group", ROSTER) => Group
]
);
generate_element!(
/// The contact list of the user.
Roster, "query", ROSTER,
attributes: [
/// Version of the contact list.
///
/// This is an opaque string that should only be sent back to the server on
/// a new connection, if this client is storing the contact list between
/// connections.
ver: Option<String> = "ver"
],
children: [
/// List of the contacts of the user.
items: Vec<Item> = ("item", ROSTER) => Item
]
);
impl IqGetPayload for Roster {}
impl IqSetPayload for Roster {}
impl IqResultPayload for Roster {}
#[cfg(test)]
mod tests {
use super::*;
use crate::util::compare_elements::NamespaceAwareCompare;
use crate::util::error::Error;
use crate::Element;
use std::str::FromStr;
use std::convert::TryFrom;
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(Group, 12);
assert_size!(Subscription, 1);
assert_size!(Ask, 1);
assert_size!(Item, 52);
assert_size!(Roster, 24);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(Group, 24);
assert_size!(Subscription, 1);
assert_size!(Ask, 1);
assert_size!(Item, 104);
assert_size!(Roster, 48);
}
#[test]
fn test_get() {
let elem: Element = "<query xmlns='jabber:iq:roster'/>".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 = "<query xmlns='jabber:iq:roster' ver='ver7'><item jid='nurse@example.com'/><item jid='romeo@example.net'/></query>".parse().unwrap();
let roster = Roster::try_from(elem).unwrap();
assert_eq!(roster.ver, Some(String::from("ver7")));
assert_eq!(roster.items.len(), 2);
let elem2: Element = "<query xmlns='jabber:iq:roster' ver='ver7'><item jid='nurse@example.com'/><item jid='romeo@example.net' name=''/></query>".parse().unwrap();
let roster2 = Roster::try_from(elem2).unwrap();
assert_eq!(roster.items, roster2.items);
let elem: Element = "<query xmlns='jabber:iq:roster' ver='ver9'/>"
.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#"
<query xmlns='jabber:iq:roster' ver='ver11'>
<item jid='romeo@example.net'
name='Romeo'
subscription='both'>
<group>Friends</group>
</item>
<item jid='mercutio@example.com'
name='Mercutio'
subscription='from'/>
<item jid='benvolio@example.net'
name='Benvolio'
subscription='both'/>
<item jid='contact@example.org'
subscription='none'
ask='subscribe'
name='MyContact'>
<group>MyBuddies</group>
</item>
</query>
"#
.parse()
.unwrap();
let roster = Roster::try_from(elem).unwrap();
assert_eq!(roster.ver, Some(String::from("ver11")));
assert_eq!(roster.items.len(), 4);
assert_eq!(
roster.items[0].jid,
BareJid::from_str("romeo@example.net").unwrap()
);
assert_eq!(roster.items[0].name, Some(String::from("Romeo")));
assert_eq!(roster.items[0].subscription, Subscription::Both);
assert_eq!(roster.items[0].ask, Ask::None);
assert_eq!(
roster.items[0].groups,
vec!(Group::from_str("Friends").unwrap())
);
assert_eq!(
roster.items[3].jid,
BareJid::from_str("contact@example.org").unwrap()
);
assert_eq!(roster.items[3].name, Some(String::from("MyContact")));
assert_eq!(roster.items[3].subscription, Subscription::None);
assert_eq!(roster.items[3].ask, Ask::Subscribe);
assert_eq!(
roster.items[3].groups,
vec!(Group::from_str("MyBuddies").unwrap())
);
}
#[test]
fn test_multiple_groups() {
let elem: Element = r#"
<query xmlns='jabber:iq:roster'>
<item jid='test@example.org'>
<group>A</group>
<group>B</group>
</item>
</query>
"#
.parse()
.unwrap();
let elem1 = elem.clone();
let roster = Roster::try_from(elem).unwrap();
assert!(roster.ver.is_none());
assert_eq!(roster.items.len(), 1);
assert_eq!(
roster.items[0].jid,
BareJid::from_str("test@example.org").unwrap()
);
assert_eq!(roster.items[0].name, None);
assert_eq!(roster.items[0].groups.len(), 2);
assert_eq!(roster.items[0].groups[0], Group::from_str("A").unwrap());
assert_eq!(roster.items[0].groups[1], Group::from_str("B").unwrap());
let elem2 = roster.into();
assert!(elem1.compare_to(&elem2));
}
#[test]
fn test_set() {
let elem: Element =
"<query xmlns='jabber:iq:roster'><item jid='nurse@example.com'/></query>"
.parse()
.unwrap();
let roster = Roster::try_from(elem).unwrap();
assert!(roster.ver.is_none());
assert_eq!(roster.items.len(), 1);
let elem: Element = r#"
<query xmlns='jabber:iq:roster'>
<item jid='nurse@example.com'
name='Nurse'>
<group>Servants</group>
</item>
</query>
"#
.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,
BareJid::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],
Group::from_str("Servants").unwrap()
);
let elem: Element = r#"
<query xmlns='jabber:iq:roster'>
<item jid='nurse@example.com'
subscription='remove'/>
</query>
"#
.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,
BareJid::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, Subscription::Remove);
}
#[cfg(not(feature = "disable-validation"))]
#[test]
fn test_invalid() {
let elem: Element = "<query xmlns='jabber:iq:roster'><coucou/></query>"
.parse()
.unwrap();
let error = Roster::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown child in query element.");
let elem: Element = "<query xmlns='jabber:iq:roster' coucou=''/>"
.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 query element.");
}
#[test]
fn test_invalid_item() {
let elem: Element = "<query xmlns='jabber:iq:roster'><item/></query>"
.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 = "<query xmlns='jabber:iq:roster'><item jid=''/></query>".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 =
"<query xmlns='jabber:iq:roster'><item jid='coucou'><coucou/></item></query>"
.parse()
.unwrap();
let error = Roster::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown child in item element.");
}
}

310
xmpp-parsers/src/rsm.rs Normal file
View file

@ -0,0 +1,310 @@
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 crate::util::error::Error;
use crate::ns;
use crate::Element;
use std::convert::TryFrom;
/// Requests paging through a potentially big set of items (represented by an
/// UID).
#[derive(Debug, Clone)]
pub struct SetQuery {
/// Limit the number of items, or use the recipients defaults if None.
pub max: Option<usize>,
/// The UID after which to give results, or if None it is the element
/// “before” the first item, effectively an index of negative one.
pub after: Option<String>,
/// The UID before which to give results, or if None it starts with the
/// last page of the full set.
pub before: Option<String>,
/// Numerical index of the page (deprecated).
pub index: Option<usize>,
}
impl TryFrom<Element> for SetQuery {
type Error = Error;
fn try_from(elem: Element) -> Result<SetQuery, Error> {
check_self!(elem, "set", RSM, "RSM set");
let mut set = SetQuery {
max: None,
after: None,
before: None,
index: None,
};
for child in elem.children() {
if child.is("max", ns::RSM) {
if set.max.is_some() {
return Err(Error::ParseError("Set cant have more than one max."));
}
set.max = Some(child.text().parse()?);
} else if child.is("after", ns::RSM) {
if set.after.is_some() {
return Err(Error::ParseError("Set cant have more than one after."));
}
set.after = Some(child.text());
} else if child.is("before", ns::RSM) {
if set.before.is_some() {
return Err(Error::ParseError("Set cant have more than one before."));
}
set.before = Some(child.text());
} else if child.is("index", ns::RSM) {
if set.index.is_some() {
return Err(Error::ParseError("Set cant have more than one index."));
}
set.index = Some(child.text().parse()?);
} else {
return Err(Error::ParseError("Unknown child in set element."));
}
}
Ok(set)
}
}
impl From<SetQuery> for Element {
fn from(set: SetQuery) -> Element {
Element::builder("set")
.ns(ns::RSM)
.append_all(set.max.map(|max| {
Element::builder("max")
.ns(ns::RSM)
.append(format!("{}", max))
}))
.append_all(
set.after
.map(|after| Element::builder("after").ns(ns::RSM).append(after))
)
.append_all(set.before.map(|before| {
Element::builder("before")
.ns(ns::RSM)
.append(before)
}))
.append_all(set.index.map(|index| {
Element::builder("index")
.ns(ns::RSM)
.append(format!("{}", index))
}))
.build()
}
}
/// Describes the paging result of a [query](struct.SetQuery.html).
#[derive(Debug, Clone)]
pub struct SetResult {
/// The UID of the first item of the page.
pub first: Option<String>,
/// The position of the [first item](#structfield.first) in the full set
/// (which may be approximate).
pub first_index: Option<usize>,
/// The UID of the last item of the page.
pub last: Option<String>,
/// How many items there are in the full set (which may be approximate).
pub count: Option<usize>,
}
impl TryFrom<Element> for SetResult {
type Error = Error;
fn try_from(elem: Element) -> Result<SetResult, Error> {
check_self!(elem, "set", RSM, "RSM set");
let mut set = SetResult {
first: None,
first_index: None,
last: None,
count: None,
};
for child in elem.children() {
if child.is("first", ns::RSM) {
if set.first.is_some() {
return Err(Error::ParseError("Set cant have more than one first."));
}
set.first_index = get_attr!(child, "index", Option);
set.first = Some(child.text());
} else if child.is("last", ns::RSM) {
if set.last.is_some() {
return Err(Error::ParseError("Set cant have more than one last."));
}
set.last = Some(child.text());
} else if child.is("count", ns::RSM) {
if set.count.is_some() {
return Err(Error::ParseError("Set cant have more than one count."));
}
set.count = Some(child.text().parse()?);
} else {
return Err(Error::ParseError("Unknown child in set element."));
}
}
Ok(set)
}
}
impl From<SetResult> for Element {
fn from(set: SetResult) -> Element {
let first = set.first.clone().map(|first| {
Element::builder("first")
.ns(ns::RSM)
.attr("index", set.first_index)
.append(first)
});
Element::builder("set")
.ns(ns::RSM)
.append_all(first)
.append_all(
set.last
.map(|last| Element::builder("last").ns(ns::RSM).append(last)),
)
.append_all(set.count.map(|count| {
Element::builder("count")
.ns(ns::RSM)
.append(format!("{}", count))
}))
.build()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::util::compare_elements::NamespaceAwareCompare;
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(SetQuery, 40);
assert_size!(SetResult, 40);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(SetQuery, 80);
assert_size!(SetResult, 80);
}
#[test]
fn test_simple() {
let elem: Element = "<set xmlns='http://jabber.org/protocol/rsm'/>"
.parse()
.unwrap();
let set = SetQuery::try_from(elem).unwrap();
assert_eq!(set.max, None);
assert_eq!(set.after, None);
assert_eq!(set.before, None);
assert_eq!(set.index, None);
let elem: Element = "<set xmlns='http://jabber.org/protocol/rsm'/>"
.parse()
.unwrap();
let set = SetResult::try_from(elem).unwrap();
match set.first {
Some(_) => panic!(),
None => (),
}
assert_eq!(set.last, None);
assert_eq!(set.count, None);
}
#[test]
fn test_unknown() {
let elem: Element = "<replace xmlns='urn:xmpp:message-correct:0'/>"
.parse()
.unwrap();
let error = SetQuery::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "This is not a RSM set element.");
let elem: Element = "<replace xmlns='urn:xmpp:message-correct:0'/>"
.parse()
.unwrap();
let error = SetResult::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "This is not a RSM set element.");
}
#[test]
fn test_invalid_child() {
let elem: Element = "<set xmlns='http://jabber.org/protocol/rsm'><coucou/></set>"
.parse()
.unwrap();
let error = SetQuery::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown child in set element.");
let elem: Element = "<set xmlns='http://jabber.org/protocol/rsm'><coucou/></set>"
.parse()
.unwrap();
let error = SetResult::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown child in set element.");
}
#[test]
fn test_serialise() {
let elem: Element = "<set xmlns='http://jabber.org/protocol/rsm'/>"
.parse()
.unwrap();
let rsm = SetQuery {
max: None,
after: None,
before: None,
index: None,
};
let elem2 = rsm.into();
assert_eq!(elem, elem2);
let elem: Element = "<set xmlns='http://jabber.org/protocol/rsm'/>"
.parse()
.unwrap();
let rsm = SetResult {
first: None,
first_index: None,
last: None,
count: None,
};
let elem2 = rsm.into();
assert_eq!(elem, elem2);
}
#[test]
fn test_first_index() {
let elem: Element =
"<set xmlns='http://jabber.org/protocol/rsm'><first index='4'>coucou</first></set>"
.parse()
.unwrap();
let elem1 = elem.clone();
let set = SetResult::try_from(elem).unwrap();
assert_eq!(set.first, Some(String::from("coucou")));
assert_eq!(set.first_index, Some(4));
let set2 = SetResult {
first: Some(String::from("coucou")),
first_index: Some(4),
last: None,
count: None,
};
let elem2 = set2.into();
assert!(elem1.compare_to(&elem2));
}
}

308
xmpp-parsers/src/sasl.rs Normal file
View file

@ -0,0 +1,308 @@
// Copyright (c) 2018 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 crate::util::error::Error;
use crate::util::helpers::Base64;
use crate::ns;
use crate::Element;
use std::collections::BTreeMap;
use std::convert::TryFrom;
generate_attribute!(
/// The list of available SASL mechanisms.
Mechanism, "mechanism", {
/// Uses no hashing mechanism and transmit the password in clear to the
/// server, using a single step.
Plain => "PLAIN",
/// Challenge-based mechanism using HMAC and SHA-1, allows both the
/// client and the server to avoid having to store the password in
/// clear.
///
/// See https://tools.ietf.org/html/rfc5802
ScramSha1 => "SCRAM-SHA-1",
/// Same as [ScramSha1](#structfield.ScramSha1), with the addition of
/// channel binding.
ScramSha1Plus => "SCRAM-SHA-1-PLUS",
/// Same as [ScramSha1](#structfield.ScramSha1), but using SHA-256
/// instead of SHA-1 as the hash function.
ScramSha256 => "SCRAM-SHA-256",
/// Same as [ScramSha256](#structfield.ScramSha256), with the addition
/// of channel binding.
ScramSha256Plus => "SCRAM-SHA-256-PLUS",
/// Creates a temporary JID on login, which will be destroyed on
/// disconnect.
Anonymous => "ANONYMOUS",
}
);
generate_element!(
/// The first step of the SASL process, selecting the mechanism and sending
/// the first part of the handshake.
Auth, "auth", SASL,
attributes: [
/// The mechanism used.
mechanism: Required<Mechanism> = "mechanism"
],
text: (
/// The content of the handshake.
data: Base64<Vec<u8>>
)
);
generate_element!(
/// In case the mechanism selected at the [auth](struct.Auth.html) step
/// requires a second step, the server sends this element with additional
/// data.
Challenge, "challenge", SASL,
text: (
/// The challenge data.
data: Base64<Vec<u8>>
)
);
generate_element!(
/// In case the mechanism selected at the [auth](struct.Auth.html) step
/// requires a second step, this contains the clients response to the
/// servers [challenge](struct.Challenge.html).
Response, "response", SASL,
text: (
/// The response data.
data: Base64<Vec<u8>>
)
);
generate_empty_element!(
/// Sent by the client at any point after [auth](struct.Auth.html) if it
/// wants to cancel the current authentication process.
Abort,
"abort",
SASL
);
generate_element!(
/// Sent by the server on SASL success.
Success, "success", SASL,
text: (
/// Possible data sent on success.
data: Base64<Vec<u8>>
)
);
generate_element_enum!(
/// List of possible failure conditions for SASL.
DefinedCondition, "defined-condition", SASL, {
/// The client aborted the authentication with
/// [abort](struct.Abort.html).
Aborted => "aborted",
/// The account the client is trying to authenticate against has been
/// disabled.
AccountDisabled => "account-disabled",
/// The credentials for this account have expired.
CredentialsExpired => "credentials-expired",
/// You must enable StartTLS or use direct TLS before using this
/// authentication mechanism.
EncryptionRequired => "encryption-required",
/// The base64 data sent by the client is invalid.
IncorrectEncoding => "incorrect-encoding",
/// The authzid provided by the client is invalid.
InvalidAuthzid => "invalid-authzid",
/// The client tried to use an invalid mechanism, or none.
InvalidMechanism => "invalid-mechanism",
/// The client sent a bad request.
MalformedRequest => "malformed-request",
/// The mechanism selected is weaker than what the server allows.
MechanismTooWeak => "mechanism-too-weak",
/// The credentials provided are invalid.
NotAuthorized => "not-authorized",
/// The server encountered an issue which may be fixed later, the
/// client should retry at some point.
TemporaryAuthFailure => "temporary-auth-failure",
}
);
type Lang = String;
/// Sent by the server on SASL failure.
#[derive(Debug, Clone)]
pub struct Failure {
/// One of the allowed defined-conditions for SASL.
pub defined_condition: DefinedCondition,
/// A human-readable explanation for the failure.
pub texts: BTreeMap<Lang, String>,
}
impl TryFrom<Element> for Failure {
type Error = Error;
fn try_from(root: Element) -> Result<Failure, Error> {
check_self!(root, "failure", SASL);
check_no_attributes!(root, "failure");
let mut defined_condition = None;
let mut texts = BTreeMap::new();
for child in root.children() {
if child.is("text", ns::SASL) {
check_no_unknown_attributes!(child, "text", ["xml:lang"]);
check_no_children!(child, "text");
let lang = get_attr!(child, "xml:lang", Default);
if texts.insert(lang, child.text()).is_some() {
return Err(Error::ParseError(
"Text element present twice for the same xml:lang in failure element.",
));
}
} else if child.has_ns(ns::SASL) {
if defined_condition.is_some() {
return Err(Error::ParseError(
"Failure must not have more than one defined-condition.",
));
}
check_no_attributes!(child, "defined-condition");
check_no_children!(child, "defined-condition");
let condition = match DefinedCondition::try_from(child.clone()) {
Ok(condition) => condition,
// TODO: do we really want to eat this error?
Err(_) => DefinedCondition::NotAuthorized,
};
defined_condition = Some(condition);
} else {
return Err(Error::ParseError("Unknown element in Failure."));
}
}
let defined_condition =
defined_condition.ok_or(Error::ParseError("Failure must have a defined-condition."))?;
Ok(Failure {
defined_condition,
texts,
})
}
}
impl From<Failure> for Element {
fn from(failure: Failure) -> Element {
Element::builder("failure")
.ns(ns::SASL)
.append(failure.defined_condition)
.append_all(
failure
.texts
.into_iter()
.map(|(lang, text)| {
Element::builder("text")
.ns(ns::SASL)
.attr("xml:lang", lang)
.append(text)
})
)
.build()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Element;
use std::convert::TryFrom;
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(Mechanism, 1);
assert_size!(Auth, 16);
assert_size!(Challenge, 12);
assert_size!(Response, 12);
assert_size!(Abort, 0);
assert_size!(Success, 12);
assert_size!(DefinedCondition, 1);
assert_size!(Failure, 16);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(Mechanism, 1);
assert_size!(Auth, 32);
assert_size!(Challenge, 24);
assert_size!(Response, 24);
assert_size!(Abort, 0);
assert_size!(Success, 24);
assert_size!(DefinedCondition, 1);
assert_size!(Failure, 32);
}
#[test]
fn test_simple() {
let elem: Element = "<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'/>"
.parse()
.unwrap();
let auth = Auth::try_from(elem).unwrap();
assert_eq!(auth.mechanism, Mechanism::Plain);
assert!(auth.data.is_empty());
}
#[test]
fn section_6_5_1() {
let elem: Element =
"<failure xmlns='urn:ietf:params:xml:ns:xmpp-sasl'><aborted/></failure>"
.parse()
.unwrap();
let failure = Failure::try_from(elem).unwrap();
assert_eq!(failure.defined_condition, DefinedCondition::Aborted);
assert!(failure.texts.is_empty());
}
#[test]
fn section_6_5_2() {
let elem: Element = "<failure xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>
<account-disabled/>
<text xml:lang='en'>Call 212-555-1212 for assistance.</text>
</failure>"
.parse()
.unwrap();
let failure = Failure::try_from(elem).unwrap();
assert_eq!(failure.defined_condition, DefinedCondition::AccountDisabled);
assert_eq!(
failure.texts["en"],
String::from("Call 212-555-1212 for assistance.")
);
}
/// Some servers apparently use a non-namespaced 'lang' attribute, which is invalid as not part
/// of the schema. This tests whether we can parse it when disabling validation.
#[cfg(feature = "disable-validation")]
#[test]
fn invalid_failure_with_non_prefixed_text_lang() {
let elem: Element = "<failure xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>
<not-authorized xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>
<text xmlns='urn:ietf:params:xml:ns:xmpp-sasl' lang='en'>Invalid username or password</text>
</failure>"
.parse()
.unwrap();
let failure = Failure::try_from(elem).unwrap();
assert_eq!(failure.defined_condition, DefinedCondition::NotAuthorized);
assert_eq!(
failure.texts[""],
String::from("Invalid username or password")
);
}
}

View file

@ -0,0 +1,209 @@
// Copyright (C) 2019 Maxime “pep” Buquet <pep@bouah.net>
// 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 crate::data_forms::{DataForm, DataFormType, Field, FieldType};
use crate::ns;
use crate::util::error::Error;
use std::convert::TryFrom;
/// Structure representing a `http://jabber.org/network/serverinfo` form type.
#[derive(Debug, Clone, PartialEq, Default)]
pub struct ServerInfo {
/// Abuse addresses
pub abuse: Vec<String>,
/// Admin addresses
pub admin: Vec<String>,
/// Feedback addresses
pub feedback: Vec<String>,
/// Sales addresses
pub sales: Vec<String>,
/// Security addresses
pub security: Vec<String>,
/// Support addresses
pub support: Vec<String>,
}
impl TryFrom<DataForm> for ServerInfo {
type Error = Error;
fn try_from(form: DataForm) -> Result<ServerInfo, Error> {
if form.type_ != DataFormType::Result_ {
return Err(Error::ParseError("Wrong type of form."));
}
if form.form_type != Some(String::from(ns::SERVER_INFO)) {
return Err(Error::ParseError("Wrong FORM_TYPE for form."));
}
let mut server_info = ServerInfo::default();
for field in form.fields {
if field.type_ != FieldType::ListMulti {
return Err(Error::ParseError("Field is not of the required type."));
}
if field.var == "abuse-addresses" {
server_info.abuse = field.values;
} else if field.var == "admin-addresses" {
server_info.admin = field.values;
} else if field.var == "feedback-addresses" {
server_info.feedback = field.values;
} else if field.var == "sales-addresses" {
server_info.sales = field.values;
} else if field.var == "security-addresses" {
server_info.security = field.values;
} else if field.var == "support-addresses" {
server_info.support = field.values;
} else {
return Err(Error::ParseError("Unknown form field var."));
}
}
Ok(server_info)
}
}
impl From<ServerInfo> for DataForm {
fn from(server_info: ServerInfo) -> DataForm {
DataForm {
type_: DataFormType::Result_,
form_type: Some(String::from(ns::SERVER_INFO)),
title: None,
instructions: None,
fields: vec![
generate_address_field("abuse-addresses", server_info.abuse),
generate_address_field("admin-addresses", server_info.admin),
generate_address_field("feedback-addresses", server_info.feedback),
generate_address_field("sales-addresses", server_info.sales),
generate_address_field("security-addresses", server_info.security),
generate_address_field("support-addresses", server_info.support),
],
}
}
}
/// Generate `Field` for addresses
pub fn generate_address_field<S: Into<String>>(var: S, values: Vec<String>) -> Field {
Field {
var: var.into(),
type_: FieldType::ListMulti,
label: None,
required: false,
options: vec![],
values,
media: vec![],
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::data_forms::{DataForm, DataFormType, Field, FieldType};
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(ServerInfo, 72);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(ServerInfo, 144);
}
#[test]
fn test_simple() {
let form = DataForm {
type_: DataFormType::Result_,
form_type: Some(String::from(ns::SERVER_INFO)),
title: None,
instructions: None,
fields: vec![
Field {
var: String::from("abuse-addresses"),
type_: FieldType::ListMulti,
label: None,
required: false,
options: vec![],
values: vec![],
media: vec![],
},
Field {
var: String::from("admin-addresses"),
type_: FieldType::ListMulti,
label: None,
required: false,
options: vec![],
values: vec![
String::from("xmpp:admin@foo.bar"),
String::from("https://foo.bar/chat/"),
String::from("mailto:admin@foo.bar"),
],
media: vec![],
},
Field {
var: String::from("feedback-addresses"),
type_: FieldType::ListMulti,
label: None,
required: false,
options: vec![],
values: vec![],
media: vec![],
},
Field {
var: String::from("sales-addresses"),
type_: FieldType::ListMulti,
label: None,
required: false,
options: vec![],
values: vec![],
media: vec![],
},
Field {
var: String::from("security-addresses"),
type_: FieldType::ListMulti,
label: None,
required: false,
options: vec![],
values: vec![
String::from("xmpp:security@foo.bar"),
String::from("mailto:security@foo.bar"),
],
media: vec![],
},
Field {
var: String::from("support-addresses"),
type_: FieldType::ListMulti,
label: None,
required: false,
options: vec![],
values: vec![String::from("mailto:support@foo.bar")],
media: vec![],
},
],
};
let server_info = ServerInfo {
abuse: vec![],
admin: vec![
String::from("xmpp:admin@foo.bar"),
String::from("https://foo.bar/chat/"),
String::from("mailto:admin@foo.bar"),
],
feedback: vec![],
sales: vec![],
security: vec![
String::from("xmpp:security@foo.bar"),
String::from("mailto:security@foo.bar"),
],
support: vec![String::from("mailto:support@foo.bar")],
};
// assert_eq!(DataForm::from(server_info), form);
assert_eq!(ServerInfo::try_from(form).unwrap(), server_info);
}
}

227
xmpp-parsers/src/sm.rs Normal file
View file

@ -0,0 +1,227 @@
// Copyright (c) 2018 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 crate::stanza_error::DefinedCondition;
generate_element!(
/// Acknowledgement of the currently received stanzas.
A, "a", SM,
attributes: [
/// The last handled stanza.
h: Required<u32> = "h",
]
);
impl A {
/// Generates a new `<a/>` element.
pub fn new(h: u32) -> A {
A { h }
}
}
generate_attribute!(
/// Whether to allow resumption of a previous stream.
ResumeAttr,
"resume",
bool
);
generate_element!(
/// Client request for enabling stream management.
#[derive(Default)]
Enable, "enable", SM,
attributes: [
/// The preferred resumption time in seconds by the client.
// TODO: should be the infinite integer set ≥ 1.
max: Option<u32> = "max",
/// Whether the client wants to be allowed to resume the stream.
resume: Default<ResumeAttr> = "resume",
]
);
impl Enable {
/// Generates a new `<enable/>` element.
pub fn new() -> Self {
Enable::default()
}
/// Sets the preferred resumption time in seconds.
pub fn with_max(mut self, max: u32) -> Self {
self.max = Some(max);
self
}
/// Asks for resumption to be possible.
pub fn with_resume(mut self) -> Self {
self.resume = ResumeAttr::True;
self
}
}
generate_id!(
/// A random identifier used for stream resumption.
StreamId
);
generate_element!(
/// Server response once stream management is enabled.
Enabled, "enabled", SM,
attributes: [
/// A random identifier used for stream resumption.
id: Option<StreamId> = "id",
/// The preferred IP, domain, IP:port or domain:port location for
/// resumption.
location: Option<String> = "location",
/// The preferred resumption time in seconds by the server.
// TODO: should be the infinite integer set ≥ 1.
max: Option<u32> = "max",
/// Whether stream resumption is allowed.
resume: Default<ResumeAttr> = "resume",
]
);
generate_element!(
/// A stream management error happened.
Failed, "failed", SM,
attributes: [
/// The last handled stanza.
h: Option<u32> = "h",
],
children: [
/// The error returned.
// XXX: implement the * handling.
error: Option<DefinedCondition> = ("*", XMPP_STANZAS) => DefinedCondition
]
);
generate_empty_element!(
/// Requests the currently received stanzas by the other party.
R,
"r",
SM
);
generate_element!(
/// Requests a stream resumption.
Resume, "resume", SM,
attributes: [
/// The last handled stanza.
h: Required<u32> = "h",
/// The previous id given by the server on
/// [enabled](struct.Enabled.html).
previd: Required<StreamId> = "previd",
]
);
generate_element!(
/// The response by the server for a successfully resumed stream.
Resumed, "resumed", SM,
attributes: [
/// The last handled stanza.
h: Required<u32> = "h",
/// The previous id given by the server on
/// [enabled](struct.Enabled.html).
previd: Required<StreamId> = "previd",
]
);
// TODO: add support for optional and required.
generate_empty_element!(
/// Represents availability of Stream Management in `<stream:features/>`.
StreamManagement,
"sm",
SM
);
#[cfg(test)]
mod tests {
use super::*;
use crate::Element;
use std::convert::TryFrom;
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(A, 4);
assert_size!(ResumeAttr, 1);
assert_size!(Enable, 12);
assert_size!(StreamId, 12);
assert_size!(Enabled, 36);
assert_size!(Failed, 12);
assert_size!(R, 0);
assert_size!(Resume, 16);
assert_size!(Resumed, 16);
assert_size!(StreamManagement, 0);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(A, 4);
assert_size!(ResumeAttr, 1);
assert_size!(Enable, 12);
assert_size!(StreamId, 24);
assert_size!(Enabled, 64);
assert_size!(Failed, 12);
assert_size!(R, 0);
assert_size!(Resume, 32);
assert_size!(Resumed, 32);
assert_size!(StreamManagement, 0);
}
#[test]
fn a() {
let elem: Element = "<a xmlns='urn:xmpp:sm:3' h='5'".parse().unwrap();
let a = A::try_from(elem).unwrap();
assert_eq!(a.h, 5);
}
#[test]
fn stream_feature() {
let elem: Element = "<sm xmlns='urn:xmpp:sm:3'/>".parse().unwrap();
StreamManagement::try_from(elem).unwrap();
}
#[test]
fn resume() {
let elem: Element = "<enable xmlns='urn:xmpp:sm:3' resume='true'/>"
.parse()
.unwrap();
let enable = Enable::try_from(elem).unwrap();
assert_eq!(enable.max, None);
assert_eq!(enable.resume, ResumeAttr::True);
let elem: Element = "<enabled xmlns='urn:xmpp:sm:3' resume='true' id='coucou' max='600'/>"
.parse()
.unwrap();
let enabled = Enabled::try_from(elem).unwrap();
let previd = enabled.id.unwrap();
assert_eq!(enabled.resume, ResumeAttr::True);
assert_eq!(previd, StreamId(String::from("coucou")));
assert_eq!(enabled.max, Some(600));
assert_eq!(enabled.location, None);
let elem: Element = "<resume xmlns='urn:xmpp:sm:3' h='5' previd='coucou'/>"
.parse()
.unwrap();
let resume = Resume::try_from(elem).unwrap();
assert_eq!(resume.h, 5);
assert_eq!(resume.previd, previd);
let elem: Element = "<resumed xmlns='urn:xmpp:sm:3' h='5' previd='coucou'/>"
.parse()
.unwrap();
let resumed = Resumed::try_from(elem).unwrap();
assert_eq!(resumed.h, 5);
assert_eq!(resumed.previd, previd);
}
}

View file

@ -0,0 +1,390 @@
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 crate::util::error::Error;
use crate::message::MessagePayload;
use crate::ns;
use crate::presence::PresencePayload;
use jid::Jid;
use crate::Element;
use std::collections::BTreeMap;
use std::convert::TryFrom;
generate_attribute!(
/// The type of the error.
ErrorType, "type", {
/// Retry after providing credentials.
Auth => "auth",
/// Do not retry (the error cannot be remedied).
Cancel => "cancel",
/// Proceed (the condition was only a warning).
Continue => "continue",
/// Retry after changing the data sent.
Modify => "modify",
/// Retry after waiting (the error is temporary).
Wait => "wait",
}
);
generate_element_enum!(
/// List of valid error conditions.
DefinedCondition, "condition", XMPP_STANZAS, {
/// The sender has sent a stanza containing XML that does not conform
/// to the appropriate schema or that cannot be processed (e.g., an IQ
/// stanza that includes an unrecognized value of the 'type' attribute,
/// or an element that is qualified by a recognized namespace but that
/// violates the defined syntax for the element); the associated error
/// type SHOULD be "modify".
BadRequest => "bad-request",
/// Access cannot be granted because an existing resource exists with
/// the same name or address; the associated error type SHOULD be
/// "cancel".
Conflict => "conflict",
/// The feature represented in the XML stanza is not implemented by the
/// intended recipient or an intermediate server and therefore the
/// stanza cannot be processed (e.g., the entity understands the
/// namespace but does not recognize the element name); the associated
/// error type SHOULD be "cancel" or "modify".
FeatureNotImplemented => "feature-not-implemented",
/// The requesting entity does not possess the necessary permissions to
/// perform an action that only certain authorized roles or individuals
/// are allowed to complete (i.e., it typically relates to
/// authorization rather than authentication); the associated error
/// type SHOULD be "auth".
Forbidden => "forbidden",
/// The recipient or server can no longer be contacted at this address,
/// typically on a permanent basis (as opposed to the <redirect/> error
/// condition, which is used for temporary addressing failures); the
/// associated error type SHOULD be "cancel" and the error stanza
/// SHOULD include a new address (if available) as the XML character
/// data of the <gone/> element (which MUST be a Uniform Resource
/// Identifier [URI] or Internationalized Resource Identifier [IRI] at
/// which the entity can be contacted, typically an XMPP IRI as
/// specified in [XMPPURI]).
Gone => "gone",
/// The server has experienced a misconfiguration or other internal
/// error that prevents it from processing the stanza; the associated
/// error type SHOULD be "cancel".
InternalServerError => "internal-server-error",
/// The addressed JID or item requested cannot be found; the associated
/// error type SHOULD be "cancel".
ItemNotFound => "item-not-found",
/// The sending entity has provided (e.g., during resource binding) or
/// communicated (e.g., in the 'to' address of a stanza) an XMPP
/// address or aspect thereof that violates the rules defined in
/// [XMPPADDR]; the associated error type SHOULD be "modify".
JidMalformed => "jid-malformed",
/// The recipient or server understands the request but cannot process
/// it because the request does not meet criteria defined by the
/// recipient or server (e.g., a request to subscribe to information
/// that does not simultaneously include configuration parameters
/// needed by the recipient); the associated error type SHOULD be
/// "modify".
NotAcceptable => "not-acceptable",
/// The recipient or server does not allow any entity to perform the
/// action (e.g., sending to entities at a blacklisted domain); the
/// associated error type SHOULD be "cancel".
NotAllowed => "not-allowed",
/// The sender needs to provide credentials before being allowed to
/// perform the action, or has provided improper credentials (the name
/// "not-authorized", which was borrowed from the "401 Unauthorized"
/// error of [HTTP], might lead the reader to think that this condition
/// relates to authorization, but instead it is typically used in
/// relation to authentication); the associated error type SHOULD be
/// "auth".
NotAuthorized => "not-authorized",
/// The entity has violated some local service policy (e.g., a message
/// contains words that are prohibited by the service) and the server
/// MAY choose to specify the policy in the <text/> element or in an
/// application-specific condition element; the associated error type
/// SHOULD be "modify" or "wait" depending on the policy being
/// violated.
PolicyViolation => "policy-violation",
/// The intended recipient is temporarily unavailable, undergoing
/// maintenance, etc.; the associated error type SHOULD be "wait".
RecipientUnavailable => "recipient-unavailable",
/// The recipient or server is redirecting requests for this
/// information to another entity, typically in a temporary fashion (as
/// opposed to the <gone/> error condition, which is used for permanent
/// addressing failures); the associated error type SHOULD be "modify"
/// and the error stanza SHOULD contain the alternate address in the
/// XML character data of the <redirect/> element (which MUST be a URI
/// or IRI with which the sender can communicate, typically an XMPP IRI
/// as specified in [XMPPURI]).
Redirect => "redirect",
/// The requesting entity is not authorized to access the requested
/// service because prior registration is necessary (examples of prior
/// registration include members-only rooms in XMPP multi-user chat
/// [XEP0045] and gateways to non-XMPP instant messaging services,
/// which traditionally required registration in order to use the
/// gateway [XEP0100]); the associated error type SHOULD be "auth".
RegistrationRequired => "registration-required",
/// A remote server or service specified as part or all of the JID of
/// the intended recipient does not exist or cannot be resolved (e.g.,
/// there is no _xmpp-server._tcp DNS SRV record, the A or AAAA
/// fallback resolution fails, or A/AAAA lookups succeed but there is
/// no response on the IANA-registered port 5269); the associated error
/// type SHOULD be "cancel".
RemoteServerNotFound => "remote-server-not-found",
/// A remote server or service specified as part or all of the JID of
/// the intended recipient (or needed to fulfill a request) was
/// resolved but communications could not be established within a
/// reasonable amount of time (e.g., an XML stream cannot be
/// established at the resolved IP address and port, or an XML stream
/// can be established but stream negotiation fails because of problems
/// with TLS, SASL, Server Dialback, etc.); the associated error type
/// SHOULD be "wait" (unless the error is of a more permanent nature,
/// e.g., the remote server is found but it cannot be authenticated or
/// it violates security policies).
RemoteServerTimeout => "remote-server-timeout",
/// The server or recipient is busy or lacks the system resources
/// necessary to service the request; the associated error type SHOULD
/// be "wait".
ResourceConstraint => "resource-constraint",
/// The server or recipient does not currently provide the requested
/// service; the associated error type SHOULD be "cancel".
ServiceUnavailable => "service-unavailable",
/// The requesting entity is not authorized to access the requested
/// service because a prior subscription is necessary (examples of
/// prior subscription include authorization to receive presence
/// information as defined in [XMPPIM] and opt-in data feeds for XMPP
/// publish-subscribe as defined in [XEP0060]); the associated error
/// type SHOULD be "auth".
SubscriptionRequired => "subscription-required",
/// The error condition is not one of those defined by the other
/// conditions in this list; any error type can be associated with this
/// condition, and it SHOULD NOT be used except in conjunction with an
/// application-specific condition.
UndefinedCondition => "undefined-condition",
/// The recipient or server understood the request but was not
/// expecting it at this time (e.g., the request was out of order); the
/// associated error type SHOULD be "wait" or "modify".
UnexpectedRequest => "unexpected-request",
}
);
type Lang = String;
/// The representation of a stanza error.
#[derive(Debug, Clone)]
pub struct StanzaError {
/// The type of this error.
pub type_: ErrorType,
/// The JID of the entity who set this error.
pub by: Option<Jid>,
/// One of the defined conditions for this error to happen.
pub defined_condition: DefinedCondition,
/// Human-readable description of this error.
pub texts: BTreeMap<Lang, String>,
/// A protocol-specific extension for this error.
pub other: Option<Element>,
}
impl MessagePayload for StanzaError {}
impl PresencePayload for StanzaError {}
impl StanzaError {
/// Create a new `<error/>` with the according content.
pub fn new<L, T>(type_: ErrorType, defined_condition: DefinedCondition, lang: L, text: T) -> StanzaError
where L: Into<Lang>,
T: Into<String>,
{
StanzaError {
type_,
by: None,
defined_condition,
texts: {
let mut map = BTreeMap::new();
map.insert(lang.into(), text.into());
map
},
other: None,
}
}
}
impl TryFrom<Element> for StanzaError {
type Error = Error;
fn try_from(elem: Element) -> Result<StanzaError, Error> {
check_self!(elem, "error", DEFAULT_NS);
check_no_unknown_attributes!(elem, "error", ["type", "by"]);
let mut stanza_error = StanzaError {
type_: get_attr!(elem, "type", Required),
by: get_attr!(elem, "by", Option),
defined_condition: DefinedCondition::UndefinedCondition,
texts: BTreeMap::new(),
other: None,
};
let mut defined_condition = None;
for child in elem.children() {
if child.is("text", ns::XMPP_STANZAS) {
check_no_children!(child, "text");
check_no_unknown_attributes!(child, "text", ["xml:lang"]);
let lang = get_attr!(elem, "xml:lang", Default);
if stanza_error.texts.insert(lang, child.text()).is_some() {
return Err(Error::ParseError(
"Text element present twice for the same xml:lang.",
));
}
} else if child.has_ns(ns::XMPP_STANZAS) {
if defined_condition.is_some() {
return Err(Error::ParseError(
"Error must not have more than one defined-condition.",
));
}
check_no_children!(child, "defined-condition");
check_no_attributes!(child, "defined-condition");
let condition = DefinedCondition::try_from(child.clone())?;
defined_condition = Some(condition);
} else {
if stanza_error.other.is_some() {
return Err(Error::ParseError(
"Error must not have more than one other element.",
));
}
stanza_error.other = Some(child.clone());
}
}
stanza_error.defined_condition =
defined_condition.ok_or(Error::ParseError("Error must have a defined-condition."))?;
Ok(stanza_error)
}
}
impl From<StanzaError> for Element {
fn from(err: StanzaError) -> Element {
Element::builder("error")
.ns(ns::DEFAULT_NS)
.attr("type", err.type_)
.attr("by", err.by)
.append(err.defined_condition)
.append_all(
err.texts.into_iter().map(|(lang, text)| {
Element::builder("text")
.ns(ns::XMPP_STANZAS)
.attr("xml:lang", lang)
.append(text)
})
)
.append_all(err.other)
.build()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(ErrorType, 1);
assert_size!(DefinedCondition, 1);
assert_size!(StanzaError, 108);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(ErrorType, 1);
assert_size!(DefinedCondition, 1);
assert_size!(StanzaError, 216);
}
#[test]
fn test_simple() {
#[cfg(not(feature = "component"))]
let elem: Element = "<error xmlns='jabber:client' type='cancel'><undefined-condition xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/></error>".parse().unwrap();
#[cfg(feature = "component")]
let elem: Element = "<error xmlns='jabber:component:accept' type='cancel'><undefined-condition xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/></error>".parse().unwrap();
let error = StanzaError::try_from(elem).unwrap();
assert_eq!(error.type_, ErrorType::Cancel);
assert_eq!(
error.defined_condition,
DefinedCondition::UndefinedCondition
);
}
#[test]
fn test_invalid_type() {
#[cfg(not(feature = "component"))]
let elem: Element = "<error xmlns='jabber:client'/>".parse().unwrap();
#[cfg(feature = "component")]
let elem: Element = "<error xmlns='jabber:component:accept'/>".parse().unwrap();
let error = StanzaError::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Required attribute 'type' missing.");
#[cfg(not(feature = "component"))]
let elem: Element = "<error xmlns='jabber:client' type='coucou'/>"
.parse()
.unwrap();
#[cfg(feature = "component")]
let elem: Element = "<error xmlns='jabber:component:accept' type='coucou'/>"
.parse()
.unwrap();
let error = StanzaError::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown value for 'type' attribute.");
}
#[test]
fn test_invalid_condition() {
#[cfg(not(feature = "component"))]
let elem: Element = "<error xmlns='jabber:client' type='cancel'/>"
.parse()
.unwrap();
#[cfg(feature = "component")]
let elem: Element = "<error xmlns='jabber:component:accept' type='cancel'/>"
.parse()
.unwrap();
let error = StanzaError::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Error must have a defined-condition.");
}
}

View file

@ -0,0 +1,124 @@
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 crate::message::MessagePayload;
use jid::Jid;
generate_element!(
/// Gives the identifier a service has stamped on this stanza, often in
/// order to identify it inside of [an archive](../mam/index.html).
StanzaId, "stanza-id", SID,
attributes: [
/// The id associated to this stanza by another entity.
id: Required<String> = "id",
/// The entity who stamped this stanza-id.
by: Required<Jid> = "by",
]
);
impl MessagePayload for StanzaId {}
generate_element!(
/// A hack for MUC before version 1.31 to track a message which may have
/// its 'id' attribute changed.
OriginId, "origin-id", SID,
attributes: [
/// The id this client set for this stanza.
id: Required<String> = "id",
]
);
impl MessagePayload for OriginId {}
#[cfg(test)]
mod tests {
use super::*;
use crate::util::error::Error;
use crate::Element;
use std::str::FromStr;
use std::convert::TryFrom;
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(StanzaId, 52);
assert_size!(OriginId, 12);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(StanzaId, 104);
assert_size!(OriginId, 24);
}
#[test]
fn test_simple() {
let elem: Element = "<stanza-id xmlns='urn:xmpp:sid:0' id='coucou' by='coucou@coucou'/>"
.parse()
.unwrap();
let stanza_id = StanzaId::try_from(elem).unwrap();
assert_eq!(stanza_id.id, String::from("coucou"));
assert_eq!(stanza_id.by, Jid::from_str("coucou@coucou").unwrap());
let elem: Element = "<origin-id xmlns='urn:xmpp:sid:0' id='coucou'/>"
.parse()
.unwrap();
let origin_id = OriginId::try_from(elem).unwrap();
assert_eq!(origin_id.id, String::from("coucou"));
}
#[test]
fn test_invalid_child() {
let elem: Element = "<stanza-id xmlns='urn:xmpp:sid:0'><coucou/></stanza-id>"
.parse()
.unwrap();
let error = StanzaId::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown child in stanza-id element.");
}
#[test]
fn test_invalid_id() {
let elem: Element = "<stanza-id xmlns='urn:xmpp:sid:0'/>".parse().unwrap();
let error = StanzaId::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Required attribute 'id' missing.");
}
#[test]
fn test_invalid_by() {
let elem: Element = "<stanza-id xmlns='urn:xmpp:sid:0' id='coucou'/>"
.parse()
.unwrap();
let error = StanzaId::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Required attribute 'by' missing.");
}
#[test]
fn test_serialise() {
let elem: Element = "<stanza-id xmlns='urn:xmpp:sid:0' id='coucou' by='coucou@coucou'/>"
.parse()
.unwrap();
let stanza_id = StanzaId {
id: String::from("coucou"),
by: Jid::from_str("coucou@coucou").unwrap(),
};
let elem2 = stanza_id.into();
assert_eq!(elem, elem2);
}
}

101
xmpp-parsers/src/stream.rs Normal file
View file

@ -0,0 +1,101 @@
// Copyright (c) 2018 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 jid::BareJid;
generate_element!(
/// The stream opening for client-server communications.
Stream, "stream", STREAM,
attributes: [
/// The JID of the entity opening this stream.
from: Option<BareJid> = "from",
/// The JID of the entity receiving this stream opening.
to: Option<BareJid> = "to",
/// The id of the stream, used for authentication challenges.
id: Option<String> = "id",
/// The XMPP version used during this stream.
version: Option<String> = "version",
/// The default human language for all subsequent stanzas, which will
/// be transmitted to other entities for better localisation.
xml_lang: Option<String> = "xml:lang",
]
);
impl Stream {
/// Creates a simple client→server `<stream:stream>` element.
pub fn new(to: BareJid) -> Stream {
Stream {
from: None,
to: Some(to),
id: None,
version: Some(String::from("1.0")),
xml_lang: None,
}
}
/// Sets the [@from](#structfield.from) attribute on this `<stream:stream>`
/// element.
pub fn with_from(mut self, from: BareJid) -> Stream {
self.from = Some(from);
self
}
/// Sets the [@id](#structfield.id) attribute on this `<stream:stream>`
/// element.
pub fn with_id(mut self, id: String) -> Stream {
self.id = Some(id);
self
}
/// Sets the [@xml:lang](#structfield.xml_lang) attribute on this
/// `<stream:stream>` element.
pub fn with_lang(mut self, xml_lang: String) -> Stream {
self.xml_lang = Some(xml_lang);
self
}
/// Checks whether the version matches the expected one.
pub fn is_version(&self, version: &str) -> bool {
match self.version {
None => false,
Some(ref self_version) => self_version == &String::from(version),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Element;
use std::convert::TryFrom;
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(Stream, 84);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(Stream, 168);
}
#[test]
fn test_simple() {
let elem: Element = "<stream:stream xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' xml:lang='en' version='1.0' id='abc' from='some-server.example'/>".parse().unwrap();
let stream = Stream::try_from(elem).unwrap();
assert_eq!(stream.from, Some(BareJid::domain("some-server.example")));
assert_eq!(stream.to, None);
assert_eq!(stream.id, Some(String::from("abc")));
assert_eq!(stream.version, Some(String::from("1.0")));
assert_eq!(stream.xml_lang, Some(String::from("en")));
}
}

112
xmpp-parsers/src/time.rs Normal file
View file

@ -0,0 +1,112 @@
// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 chrono::FixedOffset;
use crate::date::DateTime;
use crate::iq::{IqGetPayload, IqResultPayload};
use crate::ns;
use crate::util::error::Error;
use crate::Element;
use std::convert::TryFrom;
use std::str::FromStr;
generate_empty_element!(
/// An entity time query.
TimeQuery, "time", TIME
);
impl IqGetPayload for TimeQuery {}
/// An entity time result, containing an unique DateTime.
#[derive(Debug, Clone)]
pub struct TimeResult(pub DateTime);
impl IqResultPayload for TimeResult {}
impl TryFrom<Element> for TimeResult {
type Error = Error;
fn try_from(elem: Element) -> Result<TimeResult, Error> {
check_self!(elem, "time", TIME);
check_no_attributes!(elem, "time");
let mut tzo = None;
let mut utc = None;
for child in elem.children() {
if child.is("tzo", ns::TIME) {
if tzo.is_some() {
return Err(Error::ParseError("More than one tzo element in time."));
}
check_no_children!(child, "tzo");
check_no_attributes!(child, "tzo");
// TODO: Add a FromStr implementation to FixedOffset to avoid this hack.
let fake_date = String::from("2019-04-22T11:38:00") + &child.text();
let date_time = DateTime::from_str(&fake_date)?;
tzo = Some(date_time.timezone());
} else if child.is("utc", ns::TIME) {
if utc.is_some() {
return Err(Error::ParseError("More than one utc element in time."));
}
check_no_children!(child, "utc");
check_no_attributes!(child, "utc");
let date_time = DateTime::from_str(&child.text())?;
if date_time.timezone() != FixedOffset::east(0) {
return Err(Error::ParseError("Non-UTC timezone for utc element."));
}
utc = Some(date_time);
} else {
return Err(Error::ParseError(
"Unknown child in time element.",
));
}
}
let tzo = tzo.ok_or(Error::ParseError("Missing tzo child in time element."))?;
let utc = utc.ok_or(Error::ParseError("Missing utc child in time element."))?;
let date = utc.with_timezone(tzo);
Ok(TimeResult(date))
}
}
impl From<TimeResult> for Element {
fn from(time: TimeResult) -> Element {
Element::builder("time")
.ns(ns::TIME)
.append(Element::builder("tzo")
.append(format!("{}", time.0.timezone())))
.append(Element::builder("utc")
.append(time.0.with_timezone(FixedOffset::east(0)).format("%FT%TZ")))
.build()
}
}
#[cfg(test)]
mod tests {
use super::*;
// DateTimes size doesnt depend on the architecture.
#[test]
fn test_size() {
assert_size!(TimeQuery, 0);
assert_size!(TimeResult, 16);
}
#[test]
fn parse_response() {
let elem: Element =
"<time xmlns='urn:xmpp:time'><tzo>-06:00</tzo><utc>2006-12-19T17:58:35Z</utc></time>"
.parse()
.unwrap();
let elem1 = elem.clone();
let time = TimeResult::try_from(elem).unwrap();
assert_eq!(time.0.timezone(), FixedOffset::west(6 * 3600));
assert_eq!(time.0, DateTime::from_str("2006-12-19T12:58:35-05:00").unwrap());
let elem2 = Element::from(time);
assert_eq!(elem1, elem2);
}
}

228
xmpp-parsers/src/tune.rs Normal file
View file

@ -0,0 +1,228 @@
// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 crate::util::error::Error;
use crate::pubsub::PubSubPayload;
use crate::ns;
use crate::Element;
use std::convert::TryFrom;
generate_elem_id!(
/// The artist or performer of the song or piece.
Artist, "artist", TUNE
);
generate_elem_id!(
/// The duration of the song or piece in seconds.
Length, "length", TUNE,
u16
);
generate_elem_id!(
/// The user's rating of the song or piece, from 1 (lowest) to 10 (highest).
Rating, "rating", TUNE,
u8
);
generate_elem_id!(
/// The collection (e.g., album) or other source (e.g., a band website that hosts streams or
/// audio files).
Source, "source", TUNE
);
generate_elem_id!(
/// The title of the song or piece.
Title, "title", TUNE
);
generate_elem_id!(
/// A unique identifier for the tune; e.g., the track number within a collection or the
/// specific URI for the object (e.g., a stream or audio file).
Track, "track", TUNE
);
generate_elem_id!(
/// A URI or URL pointing to information about the song, collection, or artist.
Uri, "uri", TUNE
);
/// Container for formatted text.
#[derive(Debug, Clone)]
pub struct Tune {
/// The artist or performer of the song or piece.
artist: Option<Artist>,
/// The duration of the song or piece in seconds.
length: Option<Length>,
/// The user's rating of the song or piece, from 1 (lowest) to 10 (highest).
rating: Option<Rating>,
/// The collection (e.g., album) or other source (e.g., a band website that hosts streams or
/// audio files).
source: Option<Source>,
/// The title of the song or piece.
title: Option<Title>,
/// A unique identifier for the tune; e.g., the track number within a collection or the
/// specific URI for the object (e.g., a stream or audio file).
track: Option<Track>,
/// A URI or URL pointing to information about the song, collection, or artist.
uri: Option<Uri>,
}
impl PubSubPayload for Tune {}
impl Tune {
fn new() -> Tune {
Tune {
artist: None,
length: None,
rating: None,
source: None,
title: None,
track: None,
uri: None,
}
}
}
impl TryFrom<Element> for Tune {
type Error = Error;
fn try_from(elem: Element) -> Result<Tune, Error> {
check_self!(elem, "tune", TUNE);
check_no_attributes!(elem, "tune");
let mut tune = Tune::new();
for child in elem.children() {
if child.is("artist", ns::TUNE) {
if tune.artist.is_some() {
return Err(Error::ParseError("Tune cant have more than one artist."));
}
tune.artist = Some(Artist::try_from(child.clone())?);
} else if child.is("length", ns::TUNE) {
if tune.length.is_some() {
return Err(Error::ParseError("Tune cant have more than one length."));
}
tune.length = Some(Length::try_from(child.clone())?);
} else if child.is("rating", ns::TUNE) {
if tune.rating.is_some() {
return Err(Error::ParseError("Tune cant have more than one rating."));
}
tune.rating = Some(Rating::try_from(child.clone())?);
} else if child.is("source", ns::TUNE) {
if tune.source.is_some() {
return Err(Error::ParseError("Tune cant have more than one source."));
}
tune.source = Some(Source::try_from(child.clone())?);
} else if child.is("title", ns::TUNE) {
if tune.title.is_some() {
return Err(Error::ParseError("Tune cant have more than one title."));
}
tune.title = Some(Title::try_from(child.clone())?);
} else if child.is("track", ns::TUNE) {
if tune.track.is_some() {
return Err(Error::ParseError("Tune cant have more than one track."));
}
tune.track = Some(Track::try_from(child.clone())?);
} else if child.is("uri", ns::TUNE) {
if tune.uri.is_some() {
return Err(Error::ParseError("Tune cant have more than one uri."));
}
tune.uri = Some(Uri::try_from(child.clone())?);
} else {
return Err(Error::ParseError("Unknown element in User Tune."));
}
}
Ok(tune)
}
}
impl From<Tune> for Element {
fn from(tune: Tune) -> Element {
Element::builder("tune")
.ns(ns::TUNE)
.append_all(tune.artist)
.append_all(tune.length)
.append_all(tune.rating)
.append_all(tune.source)
.append_all(tune.title)
.append_all(tune.track)
.append_all(tune.uri)
.build()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
#[cfg(target_pointer_width = "32")]
#[test]
#[ignore]
fn test_size() {
assert_size!(Tune, 64);
assert_size!(Artist, 12);
assert_size!(Length, 2);
assert_size!(Rating, 1);
assert_size!(Source, 12);
assert_size!(Title, 12);
assert_size!(Track, 12);
assert_size!(Uri, 12);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(Tune, 128);
assert_size!(Artist, 24);
assert_size!(Length, 2);
assert_size!(Rating, 1);
assert_size!(Source, 24);
assert_size!(Title, 24);
assert_size!(Track, 24);
assert_size!(Uri, 24);
}
#[test]
fn empty() {
let elem: Element = "<tune xmlns='http://jabber.org/protocol/tune'/>"
.parse()
.unwrap();
let elem2 = elem.clone();
let tune = Tune::try_from(elem).unwrap();
assert!(tune.artist.is_none());
assert!(tune.length.is_none());
assert!(tune.rating.is_none());
assert!(tune.source.is_none());
assert!(tune.title.is_none());
assert!(tune.track.is_none());
assert!(tune.uri.is_none());
let elem3 = tune.into();
assert_eq!(elem2, elem3);
}
#[test]
fn full() {
let elem: Element = "<tune xmlns='http://jabber.org/protocol/tune'><artist>Yes</artist><length>686</length><rating>8</rating><source>Yessongs</source><title>Heart of the Sunrise</title><track>3</track><uri>http://www.yesworld.com/lyrics/Fragile.html#9</uri></tune>"
.parse()
.unwrap();
let tune = Tune::try_from(elem).unwrap();
assert_eq!(tune.artist, Some(Artist::from_str("Yes").unwrap()));
assert_eq!(tune.length, Some(Length(686)));
assert_eq!(tune.rating, Some(Rating(8)));
assert_eq!(tune.source, Some(Source::from_str("Yessongs").unwrap()));
assert_eq!(tune.title, Some(Title::from_str("Heart of the Sunrise").unwrap()));
assert_eq!(tune.track, Some(Track::from_str("3").unwrap()));
assert_eq!(tune.uri, Some(Uri::from_str("http://www.yesworld.com/lyrics/Fragile.html#9").unwrap()));
}
}

View file

@ -0,0 +1,125 @@
// Copyright (c) 2017 Astro <astro@spaceboyz.net>
//
// 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 minidom::{Element, Node};
pub trait NamespaceAwareCompare {
/// Namespace-aware comparison for tests
fn compare_to(&self, other: &Self) -> bool;
}
impl NamespaceAwareCompare for Node {
fn compare_to(&self, other: &Self) -> bool {
match (self, other) {
(&Node::Element(ref elem1), &Node::Element(ref elem2)) => {
Element::compare_to(elem1, elem2)
}
(&Node::Text(ref text1), &Node::Text(ref text2)) => text1 == text2,
_ => false,
}
}
}
impl NamespaceAwareCompare for Element {
fn compare_to(&self, other: &Self) -> bool {
if self.name() == other.name() && self.ns() == other.ns() && self.attrs().eq(other.attrs())
{
let child_elems = self.children().count();
let text_is_whitespace = self
.texts()
.all(|text| text.chars().all(char::is_whitespace));
if child_elems > 0 && text_is_whitespace {
// Ignore all the whitespace text nodes
self.children()
.zip(other.children())
.all(|(node1, node2)| node1.compare_to(node2))
} else {
// Compare with text nodes
self.nodes()
.zip(other.nodes())
.all(|(node1, node2)| node1.compare_to(node2))
}
} else {
false
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Element;
#[test]
fn simple() {
let elem1: Element = "<a a='b'>x <l/> 3</a>".parse().unwrap();
let elem2: Element = "<a a='b'>x <l/> 3</a>".parse().unwrap();
assert!(elem1.compare_to(&elem2));
}
#[test]
fn wrong_attr_name() {
let elem1: Element = "<a a='b'>x 3</a>".parse().unwrap();
let elem2: Element = "<a c='b'>x 3</a>".parse().unwrap();
assert!(!elem1.compare_to(&elem2));
}
#[test]
fn wrong_attr_value() {
let elem1: Element = "<a a='b'>x 3</a>".parse().unwrap();
let elem2: Element = "<a a='c'>x 3</a>".parse().unwrap();
assert!(!elem1.compare_to(&elem2));
}
#[test]
fn attr_order() {
let elem1: Element = "<e1 a='b' c='d'/>".parse().unwrap();
let elem2: Element = "<e1 c='d' a='b'/>".parse().unwrap();
assert!(elem1.compare_to(&elem2));
}
#[test]
fn wrong_texts() {
let elem1: Element = "<e1>foo</e1>".parse().unwrap();
let elem2: Element = "<e1>bar</e1>".parse().unwrap();
assert!(!elem1.compare_to(&elem2));
}
#[test]
fn children() {
let elem1: Element = "<e1><foo/><bar/></e1>".parse().unwrap();
let elem2: Element = "<e1><foo/><bar/></e1>".parse().unwrap();
assert!(elem1.compare_to(&elem2));
}
#[test]
fn wrong_children() {
let elem1: Element = "<e1><foo/></e1>".parse().unwrap();
let elem2: Element = "<e1><bar/></e1>".parse().unwrap();
assert!(!elem1.compare_to(&elem2));
}
#[test]
fn xmlns_wrong() {
let elem1: Element = "<e1 xmlns='ns1'><foo/></e1>".parse().unwrap();
let elem2: Element = "<e1 xmlns='ns2'><foo/></e1>".parse().unwrap();
assert!(!elem1.compare_to(&elem2));
}
#[test]
fn xmlns_other_prefix() {
let elem1: Element = "<e1 xmlns='ns1'><foo/></e1>".parse().unwrap();
let elem2: Element = "<x:e1 xmlns:x='ns1'><x:foo/></x:e1>".parse().unwrap();
assert!(elem1.compare_to(&elem2));
}
#[test]
fn xmlns_dup() {
let elem1: Element = "<e1 xmlns='ns1'><foo/></e1>".parse().unwrap();
let elem2: Element = "<e1 xmlns='ns1'><foo xmlns='ns1'/></e1>".parse().unwrap();
assert!(elem1.compare_to(&elem2));
}
}

View file

@ -0,0 +1,105 @@
// Copyright (c) 2017-2018 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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::error::Error as StdError;
use std::fmt;
/// Contains one of the potential errors triggered while parsing an
/// [Element](../struct.Element.html) into a specialised struct.
#[derive(Debug)]
pub enum Error {
/// The usual error when parsing something.
///
/// TODO: use a structured error so the user can report it better, instead
/// of a freeform string.
ParseError(&'static str),
/// Generated when some base64 content fails to decode, usually due to
/// extra characters.
Base64Error(base64::DecodeError),
/// Generated when text which should be an integer fails to parse.
ParseIntError(std::num::ParseIntError),
/// Generated when text which should be a string fails to parse.
ParseStringError(std::string::ParseError),
/// Generated when text which should be an IP address (IPv4 or IPv6) fails
/// to parse.
ParseAddrError(std::net::AddrParseError),
/// Generated when text which should be a [JID](../../jid/struct.Jid.html)
/// fails to parse.
JidParseError(jid::JidParseError),
/// Generated when text which should be a
/// [DateTime](../date/struct.DateTime.html) fails to parse.
ChronoParseError(chrono::ParseError),
}
impl StdError for Error {
fn cause(&self) -> Option<&dyn StdError> {
match self {
Error::ParseError(_) => None,
Error::Base64Error(e) => Some(e),
Error::ParseIntError(e) => Some(e),
Error::ParseStringError(e) => Some(e),
Error::ParseAddrError(e) => Some(e),
Error::JidParseError(e) => Some(e),
Error::ChronoParseError(e) => Some(e),
}
}
}
impl fmt::Display for Error {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
match self {
Error::ParseError(s) => write!(fmt, "parse error: {}", s),
Error::Base64Error(e) => write!(fmt, "base64 error: {}", e),
Error::ParseIntError(e) => write!(fmt, "integer parsing error: {}", e),
Error::ParseStringError(e) => write!(fmt, "string parsing error: {}", e),
Error::ParseAddrError(e) => write!(fmt, "IP address parsing error: {}", e),
Error::JidParseError(e) => write!(fmt, "JID parsing error: {}", e),
Error::ChronoParseError(e) => write!(fmt, "time parsing error: {}", e),
}
}
}
impl From<base64::DecodeError> for Error {
fn from(err: base64::DecodeError) -> Error {
Error::Base64Error(err)
}
}
impl From<std::num::ParseIntError> for Error {
fn from(err: std::num::ParseIntError) -> Error {
Error::ParseIntError(err)
}
}
impl From<std::string::ParseError> for Error {
fn from(err: std::string::ParseError) -> Error {
Error::ParseStringError(err)
}
}
impl From<std::net::AddrParseError> for Error {
fn from(err: std::net::AddrParseError) -> Error {
Error::ParseAddrError(err)
}
}
impl From<jid::JidParseError> for Error {
fn from(err: jid::JidParseError) -> Error {
Error::JidParseError(err)
}
}
impl From<chrono::ParseError> for Error {
fn from(err: chrono::ParseError) -> Error {
Error::ChronoParseError(err)
}
}

View file

@ -0,0 +1,119 @@
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 crate::util::error::Error;
use jid::Jid;
use std::str::FromStr;
/// Codec for text content.
pub struct Text;
impl Text {
pub fn decode(s: &str) -> Result<String, Error> {
Ok(s.to_owned())
}
pub fn encode(string: &str) -> Option<String> {
Some(string.to_owned())
}
}
/// Codec for plain text content.
pub struct PlainText;
impl PlainText {
pub fn decode(s: &str) -> Result<Option<String>, Error> {
Ok(match s {
"" => None,
text => Some(text.to_owned()),
})
}
pub fn encode(string: &Option<String>) -> Option<String> {
string.as_ref().map(ToOwned::to_owned)
}
}
/// Codec for trimmed plain text content.
pub struct TrimmedPlainText;
impl TrimmedPlainText {
pub fn decode(s: &str) -> Result<String, Error> {
Ok(match s.trim() {
"" => return Err(Error::ParseError("URI missing in uri.")),
text => text.to_owned(),
})
}
pub fn encode(string: &str) -> Option<String> {
Some(string.to_owned())
}
}
/// Codec wrapping base64 encode/decode.
pub struct Base64;
impl Base64 {
pub fn decode(s: &str) -> Result<Vec<u8>, Error> {
Ok(base64::decode(s)?)
}
pub fn encode(b: &[u8]) -> Option<String> {
Some(base64::encode(b))
}
}
/// Codec wrapping base64 encode/decode, while ignoring whitespace characters.
pub struct WhitespaceAwareBase64;
impl WhitespaceAwareBase64 {
pub fn decode(s: &str) -> Result<Vec<u8>, Error> {
let s: String = s.chars().filter(|ch| *ch != ' ' && *ch != '\n' && *ch != '\t').collect();
Ok(base64::decode(&s)?)
}
pub fn encode(b: &[u8]) -> Option<String> {
Some(base64::encode(b))
}
}
/// Codec for colon-separated bytes of uppercase hexadecimal.
pub struct ColonSeparatedHex;
impl ColonSeparatedHex {
pub fn decode(s: &str) -> Result<Vec<u8>, Error> {
let mut bytes = vec![];
for i in 0..(1 + s.len()) / 3 {
let byte = u8::from_str_radix(&s[3 * i..3 * i + 2], 16)?;
if 3 * i + 2 < s.len() {
assert_eq!(&s[3 * i + 2..3 * i + 3], ":");
}
bytes.push(byte);
}
Ok(bytes)
}
pub fn encode(b: &[u8]) -> Option<String> {
let mut bytes = vec![];
for byte in b {
bytes.push(format!("{:02X}", byte));
}
Some(bytes.join(":"))
}
}
/// Codec for a JID.
pub struct JidCodec;
impl JidCodec {
pub fn decode(s: &str) -> Result<Jid, Error> {
Ok(Jid::from_str(s)?)
}
pub fn encode(jid: &Jid) -> Option<String> {
Some(jid.to_string())
}
}

View file

@ -0,0 +1,808 @@
// Copyright (c) 2017-2018 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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/.
macro_rules! get_attr {
($elem:ident, $attr:tt, $type:tt) => {
get_attr!($elem, $attr, $type, value, value.parse()?)
};
($elem:ident, $attr:tt, OptionEmpty, $value:ident, $func:expr) => {
match $elem.attr($attr) {
Some("") => None,
Some($value) => Some($func),
None => None,
}
};
($elem:ident, $attr:tt, Option, $value:ident, $func:expr) => {
match $elem.attr($attr) {
Some($value) => Some($func),
None => None,
}
};
($elem:ident, $attr:tt, Required, $value:ident, $func:expr) => {
match $elem.attr($attr) {
Some($value) => $func,
None => {
return Err(crate::util::error::Error::ParseError(concat!(
"Required attribute '",
$attr,
"' missing."
)));
}
}
};
($elem:ident, $attr:tt, RequiredNonEmpty, $value:ident, $func:expr) => {
match $elem.attr($attr) {
Some("") => {
return Err(crate::util::error::Error::ParseError(concat!(
"Required attribute '",
$attr,
"' must not be empty."
)));
},
Some($value) => $func,
None => {
return Err(crate::util::error::Error::ParseError(concat!(
"Required attribute '",
$attr,
"' missing."
)));
}
}
};
($elem:ident, $attr:tt, Default, $value:ident, $func:expr) => {
match $elem.attr($attr) {
Some($value) => $func,
None => ::std::default::Default::default(),
}
};
}
macro_rules! generate_attribute {
($(#[$meta:meta])* $elem:ident, $name:tt, {$($(#[$a_meta:meta])* $a:ident => $b:tt),+,}) => (
generate_attribute!($(#[$meta])* $elem, $name, {$($(#[$a_meta])* $a => $b),+});
);
($(#[$meta:meta])* $elem:ident, $name:tt, {$($(#[$a_meta:meta])* $a:ident => $b:tt),+,}, Default = $default:ident) => (
generate_attribute!($(#[$meta])* $elem, $name, {$($(#[$a_meta])* $a => $b),+}, Default = $default);
);
($(#[$meta:meta])* $elem:ident, $name:tt, {$($(#[$a_meta:meta])* $a:ident => $b:tt),+}) => (
$(#[$meta])*
#[derive(Debug, Clone, PartialEq)]
pub enum $elem {
$(
$(#[$a_meta])*
$a
),+
}
impl ::std::str::FromStr for $elem {
type Err = crate::util::error::Error;
fn from_str(s: &str) -> Result<$elem, crate::util::error::Error> {
Ok(match s {
$($b => $elem::$a),+,
_ => return Err(crate::util::error::Error::ParseError(concat!("Unknown value for '", $name, "' attribute."))),
})
}
}
impl std::fmt::Display for $elem {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
write!(fmt, "{}", match self {
$($elem::$a => $b),+
})
}
}
impl ::minidom::IntoAttributeValue for $elem {
fn into_attribute_value(self) -> Option<String> {
Some(String::from(match self {
$($elem::$a => $b),+
}))
}
}
);
($(#[$meta:meta])* $elem:ident, $name:tt, {$($(#[$a_meta:meta])* $a:ident => $b:tt),+}, Default = $default:ident) => (
$(#[$meta])*
#[derive(Debug, Clone, PartialEq)]
pub enum $elem {
$(
$(#[$a_meta])*
$a
),+
}
impl ::std::str::FromStr for $elem {
type Err = crate::util::error::Error;
fn from_str(s: &str) -> Result<$elem, crate::util::error::Error> {
Ok(match s {
$($b => $elem::$a),+,
_ => return Err(crate::util::error::Error::ParseError(concat!("Unknown value for '", $name, "' attribute."))),
})
}
}
impl ::minidom::IntoAttributeValue for $elem {
#[allow(unreachable_patterns)]
fn into_attribute_value(self) -> Option<String> {
Some(String::from(match self {
$elem::$default => return None,
$($elem::$a => $b),+
}))
}
}
impl ::std::default::Default for $elem {
fn default() -> $elem {
$elem::$default
}
}
);
($(#[$meta:meta])* $elem:ident, $name:tt, ($(#[$meta_symbol:meta])* $symbol:ident => $value:tt)) => (
$(#[$meta])*
#[derive(Debug, Clone, PartialEq)]
pub enum $elem {
$(#[$meta_symbol])*
$symbol,
/// Value when absent.
None,
}
impl ::std::str::FromStr for $elem {
type Err = crate::util::error::Error;
fn from_str(s: &str) -> Result<Self, crate::util::error::Error> {
Ok(match s {
$value => $elem::$symbol,
_ => return Err(crate::util::error::Error::ParseError(concat!("Unknown value for '", $name, "' attribute."))),
})
}
}
impl ::minidom::IntoAttributeValue for $elem {
fn into_attribute_value(self) -> Option<String> {
match self {
$elem::$symbol => Some(String::from($value)),
$elem::None => None
}
}
}
impl ::std::default::Default for $elem {
fn default() -> $elem {
$elem::None
}
}
);
($(#[$meta:meta])* $elem:ident, $name:tt, bool) => (
$(#[$meta])*
#[derive(Debug, Clone, PartialEq)]
pub enum $elem {
/// True value, represented by either 'true' or '1'.
True,
/// False value, represented by either 'false' or '0'.
False,
}
impl ::std::str::FromStr for $elem {
type Err = crate::util::error::Error;
fn from_str(s: &str) -> Result<Self, crate::util::error::Error> {
Ok(match s {
"true" | "1" => $elem::True,
"false" | "0" => $elem::False,
_ => return Err(crate::util::error::Error::ParseError(concat!("Unknown value for '", $name, "' attribute."))),
})
}
}
impl ::minidom::IntoAttributeValue for $elem {
fn into_attribute_value(self) -> Option<String> {
match self {
$elem::True => Some(String::from("true")),
$elem::False => None
}
}
}
impl ::std::default::Default for $elem {
fn default() -> $elem {
$elem::False
}
}
);
($(#[$meta:meta])* $elem:ident, $name:tt, $type:tt, Default = $default:expr) => (
$(#[$meta])*
#[derive(Debug, Clone, PartialEq)]
pub struct $elem(pub $type);
impl ::std::str::FromStr for $elem {
type Err = crate::util::error::Error;
fn from_str(s: &str) -> Result<Self, crate::util::error::Error> {
Ok($elem($type::from_str(s)?))
}
}
impl ::minidom::IntoAttributeValue for $elem {
fn into_attribute_value(self) -> Option<String> {
match self {
$elem($default) => None,
$elem(value) => Some(format!("{}", value)),
}
}
}
impl ::std::default::Default for $elem {
fn default() -> $elem {
$elem($default)
}
}
);
}
macro_rules! generate_element_enum {
($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, {$($(#[$enum_meta:meta])* $enum:ident => $enum_name:tt),+,}) => (
generate_element_enum!($(#[$meta])* $elem, $name, $ns, {$($(#[$enum_meta])* $enum => $enum_name),+});
);
($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, {$($(#[$enum_meta:meta])* $enum:ident => $enum_name:tt),+}) => (
$(#[$meta])*
#[derive(Debug, Clone, PartialEq)]
pub enum $elem {
$(
$(#[$enum_meta])*
$enum
),+
}
impl ::std::convert::TryFrom<crate::Element> for $elem {
type Error = crate::util::error::Error;
fn try_from(elem: crate::Element) -> Result<$elem, crate::util::error::Error> {
check_ns_only!(elem, $name, $ns);
check_no_children!(elem, $name);
check_no_attributes!(elem, $name);
Ok(match elem.name() {
$($enum_name => $elem::$enum,)+
_ => return Err(crate::util::error::Error::ParseError(concat!("This is not a ", $name, " element."))),
})
}
}
impl From<$elem> for crate::Element {
fn from(elem: $elem) -> crate::Element {
crate::Element::builder(
match elem {
$($elem::$enum => $enum_name,)+
}
)
.ns(crate::ns::$ns)
.build()
}
}
impl From<$elem> for ::minidom::Node {
fn from(elem: $elem) -> ::minidom::Node {
::minidom::Node::Element(elem.into())
}
}
);
}
macro_rules! generate_attribute_enum {
($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, $attr:tt, {$($(#[$enum_meta:meta])* $enum:ident => $enum_name:tt),+,}) => (
generate_attribute_enum!($(#[$meta])* $elem, $name, $ns, $attr, {$($(#[$enum_meta])* $enum => $enum_name),+});
);
($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, $attr:tt, {$($(#[$enum_meta:meta])* $enum:ident => $enum_name:tt),+}) => (
$(#[$meta])*
#[derive(Debug, Clone, PartialEq)]
pub enum $elem {
$(
$(#[$enum_meta])*
$enum
),+
}
impl ::std::convert::TryFrom<crate::Element> for $elem {
type Error = crate::util::error::Error;
fn try_from(elem: crate::Element) -> Result<$elem, crate::util::error::Error> {
check_ns_only!(elem, $name, $ns);
check_no_children!(elem, $name);
check_no_unknown_attributes!(elem, $name, [$attr]);
Ok(match get_attr!(elem, $attr, Required) {
$($enum_name => $elem::$enum,)+
_ => return Err(crate::util::error::Error::ParseError(concat!("Invalid ", $name, " ", $attr, " value."))),
})
}
}
impl From<$elem> for crate::Element {
fn from(elem: $elem) -> crate::Element {
crate::Element::builder($name)
.ns(crate::ns::$ns)
.attr($attr, match elem {
$($elem::$enum => $enum_name,)+
})
.build()
}
}
impl From<$elem> for ::minidom::Node {
fn from(elem: $elem) -> ::minidom::Node {
::minidom::Node::Element(elem.into())
}
}
);
}
macro_rules! check_self {
($elem:ident, $name:tt, $ns:ident) => {
check_self!($elem, $name, $ns, $name);
};
($elem:ident, $name:tt, $ns:ident, $pretty_name:tt) => {
if !$elem.is($name, crate::ns::$ns) {
return Err(crate::util::error::Error::ParseError(concat!(
"This is not a ",
$pretty_name,
" element."
)));
}
};
}
macro_rules! check_ns_only {
($elem:ident, $name:tt, $ns:ident) => {
if !$elem.has_ns(crate::ns::$ns) {
return Err(crate::util::error::Error::ParseError(concat!(
"This is not a ",
$name,
" element."
)));
}
};
}
macro_rules! check_no_children {
($elem:ident, $name:tt) => {
#[cfg(not(feature = "disable-validation"))]
for _ in $elem.children() {
return Err(crate::util::error::Error::ParseError(concat!(
"Unknown child in ",
$name,
" element."
)));
}
};
}
macro_rules! check_no_attributes {
($elem:ident, $name:tt) => {
#[cfg(not(feature = "disable-validation"))]
for _ in $elem.attrs() {
return Err(crate::util::error::Error::ParseError(concat!(
"Unknown attribute in ",
$name,
" element."
)));
}
};
}
macro_rules! check_no_unknown_attributes {
($elem:ident, $name:tt, [$($attr:tt),*]) => (
#[cfg(not(feature = "disable-validation"))]
for (_attr, _) in $elem.attrs() {
$(
if _attr == $attr {
continue;
}
)*
return Err(crate::util::error::Error::ParseError(concat!("Unknown attribute in ", $name, " element.")));
}
);
}
macro_rules! generate_empty_element {
($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident) => (
$(#[$meta])*
#[derive(Debug, Clone)]
pub struct $elem;
impl ::std::convert::TryFrom<crate::Element> for $elem {
type Error = crate::util::error::Error;
fn try_from(elem: crate::Element) -> Result<$elem, crate::util::error::Error> {
check_self!(elem, $name, $ns);
check_no_children!(elem, $name);
check_no_attributes!(elem, $name);
Ok($elem)
}
}
impl From<$elem> for crate::Element {
fn from(_: $elem) -> crate::Element {
crate::Element::builder($name)
.ns(crate::ns::$ns)
.build()
}
}
impl From<$elem> for ::minidom::Node {
fn from(elem: $elem) -> ::minidom::Node {
::minidom::Node::Element(elem.into())
}
}
);
}
macro_rules! generate_id {
($(#[$meta:meta])* $elem:ident) => (
$(#[$meta])*
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct $elem(pub String);
impl ::std::str::FromStr for $elem {
type Err = crate::util::error::Error;
fn from_str(s: &str) -> Result<$elem, crate::util::error::Error> {
// TODO: add a way to parse that differently when needed.
Ok($elem(String::from(s)))
}
}
impl ::minidom::IntoAttributeValue for $elem {
fn into_attribute_value(self) -> Option<String> {
Some(self.0)
}
}
);
}
macro_rules! generate_elem_id {
($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident) => (
generate_elem_id!($(#[$meta])* $elem, $name, $ns, String);
impl ::std::str::FromStr for $elem {
type Err = crate::util::error::Error;
fn from_str(s: &str) -> Result<$elem, crate::util::error::Error> {
// TODO: add a way to parse that differently when needed.
Ok($elem(String::from(s)))
}
}
);
($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, $type:ty) => (
$(#[$meta])*
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct $elem(pub $type);
impl ::std::convert::TryFrom<crate::Element> for $elem {
type Error = crate::util::error::Error;
fn try_from(elem: crate::Element) -> Result<$elem, crate::util::error::Error> {
check_self!(elem, $name, $ns);
check_no_children!(elem, $name);
check_no_attributes!(elem, $name);
// TODO: add a way to parse that differently when needed.
Ok($elem(elem.text().parse()?))
}
}
impl From<$elem> for crate::Element {
fn from(elem: $elem) -> crate::Element {
crate::Element::builder($name)
.ns(crate::ns::$ns)
.append(elem.0.to_string())
.build()
}
}
impl From<$elem> for ::minidom::Node {
fn from(elem: $elem) -> ::minidom::Node {
::minidom::Node::Element(elem.into())
}
}
);
}
macro_rules! decl_attr {
(OptionEmpty, $type:ty) => (
Option<$type>
);
(Option, $type:ty) => (
Option<$type>
);
(Required, $type:ty) => (
$type
);
(RequiredNonEmpty, $type:ty) => (
$type
);
(Default, $type:ty) => (
$type
);
}
macro_rules! start_decl {
(Vec, $type:ty) => (
Vec<$type>
);
(Option, $type:ty) => (
Option<$type>
);
(Required, $type:ty) => (
$type
);
(Present, $type:ty) => (
bool
);
}
macro_rules! start_parse_elem {
($temp:ident: Vec) => {
let mut $temp = Vec::new();
};
($temp:ident: Option) => {
let mut $temp = None;
};
($temp:ident: Required) => {
let mut $temp = None;
};
($temp:ident: Present) => {
let mut $temp = false;
};
}
macro_rules! do_parse {
($elem:ident, Element) => {
$elem.clone()
};
($elem:ident, String) => {
$elem.text()
};
($elem:ident, $constructor:ident) => {
$constructor::try_from($elem.clone())?
};
}
macro_rules! do_parse_elem {
($temp:ident: Vec = $constructor:ident => $elem:ident, $name:tt, $parent_name:tt) => {
$temp.push(do_parse!($elem, $constructor));
};
($temp:ident: Option = $constructor:ident => $elem:ident, $name:tt, $parent_name:tt) => {
if $temp.is_some() {
return Err(crate::util::error::Error::ParseError(concat!(
"Element ",
$parent_name,
" must not have more than one ",
$name,
" child."
)));
}
$temp = Some(do_parse!($elem, $constructor));
};
($temp:ident: Required = $constructor:ident => $elem:ident, $name:tt, $parent_name:tt) => {
if $temp.is_some() {
return Err(crate::util::error::Error::ParseError(concat!(
"Element ",
$parent_name,
" must not have more than one ",
$name,
" child."
)));
}
$temp = Some(do_parse!($elem, $constructor));
};
($temp:ident: Present = $constructor:ident => $elem:ident, $name:tt, $parent_name:tt) => {
if $temp {
return Err(crate::util::error::Error::ParseError(concat!(
"Element ",
$parent_name,
" must not have more than one ",
$name,
" child."
)));
}
$temp = true;
};
}
macro_rules! finish_parse_elem {
($temp:ident: Vec = $name:tt, $parent_name:tt) => {
$temp
};
($temp:ident: Option = $name:tt, $parent_name:tt) => {
$temp
};
($temp:ident: Required = $name:tt, $parent_name:tt) => {
$temp.ok_or(crate::util::error::Error::ParseError(concat!(
"Missing child ",
$name,
" in ",
$parent_name,
" element."
)))?
};
($temp:ident: Present = $name:tt, $parent_name:tt) => {
$temp
};
}
macro_rules! generate_serialiser {
($builder:ident, $parent:ident, $elem:ident, Required, String, ($name:tt, $ns:ident)) => {
$builder.append(
crate::Element::builder($name)
.ns(crate::ns::$ns)
.append(::minidom::Node::Text($parent.$elem))
)
};
($builder:ident, $parent:ident, $elem:ident, Option, String, ($name:tt, $ns:ident)) => {
$builder.append_all($parent.$elem.map(|elem| {
crate::Element::builder($name)
.ns(crate::ns::$ns)
.append(::minidom::Node::Text(elem))
})
)
};
($builder:ident, $parent:ident, $elem:ident, Option, $constructor:ident, ($name:tt, *)) => {
$builder.append_all($parent.$elem.map(|elem| {
crate::Element::builder($name)
.ns(elem.get_ns())
.append(::minidom::Node::Element(crate::Element::from(elem)))
})
)
};
($builder:ident, $parent:ident, $elem:ident, Option, $constructor:ident, ($name:tt, $ns:ident)) => {
$builder.append_all($parent.$elem.map(|elem| {
crate::Element::builder($name)
.ns(crate::ns::$ns)
.append(::minidom::Node::Element(crate::Element::from(elem)))
})
)
};
($builder:ident, $parent:ident, $elem:ident, Vec, $constructor:ident, ($name:tt, $ns:ident)) => {
$builder.append_all($parent.$elem.into_iter())
};
($builder:ident, $parent:ident, $elem:ident, Present, $constructor:ident, ($name:tt, $ns:ident)) => {
$builder.append(::minidom::Node::Element(crate::Element::builder($name).ns(crate::ns::$ns).build()))
};
($builder:ident, $parent:ident, $elem:ident, $_:ident, $constructor:ident, ($name:tt, $ns:ident)) => {
$builder.append(::minidom::Node::Element(crate::Element::from($parent.$elem)))
};
}
macro_rules! generate_child_test {
($child:ident, $name:tt, *) => {
true
};
($child:ident, $name:tt, $ns:tt) => {
$child.is($name, crate::ns::$ns)
};
}
macro_rules! generate_element {
($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, attributes: [$($(#[$attr_meta:meta])* $attr:ident: $attr_action:tt<$attr_type:ty> = $attr_name:tt),+,]) => (
generate_element!($(#[$meta])* $elem, $name, $ns, attributes: [$($(#[$attr_meta])* $attr: $attr_action<$attr_type> = $attr_name),*], children: []);
);
($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, attributes: [$($(#[$attr_meta:meta])* $attr:ident: $attr_action:tt<$attr_type:ty> = $attr_name:tt),+]) => (
generate_element!($(#[$meta])* $elem, $name, $ns, attributes: [$($(#[$attr_meta])* $attr: $attr_action<$attr_type> = $attr_name),*], children: []);
);
($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, children: [$($(#[$child_meta:meta])* $child_ident:ident: $coucou:tt<$child_type:ty> = ($child_name:tt, $child_ns:tt) => $child_constructor:ident),*]) => (
generate_element!($(#[$meta])* $elem, $name, $ns, attributes: [], children: [$($(#[$child_meta])* $child_ident: $coucou<$child_type> = ($child_name, $child_ns) => $child_constructor),*]);
);
($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, attributes: [$($(#[$attr_meta:meta])* $attr:ident: $attr_action:tt<$attr_type:ty> = $attr_name:tt),*,], children: [$($(#[$child_meta:meta])* $child_ident:ident: $coucou:tt<$child_type:ty> = ($child_name:tt, $child_ns:tt) => $child_constructor:ident),*]) => (
generate_element!($(#[$meta])* $elem, $name, $ns, attributes: [$($(#[$attr_meta])* $attr: $attr_action<$attr_type> = $attr_name),*], children: [$($(#[$child_meta])* $child_ident: $coucou<$child_type> = ($child_name, $child_ns) => $child_constructor),*]);
);
($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, text: ($(#[$text_meta:meta])* $text_ident:ident: $codec:ident < $text_type:ty >)) => (
generate_element!($(#[$meta])* $elem, $name, $ns, attributes: [], children: [], text: ($(#[$text_meta])* $text_ident: $codec<$text_type>));
);
($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, attributes: [$($(#[$attr_meta:meta])* $attr:ident: $attr_action:tt<$attr_type:ty> = $attr_name:tt),+], text: ($(#[$text_meta:meta])* $text_ident:ident: $codec:ident < $text_type:ty >)) => (
generate_element!($(#[$meta])* $elem, $name, $ns, attributes: [$($(#[$attr_meta])* $attr: $attr_action<$attr_type> = $attr_name),*], children: [], text: ($(#[$text_meta])* $text_ident: $codec<$text_type>));
);
($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, attributes: [$($(#[$attr_meta:meta])* $attr:ident: $attr_action:tt<$attr_type:ty> = $attr_name:tt),*], children: [$($(#[$child_meta:meta])* $child_ident:ident: $coucou:tt<$child_type:ty> = ($child_name:tt, $child_ns:tt) => $child_constructor:ident),*] $(, text: ($(#[$text_meta:meta])* $text_ident:ident: $codec:ident < $text_type:ty >))*) => (
$(#[$meta])*
#[derive(Debug, Clone)]
pub struct $elem {
$(
$(#[$attr_meta])*
pub $attr: decl_attr!($attr_action, $attr_type),
)*
$(
$(#[$child_meta])*
pub $child_ident: start_decl!($coucou, $child_type),
)*
$(
$(#[$text_meta])*
pub $text_ident: $text_type,
)*
}
impl ::std::convert::TryFrom<crate::Element> for $elem {
type Error = crate::util::error::Error;
fn try_from(elem: crate::Element) -> Result<$elem, crate::util::error::Error> {
check_self!(elem, $name, $ns);
check_no_unknown_attributes!(elem, $name, [$($attr_name),*]);
$(
start_parse_elem!($child_ident: $coucou);
)*
for _child in elem.children() {
$(
if generate_child_test!(_child, $child_name, $child_ns) {
do_parse_elem!($child_ident: $coucou = $child_constructor => _child, $child_name, $name);
continue;
}
)*
return Err(crate::util::error::Error::ParseError(concat!("Unknown child in ", $name, " element.")));
}
Ok($elem {
$(
$attr: get_attr!(elem, $attr_name, $attr_action),
)*
$(
$child_ident: finish_parse_elem!($child_ident: $coucou = $child_name, $name),
)*
$(
$text_ident: $codec::decode(&elem.text())?,
)*
})
}
}
impl From<$elem> for crate::Element {
fn from(elem: $elem) -> crate::Element {
let mut builder = crate::Element::builder($name)
.ns(crate::ns::$ns);
$(
builder = builder.attr($attr_name, elem.$attr);
)*
$(
builder = generate_serialiser!(builder, elem, $child_ident, $coucou, $child_constructor, ($child_name, $child_ns));
)*
$(
builder = builder.append_all($codec::encode(&elem.$text_ident).map(::minidom::Node::Text).into_iter());
)*
builder.build()
}
}
impl From<$elem> for ::minidom::Node {
fn from(elem: $elem) -> ::minidom::Node {
::minidom::Node::Element(elem.into())
}
}
);
}
#[cfg(test)]
macro_rules! assert_size (
($t:ty, $sz:expr) => (
assert_eq!(::std::mem::size_of::<$t>(), $sz);
);
);
// TODO: move that to src/pubsub/mod.rs, once we figure out how to use macros from there.
macro_rules! impl_pubsub_item {
($item:ident, $ns:ident) => {
impl ::std::convert::TryFrom<crate::Element> for $item {
type Error = Error;
fn try_from(elem: crate::Element) -> Result<$item, Error> {
check_self!(elem, "item", $ns);
check_no_unknown_attributes!(elem, "item", ["id", "publisher"]);
let mut payloads = elem.children().cloned().collect::<Vec<_>>();
let payload = payloads.pop();
if !payloads.is_empty() {
return Err(Error::ParseError(
"More than a single payload in item element.",
));
}
Ok($item(crate::pubsub::Item {
id: get_attr!(elem, "id", Option),
publisher: get_attr!(elem, "publisher", Option),
payload,
}))
}
}
impl From<$item> for crate::Element {
fn from(item: $item) -> crate::Element {
crate::Element::builder("item")
.ns(ns::$ns)
.attr("id", item.0.id)
.attr("publisher", item.0.publisher)
.append_all(item.0.payload)
.build()
}
}
impl From<$item> for ::minidom::Node {
fn from(item: $item) -> ::minidom::Node {
::minidom::Node::Element(item.into())
}
}
impl ::std::ops::Deref for $item {
type Target = crate::pubsub::Item;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl ::std::ops::DerefMut for $item {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
}
}

View file

@ -0,0 +1,19 @@
// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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/.
/// Error type returned by every parser on failure.
pub mod error;
/// Various helpers.
pub(crate) mod helpers;
/// Helper macros to parse and serialise more easily.
#[macro_use]
mod macros;
#[cfg(test)]
/// Namespace-aware comparison for tests
pub(crate) mod compare_elements;

View file

@ -0,0 +1,89 @@
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 crate::iq::{IqGetPayload, IqResultPayload};
generate_empty_element!(
/// Represents a query for the software version a remote entity is using.
///
/// It should only be used in an `<iq type='get'/>`, as it can only
/// represent the request, and not a result.
VersionQuery,
"query",
VERSION
);
impl IqGetPayload for VersionQuery {}
generate_element!(
/// Represents the answer about the software version we are using.
///
/// It should only be used in an `<iq type='result'/>`, as it can only
/// represent the result, and not a request.
VersionResult, "query", VERSION,
children: [
/// The name of this client.
name: Required<String> = ("name", VERSION) => String,
/// The version of this client.
version: Required<String> = ("version", VERSION) => String,
/// The OS this client is running on.
os: Option<String> = ("os", VERSION) => String
]
);
impl IqResultPayload for VersionResult {}
#[cfg(test)]
mod tests {
use super::*;
use crate::util::compare_elements::NamespaceAwareCompare;
use crate::Element;
use std::convert::TryFrom;
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(VersionQuery, 0);
assert_size!(VersionResult, 36);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(VersionQuery, 0);
assert_size!(VersionResult, 72);
}
#[test]
fn simple() {
let elem: Element =
"<query xmlns='jabber:iq:version'><name>xmpp-rs</name><version>0.3.0</version></query>"
.parse()
.unwrap();
let version = VersionResult::try_from(elem).unwrap();
assert_eq!(version.name, String::from("xmpp-rs"));
assert_eq!(version.version, String::from("0.3.0"));
assert_eq!(version.os, None);
}
#[test]
fn serialisation() {
let version = VersionResult {
name: String::from("xmpp-rs"),
version: String::from("0.3.0"),
os: None,
};
let elem1 = Element::from(version);
let elem2: Element =
"<query xmlns='jabber:iq:version'><name>xmpp-rs</name><version>0.3.0</version></query>"
.parse()
.unwrap();
println!("{:?}", elem1);
assert!(elem1.compare_to(&elem2));
}
}

View file

@ -0,0 +1,102 @@
// Copyright (c) 2018 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 jid::BareJid;
generate_element!(
/// The stream opening for WebSocket.
Open, "open", WEBSOCKET,
attributes: [
/// The JID of the entity opening this stream.
from: Option<BareJid> = "from",
/// The JID of the entity receiving this stream opening.
to: Option<BareJid> = "to",
/// The id of the stream, used for authentication challenges.
id: Option<String> = "id",
/// The XMPP version used during this stream.
version: Option<String> = "version",
/// The default human language for all subsequent stanzas, which will
/// be transmitted to other entities for better localisation.
xml_lang: Option<String> = "xml:lang",
]
);
impl Open {
/// Creates a simple client→server `<open/>` element.
pub fn new(to: BareJid) -> Open {
Open {
from: None,
to: Some(to),
id: None,
version: Some(String::from("1.0")),
xml_lang: None,
}
}
/// Sets the [@from](#structfield.from) attribute on this `<open/>`
/// element.
pub fn with_from(mut self, from: BareJid) -> Open {
self.from = Some(from);
self
}
/// Sets the [@id](#structfield.id) attribute on this `<open/>` element.
pub fn with_id(mut self, id: String) -> Open {
self.id = Some(id);
self
}
/// Sets the [@xml:lang](#structfield.xml_lang) attribute on this `<open/>`
/// element.
pub fn with_lang(mut self, xml_lang: String) -> Open {
self.xml_lang = Some(xml_lang);
self
}
/// Checks whether the version matches the expected one.
pub fn is_version(&self, version: &str) -> bool {
match self.version {
None => false,
Some(ref self_version) => self_version == &String::from(version),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Element;
use std::convert::TryFrom;
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(Open, 84);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(Open, 168);
}
#[test]
fn test_simple() {
let elem: Element = "<open xmlns='urn:ietf:params:xml:ns:xmpp-framing'/>"
.parse()
.unwrap();
let open = Open::try_from(elem).unwrap();
assert_eq!(open.from, None);
assert_eq!(open.to, None);
assert_eq!(open.id, None);
assert_eq!(open.version, None);
assert_eq!(open.xml_lang, None);
}
}

515
xmpp-parsers/src/xhtml.rs Normal file
View file

@ -0,0 +1,515 @@
// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// 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 crate::util::error::Error;
use crate::message::MessagePayload;
use crate::ns;
use minidom::{Element, Node};
use std::convert::TryFrom;
use std::collections::HashMap;
// TODO: Use a proper lang type.
type Lang = String;
/// Container for formatted text.
#[derive(Debug, Clone)]
pub struct XhtmlIm {
/// Map of language to body element.
bodies: HashMap<Lang, Body>,
}
impl XhtmlIm {
/// Serialise formatted text to HTML.
pub fn to_html(self) -> String {
let mut html = Vec::new();
// TODO: use the best language instead.
for (lang, body) in self.bodies {
if lang.is_empty() {
assert!(body.xml_lang.is_none());
} else {
assert_eq!(Some(lang), body.xml_lang);
}
for tag in body.children {
html.push(tag.to_html());
}
break;
}
html.concat()
}
/// Removes all unknown elements.
fn flatten(self) -> XhtmlIm {
let mut bodies = HashMap::new();
for (lang, body) in self.bodies {
let children = body.children.into_iter().fold(vec![], |mut acc, child| {
match child {
Child::Tag(Tag::Unknown(children)) => acc.extend(children),
any => acc.push(any),
}
acc
});
let body = Body {
children,
..body
};
bodies.insert(lang, body);
}
XhtmlIm {
bodies,
}
}
}
impl MessagePayload for XhtmlIm {}
impl TryFrom<Element> for XhtmlIm {
type Error = Error;
fn try_from(elem: Element) -> Result<XhtmlIm, Error> {
check_self!(elem, "html", XHTML_IM);
check_no_attributes!(elem, "html");
let mut bodies = HashMap::new();
for child in elem.children() {
if child.is("body", ns::XHTML) {
let child = child.clone();
let lang = match child.attr("xml:lang") {
Some(lang) => lang,
None => "",
}.to_string();
let body = Body::try_from(child)?;
match bodies.insert(lang, body) {
None => (),
Some(_) => return Err(Error::ParseError("Two identical language bodies found in XHTML-IM."))
}
} else {
return Err(Error::ParseError("Unknown element in XHTML-IM."));
}
}
Ok(XhtmlIm { bodies }.flatten())
}
}
impl From<XhtmlIm> for Element {
fn from(wrapper: XhtmlIm) -> Element {
Element::builder("html")
.ns(ns::XHTML_IM)
.append_all(wrapper.bodies.into_iter().map(|(lang, body)| {
if lang.is_empty() {
assert!(body.xml_lang.is_none());
} else {
assert_eq!(Some(lang), body.xml_lang);
}
Element::from(body)
}))
.build()
}
}
#[derive(Debug, Clone)]
enum Child {
Tag(Tag),
Text(String),
}
impl Child {
fn to_html(self) -> String {
match self {
Child::Tag(tag) => tag.to_html(),
Child::Text(text) => text,
}
}
}
#[derive(Debug, Clone)]
struct Property {
key: String,
value: String,
}
type Css = Vec<Property>;
fn get_style_string(style: Css) -> Option<String> {
let mut result = vec![];
for Property { key, value } in style {
result.push(format!("{}: {}", key, value));
}
if result.is_empty() {
return None;
}
Some(result.join("; "))
}
#[derive(Debug, Clone)]
struct Body {
style: Css,
xml_lang: Option<String>,
children: Vec<Child>,
}
impl TryFrom<Element> for Body {
type Error = Error;
fn try_from(elem: Element) -> Result<Body, Error> {
let mut children = vec![];
for child in elem.nodes() {
match child {
Node::Element(child) => children.push(Child::Tag(Tag::try_from(child.clone())?)),
Node::Text(text) => children.push(Child::Text(text.clone())),
Node::Comment(_) => unimplemented!() // XXX: remove!
}
}
Ok(Body { style: parse_css(elem.attr("style")), xml_lang: elem.attr("xml:lang").map(|xml_lang| xml_lang.to_string()), children })
}
}
impl From<Body> for Element {
fn from(body: Body) -> Element {
Element::builder("body")
.ns(ns::XHTML)
.attr("style", get_style_string(body.style))
.attr("xml:lang", body.xml_lang)
.append_all(children_to_nodes(body.children))
.build()
}
}
#[derive(Debug, Clone)]
enum Tag {
A { href: Option<String>, style: Css, type_: Option<String>, children: Vec<Child> },
Blockquote { style: Css, children: Vec<Child> },
Br,
Cite { style: Css, children: Vec<Child> },
Em { children: Vec<Child> },
Img { src: Option<String>, alt: Option<String> }, // TODO: height, width, style
Li { style: Css, children: Vec<Child> },
Ol { style: Css, children: Vec<Child> },
P { style: Css, children: Vec<Child> },
Span { style: Css, children: Vec<Child> },
Strong { children: Vec<Child> },
Ul { style: Css, children: Vec<Child> },
Unknown(Vec<Child>),
}
impl Tag {
fn to_html(self) -> String {
match self {
Tag::A { href, style, type_, children } => {
let href = write_attr(href, "href");
let style = write_attr(get_style_string(style), "style");
let type_ = write_attr(type_, "type");
format!("<a{}{}{}>{}</a>", href, style, type_, children_to_html(children))
},
Tag::Blockquote { style, children } => {
let style = write_attr(get_style_string(style), "style");
format!("<blockquote{}>{}</blockquote>", style, children_to_html(children))
},
Tag::Br => String::from("<br>"),
Tag::Cite { style, children } => {
let style = write_attr(get_style_string(style), "style");
format!("<cite{}>{}</cite>", style, children_to_html(children))
},
Tag::Em { children } => format!("<em>{}</em>", children_to_html(children)),
Tag::Img { src, alt } => {
let src = write_attr(src, "src");
let alt = write_attr(alt, "alt");
format!("<img{}{}>", src, alt)
}
Tag::Li { style, children } => {
let style = write_attr(get_style_string(style), "style");
format!("<li{}>{}</li>", style, children_to_html(children))
}
Tag::Ol { style, children } => {
let style = write_attr(get_style_string(style), "style");
format!("<ol{}>{}</ol>", style, children_to_html(children))
}
Tag::P { style, children } => {
let style = write_attr(get_style_string(style), "style");
format!("<p{}>{}</p>", style, children_to_html(children))
}
Tag::Span { style, children } => {
let style = write_attr(get_style_string(style), "style");
format!("<span{}>{}</span>", style, children_to_html(children))
}
Tag::Strong { children } => format!("<strong>{}</strong>", children_to_html(children)),
Tag::Ul { style, children } => {
let style = write_attr(get_style_string(style), "style");
format!("<ul{}>{}</ul>", style, children_to_html(children))
}
Tag::Unknown(_) => panic!("No unknown element should be present in XHTML-IM after parsing."),
}
}
}
impl TryFrom<Element> for Tag {
type Error = Error;
fn try_from(elem: Element) -> Result<Tag, Error> {
let mut children = vec![];
for child in elem.nodes() {
match child {
Node::Element(child) => children.push(Child::Tag(Tag::try_from(child.clone())?)),
Node::Text(text) => children.push(Child::Text(text.clone())),
Node::Comment(_) => unimplemented!() // XXX: remove!
}
}
Ok(match elem.name() {
"a" => Tag::A { href: elem.attr("href").map(|href| href.to_string()), style: parse_css(elem.attr("style")), type_: elem.attr("type").map(|type_| type_.to_string()), children },
"blockquote" => Tag::Blockquote { style: parse_css(elem.attr("style")), children },
"br" => Tag::Br,
"cite" => Tag::Cite { style: parse_css(elem.attr("style")), children },
"em" => Tag::Em { children },
"img" => Tag::Img { src: elem.attr("src").map(|src| src.to_string()), alt: elem.attr("alt").map(|alt| alt.to_string()) },
"li" => Tag::Li { style: parse_css(elem.attr("style")), children },
"ol" => Tag::Ol { style: parse_css(elem.attr("style")), children },
"p" => Tag::P { style: parse_css(elem.attr("style")), children },
"span" => Tag::Span { style: parse_css(elem.attr("style")), children },
"strong" => Tag::Strong { children },
"ul" => Tag::Ul { style: parse_css(elem.attr("style")), children },
_ => Tag::Unknown(children),
})
}
}
impl From<Tag> for Element {
fn from(tag: Tag) -> Element {
let (name, attrs, children) = match tag {
Tag::A { href, style, type_, children } => ("a", {
let mut attrs = vec![];
if let Some(href) = href {
attrs.push(("href", href));
}
if let Some(style) = get_style_string(style) {
attrs.push(("style", style));
}
if let Some(type_) = type_ {
attrs.push(("type", type_));
}
attrs
}, children),
Tag::Blockquote { style, children } => ("blockquote", match get_style_string(style) {
Some(style) => vec![("style", style)],
None => vec![],
}, children),
Tag::Br => ("br", vec![], vec![]),
Tag::Cite { style, children } => ("cite", match get_style_string(style) {
Some(style) => vec![("style", style)],
None => vec![],
}, children),
Tag::Em { children } => ("em", vec![], children),
Tag::Img { src, alt } => {
let mut attrs = vec![];
if let Some(src) = src {
attrs.push(("src", src));
}
if let Some(alt) = alt {
attrs.push(("alt", alt));
}
("img", attrs, vec![])
},
Tag::Li { style, children } => ("li", match get_style_string(style) {
Some(style) => vec![("style", style)],
None => vec![],
}, children),
Tag::Ol { style, children } => ("ol", match get_style_string(style) {
Some(style) => vec![("style", style)],
None => vec![],
}, children),
Tag::P { style, children } => ("p", match get_style_string(style) {
Some(style) => vec![("style", style)],
None => vec![],
}, children),
Tag::Span { style, children } => ("span", match get_style_string(style) {
Some(style) => vec![("style", style)],
None => vec![],
}, children),
Tag::Strong { children } => ("strong", vec![], children),
Tag::Ul { style, children } => ("ul", match get_style_string(style) {
Some(style) => vec![("style", style)],
None => vec![],
}, children),
Tag::Unknown(_) => panic!("No unknown element should be present in XHTML-IM after parsing."),
};
let mut builder = Element::builder(name)
.ns(ns::XHTML)
.append_all(children_to_nodes(children));
for (key, value) in attrs {
builder = builder.attr(key, value);
}
builder.build()
}
}
fn children_to_nodes(children: Vec<Child>) -> impl IntoIterator<Item = Node> {
children.into_iter().map(|child| match child {
Child::Tag(tag) => Node::Element(Element::from(tag)),
Child::Text(text) => Node::Text(text),
})
}
fn children_to_html(children: Vec<Child>) -> String {
children.into_iter().map(|child| child.to_html()).collect::<Vec<_>>().concat()
}
fn write_attr(attr: Option<String>, name: &str) -> String {
match attr {
Some(attr) => format!(" {}='{}'", name, attr),
None => String::new(),
}
}
fn parse_css(style: Option<&str>) -> Css {
let mut properties = vec![];
if let Some(style) = style {
// TODO: make that parser a bit more resilient to things.
for part in style.split(";") {
let mut part = part.splitn(2, ":").map(|a| a.to_string()).collect::<Vec<_>>();
let key = part.pop().unwrap();
let value = part.pop().unwrap();
properties.push(Property { key, value });
}
}
properties
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(target_pointer_width = "32")]
#[test]
#[ignore]
fn test_size() {
assert_size!(XhtmlIm, 0);
assert_size!(Child, 0);
assert_size!(Tag, 0);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(XhtmlIm, 56);
assert_size!(Child, 112);
assert_size!(Tag, 104);
}
#[test]
fn test_empty() {
let elem: Element = "<html xmlns='http://jabber.org/protocol/xhtml-im'/>"
.parse()
.unwrap();
let xhtml = XhtmlIm::try_from(elem).unwrap();
assert_eq!(xhtml.bodies.len(), 0);
let elem: Element = "<html xmlns='http://jabber.org/protocol/xhtml-im'><body xmlns='http://www.w3.org/1999/xhtml'/></html>"
.parse()
.unwrap();
let xhtml = XhtmlIm::try_from(elem).unwrap();
assert_eq!(xhtml.bodies.len(), 1);
let elem: Element = "<html xmlns='http://jabber.org/protocol/xhtml-im' xmlns:html='http://www.w3.org/1999/xhtml'><html:body xml:lang='fr'/><html:body xml:lang='en'/></html>"
.parse()
.unwrap();
let xhtml = XhtmlIm::try_from(elem).unwrap();
assert_eq!(xhtml.bodies.len(), 2);
}
#[test]
fn invalid_two_same_langs() {
let elem: Element = "<html xmlns='http://jabber.org/protocol/xhtml-im' xmlns:html='http://www.w3.org/1999/xhtml'><html:body/><html:body/></html>"
.parse()
.unwrap();
let error = XhtmlIm::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Two identical language bodies found in XHTML-IM.");
}
#[test]
fn test_tag() {
let elem: Element = "<body xmlns='http://www.w3.org/1999/xhtml'/>"
.parse()
.unwrap();
let body = Body::try_from(elem).unwrap();
assert_eq!(body.children.len(), 0);
let elem: Element = "<body xmlns='http://www.w3.org/1999/xhtml'><p>Hello world!</p></body>"
.parse()
.unwrap();
let mut body = Body::try_from(elem).unwrap();
assert_eq!(body.style.len(), 0);
assert_eq!(body.xml_lang, None);
assert_eq!(body.children.len(), 1);
let p = match body.children.pop() {
Some(Child::Tag(tag)) => tag,
_ => panic!(),
};
let mut children = match p {
Tag::P { style, children } => {
assert_eq!(style.len(), 0);
assert_eq!(children.len(), 1);
children
},
_ => panic!(),
};
let text = match children.pop() {
Some(Child::Text(text)) => text,
_ => panic!(),
};
assert_eq!(text, "Hello world!");
}
#[test]
fn test_unknown_element() {
let elem: Element = "<html xmlns='http://jabber.org/protocol/xhtml-im'><body xmlns='http://www.w3.org/1999/xhtml'><coucou>Hello world!</coucou></body></html>"
.parse()
.unwrap();
let parsed = XhtmlIm::try_from(elem).unwrap();
let parsed2 = parsed.clone();
let html = parsed.to_html();
assert_eq!(html, "Hello world!");
let elem = Element::from(parsed2);
assert_eq!(String::from(&elem), "<?xml version=\"1.0\" encoding=\"utf-8\"?><html xmlns=\"http://jabber.org/protocol/xhtml-im\"><body xmlns=\"http://www.w3.org/1999/xhtml\">Hello world!</body></html>");
}
#[test]
fn test_generate_html() {
let elem: Element = "<html xmlns='http://jabber.org/protocol/xhtml-im'><body xmlns='http://www.w3.org/1999/xhtml'><p>Hello world!</p></body></html>"
.parse()
.unwrap();
let xhtml_im = XhtmlIm::try_from(elem).unwrap();
let html = xhtml_im.to_html();
assert_eq!(html, "<p>Hello world!</p>");
let elem: Element = "<html xmlns='http://jabber.org/protocol/xhtml-im'><body xmlns='http://www.w3.org/1999/xhtml'><p>Hello <strong>world</strong>!</p></body></html>"
.parse()
.unwrap();
let xhtml_im = XhtmlIm::try_from(elem).unwrap();
let html = xhtml_im.to_html();
assert_eq!(html, "<p>Hello <strong>world</strong>!</p>");
}
#[test]
fn generate_tree() {
let world = "world".to_string();
Body { style: vec![], xml_lang: Some("en".to_string()), children: vec![
Child::Tag(Tag::P { style: vec![], children: vec![
Child::Text("Hello ".to_string()),
Child::Tag(Tag::Strong { children: vec![
Child::Text(world),
] }),
Child::Text("!".to_string()),
] }),
] };
}
}