Merge branch 'merge-repos' into 'master'

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

Change summary

.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(-)

Detailed changes

Cargo.toml πŸ”—

@@ -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" }

jid-rs/.gitlab-ci.yml πŸ”—

@@ -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"

jid-rs/CHANGELOG.md πŸ”—

@@ -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 )

jid-rs/Cargo.toml πŸ”—

@@ -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 }

jid-rs/README.md πŸ”—

@@ -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.

jid-rs/src/lib.rs πŸ”—

@@ -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()));
+    }
+}

minidom-rs/.gitlab-ci.yml πŸ”—

@@ -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"

minidom-rs/CHANGELOG.md πŸ”—

@@ -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 )

minidom-rs/Cargo.toml πŸ”—

@@ -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 = []

minidom-rs/LICENSE πŸ”—

@@ -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.

minidom-rs/README.md πŸ”—

@@ -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.

minidom-rs/examples/articles.rs πŸ”—

@@ -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. &lt;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);
+}

minidom-rs/src/convert.rs πŸ”—

@@ -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");
+    }
+}

minidom-rs/src/element.rs πŸ”—

@@ -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"&lt;")),
+            b'>' => escapes.push((loc, b"&gt;")),
+            b'\'' => escapes.push((loc, b"&apos;")),
+            b'&' => escapes.push((loc, b"&amp;")),
+            b'"' => escapes.push((loc, b"&quot;")),
+            _ => 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[&apos;&gt;blah<blah>]]></test>";
+        let mut reader = EventReader::from_str(xml);
+        let elem = Element::from_reader(&mut reader).unwrap();
+        assert_eq!(elem.text(), "&apos;&gt;blah<blah>");
+    }
+}

minidom-rs/src/error.rs πŸ”—

@@ -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>;

minidom-rs/src/lib.rs πŸ”—

@@ -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. &lt;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;

minidom-rs/src/namespace_set.rs πŸ”—

@@ -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)))");
+    }
+}

minidom-rs/src/node.rs πŸ”—

@@ -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())
+    }
+}

minidom-rs/src/tests.rs πŸ”—

@@ -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="&quot;Air&quot; 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>&lt;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)
+    }
+}

tokio-xmpp/.gitlab-ci.yml πŸ”—

@@ -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

tokio-xmpp/Cargo.toml πŸ”—

@@ -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"

tokio-xmpp/README.md πŸ”—

@@ -0,0 +1,6 @@
+# TODO
+
+- [ ] minidom ns
+- [ ] replace debug output with log crate
+- [ ] customize tls verify?
+- [ ] more tests

tokio-xmpp/examples/contact_addr.rs πŸ”—

@@ -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));
+    }
+}

tokio-xmpp/examples/download_avatars.rs πŸ”—

@@ -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!(
+                                            "{} 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 {} to {}.",
+        from, filename
+    );
+    create_dir_all(directory)?;
+    let mut file = File::create(filename)?;
+    file.write_all(data)
+}

tokio-xmpp/examples/echo_bot.rs πŸ”—

@@ -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()
+}

tokio-xmpp/examples/echo_component.rs πŸ”—

@@ -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()
+}

tokio-xmpp/logo.svg πŸ”—

@@ -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"

tokio-xmpp/src/client/auth.rs πŸ”—

@@ -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()
+    }
+}

tokio-xmpp/src/client/bind.rs πŸ”—

@@ -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!(),
+        }
+    }
+}

tokio-xmpp/src/client/mod.rs πŸ”—

@@ -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(())),
+        }
+    }
+}

tokio-xmpp/src/component/auth.rs πŸ”—

@@ -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!(),
+        }
+    }
+}

tokio-xmpp/src/component/mod.rs πŸ”—

@@ -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(())),
+        }
+    }
+}

tokio-xmpp/src/error.rs πŸ”—

@@ -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)
+    }
+}

tokio-xmpp/src/event.rs πŸ”—

@@ -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,
+        }
+    }
+}

tokio-xmpp/src/happy_eyeballs.rs πŸ”—

@@ -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!(""),
+        }
+    }
+}

tokio-xmpp/src/lib.rs πŸ”—

@@ -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};

tokio-xmpp/src/starttls.rs πŸ”—

@@ -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
+        }
+    }
+}

tokio-xmpp/src/stream_start.rs πŸ”—

@@ -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
+        }
+    }
+}

tokio-xmpp/src/xmpp_codec.rs πŸ”—

@@ -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("&amp;"),
+            '<' => result.push_str("&lt;"),
+            '>' => result.push_str("&gt;"),
+            '\'' => result.push_str("&apos;"),
+            '"' => result.push_str("&quot;"),
+            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,
+        });
+    }
+}

tokio-xmpp/src/xmpp_stream.rs πŸ”—

@@ -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()
+    }
+}

xmpp-parsers/.gitlab-ci.yml πŸ”—

@@ -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"

xmpp-parsers/Cargo.toml πŸ”—

@@ -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" ]

xmpp-parsers/ChangeLog πŸ”—

@@ -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.

xmpp-parsers/LICENSE πŸ”—

@@ -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.

xmpp-parsers/doap.xml πŸ”—

@@ -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>

xmpp-parsers/examples/generate-caps.rs πŸ”—

@@ -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(())
+}

xmpp-parsers/src/attention.rs πŸ”—

@@ -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);
+    }
+}

xmpp-parsers/src/avatar.rs πŸ”—

@@ -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.")
+    }
+}

xmpp-parsers/src/bind.rs πŸ”—

@@ -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.");
+    }
+}

xmpp-parsers/src/blocking.rs πŸ”—

@@ -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.");
+    }
+}

xmpp-parsers/src/bob.rs πŸ”—

@@ -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.");
+    }
+}

xmpp-parsers/src/bookmarks.rs πŸ”—

@@ -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");
+    }
+}

xmpp-parsers/src/bookmarks2.rs πŸ”—

@@ -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");
+    }
+}

xmpp-parsers/src/caps.rs πŸ”—

@@ -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()
+        );
+    }
+}

xmpp-parsers/src/carbons.rs πŸ”—

@@ -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());
+    }
+}

xmpp-parsers/src/cert_management.rs πŸ”—

@@ -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");
+    }
+}

xmpp-parsers/src/chatstates.rs πŸ”—

@@ -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));
+    }
+}

xmpp-parsers/src/component.rs πŸ”—

@@ -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"))
+        );
+    }
+}

xmpp-parsers/src/csi.rs πŸ”—

@@ -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));
+    }
+}

xmpp-parsers/src/data_forms.rs πŸ”—

@@ -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."
+        );
+    }
+}

xmpp-parsers/src/date.rs πŸ”—

@@ -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")));
+    }
+}

xmpp-parsers/src/delay.rs πŸ”—

@@ -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);
+    }
+}

xmpp-parsers/src/disco.rs πŸ”—

@@ -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")));
+    }
+}

xmpp-parsers/src/ecaps2.rs πŸ”—

@@ -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);
+    }
+}

xmpp-parsers/src/eme.rs πŸ”—

@@ -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);
+    }
+}

xmpp-parsers/src/forwarding.rs πŸ”—

@@ -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);
+    }
+}

xmpp-parsers/src/hashes.rs πŸ”—

@@ -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.");
+    }
+}

xmpp-parsers/src/ibb.rs πŸ”—

@@ -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.");
+    }
+}

xmpp-parsers/src/ibr.rs πŸ”—

@@ -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));
+    }
+}

xmpp-parsers/src/idle.rs πŸ”—

@@ -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);
+    }
+}

xmpp-parsers/src/iq.rs πŸ”—

@@ -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());
+    }
+}

xmpp-parsers/src/jid_prep.rs πŸ”—

@@ -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());
+    }
+}

xmpp-parsers/src/jingle.rs πŸ”—

@@ -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.");
+    }
+}

xmpp-parsers/src/jingle_dtls_srtp.rs πŸ”—

@@ -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]);
+    }
+}

xmpp-parsers/src/jingle_ft.rs πŸ”—

@@ -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.");
+    }
+}

xmpp-parsers/src/jingle_ibb.rs πŸ”—

@@ -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.");
+    }
+}

xmpp-parsers/src/jingle_ice_udp.rs πŸ”—

@@ -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]);
+    }
+}

xmpp-parsers/src/jingle_message.rs πŸ”—

@@ -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.");
+    }
+}

xmpp-parsers/src/jingle_rtcp_fb.rs πŸ”—

@@ -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");
+    }
+}

xmpp-parsers/src/jingle_rtp.rs πŸ”—

@@ -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);
+    }
+}

xmpp-parsers/src/jingle_s5b.rs πŸ”—

@@ -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));
+    }
+}

xmpp-parsers/src/jingle_ssma.rs πŸ”—

@@ -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");
+    }
+}

xmpp-parsers/src/lib.rs πŸ”—

@@ -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;

xmpp-parsers/src/mam.rs πŸ”—

@@ -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);
+    }
+}

xmpp-parsers/src/media_element.rs πŸ”—

@@ -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"
+        );
+    }
+}

xmpp-parsers/src/message.rs πŸ”—

@@ -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);
+    }
+}

xmpp-parsers/src/message_correct.rs πŸ”—

@@ -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);
+    }
+}

xmpp-parsers/src/mood.rs πŸ”—

@@ -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);
+    }
+}

xmpp-parsers/src/muc/mod.rs πŸ”—

@@ -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;

xmpp-parsers/src/muc/muc.rs πŸ”—

@@ -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()
+        );
+    }
+}

xmpp-parsers/src/muc/user.rs πŸ”—

@@ -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()))),
+        }
+    }
+}

xmpp-parsers/src/nick.rs πŸ”—

@@ -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.");
+    }
+}

xmpp-parsers/src/ns.rs πŸ”—

@@ -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;

xmpp-parsers/src/occupant_id.rs πŸ”—

@@ -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);
+    }
+}

xmpp-parsers/src/openpgp.rs πŸ”—

@@ -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);
+    }
+}

xmpp-parsers/src/ping.rs πŸ”—

@@ -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.");
+    }
+}

xmpp-parsers/src/presence.rs πŸ”—

@@ -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"));
+    }
+}

xmpp-parsers/src/pubsub/event.rs πŸ”—

@@ -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));
+    }
+}

xmpp-parsers/src/pubsub/mod.rs πŸ”—

@@ -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> {}

xmpp-parsers/src/pubsub/pubsub.rs πŸ”—

@@ -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);
+    }
+}

xmpp-parsers/src/receipts.rs πŸ”—

@@ -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"));
+    }
+}

xmpp-parsers/src/roster.rs πŸ”—

@@ -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.");
+    }
+}

xmpp-parsers/src/rsm.rs πŸ”—

@@ -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));
+    }
+}

xmpp-parsers/src/sasl.rs πŸ”—

@@ -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")
+        );
+    }
+}

xmpp-parsers/src/server_info.rs πŸ”—

@@ -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);
+    }
+}

xmpp-parsers/src/sm.rs πŸ”—

@@ -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);
+    }
+}

xmpp-parsers/src/stanza_error.rs πŸ”—

@@ -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.");
+    }
+}

xmpp-parsers/src/stanza_id.rs πŸ”—

@@ -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);
+    }
+}

xmpp-parsers/src/stream.rs πŸ”—

@@ -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")));
+    }
+}

xmpp-parsers/src/time.rs πŸ”—

@@ -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);
+    }
+}

xmpp-parsers/src/tune.rs πŸ”—

@@ -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()));
+    }
+}

xmpp-parsers/src/util/compare_elements.rs πŸ”—

@@ -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));
+    }
+}

xmpp-parsers/src/util/error.rs πŸ”—

@@ -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)
+    }
+}

xmpp-parsers/src/util/helpers.rs πŸ”—

@@ -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())
+    }
+}

xmpp-parsers/src/util/macros.rs πŸ”—

@@ -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
+            }
+        }
+    }
+}

xmpp-parsers/src/util/mod.rs πŸ”—

@@ -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;

xmpp-parsers/src/version.rs πŸ”—

@@ -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));
+    }
+}

xmpp-parsers/src/websocket.rs πŸ”—

@@ -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);
+    }
+}

xmpp-parsers/src/xhtml.rs πŸ”—

@@ -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()),
+            ] }),
+        ] };
+    }
+}

xmpp-rs/Cargo.toml πŸ”—

@@ -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"

xmpp-rs/LICENSE πŸ”—

@@ -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.

src/lib.rs β†’ xmpp-rs/src/lib.rs πŸ”—

@@ -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)