.hgignore π
@@ -0,0 +1,2 @@
+target
+Cargo.lock
Maxime Buquet created
Merge repos
Closes #2, #3, #1, #4, #7, #15, tokio-webhook2muc#2, #11, and #18
See merge request xmpp-rs/xmpp-rs!36
.hgignore | 2
Cargo.toml | 32
jid-rs/.gitlab-ci.yml | 62 +
jid-rs/CHANGELOG.md | 62 +
jid-rs/Cargo.toml | 22
jid-rs/LICENSE | 0
jid-rs/README.md | 18
jid-rs/src/lib.rs | 813 ++++++++++++++++++++
minidom-rs/.gitlab-ci.yml | 62 +
minidom-rs/CHANGELOG.md | 51 +
minidom-rs/Cargo.toml | 28
minidom-rs/LICENSE | 15
minidom-rs/README.md | 32
minidom-rs/examples/articles.rs | 45 +
minidom-rs/src/convert.rs | 69 +
minidom-rs/src/element.rs | 979 +++++++++++++++++++++++++
minidom-rs/src/error.rs | 83 ++
minidom-rs/src/lib.rs | 80 ++
minidom-rs/src/namespace_set.rs | 170 ++++
minidom-rs/src/node.rs | 207 +++++
minidom-rs/src/tests.rs | 251 ++++++
tokio-xmpp/.gitignore | 1
tokio-xmpp/.gitlab-ci.yml | 14
tokio-xmpp/Cargo.toml | 28
tokio-xmpp/README.md | 6
tokio-xmpp/examples/contact_addr.rs | 130 +++
tokio-xmpp/examples/download_avatars.rs | 232 +++++
tokio-xmpp/examples/echo_bot.rs | 106 ++
tokio-xmpp/examples/echo_component.rs | 99 ++
tokio-xmpp/logo.svg | 126 +++
tokio-xmpp/src/client/auth.rs | 116 ++
tokio-xmpp/src/client/bind.rs | 102 ++
tokio-xmpp/src/client/mod.rs | 236 ++++++
tokio-xmpp/src/component/auth.rs | 89 ++
tokio-xmpp/src/component/mod.rs | 163 ++++
tokio-xmpp/src/error.rs | 224 +++++
tokio-xmpp/src/event.rs | 54 +
tokio-xmpp/src/happy_eyeballs.rs | 196 +++++
tokio-xmpp/src/lib.rs | 19
tokio-xmpp/src/starttls.rs | 114 ++
tokio-xmpp/src/stream_start.rs | 125 +++
tokio-xmpp/src/xmpp_codec.rs | 532 +++++++++++++
tokio-xmpp/src/xmpp_stream.rs | 92 ++
xmpp-parsers/.gitlab-ci.yml | 62 +
xmpp-parsers/Cargo.toml | 34
xmpp-parsers/ChangeLog | 325 ++++++++
xmpp-parsers/LICENSE | 373 +++++++++
xmpp-parsers/doap.xml | 643 ++++++++++++++++
xmpp-parsers/examples/generate-caps.rs | 62 +
xmpp-parsers/src/attention.rs | 72 +
xmpp-parsers/src/avatar.rs | 128 +++
xmpp-parsers/src/bind.rs | 198 +++++
xmpp-parsers/src/blocking.rs | 223 +++++
xmpp-parsers/src/bob.rs | 168 ++++
xmpp-parsers/src/bookmarks.rs | 122 +++
xmpp-parsers/src/bookmarks2.rs | 119 +++
xmpp-parsers/src/caps.rs | 341 ++++++++
xmpp-parsers/src/carbons.rs | 127 +++
xmpp-parsers/src/cert_management.rs | 214 +++++
xmpp-parsers/src/chatstates.rs | 100 ++
xmpp-parsers/src/component.rs | 87 ++
xmpp-parsers/src/csi.rs | 59 +
xmpp-parsers/src/data_forms.rs | 395 ++++++++++
xmpp-parsers/src/date.rs | 138 +++
xmpp-parsers/src/delay.rs | 118 +++
xmpp-parsers/src/disco.rs | 457 +++++++++++
xmpp-parsers/src/ecaps2.rs | 472 ++++++++++++
xmpp-parsers/src/eme.rs | 96 ++
xmpp-parsers/src/forwarding.rs | 74 +
xmpp-parsers/src/hashes.rs | 269 ++++++
xmpp-parsers/src/ibb.rs | 172 ++++
xmpp-parsers/src/ibr.rs | 247 ++++++
xmpp-parsers/src/idle.rs | 147 +++
xmpp-parsers/src/iq.rs | 447 +++++++++++
xmpp-parsers/src/jid_prep.rs | 73 +
xmpp-parsers/src/jingle.rs | 867 ++++++++++++++++++++++
xmpp-parsers/src/jingle_dtls_srtp.rs | 98 ++
xmpp-parsers/src/jingle_ft.rs | 616 +++++++++++++++
xmpp-parsers/src/jingle_ibb.rs | 114 ++
xmpp-parsers/src/jingle_ice_udp.rs | 189 ++++
xmpp-parsers/src/jingle_message.rs | 146 +++
xmpp-parsers/src/jingle_rtcp_fb.rs | 46 +
xmpp-parsers/src/jingle_rtp.rs | 173 ++++
xmpp-parsers/src/jingle_s5b.rs | 352 ++++++++
xmpp-parsers/src/jingle_ssma.rs | 114 ++
xmpp-parsers/src/lib.rs | 214 +++++
xmpp-parsers/src/mam.rs | 392 ++++++++++
xmpp-parsers/src/media_element.rs | 256 ++++++
xmpp-parsers/src/message.rs | 418 ++++++++++
xmpp-parsers/src/message_correct.rs | 99 ++
xmpp-parsers/src/mood.rs | 312 +++++++
xmpp-parsers/src/muc/mod.rs | 14
xmpp-parsers/src/muc/muc.rs | 197 +++++
xmpp-parsers/src/muc/user.rs | 693 +++++++++++++++++
xmpp-parsers/src/nick.rs | 79 ++
xmpp-parsers/src/ns.rs | 235 ++++++
xmpp-parsers/src/occupant_id.rs | 89 ++
xmpp-parsers/src/openpgp.rs | 104 ++
xmpp-parsers/src/ping.rs | 71 +
xmpp-parsers/src/presence.rs | 661 ++++++++++++++++
xmpp-parsers/src/pubsub/event.rs | 429 ++++++++++
xmpp-parsers/src/pubsub/mod.rs | 76 +
xmpp-parsers/src/pubsub/pubsub.rs | 657 ++++++++++++++++
xmpp-parsers/src/receipts.rs | 89 ++
xmpp-parsers/src/roster.rs | 337 ++++++++
xmpp-parsers/src/rsm.rs | 310 +++++++
xmpp-parsers/src/sasl.rs | 308 +++++++
xmpp-parsers/src/server_info.rs | 209 +++++
xmpp-parsers/src/sm.rs | 227 +++++
xmpp-parsers/src/stanza_error.rs | 390 +++++++++
xmpp-parsers/src/stanza_id.rs | 124 +++
xmpp-parsers/src/stream.rs | 101 ++
xmpp-parsers/src/time.rs | 112 ++
xmpp-parsers/src/tune.rs | 228 +++++
xmpp-parsers/src/util/compare_elements.rs | 125 +++
xmpp-parsers/src/util/error.rs | 105 ++
xmpp-parsers/src/util/helpers.rs | 119 +++
xmpp-parsers/src/util/macros.rs | 808 ++++++++++++++++++++
xmpp-parsers/src/util/mod.rs | 19
xmpp-parsers/src/version.rs | 89 ++
xmpp-parsers/src/websocket.rs | 102 ++
xmpp-parsers/src/xhtml.rs | 515 +++++++++++++
xmpp-rs/.gitlab-ci.yml | 0
xmpp-rs/Cargo.toml | 21
xmpp-rs/ChangeLog | 0
xmpp-rs/LICENSE | 373 +++++++++
xmpp-rs/README.md | 0
xmpp-rs/examples/hello_bot.rs | 0
xmpp-rs/src/lib.rs | 2
xmpp-rs/src/pubsub/avatar.rs | 0
xmpp-rs/src/pubsub/mod.rs | 0
131 files changed, 24,853 insertions(+), 20 deletions(-)
@@ -0,0 +1,2 @@
+target
+Cargo.lock
@@ -1,21 +1,15 @@
-[package]
-name = "xmpp"
-version = "0.3.0"
-authors = [
- "Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>",
- "Maxime βpepβ Buquet <pep@bouah.net>",
+[workspace]
+members = [ # alphabetically sorted
+ "jid-rs",
+ "minidom-rs",
+ "tokio-xmpp",
+ "xmpp-parsers",
+ "xmpp-rs",
]
-description = "High-level XMPP library"
-homepage = "https://gitlab.com/linkmauve/xmpp-rs"
-repository = "https://gitlab.com/linkmauve/xmpp-rs"
-keywords = ["xmpp", "jabber", "chat", "messaging", "bot"]
-categories = ["network-programming"]
-license = "MPL-2.0"
-edition = "2018"
-[dependencies]
-tokio-xmpp = "1.0.1"
-xmpp-parsers = "0.15"
-futures = "0.1"
-tokio = "0.1"
-log = "0.4"
+[patch.crates-io]
+jid = { path = "jid-rs" }
+minidom = { path = "minidom-rs" }
+tokio-xmpp = { path = "tokio-xmpp" }
+xmpp-parsers = { path = "xmpp-parsers" }
+xmpp = { path = "xmpp-rs" }
@@ -0,0 +1,62 @@
+stages:
+ - build
+ - test
+
+variables:
+ FEATURES: ""
+ RUST_BACKTRACE: "full"
+
+.stable:
+ image: rust:latest
+ cache:
+ key: stable
+ paths:
+ - target/
+
+.nightly:
+ image: rustlang/rust:nightly
+ cache:
+ key: nightly
+ paths:
+ - target/
+
+.build:
+ stage: build
+ script:
+ - cargo build --verbose --no-default-features --features=$FEATURES
+
+.test:
+ stage: test
+ script:
+ - cargo test --lib --verbose --no-default-features --features=$FEATURES
+
+rust-latest-build:
+ extends:
+ - .build
+ - .stable
+
+rust-nightly-build:
+ extends:
+ - .build
+ - .nightly
+
+
+rust-latest-test:
+ extends:
+ - .test
+ - .stable
+
+rust-nightly-test:
+ extends:
+ - .test
+ - .nightly
+
+rust-latest-build with features=minidom:
+ extends: rust-latest-build
+ variables:
+ FEATURES: "minidom"
+
+rust-latest-test with features=minidom:
+ extends: rust-latest-test
+ variables:
+ FEATURES: "minidom"
@@ -0,0 +1,62 @@
+Version 0.8, released 2019-10-15:
+ * Updates
+ - CI: Split jobs, add tests, and caching
+ * Breaking
+ - 0.7.1 was actually a breaking release
+
+Version 0.7.2, released 2019-09-13:
+ * Updates
+ - Impl Error for JidParseError again, it got removed due to the failure removal but is still wanted.
+
+Version 0.7.1, released 2019-09-06:
+ * Updates
+ - Remove failure dependency, to keep compilation times in check
+ - Impl Display for Jid
+
+Version 0.7.0, released 2019-07-26:
+ * Breaking
+ - Update minidom dependency to 0.11
+
+Version 0.6.2, released 2019-07-20:
+ * Updates
+ - Implement From<BareJid> and From<FullJid> for Jid
+ - Add node and domain getters on Jid
+
+Version 0.6.1, released 2019-06-10:
+ * Updates
+ - Change the license from LGPLv3 to MPL-2.0.
+
+Version 0.6.0, released 2019-06-10:
+ * Updates
+ - Jid is now an enum, with two variants, Bare(BareJid) and Full(FullJid).
+ - BareJid and FullJid are two specialised variants of a JID.
+
+Version 0.5.3, released 2019-01-16:
+ * Updates
+ - Link Mauve bumped the minidom dependency version.
+ - Use Edition 2018, putting the baseline rustc version to 1.31.
+ - Run cargo-fmt on the code, to lower the barrier of entry.
+
+Version 0.5.2, released 2018-07-31:
+ * Updates
+ - Astro bumped the minidom dependency version.
+ - Updated the changelog to reflect that 0.5.1 was never actually released.
+
+Version 0.5.1, "released" 2018-03-01:
+ * Updates
+ - Link Mauve implemented failure::Fail on JidParseError.
+ - Link Mauve simplified the code a bit.
+
+Version 0.5.0, released 2018-02-18:
+ * Updates
+ - Link Mauve has updated the optional `minidom` dependency.
+ - Link Mauve has added tests for invalid JIDs, which adds more error cases.
+
+Version 0.4.0, released 2017-12-27:
+ * Updates
+ - Maxime Buquet has updated the optional `minidom` dependency.
+ - The repository has been transferred to xmpp-rs/jid-rs.
+
+Version 0.3.1, released 2017-10-31:
+ * Additions
+ - Link Mauve added a minidom::IntoElements implementation on Jid behind the "minidom" feature. ( https://gitlab.com/lumi/jid-rs/merge_requests/9 )
@@ -0,0 +1,22 @@
+[package]
+name = "jid"
+version = "0.8.0"
+authors = [
+ "lumi <lumi@pew.im>",
+ "Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>",
+ "Maxime βpepβ Buquet <pep@bouah.net>",
+]
+description = "A crate which provides a Jid struct for Jabber IDs."
+homepage = "https://gitlab.com/xmpp-rs/jid-rs"
+repository = "https://gitlab.com/xmpp-rs/jid-rs"
+documentation = "https://docs.rs/jid"
+readme = "README.md"
+keywords = ["xmpp", "jid"]
+license = "MPL-2.0"
+edition = "2018"
+
+[badges]
+gitlab = { repository = "xmpp-rs/jid-rs" }
+
+[dependencies]
+minidom = { version = "0.11", optional = true }
@@ -0,0 +1,18 @@
+jid-rs
+======
+
+What's this?
+------------
+
+A crate which provides a struct Jid for Jabber IDs. It's used in xmpp-rs but other XMPP libraries
+can of course use this.
+
+What license is it under?
+-------------------------
+
+MPL-2.0 or later, see the `LICENSE` file.
+
+Notes
+-----
+
+This library does not yet implement RFC7622.
@@ -0,0 +1,813 @@
+// Copyright (c) 2017, 2018 lumi <lumi@pew.im>
+// Copyright (c) 2017, 2018, 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+// Copyright (c) 2017, 2018, 2019 Maxime βpepβ Buquet <pep@bouah.net>
+// Copyright (c) 2017, 2018 Astro <astro@spaceboyz.net>
+// Copyright (c) 2017 Bastien Orivel <eijebong@bananium.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/.
+
+#![deny(missing_docs)]
+
+//! Provides a type for Jabber IDs.
+//!
+//! For usage, check the documentation on the `Jid` struct.
+
+use std::convert::{Into, TryFrom};
+use std::error::Error as StdError;
+use std::fmt;
+use std::str::FromStr;
+
+/// An error that signifies that a `Jid` cannot be parsed from a string.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum JidParseError {
+ /// Happens when there is no domain, that is either the string is empty,
+ /// starts with a /, or contains the @/ sequence.
+ NoDomain,
+
+ /// Happens when there is no resource, that is string contains no /.
+ NoResource,
+
+ /// Happens when the node is empty, that is the string starts with a @.
+ EmptyNode,
+
+ /// Happens when the resource is empty, that is the string ends with a /.
+ EmptyResource,
+}
+
+impl StdError for JidParseError {}
+
+impl fmt::Display for JidParseError {
+ fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
+ write!(fmt, "{}", match self {
+ JidParseError::NoDomain => "no domain found in this JID",
+ JidParseError::NoResource => "no resource found in this full JID",
+ JidParseError::EmptyNode => "nodepart empty despite the presence of a @",
+ JidParseError::EmptyResource => "resource empty despite the presence of a /",
+ })
+ }
+}
+
+/// An enum representing a Jabber ID. It can be either a `FullJid` or a `BareJid`.
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub enum Jid {
+ /// Bare Jid
+ Bare(BareJid),
+
+ /// Full Jid
+ Full(FullJid),
+}
+
+impl FromStr for Jid {
+ type Err = JidParseError;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let (ns, ds, rs): StringJid = _from_str(s)?;
+ Ok(match rs {
+ Some(rs) => Jid::Full(FullJid {
+ node: ns,
+ domain: ds,
+ resource: rs,
+ }),
+ None => Jid::Bare(BareJid {
+ node: ns,
+ domain: ds,
+ }),
+ })
+ }
+}
+
+impl From<Jid> for String {
+ fn from(jid: Jid) -> String {
+ match jid {
+ Jid::Bare(bare) => String::from(bare),
+ Jid::Full(full) => String::from(full),
+ }
+ }
+}
+
+impl From<BareJid> for Jid {
+ fn from(bare_jid: BareJid) -> Jid {
+ Jid::Bare(bare_jid)
+ }
+}
+
+impl From<FullJid> for Jid {
+ fn from(full_jid: FullJid) -> Jid {
+ Jid::Full(full_jid)
+ }
+}
+
+impl fmt::Display for Jid {
+ fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> {
+ fmt.write_str(String::from(self.clone()).as_ref())
+ }
+}
+
+impl Jid {
+ /// The node part of the Jabber ID, if it exists, else None.
+ pub fn node(self) -> Option<String> {
+ match self {
+ Jid::Bare(BareJid { node, .. }) | Jid::Full(FullJid { node, .. }) => node,
+ }
+ }
+
+ /// The domain of the Jabber ID.
+ pub fn domain(self) -> String {
+ match self {
+ Jid::Bare(BareJid { domain, .. }) | Jid::Full(FullJid { domain, .. }) => domain,
+ }
+ }
+}
+
+impl From<Jid> for BareJid {
+ fn from(jid: Jid) -> BareJid {
+ match jid {
+ Jid::Full(full) => full.into(),
+ Jid::Bare(bare) => bare,
+ }
+ }
+}
+
+impl TryFrom<Jid> for FullJid {
+ type Error = JidParseError;
+
+ fn try_from(jid: Jid) -> Result<Self, Self::Error> {
+ match jid {
+ Jid::Full(full) => Ok(full),
+ Jid::Bare(_) => Err(JidParseError::NoResource),
+ }
+ }
+}
+
+/// A struct representing a full Jabber ID.
+///
+/// A full Jabber ID is composed of 3 components, of which one is optional:
+///
+/// - A node/name, `node`, which is the optional part before the @.
+/// - A domain, `domain`, which is the mandatory part after the @ but before the /.
+/// - A resource, `resource`, which is the part after the /.
+///
+/// Unlike a `BareJid`, it always contains a resource, and should only be used when you are certain
+/// there is no case where a resource can be missing. Otherwise, use a `Jid` enum.
+#[derive(Clone, PartialEq, Eq, Hash)]
+pub struct FullJid {
+ /// The node part of the Jabber ID, if it exists, else None.
+ pub node: Option<String>,
+ /// The domain of the Jabber ID.
+ pub domain: String,
+ /// The resource of the Jabber ID.
+ pub resource: String,
+}
+
+/// A struct representing a bare Jabber ID.
+///
+/// A bare Jabber ID is composed of 2 components, of which one is optional:
+///
+/// - A node/name, `node`, which is the optional part before the @.
+/// - A domain, `domain`, which is the mandatory part after the @.
+///
+/// Unlike a `FullJid`, it canβt contain a resource, and should only be used when you are certain
+/// there is no case where a resource can be set. Otherwise, use a `Jid` enum.
+#[derive(Clone, PartialEq, Eq, Hash)]
+pub struct BareJid {
+ /// The node part of the Jabber ID, if it exists, else None.
+ pub node: Option<String>,
+ /// The domain of the Jabber ID.
+ pub domain: String,
+}
+
+impl From<FullJid> for String {
+ fn from(jid: FullJid) -> String {
+ let mut string = String::new();
+ if let Some(ref node) = jid.node {
+ string.push_str(node);
+ string.push('@');
+ }
+ string.push_str(&jid.domain);
+ string.push('/');
+ string.push_str(&jid.resource);
+ string
+ }
+}
+
+impl From<BareJid> for String {
+ fn from(jid: BareJid) -> String {
+ let mut string = String::new();
+ if let Some(ref node) = jid.node {
+ string.push_str(node);
+ string.push('@');
+ }
+ string.push_str(&jid.domain);
+ string
+ }
+}
+
+impl Into<BareJid> for FullJid {
+ fn into(self) -> BareJid {
+ BareJid {
+ node: self.node,
+ domain: self.domain,
+ }
+ }
+}
+
+impl fmt::Debug for FullJid {
+ fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> {
+ write!(fmt, "FullJID({})", self)
+ }
+}
+
+impl fmt::Debug for BareJid {
+ fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> {
+ write!(fmt, "BareJID({})", self)
+ }
+}
+
+impl fmt::Display for FullJid {
+ fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> {
+ fmt.write_str(String::from(self.clone()).as_ref())
+ }
+}
+
+impl fmt::Display for BareJid {
+ fn fmt(&self, fmt: &mut fmt::Formatter) -> Result<(), fmt::Error> {
+ fmt.write_str(String::from(self.clone()).as_ref())
+ }
+}
+
+enum ParserState {
+ Node,
+ Domain,
+ Resource,
+}
+
+type StringJid = (Option<String>, String, Option<String>);
+fn _from_str(s: &str) -> Result<StringJid, JidParseError> {
+ // TODO: very naive, may need to do it differently
+ let iter = s.chars();
+ let mut buf = String::with_capacity(s.len());
+ let mut state = ParserState::Node;
+ let mut node = None;
+ let mut domain = None;
+ let mut resource = None;
+ for c in iter {
+ match state {
+ ParserState::Node => {
+ match c {
+ '@' => {
+ if buf == "" {
+ return Err(JidParseError::EmptyNode);
+ }
+ state = ParserState::Domain;
+ node = Some(buf.clone()); // TODO: performance tweaks, do not need to copy it
+ buf.clear();
+ }
+ '/' => {
+ if buf == "" {
+ return Err(JidParseError::NoDomain);
+ }
+ state = ParserState::Resource;
+ domain = Some(buf.clone()); // TODO: performance tweaks
+ buf.clear();
+ }
+ c => {
+ buf.push(c);
+ }
+ }
+ }
+ ParserState::Domain => {
+ match c {
+ '/' => {
+ if buf == "" {
+ return Err(JidParseError::NoDomain);
+ }
+ state = ParserState::Resource;
+ domain = Some(buf.clone()); // TODO: performance tweaks
+ buf.clear();
+ }
+ c => {
+ buf.push(c);
+ }
+ }
+ }
+ ParserState::Resource => {
+ buf.push(c);
+ }
+ }
+ }
+ if !buf.is_empty() {
+ match state {
+ ParserState::Node => {
+ domain = Some(buf);
+ }
+ ParserState::Domain => {
+ domain = Some(buf);
+ }
+ ParserState::Resource => {
+ resource = Some(buf);
+ }
+ }
+ } else if let ParserState::Resource = state {
+ return Err(JidParseError::EmptyResource);
+ }
+ Ok((node, domain.ok_or(JidParseError::NoDomain)?, resource))
+}
+
+impl FromStr for FullJid {
+ type Err = JidParseError;
+
+ fn from_str(s: &str) -> Result<FullJid, JidParseError> {
+ let (ns, ds, rs): StringJid = _from_str(s)?;
+ Ok(FullJid {
+ node: ns,
+ domain: ds,
+ resource: rs.ok_or(JidParseError::NoResource)?,
+ })
+ }
+}
+
+impl FullJid {
+ /// Constructs a full Jabber ID containing all three components.
+ ///
+ /// This is of the form `node`@`domain`/`resource`.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use jid::FullJid;
+ ///
+ /// let jid = FullJid::new("node", "domain", "resource");
+ ///
+ /// assert_eq!(jid.node, Some("node".to_owned()));
+ /// assert_eq!(jid.domain, "domain".to_owned());
+ /// assert_eq!(jid.resource, "resource".to_owned());
+ /// ```
+ pub fn new<NS, DS, RS>(node: NS, domain: DS, resource: RS) -> FullJid
+ where
+ NS: Into<String>,
+ DS: Into<String>,
+ RS: Into<String>,
+ {
+ FullJid {
+ node: Some(node.into()),
+ domain: domain.into(),
+ resource: resource.into(),
+ }
+ }
+
+ /// Constructs a new Jabber ID from an existing one, with the node swapped out with a new one.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use jid::FullJid;
+ ///
+ /// let jid = FullJid::new("node", "domain", "resource");
+ ///
+ /// assert_eq!(jid.node, Some("node".to_owned()));
+ ///
+ /// let new_jid = jid.with_node("new_node");
+ ///
+ /// assert_eq!(new_jid.node, Some("new_node".to_owned()));
+ /// ```
+ pub fn with_node<NS>(&self, node: NS) -> FullJid
+ where
+ NS: Into<String>,
+ {
+ FullJid {
+ node: Some(node.into()),
+ domain: self.domain.clone(),
+ resource: self.resource.clone(),
+ }
+ }
+
+ /// Constructs a new Jabber ID from an existing one, with the domain swapped out with a new one.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use jid::FullJid;
+ ///
+ /// let jid = FullJid::new("node", "domain", "resource");
+ ///
+ /// assert_eq!(jid.domain, "domain".to_owned());
+ ///
+ /// let new_jid = jid.with_domain("new_domain");
+ ///
+ /// assert_eq!(new_jid.domain, "new_domain");
+ /// ```
+ pub fn with_domain<DS>(&self, domain: DS) -> FullJid
+ where
+ DS: Into<String>,
+ {
+ FullJid {
+ node: self.node.clone(),
+ domain: domain.into(),
+ resource: self.resource.clone(),
+ }
+ }
+
+ /// Constructs a full Jabber ID from a bare Jabber ID, specifying a `resource`.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use jid::FullJid;
+ ///
+ /// let jid = FullJid::new("node", "domain", "resource");
+ ///
+ /// assert_eq!(jid.resource, "resource".to_owned());
+ ///
+ /// let new_jid = jid.with_resource("new_resource");
+ ///
+ /// assert_eq!(new_jid.resource, "new_resource");
+ /// ```
+ pub fn with_resource<RS>(&self, resource: RS) -> FullJid
+ where
+ RS: Into<String>,
+ {
+ FullJid {
+ node: self.node.clone(),
+ domain: self.domain.clone(),
+ resource: resource.into(),
+ }
+ }
+}
+
+impl FromStr for BareJid {
+ type Err = JidParseError;
+
+ fn from_str(s: &str) -> Result<BareJid, JidParseError> {
+ let (ns, ds, _rs): StringJid = _from_str(s)?;
+ Ok(BareJid {
+ node: ns,
+ domain: ds,
+ })
+ }
+}
+
+impl BareJid {
+ /// Constructs a bare Jabber ID, containing two components.
+ ///
+ /// This is of the form `node`@`domain`.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use jid::BareJid;
+ ///
+ /// let jid = BareJid::new("node", "domain");
+ ///
+ /// assert_eq!(jid.node, Some("node".to_owned()));
+ /// assert_eq!(jid.domain, "domain".to_owned());
+ /// ```
+ pub fn new<NS, DS>(node: NS, domain: DS) -> BareJid
+ where
+ NS: Into<String>,
+ DS: Into<String>,
+ {
+ BareJid {
+ node: Some(node.into()),
+ domain: domain.into(),
+ }
+ }
+
+ /// Constructs a bare Jabber ID containing only a `domain`.
+ ///
+ /// This is of the form `domain`.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use jid::BareJid;
+ ///
+ /// let jid = BareJid::domain("domain");
+ ///
+ /// assert_eq!(jid.node, None);
+ /// assert_eq!(jid.domain, "domain".to_owned());
+ /// ```
+ pub fn domain<DS>(domain: DS) -> BareJid
+ where
+ DS: Into<String>,
+ {
+ BareJid {
+ node: None,
+ domain: domain.into(),
+ }
+ }
+
+ /// Constructs a new Jabber ID from an existing one, with the node swapped out with a new one.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use jid::BareJid;
+ ///
+ /// let jid = BareJid::domain("domain");
+ ///
+ /// assert_eq!(jid.node, None);
+ ///
+ /// let new_jid = jid.with_node("node");
+ ///
+ /// assert_eq!(new_jid.node, Some("node".to_owned()));
+ /// ```
+ pub fn with_node<NS>(&self, node: NS) -> BareJid
+ where
+ NS: Into<String>,
+ {
+ BareJid {
+ node: Some(node.into()),
+ domain: self.domain.clone(),
+ }
+ }
+
+ /// Constructs a new Jabber ID from an existing one, with the domain swapped out with a new one.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use jid::BareJid;
+ ///
+ /// let jid = BareJid::domain("domain");
+ ///
+ /// assert_eq!(jid.domain, "domain");
+ ///
+ /// let new_jid = jid.with_domain("new_domain");
+ ///
+ /// assert_eq!(new_jid.domain, "new_domain");
+ /// ```
+ pub fn with_domain<DS>(&self, domain: DS) -> BareJid
+ where
+ DS: Into<String>,
+ {
+ BareJid {
+ node: self.node.clone(),
+ domain: domain.into(),
+ }
+ }
+
+ /// Constructs a full Jabber ID from a bare Jabber ID, specifying a `resource`.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use jid::BareJid;
+ ///
+ /// let bare = BareJid::new("node", "domain");
+ /// let full = bare.with_resource("resource");
+ ///
+ /// assert_eq!(full.node, Some("node".to_owned()));
+ /// assert_eq!(full.domain, "domain".to_owned());
+ /// assert_eq!(full.resource, "resource".to_owned());
+ /// ```
+ pub fn with_resource<RS>(self, resource: RS) -> FullJid
+ where
+ RS: Into<String>,
+ {
+ FullJid {
+ node: self.node,
+ domain: self.domain,
+ resource: resource.into(),
+ }
+ }
+}
+
+#[cfg(feature = "minidom")]
+use minidom::{IntoAttributeValue, Node};
+
+#[cfg(feature = "minidom")]
+impl IntoAttributeValue for Jid {
+ fn into_attribute_value(self) -> Option<String> {
+ Some(String::from(self))
+ }
+}
+
+#[cfg(feature = "minidom")]
+impl Into<Node> for Jid {
+ fn into(self) -> Node {
+ Node::Text(String::from(self))
+ }
+}
+
+#[cfg(feature = "minidom")]
+impl IntoAttributeValue for FullJid {
+ fn into_attribute_value(self) -> Option<String> {
+ Some(String::from(self))
+ }
+}
+
+#[cfg(feature = "minidom")]
+impl Into<Node> for FullJid {
+ fn into(self) -> Node {
+ Node::Text(String::from(self))
+ }
+}
+
+#[cfg(feature = "minidom")]
+impl IntoAttributeValue for BareJid {
+ fn into_attribute_value(self) -> Option<String> {
+ Some(String::from(self))
+ }
+}
+
+#[cfg(feature = "minidom")]
+impl Into<Node> for BareJid {
+ fn into(self) -> Node {
+ Node::Text(String::from(self))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ use std::str::FromStr;
+ use std::collections::HashMap;
+
+ #[test]
+ fn can_parse_full_jids() {
+ assert_eq!(
+ FullJid::from_str("a@b.c/d"),
+ Ok(FullJid::new("a", "b.c", "d"))
+ );
+ assert_eq!(
+ FullJid::from_str("b.c/d"),
+ Ok(FullJid {
+ node: None,
+ domain: "b.c".to_owned(),
+ resource: "d".to_owned(),
+ })
+ );
+
+ assert_eq!(FullJid::from_str("a@b.c"), Err(JidParseError::NoResource));
+ assert_eq!(FullJid::from_str("b.c"), Err(JidParseError::NoResource));
+ }
+
+ #[test]
+ fn can_parse_bare_jids() {
+ assert_eq!(BareJid::from_str("a@b.c/d"), Ok(BareJid::new("a", "b.c")));
+ assert_eq!(
+ BareJid::from_str("b.c/d"),
+ Ok(BareJid {
+ node: None,
+ domain: "b.c".to_owned(),
+ })
+ );
+
+ assert_eq!(BareJid::from_str("a@b.c"), Ok(BareJid::new("a", "b.c")));
+ assert_eq!(
+ BareJid::from_str("b.c"),
+ Ok(BareJid {
+ node: None,
+ domain: "b.c".to_owned(),
+ })
+ );
+ }
+
+ #[test]
+ fn can_parse_jids() {
+ let full = FullJid::from_str("a@b.c/d").unwrap();
+ let bare = BareJid::from_str("e@f.g").unwrap();
+
+ assert_eq!(Jid::from_str("a@b.c/d"), Ok(Jid::Full(full)));
+ assert_eq!(Jid::from_str("e@f.g"), Ok(Jid::Bare(bare)));
+ }
+
+ #[test]
+ fn full_to_bare_jid() {
+ let bare: BareJid = FullJid::new("a", "b.c", "d").into();
+ assert_eq!(bare, BareJid::new("a", "b.c"));
+ }
+
+ #[test]
+ fn bare_to_full_jid() {
+ assert_eq!(
+ BareJid::new("a", "b.c").with_resource("d"),
+ FullJid::new("a", "b.c", "d")
+ );
+ }
+
+ #[test]
+ fn node_from_jid() {
+ assert_eq!(
+ Jid::Full(FullJid::new("a", "b.c", "d")).node(),
+ Some(String::from("a")),
+ );
+ }
+
+ #[test]
+ fn domain_from_jid() {
+ assert_eq!(
+ Jid::Bare(BareJid::new("a", "b.c")).domain(),
+ String::from("b.c"),
+ );
+ }
+
+ #[test]
+ fn jid_to_full_bare() {
+ let full = FullJid::new("a", "b.c", "d");
+ let bare = BareJid::new("a", "b.c");
+
+ assert_eq!(
+ FullJid::try_from(Jid::Full(full.clone())),
+ Ok(full.clone()),
+ );
+ assert_eq!(
+ FullJid::try_from(Jid::Bare(bare.clone())),
+ Err(JidParseError::NoResource),
+ );
+ assert_eq!(
+ BareJid::from(Jid::Full(full.clone())),
+ bare.clone(),
+ );
+ assert_eq!(
+ BareJid::from(Jid::Bare(bare.clone())),
+ bare,
+ );
+ }
+
+ #[test]
+ fn serialise() {
+ assert_eq!(
+ String::from(FullJid::new("a", "b", "c")),
+ String::from("a@b/c")
+ );
+ assert_eq!(String::from(BareJid::new("a", "b")), String::from("a@b"));
+ }
+
+ #[test]
+ fn hash() {
+ let _map: HashMap<Jid, String> = HashMap::new();
+ }
+
+ #[test]
+ fn invalid_jids() {
+ assert_eq!(BareJid::from_str(""), Err(JidParseError::NoDomain));
+ assert_eq!(BareJid::from_str("/c"), Err(JidParseError::NoDomain));
+ assert_eq!(BareJid::from_str("a@/c"), Err(JidParseError::NoDomain));
+ assert_eq!(BareJid::from_str("@b"), Err(JidParseError::EmptyNode));
+ assert_eq!(BareJid::from_str("b/"), Err(JidParseError::EmptyResource));
+
+ assert_eq!(FullJid::from_str(""), Err(JidParseError::NoDomain));
+ assert_eq!(FullJid::from_str("/c"), Err(JidParseError::NoDomain));
+ assert_eq!(FullJid::from_str("a@/c"), Err(JidParseError::NoDomain));
+ assert_eq!(FullJid::from_str("@b"), Err(JidParseError::EmptyNode));
+ assert_eq!(FullJid::from_str("b/"), Err(JidParseError::EmptyResource));
+ assert_eq!(FullJid::from_str("a@b"), Err(JidParseError::NoResource));
+ }
+
+ #[test]
+ fn display_jids() {
+ assert_eq!(format!("{}", FullJid::new("a", "b", "c")), String::from("a@b/c"));
+ assert_eq!(format!("{}", BareJid::new("a", "b")), String::from("a@b"));
+ assert_eq!(format!("{}", Jid::Full(FullJid::new("a", "b", "c"))), String::from("a@b/c"));
+ assert_eq!(format!("{}", Jid::Bare(BareJid::new("a", "b"))), String::from("a@b"));
+ }
+
+ #[cfg(feature = "minidom")]
+ #[test]
+ fn minidom() {
+ let elem: minidom::Element = "<message from='a@b/c'/>".parse().unwrap();
+ let to: Jid = elem.attr("from").unwrap().parse().unwrap();
+ assert_eq!(to, Jid::Full(FullJid::new("a", "b", "c")));
+
+ let elem: minidom::Element = "<message from='a@b'/>".parse().unwrap();
+ let to: Jid = elem.attr("from").unwrap().parse().unwrap();
+ assert_eq!(to, Jid::Bare(BareJid::new("a", "b")));
+
+ let elem: minidom::Element = "<message from='a@b/c'/>".parse().unwrap();
+ let to: FullJid = elem.attr("from").unwrap().parse().unwrap();
+ assert_eq!(to, FullJid::new("a", "b", "c"));
+
+ let elem: minidom::Element = "<message from='a@b'/>".parse().unwrap();
+ let to: BareJid = elem.attr("from").unwrap().parse().unwrap();
+ assert_eq!(to, BareJid::new("a", "b"));
+ }
+
+ #[cfg(feature = "minidom")]
+ #[test]
+ fn minidom_into_attr() {
+ let full = FullJid::new("a", "b", "c");
+ let elem = minidom::Element::builder("message")
+ .ns("jabber:client")
+ .attr("from", full.clone())
+ .build();
+ assert_eq!(elem.attr("from"), Some(String::from(full).as_ref()));
+
+ let bare = BareJid::new("a", "b");
+ let elem = minidom::Element::builder("message")
+ .ns("jabber:client")
+ .attr("from", bare.clone())
+ .build();
+ assert_eq!(elem.attr("from"), Some(String::from(bare.clone()).as_ref()));
+
+ let jid = Jid::Bare(bare.clone());
+ let _elem = minidom::Element::builder("message")
+ .ns("jabber:client")
+ .attr("from", jid)
+ .build();
+ assert_eq!(elem.attr("from"), Some(String::from(bare).as_ref()));
+ }
+}
@@ -0,0 +1,62 @@
+stages:
+ - build
+ - test
+
+variables:
+ FEATURES: ""
+ RUST_BACKTRACE: "full"
+
+.stable:
+ image: rust:latest
+ cache:
+ key: stable
+ paths:
+ - target/
+
+.nightly:
+ image: rustlang/rust:nightly
+ cache:
+ key: nightly
+ paths:
+ - target/
+
+.build:
+ stage: build
+ script:
+ - cargo build --verbose --no-default-features --features=$FEATURES
+
+.test:
+ stage: test
+ script:
+ - cargo test --verbose --no-default-features --features=$FEATURES
+
+rust-latest-build:
+ extends:
+ - .build
+ - .stable
+
+rust-nightly-build:
+ extends:
+ - .build
+ - .nightly
+
+
+rust-latest-test:
+ extends:
+ - .test
+ - .stable
+
+rust-nightly-test:
+ extends:
+ - .test
+ - .nightly
+
+rust-latest-build with features=comments:
+ extends: rust-latest-build
+ variables:
+ FEATURES: "comments"
+
+rust-latest-test with features=comments:
+ extends: rust-latest-test
+ variables:
+ FEATURES: "comments"
@@ -0,0 +1,51 @@
+Version XXX, released YYY:
+ * Changes
+ * Update edition to 2018
+ * Fixes
+ * Update old CI configuration with newer Rust images
+Version 0.11.1, released 2019-09-06:
+ * Changes
+ * Update to quick-xml 0.16
+ * Add a default "comments" feature to transform comments into errors when unset.
+Version 0.11.0, released 2019-06-14:
+ * Breaking
+ * Get rid of IntoElements, replace with `Into<Node>` and `<T: Into<Node> IntoIterator<Item = T>>`
+ * Fixes
+ * Remote unused `mut` attribute on variable
+ * Changes
+ * Update quick-xml to 0.14
+ * Split Node into its own module
+ * Nicer Debug implementation for NamespaceSet
+Version 0.10.0, released 2018-10-21:
+ * Changes
+ * Update quick-xml to 0.13
+ * Update doc to reflect switch from xml-rs to quick-xml.
+Version 0.9.1, released 2018-05-29:
+ * Fixes
+ * Lumi fixed CDATA handling, minidom will not unescape CDATA bodies anymore.
+ * Small changes
+ - Link Mauve implemented IntoAttributeValue on std::net::IpAddr.
+Version 0.9.0, released 2018-04-10:
+ * Small changes
+ - Upgrade quick_xml to 0.12.1
+Version 0.8.0, released 2018-02-18:
+ * Additions
+ - Link Mauve replaced error\_chain with failure ( https://gitlab.com/lumi/minidom-rs/merge_requests/27 )
+ - Yue Liu added support for writing comments and made the writing methods use quick-xml's EventWriter ( https://gitlab.com/lumi/minidom-rs/merge_requests/26 )
+Version 0.6.2, released 2017-08-27:
+ * Additions
+ - Link Mauve added an implementation of IntoElements for all Into<Element> ( https://gitlab.com/lumi/minidom-rs/merge_requests/19 )
+Version 0.6.1, released 2017-08-20:
+ * Additions
+ - Astro added Element::has_ns, which checks whether an element's namespace matches the passed argument. ( https://gitlab.com/lumi/minidom-rs/merge_requests/16 )
+ - Link Mauve updated the quick-xml dependency to the latest version.
+ * Fixes
+ - Because break value is now stable, Link Mauve rewrote some code marked FIXME to use it.
+Version 0.6.0, released 2017-08-13:
+ * Big changes
+ - Astro added proper support for namespace prefixes. ( https://gitlab.com/lumi/minidom-rs/merge_requests/14 )
+ * Fixes
+ - Astro fixed a regression that caused the writer not to escape its xml output properly. ( https://gitlab.com/lumi/minidom-rs/merge_requests/15 )
+Version 0.5.0, released 2017-06-10:
+ * Big changes
+ - Eijebong made parsing a lot faster by switching the crate from xml-rs to quick_xml. ( https://gitlab.com/lumi/minidom-rs/merge_requests/11 )
@@ -0,0 +1,28 @@
+[package]
+name = "minidom"
+version = "0.11.1"
+authors = [
+ "lumi <lumi@pew.im>",
+ "Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>",
+ "Bastien Orivel <eijebong+minidom@bananium.fr>",
+ "Astro <astro@spaceboyz.net>",
+ "Maxime βpepβ Buquet <pep@bouah.net>",
+]
+description = "A small, simple DOM implementation on top of quick-xml"
+homepage = "https://gitlab.com/lumi/minidom-rs"
+repository = "https://gitlab.com/lumi/minidom-rs"
+documentation = "https://docs.rs/minidom"
+readme = "README.md"
+keywords = ["xml"]
+license = "MIT"
+edition = "2018"
+
+[badges]
+gitlab = { repository = "lumi/minidom-rs" }
+
+[dependencies]
+quick-xml = "0.17"
+
+[features]
+default = ["comments"]
+comments = []
@@ -0,0 +1,15 @@
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
+associated documentation files (the "Software"), to deal in the Software without restriction,
+including without limitation the rights to use, copy, modify, merge, publish, distribute,
+sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or
+substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
+NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT
+OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,32 @@
+minidom-rs
+==========
+
+What's this?
+------------
+
+A minimal DOM library on top of quick-xml.
+
+What license is it under?
+-------------------------
+
+MIT. See `LICENSE`.
+
+License yadda yadda.
+--------------------
+
+Copyright 2017 minidom-rs contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
+associated documentation files (the "Software"), to deal in the Software without restriction,
+including without limitation the rights to use, copy, modify, merge, publish, distribute,
+sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or
+substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
+NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT
+OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,45 @@
+extern crate minidom;
+
+use minidom::Element;
+
+const DATA: &str = r#"<articles xmlns="article">
+ <article>
+ <title>10 Terrible Bugs You Would NEVER Believe Happened</title>
+ <body>
+ Rust fixed them all. <3
+ </body>
+ </article>
+ <article>
+ <title>BREAKING NEWS: Physical Bug Jumps Out Of Programmer's Screen</title>
+ <body>
+ Just kidding!
+ </body>
+ </article>
+</articles>"#;
+
+const ARTICLE_NS: &str = "article";
+
+#[derive(Debug)]
+pub struct Article {
+ title: String,
+ body: String,
+}
+
+fn main() {
+ let root: Element = DATA.parse().unwrap();
+
+ let mut articles: Vec<Article> = Vec::new();
+
+ for child in root.children() {
+ if child.is("article", ARTICLE_NS) {
+ let title = child.get_child("title", ARTICLE_NS).unwrap().text();
+ let body = child.get_child("body", ARTICLE_NS).unwrap().text();
+ articles.push(Article {
+ title: title,
+ body: body.trim().to_owned(),
+ });
+ }
+ }
+
+ println!("{:?}", articles);
+}
@@ -0,0 +1,69 @@
+//! A module which exports a few traits for converting types to elements and attributes.
+
+/// A trait for types which can be converted to an attribute value.
+pub trait IntoAttributeValue {
+ /// Turns this into an attribute string, or None if it shouldn't be added.
+ fn into_attribute_value(self) -> Option<String>;
+}
+
+macro_rules! impl_into_attribute_value {
+ ($t:ty) => {
+ impl IntoAttributeValue for $t {
+ fn into_attribute_value(self) -> Option<String> {
+ Some(format!("{}", self))
+ }
+ }
+ }
+}
+
+macro_rules! impl_into_attribute_values {
+ ($($t:ty),*) => {
+ $(impl_into_attribute_value!($t);)*
+ }
+}
+
+impl_into_attribute_values!(usize, u64, u32, u16, u8, isize, i64, i32, i16, i8, ::std::net::IpAddr);
+
+impl IntoAttributeValue for String {
+ fn into_attribute_value(self) -> Option<String> {
+ Some(self)
+ }
+}
+
+impl<'a> IntoAttributeValue for &'a String {
+ fn into_attribute_value(self) -> Option<String> {
+ Some(self.to_owned())
+ }
+}
+
+impl<'a> IntoAttributeValue for &'a str {
+ fn into_attribute_value(self) -> Option<String> {
+ Some(self.to_owned())
+ }
+}
+
+impl<T: IntoAttributeValue> IntoAttributeValue for Option<T> {
+ fn into_attribute_value(self) -> Option<String> {
+ self.and_then(IntoAttributeValue::into_attribute_value)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::IntoAttributeValue;
+ use std::net::IpAddr;
+ use std::str::FromStr;
+
+ #[test]
+ fn test_into_attribute_value_on_ints() {
+ assert_eq!(16u8.into_attribute_value().unwrap() , "16");
+ assert_eq!(17u16.into_attribute_value().unwrap() , "17");
+ assert_eq!(18u32.into_attribute_value().unwrap() , "18");
+ assert_eq!(19u64.into_attribute_value().unwrap() , "19");
+ assert_eq!( 16i8.into_attribute_value().unwrap() , "16");
+ assert_eq!((-17i16).into_attribute_value().unwrap(), "-17");
+ assert_eq!( 18i32.into_attribute_value().unwrap(), "18");
+ assert_eq!((-19i64).into_attribute_value().unwrap(), "-19");
+ assert_eq!(IpAddr::from_str("127.000.0.1").unwrap().into_attribute_value().unwrap(), "127.0.0.1");
+ }
+}
@@ -0,0 +1,979 @@
+//! Provides an `Element` type, which represents DOM nodes, and a builder to create them with.
+
+use crate::convert::IntoAttributeValue;
+use crate::error::{Error, Result};
+use crate::namespace_set::NamespaceSet;
+use crate::node::Node;
+
+use std::io:: Write;
+use std::collections::{btree_map, BTreeMap};
+
+use std::str;
+use std::rc::Rc;
+use std::borrow::Cow;
+
+use quick_xml::Reader as EventReader;
+use quick_xml::Writer as EventWriter;
+use quick_xml::events::{Event, BytesStart, BytesEnd, BytesDecl};
+
+use std::io::BufRead;
+
+use std::str::FromStr;
+
+use std::slice;
+
+/// helper function to escape a `&[u8]` and replace all
+/// xml special characters (<, >, &, ', ") with their corresponding
+/// xml escaped value.
+pub fn escape(raw: &[u8]) -> Cow<[u8]> {
+ let mut escapes: Vec<(usize, &'static [u8])> = Vec::new();
+ let mut bytes = raw.iter();
+ fn to_escape(b: u8) -> bool {
+ match b {
+ b'<' | b'>' | b'\'' | b'&' | b'"' => true,
+ _ => false,
+ }
+ }
+
+ let mut loc = 0;
+ while let Some(i) = bytes.position(|&b| to_escape(b)) {
+ loc += i;
+ match raw[loc] {
+ b'<' => escapes.push((loc, b"<")),
+ b'>' => escapes.push((loc, b">")),
+ b'\'' => escapes.push((loc, b"'")),
+ b'&' => escapes.push((loc, b"&")),
+ b'"' => escapes.push((loc, b""")),
+ _ => unreachable!("Only '<', '>','\', '&' and '\"' are escaped"),
+ }
+ loc += 1;
+ }
+
+ if escapes.is_empty() {
+ Cow::Borrowed(raw)
+ } else {
+ let len = raw.len();
+ let mut v = Vec::with_capacity(len);
+ let mut start = 0;
+ for (i, r) in escapes {
+ v.extend_from_slice(&raw[start..i]);
+ v.extend_from_slice(r);
+ start = i + 1;
+ }
+
+ if start < len {
+ v.extend_from_slice(&raw[start..]);
+ }
+ Cow::Owned(v)
+ }
+}
+
+
+#[derive(Clone, PartialEq, Eq, Debug)]
+/// A struct representing a DOM Element.
+pub struct Element {
+ prefix: Option<String>,
+ name: String,
+ namespaces: Rc<NamespaceSet>,
+ attributes: BTreeMap<String, String>,
+ children: Vec<Node>,
+}
+
+impl<'a> From<&'a Element> for String {
+ fn from(elem: &'a Element) -> String {
+ let mut writer = Vec::new();
+ elem.write_to(&mut writer).unwrap();
+ String::from_utf8(writer).unwrap()
+ }
+}
+
+impl FromStr for Element {
+ type Err = Error;
+
+ fn from_str(s: &str) -> Result<Element> {
+ let mut reader = EventReader::from_str(s);
+ Element::from_reader(&mut reader)
+ }
+}
+
+impl Element {
+ fn new<NS: Into<NamespaceSet>>(name: String, prefix: Option<String>, namespaces: NS, attributes: BTreeMap<String, String>, children: Vec<Node>) -> Element {
+ Element {
+ prefix, name,
+ namespaces: Rc::new(namespaces.into()),
+ attributes,
+ children,
+ }
+ }
+
+ /// Return a builder for an `Element` with the given `name`.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use minidom::Element;
+ ///
+ /// let elem = Element::builder("name")
+ /// .ns("namespace")
+ /// .attr("name", "value")
+ /// .append("inner")
+ /// .build();
+ ///
+ /// assert_eq!(elem.name(), "name");
+ /// assert_eq!(elem.ns(), Some("namespace".to_owned()));
+ /// assert_eq!(elem.attr("name"), Some("value"));
+ /// assert_eq!(elem.attr("inexistent"), None);
+ /// assert_eq!(elem.text(), "inner");
+ /// ```
+ pub fn builder<S: AsRef<str>>(name: S) -> ElementBuilder {
+ let (prefix, name) = split_element_name(name).unwrap();
+ ElementBuilder {
+ root: Element::new(name, prefix, None, BTreeMap::new(), Vec::new()),
+ namespaces: Default::default(),
+ }
+ }
+
+ /// Returns a bare minimum `Element` with this name.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use minidom::Element;
+ ///
+ /// let bare = Element::bare("name");
+ ///
+ /// assert_eq!(bare.name(), "name");
+ /// assert_eq!(bare.ns(), None);
+ /// assert_eq!(bare.attr("name"), None);
+ /// assert_eq!(bare.text(), "");
+ /// ```
+ pub fn bare<S: Into<String>>(name: S) -> Element {
+ Element {
+ prefix: None,
+ name: name.into(),
+ namespaces: Rc::new(NamespaceSet::default()),
+ attributes: BTreeMap::new(),
+ children: Vec::new(),
+ }
+ }
+
+ /// Returns a reference to the name of this element.
+ pub fn name(&self) -> &str {
+ &self.name
+ }
+
+ /// Returns a reference to the prefix of this element.
+ ///
+ /// # Examples
+ /// ```rust
+ /// use minidom::Element;
+ ///
+ /// let elem = Element::builder("prefix:name")
+ /// .build();
+ ///
+ /// assert_eq!(elem.name(), "name");
+ /// assert_eq!(elem.prefix(), Some("prefix"));
+ /// ```
+ pub fn prefix(&self) -> Option<&str> {
+ self.prefix.as_ref().map(String::as_ref)
+ }
+
+ /// Returns a reference to the namespace of this element, if it has one, else `None`.
+ pub fn ns(&self) -> Option<String> {
+ self.namespaces.get(&self.prefix)
+ }
+
+ /// Returns a reference to the value of the given attribute, if it exists, else `None`.
+ pub fn attr(&self, name: &str) -> Option<&str> {
+ if let Some(value) = self.attributes.get(name) {
+ return Some(value)
+ }
+ None
+ }
+
+ /// Returns an iterator over the attributes of this element.
+ ///
+ /// # Example
+ ///
+ /// ```rust
+ /// use minidom::Element;
+ ///
+ /// let elm: Element = "<elem a=\"b\" />".parse().unwrap();
+ ///
+ /// let mut iter = elm.attrs();
+ ///
+ /// assert_eq!(iter.next().unwrap(), ("a", "b"));
+ /// assert_eq!(iter.next(), None);
+ /// ```
+ pub fn attrs(&self) -> Attrs {
+ Attrs {
+ iter: self.attributes.iter(),
+ }
+ }
+
+ /// Returns an iterator over the attributes of this element, with the value being a mutable
+ /// reference.
+ pub fn attrs_mut(&mut self) -> AttrsMut {
+ AttrsMut {
+ iter: self.attributes.iter_mut(),
+ }
+ }
+
+ /// Modifies the value of an attribute.
+ pub fn set_attr<S: Into<String>, V: IntoAttributeValue>(&mut self, name: S, val: V) {
+ let name = name.into();
+ let val = val.into_attribute_value();
+
+ if let Some(value) = self.attributes.get_mut(&name) {
+ *value = val.expect("removing existing value via set_attr, this is not yet supported (TODO)"); // TODO
+ return;
+ }
+
+ if let Some(val) = val {
+ self.attributes.insert(name, val);
+ }
+ }
+
+ /// Returns whether the element has the given name and namespace.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use minidom::Element;
+ ///
+ /// let elem = Element::builder("name").ns("namespace").build();
+ ///
+ /// assert_eq!(elem.is("name", "namespace"), true);
+ /// assert_eq!(elem.is("name", "wrong"), false);
+ /// assert_eq!(elem.is("wrong", "namespace"), false);
+ /// assert_eq!(elem.is("wrong", "wrong"), false);
+ /// ```
+ pub fn is<N: AsRef<str>, NS: AsRef<str>>(&self, name: N, namespace: NS) -> bool {
+ self.name == name.as_ref() &&
+ self.has_ns(namespace)
+ }
+
+ /// Returns whether the element has the given namespace.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use minidom::Element;
+ ///
+ /// let elem = Element::builder("name").ns("namespace").build();
+ ///
+ /// assert_eq!(elem.has_ns("namespace"), true);
+ /// assert_eq!(elem.has_ns("wrong"), false);
+ /// ```
+ pub fn has_ns<NS: AsRef<str>>(&self, namespace: NS) -> bool {
+ self.namespaces.has(&self.prefix, namespace)
+ }
+
+ /// Parse a document from an `EventReader`.
+ pub fn from_reader<R: BufRead>(reader: &mut EventReader<R>) -> Result<Element> {
+ let mut buf = Vec::new();
+
+ let root: Element = loop {
+ let e = reader.read_event(&mut buf)?;
+ match e {
+ Event::Empty(ref e) | Event::Start(ref e) => {
+ break build_element(reader, e)?;
+ },
+ Event::Eof => {
+ return Err(Error::EndOfDocument);
+ },
+ #[cfg(not(feature = "comments"))]
+ Event::Comment { .. } => {
+ return Err(Error::CommentsDisabled);
+ }
+ #[cfg(feature = "comments")]
+ Event::Comment { .. } => (),
+ Event::Text { .. } |
+ Event::End { .. } |
+ Event::CData { .. } |
+ Event::Decl { .. } |
+ Event::PI { .. } |
+ Event::DocType { .. } => (), // TODO: may need more errors
+ }
+ };
+
+ let mut stack = vec![root];
+
+ loop {
+ match reader.read_event(&mut buf)? {
+ Event::Empty(ref e) => {
+ let elem = build_element(reader, e)?;
+ // Since there is no Event::End after, directly append it to the current node
+ stack.last_mut().unwrap().append_child(elem);
+ },
+ Event::Start(ref e) => {
+ let elem = build_element(reader, e)?;
+ stack.push(elem);
+ },
+ Event::End(ref e) => {
+ if stack.len() <= 1 {
+ break;
+ }
+ let elem = stack.pop().unwrap();
+ if let Some(to) = stack.last_mut() {
+ // TODO: check whether this is correct, we are comparing &[u8]s, not &strs
+ let elem_name = e.name();
+ let mut split_iter = elem_name.splitn(2, |u| *u == 0x3A);
+ let possible_prefix = split_iter.next().unwrap(); // Can't be empty.
+ match split_iter.next() {
+ Some(name) => {
+ match elem.prefix() {
+ Some(prefix) => {
+ if possible_prefix != prefix.as_bytes() {
+ return Err(Error::InvalidElementClosed);
+ }
+ },
+ None => {
+ return Err(Error::InvalidElementClosed);
+ },
+ }
+ if name != elem.name().as_bytes() {
+ return Err(Error::InvalidElementClosed);
+ }
+ },
+ None => {
+ if elem.prefix().is_some() {
+ return Err(Error::InvalidElementClosed);
+ }
+ if possible_prefix != elem.name().as_bytes() {
+ return Err(Error::InvalidElementClosed);
+ }
+ },
+ }
+ to.append_child(elem);
+ }
+ },
+ Event::Text(s) => {
+ let text = s.unescape_and_decode(reader)?;
+ if text != "" {
+ let current_elem = stack.last_mut().unwrap();
+ current_elem.append_text_node(text);
+ }
+ },
+ Event::CData(s) => {
+ let text = reader.decode(&s)?.to_owned();
+ if text != "" {
+ let current_elem = stack.last_mut().unwrap();
+ current_elem.append_text_node(text);
+ }
+ },
+ Event::Eof => {
+ break;
+ },
+ #[cfg(not(feature = "comments"))]
+ Event::Comment(_) => return Err(Error::CommentsDisabled),
+ #[cfg(feature = "comments")]
+ Event::Comment(s) => {
+ let comment = reader.decode(&s)?.to_owned();
+ if comment != "" {
+ let current_elem = stack.last_mut().unwrap();
+ current_elem.append_comment_node(comment);
+ }
+ },
+ Event::Decl { .. } |
+ Event::PI { .. } |
+ Event::DocType { .. } => (),
+ }
+ }
+ Ok(stack.pop().unwrap())
+ }
+
+ /// Output a document to a `Writer`.
+ pub fn write_to<W: Write>(&self, writer: &mut W) -> Result<()> {
+ self.to_writer(&mut EventWriter::new(writer))
+ }
+
+ /// Output the document to quick-xml `Writer`
+ pub fn to_writer<W: Write>(&self, writer: &mut EventWriter<W>) -> Result<()> {
+ writer.write_event(Event::Decl(BytesDecl::new(b"1.0", Some(b"utf-8"), None)))?;
+ self.write_to_inner(writer)
+ }
+
+ /// Like `write_to()` but without the `<?xml?>` prelude
+ pub fn write_to_inner<W: Write>(&self, writer: &mut EventWriter<W>) -> Result<()> {
+ let name = match self.prefix {
+ None => Cow::Borrowed(&self.name),
+ Some(ref prefix) => Cow::Owned(format!("{}:{}", prefix, self.name)),
+ };
+
+ let mut start = BytesStart::borrowed(name.as_bytes(), name.len());
+ for (prefix, ns) in self.namespaces.declared_ns() {
+ match *prefix {
+ None => start.push_attribute(("xmlns", ns.as_ref())),
+ Some(ref prefix) => {
+ let key = format!("xmlns:{}", prefix);
+ start.push_attribute((key.as_bytes(), ns.as_bytes()))
+ },
+ }
+ }
+ for (key, value) in &self.attributes {
+ start.push_attribute((key.as_bytes(), escape(value.as_bytes()).as_ref()));
+ }
+
+ if self.children.is_empty() {
+ writer.write_event(Event::Empty(start))?;
+ return Ok(())
+ }
+
+ writer.write_event(Event::Start(start))?;
+
+ for child in &self.children {
+ child.write_to_inner(writer)?;
+ }
+
+ writer.write_event(Event::End(BytesEnd::borrowed(name.as_bytes())))?;
+ Ok(())
+ }
+
+ /// Returns an iterator over references to every child node of this element.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use minidom::Element;
+ ///
+ /// let elem: Element = "<root>a<c1 />b<c2 />c</root>".parse().unwrap();
+ ///
+ /// let mut iter = elem.nodes();
+ ///
+ /// assert_eq!(iter.next().unwrap().as_text().unwrap(), "a");
+ /// assert_eq!(iter.next().unwrap().as_element().unwrap().name(), "c1");
+ /// assert_eq!(iter.next().unwrap().as_text().unwrap(), "b");
+ /// assert_eq!(iter.next().unwrap().as_element().unwrap().name(), "c2");
+ /// assert_eq!(iter.next().unwrap().as_text().unwrap(), "c");
+ /// assert_eq!(iter.next(), None);
+ /// ```
+ #[inline] pub fn nodes(&self) -> Nodes {
+ self.children.iter()
+ }
+
+ /// Returns an iterator over mutable references to every child node of this element.
+ #[inline] pub fn nodes_mut(&mut self) -> NodesMut {
+ self.children.iter_mut()
+ }
+
+ /// Returns an iterator over references to every child element of this element.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use minidom::Element;
+ ///
+ /// let elem: Element = "<root>hello<child1 />this<child2 />is<child3 />ignored</root>".parse().unwrap();
+ ///
+ /// let mut iter = elem.children();
+ /// assert_eq!(iter.next().unwrap().name(), "child1");
+ /// assert_eq!(iter.next().unwrap().name(), "child2");
+ /// assert_eq!(iter.next().unwrap().name(), "child3");
+ /// assert_eq!(iter.next(), None);
+ /// ```
+ #[inline] pub fn children(&self) -> Children {
+ Children {
+ iter: self.children.iter(),
+ }
+ }
+
+ /// Returns an iterator over mutable references to every child element of this element.
+ #[inline] pub fn children_mut(&mut self) -> ChildrenMut {
+ ChildrenMut {
+ iter: self.children.iter_mut(),
+ }
+ }
+
+ /// Returns an iterator over references to every text node of this element.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use minidom::Element;
+ ///
+ /// let elem: Element = "<root>hello<c /> world!</root>".parse().unwrap();
+ ///
+ /// let mut iter = elem.texts();
+ /// assert_eq!(iter.next().unwrap(), "hello");
+ /// assert_eq!(iter.next().unwrap(), " world!");
+ /// assert_eq!(iter.next(), None);
+ /// ```
+ #[inline] pub fn texts(&self) -> Texts {
+ Texts {
+ iter: self.children.iter(),
+ }
+ }
+
+ /// Returns an iterator over mutable references to every text node of this element.
+ #[inline] pub fn texts_mut(&mut self) -> TextsMut {
+ TextsMut {
+ iter: self.children.iter_mut(),
+ }
+ }
+
+ /// Appends a child node to the `Element`, returning the appended node.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use minidom::Element;
+ ///
+ /// let mut elem = Element::bare("root");
+ ///
+ /// assert_eq!(elem.children().count(), 0);
+ ///
+ /// elem.append_child(Element::bare("child"));
+ ///
+ /// {
+ /// let mut iter = elem.children();
+ /// assert_eq!(iter.next().unwrap().name(), "child");
+ /// assert_eq!(iter.next(), None);
+ /// }
+ ///
+ /// let child = elem.append_child(Element::bare("new"));
+ ///
+ /// assert_eq!(child.name(), "new");
+ /// ```
+ pub fn append_child(&mut self, child: Element) -> &mut Element {
+ child.namespaces.set_parent(Rc::clone(&self.namespaces));
+
+ self.children.push(Node::Element(child));
+ if let Node::Element(ref mut cld) = *self.children.last_mut().unwrap() {
+ cld
+ } else {
+ unreachable!()
+ }
+ }
+
+ /// Appends a text node to an `Element`.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use minidom::Element;
+ ///
+ /// let mut elem = Element::bare("node");
+ ///
+ /// assert_eq!(elem.text(), "");
+ ///
+ /// elem.append_text_node("text");
+ ///
+ /// assert_eq!(elem.text(), "text");
+ /// ```
+ pub fn append_text_node<S: Into<String>>(&mut self, child: S) {
+ self.children.push(Node::Text(child.into()));
+ }
+
+ /// Appends a comment node to an `Element`.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use minidom::Element;
+ ///
+ /// let mut elem = Element::bare("node");
+ ///
+ /// elem.append_comment_node("comment");
+ /// ```
+ #[cfg(feature = "comments")]
+ pub fn append_comment_node<S: Into<String>>(&mut self, child: S) {
+ self.children.push(Node::Comment(child.into()));
+ }
+
+ /// Appends a node to an `Element`.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use minidom::{Element, Node};
+ ///
+ /// let mut elem = Element::bare("node");
+ ///
+ /// elem.append_node(Node::Text("hello".to_owned()));
+ ///
+ /// assert_eq!(elem.text(), "hello");
+ /// ```
+ pub fn append_node(&mut self, node: Node) {
+ self.children.push(node);
+ }
+
+ /// Returns the concatenation of all text nodes in the `Element`.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use minidom::Element;
+ ///
+ /// let elem: Element = "<node>hello,<split /> world!</node>".parse().unwrap();
+ ///
+ /// assert_eq!(elem.text(), "hello, world!");
+ /// ```
+ pub fn text(&self) -> String {
+ self.texts().fold(String::new(), |ret, new| ret + new)
+ }
+
+ /// Returns a reference to the first child element with the specific name and namespace, if it
+ /// exists in the direct descendants of this `Element`, else returns `None`.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use minidom::Element;
+ ///
+ /// let elem: Element = r#"<node xmlns="ns"><a /><a xmlns="other_ns" /><b /></node>"#.parse().unwrap();
+ ///
+ /// assert!(elem.get_child("a", "ns").unwrap().is("a", "ns"));
+ /// assert!(elem.get_child("a", "other_ns").unwrap().is("a", "other_ns"));
+ /// assert!(elem.get_child("b", "ns").unwrap().is("b", "ns"));
+ /// assert_eq!(elem.get_child("c", "ns"), None);
+ /// assert_eq!(elem.get_child("b", "other_ns"), None);
+ /// assert_eq!(elem.get_child("a", "inexistent_ns"), None);
+ /// ```
+ pub fn get_child<N: AsRef<str>, NS: AsRef<str>>(&self, name: N, namespace: NS) -> Option<&Element> {
+ for fork in &self.children {
+ if let Node::Element(ref e) = *fork {
+ if e.is(name.as_ref(), namespace.as_ref()) {
+ return Some(e);
+ }
+ }
+ }
+ None
+ }
+
+ /// Returns a mutable reference to the first child element with the specific name and namespace,
+ /// if it exists in the direct descendants of this `Element`, else returns `None`.
+ pub fn get_child_mut<N: AsRef<str>, NS: AsRef<str>>(&mut self, name: N, namespace: NS) -> Option<&mut Element> {
+ for fork in &mut self.children {
+ if let Node::Element(ref mut e) = *fork {
+ if e.is(name.as_ref(), namespace.as_ref()) {
+ return Some(e);
+ }
+ }
+ }
+ None
+ }
+
+ /// Returns whether a specific child with this name and namespace exists in the direct
+ /// descendants of the `Element`.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use minidom::Element;
+ ///
+ /// let elem: Element = r#"<node xmlns="ns"><a /><a xmlns="other_ns" /><b /></node>"#.parse().unwrap();
+ ///
+ /// assert_eq!(elem.has_child("a", "other_ns"), true);
+ /// assert_eq!(elem.has_child("a", "ns"), true);
+ /// assert_eq!(elem.has_child("a", "inexistent_ns"), false);
+ /// assert_eq!(elem.has_child("b", "ns"), true);
+ /// assert_eq!(elem.has_child("b", "other_ns"), false);
+ /// assert_eq!(elem.has_child("b", "inexistent_ns"), false);
+ /// ```
+ pub fn has_child<N: AsRef<str>, NS: AsRef<str>>(&self, name: N, namespace: NS) -> bool {
+ self.get_child(name, namespace).is_some()
+ }
+
+ /// Removes the first child with this name and namespace, if it exists, and returns an
+ /// `Option<Element>` containing this child if it succeeds.
+ /// Returns `None` if no child matches this name and namespace.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use minidom::Element;
+ ///
+ /// let mut elem: Element = r#"<node xmlns="ns"><a /><a xmlns="other_ns" /><b /></node>"#.parse().unwrap();
+ ///
+ /// assert!(elem.remove_child("a", "ns").unwrap().is("a", "ns"));
+ /// assert!(elem.remove_child("a", "ns").is_none());
+ /// assert!(elem.remove_child("inexistent", "inexistent").is_none());
+ /// ```
+ pub fn remove_child<N: AsRef<str>, NS: AsRef<str>>(&mut self, name: N, namespace: NS) -> Option<Element> {
+ let name = name.as_ref();
+ let namespace = namespace.as_ref();
+ let idx = self.children.iter().position(|x| {
+ if let Node::Element(ref elm) = x {
+ elm.is(name, namespace)
+ } else {
+ false
+ }
+ })?;
+ self.children.remove(idx).into_element()
+ }
+}
+
+fn split_element_name<S: AsRef<str>>(s: S) -> Result<(Option<String>, String)> {
+ let name_parts = s.as_ref().split(':').collect::<Vec<&str>>();
+ match name_parts.len() {
+ 2 => Ok((Some(name_parts[0].to_owned()), name_parts[1].to_owned())),
+ 1 => Ok((None, name_parts[0].to_owned())),
+ _ => Err(Error::InvalidElement),
+ }
+}
+
+fn build_element<R: BufRead>(reader: &EventReader<R>, event: &BytesStart) -> Result<Element> {
+ let mut namespaces = BTreeMap::new();
+ let attributes = event.attributes()
+ .map(|o| {
+ let o = o?;
+ let key = str::from_utf8(o.key)?.to_owned();
+ let value = o.unescape_and_decode_value(reader)?;
+ Ok((key, value))
+ })
+ .filter(|o| {
+ match *o {
+ Ok((ref key, ref value)) if key == "xmlns" => {
+ namespaces.insert(None, value.to_owned());
+ false
+ },
+ Ok((ref key, ref value)) if key.starts_with("xmlns:") => {
+ namespaces.insert(Some(key[6..].to_owned()), value.to_owned());
+ false
+ },
+ _ => true,
+ }
+ })
+ .collect::<Result<BTreeMap<String, String>>>()?;
+
+ let (prefix, name) = split_element_name(str::from_utf8(event.name())?)?;
+ let element = Element::new(name, prefix, namespaces, attributes, Vec::new());
+ Ok(element)
+}
+
+/// An iterator over references to child elements of an `Element`.
+pub struct Children<'a> {
+ iter: slice::Iter<'a, Node>,
+}
+
+impl<'a> Iterator for Children<'a> {
+ type Item = &'a Element;
+
+ fn next(&mut self) -> Option<&'a Element> {
+ for item in &mut self.iter {
+ if let Node::Element(ref child) = *item {
+ return Some(child);
+ }
+ }
+ None
+ }
+}
+
+/// An iterator over mutable references to child elements of an `Element`.
+pub struct ChildrenMut<'a> {
+ iter: slice::IterMut<'a, Node>,
+}
+
+impl<'a> Iterator for ChildrenMut<'a> {
+ type Item = &'a mut Element;
+
+ fn next(&mut self) -> Option<&'a mut Element> {
+ for item in &mut self.iter {
+ if let Node::Element(ref mut child) = *item {
+ return Some(child);
+ }
+ }
+ None
+ }
+}
+
+/// An iterator over references to child text nodes of an `Element`.
+pub struct Texts<'a> {
+ iter: slice::Iter<'a, Node>,
+}
+
+impl<'a> Iterator for Texts<'a> {
+ type Item = &'a str;
+
+ fn next(&mut self) -> Option<&'a str> {
+ for item in &mut self.iter {
+ if let Node::Text(ref child) = *item {
+ return Some(child);
+ }
+ }
+ None
+ }
+}
+
+/// An iterator over mutable references to child text nodes of an `Element`.
+pub struct TextsMut<'a> {
+ iter: slice::IterMut<'a, Node>,
+}
+
+impl<'a> Iterator for TextsMut<'a> {
+ type Item = &'a mut String;
+
+ fn next(&mut self) -> Option<&'a mut String> {
+ for item in &mut self.iter {
+ if let Node::Text(ref mut child) = *item {
+ return Some(child);
+ }
+ }
+ None
+ }
+}
+
+/// An iterator over references to all child nodes of an `Element`.
+pub type Nodes<'a> = slice::Iter<'a, Node>;
+
+/// An iterator over mutable references to all child nodes of an `Element`.
+pub type NodesMut<'a> = slice::IterMut<'a, Node>;
+
+/// An iterator over the attributes of an `Element`.
+pub struct Attrs<'a> {
+ iter: btree_map::Iter<'a, String, String>,
+}
+
+impl<'a> Iterator for Attrs<'a> {
+ type Item = (&'a str, &'a str);
+
+ fn next(&mut self) -> Option<Self::Item> {
+ self.iter.next().map(|(x, y)| (x.as_ref(), y.as_ref()))
+ }
+}
+
+/// An iterator over the attributes of an `Element`, with the values mutable.
+pub struct AttrsMut<'a> {
+ iter: btree_map::IterMut<'a, String, String>,
+}
+
+impl<'a> Iterator for AttrsMut<'a> {
+ type Item = (&'a str, &'a mut String);
+
+ fn next(&mut self) -> Option<Self::Item> {
+ self.iter.next().map(|(x, y)| (x.as_ref(), y))
+ }
+}
+
+/// A builder for `Element`s.
+pub struct ElementBuilder {
+ root: Element,
+ namespaces: BTreeMap<Option<String>, String>,
+}
+
+impl ElementBuilder {
+ /// Sets the namespace.
+ pub fn ns<S: Into<String>>(mut self, namespace: S) -> ElementBuilder {
+ self.namespaces
+ .insert(self.root.prefix.clone(), namespace.into());
+ self
+ }
+
+ /// Sets an attribute.
+ pub fn attr<S: Into<String>, V: IntoAttributeValue>(mut self, name: S, value: V) -> ElementBuilder {
+ self.root.set_attr(name, value);
+ self
+ }
+
+ /// Appends anything implementing `Into<Node>` into the tree.
+ pub fn append<T: Into<Node>>(mut self, node: T) -> ElementBuilder {
+ self.root.append_node(node.into());
+ self
+ }
+
+ /// Appends an iterator of things implementing `Into<Node>` into the tree.
+ pub fn append_all<T: Into<Node>, I: IntoIterator<Item = T>>(mut self, iter: I) -> ElementBuilder {
+ for node in iter {
+ self.root.append_node(node.into());
+ }
+ self
+ }
+
+ /// Builds the `Element`.
+ pub fn build(self) -> Element {
+ let mut element = self.root;
+ // Set namespaces
+ element.namespaces = Rc::new(NamespaceSet::from(self.namespaces));
+ // Propagate namespaces
+ for node in &element.children {
+ if let Node::Element(ref e) = *node {
+ e.namespaces.set_parent(Rc::clone(&element.namespaces));
+ }
+ }
+ element
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_element_new() {
+ use std::iter::FromIterator;
+
+ let elem = Element::new( "name".to_owned()
+ , None
+ , Some("namespace".to_owned())
+ , BTreeMap::from_iter(vec![ ("name".to_string(), "value".to_string()) ].into_iter() )
+ , Vec::new() );
+
+ assert_eq!(elem.name(), "name");
+ assert_eq!(elem.ns(), Some("namespace".to_owned()));
+ assert_eq!(elem.attr("name"), Some("value"));
+ assert_eq!(elem.attr("inexistent"), None);
+ }
+
+ #[test]
+ fn test_from_reader_simple() {
+ let xml = "<foo></foo>";
+ let mut reader = EventReader::from_str(xml);
+ let elem = Element::from_reader(&mut reader);
+
+ let elem2 = Element::builder("foo").build();
+
+ assert_eq!(elem.unwrap(), elem2);
+ }
+
+ #[test]
+ fn test_from_reader_nested() {
+ let xml = "<foo><bar baz='qxx' /></foo>";
+ let mut reader = EventReader::from_str(xml);
+ let elem = Element::from_reader(&mut reader);
+
+ let nested = Element::builder("bar")
+ .attr("baz", "qxx")
+ .build();
+ let elem2 = Element::builder("foo")
+ .append(nested)
+ .build();
+
+ assert_eq!(elem.unwrap(), elem2);
+ }
+
+ #[test]
+ fn test_from_reader_with_prefix() {
+ let xml = "<foo><prefix:bar baz='qxx' /></foo>";
+ let mut reader = EventReader::from_str(xml);
+ let elem = Element::from_reader(&mut reader);
+
+ let nested = Element::builder("prefix:bar")
+ .attr("baz", "qxx")
+ .build();
+ let elem2 = Element::builder("foo")
+ .append(nested)
+ .build();
+
+ assert_eq!(elem.unwrap(), elem2);
+ }
+
+ #[test]
+ fn parses_spectest_xml() { // From: https://gitlab.com/lumi/minidom-rs/issues/8
+ let xml = r#"
+ <rng:grammar xmlns:rng="http://relaxng.org/ns/structure/1.0">
+ <rng:name xmlns:rng="http://relaxng.org/ns/structure/1.0"></rng:name>
+ </rng:grammar>
+ "#;
+ let mut reader = EventReader::from_str(xml);
+ let _ = Element::from_reader(&mut reader).unwrap();
+ }
+
+ #[test]
+ fn does_not_unescape_cdata() {
+ let xml = "<test><![CDATA['>blah<blah>]]></test>";
+ let mut reader = EventReader::from_str(xml);
+ let elem = Element::from_reader(&mut reader).unwrap();
+ assert_eq!(elem.text(), "'>blah<blah>");
+ }
+}
@@ -0,0 +1,83 @@
+//! Provides an error type for this crate.
+
+use std::convert::From;
+use std::error::Error as StdError;
+
+/// Our main error type.
+#[derive(Debug)]
+pub enum Error {
+ /// An error from quick_xml.
+ XmlError(::quick_xml::Error),
+
+ /// An UTF-8 conversion error.
+ Utf8Error(::std::str::Utf8Error),
+
+ /// An I/O error, from std::io.
+ IoError(::std::io::Error),
+
+ /// An error which is returned when the end of the document was reached prematurely.
+ EndOfDocument,
+
+ /// An error which is returned when an element is closed when it shouldn't be
+ InvalidElementClosed,
+
+ /// An error which is returned when an elemet's name contains more than one colon
+ InvalidElement,
+
+ /// An error which is returned when a comment is to be parsed by minidom
+ #[cfg(not(comments))]
+ CommentsDisabled,
+}
+
+impl StdError for Error {
+ fn cause(&self) -> Option<&dyn StdError> {
+ match self {
+ // TODO: return Some(e) for this case after the merge of
+ // https://github.com/tafia/quick-xml/pull/170
+ Error::XmlError(_e) => None,
+ Error::Utf8Error(e) => Some(e),
+ Error::IoError(e) => Some(e),
+ Error::EndOfDocument => None,
+ Error::InvalidElementClosed => None,
+ Error::InvalidElement => None,
+ #[cfg(not(comments))]
+ Error::CommentsDisabled => None,
+ }
+ }
+}
+
+impl std::fmt::Display for Error {
+ fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
+ match self {
+ Error::XmlError(e) => write!(fmt, "XML error: {}", e),
+ Error::Utf8Error(e) => write!(fmt, "UTF-8 error: {}", e),
+ Error::IoError(e) => write!(fmt, "IO error: {}", e),
+ Error::EndOfDocument => write!(fmt, "the end of the document has been reached prematurely"),
+ Error::InvalidElementClosed => write!(fmt, "the XML is invalid, an element was wrongly closed"),
+ Error::InvalidElement => write!(fmt, "the XML element is invalid"),
+ #[cfg(not(comments))]
+ Error::CommentsDisabled => write!(fmt, "a comment has been found even though comments are disabled by feature"),
+ }
+ }
+}
+
+impl From<::quick_xml::Error> for Error {
+ fn from(err: ::quick_xml::Error) -> Error {
+ Error::XmlError(err)
+ }
+}
+
+impl From<::std::str::Utf8Error> for Error {
+ fn from(err: ::std::str::Utf8Error) -> Error {
+ Error::Utf8Error(err)
+ }
+}
+
+impl From<::std::io::Error> for Error {
+ fn from(err: ::std::io::Error) -> Error {
+ Error::IoError(err)
+ }
+}
+
+/// Our simplified Result type.
+pub type Result<T> = ::std::result::Result<T, Error>;
@@ -0,0 +1,80 @@
+#![deny(missing_docs)]
+
+//! A minimal DOM crate built on top of quick-xml.
+//!
+//! This library exports an `Element` struct which represents a DOM tree.
+//!
+//! # Example
+//!
+//! Run with `cargo run --example articles`. Located in `examples/articles.rs`.
+//!
+//! ```rust,ignore
+//! extern crate minidom;
+//!
+//! use minidom::Element;
+//!
+//! const DATA: &'static str = r#"<articles xmlns="article">
+//! <article>
+//! <title>10 Terrible Bugs You Would NEVER Believe Happened</title>
+//! <body>
+//! Rust fixed them all. <3
+//! </body>
+//! </article>
+//! <article>
+//! <title>BREAKING NEWS: Physical Bug Jumps Out Of Programmer's Screen</title>
+//! <body>
+//! Just kidding!
+//! </body>
+//! </article>
+//! </articles>"#;
+//!
+//! const ARTICLE_NS: &'static str = "article";
+//!
+//! #[derive(Debug)]
+//! pub struct Article {
+//! title: String,
+//! body: String,
+//! }
+//!
+//! fn main() {
+//! let root: Element = DATA.parse().unwrap();
+//!
+//! let mut articles: Vec<Article> = Vec::new();
+//!
+//! for child in root.children() {
+//! if child.is("article", ARTICLE_NS) {
+//! let title = child.get_child("title", ARTICLE_NS).unwrap().text();
+//! let body = child.get_child("body", ARTICLE_NS).unwrap().text();
+//! articles.push(Article {
+//! title: title,
+//! body: body.trim().to_owned(),
+//! });
+//! }
+//! }
+//!
+//! println!("{:?}", articles);
+//! }
+//! ```
+//!
+//! # Usage
+//!
+//! To use `minidom`, add this to your `Cargo.toml` under `dependencies`:
+//!
+//! ```toml,ignore
+//! minidom = "*"
+//! ```
+
+pub use quick_xml;
+
+pub mod error;
+pub mod element;
+pub mod convert;
+pub mod node;
+mod namespace_set;
+
+#[cfg(test)] mod tests;
+
+pub use error::{Error, Result};
+pub use element::{Element, Children, ChildrenMut, ElementBuilder};
+pub use node::Node;
+pub use convert::IntoAttributeValue;
@@ -0,0 +1,170 @@
+use std::collections::BTreeMap;
+use std::cell::RefCell;
+use std::fmt;
+use std::rc::Rc;
+
+
+#[derive(Clone, PartialEq, Eq)]
+pub struct NamespaceSet {
+ parent: RefCell<Option<Rc<NamespaceSet>>>,
+ namespaces: BTreeMap<Option<String>, String>,
+}
+
+impl Default for NamespaceSet {
+ fn default() -> Self {
+ NamespaceSet {
+ parent: RefCell::new(None),
+ namespaces: BTreeMap::new(),
+ }
+ }
+}
+
+impl fmt::Debug for NamespaceSet {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "NamespaceSet(")?;
+ for (prefix, namespace) in &self.namespaces {
+ write!(f, "xmlns{}={:?}, ", match prefix {
+ None => String::new(),
+ Some(prefix) => format!(":{}", prefix),
+ }, namespace)?;
+ }
+ write!(f, "parent: {:?})", *self.parent.borrow())
+ }
+}
+
+impl NamespaceSet {
+ pub fn declared_ns(&self) -> &BTreeMap<Option<String>, String> {
+ &self.namespaces
+ }
+
+ pub fn get(&self, prefix: &Option<String>) -> Option<String> {
+ match self.namespaces.get(prefix) {
+ Some(ns) => Some(ns.clone()),
+ None => match *self.parent.borrow() {
+ None => None,
+ Some(ref parent) => parent.get(prefix)
+ },
+ }
+ }
+
+ pub fn has<NS: AsRef<str>>(&self, prefix: &Option<String>, wanted_ns: NS) -> bool {
+ match self.namespaces.get(prefix) {
+ Some(ns) =>
+ ns == wanted_ns.as_ref(),
+ None => match *self.parent.borrow() {
+ None =>
+ false,
+ Some(ref parent) =>
+ parent.has(prefix, wanted_ns),
+ },
+ }
+ }
+
+ pub fn set_parent(&self, parent: Rc<NamespaceSet>) {
+ let mut parent_ns = self.parent.borrow_mut();
+ let new_set = parent;
+ *parent_ns = Some(new_set);
+ }
+
+}
+
+impl From<BTreeMap<Option<String>, String>> for NamespaceSet {
+ fn from(namespaces: BTreeMap<Option<String>, String>) -> Self {
+ NamespaceSet {
+ parent: RefCell::new(None),
+ namespaces,
+ }
+ }
+}
+
+impl From<Option<String>> for NamespaceSet {
+ fn from(namespace: Option<String>) -> Self {
+ match namespace {
+ None => Self::default(),
+ Some(namespace) => Self::from(namespace),
+ }
+ }
+}
+
+impl From<String> for NamespaceSet {
+ fn from(namespace: String) -> Self {
+ let mut namespaces = BTreeMap::new();
+ namespaces.insert(None, namespace);
+
+ NamespaceSet {
+ parent: RefCell::new(None),
+ namespaces,
+ }
+ }
+}
+
+impl From<(Option<String>, String)> for NamespaceSet {
+ fn from(prefix_namespace: (Option<String>, String)) -> Self {
+ let (prefix, namespace) = prefix_namespace;
+ let mut namespaces = BTreeMap::new();
+ namespaces.insert(prefix, namespace);
+
+ NamespaceSet {
+ parent: RefCell::new(None),
+ namespaces,
+ }
+ }
+}
+
+impl From<(String, String)> for NamespaceSet {
+ fn from(prefix_namespace: (String, String)) -> Self {
+ let (prefix, namespace) = prefix_namespace;
+ Self::from((Some(prefix), namespace))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn get_has() {
+ let namespaces = NamespaceSet::from("foo".to_owned());
+ assert_eq!(namespaces.get(&None), Some("foo".to_owned()));
+ assert!(namespaces.has(&None, "foo"));
+ }
+
+ #[test]
+ fn get_has_prefixed() {
+ let namespaces = NamespaceSet::from(("x".to_owned(), "bar".to_owned()));
+ assert_eq!(namespaces.get(&Some("x".to_owned())), Some("bar".to_owned()));
+ assert!(namespaces.has(&Some("x".to_owned()), "bar"));
+ }
+
+ #[test]
+ fn get_has_recursive() {
+ let mut parent = NamespaceSet::from("foo".to_owned());
+ for _ in 0..1000 {
+ let namespaces = NamespaceSet::default();
+ namespaces.set_parent(Rc::new(parent));
+ assert_eq!(namespaces.get(&None), Some("foo".to_owned()));
+ assert!(namespaces.has(&None, "foo"));
+ parent = namespaces;
+ }
+ }
+
+ #[test]
+ fn get_has_prefixed_recursive() {
+ let mut parent = NamespaceSet::from(("x".to_owned(), "bar".to_owned()));
+ for _ in 0..1000 {
+ let namespaces = NamespaceSet::default();
+ namespaces.set_parent(Rc::new(parent));
+ assert_eq!(namespaces.get(&Some("x".to_owned())), Some("bar".to_owned()));
+ assert!(namespaces.has(&Some("x".to_owned()), "bar"));
+ parent = namespaces;
+ }
+ }
+
+ #[test]
+ fn debug_looks_correct() {
+ let parent = NamespaceSet::from("http://www.w3.org/2000/svg".to_owned());
+ let namespaces = NamespaceSet::from(("xhtml".to_owned(), "http://www.w3.org/1999/xhtml".to_owned()));
+ namespaces.set_parent(Rc::new(parent));
+ assert_eq!(format!("{:?}", namespaces), "NamespaceSet(xmlns:xhtml=\"http://www.w3.org/1999/xhtml\", parent: Some(NamespaceSet(xmlns=\"http://www.w3.org/2000/svg\", parent: None)))");
+ }
+}
@@ -0,0 +1,207 @@
+//! Provides the `Node` struct, which represents a node in the DOM.
+
+use crate::element::{Element, ElementBuilder};
+use crate::error::Result;
+
+use std::io::Write;
+
+use quick_xml::Writer as EventWriter;
+use quick_xml::events::{Event, BytesText};
+
+/// A node in an element tree.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum Node {
+ /// An `Element`.
+ Element(Element),
+ /// A text node.
+ Text(String),
+ #[cfg(feature = "comments")]
+ /// A comment node.
+ Comment(String),
+}
+
+impl Node {
+ /// Turns this into a reference to an `Element` if this is an element node.
+ /// Else this returns `None`.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use minidom::Node;
+ ///
+ /// let elm = Node::Element("<meow />".parse().unwrap());
+ /// let txt = Node::Text("meow".to_owned());
+ ///
+ /// assert_eq!(elm.as_element().unwrap().name(), "meow");
+ /// assert_eq!(txt.as_element(), None);
+ /// ```
+ pub fn as_element(&self) -> Option<&Element> {
+ match *self {
+ Node::Element(ref e) => Some(e),
+ Node::Text(_) => None,
+ #[cfg(feature = "comments")]
+ Node::Comment(_) => None,
+ }
+ }
+
+ /// Turns this into a mutable reference of an `Element` if this is an element node.
+ /// Else this returns `None`.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use minidom::Node;
+ ///
+ /// let mut elm = Node::Element("<meow />".parse().unwrap());
+ /// let mut txt = Node::Text("meow".to_owned());
+ ///
+ /// assert_eq!(elm.as_element_mut().unwrap().name(), "meow");
+ /// assert_eq!(txt.as_element_mut(), None);
+ /// ```
+ pub fn as_element_mut(&mut self) -> Option<&mut Element> {
+ match *self {
+ Node::Element(ref mut e) => Some(e),
+ Node::Text(_) => None,
+ #[cfg(feature = "comments")]
+ Node::Comment(_) => None,
+ }
+ }
+
+ /// Turns this into an `Element`, consuming self, if this is an element node.
+ /// Else this returns `None`.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use minidom::Node;
+ ///
+ /// let elm = Node::Element("<meow />".parse().unwrap());
+ /// let txt = Node::Text("meow".to_owned());
+ ///
+ /// assert_eq!(elm.into_element().unwrap().name(), "meow");
+ /// assert_eq!(txt.into_element(), None);
+ /// ```
+ pub fn into_element(self) -> Option<Element> {
+ match self {
+ Node::Element(e) => Some(e),
+ Node::Text(_) => None,
+ #[cfg(feature = "comments")]
+ Node::Comment(_) => None,
+ }
+ }
+
+ /// Turns this into an `&str` if this is a text node.
+ /// Else this returns `None`.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use minidom::Node;
+ ///
+ /// let elm = Node::Element("<meow />".parse().unwrap());
+ /// let txt = Node::Text("meow".to_owned());
+ ///
+ /// assert_eq!(elm.as_text(), None);
+ /// assert_eq!(txt.as_text().unwrap(), "meow");
+ /// ```
+ pub fn as_text(&self) -> Option<&str> {
+ match *self {
+ Node::Element(_) => None,
+ Node::Text(ref s) => Some(s),
+ #[cfg(feature = "comments")]
+ Node::Comment(_) => None,
+ }
+ }
+
+ /// Turns this into an `&mut String` if this is a text node.
+ /// Else this returns `None`.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use minidom::Node;
+ ///
+ /// let mut elm = Node::Element("<meow />".parse().unwrap());
+ /// let mut txt = Node::Text("meow".to_owned());
+ ///
+ /// assert_eq!(elm.as_text_mut(), None);
+ /// {
+ /// let text_mut = txt.as_text_mut().unwrap();
+ /// assert_eq!(text_mut, "meow");
+ /// text_mut.push_str("zies");
+ /// assert_eq!(text_mut, "meowzies");
+ /// }
+ /// assert_eq!(txt.as_text().unwrap(), "meowzies");
+ /// ```
+ pub fn as_text_mut(&mut self) -> Option<&mut String> {
+ match *self {
+ Node::Element(_) => None,
+ Node::Text(ref mut s) => Some(s),
+ #[cfg(feature = "comments")]
+ Node::Comment(_) => None,
+ }
+ }
+
+ /// Turns this into an `String`, consuming self, if this is a text node.
+ /// Else this returns `None`.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use minidom::Node;
+ ///
+ /// let elm = Node::Element("<meow />".parse().unwrap());
+ /// let txt = Node::Text("meow".to_owned());
+ ///
+ /// assert_eq!(elm.into_text(), None);
+ /// assert_eq!(txt.into_text().unwrap(), "meow");
+ /// ```
+ pub fn into_text(self) -> Option<String> {
+ match self {
+ Node::Element(_) => None,
+ Node::Text(s) => Some(s),
+ #[cfg(feature = "comments")]
+ Node::Comment(_) => None,
+ }
+ }
+
+ #[doc(hidden)]
+ pub(crate) fn write_to_inner<W: Write>(&self, writer: &mut EventWriter<W>) -> Result<()>{
+ match *self {
+ Node::Element(ref elmt) => elmt.write_to_inner(writer)?,
+ Node::Text(ref s) => {
+ writer.write_event(Event::Text(BytesText::from_plain_str(s)))?;
+ },
+ #[cfg(feature = "comments")]
+ Node::Comment(ref s) => {
+ writer.write_event(Event::Comment(BytesText::from_plain_str(s)))?;
+ },
+ }
+
+ Ok(())
+ }
+}
+
+impl From<Element> for Node {
+ fn from(elm: Element) -> Node {
+ Node::Element(elm)
+ }
+}
+
+impl From<String> for Node {
+ fn from(s: String) -> Node {
+ Node::Text(s)
+ }
+}
+
+impl<'a> From<&'a str> for Node {
+ fn from(s: &'a str) -> Node {
+ Node::Text(s.to_owned())
+ }
+}
+
+impl From<ElementBuilder> for Node {
+ fn from(builder: ElementBuilder) -> Node {
+ Node::Element(builder.build())
+ }
+}
@@ -0,0 +1,251 @@
+use crate::element::Element;
+
+use quick_xml::Reader;
+
+const TEST_STRING: &'static str = r#"<?xml version="1.0" encoding="utf-8"?><root xmlns="root_ns" a="b" xml:lang="en">meow<child c="d"/><child xmlns="child_ns" d="e" xml:lang="fr"/>nya</root>"#;
+
+fn build_test_tree() -> Element {
+ let mut root = Element::builder("root")
+ .ns("root_ns")
+ .attr("xml:lang", "en")
+ .attr("a", "b")
+ .build();
+ root.append_text_node("meow");
+ let child = Element::builder("child")
+ .attr("c", "d")
+ .build();
+ root.append_child(child);
+ let other_child = Element::builder("child")
+ .ns("child_ns")
+ .attr("d", "e")
+ .attr("xml:lang", "fr")
+ .build();
+ root.append_child(other_child);
+ root.append_text_node("nya");
+ root
+}
+
+#[cfg(feature = "comments")]
+const COMMENT_TEST_STRING: &'static str = r#"<?xml version="1.0" encoding="utf-8"?><root><!--This is a child.--><child attr="val"><!--This is a grandchild.--><grandchild/></child></root>"#;
+
+#[cfg(feature = "comments")]
+fn build_comment_test_tree() -> Element {
+ let mut root = Element::builder("root").build();
+ root.append_comment_node("This is a child.");
+ let mut child = Element::builder("child").attr("attr", "val").build();
+ child.append_comment_node("This is a grandchild.");
+ let grand_child = Element::builder("grandchild").build();
+ child.append_child(grand_child);
+ root.append_child(child);
+ root
+}
+
+#[test]
+fn reader_works() {
+ let mut reader = Reader::from_str(TEST_STRING);
+ assert_eq!(Element::from_reader(&mut reader).unwrap(), build_test_tree());
+}
+
+#[test]
+fn writer_works() {
+ let root = build_test_tree();
+ let mut writer = Vec::new();
+ {
+ root.write_to(&mut writer).unwrap();
+ }
+ assert_eq!(String::from_utf8(writer).unwrap(), TEST_STRING);
+}
+
+#[test]
+fn writer_escapes_attributes() {
+ let root = Element::builder("root")
+ .attr("a", "\"Air\" quotes")
+ .build();
+ let mut writer = Vec::new();
+ {
+ root.write_to(&mut writer).unwrap();
+ }
+ assert_eq!(String::from_utf8(writer).unwrap(),
+ r#"<?xml version="1.0" encoding="utf-8"?><root a=""Air" quotes"/>"#
+ );
+}
+
+#[test]
+fn writer_escapes_text() {
+ let root = Element::builder("root")
+ .append("<3")
+ .build();
+ let mut writer = Vec::new();
+ {
+ root.write_to(&mut writer).unwrap();
+ }
+ assert_eq!(String::from_utf8(writer).unwrap(),
+ r#"<?xml version="1.0" encoding="utf-8"?><root><3</root>"#
+ );
+}
+
+#[test]
+fn builder_works() {
+ let elem = Element::builder("a")
+ .ns("b")
+ .attr("c", "d")
+ .append(Element::builder("child"))
+ .append("e")
+ .build();
+ assert_eq!(elem.name(), "a");
+ assert_eq!(elem.ns(), Some("b".to_owned()));
+ assert_eq!(elem.attr("c"), Some("d"));
+ assert_eq!(elem.attr("x"), None);
+ assert_eq!(elem.text(), "e");
+ assert!(elem.has_child("child", "b"));
+ assert!(elem.is("a", "b"));
+}
+
+#[test]
+fn children_iter_works() {
+ let root = build_test_tree();
+ let mut iter = root.children();
+ assert!(iter.next().unwrap().is("child", "root_ns"));
+ assert!(iter.next().unwrap().is("child", "child_ns"));
+ assert_eq!(iter.next(), None);
+}
+
+#[test]
+fn get_child_works() {
+ let root = build_test_tree();
+ assert_eq!(root.get_child("child", "inexistent_ns"), None);
+ assert_eq!(root.get_child("not_a_child", "root_ns"), None);
+ assert!(root.get_child("child", "root_ns").unwrap().is("child", "root_ns"));
+ assert!(root.get_child("child", "child_ns").unwrap().is("child", "child_ns"));
+ assert_eq!(root.get_child("child", "root_ns").unwrap().attr("c"), Some("d"));
+ assert_eq!(root.get_child("child", "child_ns").unwrap().attr("d"), Some("e"));
+}
+
+#[test]
+fn namespace_propagation_works() {
+ let mut root = Element::builder("root").ns("root_ns").build();
+ let mut child = Element::bare("child");
+ let grandchild = Element::bare("grandchild");
+ child.append_child(grandchild);
+ root.append_child(child);
+
+ assert_eq!(root.get_child("child", "root_ns").unwrap().ns(), root.ns());
+ assert_eq!(root.get_child("child", "root_ns").unwrap()
+ .get_child("grandchild", "root_ns").unwrap()
+ .ns(), root.ns());
+}
+
+#[test]
+fn two_elements_with_same_arguments_different_order_are_equal() {
+ let elem1: Element = "<a b='a' c=''/>".parse().unwrap();
+ let elem2: Element = "<a c='' b='a'/>".parse().unwrap();
+ assert_eq!(elem1, elem2);
+
+ let elem1: Element = "<a b='a' c=''/>".parse().unwrap();
+ let elem2: Element = "<a c='d' b='a'/>".parse().unwrap();
+ assert_ne!(elem1, elem2);
+}
+
+#[test]
+fn namespace_attributes_works() {
+ let mut reader = Reader::from_str(TEST_STRING);
+ let root = Element::from_reader(&mut reader).unwrap();
+ assert_eq!("en", root.attr("xml:lang").unwrap());
+ assert_eq!("fr", root.get_child("child", "child_ns").unwrap().attr("xml:lang").unwrap());
+}
+
+#[test]
+fn wrongly_closed_elements_error() {
+ let elem1 = "<a></b>".parse::<Element>();
+ assert!(elem1.is_err());
+ let elem1 = "<a></c></a>".parse::<Element>();
+ assert!(elem1.is_err());
+ let elem1 = "<a><c><d/></c></a>".parse::<Element>();
+ assert!(elem1.is_ok());
+}
+
+#[test]
+fn namespace_simple() {
+ let elem: Element = "<message xmlns='jabber:client'/>".parse().unwrap();
+ assert_eq!(elem.name(), "message");
+ assert_eq!(elem.ns(), Some("jabber:client".to_owned()));
+}
+
+#[test]
+fn namespace_prefixed() {
+ let elem: Element = "<stream:features xmlns:stream='http://etherx.jabber.org/streams'/>"
+ .parse().unwrap();
+ assert_eq!(elem.name(), "features");
+ assert_eq!(elem.ns(), Some("http://etherx.jabber.org/streams".to_owned()));
+}
+
+#[test]
+fn namespace_inherited_simple() {
+ let elem: Element = "<stream xmlns='jabber:client'><message/></stream>".parse().unwrap();
+ assert_eq!(elem.name(), "stream");
+ assert_eq!(elem.ns(), Some("jabber:client".to_owned()));
+ let child = elem.children().next().unwrap();
+ assert_eq!(child.name(), "message");
+ assert_eq!(child.ns(), Some("jabber:client".to_owned()));
+}
+
+#[test]
+fn namespace_inherited_prefixed1() {
+ let elem: Element = "<stream:features xmlns:stream='http://etherx.jabber.org/streams' xmlns='jabber:client'><message/></stream:features>"
+ .parse().unwrap();
+ assert_eq!(elem.name(), "features");
+ assert_eq!(elem.ns(), Some("http://etherx.jabber.org/streams".to_owned()));
+ let child = elem.children().next().unwrap();
+ assert_eq!(child.name(), "message");
+ assert_eq!(child.ns(), Some("jabber:client".to_owned()));
+}
+
+#[test]
+fn namespace_inherited_prefixed2() {
+ let elem: Element = "<stream xmlns='http://etherx.jabber.org/streams' xmlns:jabber='jabber:client'><jabber:message/></stream>"
+ .parse().unwrap();
+ assert_eq!(elem.name(), "stream");
+ assert_eq!(elem.ns(), Some("http://etherx.jabber.org/streams".to_owned()));
+ let child = elem.children().next().unwrap();
+ assert_eq!(child.name(), "message");
+ assert_eq!(child.ns(), Some("jabber:client".to_owned()));
+}
+
+#[cfg(feature = "comments")]
+#[test]
+fn read_comments() {
+ let mut reader = Reader::from_str(COMMENT_TEST_STRING);
+ assert_eq!(Element::from_reader(&mut reader).unwrap(), build_comment_test_tree());
+}
+
+#[cfg(feature = "comments")]
+#[test]
+fn write_comments() {
+ let root = build_comment_test_tree();
+ let mut writer = Vec::new();
+ {
+ root.write_to(&mut writer).unwrap();
+ }
+ assert_eq!(String::from_utf8(writer).unwrap(), COMMENT_TEST_STRING);
+}
+
+#[test]
+fn xml_error() {
+ match "<a></b>".parse::<Element>() {
+ Err(crate::error::Error::XmlError(_)) => (),
+ err => panic!("No or wrong error: {:?}", err)
+ }
+
+ match "<a></".parse::<Element>() {
+ Err(crate::error::Error::XmlError(_)) => (),
+ err => panic!("No or wrong error: {:?}", err)
+ }
+}
+
+#[test]
+fn invalid_element_error() {
+ match "<a:b:c>".parse::<Element>() {
+ Err(crate::error::Error::InvalidElement) => (),
+ err => panic!("No or wrong error: {:?}", err)
+ }
+}
@@ -0,0 +1 @@
+target
@@ -0,0 +1,14 @@
+stages:
+ - build
+rust-latest:
+ stage: build
+ image: rust:latest
+ script:
+ - cargo build --verbose
+ - cargo test --verbose
+rust-nightly:
+ stage: build
+ image: rustlang/rust:nightly
+ script:
+ - cargo build --verbose
+ - cargo test --verbose
@@ -0,0 +1,28 @@
+[package]
+name = "tokio-xmpp"
+version = "1.0.1"
+authors = ["Astro <astro@spaceboyz.net>", "Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>", "pep <pep+code@bouah.net>", "O01eg <o01eg@yandex.ru>"]
+description = "Asynchronous XMPP for Rust with tokio"
+license = "MPL-2.0"
+homepage = "https://gitlab.com/xmpp-rs/tokio-xmpp"
+repository = "https://gitlab.com/xmpp-rs/tokio-xmpp"
+documentation = "https://docs.rs/tokio-xmpp"
+categories = ["asynchronous", "network-programming"]
+keywords = ["xmpp", "tokio"]
+edition = "2018"
+
+[dependencies]
+bytes = "0.4"
+futures = "0.1"
+idna = "0.2"
+native-tls = "0.2"
+sasl = "0.4"
+tokio = "0.1"
+tokio-codec = "0.1"
+trust-dns-resolver = "0.12"
+trust-dns-proto = "0.8"
+tokio-io = "0.1"
+tokio-tls = "0.2"
+quick-xml = "0.17"
+xml5ever = "0.15"
+xmpp-parsers = "0.16"
@@ -0,0 +1,6 @@
+# TODO
+
+- [ ] minidom ns
+- [ ] replace debug output with log crate
+- [ ] customize tls verify?
+- [ ] more tests
@@ -0,0 +1,130 @@
+use futures::{future, Sink, Stream};
+use std::convert::TryFrom;
+use std::env::args;
+use std::process::exit;
+use tokio::runtime::current_thread::Runtime;
+use tokio_xmpp::{Client, xmpp_codec::Packet};
+use xmpp_parsers::{
+ Element,
+ Jid,
+ ns,
+ iq::{
+ Iq,
+ IqType,
+ },
+ disco::{
+ DiscoInfoResult,
+ DiscoInfoQuery,
+ },
+ server_info::ServerInfo,
+};
+
+fn main() {
+ let args: Vec<String> = args().collect();
+ if args.len() != 4 {
+ println!("Usage: {} <jid> <password> <target>", args[0]);
+ exit(1);
+ }
+ let jid = &args[1];
+ let password = &args[2];
+ let target = &args[3];
+
+ // tokio_core context
+ let mut rt = Runtime::new().unwrap();
+ // Client instance
+ let client = Client::new(jid, password).unwrap();
+
+ // Make the two interfaces for sending and receiving independent
+ // of each other so we can move one into a closure.
+ let (mut sink, stream) = client.split();
+ // Wrap sink in Option so that we can take() it for the send(self)
+ // to consume and return it back when ready.
+ let mut send = move |packet| {
+ sink.start_send(packet).expect("start_send");
+ };
+ // Main loop, processes events
+ let mut wait_for_stream_end = false;
+ let done = stream.for_each(|event| {
+ if wait_for_stream_end {
+ /* Do Nothing. */
+ } else if event.is_online() {
+ println!("Online!");
+
+ let target_jid: Jid = target.clone().parse().unwrap();
+ let iq = make_disco_iq(target_jid);
+ println!("Sending disco#info request to {}", target.clone());
+ println!(">> {}", String::from(&iq));
+ send(Packet::Stanza(iq));
+ } else if let Some(stanza) = event.into_stanza() {
+ if stanza.is("iq", "jabber:client") {
+ let iq = Iq::try_from(stanza).unwrap();
+ if let IqType::Result(Some(payload)) = iq.payload {
+ if payload.is("query", ns::DISCO_INFO) {
+ if let Ok(disco_info) = DiscoInfoResult::try_from(payload) {
+ for ext in disco_info.extensions {
+ if let Ok(server_info) = ServerInfo::try_from(ext) {
+ print_server_info(server_info);
+ wait_for_stream_end = true;
+ send(Packet::StreamEnd);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ Box::new(future::ok(()))
+ });
+
+ // Start polling `done`
+ match rt.block_on(done) {
+ Ok(_) => (),
+ Err(e) => {
+ println!("Fatal: {}", e);
+ ()
+ }
+ }
+}
+
+fn make_disco_iq(target: Jid) -> Element {
+ Iq::from_get("disco", DiscoInfoQuery { node: None })
+ .with_id(String::from("contact"))
+ .with_to(target)
+ .into()
+}
+
+fn convert_field(field: Vec<String>) -> String {
+ field.iter()
+ .fold((field.len(), String::new()), |(l, mut acc), s| {
+ acc.push('<');
+ acc.push_str(&s);
+ acc.push('>');
+ if l > 1 {
+ acc.push(',');
+ acc.push(' ');
+ }
+ (0, acc)
+ }).1
+}
+
+fn print_server_info(server_info: ServerInfo) {
+ if server_info.abuse.len() != 0 {
+ println!("abuse: {}", convert_field(server_info.abuse));
+ }
+ if server_info.admin.len() != 0 {
+ println!("admin: {}", convert_field(server_info.admin));
+ }
+ if server_info.feedback.len() != 0 {
+ println!("feedback: {}", convert_field(server_info.feedback));
+ }
+ if server_info.sales.len() != 0 {
+ println!("sales: {}", convert_field(server_info.sales));
+ }
+ if server_info.security.len() != 0 {
+ println!("security: {}", convert_field(server_info.security));
+ }
+ if server_info.support.len() != 0 {
+ println!("support: {}", convert_field(server_info.support));
+ }
+}
@@ -0,0 +1,232 @@
+use futures::{future, Future, Sink, Stream};
+use std::convert::TryFrom;
+use std::env::args;
+use std::fs::{create_dir_all, File};
+use std::io::{self, Write};
+use std::process::exit;
+use std::str::FromStr;
+use tokio::runtime::current_thread::Runtime;
+use tokio_xmpp::{Client, Packet};
+use xmpp_parsers::{
+ avatar::{Data as AvatarData, Metadata as AvatarMetadata},
+ caps::{compute_disco, hash_caps, Caps},
+ disco::{DiscoInfoQuery, DiscoInfoResult, Feature, Identity},
+ hashes::Algo,
+ iq::{Iq, IqType},
+ message::Message,
+ ns,
+ presence::{Presence, Type as PresenceType},
+ pubsub::{
+ event::PubSubEvent,
+ pubsub::{Items, PubSub},
+ NodeName,
+ },
+ stanza_error::{StanzaError, ErrorType, DefinedCondition},
+ Jid,
+};
+
+fn main() {
+ let args: Vec<String> = args().collect();
+ if args.len() != 3 {
+ println!("Usage: {} <jid> <password>", args[0]);
+ exit(1);
+ }
+ let jid = &args[1];
+ let password = &args[2];
+
+ // tokio_core context
+ let mut rt = Runtime::new().unwrap();
+ // Client instance
+ let client = Client::new(jid, password).unwrap();
+
+ // Make the two interfaces for sending and receiving independent
+ // of each other so we can move one into a closure.
+ let (sink, stream) = client.split();
+
+ // Create outgoing pipe
+ let (mut tx, rx) = futures::unsync::mpsc::unbounded();
+ rt.spawn(
+ rx.forward(
+ sink.sink_map_err(|_| panic!("Pipe"))
+ )
+ .map(|(rx, mut sink)| {
+ drop(rx);
+ let _ = sink.close();
+ })
+ .map_err(|e| {
+ panic!("Send error: {:?}", e);
+ })
+ );
+
+ let disco_info = make_disco();
+
+ // Main loop, processes events
+ let mut wait_for_stream_end = false;
+ let done = stream.for_each(move |event| {
+ // Helper function to send an iq error.
+ let mut send_error = |to, id, type_, condition, text: &str| {
+ let error = StanzaError::new(type_, condition, "en", text);
+ let iq = Iq::from_error(id, error)
+ .with_to(to);
+ tx.start_send(Packet::Stanza(iq.into())).unwrap();
+ };
+
+ if wait_for_stream_end {
+ /* Do nothing */
+ } else if event.is_online() {
+ println!("Online!");
+
+ let caps = get_disco_caps(&disco_info, "https://gitlab.com/xmpp-rs/tokio-xmpp");
+ let presence = make_presence(caps);
+ tx.start_send(Packet::Stanza(presence.into())).unwrap();
+ } else if let Some(stanza) = event.into_stanza() {
+ if stanza.is("iq", "jabber:client") {
+ let iq = Iq::try_from(stanza).unwrap();
+ if let IqType::Get(payload) = iq.payload {
+ if payload.is("query", ns::DISCO_INFO) {
+ let query = DiscoInfoQuery::try_from(payload);
+ match query {
+ Ok(query) => {
+ let mut disco = disco_info.clone();
+ disco.node = query.node;
+ let iq = Iq::from_result(iq.id, Some(disco))
+ .with_to(iq.from.unwrap());
+ tx.start_send(Packet::Stanza(iq.into())).unwrap();
+ },
+ Err(err) => {
+ send_error(iq.from.unwrap(), iq.id, ErrorType::Modify, DefinedCondition::BadRequest, &format!("{}", err));
+ },
+ }
+ } else {
+ // We MUST answer unhandled get iqs with a service-unavailable error.
+ send_error(iq.from.unwrap(), iq.id, ErrorType::Cancel, DefinedCondition::ServiceUnavailable, "No handler defined for this kind of iq.");
+ }
+ } else if let IqType::Result(Some(payload)) = iq.payload {
+ if payload.is("pubsub", ns::PUBSUB) {
+ let pubsub = PubSub::try_from(payload).unwrap();
+ let from =
+ iq.from.clone().unwrap_or(Jid::from_str(jid).unwrap());
+ handle_iq_result(pubsub, &from);
+ }
+ } else if let IqType::Set(_) = iq.payload {
+ // We MUST answer unhandled set iqs with a service-unavailable error.
+ send_error(iq.from.unwrap(), iq.id, ErrorType::Cancel, DefinedCondition::ServiceUnavailable, "No handler defined for this kind of iq.");
+ }
+ } else if stanza.is("message", "jabber:client") {
+ let message = Message::try_from(stanza).unwrap();
+ let from = message.from.clone().unwrap();
+ if let Some(body) = message.get_best_body(vec!["en"]) {
+ if body.1 .0 == "die" {
+ println!("Secret die command triggered by {}", from);
+ wait_for_stream_end = true;
+ tx.start_send(Packet::StreamEnd).unwrap();
+ }
+ }
+ for child in message.payloads {
+ if child.is("event", ns::PUBSUB_EVENT) {
+ let event = PubSubEvent::try_from(child).unwrap();
+ if let PubSubEvent::PublishedItems { node, items } = event {
+ if node.0 == ns::AVATAR_METADATA {
+ for item in items.into_iter() {
+ let payload = item.payload.clone().unwrap();
+ if payload.is("metadata", ns::AVATAR_METADATA) {
+ // TODO: do something with these metadata.
+ let _metadata = AvatarMetadata::try_from(payload).unwrap();
+ println!(
+ "[1m{}[0m has published an avatar, downloading...",
+ from.clone()
+ );
+ let iq = download_avatar(from.clone());
+ tx.start_send(Packet::Stanza(iq.into())).unwrap();
+ }
+ }
+ }
+ }
+ }
+ }
+ } else if stanza.is("presence", "jabber:client") {
+ // Nothing to do here.
+ } else {
+ panic!("Unknown stanza: {}", String::from(&stanza));
+ }
+ }
+
+ future::ok(())
+ });
+
+ // Start polling `done`
+ match rt.block_on(done) {
+ Ok(_) => (),
+ Err(e) => {
+ println!("Fatal: {}", e);
+ ()
+ }
+ }
+}
+
+fn make_disco() -> DiscoInfoResult {
+ let identities = vec![Identity::new("client", "bot", "en", "tokio-xmpp")];
+ let features = vec![
+ Feature::new(ns::DISCO_INFO),
+ Feature::new(format!("{}+notify", ns::AVATAR_METADATA)),
+ ];
+ DiscoInfoResult {
+ node: None,
+ identities,
+ features,
+ extensions: vec![],
+ }
+}
+
+fn get_disco_caps(disco: &DiscoInfoResult, node: &str) -> Caps {
+ let caps_data = compute_disco(disco);
+ let hash = hash_caps(&caps_data, Algo::Sha_1).unwrap();
+ Caps::new(node, hash)
+}
+
+// Construct a <presence/>
+fn make_presence(caps: Caps) -> Presence {
+ let mut presence = Presence::new(PresenceType::None)
+ .with_priority(-1);
+ presence.set_status("en", "Downloading avatars.");
+ presence.add_payload(caps);
+ presence
+}
+
+fn download_avatar(from: Jid) -> Iq {
+ Iq::from_get("coucou", PubSub::Items(Items {
+ max_items: None,
+ node: NodeName(String::from(ns::AVATAR_DATA)),
+ subid: None,
+ items: Vec::new(),
+ }))
+ .with_to(from)
+}
+
+fn handle_iq_result(pubsub: PubSub, from: &Jid) {
+ if let PubSub::Items(items) = pubsub {
+ if items.node.0 == ns::AVATAR_DATA {
+ for item in items.items {
+ match (item.id.clone(), item.payload.clone()) {
+ (Some(id), Some(payload)) => {
+ let data = AvatarData::try_from(payload).unwrap();
+ save_avatar(from, id.0, &data.data).unwrap();
+ }
+ _ => {}
+ }
+ }
+ }
+ }
+}
+
+fn save_avatar(from: &Jid, id: String, data: &[u8]) -> io::Result<()> {
+ let directory = format!("data/{}", from);
+ let filename = format!("data/{}/{}", from, id);
+ println!(
+ "Saving avatar from [1m{}[0m to [4m{}[0m.",
+ from, filename
+ );
+ create_dir_all(directory)?;
+ let mut file = File::create(filename)?;
+ file.write_all(data)
+}
@@ -0,0 +1,106 @@
+use futures::{future, Future, Sink, Stream};
+use std::convert::TryFrom;
+use std::env::args;
+use std::process::exit;
+use tokio::runtime::current_thread::Runtime;
+use tokio_xmpp::{Client, Packet};
+use xmpp_parsers::{Jid, Element};
+use xmpp_parsers::message::{Body, Message, MessageType};
+use xmpp_parsers::presence::{Presence, Show as PresenceShow, Type as PresenceType};
+
+fn main() {
+ let args: Vec<String> = args().collect();
+ if args.len() != 3 {
+ println!("Usage: {} <jid> <password>", args[0]);
+ exit(1);
+ }
+ let jid = &args[1];
+ let password = &args[2];
+
+ // tokio_core context
+ let mut rt = Runtime::new().unwrap();
+ // Client instance
+ let client = Client::new(jid, password).unwrap();
+
+ // Make the two interfaces for sending and receiving independent
+ // of each other so we can move one into a closure.
+ let (sink, stream) = client.split();
+
+ // Create outgoing pipe
+ let (mut tx, rx) = futures::unsync::mpsc::unbounded();
+ rt.spawn(
+ rx.forward(
+ sink.sink_map_err(|_| panic!("Pipe"))
+ )
+ .map(|(rx, mut sink)| {
+ drop(rx);
+ let _ = sink.close();
+ })
+ .map_err(|e| {
+ panic!("Send error: {:?}", e);
+ })
+ );
+
+ // Main loop, processes events
+ let mut wait_for_stream_end = false;
+ let done = stream.for_each(move |event| {
+ if wait_for_stream_end {
+ /* Do nothing */
+ } else if event.is_online() {
+ let jid = event.get_jid()
+ .map(|jid| format!("{}", jid))
+ .unwrap_or("unknown".to_owned());
+ println!("Online at {}", jid);
+
+ let presence = make_presence();
+ tx.start_send(Packet::Stanza(presence)).unwrap();
+ } else if let Some(message) = event
+ .into_stanza()
+ .and_then(|stanza| Message::try_from(stanza).ok())
+ {
+ match (message.from, message.bodies.get("")) {
+ (Some(ref from), Some(ref body)) if body.0 == "die" => {
+ println!("Secret die command triggered by {}", from);
+ wait_for_stream_end = true;
+ tx.start_send(Packet::StreamEnd).unwrap();
+ }
+ (Some(ref from), Some(ref body)) => {
+ if message.type_ != MessageType::Error {
+ // This is a message we'll echo
+ let reply = make_reply(from.clone(), &body.0);
+ tx.start_send(Packet::Stanza(reply)).unwrap();
+ }
+ }
+ _ => {}
+ }
+ }
+
+ future::ok(())
+ });
+
+ // Start polling `done`
+ match rt.block_on(done) {
+ Ok(_) => (),
+ Err(e) => {
+ println!("Fatal: {}", e);
+ ()
+ }
+ }
+}
+
+// Construct a <presence/>
+fn make_presence() -> Element {
+ let mut presence = Presence::new(PresenceType::None);
+ presence.show = Some(PresenceShow::Chat);
+ presence
+ .statuses
+ .insert(String::from("en"), String::from("Echoing messages."));
+ presence.into()
+}
+
+// Construct a chat <message/>
+fn make_reply(to: Jid, body: &str) -> Element {
+ let mut message = Message::new(Some(to));
+ message.bodies.insert(String::new(), Body(body.to_owned()));
+ message.into()
+}
@@ -0,0 +1,99 @@
+use futures::{future, Sink, Stream};
+use std::convert::TryFrom;
+use std::env::args;
+use std::process::exit;
+use std::str::FromStr;
+use tokio::runtime::current_thread::Runtime;
+use tokio_xmpp::Component;
+use xmpp_parsers::{Jid, Element};
+use xmpp_parsers::message::{Body, Message, MessageType};
+use xmpp_parsers::presence::{Presence, Show as PresenceShow, Type as PresenceType};
+
+fn main() {
+ let args: Vec<String> = args().collect();
+ if args.len() < 3 || args.len() > 5 {
+ println!("Usage: {} <jid> <password> [server] [port]", args[0]);
+ exit(1);
+ }
+ let jid = &args[1];
+ let password = &args[2];
+ let server = &args
+ .get(3)
+ .unwrap()
+ .parse()
+ .unwrap_or("127.0.0.1".to_owned());
+ let port: u16 = args.get(4).unwrap().parse().unwrap_or(5347u16);
+
+ // tokio_core context
+ let mut rt = Runtime::new().unwrap();
+ // Component instance
+ println!("{} {} {} {}", jid, password, server, port);
+ let component = Component::new(jid, password, server, port).unwrap();
+
+ // Make the two interfaces for sending and receiving independent
+ // of each other so we can move one into a closure.
+ println!("Got it: {}", component.jid.clone());
+ let (mut sink, stream) = component.split();
+ // Wrap sink in Option so that we can take() it for the send(self)
+ // to consume and return it back when ready.
+ let mut send = move |stanza| {
+ sink.start_send(stanza).expect("start_send");
+ };
+ // Main loop, processes events
+ let done = stream.for_each(|event| {
+ if event.is_online() {
+ println!("Online!");
+
+ // TODO: replace these hardcoded JIDs
+ let presence = make_presence(
+ Jid::from_str("test@component.linkmauve.fr/coucou").unwrap(),
+ Jid::from_str("linkmauve@linkmauve.fr").unwrap(),
+ );
+ send(presence);
+ } else if let Some(message) = event
+ .into_stanza()
+ .and_then(|stanza| Message::try_from(stanza).ok())
+ {
+ // This is a message we'll echo
+ match (message.from, message.bodies.get("")) {
+ (Some(from), Some(body)) => {
+ if message.type_ != MessageType::Error {
+ let reply = make_reply(from, &body.0);
+ send(reply);
+ }
+ }
+ _ => (),
+ }
+ }
+
+ Box::new(future::ok(()))
+ });
+
+ // Start polling `done`
+ match rt.block_on(done) {
+ Ok(_) => (),
+ Err(e) => {
+ println!("Fatal: {}", e);
+ ()
+ }
+ }
+}
+
+// Construct a <presence/>
+fn make_presence(from: Jid, to: Jid) -> Element {
+ let mut presence = Presence::new(PresenceType::None);
+ presence.from = Some(from);
+ presence.to = Some(to);
+ presence.show = Some(PresenceShow::Chat);
+ presence
+ .statuses
+ .insert(String::from("en"), String::from("Echoing messages."));
+ presence.into()
+}
+
+// Construct a chat <message/>
+fn make_reply(to: Jid, body: &str) -> Element {
+ let mut message = Message::new(Some(to));
+ message.bodies.insert(String::new(), Body(body.to_owned()));
+ message.into()
+}
@@ -0,0 +1,172 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Generator: Adobe Illustrator 13.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ id="Layer_1"
+ xml:space="preserve"
+ height="1070.54"
+ viewBox="0 0 1251.4018 1070.5223"
+ width="1251.4302"
+ version="1.1"
+ y="0px"
+ x="0px"
+ enable-background="new 0 0 176.486 181.437"
+ sodipodi:docname="logo.svg"
+ inkscape:version="0.92.3 (2405546, 2018-03-11)"
+ inkscape:export-filename="/tmp/tokio-xmpp.png"
+ inkscape:export-xdpi="63.689999"
+ inkscape:export-ydpi="63.689999"><metadata
+ id="metadata41"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+ id="defs39"><radialGradient
+ inkscape:collect="always"
+ xlink:href="#SVGID_1_"
+ id="radialGradient4654"
+ cx="166.88985"
+ cy="40.555923"
+ fx="166.88985"
+ fy="40.555923"
+ r="498.49179"
+ gradientTransform="matrix(1.2093833,0,0,1.0737613,423.8704,491.7138)"
+ gradientUnits="userSpaceOnUse" /><linearGradient
+ inkscape:collect="always"
+ xlink:href="#SVGID_1_"
+ id="linearGradient4656"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(1885.557,-704.53708)"
+ x1="-1807.2"
+ y1="125.86"
+ x2="-1807.2"
+ y2="0.00048828" /></defs><sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1446"
+ inkscape:window-height="1056"
+ id="namedview37"
+ showgrid="false"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0"
+ inkscape:zoom="0.32517636"
+ inkscape:cx="488.51154"
+ inkscape:cy="460.25958"
+ inkscape:window-x="0"
+ inkscape:window-y="20"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="Layer_1" />
+<linearGradient
+ id="SVGID_1_"
+ y2="0.00048828"
+ gradientUnits="userSpaceOnUse"
+ x2="-1807.2"
+ gradientTransform="translate(1885.557,-704.53708)"
+ y1="125.86"
+ x1="-1807.2">
+ <stop
+ stop-color="#1b3967"
+ offset=".011"
+ id="stop2" />
+ <stop
+ stop-color="#13b5ea"
+ offset=".467"
+ id="stop4" />
+ <stop
+ stop-color="#002b5c"
+ offset=".9945"
+ id="stop6" />
+</linearGradient>
+
+
+<linearGradient
+ id="SVGID_2_"
+ y2="1.279e-13"
+ gradientUnits="userSpaceOnUse"
+ x2="-1073.2"
+ gradientTransform="matrix(-1,0,0,1,-1038.643,-704.53708)"
+ y1="126.85"
+ x1="-1073.2">
+ <stop
+ stop-color="#1b3967"
+ offset=".011"
+ id="stop13" />
+ <stop
+ stop-color="#13b5ea"
+ offset=".467"
+ id="stop15" />
+ <stop
+ stop-color="#002b5c"
+ offset=".9945"
+ id="stop17" />
+</linearGradient>
+
+
+
+
+
+
+
+
+<path
+ style="fill:url(#radialGradient4654);fill-opacity:1;stroke-width:1.20936334"
@@ -0,0 +1,116 @@
+use std::str::FromStr;
+use std::collections::HashSet;
+use std::convert::TryFrom;
+use futures::{Future, Poll, Stream, future::{ok, err, IntoFuture}};
+use sasl::client::mechanisms::{Anonymous, Plain, Scram};
+use sasl::client::Mechanism;
+use sasl::common::scram::{Sha1, Sha256};
+use sasl::common::Credentials;
+use tokio_io::{AsyncRead, AsyncWrite};
+use xmpp_parsers::sasl::{Auth, Challenge, Failure, Mechanism as XMPPMechanism, Response, Success};
+
+use crate::xmpp_codec::Packet;
+use crate::xmpp_stream::XMPPStream;
+use crate::{AuthError, Error, ProtocolError};
+
+const NS_XMPP_SASL: &str = "urn:ietf:params:xml:ns:xmpp-sasl";
+
+pub struct ClientAuth<S: AsyncRead + AsyncWrite> {
+ future: Box<dyn Future<Item = XMPPStream<S>, Error = Error>>,
+}
+
+impl<S: AsyncRead + AsyncWrite + 'static> ClientAuth<S> {
+ pub fn new(stream: XMPPStream<S>, creds: Credentials) -> Result<Self, Error> {
+ let local_mechs: Vec<Box<dyn Fn() -> Box<dyn Mechanism>>> = vec![
+ Box::new(|| Box::new(Scram::<Sha256>::from_credentials(creds.clone()).unwrap())),
+ Box::new(|| Box::new(Scram::<Sha1>::from_credentials(creds.clone()).unwrap())),
+ Box::new(|| Box::new(Plain::from_credentials(creds.clone()).unwrap())),
+ Box::new(|| Box::new(Anonymous::new())),
+ ];
+
+ let remote_mechs: HashSet<String> = stream
+ .stream_features
+ .get_child("mechanisms", NS_XMPP_SASL)
+ .ok_or(AuthError::NoMechanism)?
+ .children()
+ .filter(|child| child.is("mechanism", NS_XMPP_SASL))
+ .map(|mech_el| mech_el.text())
+ .collect();
+
+ for local_mech in local_mechs {
+ let mut mechanism = local_mech();
+ if remote_mechs.contains(mechanism.name()) {
+ let initial = mechanism.initial().map_err(AuthError::Sasl)?;
+ let mechanism_name = XMPPMechanism::from_str(mechanism.name()).map_err(ProtocolError::Parsers)?;
+
+ let send_initial = Box::new(stream.send_stanza(Auth {
+ mechanism: mechanism_name,
+ data: initial,
+ }))
+ .map_err(Error::Io);
+ let future = Box::new(send_initial.and_then(
+ |stream| Self::handle_challenge(stream, mechanism)
+ ).and_then(
+ |stream| stream.restart()
+ ));
+ return Ok(ClientAuth {
+ future,
+ });
+ }
+ }
+
+ Err(AuthError::NoMechanism)?
+ }
+
+ fn handle_challenge(stream: XMPPStream<S>, mut mechanism: Box<dyn Mechanism>) -> Box<dyn Future<Item = XMPPStream<S>, Error = Error>> {
+ Box::new(
+ stream.into_future()
+ .map_err(|(e, _stream)| e.into())
+ .and_then(|(stanza, stream)| {
+ match stanza {
+ Some(Packet::Stanza(stanza)) => {
+ if let Ok(challenge) = Challenge::try_from(stanza.clone()) {
+ let response = mechanism
+ .response(&challenge.data);
+ Box::new(
+ response
+ .map_err(|e| AuthError::Sasl(e).into())
+ .into_future()
+ .and_then(|response| {
+ // Send response and loop
+ stream.send_stanza(Response { data: response })
+ .map_err(Error::Io)
+ .and_then(|stream| Self::handle_challenge(stream, mechanism))
+ })
+ )
+ } else if let Ok(_) = Success::try_from(stanza.clone()) {
+ Box::new(ok(stream))
+ } else if let Ok(failure) = Failure::try_from(stanza.clone()) {
+ Box::new(err(Error::Auth(AuthError::Fail(failure.defined_condition))))
+ } else if stanza.name() == "failure" {
+ // Workaround for https://gitlab.com/xmpp-rs/xmpp-parsers/merge_requests/1
+ Box::new(err(Error::Auth(AuthError::Sasl("failure".to_string()))))
+ } else {
+ // ignore and loop
+ Self::handle_challenge(stream, mechanism)
+ }
+ }
+ Some(_) => {
+ // ignore and loop
+ Self::handle_challenge(stream, mechanism)
+ }
+ None => Box::new(err(Error::Disconnected))
+ }
+ })
+ )
+ }
+}
+
+impl<S: AsyncRead + AsyncWrite> Future for ClientAuth<S> {
+ type Item = XMPPStream<S>;
+ type Error = Error;
+
+ fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
+ self.future.poll()
+ }
+}
@@ -0,0 +1,102 @@
+use futures::{sink, Async, Future, Poll, Stream};
+use std::convert::TryFrom;
+use std::mem::replace;
+use tokio_io::{AsyncRead, AsyncWrite};
+use xmpp_parsers::Jid;
+use xmpp_parsers::bind::{BindQuery, BindResponse};
+use xmpp_parsers::iq::{Iq, IqType};
+
+use crate::xmpp_codec::Packet;
+use crate::xmpp_stream::XMPPStream;
+use crate::{Error, ProtocolError};
+
+const NS_XMPP_BIND: &str = "urn:ietf:params:xml:ns:xmpp-bind";
+const BIND_REQ_ID: &str = "resource-bind";
+
+pub enum ClientBind<S: AsyncWrite> {
+ Unsupported(XMPPStream<S>),
+ WaitSend(sink::Send<XMPPStream<S>>),
+ WaitRecv(XMPPStream<S>),
+ Invalid,
+}
+
+impl<S: AsyncWrite> ClientBind<S> {
+ /// Consumes and returns the stream to express that you cannot use
+ /// the stream for anything else until the resource binding
+ /// req/resp are done.
+ pub fn new(stream: XMPPStream<S>) -> Self {
+ match stream.stream_features.get_child("bind", NS_XMPP_BIND) {
+ None =>
+ // No resource binding available,
+ // return the (probably // usable) stream immediately
+ {
+ ClientBind::Unsupported(stream)
+ }
+ Some(_) => {
+ let resource;
+ if let Jid::Full(jid) = stream.jid.clone() {
+ resource = Some(jid.resource);
+ } else {
+ resource = None;
+ }
+ let iq = Iq::from_set(BIND_REQ_ID, BindQuery::new(resource));
+ let send = stream.send_stanza(iq);
+ ClientBind::WaitSend(send)
+ }
+ }
+ }
+}
+
+impl<S: AsyncRead + AsyncWrite> Future for ClientBind<S> {
+ type Item = XMPPStream<S>;
+ type Error = Error;
+
+ fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
+ let state = replace(self, ClientBind::Invalid);
+
+ match state {
+ ClientBind::Unsupported(stream) => Ok(Async::Ready(stream)),
+ ClientBind::WaitSend(mut send) => match send.poll() {
+ Ok(Async::Ready(stream)) => {
+ replace(self, ClientBind::WaitRecv(stream));
+ self.poll()
+ }
+ Ok(Async::NotReady) => {
+ replace(self, ClientBind::WaitSend(send));
+ Ok(Async::NotReady)
+ }
+ Err(e) => Err(e)?,
+ },
+ ClientBind::WaitRecv(mut stream) => match stream.poll() {
+ Ok(Async::Ready(Some(Packet::Stanza(stanza)))) => match Iq::try_from(stanza) {
+ Ok(iq) => {
+ if iq.id == BIND_REQ_ID {
+ match iq.payload {
+ IqType::Result(payload) => {
+ payload
+ .and_then(|payload| BindResponse::try_from(payload).ok())
+ .map(|bind| stream.jid = bind.into());
+ Ok(Async::Ready(stream))
+ }
+ _ => Err(ProtocolError::InvalidBindResponse)?,
+ }
+ } else {
+ Ok(Async::NotReady)
+ }
+ }
+ _ => Ok(Async::NotReady),
+ },
+ Ok(Async::Ready(_)) => {
+ replace(self, ClientBind::WaitRecv(stream));
+ self.poll()
+ }
+ Ok(Async::NotReady) => {
+ replace(self, ClientBind::WaitRecv(stream));
+ Ok(Async::NotReady)
+ }
+ Err(e) => Err(e)?,
+ },
+ ClientBind::Invalid => unreachable!(),
+ }
+ }
+}
@@ -0,0 +1,236 @@
+use futures::{done, Async, AsyncSink, Future, Poll, Sink, StartSend, Stream};
+use idna;
+use xmpp_parsers::{Jid, JidParseError};
+use sasl::common::{ChannelBinding, Credentials};
+use std::mem::replace;
+use std::str::FromStr;
+use tokio::net::TcpStream;
+use tokio_io::{AsyncRead, AsyncWrite};
+use tokio_tls::TlsStream;
+
+use super::event::Event;
+use super::happy_eyeballs::Connecter;
+use super::starttls::{StartTlsClient, NS_XMPP_TLS};
+use super::xmpp_codec::Packet;
+use super::xmpp_stream;
+use super::{Error, ProtocolError};
+
+mod auth;
+use self::auth::ClientAuth;
+mod bind;
+use self::bind::ClientBind;
+
+/// XMPP client connection and state
+pub struct Client {
+ state: ClientState,
+}
+
+type XMPPStream = xmpp_stream::XMPPStream<TlsStream<TcpStream>>;
+const NS_JABBER_CLIENT: &str = "jabber:client";
+
+enum ClientState {
+ Invalid,
+ Disconnected,
+ Connecting(Box<dyn Future<Item = XMPPStream, Error = Error>>),
+ Connected(XMPPStream),
+}
+
+impl Client {
+ /// Start a new XMPP client
+ ///
+ /// Start polling the returned instance so that it will connect
+ /// and yield events.
+ pub fn new(jid: &str, password: &str) -> Result<Self, JidParseError> {
+ let jid = Jid::from_str(jid)?;
+ let client = Self::new_with_jid(jid, password);
+ Ok(client)
+ }
+
+ /// Start a new client given that the JID is already parsed.
+ pub fn new_with_jid(jid: Jid, password: &str) -> Self {
+ let password = password.to_owned();
+ let connect = Self::make_connect(jid, password.clone());
+ let client = Client {
+ state: ClientState::Connecting(Box::new(connect)),
+ };
+ client
+ }
+
+ fn make_connect(jid: Jid, password: String) -> impl Future<Item = XMPPStream, Error = Error> {
+ let username = jid.clone().node().unwrap();
+ let jid1 = jid.clone();
+ let jid2 = jid.clone();
+ let password = password;
+ done(idna::domain_to_ascii(&jid.domain()))
+ .map_err(|_| Error::Idna)
+ .and_then(|domain| {
+ done(Connecter::from_lookup(
+ &domain,
+ Some("_xmpp-client._tcp"),
+ 5222,
+ ))
+ })
+ .flatten()
+ .and_then(move |tcp_stream| {
+ xmpp_stream::XMPPStream::start(tcp_stream, jid1, NS_JABBER_CLIENT.to_owned())
+ })
+ .and_then(|xmpp_stream| {
+ if Self::can_starttls(&xmpp_stream) {
+ Ok(Self::starttls(xmpp_stream))
+ } else {
+ Err(Error::Protocol(ProtocolError::NoTls))
+ }
+ })
+ .flatten()
+ .and_then(|tls_stream| XMPPStream::start(tls_stream, jid2, NS_JABBER_CLIENT.to_owned()))
+ .and_then(
+ move |xmpp_stream| done(Self::auth(xmpp_stream, username, password)), // TODO: flatten?
+ )
+ .and_then(|auth| auth)
+ .and_then(|xmpp_stream| Self::bind(xmpp_stream))
+ .and_then(|xmpp_stream| {
+ // println!("Bound to {}", xmpp_stream.jid);
+ Ok(xmpp_stream)
+ })
+ }
+
+ fn can_starttls<S>(stream: &xmpp_stream::XMPPStream<S>) -> bool {
+ stream
+ .stream_features
+ .get_child("starttls", NS_XMPP_TLS)
+ .is_some()
+ }
+
+ fn starttls<S: AsyncRead + AsyncWrite>(
+ stream: xmpp_stream::XMPPStream<S>,
+ ) -> StartTlsClient<S> {
+ StartTlsClient::from_stream(stream)
+ }
+
+ fn auth<S: AsyncRead + AsyncWrite + 'static>(
+ stream: xmpp_stream::XMPPStream<S>,
+ username: String,
+ password: String,
+ ) -> Result<ClientAuth<S>, Error> {
+ let creds = Credentials::default()
+ .with_username(username)
+ .with_password(password)
+ .with_channel_binding(ChannelBinding::None);
+ ClientAuth::new(stream, creds)
+ }
+
+ fn bind<S: AsyncWrite>(stream: xmpp_stream::XMPPStream<S>) -> ClientBind<S> {
+ ClientBind::new(stream)
+ }
+
+ /// Get the client's bound JID (the one reported by the XMPP
+ /// server).
+ pub fn bound_jid(&self) -> Option<&Jid> {
+ match self.state {
+ ClientState::Connected(ref stream) => Some(&stream.jid),
+ _ => None,
+ }
+ }
+}
+
+impl Stream for Client {
+ type Item = Event;
+ type Error = Error;
+
+ fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> {
+ let state = replace(&mut self.state, ClientState::Invalid);
+
+ match state {
+ ClientState::Invalid => Err(Error::InvalidState),
+ ClientState::Disconnected => Ok(Async::Ready(None)),
+ ClientState::Connecting(mut connect) => match connect.poll() {
+ Ok(Async::Ready(stream)) => {
+ let jid = stream.jid.clone();
+ self.state = ClientState::Connected(stream);
+ Ok(Async::Ready(Some(Event::Online(jid))))
+ }
+ Ok(Async::NotReady) => {
+ self.state = ClientState::Connecting(connect);
+ Ok(Async::NotReady)
+ }
+ Err(e) => Err(e),
+ },
+ ClientState::Connected(mut stream) => {
+ // Poll sink
+ match stream.poll_complete() {
+ Ok(Async::NotReady) => (),
+ Ok(Async::Ready(())) => (),
+ Err(e) => return Err(e)?,
+ };
+
+ // Poll stream
+ match stream.poll() {
+ Ok(Async::Ready(None)) => {
+ // EOF
+ self.state = ClientState::Disconnected;
+ Ok(Async::Ready(Some(Event::Disconnected)))
+ }
+ Ok(Async::Ready(Some(Packet::Stanza(stanza)))) => {
+ // Receive stanza
+ self.state = ClientState::Connected(stream);
+ Ok(Async::Ready(Some(Event::Stanza(stanza))))
+ }
+ Ok(Async::Ready(Some(Packet::Text(_)))) => {
+ // Ignore text between stanzas
+ Ok(Async::NotReady)
+ }
+ Ok(Async::Ready(Some(Packet::StreamStart(_)))) => {
+ // <stream:stream>
+ Err(ProtocolError::InvalidStreamStart.into())
+ }
+ Ok(Async::Ready(Some(Packet::StreamEnd))) => {
+ // End of stream: </stream:stream>
+ Ok(Async::Ready(None))
+ }
+ Ok(Async::NotReady) => {
+ // Try again later
+ self.state = ClientState::Connected(stream);
+ Ok(Async::NotReady)
+ }
+ Err(e) => Err(e)?,
+ }
+ }
+ }
+ }
+}
+
+impl Sink for Client {
+ type SinkItem = Packet;
+ type SinkError = Error;
+
+ fn start_send(&mut self, item: Self::SinkItem) -> StartSend<Self::SinkItem, Self::SinkError> {
+ match self.state {
+ ClientState::Connected(ref mut stream) =>
+ Ok(stream.start_send(item)?),
+ _ =>
+ Ok(AsyncSink::NotReady(item)),
+ }
+ }
+
+ fn poll_complete(&mut self) -> Poll<(), Self::SinkError> {
+ match self.state {
+ ClientState::Connected(ref mut stream) => stream.poll_complete().map_err(|e| e.into()),
+ _ => Ok(Async::Ready(())),
+ }
+ }
+
+ /// This closes the inner TCP stream.
+ ///
+ /// To synchronize your shutdown with the server side, you should
+ /// first send `Packet::StreamEnd` and wait for the end of the
+ /// incoming stream before closing the connection.
+ fn close(&mut self) -> Poll<(), Self::SinkError> {
+ match self.state {
+ ClientState::Connected(ref mut stream) =>
+ stream.close()
+ .map_err(|e| e.into()),
+ _ =>
+ Ok(Async::Ready(())),
+ }
+ }
+}
@@ -0,0 +1,89 @@
+use futures::{sink, Async, Future, Poll, Stream};
+use std::mem::replace;
+use tokio_io::{AsyncRead, AsyncWrite};
+use xmpp_parsers::component::Handshake;
+
+use crate::xmpp_codec::Packet;
+use crate::xmpp_stream::XMPPStream;
+use crate::{AuthError, Error};
+
+const NS_JABBER_COMPONENT_ACCEPT: &str = "jabber:component:accept";
+
+pub struct ComponentAuth<S: AsyncWrite> {
+ state: ComponentAuthState<S>,
+}
+
+enum ComponentAuthState<S: AsyncWrite> {
+ WaitSend(sink::Send<XMPPStream<S>>),
+ WaitRecv(XMPPStream<S>),
+ Invalid,
+}
+
+impl<S: AsyncWrite> ComponentAuth<S> {
+ // TODO: doesn't have to be a Result<> actually
+ pub fn new(stream: XMPPStream<S>, password: String) -> Result<Self, Error> {
+ // FIXME: huge hack, shouldnβt be an element!
+ let sid = stream.stream_features.name().to_owned();
+ let mut this = ComponentAuth {
+ state: ComponentAuthState::Invalid,
+ };
+ this.send(
+ stream,
+ Handshake::from_password_and_stream_id(&password, &sid),
+ );
+ Ok(this)
+ }
+
+ fn send(&mut self, stream: XMPPStream<S>, handshake: Handshake) {
+ let nonza = handshake;
+ let send = stream.send_stanza(nonza);
+
+ self.state = ComponentAuthState::WaitSend(send);
+ }
+}
+
+impl<S: AsyncRead + AsyncWrite> Future for ComponentAuth<S> {
+ type Item = XMPPStream<S>;
+ type Error = Error;
+
+ fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
+ let state = replace(&mut self.state, ComponentAuthState::Invalid);
+
+ match state {
+ ComponentAuthState::WaitSend(mut send) => match send.poll() {
+ Ok(Async::Ready(stream)) => {
+ self.state = ComponentAuthState::WaitRecv(stream);
+ self.poll()
+ }
+ Ok(Async::NotReady) => {
+ self.state = ComponentAuthState::WaitSend(send);
+ Ok(Async::NotReady)
+ }
+ Err(e) => Err(e)?,
+ },
+ ComponentAuthState::WaitRecv(mut stream) => match stream.poll() {
+ Ok(Async::Ready(Some(Packet::Stanza(ref stanza))))
+ if stanza.is("handshake", NS_JABBER_COMPONENT_ACCEPT) =>
+ {
+ self.state = ComponentAuthState::Invalid;
+ Ok(Async::Ready(stream))
+ }
+ Ok(Async::Ready(Some(Packet::Stanza(ref stanza))))
+ if stanza.is("error", "http://etherx.jabber.org/streams") =>
+ {
+ Err(AuthError::ComponentFail.into())
+ }
+ Ok(Async::Ready(_event)) => {
+ // println!("ComponentAuth ignore {:?}", _event);
+ Ok(Async::NotReady)
+ }
+ Ok(_) => {
+ self.state = ComponentAuthState::WaitRecv(stream);
+ Ok(Async::NotReady)
+ }
+ Err(e) => Err(e)?,
+ },
+ ComponentAuthState::Invalid => unreachable!(),
+ }
+ }
+}
@@ -0,0 +1,163 @@
+//! Components in XMPP are services/gateways that are logged into an
+//! XMPP server under a JID consisting of just a domain name. They are
+//! allowed to use any user and resource identifiers in their stanzas.
+use futures::{done, Async, AsyncSink, Future, Poll, Sink, StartSend, Stream};
+use xmpp_parsers::{Jid, JidParseError, Element};
+use std::mem::replace;
+use std::str::FromStr;
+use tokio::net::TcpStream;
+use tokio_io::{AsyncRead, AsyncWrite};
+
+use super::event::Event;
+use super::happy_eyeballs::Connecter;
+use super::xmpp_codec::Packet;
+use super::xmpp_stream;
+use super::Error;
+
+mod auth;
+use self::auth::ComponentAuth;
+
+/// Component connection to an XMPP server
+pub struct Component {
+ /// The component's Jabber-Id
+ pub jid: Jid,
+ state: ComponentState,
+}
+
+type XMPPStream = xmpp_stream::XMPPStream<TcpStream>;
+const NS_JABBER_COMPONENT_ACCEPT: &str = "jabber:component:accept";
+
+enum ComponentState {
+ Invalid,
+ Disconnected,
+ Connecting(Box<dyn Future<Item = XMPPStream, Error = Error>>),
+ Connected(XMPPStream),
+}
+
+impl Component {
+ /// Start a new XMPP component
+ ///
+ /// Start polling the returned instance so that it will connect
+ /// and yield events.
+ pub fn new(jid: &str, password: &str, server: &str, port: u16) -> Result<Self, JidParseError> {
+ let jid = Jid::from_str(jid)?;
+ let password = password.to_owned();
+ let connect = Self::make_connect(jid.clone(), password, server, port);
+ Ok(Component {
+ jid,
+ state: ComponentState::Connecting(Box::new(connect)),
+ })
+ }
+
+ fn make_connect(
+ jid: Jid,
+ password: String,
+ server: &str,
+ port: u16,
+ ) -> impl Future<Item = XMPPStream, Error = Error> {
+ let jid1 = jid.clone();
+ let password = password;
+ done(Connecter::from_lookup(server, None, port))
+ .flatten()
+ .and_then(move |tcp_stream| {
+ xmpp_stream::XMPPStream::start(
+ tcp_stream,
+ jid1,
+ NS_JABBER_COMPONENT_ACCEPT.to_owned(),
+ )
+ })
+ .and_then(move |xmpp_stream| Self::auth(xmpp_stream, password).expect("auth"))
+ }
+
+ fn auth<S: AsyncRead + AsyncWrite>(
+ stream: xmpp_stream::XMPPStream<S>,
+ password: String,
+ ) -> Result<ComponentAuth<S>, Error> {
+ ComponentAuth::new(stream, password)
+ }
+}
+
+impl Stream for Component {
+ type Item = Event;
+ type Error = Error;
+
+ fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> {
+ let state = replace(&mut self.state, ComponentState::Invalid);
+
+ match state {
+ ComponentState::Invalid => Err(Error::InvalidState),
+ ComponentState::Disconnected => Ok(Async::Ready(None)),
+ ComponentState::Connecting(mut connect) => match connect.poll() {
+ Ok(Async::Ready(stream)) => {
+ self.state = ComponentState::Connected(stream);
+ Ok(Async::Ready(Some(Event::Online(self.jid.clone()))))
+ }
+ Ok(Async::NotReady) => {
+ self.state = ComponentState::Connecting(connect);
+ Ok(Async::NotReady)
+ }
+ Err(e) => Err(e),
+ },
+ ComponentState::Connected(mut stream) => {
+ // Poll sink
+ match stream.poll_complete() {
+ Ok(Async::NotReady) => (),
+ Ok(Async::Ready(())) => (),
+ Err(e) => return Err(e)?,
+ };
+
+ // Poll stream
+ match stream.poll() {
+ Ok(Async::NotReady) => {
+ self.state = ComponentState::Connected(stream);
+ Ok(Async::NotReady)
+ }
+ Ok(Async::Ready(None)) => {
+ // EOF
+ self.state = ComponentState::Disconnected;
+ Ok(Async::Ready(Some(Event::Disconnected)))
+ }
+ Ok(Async::Ready(Some(Packet::Stanza(stanza)))) => {
+ self.state = ComponentState::Connected(stream);
+ Ok(Async::Ready(Some(Event::Stanza(stanza))))
+ }
+ Ok(Async::Ready(_)) => {
+ self.state = ComponentState::Connected(stream);
+ Ok(Async::NotReady)
+ }
+ Err(e) => Err(e)?,
+ }
+ }
+ }
+ }
+}
+
+impl Sink for Component {
+ type SinkItem = Element;
+ type SinkError = Error;
+
+ fn start_send(&mut self, item: Self::SinkItem) -> StartSend<Self::SinkItem, Self::SinkError> {
+ match self.state {
+ ComponentState::Connected(ref mut stream) => match stream
+ .start_send(Packet::Stanza(item))
+ {
+ Ok(AsyncSink::NotReady(Packet::Stanza(stanza))) => Ok(AsyncSink::NotReady(stanza)),
+ Ok(AsyncSink::NotReady(_)) => {
+ panic!("Component.start_send with stanza but got something else back")
+ }
+ Ok(AsyncSink::Ready) => Ok(AsyncSink::Ready),
+ Err(e) => Err(e)?,
+ },
+ _ => Ok(AsyncSink::NotReady(item)),
+ }
+ }
+
+ fn poll_complete(&mut self) -> Poll<(), Self::SinkError> {
+ match &mut self.state {
+ &mut ComponentState::Connected(ref mut stream) => {
+ stream.poll_complete().map_err(|e| e.into())
+ }
+ _ => Ok(Async::Ready(())),
+ }
+ }
+}
@@ -0,0 +1,224 @@
+use native_tls::Error as TlsError;
+use std::borrow::Cow;
+use std::error::Error as StdError;
+use std::fmt;
+use std::io::Error as IoError;
+use std::str::Utf8Error;
+use trust_dns_proto::error::ProtoError;
+use trust_dns_resolver::error::ResolveError;
+
+use xmpp_parsers::Error as ParsersError;
+use xmpp_parsers::sasl::DefinedCondition as SaslDefinedCondition;
+
+/// Top-level error type
+#[derive(Debug)]
+pub enum Error {
+ /// I/O error
+ Io(IoError),
+ /// Error resolving DNS and establishing a connection
+ Connection(ConnecterError),
+ /// DNS label conversion error, no details available from module
+ /// `idna`
+ Idna,
+ /// Protocol-level error
+ Protocol(ProtocolError),
+ /// Authentication error
+ Auth(AuthError),
+ /// TLS error
+ Tls(TlsError),
+ /// Connection closed
+ Disconnected,
+ /// Shoud never happen
+ InvalidState,
+}
+
+impl fmt::Display for Error {
+ fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ Error::Io(e) => write!(fmt, "IO error: {}", e),
+ Error::Connection(e) => write!(fmt, "connection error: {}", e),
+ Error::Idna => write!(fmt, "IDNA error"),
+ Error::Protocol(e) => write!(fmt, "protocol error: {}", e),
+ Error::Auth(e) => write!(fmt, "authentication error: {}", e),
+ Error::Tls(e) => write!(fmt, "TLS error: {}", e),
+ Error::Disconnected => write!(fmt, "disconnected"),
+ Error::InvalidState => write!(fmt, "invalid state"),
+ }
+ }
+}
+
+impl From<IoError> for Error {
+ fn from(e: IoError) -> Self {
+ Error::Io(e)
+ }
+}
+
+impl From<ConnecterError> for Error {
+ fn from(e: ConnecterError) -> Self {
+ Error::Connection(e)
+ }
+}
+
+impl From<ProtocolError> for Error {
+ fn from(e: ProtocolError) -> Self {
+ Error::Protocol(e)
+ }
+}
+
+impl From<AuthError> for Error {
+ fn from(e: AuthError) -> Self {
+ Error::Auth(e)
+ }
+}
+
+impl From<TlsError> for Error {
+ fn from(e: TlsError) -> Self {
+ Error::Tls(e)
+ }
+}
+
+/// Causes for stream parsing errors
+#[derive(Debug)]
+pub enum ParserError {
+ /// Encoding error
+ Utf8(Utf8Error),
+ /// XML parse error
+ Parse(ParseError),
+ /// Illegal `</>`
+ ShortTag,
+ /// Required by `impl Decoder`
+ Io(IoError),
+}
+
+impl fmt::Display for ParserError {
+ fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ ParserError::Utf8(e) => write!(fmt, "UTF-8 error: {}", e),
+ ParserError::Parse(e) => write!(fmt, "parse error: {}", e),
+ ParserError::ShortTag => write!(fmt, "short tag"),
+ ParserError::Io(e) => write!(fmt, "IO error: {}", e),
+ }
+ }
+}
+
+impl From<IoError> for ParserError {
+ fn from(e: IoError) -> Self {
+ ParserError::Io(e)
+ }
+}
+
+impl From<ParserError> for Error {
+ fn from(e: ParserError) -> Self {
+ ProtocolError::Parser(e).into()
+ }
+}
+
+/// XML parse error wrapper type
+#[derive(Debug)]
+pub struct ParseError(pub Cow<'static, str>);
+
+impl StdError for ParseError {
+ fn description(&self) -> &str {
+ self.0.as_ref()
+ }
+ fn cause(&self) -> Option<&dyn StdError> {
+ None
+ }
+}
+
+impl fmt::Display for ParseError {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "{}", self.0)
+ }
+}
+
+/// XMPP protocol-level error
+#[derive(Debug)]
+pub enum ProtocolError {
+ /// XML parser error
+ Parser(ParserError),
+ /// Error with expected stanza schema
+ Parsers(ParsersError),
+ /// No TLS available
+ NoTls,
+ /// Invalid response to resource binding
+ InvalidBindResponse,
+ /// No xmlns attribute in <stream:stream>
+ NoStreamNamespace,
+ /// No id attribute in <stream:stream>
+ NoStreamId,
+ /// Encountered an unexpected XML token
+ InvalidToken,
+ /// Unexpected <stream:stream> (shouldn't occur)
+ InvalidStreamStart,
+}
+
+impl fmt::Display for ProtocolError {
+ fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ ProtocolError::Parser(e) => write!(fmt, "XML parser error: {}", e),
+ ProtocolError::Parsers(e) => write!(fmt, "error with expected stanza schema: {}", e),
+ ProtocolError::NoTls => write!(fmt, "no TLS available"),
+ ProtocolError::InvalidBindResponse => write!(fmt, "invalid response to resource binding"),
+ ProtocolError::NoStreamNamespace => write!(fmt, "no xmlns attribute in <stream:stream>"),
+ ProtocolError::NoStreamId => write!(fmt, "no id attribute in <stream:stream>"),
+ ProtocolError::InvalidToken => write!(fmt, "encountered an unexpected XML token"),
+ ProtocolError::InvalidStreamStart => write!(fmt, "unexpected <stream:stream>"),
+ }
+ }
+}
+
+impl From<ParserError> for ProtocolError {
+ fn from(e: ParserError) -> Self {
+ ProtocolError::Parser(e)
+ }
+}
+
+impl From<ParsersError> for ProtocolError {
+ fn from(e: ParsersError) -> Self {
+ ProtocolError::Parsers(e)
+ }
+}
+
+/// Authentication error
+#[derive(Debug)]
+pub enum AuthError {
+ /// No matching SASL mechanism available
+ NoMechanism,
+ /// Local SASL implementation error
+ Sasl(String),
+ /// Failure from server
+ Fail(SaslDefinedCondition),
+ /// Component authentication failure
+ ComponentFail,
+}
+
+impl fmt::Display for AuthError {
+ fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ AuthError::NoMechanism => write!(fmt, "no matching SASL mechanism available"),
+ AuthError::Sasl(s) => write!(fmt, "local SASL implementation error: {}", s),
+ AuthError::Fail(c) => write!(fmt, "failure from the server: {:?}", c),
+ AuthError::ComponentFail => write!(fmt, "component authentication failure"),
+ }
+ }
+}
+
+/// Error establishing connection
+#[derive(Debug)]
+pub enum ConnecterError {
+ /// All attempts failed, no error available
+ AllFailed,
+ /// DNS protocol error
+ Dns(ProtoError),
+ /// DNS resolution error
+ Resolve(ResolveError),
+}
+
+impl std::error::Error for ConnecterError {}
+
+impl std::fmt::Display for ConnecterError {
+ fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
+ write!(fmt, "{:?}", self)
+ }
+}
@@ -0,0 +1,54 @@
+use xmpp_parsers::{Element, Jid};
+
+/// High-level event on the Stream implemented by Client and Component
+#[derive(Debug)]
+pub enum Event {
+ /// Stream is connected and initialized
+ Online(Jid),
+ /// Stream end
+ Disconnected,
+ /// Received stanza/nonza
+ Stanza(Element),
+}
+
+impl Event {
+ /// `Online` event?
+ pub fn is_online(&self) -> bool {
+ match *self {
+ Event::Online(_) => true,
+ _ => false,
+ }
+ }
+
+ /// Get the server-assigned JID for the `Online` event
+ pub fn get_jid(&self) -> Option<&Jid> {
+ match *self {
+ Event::Online(ref jid) => Some(jid),
+ _ => None,
+ }
+ }
+
+ /// `Stanza` event?
+ pub fn is_stanza(&self, name: &str) -> bool {
+ match *self {
+ Event::Stanza(ref stanza) => stanza.name() == name,
+ _ => false,
+ }
+ }
+
+ /// If this is a `Stanza` event, get its data
+ pub fn as_stanza(&self) -> Option<&Element> {
+ match *self {
+ Event::Stanza(ref stanza) => Some(stanza),
+ _ => None,
+ }
+ }
+
+ /// If this is a `Stanza` event, unwrap into its data
+ pub fn into_stanza(self) -> Option<Element> {
+ match self {
+ Event::Stanza(stanza) => Some(stanza),
+ _ => None,
+ }
+ }
+}
@@ -0,0 +1,196 @@
+use crate::{ConnecterError, Error};
+use futures::{Async, Future, Poll};
+use std::cell::RefCell;
+use std::collections::BTreeMap;
+use std::collections::VecDeque;
+use std::io::Error as IoError;
+use std::mem;
+use std::net::SocketAddr;
+use tokio::net::tcp::ConnectFuture;
+use tokio::net::TcpStream;
+use trust_dns_resolver::{AsyncResolver, Name, IntoName, Background, BackgroundLookup};
+use trust_dns_resolver::config::LookupIpStrategy;
+use trust_dns_resolver::lookup::SrvLookupFuture;
+use trust_dns_resolver::lookup_ip::LookupIpFuture;
+
+
+enum State {
+ ResolveSrv(AsyncResolver, BackgroundLookup<SrvLookupFuture>),
+ ResolveTarget(AsyncResolver, Background<LookupIpFuture>, u16),
+ Connecting(Option<AsyncResolver>, Vec<RefCell<ConnectFuture>>),
+ Invalid,
+}
+
+pub struct Connecter {
+ fallback_port: u16,
+ srv_domain: Option<Name>,
+ domain: Name,
+ state: State,
+ targets: VecDeque<(Name, u16)>,
+ error: Option<Error>,
+}
+
+fn resolver() -> Result<AsyncResolver, IoError> {
+ let (config, mut opts) = trust_dns_resolver::system_conf::read_system_conf()?;
+ opts.ip_strategy = LookupIpStrategy::Ipv4AndIpv6;
+ let (resolver, resolver_background) = AsyncResolver::new(config, opts);
+ tokio::runtime::current_thread::spawn(resolver_background);
+ Ok(resolver)
+}
+
+impl Connecter {
+ pub fn from_lookup(
+ domain: &str,
+ srv: Option<&str>,
+ fallback_port: u16,
+ ) -> Result<Connecter, Error> {
+ if let Ok(ip) = domain.parse() {
+ // use specified IP address, not domain name, skip the whole dns part
+ let connect = RefCell::new(TcpStream::connect(&SocketAddr::new(ip, fallback_port)));
+ return Ok(Connecter {
+ fallback_port,
+ srv_domain: None,
+ domain: "nohost".into_name().map_err(ConnecterError::Dns)?,
+ state: State::Connecting(None, vec![connect]),
+ targets: VecDeque::new(),
+ error: None,
+ });
+ }
+
+ let srv_domain = match srv {
+ Some(srv) => Some(
+ format!("{}.{}.", srv, domain)
+ .into_name()
+ .map_err(ConnecterError::Dns)?,
+ ),
+ None => None,
+ };
+
+ let mut self_ = Connecter {
+ fallback_port,
+ srv_domain,
+ domain: domain.into_name().map_err(ConnecterError::Dns)?,
+ state: State::Invalid,
+ targets: VecDeque::new(),
+ error: None,
+ };
+
+ let resolver = resolver()?;
+ // Initialize state
+ match &self_.srv_domain {
+ &Some(ref srv_domain) => {
+ let srv_lookup = resolver.lookup_srv(srv_domain.clone());
+ self_.state = State::ResolveSrv(resolver, srv_lookup);
+ }
+ None => {
+ self_.targets = [(self_.domain.clone(), self_.fallback_port)]
+ .into_iter()
+ .cloned()
+ .collect();
+ self_.state = State::Connecting(Some(resolver), vec![]);
+ }
+ }
+
+ Ok(self_)
+ }
+}
+
+impl Future for Connecter {
+ type Item = TcpStream;
+ type Error = Error;
+
+ fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
+ let state = mem::replace(&mut self.state, State::Invalid);
+ match state {
+ State::ResolveSrv(resolver, mut srv_lookup) => {
+ match srv_lookup.poll() {
+ Ok(Async::NotReady) => {
+ self.state = State::ResolveSrv(resolver, srv_lookup);
+ Ok(Async::NotReady)
+ }
+ Ok(Async::Ready(srv_result)) => {
+ let srv_map: BTreeMap<_, _> = srv_result
+ .iter()
+ .map(|srv| (srv.priority(), (srv.target().clone(), srv.port())))
+ .collect();
+ let targets = srv_map.into_iter().map(|(_, tp)| tp).collect();
+ self.targets = targets;
+ self.state = State::Connecting(Some(resolver), vec![]);
+ self.poll()
+ }
+ Err(_) => {
+ // ignore, fallback
+ self.targets = [(self.domain.clone(), self.fallback_port)]
+ .into_iter()
+ .cloned()
+ .collect();
+ self.state = State::Connecting(Some(resolver), vec![]);
+ self.poll()
+ }
+ }
+ }
+ State::Connecting(resolver, mut connects) => {
+ if resolver.is_some() && connects.len() == 0 && self.targets.len() > 0 {
+ let resolver = resolver.unwrap();
+ let (host, port) = self.targets.pop_front().unwrap();
+ let ip_lookup = resolver.lookup_ip(host);
+ self.state = State::ResolveTarget(resolver, ip_lookup, port);
+ self.poll()
+ } else if connects.len() > 0 {
+ let mut success = None;
+ connects.retain(|connect| match connect.borrow_mut().poll() {
+ Ok(Async::NotReady) => true,
+ Ok(Async::Ready(connection)) => {
+ success = Some(connection);
+ false
+ }
+ Err(e) => {
+ if self.error.is_none() {
+ self.error = Some(e.into());
+ }
+ false
+ }
+ });
+ match success {
+ Some(connection) => Ok(Async::Ready(connection)),
+ None => {
+ self.state = State::Connecting(resolver, connects);
+ Ok(Async::NotReady)
+ }
+ }
+ } else {
+ // All targets tried
+ match self.error.take() {
+ None => Err(ConnecterError::AllFailed.into()),
+ Some(e) => Err(e),
+ }
+ }
+ }
+ State::ResolveTarget(resolver, mut ip_lookup, port) => {
+ match ip_lookup.poll() {
+ Ok(Async::NotReady) => {
+ self.state = State::ResolveTarget(resolver, ip_lookup, port);
+ Ok(Async::NotReady)
+ }
+ Ok(Async::Ready(ip_result)) => {
+ let connects = ip_result
+ .iter()
+ .map(|ip| RefCell::new(TcpStream::connect(&SocketAddr::new(ip, port))))
+ .collect();
+ self.state = State::Connecting(Some(resolver), connects);
+ self.poll()
+ }
+ Err(e) => {
+ if self.error.is_none() {
+ self.error = Some(ConnecterError::Resolve(e).into());
+ }
+ // ignore, nextβ¦
+ self.state = State::Connecting(Some(resolver), vec![]);
+ self.poll()
+ }
+ }
+ }
+ _ => panic!(""),
+ }
+ }
+}
@@ -0,0 +1,19 @@
+#![deny(unsafe_code, unused, missing_docs, bare_trait_objects)]
+
+//! XMPP implementation with asynchronous I/O using Tokio.
+
+mod starttls;
+mod stream_start;
+pub mod xmpp_codec;
+pub use crate::xmpp_codec::Packet;
+pub mod xmpp_stream;
+pub use crate::starttls::StartTlsClient;
+mod event;
+mod happy_eyeballs;
+pub use crate::event::Event;
+mod client;
+pub use crate::client::Client;
+mod component;
+pub use crate::component::Component;
+mod error;
+pub use crate::error::{AuthError, ConnecterError, Error, ParseError, ParserError, ProtocolError};
@@ -0,0 +1,114 @@
+use futures::sink;
+use futures::stream::Stream;
+use futures::{Async, Future, Poll, Sink};
+use xmpp_parsers::{Jid, Element};
+use native_tls::TlsConnector as NativeTlsConnector;
+use std::mem::replace;
+use tokio_io::{AsyncRead, AsyncWrite};
+use tokio_tls::{Connect, TlsConnector, TlsStream};
+
+use crate::xmpp_codec::Packet;
+use crate::xmpp_stream::XMPPStream;
+use crate::Error;
+
+/// XMPP TLS XML namespace
+pub const NS_XMPP_TLS: &str = "urn:ietf:params:xml:ns:xmpp-tls";
+
+/// XMPP stream that switches to TLS if available in received features
+pub struct StartTlsClient<S: AsyncRead + AsyncWrite> {
+ state: StartTlsClientState<S>,
+ jid: Jid,
+}
+
+enum StartTlsClientState<S: AsyncRead + AsyncWrite> {
+ Invalid,
+ SendStartTls(sink::Send<XMPPStream<S>>),
+ AwaitProceed(XMPPStream<S>),
+ StartingTls(Connect<S>),
+}
+
+impl<S: AsyncRead + AsyncWrite> StartTlsClient<S> {
+ /// Waits for <stream:features>
+ pub fn from_stream(xmpp_stream: XMPPStream<S>) -> Self {
+ let jid = xmpp_stream.jid.clone();
+
+ let nonza = Element::builder("starttls").ns(NS_XMPP_TLS).build();
+ let packet = Packet::Stanza(nonza);
+ let send = xmpp_stream.send(packet);
+
+ StartTlsClient {
+ state: StartTlsClientState::SendStartTls(send),
+ jid,
+ }
+ }
+}
+
+impl<S: AsyncRead + AsyncWrite> Future for StartTlsClient<S> {
+ type Item = TlsStream<S>;
+ type Error = Error;
+
+ fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
+ let old_state = replace(&mut self.state, StartTlsClientState::Invalid);
+ let mut retry = false;
+
+ let (new_state, result) = match old_state {
+ StartTlsClientState::SendStartTls(mut send) => match send.poll() {
+ Ok(Async::Ready(xmpp_stream)) => {
+ let new_state = StartTlsClientState::AwaitProceed(xmpp_stream);
+ retry = true;
+ (new_state, Ok(Async::NotReady))
+ }
+ Ok(Async::NotReady) => {
+ (StartTlsClientState::SendStartTls(send), Ok(Async::NotReady))
+ }
+ Err(e) => (StartTlsClientState::SendStartTls(send), Err(e.into())),
+ },
+ StartTlsClientState::AwaitProceed(mut xmpp_stream) => match xmpp_stream.poll() {
+ Ok(Async::Ready(Some(Packet::Stanza(ref stanza))))
+ if stanza.name() == "proceed" =>
+ {
+ let stream = xmpp_stream.stream.into_inner();
+ let connect =
+ TlsConnector::from(NativeTlsConnector::builder().build().unwrap())
+ .connect(&self.jid.clone().domain(), stream);
+ let new_state = StartTlsClientState::StartingTls(connect);
+ retry = true;
+ (new_state, Ok(Async::NotReady))
+ }
+ Ok(Async::Ready(_value)) => {
+ // println!("StartTlsClient ignore {:?}", _value);
+ (
+ StartTlsClientState::AwaitProceed(xmpp_stream),
+ Ok(Async::NotReady),
+ )
+ }
+ Ok(_) => (
+ StartTlsClientState::AwaitProceed(xmpp_stream),
+ Ok(Async::NotReady),
+ ),
+ Err(e) => (
+ StartTlsClientState::AwaitProceed(xmpp_stream),
+ Err(Error::Protocol(e.into())),
+ ),
+ },
+ StartTlsClientState::StartingTls(mut connect) => match connect.poll() {
+ Ok(Async::Ready(tls_stream)) => {
+ (StartTlsClientState::Invalid, Ok(Async::Ready(tls_stream)))
+ }
+ Ok(Async::NotReady) => (
+ StartTlsClientState::StartingTls(connect),
+ Ok(Async::NotReady),
+ ),
+ Err(e) => (StartTlsClientState::Invalid, Err(e.into())),
+ },
+ StartTlsClientState::Invalid => unreachable!(),
+ };
+
+ self.state = new_state;
+ if retry {
+ self.poll()
+ } else {
+ result
+ }
+ }
+}
@@ -0,0 +1,125 @@
+use futures::{sink, Async, Future, Poll, Sink, Stream};
+use xmpp_parsers::{Jid, Element};
+use std::mem::replace;
+use tokio_codec::Framed;
+use tokio_io::{AsyncRead, AsyncWrite};
+
+use crate::xmpp_codec::{Packet, XMPPCodec};
+use crate::xmpp_stream::XMPPStream;
+use crate::{Error, ProtocolError};
+
+const NS_XMPP_STREAM: &str = "http://etherx.jabber.org/streams";
+
+pub struct StreamStart<S: AsyncWrite> {
+ state: StreamStartState<S>,
+ jid: Jid,
+ ns: String,
+}
+
+enum StreamStartState<S: AsyncWrite> {
+ SendStart(sink::Send<Framed<S, XMPPCodec>>),
+ RecvStart(Framed<S, XMPPCodec>),
+ RecvFeatures(Framed<S, XMPPCodec>, String),
+ Invalid,
+}
+
+impl<S: AsyncWrite> StreamStart<S> {
+ pub fn from_stream(stream: Framed<S, XMPPCodec>, jid: Jid, ns: String) -> Self {
+ let attrs = [
+ ("to".to_owned(), jid.clone().domain()),
+ ("version".to_owned(), "1.0".to_owned()),
+ ("xmlns".to_owned(), ns.clone()),
+ ("xmlns:stream".to_owned(), NS_XMPP_STREAM.to_owned()),
+ ]
+ .iter()
+ .cloned()
+ .collect();
+ let send = stream.send(Packet::StreamStart(attrs));
+
+ StreamStart {
+ state: StreamStartState::SendStart(send),
+ jid,
+ ns,
+ }
+ }
+}
+
+impl<S: AsyncRead + AsyncWrite> Future for StreamStart<S> {
+ type Item = XMPPStream<S>;
+ type Error = Error;
+
+ fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
+ let old_state = replace(&mut self.state, StreamStartState::Invalid);
+ let mut retry = false;
+
+ let (new_state, result) = match old_state {
+ StreamStartState::SendStart(mut send) => match send.poll() {
+ Ok(Async::Ready(stream)) => {
+ retry = true;
+ (StreamStartState::RecvStart(stream), Ok(Async::NotReady))
+ }
+ Ok(Async::NotReady) => (StreamStartState::SendStart(send), Ok(Async::NotReady)),
+ Err(e) => (StreamStartState::Invalid, Err(e.into())),
+ },
+ StreamStartState::RecvStart(mut stream) => match stream.poll() {
+ Ok(Async::Ready(Some(Packet::StreamStart(stream_attrs)))) => {
+ let stream_ns = stream_attrs
+ .get("xmlns")
+ .ok_or(ProtocolError::NoStreamNamespace)?
+ .clone();
+ if self.ns == "jabber:client" {
+ retry = true;
+ // TODO: skip RecvFeatures for version < 1.0
+ (
+ StreamStartState::RecvFeatures(stream, stream_ns),
+ Ok(Async::NotReady),
+ )
+ } else {
+ let id = stream_attrs
+ .get("id")
+ .ok_or(ProtocolError::NoStreamId)?
+ .clone();
+ // FIXME: huge hack, shouldnβt be an element!
+ let stream = XMPPStream::new(
+ self.jid.clone(),
+ stream,
+ self.ns.clone(),
+ Element::builder(id).build(),
+ );
+ (StreamStartState::Invalid, Ok(Async::Ready(stream)))
+ }
+ }
+ Ok(Async::Ready(_)) => return Err(ProtocolError::InvalidToken.into()),
+ Ok(Async::NotReady) => (StreamStartState::RecvStart(stream), Ok(Async::NotReady)),
+ Err(e) => return Err(ProtocolError::from(e).into()),
+ },
+ StreamStartState::RecvFeatures(mut stream, stream_ns) => match stream.poll() {
+ Ok(Async::Ready(Some(Packet::Stanza(stanza)))) => {
+ if stanza.is("features", NS_XMPP_STREAM) {
+ let stream =
+ XMPPStream::new(self.jid.clone(), stream, self.ns.clone(), stanza);
+ (StreamStartState::Invalid, Ok(Async::Ready(stream)))
+ } else {
+ (
+ StreamStartState::RecvFeatures(stream, stream_ns),
+ Ok(Async::NotReady),
+ )
+ }
+ }
+ Ok(Async::Ready(_)) | Ok(Async::NotReady) => (
+ StreamStartState::RecvFeatures(stream, stream_ns),
+ Ok(Async::NotReady),
+ ),
+ Err(e) => return Err(ProtocolError::from(e).into()),
+ },
+ StreamStartState::Invalid => unreachable!(),
+ };
+
+ self.state = new_state;
+ if retry {
+ self.poll()
+ } else {
+ result
+ }
+ }
+}
@@ -0,0 +1,532 @@
+//! XML stream parser for XMPP
+
+use crate::{ParseError, ParserError};
+use bytes::{BufMut, BytesMut};
+use xmpp_parsers::Element;
+use quick_xml::Writer as EventWriter;
+use std;
+use std::cell::RefCell;
+use std::collections::vec_deque::VecDeque;
+use std::collections::HashMap;
+use std::default::Default;
+use std::fmt::Write;
+use std::io;
+use std::iter::FromIterator;
+use std::rc::Rc;
+use std::str::from_utf8;
+use std::borrow::Cow;
+use tokio_codec::{Decoder, Encoder};
+use xml5ever::interface::Attribute;
+use xml5ever::tokenizer::{Tag, TagKind, Token, TokenSink, XmlTokenizer};
+use xml5ever::buffer_queue::BufferQueue;
+
+/// Anything that can be sent or received on an XMPP/XML stream
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum Packet {
+ /// `<stream:stream>` start tag
+ StreamStart(HashMap<String, String>),
+ /// A complete stanza or nonza
+ Stanza(Element),
+ /// Plain text (think whitespace keep-alive)
+ Text(String),
+ /// `</stream:stream>` closing tag
+ StreamEnd,
+}
+
+type QueueItem = Result<Packet, ParserError>;
+
+/// Parser state
+struct ParserSink {
+ // Ready stanzas, shared with XMPPCodec
+ queue: Rc<RefCell<VecDeque<QueueItem>>>,
+ // Parsing stack
+ stack: Vec<Element>,
+ ns_stack: Vec<HashMap<Option<String>, String>>,
+}
+
+impl ParserSink {
+ pub fn new(queue: Rc<RefCell<VecDeque<QueueItem>>>) -> Self {
+ ParserSink {
+ queue,
+ stack: vec![],
+ ns_stack: vec![],
+ }
+ }
+
+ fn push_queue(&self, pkt: Packet) {
+ self.queue.borrow_mut().push_back(Ok(pkt));
+ }
+
+ fn push_queue_error(&self, e: ParserError) {
+ self.queue.borrow_mut().push_back(Err(e));
+ }
+
+ /// Lookup XML namespace declaration for given prefix (or no prefix)
+ fn lookup_ns(&self, prefix: &Option<String>) -> Option<&str> {
+ for nss in self.ns_stack.iter().rev() {
+ if let Some(ns) = nss.get(prefix) {
+ return Some(ns);
+ }
+ }
+
+ None
+ }
+
+ fn handle_start_tag(&mut self, tag: Tag) {
+ let mut nss = HashMap::new();
+ let is_prefix_xmlns = |attr: &Attribute| {
+ attr.name
+ .prefix
+ .as_ref()
+ .map(|prefix| prefix.eq_str_ignore_ascii_case("xmlns"))
+ .unwrap_or(false)
+ };
+ for attr in &tag.attrs {
+ match attr.name.local.as_ref() {
+ "xmlns" => {
+ nss.insert(None, attr.value.as_ref().to_owned());
+ }
+ prefix if is_prefix_xmlns(attr) => {
+ nss.insert(Some(prefix.to_owned()), attr.value.as_ref().to_owned());
+ }
+ _ => (),
+ }
+ }
+ self.ns_stack.push(nss);
+
+ let el = {
+ let mut el_builder = Element::builder(tag.name.local.as_ref());
+ if let Some(el_ns) =
+ self.lookup_ns(&tag.name.prefix.map(|prefix| prefix.as_ref().to_owned()))
+ {
+ el_builder = el_builder.ns(el_ns);
+ }
+ for attr in &tag.attrs {
+ match attr.name.local.as_ref() {
+ "xmlns" => (),
+ _ if is_prefix_xmlns(attr) => (),
+ _ => {
+ let attr_name = if let Some(ref prefix) = attr.name.prefix {
+ Cow::Owned(format!("{}:{}", prefix, attr.name.local))
+ } else {
+ Cow::Borrowed(attr.name.local.as_ref())
+ };
+ el_builder = el_builder.attr(attr_name, attr.value.as_ref());
+ }
+ }
+ }
+ el_builder.build()
+ };
+
+ if self.stack.is_empty() {
+ let attrs = HashMap::from_iter(tag.attrs.iter().map(|attr| {
+ (
+ attr.name.local.as_ref().to_owned(),
+ attr.value.as_ref().to_owned(),
+ )
+ }));
+ self.push_queue(Packet::StreamStart(attrs));
+ }
+
+ self.stack.push(el);
+ }
+
+ fn handle_end_tag(&mut self) {
+ let el = self.stack.pop().unwrap();
+ self.ns_stack.pop();
+
+ match self.stack.len() {
+ // </stream:stream>
+ 0 => self.push_queue(Packet::StreamEnd),
+ // </stanza>
+ 1 => self.push_queue(Packet::Stanza(el)),
+ len => {
+ let parent = &mut self.stack[len - 1];
+ parent.append_child(el);
+ }
+ }
+ }
+}
+
+impl TokenSink for ParserSink {
+ fn process_token(&mut self, token: Token) {
+ match token {
+ Token::TagToken(tag) => match tag.kind {
+ TagKind::StartTag => self.handle_start_tag(tag),
+ TagKind::EndTag => self.handle_end_tag(),
+ TagKind::EmptyTag => {
+ self.handle_start_tag(tag);
+ self.handle_end_tag();
+ }
+ TagKind::ShortTag => self.push_queue_error(ParserError::ShortTag),
+ },
+ Token::CharacterTokens(tendril) => match self.stack.len() {
+ 0 | 1 => self.push_queue(Packet::Text(tendril.into())),
+ len => {
+ let el = &mut self.stack[len - 1];
+ el.append_text_node(tendril);
+ }
+ },
+ Token::EOFToken => self.push_queue(Packet::StreamEnd),
+ Token::ParseError(s) => {
+ // println!("ParseError: {:?}", s);
+ self.push_queue_error(ParserError::Parse(ParseError(s)));
+ }
+ _ => (),
+ }
+ }
+
+ // fn end(&mut self) {
+ // }
+}
+
+/// Stateful encoder/decoder for a bytestream from/to XMPP `Packet`
+pub struct XMPPCodec {
+ /// Outgoing
+ ns: Option<String>,
+ /// Incoming
+ parser: XmlTokenizer<ParserSink>,
+ /// For handling incoming truncated utf8
+ // TODO: optimize using tendrils?
+ buf: Vec<u8>,
+ /// Shared with ParserSink
+ queue: Rc<RefCell<VecDeque<QueueItem>>>,
+}
+
+impl XMPPCodec {
+ /// Constructor
+ pub fn new() -> Self {
+ let queue = Rc::new(RefCell::new(VecDeque::new()));
+ let sink = ParserSink::new(queue.clone());
+ // TODO: configure parser?
+ let parser = XmlTokenizer::new(sink, Default::default());
+ XMPPCodec {
+ ns: None,
+ parser,
+ queue,
+ buf: vec![],
+ }
+ }
+}
+
+impl Default for XMPPCodec {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl Decoder for XMPPCodec {
+ type Item = Packet;
+ type Error = ParserError;
+
+ fn decode(&mut self, buf: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
+ let buf1: Box<dyn AsRef<[u8]>> = if !self.buf.is_empty() && !buf.is_empty() {
+ let mut prefix = std::mem::replace(&mut self.buf, vec![]);
+ prefix.extend_from_slice(buf.take().as_ref());
+ Box::new(prefix)
+ } else {
+ Box::new(buf.take())
+ };
+ let buf1 = buf1.as_ref().as_ref();
+ match from_utf8(buf1) {
+ Ok(mut s) => {
+ s = s.trim();
+ if !s.is_empty() {
+ // println!("<< {}", s);
+ let mut buffer_queue = BufferQueue::new();
+ let tendril = FromIterator::from_iter(s.chars());
+ buffer_queue.push_back(tendril);
+ self.parser.feed(&mut buffer_queue);
+ }
+ }
+ // Remedies for truncated utf8
+ Err(e) if e.valid_up_to() >= buf1.len() - 3 => {
+ // Prepare all the valid data
+ let mut b = BytesMut::with_capacity(e.valid_up_to());
+ b.put(&buf1[0..e.valid_up_to()]);
+
+ // Retry
+ let result = self.decode(&mut b);
+
+ // Keep the tail back in
+ self.buf.extend_from_slice(&buf1[e.valid_up_to()..]);
+
+ return result;
+ }
+ Err(e) => {
+ // println!("error {} at {}/{} in {:?}", e, e.valid_up_to(), buf1.len(), buf1);
+ return Err(ParserError::Utf8(e));
+ }
+ }
+
+ match self.queue.borrow_mut().pop_front() {
+ None => Ok(None),
+ Some(result) => result.map(|pkt| Some(pkt)),
+ }
+ }
+
+ fn decode_eof(&mut self, buf: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
+ self.decode(buf)
+ }
+}
+
+impl Encoder for XMPPCodec {
+ type Item = Packet;
+ type Error = io::Error;
+
+ fn encode(&mut self, item: Self::Item, dst: &mut BytesMut) -> Result<(), Self::Error> {
+ let remaining = dst.capacity() - dst.len();
+ let max_stanza_size: usize = 2usize.pow(16);
+ if remaining < max_stanza_size {
+ dst.reserve(max_stanza_size - remaining);
+ }
+
+ fn to_io_err<E: Into<Box<dyn std::error::Error + Send + Sync>>>(e: E) -> io::Error {
+ io::Error::new(io::ErrorKind::InvalidInput, e)
+ }
+
+ match item {
+ Packet::StreamStart(start_attrs) => {
+ let mut buf = String::new();
+ write!(buf, "<stream:stream")
+ .map_err(to_io_err)?;
+ for (name, value) in start_attrs {
+ write!(buf, " {}=\"{}\"", escape(&name), escape(&value))
+ .map_err(to_io_err)?;
+ if name == "xmlns" {
+ self.ns = Some(value);
+ }
+ }
+ write!(buf, ">\n")
+ .map_err(to_io_err)?;
+
+ // print!(">> {}", buf);
+ write!(dst, "{}", buf)
+ .map_err(to_io_err)
+ }
+ Packet::Stanza(stanza) => {
+ stanza
+ .write_to_inner(&mut EventWriter::new(WriteBytes::new(dst)))
+ .and_then(|_| {
+ // println!(">> {:?}", dst);
+ Ok(())
+ })
+ .map_err(|e| to_io_err(format!("{}", e)))
+ }
+ Packet::Text(text) => {
+ write_text(&text, dst)
+ .and_then(|_| {
+ // println!(">> {:?}", dst);
+ Ok(())
+ })
+ .map_err(to_io_err)
+ }
+ Packet::StreamEnd => {
+ write!(dst, "</stream:stream>\n")
+ .map_err(to_io_err)
+ }
+ }
+ }
+}
+
+/// Write XML-escaped text string
+pub fn write_text<W: Write>(text: &str, writer: &mut W) -> Result<(), std::fmt::Error> {
+ write!(writer, "{}", escape(text))
+}
+
+/// Copied from `RustyXML` for now
+pub fn escape(input: &str) -> String {
+ let mut result = String::with_capacity(input.len());
+
+ for c in input.chars() {
+ match c {
+ '&' => result.push_str("&"),
+ '<' => result.push_str("<"),
+ '>' => result.push_str(">"),
+ '\'' => result.push_str("'"),
+ '"' => result.push_str("""),
+ o => result.push(o),
+ }
+ }
+ result
+}
+
+/// BytesMut impl only std::fmt::Write but not std::io::Write. The
+/// latter trait is required for minidom's
+/// `Element::write_to_inner()`.
+struct WriteBytes<'a> {
+ dst: &'a mut BytesMut,
+}
+
+impl<'a> WriteBytes<'a> {
+ fn new(dst: &'a mut BytesMut) -> Self {
+ WriteBytes { dst }
+ }
+}
+
+impl<'a> std::io::Write for WriteBytes<'a> {
+ fn write(&mut self, buf: &[u8]) -> std::result::Result<usize, std::io::Error> {
+ self.dst.put_slice(buf);
+ Ok(buf.len())
+ }
+
+ fn flush(&mut self) -> std::result::Result<(), std::io::Error> {
+ Ok(())
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use bytes::BytesMut;
+
+ #[test]
+ fn test_stream_start() {
+ let mut c = XMPPCodec::new();
+ let mut b = BytesMut::with_capacity(1024);
+ b.put(r"<?xml version='1.0'?><stream:stream xmlns:stream='http://etherx.jabber.org/streams' version='1.0' xmlns='jabber:client'>");
+ let r = c.decode(&mut b);
+ assert!(match r {
+ Ok(Some(Packet::StreamStart(_))) => true,
+ _ => false,
+ });
+ }
+
+ #[test]
+ fn test_stream_end() {
+ let mut c = XMPPCodec::new();
+ let mut b = BytesMut::with_capacity(1024);
+ b.put(r"<?xml version='1.0'?><stream:stream xmlns:stream='http://etherx.jabber.org/streams' version='1.0' xmlns='jabber:client'>");
+ let r = c.decode(&mut b);
+ assert!(match r {
+ Ok(Some(Packet::StreamStart(_))) => true,
+ _ => false,
+ });
+ b.clear();
+ b.put(r"</stream:stream>");
+ let r = c.decode(&mut b);
+ assert!(match r {
+ Ok(Some(Packet::StreamEnd)) => true,
+ _ => false,
+ });
+ }
+
+ #[test]
+ fn test_truncated_stanza() {
+ let mut c = XMPPCodec::new();
+ let mut b = BytesMut::with_capacity(1024);
+ b.put(r"<?xml version='1.0'?><stream:stream xmlns:stream='http://etherx.jabber.org/streams' version='1.0' xmlns='jabber:client'>");
+ let r = c.decode(&mut b);
+ assert!(match r {
+ Ok(Some(Packet::StreamStart(_))) => true,
+ _ => false,
+ });
+
+ b.clear();
+ b.put(r"<test>Γ</test");
+ let r = c.decode(&mut b);
+ assert!(match r {
+ Ok(None) => true,
+ _ => false,
+ });
+
+ b.clear();
+ b.put(r">");
+ let r = c.decode(&mut b);
+ assert!(match r {
+ Ok(Some(Packet::Stanza(ref el))) if el.name() == "test" && el.text() == "Γ" => true,
+ _ => false,
+ });
+ }
+
+ #[test]
+ fn test_truncated_utf8() {
+ let mut c = XMPPCodec::new();
+ let mut b = BytesMut::with_capacity(1024);
+ b.put(r"<?xml version='1.0'?><stream:stream xmlns:stream='http://etherx.jabber.org/streams' version='1.0' xmlns='jabber:client'>");
+ let r = c.decode(&mut b);
+ assert!(match r {
+ Ok(Some(Packet::StreamStart(_))) => true,
+ _ => false,
+ });
+
+ b.clear();
+ b.put(&b"<test>\xc3"[..]);
+ let r = c.decode(&mut b);
+ assert!(match r {
+ Ok(None) => true,
+ _ => false,
+ });
+
+ b.clear();
+ b.put(&b"\x9f</test>"[..]);
+ let r = c.decode(&mut b);
+ assert!(match r {
+ Ok(Some(Packet::Stanza(ref el))) if el.name() == "test" && el.text() == "Γ" => true,
+ _ => false,
+ });
+ }
+
+ /// test case for https://gitlab.com/xmpp-rs/tokio-xmpp/issues/3
+ #[test]
+ fn test_atrribute_prefix() {
+ let mut c = XMPPCodec::new();
+ let mut b = BytesMut::with_capacity(1024);
+ b.put(r"<?xml version='1.0'?><stream:stream xmlns:stream='http://etherx.jabber.org/streams' version='1.0' xmlns='jabber:client'>");
+ let r = c.decode(&mut b);
+ assert!(match r {
+ Ok(Some(Packet::StreamStart(_))) => true,
+ _ => false,
+ });
+
+ b.clear();
+ b.put(r"<status xml:lang='en'>Test status</status>");
+ let r = c.decode(&mut b);
+ assert!(match r {
+ Ok(Some(Packet::Stanza(ref el))) if el.name() == "status" && el.text() == "Test status" && el.attr("xml:lang").map_or(false, |a| a == "en") => true,
+ _ => false,
+ });
+
+ }
+
+ /// By default, encode() only get's a BytesMut that has 8kb space reserved.
+ #[test]
+ fn test_large_stanza() {
+ use futures::{Future, Sink};
+ use std::io::Cursor;
+ use tokio_codec::FramedWrite;
+ let framed = FramedWrite::new(Cursor::new(vec![]), XMPPCodec::new());
+ let mut text = "".to_owned();
+ for _ in 0..2usize.pow(15) {
+ text = text + "A";
+ }
+ let stanza = Element::builder("message")
+ .append(Element::builder("body").append(text.as_ref()).build())
+ .build();
+ let framed = framed.send(Packet::Stanza(stanza)).wait().expect("send");
+ assert_eq!(
+ framed.get_ref().get_ref(),
+ &("<message><body>".to_owned() + &text + "</body></message>").as_bytes()
+ );
+ }
+
+ #[test]
+ fn test_lone_whitespace() {
+ let mut c = XMPPCodec::new();
+ let mut b = BytesMut::with_capacity(1024);
+ b.put(r"<?xml version='1.0'?><stream:stream xmlns:stream='http://etherx.jabber.org/streams' version='1.0' xmlns='jabber:client'>");
+ let r = c.decode(&mut b);
+ assert!(match r {
+ Ok(Some(Packet::StreamStart(_))) => true,
+ _ => false,
+ });
+
+ b.clear();
+ b.put(r" ");
+ let r = c.decode(&mut b);
+ assert!(match r {
+ Ok(None) => true,
+ _ => false,
+ });
+ }
+}
@@ -0,0 +1,92 @@
+//! `XMPPStream` is the common container for all XMPP network connections
+
+use futures::sink::Send;
+use futures::{Poll, Sink, StartSend, Stream};
+use xmpp_parsers::{Jid, Element};
+use tokio_codec::Framed;
+use tokio_io::{AsyncRead, AsyncWrite};
+
+use crate::stream_start::StreamStart;
+use crate::xmpp_codec::{Packet, XMPPCodec};
+
+/// <stream:stream> namespace
+pub const NS_XMPP_STREAM: &str = "http://etherx.jabber.org/streams";
+
+/// Wraps a `stream`
+pub struct XMPPStream<S> {
+ /// The local Jabber-Id
+ pub jid: Jid,
+ /// Codec instance
+ pub stream: Framed<S, XMPPCodec>,
+ /// `<stream:features/>` for XMPP version 1.0
+ pub stream_features: Element,
+ /// Root namespace
+ ///
+ /// This is different for either c2s, s2s, or component
+ /// connections.
+ pub ns: String,
+}
+
+impl<S: AsyncRead + AsyncWrite> XMPPStream<S> {
+ /// Constructor
+ pub fn new(
+ jid: Jid,
+ stream: Framed<S, XMPPCodec>,
+ ns: String,
+ stream_features: Element,
+ ) -> Self {
+ XMPPStream {
+ jid,
+ stream,
+ stream_features,
+ ns,
+ }
+ }
+
+ /// Send a `<stream:stream>` start tag
+ pub fn start(stream: S, jid: Jid, ns: String) -> StreamStart<S> {
+ let xmpp_stream = Framed::new(stream, XMPPCodec::new());
+ StreamStart::from_stream(xmpp_stream, jid, ns)
+ }
+
+ /// Unwraps the inner stream
+ pub fn into_inner(self) -> S {
+ self.stream.into_inner()
+ }
+
+ /// Re-run `start()`
+ pub fn restart(self) -> StreamStart<S> {
+ Self::start(self.stream.into_inner(), self.jid, self.ns)
+ }
+}
+
+impl<S: AsyncWrite> XMPPStream<S> {
+ /// Convenience method
+ pub fn send_stanza<E: Into<Element>>(self, e: E) -> Send<Self> {
+ self.send(Packet::Stanza(e.into()))
+ }
+}
+
+/// Proxy to self.stream
+impl<S: AsyncWrite> Sink for XMPPStream<S> {
+ type SinkItem = <Framed<S, XMPPCodec> as Sink>::SinkItem;
+ type SinkError = <Framed<S, XMPPCodec> as Sink>::SinkError;
+
+ fn start_send(&mut self, item: Self::SinkItem) -> StartSend<Self::SinkItem, Self::SinkError> {
+ self.stream.start_send(item)
+ }
+
+ fn poll_complete(&mut self) -> Poll<(), Self::SinkError> {
+ self.stream.poll_complete()
+ }
+}
+
+/// Proxy to self.stream
+impl<S: AsyncRead> Stream for XMPPStream<S> {
+ type Item = <Framed<S, XMPPCodec> as Stream>::Item;
+ type Error = <Framed<S, XMPPCodec> as Stream>::Error;
+
+ fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> {
+ self.stream.poll()
+ }
+}
@@ -0,0 +1,62 @@
+stages:
+ - build
+ - test
+
+variables:
+ FEATURES: ""
+ RUST_BACKTRACE: "full"
+
+.stable:
+ image: rust:latest
+ cache:
+ key: stable
+ paths:
+ - target/
+
+.nightly:
+ image: rustlang/rust:nightly
+ cache:
+ key: nightly
+ paths:
+ - target/
+
+.build:
+ stage: build
+ script:
+ - cargo build --verbose --no-default-features --features=$FEATURES
+
+.test:
+ stage: test
+ script:
+ - cargo test --verbose --no-default-features --features=$FEATURES
+
+rust-latest-build:
+ extends:
+ - .build
+ - .stable
+
+rust-nightly-build:
+ extends:
+ - .build
+ - .nightly
+
+
+rust-latest-test:
+ extends:
+ - .test
+ - .stable
+
+rust-nightly-test:
+ extends:
+ - .test
+ - .nightly
+
+rust-latest-build with features=disable-validation:
+ extends: rust-latest-build
+ variables:
+ FEATURES: "disable-validation"
+
+rust-latest-test with features=disable-validation:
+ extends: rust-latest-test
+ variables:
+ FEATURES: "disable-validation"
@@ -0,0 +1,34 @@
+[package]
+name = "xmpp-parsers"
+version = "0.16.0"
+authors = [
+ "Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>",
+ "Maxime βpepβ Buquet <pep@bouah.net>",
+]
+description = "Collection of parsers and serialisers for XMPP extensions"
+homepage = "https://gitlab.com/xmpp-rs/xmpp-parsers"
+repository = "https://gitlab.com/xmpp-rs/xmpp-parsers"
+keywords = ["xmpp", "jabber", "xml"]
+categories = ["parsing", "network-programming"]
+license = "MPL-2.0"
+edition = "2018"
+
+[dependencies]
+minidom = "0.11.0"
+jid = { version = "0.8", features = ["minidom"] }
+base64 = "0.10"
+digest = "0.8"
+sha-1 = "0.8"
+sha2 = "0.8"
+sha3 = "0.8"
+blake2 = "0.8"
+chrono = "0.4.5"
+
+[features]
+# Build xmpp-parsers to make components instead of clients.
+component = []
+# Disable validation of unknown attributes.
+disable-validation = []
+
+[package.metadata.docs.rs]
+rustdoc-args = [ "--sort-modules-by-appearance", "-Zunstable-options" ]
@@ -0,0 +1,325 @@
+Version 0.16.0:
+2019-10-15 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+ * New parsers/serialisers:
+ - Client Certificate Management for SASL EXTERNAL (XEP-0257)
+ - JID Prep (XEP-0328)
+ - Client State Indication (XEP-0352)
+ - OpenPGP for XMPP (XEP-0373)
+ - Bookmarks 2 (This Time it's Serious) (XEP-0402)
+ - Anonymous unique occupant identifiers for MUCs (XEP-0421)
+ - Source-Specific Media Attributes in Jingle (XEP-0339)
+ - Jingle RTP Feedback Negotiation (XEP-0293)
+ * Breaking changes:
+ - Presence constructors now take Into<Jid> and assume Some.
+ * Improvements:
+ - CI: refactor, add caching
+ - Update jid-rs to 0.8
+
+Version 0.15.0:
+2019-09-06 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+ * New parsers/serialisers:
+ - XHTML-IM (XEP-0071)
+ - User Tune (XEP-0118)
+ - Bits of Binary (XEP-0231)
+ - Message Carbons (XEP-0280)
+ * Breaking changes:
+ - Stop reexporting TryFrom and TryInto, they are available in
+ std::convert nowadays.
+ - Bind has been split into BindQuery and BindResponse.
+ * Improvements:
+ - New DOAP file for a machine-readable description of the features.
+ - Add various parser and formatter helpers on Hash.
+
+Version 0.14.0:
+2019-07-13 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>, Maxime βpepβ Buquet <pep@bouah.net>
+ * New parsers/serialisers:
+ - Entity Time (XEP-0202).
+ * Improvements:
+ - Microblog NS (XEP-0227).
+ - Update jid-rs dependency with jid split change (Jid, FullJid,
+ BareJid) and reexport them.
+ - Fix rustdoc options in Cargo.toml for docs.rs
+ * Breaking changes:
+ - Presence's show attribute is now Option<Show> and Show::None is no
+ more.
+
+Version 0.13.1:
+2019-04-12 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+ * Bugfixes:
+ - Fix invalid serialisation of priority in presence.
+ - Bump image size to u16 from u8, as per XEP-0084 version 1.1.2.
+ * Improvements:
+ - Drop try_from dependency, as std::convert::TryFrom got
+ stabilised.
+
+Version 0.13.0:
+2019-03-20 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+ * New parsers/serialisers:
+ - User Avatar (XEP-0084).
+ - Contact Addresses for XMPP Services (XEP-0157).
+ - Jingle RTP Sessions (XEP-0167).
+ - Jingle ICE-UDP Transport Method (XEP-0176).
+ - Use of DTLS-SRTP in Jingle Sessions (XEP-0320).
+ * Breaking changes:
+ - Make 'id' required on iq, as per RFC6120 Β§8.1.3.
+ - Refactor PubSub to have more type-safety.
+ - Treat FORM_TYPE as a special case in data forms, to avoid
+ duplicating it into a field.
+ - Add forgotten i18n to Jingle text element.
+ * Improvements:
+ - Add various helpers for hash representations.
+ - Add helpers constructors for multiple extensions (disco, caps,
+ pubsub, stanza_error).
+ - Use Into<String> in more constructors.
+ - Internal change on attribute declaration in macros.
+ - Reexport missing try_from::TryInto.
+
+Version 0.12.2:
+2019-01-16 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+ * Improvements:
+ - Reexport missing util::error::Error and try_from::TryFrom.
+
+Version 0.12.1:
+2019-01-16 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+ * Improvements:
+ - Reexport missing JidParseError from the jid crate.
+
+Version 0.12.0:
+2019-01-16 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+ * Breaking changes:
+ - Update dependencies.
+ - Switch to git, upstream is now available at
+ https://gitlab.com/xmpp-rs/xmpp-parsers
+ - Switch to Edition 2018, this removes support for rustc
+ versions older than 1.31.
+ - Implement support for XEP-0030 2.5rc3, relaxing the ordering
+ of children in disco#info.
+ * Improvements:
+ - Test for struct size, to keep them known and avoid bloat.
+ - Add various constructors to make the API easier to use.
+ - Reexport Jid from the jid crate, to avoid any weird issue on
+ using different incompatible versions of the same crate.
+ - Add forgotten 'ask' attribute on roster item (thanks O01eg!).
+ - Use cargo-fmt on the codebase, to lower the barrier of entry.
+ - Add a disable-validation feature, disabling many checks
+ xmpp-parsers is doing. This should be used for software
+ which want to let invalid XMPP pass through instead of being
+ rejected as invalid (thanks Astro-!).
+
+Version 0.11.1:
+2018-09-20 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+ * Improvements:
+ - Document all of the modules.
+
+Version 0.11.0:
+2018-08-03 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+ * Breaking changes:
+ - Split Software Version (XEP-0092) into a query and response
+ elements.
+ - Split RSM (XEP-0059) into a query and response elements.
+ - Fix type safety and spec issues in RSM and MAM (XEP-0313).
+ - Remove item@node and EmptyItems from PubSub events
+ (XEP-0060).
+ * Improvements:
+ - Document many additional modules.
+ - Add the <failure/> SASL nonza, as well as the SCRAM-SHA-256
+ and the two -PLUS mechanisms.
+
+Version 0.10.0:
+2018-07-31 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+ * New parsers/serialisers:
+ - Added <stream:stream>, SASL and bind (RFC6120) parsers.
+ - Added a WebSocket <open/> (RFC7395) implementation.
+ - Added a Jabber Component <handshake/> (XEP-0114).
+ - Added support for User Nickname (XEP-0172).
+ - Added support for Stream Management (XEP-0198).
+ - Added support for Bookmarks (XEP-0048).
+ - Publish-Subscribe (XEP-0060) now supports requests in
+ addition to events.
+ * Breaking changes:
+ - Switch from std::error to failure to report better errors.
+ - Bump to minidom 0.9.1, and reexport minidom::Element.
+ * Improvements:
+ - Add getters for the best body and subject in message, to make
+ it easier to determine which one the user wants based on
+ their language preferences.
+ - Add constructors and setters for most Jingle elements, to
+ ease their creation.
+ - Add constructors for hash, MUC item, iq and more.
+ - Use more macros to simplify and factorise the code.
+ - Use traits to define iq payloads.
+ - Document more modules.
+
+Version 0.9.0:
+2017-10-31 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+ * New parsers/serialisers:
+ - Blocking Command (XEP-0191) has been added.
+ - Date and Time Profiles (XEP-0082) has been added, replacing
+ ad-hoc use of chrono in various places.
+ - User Mood (XEP-0107) has been added.
+ * Breaking changes:
+ - Fix subscription="none" not being the default.
+ - Add more type safety to pubsub#event.
+ - Reuse Jingleβs ContentId type in JingleFT.
+ - Import the disposition attribute values in Jingle.
+ * Improvements:
+ - Refactor a good part of the code using macros.
+ - Simplify the parsing code wherever it makes sense.
+ - Check for children ordering in disco#info result.
+ - Finish implementation of <received/>, <checksum/> and
+ <range/> in JingleFT.
+ - Correctly serialise <ping/>, and test it.
+
+Version 0.8.0:
+2017-08-27 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+ * New parsers/serialisers:
+ - iq:version (XEP-0092) has been added.
+ - Finally implement extension serialisation in disco.
+ * Breaking changes:
+ - Wrap even more elements into their own type, in jingle,
+ jingle_ft, roster, message.
+ - Split loose enums into multiple structs where it makes sense,
+ such as for IBB, StanzaId, Receipts.
+ - Split disco query and answer elements into their own struct,
+ to enforce more guarantees on both.
+ * Improvements:
+ - Use Vec::into_iter() more to avoid references and clones.
+ - Make data_forms propagate a media_element error.
+ - Document more of disco, roster, chatstates.
+ - Use the minidom feature of jid, for IntoAttributeValue.
+ - Add a component feature, changing the default namespace to
+ jabber:component:accept.
+ - Add support for indicating ranged transfers in jingle_ft.
+
+Version 0.7.1:
+2017-07-24 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+ * Hotfixes:
+ - Stub out blake2 support, since the blake2 crate broke its API
+ between their 0.6.0 and 0.6.1 releasesβ¦
+
+Version 0.7.0:
+2017-07-23 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+ * New parsers/serialisers:
+ - Jingle Message Initialisation (XEP-0353) was added.
+ - The disco#items query (XEP-0030) is now supported, in
+ addition to the existing disco#info one.
+ * Breaking changes:
+ - Replaced many type aliases with proper wrapping structs.
+ - Split Disco into a query and a result part, since they have
+ very different constraints.
+ - Split IqPayload in three to avoid parsing queries as results
+ for example.
+ * Improvements:
+ - Use TryFrom from the try_from crate, thus removing the
+ dependency on nightly!
+ - Always implement From instead of Into, the latter is
+ generated anyway.
+ - Add helpers to construct your Presence stanza.
+
+Version 0.6.0:
+2017-06-27 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+ * New parsers/serialisers:
+ - In-Band Registration (XEP-0077) was added.
+ - Multi-User Chat (XEP-0045) got expanded a lot, thanks pep.!
+ * Breaking changes:
+ - Added wrappers for Strings used as identifiers, to add type
+ safety.
+ - Use chronoβs DateTime for JingleFTβs date element.
+ - Use Jid for JingleS5Bβs jid attribute.
+ * Improvements:
+ - Use more macros for common tasks.
+ - Add a constructor for Message and Presence.
+ - Implement std::fmt::Display and std::error::Error on our
+ error type.
+ - Fix DataForms serialisation.
+ - Fix roster group serialisation.
+ - Update libraries, notably chrono whose version 0.3.1 got
+ yanked.
+
+Version 0.5.0:
+2017-06-11 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+ * New parsers/serialisers:
+ - Implementation of the roster management protocol defined in
+ RFCΒ 6121 Β§2.
+ - Implementation of PubSub events (except collections).
+ - Early implementation of MUC.
+ * Breaking changes:
+ - Rename presence enums to make them easier to use.
+ * Improvements:
+ - Make hashes comparable and hashable.
+ - Make data forms embeddable easily into minidom
+ Element::builder.
+
+Version 0.4.0:
+2017-05-28 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+ * Incompatible changes:
+ - Receipts now make the id optional, as per the specification.
+ - Hashes now expose their raw binary value, instead of staying
+ base64-encoded.
+ - Parse dates (XEP-0082) in delayed delivery (XEP-0203) and
+ last user interaction (XEP-0319), using the chrono crate.
+ * Improvements:
+ - Removal of most of the remaining clones, the only ones left
+ are due to minidom not exposing a draining iterator over the
+ children.
+ - Finish to parse all of the attributes using get_attr!().
+ - More attribute checks.
+ - Split more parsers into one parser per element.
+ - Rely on minidomΒ 0.4.3 to serialise more standard types
+ automatically.
+ - Implement forgotten serialisation for data forms (XEP-0004).
+ - Implement legacy capabilities (XEP-0115) for compatibility
+ with older software.
+
+Version 0.3.0:
+2017-05-23 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+ * Big changes:
+ - All parsers and serialisers now consume their argument, this
+ makes the API way more efficient, but you will have to clone
+ before passing your structs in it if you want to keep them.
+ - Payloads of stanzas are not parsed automatically anymore, to
+ let applications which want to forward them as-is do so more
+ easily. Parsing now always succeeds on unknown payloads, it
+ just puts them into an Unknown value containing the existing
+ minidom Element.
+ * New parsers/serialisers:
+ - Last User Interaction in Presence, XEP-0319.
+ * Improved parsers/serialisers:
+ - Message now supports subject, bodies and threads as per
+ RFCΒ 6121 Β§5.2.
+ - Replace most attribute reads with a nice macro.
+ - Use enums for more enum-like things, for example Algo in
+ Hash, or FieldType in DataForm.
+ - Wire up stanza-id and origin-id to MessagePayload.
+ - Wire up MAM elements to message and iq payloads.
+ - Changes in the RSM API.
+ - Add support for more data forms elements, but still not the
+ complete set.
+ - Thanks to minidomΒ 0.3.1, check for explicitly disallowed
+ extra attributes in some elements.
+ * Crate updates:
+ - minidomΒ 0.4.1
+
+Version 0.2.0:
+2017-05-06 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+ * New parsers/serialisers:
+ - Stanza error, as per RFCΒ 6120 Β§8.3.
+ - Jingle SOCKS5 Transport, XEP-0260.
+ * Incompatible changes:
+ - Parsers and serialisers now all implement TryFrom<Element>
+ and Into<Element>, instead of the old parse_* and serialise_*
+ functions.
+ - Presence has got an overhaul, it now hosts show, statuses and
+ priority in its struct. The status module has also been
+ dropped.
+ - Message now supports multiple bodies, each in a different
+ language. The body module has also been dropped.
+ - Iq now gets a proper StanzaError when the type is error.
+ - Fix bogus Jingle payload, which was requiring both
+ description and transport.
+ * Crate updates:
+ - minidom 0.3.0
+
+Version 0.1.0:
+2017-04-29 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+ * Implement many extensions.
@@ -0,0 +1,373 @@
+Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+ means each individual or legal entity that creates, contributes to
+ the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+ means the combination of the Contributions of others (if any) used
+ by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+ means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+ means Source Code Form to which the initial Contributor has attached
+ the notice in Exhibit A, the Executable Form of such Source Code
+ Form, and Modifications of such Source Code Form, in each case
+ including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+ means
+
+ (a) that the initial Contributor has attached the notice described
+ in Exhibit B to the Covered Software; or
+
+ (b) that the Covered Software was made available under the terms of
+ version 1.1 or earlier of the License, but not also under the
+ terms of a Secondary License.
+
+1.6. "Executable Form"
+ means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+ means a work that combines Covered Software with other material, in
+ a separate file or files, that is not Covered Software.
+
+1.8. "License"
+ means this document.
+
+1.9. "Licensable"
+ means having the right to grant, to the maximum extent possible,
+ whether at the time of the initial grant or subsequently, any and
+ all of the rights conveyed by this License.
+
+1.10. "Modifications"
+ means any of the following:
+
+ (a) any file in Source Code Form that results from an addition to,
+ deletion from, or modification of the contents of Covered
+ Software; or
+
+ (b) any new file in Source Code Form that contains any Covered
+ Software.
+
+1.11. "Patent Claims" of a Contributor
+ means any patent claim(s), including without limitation, method,
+ process, and apparatus claims, in any patent Licensable by such
+ Contributor that would be infringed, but for the grant of the
+ License, by the making, using, selling, offering for sale, having
+ made, import, or transfer of either its Contributions or its
+ Contributor Version.
+
+1.12. "Secondary License"
+ means either the GNU General Public License, Version 2.0, the GNU
+ Lesser General Public License, Version 2.1, the GNU Affero General
+ Public License, Version 3.0, or any later versions of those
+ licenses.
+
+1.13. "Source Code Form"
+ means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+ means an individual or a legal entity exercising rights under this
+ License. For legal entities, "You" includes any entity that
+ controls, is controlled by, or is under common control with You. For
+ purposes of this definition, "control" means (a) the power, direct
+ or indirect, to cause the direction or management of such entity,
+ whether by contract or otherwise, or (b) ownership of more than
+ fifty percent (50%) of the outstanding shares or beneficial
+ ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+ Licensable by such Contributor to use, reproduce, make available,
+ modify, display, perform, distribute, and otherwise exploit its
+ Contributions, either on an unmodified basis, with Modifications, or
+ as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+ for sale, have made, import, and otherwise transfer either its
+ Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+ or
+
+(b) for infringements caused by: (i) Your and any other third party's
+ modifications of Covered Software, or (ii) the combination of its
+ Contributions with other software (except as part of its Contributor
+ Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+ its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+ Form, as described in Section 3.1, and You must inform recipients of
+ the Executable Form how they can obtain a copy of such Source Code
+ Form by reasonable means in a timely manner, at a charge no more
+ than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+ License, or sublicense it under different terms, provided that the
+ license for the Executable Form does not attempt to limit or alter
+ the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+* *
+* 6. Disclaimer of Warranty *
+* ------------------------- *
+* *
+* Covered Software is provided under this License on an "as is" *
+* basis, without warranty of any kind, either expressed, implied, or *
+* statutory, including, without limitation, warranties that the *
+* Covered Software is free of defects, merchantable, fit for a *
+* particular purpose or non-infringing. The entire risk as to the *
+* quality and performance of the Covered Software is with You. *
+* Should any Covered Software prove defective in any respect, You *
+* (not any Contributor) assume the cost of any necessary servicing, *
+* repair, or correction. This disclaimer of warranty constitutes an *
+* essential part of this License. No use of any Covered Software is *
+* authorized under this License except under this disclaimer. *
+* *
+************************************************************************
+
+************************************************************************
+* *
+* 7. Limitation of Liability *
+* -------------------------- *
+* *
+* Under no circumstances and under no legal theory, whether tort *
+* (including negligence), contract, or otherwise, shall any *
+* Contributor, or anyone who distributes Covered Software as *
+* permitted above, be liable to You for any direct, indirect, *
+* special, incidental, or consequential damages of any character *
+* including, without limitation, damages for lost profits, loss of *
+* goodwill, work stoppage, computer failure or malfunction, or any *
+* and all other commercial damages or losses, even if such party *
+* shall have been informed of the possibility of such damages. This *
+* limitation of liability shall not apply to liability for death or *
+* personal injury resulting from such party's negligence to the *
+* extent applicable law prohibits such limitation. Some *
+* jurisdictions do not allow the exclusion or limitation of *
+* incidental or consequential damages, so this exclusion and *
+* limitation may not apply to You. *
+* *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+ 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/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+ This Source Code Form is "Incompatible With Secondary Licenses", as
+ defined by the Mozilla Public License, v. 2.0.
@@ -0,0 +1,643 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="../style.xsl" type="text/xsl"?>
+<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+<Project xmlns="http://usefulinc.com/ns/doap#" xmlns:foaf="http://xmlns.com/foaf/0.1/" xmlns:xmpp="https://linkmauve.fr/ns/xmpp-doap#">
+ <name>xmpp-parsers</name>
+
+ <created>2017-04-18</created>
+
+ <shortdesc xml:lang="en">Collection of parsers and serialisers for XMPP extensions</shortdesc>
+ <shortdesc xml:lang="fr">Collection de parseurs et de sΓ©rialiseurs pour extensions XMPP</shortdesc>
+
+ <description xml:lang="en">TODO</description>
+ <description xml:lang="fr">TODO</description>
+
+ <homepage rdf:resource="https://gitlab.com/xmpp-rs/xmpp-parsers"/>
+ <!-- TODO: https://github.com/ewilderj/doap/issues/51 -->
+ <!--<doc rdf:resource="https://docs.rs/xmpp-parsers/"/>-->
+ <download-page rdf:resource="https://crates.io/crates/xmpp-parsers"/>
+ <bug-database rdf:resource="https://gitlab.com/xmpp-rs/xmpp-parsers/issues"/>
+ <!-- See https://github.com/ewilderj/doap/issues/53 -->
+ <developer-forum rdf:resource="xmpp:chat@xmpp.rs?join"/>
+ <support-forum rdf:resource="xmpp:chat@xmpp.rs?join"/>
+
+ <license rdf:resource="https://gitlab.com/xmpp-rs/xmpp-parsers/raw/master/LICENSE"/>
+
+ <!-- TODO: https://github.com/ewilderj/doap/issues/40 -->
+ <!--<logo rdf:resource="https://poez.io/img/logo.png"/>-->
+
+ <programming-language>Rust</programming-language>
+
+ <category rdf:resource="https://linkmauve.fr/ns/xmpp-doap#category-library"/>
+
+ <maintainer>
+ <foaf:Person>
+ <foaf:name>Link Mauve</foaf:name>
+ <foaf:homepage rdf:resource="https://linkmauve.fr/"/>
+ <foaf:mbox_sha1sum>aaa4dac2b31c1be4ee8f8e2ab986d34fb261974f</foaf:mbox_sha1sum>
+ </foaf:Person>
+ </maintainer>
+ <maintainer>
+ <foaf:Person>
+ <foaf:name>pep.</foaf:name>
+ <foaf:homepage rdf:resource="https://bouah.net/"/>
+ <foaf:mbox_sha1sum>99bcf9784288e323b0d2dea9c9ac7a2ede98395a</foaf:mbox_sha1sum>
+ </foaf:Person>
+ </maintainer>
+
+ <repository>
+ <GitRepository>
+ <browse rdf:resource="https://gitlab.com/xmpp-rs/xmpp-parsers"/>
+ <location rdf:resource="https://gitlab.com/xmpp-rs/xmpp-parsers.git"/>
+ </GitRepository>
+ </repository>
+
+ <implements rdf:resource="https://xmpp.org/rfcs/rfc6120.html"/>
+ <implements rdf:resource="https://xmpp.org/rfcs/rfc6121.html"/>
+ <implements rdf:resource="https://xmpp.org/rfcs/rfc7395.html"/>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0004.html"/>
+ <xmpp:status>partial</xmpp:status>
+ <xmpp:version>2.9</xmpp:version>
+ <xmpp:since>0.1.0</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0030.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>2.5rc3</xmpp:version>
+ <xmpp:since>0.1.0</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0045.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>1.32.0</xmpp:version>
+ <xmpp:since>0.5.0</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0047.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>2.0</xmpp:version>
+ <xmpp:since>0.1.0</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0048.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>1.1</xmpp:version>
+ <xmpp:since>0.10.0</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0059.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>1.0</xmpp:version>
+ <xmpp:since>0.1.0</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0060.html"/>
+ <xmpp:status>partial</xmpp:status>
+ <xmpp:version>1.15.8</xmpp:version>
+ <xmpp:since>0.5.0</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0068.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>1.2</xmpp:version>
+ <xmpp:since>0.1.0</xmpp:since>
+ <xmpp:note>there is no specific module for this, the feature is all in the XEP-0004 module</xmpp:note>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0071.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>1.5.4</xmpp:version>
+ <xmpp:since>0.15.0</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0077.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>2.4</xmpp:version>
+ <xmpp:since>0.6.0</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0082.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>1.1</xmpp:version>
+ <xmpp:since>0.9.0</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0084.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>1.1.2</xmpp:version>
+ <xmpp:since>0.13.0</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0085.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>2.1</xmpp:version>
+ <xmpp:since>0.1.0</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0092.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>1.1</xmpp:version>
+ <xmpp:since>0.8.0</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0107.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>1.2.1</xmpp:version>
+ <xmpp:since>0.9.0</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0114.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>1.6</xmpp:version>
+ <xmpp:since>0.10.0</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0115.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>1.5.1</xmpp:version>
+ <xmpp:since>0.4.0</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0118.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>1.2</xmpp:version>
+ <xmpp:since>0.15.0</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0157.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>1.0.1</xmpp:version>
+ <xmpp:since>0.13.0</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0166.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>1.1.2</xmpp:version>
+ <xmpp:since>0.1.0</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0167.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>1.1.1</xmpp:version>
+ <xmpp:since>0.13.0</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0172.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>1.1</xmpp:version>
+ <xmpp:since>0.10.0</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0176.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>1.0</xmpp:version>
+ <xmpp:since>0.13.0</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0184.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>1.4.0</xmpp:version>
+ <xmpp:since>0.1.0</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0191.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>1.3</xmpp:version>
+ <xmpp:since>0.9.0</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0198.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>1.6</xmpp:version>
+ <xmpp:since>0.10.0</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0199.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>2.0.1</xmpp:version>
+ <xmpp:since>0.1.0</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0202.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>2.0</xmpp:version>
+ <xmpp:since>0.14.0</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0203.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>2.0</xmpp:version>
+ <xmpp:since>0.1.0</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0221.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>1.0</xmpp:version>
+ <xmpp:since>0.1.0</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0224.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>1.0</xmpp:version>
+ <xmpp:since>0.1.0</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0231.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>1.0</xmpp:version>
+ <xmpp:since>0.15.0</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0234.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>0.19.1</xmpp:version>
+ <xmpp:since>0.1.0</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0257.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>0.3</xmpp:version>
+ <xmpp:since>0.16.0</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0260.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>1.0.3</xmpp:version>
+ <xmpp:since>0.2.0</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0261.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>1.0</xmpp:version>
+ <xmpp:since>0.1.0</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0277.html"/>
+ <xmpp:status>partial</xmpp:status>
+ <xmpp:version>0.6.3</xmpp:version>
+ <xmpp:since>0.14.0</xmpp:since>
+ <xmpp:note>only the namespace is included for now</xmpp:note>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0280.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>0.13.0</xmpp:version>
+ <xmpp:since>0.15.0</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0293.html"/>
+ <xmpp:status>partial</xmpp:status>
+ <xmpp:version>1.0.1</xmpp:version>
+ <xmpp:since>0.16.0</xmpp:since>
+ <xmpp:note>Only supported in payload-type, and only for rtcp-fb.</xmpp:note>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0297.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>1.0</xmpp:version>
+ <xmpp:since>0.1.0</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0300.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>0.6.0</xmpp:version>
+ <xmpp:since>0.1.0</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0308.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>1.1.0</xmpp:version>
+ <xmpp:since>0.1.0</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0313.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>0.6.3</xmpp:version>
+ <xmpp:since>0.1.0</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0319.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>1.0.2</xmpp:version>
+ <xmpp:since>0.3.0</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0320.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>0.3.1</xmpp:version>
+ <xmpp:since>0.13.0</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0328.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>0.1</xmpp:version>
+ <xmpp:since>0.16.0</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0339.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>0.3</xmpp:version>
+ <xmpp:since>0.16.0</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0352.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>0.3.0</xmpp:version>
+ <xmpp:since>0.16.0</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0353.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>0.3</xmpp:version>
+ <xmpp:since>0.7.0</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0359.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>0.6.0</xmpp:version>
+ <xmpp:since>0.1.0</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0373.html"/>
+ <xmpp:status>partial</xmpp:status>
+ <xmpp:version>0.4.0</xmpp:version>
+ <xmpp:since>0.16.0</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0380.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>0.2.0</xmpp:version>
+ <xmpp:since>0.1.0</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0390.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>0.3.0</xmpp:version>
+ <xmpp:since>0.1.0</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0402.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>0.3.0</xmpp:version>
+ <xmpp:since>0.16.0</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0421.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>0.1.0</xmpp:version>
+ <xmpp:since>0.16.0</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+
+ <release>
+ <Version>
+ <revision>0.15.0</revision>
+ <created>2019-09-06</created>
+ <file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.15.0/download"/>
+ </Version>
+ </release>
+ <release>
+ <Version>
+ <revision>0.14.0</revision>
+ <created>2019-07-13</created>
+ <file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.14.0/download"/>
+ </Version>
+ </release>
+ <release>
+ <Version>
+ <revision>0.13.1</revision>
+ <created>2019-04-12</created>
+ <file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.13.1/download"/>
+ </Version>
+ </release>
+ <release>
+ <Version>
+ <revision>0.13.0</revision>
+ <created>2019-03-20</created>
+ <file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.13.0/download"/>
+ </Version>
+ </release>
+ <release>
+ <Version>
+ <revision>0.12.2</revision>
+ <created>2019-01-16</created>
+ <file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.12.2/download"/>
+ </Version>
+ </release>
+ <release>
+ <Version>
+ <revision>0.12.1</revision>
+ <created>2019-01-16</created>
+ <file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.12.1/download"/>
+ </Version>
+ </release>
+ <release>
+ <Version>
+ <revision>0.12.0</revision>
+ <created>2019-01-16</created>
+ <file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.12.0/download"/>
+ </Version>
+ </release>
+ <release>
+ <Version>
+ <revision>0.11.1</revision>
+ <created>2018-09-20</created>
+ <file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.11.1/download"/>
+ </Version>
+ </release>
+ <release>
+ <Version>
+ <revision>0.11.0</revision>
+ <created>2018-08-02</created>
+ <file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.11.0/download"/>
+ </Version>
+ </release>
+ <release>
+ <Version>
+ <revision>0.10.0</revision>
+ <created>2018-07-31</created>
+ <file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.10.0/download"/>
+ </Version>
+ </release>
+ <release>
+ <Version>
+ <revision>0.9.0</revision>
+ <created>2017-12-27</created>
+ <file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.9.0/download"/>
+ </Version>
+ </release>
+ <release>
+ <Version>
+ <revision>0.8.0</revision>
+ <created>2017-11-30</created>
+ <file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.8.0/download"/>
+ </Version>
+ </release>
+ <release>
+ <Version>
+ <revision>0.7.1</revision>
+ <created>2017-11-30</created>
+ <file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.7.1/download"/>
+ </Version>
+ </release>
+ <release>
+ <Version>
+ <revision>0.7.0</revision>
+ <created>2017-11-30</created>
+ <file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.7.0/download"/>
+ </Version>
+ </release>
+ <release>
+ <Version>
+ <revision>0.6.0</revision>
+ <created>2017-11-30</created>
+ <file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.6.0/download"/>
+ </Version>
+ </release>
+ <release>
+ <Version>
+ <revision>0.5.0</revision>
+ <created>2017-11-30</created>
+ <file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.5.0/download"/>
+ </Version>
+ </release>
+ <release>
+ <Version>
+ <revision>0.4.0</revision>
+ <created>2017-11-30</created>
+ <file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.4.0/download"/>
+ </Version>
+ </release>
+ <release>
+ <Version>
+ <revision>0.3.0</revision>
+ <created>2017-11-30</created>
+ <file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.3.0/download"/>
+ </Version>
+ </release>
+ <release>
+ <Version>
+ <revision>0.2.0</revision>
+ <created>2017-11-30</created>
+ <file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.2.0/download"/>
+ </Version>
+ </release>
+ <release>
+ <Version>
+ <revision>0.1.0</revision>
+ <created>2017-11-30</created>
+ <file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.1.0/download"/>
+ </Version>
+ </release>
+</Project>
+</rdf:RDF>
@@ -0,0 +1,62 @@
+// 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 std::convert::TryFrom;
+use std::io::{self, Read};
+use std::env;
+use xmpp_parsers::{
+ Element,
+ disco::DiscoInfoResult,
+ caps::{Caps, compute_disco as compute_disco_caps, hash_caps},
+ ecaps2::{ECaps2, compute_disco as compute_disco_ecaps2, hash_ecaps2},
+ hashes::Algo,
+};
+
+fn get_caps(disco: &DiscoInfoResult, node: String) -> Result<Caps, String> {
+ let caps_data = compute_disco_caps(&disco);
+ let caps_hash = hash_caps(&caps_data, Algo::Sha_1)?;
+ Ok(Caps::new(node, caps_hash))
+}
+
+fn get_ecaps2(disco: &DiscoInfoResult) -> Result<ECaps2, String> {
+ let ecaps2_data = compute_disco_ecaps2(&disco).unwrap();
+ let ecaps2_sha256 = hash_ecaps2(&ecaps2_data, Algo::Sha_256)?;
+ let ecaps2_sha3_256 = hash_ecaps2(&ecaps2_data, Algo::Sha3_256)?;
+ Ok(ECaps2::new(vec![ecaps2_sha256, ecaps2_sha3_256]))
+}
+
+fn main() -> Result<(), ()> {
+ let args: Vec<_> = env::args().collect();
+ if args.len() != 2 {
+ println!("Usage: {} <node>", args[0]);
+ return Err(());
+ }
+ let node = args[1].clone();
+
+ eprintln!("Reading a disco#info payload from stdin...");
+
+ // Read from stdin.
+ let stdin = io::stdin();
+ let mut data = String::new();
+ let mut handle = stdin.lock();
+ handle.read_to_string(&mut data).unwrap();
+
+ // Parse the payload into a DiscoInfoResult.
+ let elem: Element = data.parse().unwrap();
+ let disco = DiscoInfoResult::try_from(elem).unwrap();
+
+ // Compute both kinds of caps.
+ let caps = get_caps(&disco, node).unwrap();
+ let ecaps2 = get_ecaps2(&disco).unwrap();
+
+ // Print them.
+ let caps_elem = Element::from(caps);
+ let ecaps2_elem = Element::from(ecaps2);
+ println!("{}", String::from(&caps_elem));
+ println!("{}", String::from(&ecaps2_elem));
+
+ Ok(())
+}
@@ -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);
+ }
+}
@@ -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 avatarβs 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.")
+ }
+}
@@ -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 clientβs 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 resourceprepβd 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.");
+ }
+}
@@ -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.");
+ }
+}
@@ -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.");
+ }
+}
@@ -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");
+ }
+}
@@ -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");
+ }
+}
@@ -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 applicationβs
+ /// [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()
+ );
+ }
+}
@@ -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());
+ }
+}
@@ -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");
+ }
+}
@@ -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));
+ }
+}
@@ -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"))
+ );
+ }
+}
@@ -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));
+ }
+}
@@ -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 isnβt 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 isnβt 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."
+ );
+ }
+}
@@ -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;
+
+ // DateTimeβs size doesnβt 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 arenβt 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 arenβt 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 weβll 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")));
+ }
+}
@@ -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);
+ }
+}
@@ -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")));
+ }
+}
@@ -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);
+ }
+}
@@ -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);
+ }
+}
@@ -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);
+ }
+}
@@ -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 canβt 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.");
+ }
+}
@@ -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 doesnβt 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.");
+ }
+}
@@ -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 doesnβt 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));
+ }
+}
@@ -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 arenβt 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 arenβt 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 weβll 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);
+ }
+}
@@ -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());
+ }
+}
@@ -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 stringprepβd/PRECISβd 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());
+ }
+}
@@ -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 isnβt 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 isnβt 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 sessionβs 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 doesnβt 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 initiatorβs JID.
+ pub fn with_initiator(mut self, initiator: Jid) -> Jingle {
+ self.initiator = Some(initiator);
+ self
+ }
+
+ /// Set the responderβs 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 doesnβt contain a valid reason.");
+
+ let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><a/></reason></jingle>".parse().unwrap();
+ let error = Jingle::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.");
+ }
+}
@@ -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 isnβt 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]);
+ }
+}
@@ -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.");
+ }
+}
@@ -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.");
+ }
+}
@@ -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]);
+ }
+}
@@ -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 doesnβt 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.");
+ }
+}
@@ -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");
+ }
+}
@@ -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);
+ }
+}
@@ -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 canβt 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));
+ }
+}
@@ -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");
+ }
+}
@@ -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;
@@ -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 nodeβs 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 userβs [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);
+ }
+}
@@ -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"
+ );
+ }
+}
@@ -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);
+ }
+}
@@ -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);
+ }
+}
@@ -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);
+ }
+}
@@ -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;
@@ -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()
+ );
+ }
+}
@@ -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 isnβt 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()))),
+ }
+ }
+}
@@ -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.");
+ }
+}
@@ -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 isnβt 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;
@@ -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);
+ }
+}
@@ -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);
+ }
+}
@@ -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.");
+ }
+}
@@ -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 [XMPPβCORE]).
+ 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 senderβs resource priority, if negative it wonβt receive messages
+ /// that havenβt 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"));
+ }
+}
@@ -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 nodeβs 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 userβs 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));
+ }
+}
@@ -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 userβs subscription to this node is still pending.
+ Pending => "pending",
+
+ /// The user is subscribed to this node.
+ Subscribed => "subscribed",
+
+ /// The userβs 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> {}
@@ -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 donβt 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);
+ }
+}
@@ -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"));
+ }
+}
@@ -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 doesnβt have any subscription to this contactβs 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 otherβs 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 userβs 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.");
+ }
+}
@@ -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 recipientβs 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 canβt 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 canβt 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 canβt 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 canβt 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 canβt 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 canβt 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 canβt 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));
+ }
+}
@@ -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 clientβs response to the
+ /// serverβs [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")
+ );
+ }
+}
@@ -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);
+ }
+}
@@ -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);
+ }
+}
@@ -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 [XMPPβURI]).
+ 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
+ /// [XMPPβADDR]; 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 [XMPPβURI]).
+ 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
+ /// [XEPβ0045] and gateways to non-XMPP instant messaging services,
+ /// which traditionally required registration in order to use the
+ /// gateway [XEPβ0100]); 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 [XMPPβIM] and opt-in data feeds for XMPP
+ /// publish-subscribe as defined in [XEPβ0060]); 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.");
+ }
+}
@@ -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);
+ }
+}
@@ -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")));
+ }
+}
@@ -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::*;
+
+ // DateTimeβs size doesnβt 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);
+ }
+}
@@ -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 canβt 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 canβt 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 canβt 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 canβt 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 canβt 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 canβt 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 canβt 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()));
+ }
+}
@@ -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));
+ }
+}
@@ -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)
+ }
+}
@@ -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())
+ }
+}
@@ -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
+ }
+ }
+ }
+}
@@ -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;
@@ -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));
+ }
+}
@@ -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);
+ }
+}
@@ -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()),
+ ] }),
+ ] };
+ }
+}
@@ -0,0 +1,21 @@
+[package]
+name = "xmpp"
+version = "0.3.0"
+authors = [
+ "Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>",
+ "Maxime βpepβ Buquet <pep@bouah.net>",
+]
+description = "High-level XMPP library"
+homepage = "https://gitlab.com/linkmauve/xmpp-rs"
+repository = "https://gitlab.com/linkmauve/xmpp-rs"
+keywords = ["xmpp", "jabber", "chat", "messaging", "bot"]
+categories = ["network-programming"]
+license = "MPL-2.0"
+edition = "2018"
+
+[dependencies]
+tokio-xmpp = "1.0.1"
+xmpp-parsers = "0.16"
+futures = "0.1"
+tokio = "0.1"
+log = "0.4"
@@ -0,0 +1,373 @@
+Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+ means each individual or legal entity that creates, contributes to
+ the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+ means the combination of the Contributions of others (if any) used
+ by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+ means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+ means Source Code Form to which the initial Contributor has attached
+ the notice in Exhibit A, the Executable Form of such Source Code
+ Form, and Modifications of such Source Code Form, in each case
+ including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+ means
+
+ (a) that the initial Contributor has attached the notice described
+ in Exhibit B to the Covered Software; or
+
+ (b) that the Covered Software was made available under the terms of
+ version 1.1 or earlier of the License, but not also under the
+ terms of a Secondary License.
+
+1.6. "Executable Form"
+ means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+ means a work that combines Covered Software with other material, in
+ a separate file or files, that is not Covered Software.
+
+1.8. "License"
+ means this document.
+
+1.9. "Licensable"
+ means having the right to grant, to the maximum extent possible,
+ whether at the time of the initial grant or subsequently, any and
+ all of the rights conveyed by this License.
+
+1.10. "Modifications"
+ means any of the following:
+
+ (a) any file in Source Code Form that results from an addition to,
+ deletion from, or modification of the contents of Covered
+ Software; or
+
+ (b) any new file in Source Code Form that contains any Covered
+ Software.
+
+1.11. "Patent Claims" of a Contributor
+ means any patent claim(s), including without limitation, method,
+ process, and apparatus claims, in any patent Licensable by such
+ Contributor that would be infringed, but for the grant of the
+ License, by the making, using, selling, offering for sale, having
+ made, import, or transfer of either its Contributions or its
+ Contributor Version.
+
+1.12. "Secondary License"
+ means either the GNU General Public License, Version 2.0, the GNU
+ Lesser General Public License, Version 2.1, the GNU Affero General
+ Public License, Version 3.0, or any later versions of those
+ licenses.
+
+1.13. "Source Code Form"
+ means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+ means an individual or a legal entity exercising rights under this
+ License. For legal entities, "You" includes any entity that
+ controls, is controlled by, or is under common control with You. For
+ purposes of this definition, "control" means (a) the power, direct
+ or indirect, to cause the direction or management of such entity,
+ whether by contract or otherwise, or (b) ownership of more than
+ fifty percent (50%) of the outstanding shares or beneficial
+ ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+ Licensable by such Contributor to use, reproduce, make available,
+ modify, display, perform, distribute, and otherwise exploit its
+ Contributions, either on an unmodified basis, with Modifications, or
+ as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+ for sale, have made, import, and otherwise transfer either its
+ Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+ or
+
+(b) for infringements caused by: (i) Your and any other third party's
+ modifications of Covered Software, or (ii) the combination of its
+ Contributions with other software (except as part of its Contributor
+ Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+ its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+ Form, as described in Section 3.1, and You must inform recipients of
+ the Executable Form how they can obtain a copy of such Source Code
+ Form by reasonable means in a timely manner, at a charge no more
+ than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+ License, or sublicense it under different terms, provided that the
+ license for the Executable Form does not attempt to limit or alter
+ the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+* *
+* 6. Disclaimer of Warranty *
+* ------------------------- *
+* *
+* Covered Software is provided under this License on an "as is" *
+* basis, without warranty of any kind, either expressed, implied, or *
+* statutory, including, without limitation, warranties that the *
+* Covered Software is free of defects, merchantable, fit for a *
+* particular purpose or non-infringing. The entire risk as to the *
+* quality and performance of the Covered Software is with You. *
+* Should any Covered Software prove defective in any respect, You *
+* (not any Contributor) assume the cost of any necessary servicing, *
+* repair, or correction. This disclaimer of warranty constitutes an *
+* essential part of this License. No use of any Covered Software is *
+* authorized under this License except under this disclaimer. *
+* *
+************************************************************************
+
+************************************************************************
+* *
+* 7. Limitation of Liability *
+* -------------------------- *
+* *
+* Under no circumstances and under no legal theory, whether tort *
+* (including negligence), contract, or otherwise, shall any *
+* Contributor, or anyone who distributes Covered Software as *
+* permitted above, be liable to You for any direct, indirect, *
+* special, incidental, or consequential damages of any character *
+* including, without limitation, damages for lost profits, loss of *
+* goodwill, work stoppage, computer failure or malfunction, or any *
+* and all other commercial damages or losses, even if such party *
+* shall have been informed of the possibility of such damages. This *
+* limitation of liability shall not apply to liability for death or *
+* personal injury resulting from such party's negligence to the *
+* extent applicable law prohibits such limitation. Some *
+* jurisdictions do not allow the exclusion or limitation of *
+* incidental or consequential damages, so this exclusion and *
+* limitation may not apply to You. *
+* *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+ 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/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+ This Source Code Form is "Incompatible With Secondary Licenses", as
+ defined by the Mozilla Public License, v. 2.0.
@@ -201,7 +201,7 @@ impl ClientBuilder<'_> {
};
match event {
- TokioXmppEvent::Online => {
+ TokioXmppEvent::Online(_) => {
let presence = ClientBuilder::make_initial_presence(&disco, &node).into();
let packet = Packet::Stanza(presence);
sender_tx.unbounded_send(packet)