Merge branch 'merge-repos' into 'master'
Merge repos Closes #2, #3, #1, #4, #7, #15, tokio-webhook2muc#2, #11, and #18 See merge request xmpp-rs/xmpp-rs!36
This commit is contained in:
commit
e43336fbdf
131 changed files with 24899 additions and 20 deletions
2
.hgignore
Normal file
2
.hgignore
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
target
|
||||
Cargo.lock
|
||||
32
Cargo.toml
32
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" }
|
||||
|
|
|
|||
62
jid-rs/.gitlab-ci.yml
Normal file
62
jid-rs/.gitlab-ci.yml
Normal file
|
|
@ -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"
|
||||
62
jid-rs/CHANGELOG.md
Normal file
62
jid-rs/CHANGELOG.md
Normal file
|
|
@ -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 )
|
||||
22
jid-rs/Cargo.toml
Normal file
22
jid-rs/Cargo.toml
Normal file
|
|
@ -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 }
|
||||
18
jid-rs/README.md
Normal file
18
jid-rs/README.md
Normal file
|
|
@ -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.
|
||||
813
jid-rs/src/lib.rs
Normal file
813
jid-rs/src/lib.rs
Normal file
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
62
minidom-rs/.gitlab-ci.yml
Normal file
62
minidom-rs/.gitlab-ci.yml
Normal file
|
|
@ -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"
|
||||
51
minidom-rs/CHANGELOG.md
Normal file
51
minidom-rs/CHANGELOG.md
Normal file
|
|
@ -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 )
|
||||
28
minidom-rs/Cargo.toml
Normal file
28
minidom-rs/Cargo.toml
Normal file
|
|
@ -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 = []
|
||||
15
minidom-rs/LICENSE
Normal file
15
minidom-rs/LICENSE
Normal file
|
|
@ -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.
|
||||
32
minidom-rs/README.md
Normal file
32
minidom-rs/README.md
Normal file
|
|
@ -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.
|
||||
45
minidom-rs/examples/articles.rs
Normal file
45
minidom-rs/examples/articles.rs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
extern crate minidom;
|
||||
|
||||
use minidom::Element;
|
||||
|
||||
const DATA: &str = r#"<articles xmlns="article">
|
||||
<article>
|
||||
<title>10 Terrible Bugs You Would NEVER Believe Happened</title>
|
||||
<body>
|
||||
Rust fixed them all. <3
|
||||
</body>
|
||||
</article>
|
||||
<article>
|
||||
<title>BREAKING NEWS: Physical Bug Jumps Out Of Programmer's Screen</title>
|
||||
<body>
|
||||
Just kidding!
|
||||
</body>
|
||||
</article>
|
||||
</articles>"#;
|
||||
|
||||
const ARTICLE_NS: &str = "article";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Article {
|
||||
title: String,
|
||||
body: String,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let root: Element = DATA.parse().unwrap();
|
||||
|
||||
let mut articles: Vec<Article> = Vec::new();
|
||||
|
||||
for child in root.children() {
|
||||
if child.is("article", ARTICLE_NS) {
|
||||
let title = child.get_child("title", ARTICLE_NS).unwrap().text();
|
||||
let body = child.get_child("body", ARTICLE_NS).unwrap().text();
|
||||
articles.push(Article {
|
||||
title: title,
|
||||
body: body.trim().to_owned(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
println!("{:?}", articles);
|
||||
}
|
||||
69
minidom-rs/src/convert.rs
Normal file
69
minidom-rs/src/convert.rs
Normal file
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
979
minidom-rs/src/element.rs
Normal file
979
minidom-rs/src/element.rs
Normal file
|
|
@ -0,0 +1,979 @@
|
|||
//! Provides an `Element` type, which represents DOM nodes, and a builder to create them with.
|
||||
|
||||
use crate::convert::IntoAttributeValue;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::namespace_set::NamespaceSet;
|
||||
use crate::node::Node;
|
||||
|
||||
use std::io:: Write;
|
||||
use std::collections::{btree_map, BTreeMap};
|
||||
|
||||
use std::str;
|
||||
use std::rc::Rc;
|
||||
use std::borrow::Cow;
|
||||
|
||||
use quick_xml::Reader as EventReader;
|
||||
use quick_xml::Writer as EventWriter;
|
||||
use quick_xml::events::{Event, BytesStart, BytesEnd, BytesDecl};
|
||||
|
||||
use std::io::BufRead;
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
use std::slice;
|
||||
|
||||
/// helper function to escape a `&[u8]` and replace all
|
||||
/// xml special characters (<, >, &, ', ") with their corresponding
|
||||
/// xml escaped value.
|
||||
pub fn escape(raw: &[u8]) -> Cow<[u8]> {
|
||||
let mut escapes: Vec<(usize, &'static [u8])> = Vec::new();
|
||||
let mut bytes = raw.iter();
|
||||
fn to_escape(b: u8) -> bool {
|
||||
match b {
|
||||
b'<' | b'>' | b'\'' | b'&' | b'"' => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
let mut loc = 0;
|
||||
while let Some(i) = bytes.position(|&b| to_escape(b)) {
|
||||
loc += i;
|
||||
match raw[loc] {
|
||||
b'<' => escapes.push((loc, b"<")),
|
||||
b'>' => escapes.push((loc, b">")),
|
||||
b'\'' => escapes.push((loc, b"'")),
|
||||
b'&' => escapes.push((loc, b"&")),
|
||||
b'"' => escapes.push((loc, b""")),
|
||||
_ => unreachable!("Only '<', '>','\', '&' and '\"' are escaped"),
|
||||
}
|
||||
loc += 1;
|
||||
}
|
||||
|
||||
if escapes.is_empty() {
|
||||
Cow::Borrowed(raw)
|
||||
} else {
|
||||
let len = raw.len();
|
||||
let mut v = Vec::with_capacity(len);
|
||||
let mut start = 0;
|
||||
for (i, r) in escapes {
|
||||
v.extend_from_slice(&raw[start..i]);
|
||||
v.extend_from_slice(r);
|
||||
start = i + 1;
|
||||
}
|
||||
|
||||
if start < len {
|
||||
v.extend_from_slice(&raw[start..]);
|
||||
}
|
||||
Cow::Owned(v)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
/// A struct representing a DOM Element.
|
||||
pub struct Element {
|
||||
prefix: Option<String>,
|
||||
name: String,
|
||||
namespaces: Rc<NamespaceSet>,
|
||||
attributes: BTreeMap<String, String>,
|
||||
children: Vec<Node>,
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Element> for String {
|
||||
fn from(elem: &'a Element) -> String {
|
||||
let mut writer = Vec::new();
|
||||
elem.write_to(&mut writer).unwrap();
|
||||
String::from_utf8(writer).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Element {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Element> {
|
||||
let mut reader = EventReader::from_str(s);
|
||||
Element::from_reader(&mut reader)
|
||||
}
|
||||
}
|
||||
|
||||
impl Element {
|
||||
fn new<NS: Into<NamespaceSet>>(name: String, prefix: Option<String>, namespaces: NS, attributes: BTreeMap<String, String>, children: Vec<Node>) -> Element {
|
||||
Element {
|
||||
prefix, name,
|
||||
namespaces: Rc::new(namespaces.into()),
|
||||
attributes,
|
||||
children,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a builder for an `Element` with the given `name`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use minidom::Element;
|
||||
///
|
||||
/// let elem = Element::builder("name")
|
||||
/// .ns("namespace")
|
||||
/// .attr("name", "value")
|
||||
/// .append("inner")
|
||||
/// .build();
|
||||
///
|
||||
/// assert_eq!(elem.name(), "name");
|
||||
/// assert_eq!(elem.ns(), Some("namespace".to_owned()));
|
||||
/// assert_eq!(elem.attr("name"), Some("value"));
|
||||
/// assert_eq!(elem.attr("inexistent"), None);
|
||||
/// assert_eq!(elem.text(), "inner");
|
||||
/// ```
|
||||
pub fn builder<S: AsRef<str>>(name: S) -> ElementBuilder {
|
||||
let (prefix, name) = split_element_name(name).unwrap();
|
||||
ElementBuilder {
|
||||
root: Element::new(name, prefix, None, BTreeMap::new(), Vec::new()),
|
||||
namespaces: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a bare minimum `Element` with this name.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use minidom::Element;
|
||||
///
|
||||
/// let bare = Element::bare("name");
|
||||
///
|
||||
/// assert_eq!(bare.name(), "name");
|
||||
/// assert_eq!(bare.ns(), None);
|
||||
/// assert_eq!(bare.attr("name"), None);
|
||||
/// assert_eq!(bare.text(), "");
|
||||
/// ```
|
||||
pub fn bare<S: Into<String>>(name: S) -> Element {
|
||||
Element {
|
||||
prefix: None,
|
||||
name: name.into(),
|
||||
namespaces: Rc::new(NamespaceSet::default()),
|
||||
attributes: BTreeMap::new(),
|
||||
children: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a reference to the name of this element.
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
/// Returns a reference to the prefix of this element.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// use minidom::Element;
|
||||
///
|
||||
/// let elem = Element::builder("prefix:name")
|
||||
/// .build();
|
||||
///
|
||||
/// assert_eq!(elem.name(), "name");
|
||||
/// assert_eq!(elem.prefix(), Some("prefix"));
|
||||
/// ```
|
||||
pub fn prefix(&self) -> Option<&str> {
|
||||
self.prefix.as_ref().map(String::as_ref)
|
||||
}
|
||||
|
||||
/// Returns a reference to the namespace of this element, if it has one, else `None`.
|
||||
pub fn ns(&self) -> Option<String> {
|
||||
self.namespaces.get(&self.prefix)
|
||||
}
|
||||
|
||||
/// Returns a reference to the value of the given attribute, if it exists, else `None`.
|
||||
pub fn attr(&self, name: &str) -> Option<&str> {
|
||||
if let Some(value) = self.attributes.get(name) {
|
||||
return Some(value)
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns an iterator over the attributes of this element.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use minidom::Element;
|
||||
///
|
||||
/// let elm: Element = "<elem a=\"b\" />".parse().unwrap();
|
||||
///
|
||||
/// let mut iter = elm.attrs();
|
||||
///
|
||||
/// assert_eq!(iter.next().unwrap(), ("a", "b"));
|
||||
/// assert_eq!(iter.next(), None);
|
||||
/// ```
|
||||
pub fn attrs(&self) -> Attrs {
|
||||
Attrs {
|
||||
iter: self.attributes.iter(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an iterator over the attributes of this element, with the value being a mutable
|
||||
/// reference.
|
||||
pub fn attrs_mut(&mut self) -> AttrsMut {
|
||||
AttrsMut {
|
||||
iter: self.attributes.iter_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Modifies the value of an attribute.
|
||||
pub fn set_attr<S: Into<String>, V: IntoAttributeValue>(&mut self, name: S, val: V) {
|
||||
let name = name.into();
|
||||
let val = val.into_attribute_value();
|
||||
|
||||
if let Some(value) = self.attributes.get_mut(&name) {
|
||||
*value = val.expect("removing existing value via set_attr, this is not yet supported (TODO)"); // TODO
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(val) = val {
|
||||
self.attributes.insert(name, val);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether the element has the given name and namespace.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use minidom::Element;
|
||||
///
|
||||
/// let elem = Element::builder("name").ns("namespace").build();
|
||||
///
|
||||
/// assert_eq!(elem.is("name", "namespace"), true);
|
||||
/// assert_eq!(elem.is("name", "wrong"), false);
|
||||
/// assert_eq!(elem.is("wrong", "namespace"), false);
|
||||
/// assert_eq!(elem.is("wrong", "wrong"), false);
|
||||
/// ```
|
||||
pub fn is<N: AsRef<str>, NS: AsRef<str>>(&self, name: N, namespace: NS) -> bool {
|
||||
self.name == name.as_ref() &&
|
||||
self.has_ns(namespace)
|
||||
}
|
||||
|
||||
/// Returns whether the element has the given namespace.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use minidom::Element;
|
||||
///
|
||||
/// let elem = Element::builder("name").ns("namespace").build();
|
||||
///
|
||||
/// assert_eq!(elem.has_ns("namespace"), true);
|
||||
/// assert_eq!(elem.has_ns("wrong"), false);
|
||||
/// ```
|
||||
pub fn has_ns<NS: AsRef<str>>(&self, namespace: NS) -> bool {
|
||||
self.namespaces.has(&self.prefix, namespace)
|
||||
}
|
||||
|
||||
/// Parse a document from an `EventReader`.
|
||||
pub fn from_reader<R: BufRead>(reader: &mut EventReader<R>) -> Result<Element> {
|
||||
let mut buf = Vec::new();
|
||||
|
||||
let root: Element = loop {
|
||||
let e = reader.read_event(&mut buf)?;
|
||||
match e {
|
||||
Event::Empty(ref e) | Event::Start(ref e) => {
|
||||
break build_element(reader, e)?;
|
||||
},
|
||||
Event::Eof => {
|
||||
return Err(Error::EndOfDocument);
|
||||
},
|
||||
#[cfg(not(feature = "comments"))]
|
||||
Event::Comment { .. } => {
|
||||
return Err(Error::CommentsDisabled);
|
||||
}
|
||||
#[cfg(feature = "comments")]
|
||||
Event::Comment { .. } => (),
|
||||
Event::Text { .. } |
|
||||
Event::End { .. } |
|
||||
Event::CData { .. } |
|
||||
Event::Decl { .. } |
|
||||
Event::PI { .. } |
|
||||
Event::DocType { .. } => (), // TODO: may need more errors
|
||||
}
|
||||
};
|
||||
|
||||
let mut stack = vec![root];
|
||||
|
||||
loop {
|
||||
match reader.read_event(&mut buf)? {
|
||||
Event::Empty(ref e) => {
|
||||
let elem = build_element(reader, e)?;
|
||||
// Since there is no Event::End after, directly append it to the current node
|
||||
stack.last_mut().unwrap().append_child(elem);
|
||||
},
|
||||
Event::Start(ref e) => {
|
||||
let elem = build_element(reader, e)?;
|
||||
stack.push(elem);
|
||||
},
|
||||
Event::End(ref e) => {
|
||||
if stack.len() <= 1 {
|
||||
break;
|
||||
}
|
||||
let elem = stack.pop().unwrap();
|
||||
if let Some(to) = stack.last_mut() {
|
||||
// TODO: check whether this is correct, we are comparing &[u8]s, not &strs
|
||||
let elem_name = e.name();
|
||||
let mut split_iter = elem_name.splitn(2, |u| *u == 0x3A);
|
||||
let possible_prefix = split_iter.next().unwrap(); // Can't be empty.
|
||||
match split_iter.next() {
|
||||
Some(name) => {
|
||||
match elem.prefix() {
|
||||
Some(prefix) => {
|
||||
if possible_prefix != prefix.as_bytes() {
|
||||
return Err(Error::InvalidElementClosed);
|
||||
}
|
||||
},
|
||||
None => {
|
||||
return Err(Error::InvalidElementClosed);
|
||||
},
|
||||
}
|
||||
if name != elem.name().as_bytes() {
|
||||
return Err(Error::InvalidElementClosed);
|
||||
}
|
||||
},
|
||||
None => {
|
||||
if elem.prefix().is_some() {
|
||||
return Err(Error::InvalidElementClosed);
|
||||
}
|
||||
if possible_prefix != elem.name().as_bytes() {
|
||||
return Err(Error::InvalidElementClosed);
|
||||
}
|
||||
},
|
||||
}
|
||||
to.append_child(elem);
|
||||
}
|
||||
},
|
||||
Event::Text(s) => {
|
||||
let text = s.unescape_and_decode(reader)?;
|
||||
if text != "" {
|
||||
let current_elem = stack.last_mut().unwrap();
|
||||
current_elem.append_text_node(text);
|
||||
}
|
||||
},
|
||||
Event::CData(s) => {
|
||||
let text = reader.decode(&s)?.to_owned();
|
||||
if text != "" {
|
||||
let current_elem = stack.last_mut().unwrap();
|
||||
current_elem.append_text_node(text);
|
||||
}
|
||||
},
|
||||
Event::Eof => {
|
||||
break;
|
||||
},
|
||||
#[cfg(not(feature = "comments"))]
|
||||
Event::Comment(_) => return Err(Error::CommentsDisabled),
|
||||
#[cfg(feature = "comments")]
|
||||
Event::Comment(s) => {
|
||||
let comment = reader.decode(&s)?.to_owned();
|
||||
if comment != "" {
|
||||
let current_elem = stack.last_mut().unwrap();
|
||||
current_elem.append_comment_node(comment);
|
||||
}
|
||||
},
|
||||
Event::Decl { .. } |
|
||||
Event::PI { .. } |
|
||||
Event::DocType { .. } => (),
|
||||
}
|
||||
}
|
||||
Ok(stack.pop().unwrap())
|
||||
}
|
||||
|
||||
/// Output a document to a `Writer`.
|
||||
pub fn write_to<W: Write>(&self, writer: &mut W) -> Result<()> {
|
||||
self.to_writer(&mut EventWriter::new(writer))
|
||||
}
|
||||
|
||||
/// Output the document to quick-xml `Writer`
|
||||
pub fn to_writer<W: Write>(&self, writer: &mut EventWriter<W>) -> Result<()> {
|
||||
writer.write_event(Event::Decl(BytesDecl::new(b"1.0", Some(b"utf-8"), None)))?;
|
||||
self.write_to_inner(writer)
|
||||
}
|
||||
|
||||
/// Like `write_to()` but without the `<?xml?>` prelude
|
||||
pub fn write_to_inner<W: Write>(&self, writer: &mut EventWriter<W>) -> Result<()> {
|
||||
let name = match self.prefix {
|
||||
None => Cow::Borrowed(&self.name),
|
||||
Some(ref prefix) => Cow::Owned(format!("{}:{}", prefix, self.name)),
|
||||
};
|
||||
|
||||
let mut start = BytesStart::borrowed(name.as_bytes(), name.len());
|
||||
for (prefix, ns) in self.namespaces.declared_ns() {
|
||||
match *prefix {
|
||||
None => start.push_attribute(("xmlns", ns.as_ref())),
|
||||
Some(ref prefix) => {
|
||||
let key = format!("xmlns:{}", prefix);
|
||||
start.push_attribute((key.as_bytes(), ns.as_bytes()))
|
||||
},
|
||||
}
|
||||
}
|
||||
for (key, value) in &self.attributes {
|
||||
start.push_attribute((key.as_bytes(), escape(value.as_bytes()).as_ref()));
|
||||
}
|
||||
|
||||
if self.children.is_empty() {
|
||||
writer.write_event(Event::Empty(start))?;
|
||||
return Ok(())
|
||||
}
|
||||
|
||||
writer.write_event(Event::Start(start))?;
|
||||
|
||||
for child in &self.children {
|
||||
child.write_to_inner(writer)?;
|
||||
}
|
||||
|
||||
writer.write_event(Event::End(BytesEnd::borrowed(name.as_bytes())))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns an iterator over references to every child node of this element.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use minidom::Element;
|
||||
///
|
||||
/// let elem: Element = "<root>a<c1 />b<c2 />c</root>".parse().unwrap();
|
||||
///
|
||||
/// let mut iter = elem.nodes();
|
||||
///
|
||||
/// assert_eq!(iter.next().unwrap().as_text().unwrap(), "a");
|
||||
/// assert_eq!(iter.next().unwrap().as_element().unwrap().name(), "c1");
|
||||
/// assert_eq!(iter.next().unwrap().as_text().unwrap(), "b");
|
||||
/// assert_eq!(iter.next().unwrap().as_element().unwrap().name(), "c2");
|
||||
/// assert_eq!(iter.next().unwrap().as_text().unwrap(), "c");
|
||||
/// assert_eq!(iter.next(), None);
|
||||
/// ```
|
||||
#[inline] pub fn nodes(&self) -> Nodes {
|
||||
self.children.iter()
|
||||
}
|
||||
|
||||
/// Returns an iterator over mutable references to every child node of this element.
|
||||
#[inline] pub fn nodes_mut(&mut self) -> NodesMut {
|
||||
self.children.iter_mut()
|
||||
}
|
||||
|
||||
/// Returns an iterator over references to every child element of this element.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use minidom::Element;
|
||||
///
|
||||
/// let elem: Element = "<root>hello<child1 />this<child2 />is<child3 />ignored</root>".parse().unwrap();
|
||||
///
|
||||
/// let mut iter = elem.children();
|
||||
/// assert_eq!(iter.next().unwrap().name(), "child1");
|
||||
/// assert_eq!(iter.next().unwrap().name(), "child2");
|
||||
/// assert_eq!(iter.next().unwrap().name(), "child3");
|
||||
/// assert_eq!(iter.next(), None);
|
||||
/// ```
|
||||
#[inline] pub fn children(&self) -> Children {
|
||||
Children {
|
||||
iter: self.children.iter(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an iterator over mutable references to every child element of this element.
|
||||
#[inline] pub fn children_mut(&mut self) -> ChildrenMut {
|
||||
ChildrenMut {
|
||||
iter: self.children.iter_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an iterator over references to every text node of this element.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use minidom::Element;
|
||||
///
|
||||
/// let elem: Element = "<root>hello<c /> world!</root>".parse().unwrap();
|
||||
///
|
||||
/// let mut iter = elem.texts();
|
||||
/// assert_eq!(iter.next().unwrap(), "hello");
|
||||
/// assert_eq!(iter.next().unwrap(), " world!");
|
||||
/// assert_eq!(iter.next(), None);
|
||||
/// ```
|
||||
#[inline] pub fn texts(&self) -> Texts {
|
||||
Texts {
|
||||
iter: self.children.iter(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an iterator over mutable references to every text node of this element.
|
||||
#[inline] pub fn texts_mut(&mut self) -> TextsMut {
|
||||
TextsMut {
|
||||
iter: self.children.iter_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Appends a child node to the `Element`, returning the appended node.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use minidom::Element;
|
||||
///
|
||||
/// let mut elem = Element::bare("root");
|
||||
///
|
||||
/// assert_eq!(elem.children().count(), 0);
|
||||
///
|
||||
/// elem.append_child(Element::bare("child"));
|
||||
///
|
||||
/// {
|
||||
/// let mut iter = elem.children();
|
||||
/// assert_eq!(iter.next().unwrap().name(), "child");
|
||||
/// assert_eq!(iter.next(), None);
|
||||
/// }
|
||||
///
|
||||
/// let child = elem.append_child(Element::bare("new"));
|
||||
///
|
||||
/// assert_eq!(child.name(), "new");
|
||||
/// ```
|
||||
pub fn append_child(&mut self, child: Element) -> &mut Element {
|
||||
child.namespaces.set_parent(Rc::clone(&self.namespaces));
|
||||
|
||||
self.children.push(Node::Element(child));
|
||||
if let Node::Element(ref mut cld) = *self.children.last_mut().unwrap() {
|
||||
cld
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
/// Appends a text node to an `Element`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use minidom::Element;
|
||||
///
|
||||
/// let mut elem = Element::bare("node");
|
||||
///
|
||||
/// assert_eq!(elem.text(), "");
|
||||
///
|
||||
/// elem.append_text_node("text");
|
||||
///
|
||||
/// assert_eq!(elem.text(), "text");
|
||||
/// ```
|
||||
pub fn append_text_node<S: Into<String>>(&mut self, child: S) {
|
||||
self.children.push(Node::Text(child.into()));
|
||||
}
|
||||
|
||||
/// Appends a comment node to an `Element`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use minidom::Element;
|
||||
///
|
||||
/// let mut elem = Element::bare("node");
|
||||
///
|
||||
/// elem.append_comment_node("comment");
|
||||
/// ```
|
||||
#[cfg(feature = "comments")]
|
||||
pub fn append_comment_node<S: Into<String>>(&mut self, child: S) {
|
||||
self.children.push(Node::Comment(child.into()));
|
||||
}
|
||||
|
||||
/// Appends a node to an `Element`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use minidom::{Element, Node};
|
||||
///
|
||||
/// let mut elem = Element::bare("node");
|
||||
///
|
||||
/// elem.append_node(Node::Text("hello".to_owned()));
|
||||
///
|
||||
/// assert_eq!(elem.text(), "hello");
|
||||
/// ```
|
||||
pub fn append_node(&mut self, node: Node) {
|
||||
self.children.push(node);
|
||||
}
|
||||
|
||||
/// Returns the concatenation of all text nodes in the `Element`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use minidom::Element;
|
||||
///
|
||||
/// let elem: Element = "<node>hello,<split /> world!</node>".parse().unwrap();
|
||||
///
|
||||
/// assert_eq!(elem.text(), "hello, world!");
|
||||
/// ```
|
||||
pub fn text(&self) -> String {
|
||||
self.texts().fold(String::new(), |ret, new| ret + new)
|
||||
}
|
||||
|
||||
/// Returns a reference to the first child element with the specific name and namespace, if it
|
||||
/// exists in the direct descendants of this `Element`, else returns `None`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use minidom::Element;
|
||||
///
|
||||
/// let elem: Element = r#"<node xmlns="ns"><a /><a xmlns="other_ns" /><b /></node>"#.parse().unwrap();
|
||||
///
|
||||
/// assert!(elem.get_child("a", "ns").unwrap().is("a", "ns"));
|
||||
/// assert!(elem.get_child("a", "other_ns").unwrap().is("a", "other_ns"));
|
||||
/// assert!(elem.get_child("b", "ns").unwrap().is("b", "ns"));
|
||||
/// assert_eq!(elem.get_child("c", "ns"), None);
|
||||
/// assert_eq!(elem.get_child("b", "other_ns"), None);
|
||||
/// assert_eq!(elem.get_child("a", "inexistent_ns"), None);
|
||||
/// ```
|
||||
pub fn get_child<N: AsRef<str>, NS: AsRef<str>>(&self, name: N, namespace: NS) -> Option<&Element> {
|
||||
for fork in &self.children {
|
||||
if let Node::Element(ref e) = *fork {
|
||||
if e.is(name.as_ref(), namespace.as_ref()) {
|
||||
return Some(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the first child element with the specific name and namespace,
|
||||
/// if it exists in the direct descendants of this `Element`, else returns `None`.
|
||||
pub fn get_child_mut<N: AsRef<str>, NS: AsRef<str>>(&mut self, name: N, namespace: NS) -> Option<&mut Element> {
|
||||
for fork in &mut self.children {
|
||||
if let Node::Element(ref mut e) = *fork {
|
||||
if e.is(name.as_ref(), namespace.as_ref()) {
|
||||
return Some(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns whether a specific child with this name and namespace exists in the direct
|
||||
/// descendants of the `Element`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use minidom::Element;
|
||||
///
|
||||
/// let elem: Element = r#"<node xmlns="ns"><a /><a xmlns="other_ns" /><b /></node>"#.parse().unwrap();
|
||||
///
|
||||
/// assert_eq!(elem.has_child("a", "other_ns"), true);
|
||||
/// assert_eq!(elem.has_child("a", "ns"), true);
|
||||
/// assert_eq!(elem.has_child("a", "inexistent_ns"), false);
|
||||
/// assert_eq!(elem.has_child("b", "ns"), true);
|
||||
/// assert_eq!(elem.has_child("b", "other_ns"), false);
|
||||
/// assert_eq!(elem.has_child("b", "inexistent_ns"), false);
|
||||
/// ```
|
||||
pub fn has_child<N: AsRef<str>, NS: AsRef<str>>(&self, name: N, namespace: NS) -> bool {
|
||||
self.get_child(name, namespace).is_some()
|
||||
}
|
||||
|
||||
/// Removes the first child with this name and namespace, if it exists, and returns an
|
||||
/// `Option<Element>` containing this child if it succeeds.
|
||||
/// Returns `None` if no child matches this name and namespace.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use minidom::Element;
|
||||
///
|
||||
/// let mut elem: Element = r#"<node xmlns="ns"><a /><a xmlns="other_ns" /><b /></node>"#.parse().unwrap();
|
||||
///
|
||||
/// assert!(elem.remove_child("a", "ns").unwrap().is("a", "ns"));
|
||||
/// assert!(elem.remove_child("a", "ns").is_none());
|
||||
/// assert!(elem.remove_child("inexistent", "inexistent").is_none());
|
||||
/// ```
|
||||
pub fn remove_child<N: AsRef<str>, NS: AsRef<str>>(&mut self, name: N, namespace: NS) -> Option<Element> {
|
||||
let name = name.as_ref();
|
||||
let namespace = namespace.as_ref();
|
||||
let idx = self.children.iter().position(|x| {
|
||||
if let Node::Element(ref elm) = x {
|
||||
elm.is(name, namespace)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})?;
|
||||
self.children.remove(idx).into_element()
|
||||
}
|
||||
}
|
||||
|
||||
fn split_element_name<S: AsRef<str>>(s: S) -> Result<(Option<String>, String)> {
|
||||
let name_parts = s.as_ref().split(':').collect::<Vec<&str>>();
|
||||
match name_parts.len() {
|
||||
2 => Ok((Some(name_parts[0].to_owned()), name_parts[1].to_owned())),
|
||||
1 => Ok((None, name_parts[0].to_owned())),
|
||||
_ => Err(Error::InvalidElement),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_element<R: BufRead>(reader: &EventReader<R>, event: &BytesStart) -> Result<Element> {
|
||||
let mut namespaces = BTreeMap::new();
|
||||
let attributes = event.attributes()
|
||||
.map(|o| {
|
||||
let o = o?;
|
||||
let key = str::from_utf8(o.key)?.to_owned();
|
||||
let value = o.unescape_and_decode_value(reader)?;
|
||||
Ok((key, value))
|
||||
})
|
||||
.filter(|o| {
|
||||
match *o {
|
||||
Ok((ref key, ref value)) if key == "xmlns" => {
|
||||
namespaces.insert(None, value.to_owned());
|
||||
false
|
||||
},
|
||||
Ok((ref key, ref value)) if key.starts_with("xmlns:") => {
|
||||
namespaces.insert(Some(key[6..].to_owned()), value.to_owned());
|
||||
false
|
||||
},
|
||||
_ => true,
|
||||
}
|
||||
})
|
||||
.collect::<Result<BTreeMap<String, String>>>()?;
|
||||
|
||||
let (prefix, name) = split_element_name(str::from_utf8(event.name())?)?;
|
||||
let element = Element::new(name, prefix, namespaces, attributes, Vec::new());
|
||||
Ok(element)
|
||||
}
|
||||
|
||||
/// An iterator over references to child elements of an `Element`.
|
||||
pub struct Children<'a> {
|
||||
iter: slice::Iter<'a, Node>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for Children<'a> {
|
||||
type Item = &'a Element;
|
||||
|
||||
fn next(&mut self) -> Option<&'a Element> {
|
||||
for item in &mut self.iter {
|
||||
if let Node::Element(ref child) = *item {
|
||||
return Some(child);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator over mutable references to child elements of an `Element`.
|
||||
pub struct ChildrenMut<'a> {
|
||||
iter: slice::IterMut<'a, Node>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for ChildrenMut<'a> {
|
||||
type Item = &'a mut Element;
|
||||
|
||||
fn next(&mut self) -> Option<&'a mut Element> {
|
||||
for item in &mut self.iter {
|
||||
if let Node::Element(ref mut child) = *item {
|
||||
return Some(child);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator over references to child text nodes of an `Element`.
|
||||
pub struct Texts<'a> {
|
||||
iter: slice::Iter<'a, Node>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for Texts<'a> {
|
||||
type Item = &'a str;
|
||||
|
||||
fn next(&mut self) -> Option<&'a str> {
|
||||
for item in &mut self.iter {
|
||||
if let Node::Text(ref child) = *item {
|
||||
return Some(child);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator over mutable references to child text nodes of an `Element`.
|
||||
pub struct TextsMut<'a> {
|
||||
iter: slice::IterMut<'a, Node>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for TextsMut<'a> {
|
||||
type Item = &'a mut String;
|
||||
|
||||
fn next(&mut self) -> Option<&'a mut String> {
|
||||
for item in &mut self.iter {
|
||||
if let Node::Text(ref mut child) = *item {
|
||||
return Some(child);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator over references to all child nodes of an `Element`.
|
||||
pub type Nodes<'a> = slice::Iter<'a, Node>;
|
||||
|
||||
/// An iterator over mutable references to all child nodes of an `Element`.
|
||||
pub type NodesMut<'a> = slice::IterMut<'a, Node>;
|
||||
|
||||
/// An iterator over the attributes of an `Element`.
|
||||
pub struct Attrs<'a> {
|
||||
iter: btree_map::Iter<'a, String, String>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for Attrs<'a> {
|
||||
type Item = (&'a str, &'a str);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.iter.next().map(|(x, y)| (x.as_ref(), y.as_ref()))
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator over the attributes of an `Element`, with the values mutable.
|
||||
pub struct AttrsMut<'a> {
|
||||
iter: btree_map::IterMut<'a, String, String>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for AttrsMut<'a> {
|
||||
type Item = (&'a str, &'a mut String);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.iter.next().map(|(x, y)| (x.as_ref(), y))
|
||||
}
|
||||
}
|
||||
|
||||
/// A builder for `Element`s.
|
||||
pub struct ElementBuilder {
|
||||
root: Element,
|
||||
namespaces: BTreeMap<Option<String>, String>,
|
||||
}
|
||||
|
||||
impl ElementBuilder {
|
||||
/// Sets the namespace.
|
||||
pub fn ns<S: Into<String>>(mut self, namespace: S) -> ElementBuilder {
|
||||
self.namespaces
|
||||
.insert(self.root.prefix.clone(), namespace.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets an attribute.
|
||||
pub fn attr<S: Into<String>, V: IntoAttributeValue>(mut self, name: S, value: V) -> ElementBuilder {
|
||||
self.root.set_attr(name, value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Appends anything implementing `Into<Node>` into the tree.
|
||||
pub fn append<T: Into<Node>>(mut self, node: T) -> ElementBuilder {
|
||||
self.root.append_node(node.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Appends an iterator of things implementing `Into<Node>` into the tree.
|
||||
pub fn append_all<T: Into<Node>, I: IntoIterator<Item = T>>(mut self, iter: I) -> ElementBuilder {
|
||||
for node in iter {
|
||||
self.root.append_node(node.into());
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Builds the `Element`.
|
||||
pub fn build(self) -> Element {
|
||||
let mut element = self.root;
|
||||
// Set namespaces
|
||||
element.namespaces = Rc::new(NamespaceSet::from(self.namespaces));
|
||||
// Propagate namespaces
|
||||
for node in &element.children {
|
||||
if let Node::Element(ref e) = *node {
|
||||
e.namespaces.set_parent(Rc::clone(&element.namespaces));
|
||||
}
|
||||
}
|
||||
element
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_element_new() {
|
||||
use std::iter::FromIterator;
|
||||
|
||||
let elem = Element::new( "name".to_owned()
|
||||
, None
|
||||
, Some("namespace".to_owned())
|
||||
, BTreeMap::from_iter(vec![ ("name".to_string(), "value".to_string()) ].into_iter() )
|
||||
, Vec::new() );
|
||||
|
||||
assert_eq!(elem.name(), "name");
|
||||
assert_eq!(elem.ns(), Some("namespace".to_owned()));
|
||||
assert_eq!(elem.attr("name"), Some("value"));
|
||||
assert_eq!(elem.attr("inexistent"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_reader_simple() {
|
||||
let xml = "<foo></foo>";
|
||||
let mut reader = EventReader::from_str(xml);
|
||||
let elem = Element::from_reader(&mut reader);
|
||||
|
||||
let elem2 = Element::builder("foo").build();
|
||||
|
||||
assert_eq!(elem.unwrap(), elem2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_reader_nested() {
|
||||
let xml = "<foo><bar baz='qxx' /></foo>";
|
||||
let mut reader = EventReader::from_str(xml);
|
||||
let elem = Element::from_reader(&mut reader);
|
||||
|
||||
let nested = Element::builder("bar")
|
||||
.attr("baz", "qxx")
|
||||
.build();
|
||||
let elem2 = Element::builder("foo")
|
||||
.append(nested)
|
||||
.build();
|
||||
|
||||
assert_eq!(elem.unwrap(), elem2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_reader_with_prefix() {
|
||||
let xml = "<foo><prefix:bar baz='qxx' /></foo>";
|
||||
let mut reader = EventReader::from_str(xml);
|
||||
let elem = Element::from_reader(&mut reader);
|
||||
|
||||
let nested = Element::builder("prefix:bar")
|
||||
.attr("baz", "qxx")
|
||||
.build();
|
||||
let elem2 = Element::builder("foo")
|
||||
.append(nested)
|
||||
.build();
|
||||
|
||||
assert_eq!(elem.unwrap(), elem2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_spectest_xml() { // From: https://gitlab.com/lumi/minidom-rs/issues/8
|
||||
let xml = r#"
|
||||
<rng:grammar xmlns:rng="http://relaxng.org/ns/structure/1.0">
|
||||
<rng:name xmlns:rng="http://relaxng.org/ns/structure/1.0"></rng:name>
|
||||
</rng:grammar>
|
||||
"#;
|
||||
let mut reader = EventReader::from_str(xml);
|
||||
let _ = Element::from_reader(&mut reader).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn does_not_unescape_cdata() {
|
||||
let xml = "<test><![CDATA['>blah<blah>]]></test>";
|
||||
let mut reader = EventReader::from_str(xml);
|
||||
let elem = Element::from_reader(&mut reader).unwrap();
|
||||
assert_eq!(elem.text(), "'>blah<blah>");
|
||||
}
|
||||
}
|
||||
83
minidom-rs/src/error.rs
Normal file
83
minidom-rs/src/error.rs
Normal file
|
|
@ -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>;
|
||||
80
minidom-rs/src/lib.rs
Normal file
80
minidom-rs/src/lib.rs
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
#![deny(missing_docs)]
|
||||
|
||||
//! A minimal DOM crate built on top of quick-xml.
|
||||
//!
|
||||
//! This library exports an `Element` struct which represents a DOM tree.
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! Run with `cargo run --example articles`. Located in `examples/articles.rs`.
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
//! extern crate minidom;
|
||||
//!
|
||||
//! use minidom::Element;
|
||||
//!
|
||||
//! const DATA: &'static str = r#"<articles xmlns="article">
|
||||
//! <article>
|
||||
//! <title>10 Terrible Bugs You Would NEVER Believe Happened</title>
|
||||
//! <body>
|
||||
//! Rust fixed them all. <3
|
||||
//! </body>
|
||||
//! </article>
|
||||
//! <article>
|
||||
//! <title>BREAKING NEWS: Physical Bug Jumps Out Of Programmer's Screen</title>
|
||||
//! <body>
|
||||
//! Just kidding!
|
||||
//! </body>
|
||||
//! </article>
|
||||
//! </articles>"#;
|
||||
//!
|
||||
//! const ARTICLE_NS: &'static str = "article";
|
||||
//!
|
||||
//! #[derive(Debug)]
|
||||
//! pub struct Article {
|
||||
//! title: String,
|
||||
//! body: String,
|
||||
//! }
|
||||
//!
|
||||
//! fn main() {
|
||||
//! let root: Element = DATA.parse().unwrap();
|
||||
//!
|
||||
//! let mut articles: Vec<Article> = Vec::new();
|
||||
//!
|
||||
//! for child in root.children() {
|
||||
//! if child.is("article", ARTICLE_NS) {
|
||||
//! let title = child.get_child("title", ARTICLE_NS).unwrap().text();
|
||||
//! let body = child.get_child("body", ARTICLE_NS).unwrap().text();
|
||||
//! articles.push(Article {
|
||||
//! title: title,
|
||||
//! body: body.trim().to_owned(),
|
||||
//! });
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! println!("{:?}", articles);
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! # Usage
|
||||
//!
|
||||
//! To use `minidom`, add this to your `Cargo.toml` under `dependencies`:
|
||||
//!
|
||||
//! ```toml,ignore
|
||||
//! minidom = "*"
|
||||
//! ```
|
||||
|
||||
pub use quick_xml;
|
||||
|
||||
pub mod error;
|
||||
pub mod element;
|
||||
pub mod convert;
|
||||
pub mod node;
|
||||
mod namespace_set;
|
||||
|
||||
#[cfg(test)] mod tests;
|
||||
|
||||
pub use error::{Error, Result};
|
||||
pub use element::{Element, Children, ChildrenMut, ElementBuilder};
|
||||
pub use node::Node;
|
||||
pub use convert::IntoAttributeValue;
|
||||
170
minidom-rs/src/namespace_set.rs
Normal file
170
minidom-rs/src/namespace_set.rs
Normal file
|
|
@ -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)))");
|
||||
}
|
||||
}
|
||||
207
minidom-rs/src/node.rs
Normal file
207
minidom-rs/src/node.rs
Normal file
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
251
minidom-rs/src/tests.rs
Normal file
251
minidom-rs/src/tests.rs
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
use crate::element::Element;
|
||||
|
||||
use quick_xml::Reader;
|
||||
|
||||
const TEST_STRING: &'static str = r#"<?xml version="1.0" encoding="utf-8"?><root xmlns="root_ns" a="b" xml:lang="en">meow<child c="d"/><child xmlns="child_ns" d="e" xml:lang="fr"/>nya</root>"#;
|
||||
|
||||
fn build_test_tree() -> Element {
|
||||
let mut root = Element::builder("root")
|
||||
.ns("root_ns")
|
||||
.attr("xml:lang", "en")
|
||||
.attr("a", "b")
|
||||
.build();
|
||||
root.append_text_node("meow");
|
||||
let child = Element::builder("child")
|
||||
.attr("c", "d")
|
||||
.build();
|
||||
root.append_child(child);
|
||||
let other_child = Element::builder("child")
|
||||
.ns("child_ns")
|
||||
.attr("d", "e")
|
||||
.attr("xml:lang", "fr")
|
||||
.build();
|
||||
root.append_child(other_child);
|
||||
root.append_text_node("nya");
|
||||
root
|
||||
}
|
||||
|
||||
#[cfg(feature = "comments")]
|
||||
const COMMENT_TEST_STRING: &'static str = r#"<?xml version="1.0" encoding="utf-8"?><root><!--This is a child.--><child attr="val"><!--This is a grandchild.--><grandchild/></child></root>"#;
|
||||
|
||||
#[cfg(feature = "comments")]
|
||||
fn build_comment_test_tree() -> Element {
|
||||
let mut root = Element::builder("root").build();
|
||||
root.append_comment_node("This is a child.");
|
||||
let mut child = Element::builder("child").attr("attr", "val").build();
|
||||
child.append_comment_node("This is a grandchild.");
|
||||
let grand_child = Element::builder("grandchild").build();
|
||||
child.append_child(grand_child);
|
||||
root.append_child(child);
|
||||
root
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reader_works() {
|
||||
let mut reader = Reader::from_str(TEST_STRING);
|
||||
assert_eq!(Element::from_reader(&mut reader).unwrap(), build_test_tree());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn writer_works() {
|
||||
let root = build_test_tree();
|
||||
let mut writer = Vec::new();
|
||||
{
|
||||
root.write_to(&mut writer).unwrap();
|
||||
}
|
||||
assert_eq!(String::from_utf8(writer).unwrap(), TEST_STRING);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn writer_escapes_attributes() {
|
||||
let root = Element::builder("root")
|
||||
.attr("a", "\"Air\" quotes")
|
||||
.build();
|
||||
let mut writer = Vec::new();
|
||||
{
|
||||
root.write_to(&mut writer).unwrap();
|
||||
}
|
||||
assert_eq!(String::from_utf8(writer).unwrap(),
|
||||
r#"<?xml version="1.0" encoding="utf-8"?><root a=""Air" quotes"/>"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn writer_escapes_text() {
|
||||
let root = Element::builder("root")
|
||||
.append("<3")
|
||||
.build();
|
||||
let mut writer = Vec::new();
|
||||
{
|
||||
root.write_to(&mut writer).unwrap();
|
||||
}
|
||||
assert_eq!(String::from_utf8(writer).unwrap(),
|
||||
r#"<?xml version="1.0" encoding="utf-8"?><root><3</root>"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builder_works() {
|
||||
let elem = Element::builder("a")
|
||||
.ns("b")
|
||||
.attr("c", "d")
|
||||
.append(Element::builder("child"))
|
||||
.append("e")
|
||||
.build();
|
||||
assert_eq!(elem.name(), "a");
|
||||
assert_eq!(elem.ns(), Some("b".to_owned()));
|
||||
assert_eq!(elem.attr("c"), Some("d"));
|
||||
assert_eq!(elem.attr("x"), None);
|
||||
assert_eq!(elem.text(), "e");
|
||||
assert!(elem.has_child("child", "b"));
|
||||
assert!(elem.is("a", "b"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn children_iter_works() {
|
||||
let root = build_test_tree();
|
||||
let mut iter = root.children();
|
||||
assert!(iter.next().unwrap().is("child", "root_ns"));
|
||||
assert!(iter.next().unwrap().is("child", "child_ns"));
|
||||
assert_eq!(iter.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_child_works() {
|
||||
let root = build_test_tree();
|
||||
assert_eq!(root.get_child("child", "inexistent_ns"), None);
|
||||
assert_eq!(root.get_child("not_a_child", "root_ns"), None);
|
||||
assert!(root.get_child("child", "root_ns").unwrap().is("child", "root_ns"));
|
||||
assert!(root.get_child("child", "child_ns").unwrap().is("child", "child_ns"));
|
||||
assert_eq!(root.get_child("child", "root_ns").unwrap().attr("c"), Some("d"));
|
||||
assert_eq!(root.get_child("child", "child_ns").unwrap().attr("d"), Some("e"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn namespace_propagation_works() {
|
||||
let mut root = Element::builder("root").ns("root_ns").build();
|
||||
let mut child = Element::bare("child");
|
||||
let grandchild = Element::bare("grandchild");
|
||||
child.append_child(grandchild);
|
||||
root.append_child(child);
|
||||
|
||||
assert_eq!(root.get_child("child", "root_ns").unwrap().ns(), root.ns());
|
||||
assert_eq!(root.get_child("child", "root_ns").unwrap()
|
||||
.get_child("grandchild", "root_ns").unwrap()
|
||||
.ns(), root.ns());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_elements_with_same_arguments_different_order_are_equal() {
|
||||
let elem1: Element = "<a b='a' c=''/>".parse().unwrap();
|
||||
let elem2: Element = "<a c='' b='a'/>".parse().unwrap();
|
||||
assert_eq!(elem1, elem2);
|
||||
|
||||
let elem1: Element = "<a b='a' c=''/>".parse().unwrap();
|
||||
let elem2: Element = "<a c='d' b='a'/>".parse().unwrap();
|
||||
assert_ne!(elem1, elem2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn namespace_attributes_works() {
|
||||
let mut reader = Reader::from_str(TEST_STRING);
|
||||
let root = Element::from_reader(&mut reader).unwrap();
|
||||
assert_eq!("en", root.attr("xml:lang").unwrap());
|
||||
assert_eq!("fr", root.get_child("child", "child_ns").unwrap().attr("xml:lang").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrongly_closed_elements_error() {
|
||||
let elem1 = "<a></b>".parse::<Element>();
|
||||
assert!(elem1.is_err());
|
||||
let elem1 = "<a></c></a>".parse::<Element>();
|
||||
assert!(elem1.is_err());
|
||||
let elem1 = "<a><c><d/></c></a>".parse::<Element>();
|
||||
assert!(elem1.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn namespace_simple() {
|
||||
let elem: Element = "<message xmlns='jabber:client'/>".parse().unwrap();
|
||||
assert_eq!(elem.name(), "message");
|
||||
assert_eq!(elem.ns(), Some("jabber:client".to_owned()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn namespace_prefixed() {
|
||||
let elem: Element = "<stream:features xmlns:stream='http://etherx.jabber.org/streams'/>"
|
||||
.parse().unwrap();
|
||||
assert_eq!(elem.name(), "features");
|
||||
assert_eq!(elem.ns(), Some("http://etherx.jabber.org/streams".to_owned()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn namespace_inherited_simple() {
|
||||
let elem: Element = "<stream xmlns='jabber:client'><message/></stream>".parse().unwrap();
|
||||
assert_eq!(elem.name(), "stream");
|
||||
assert_eq!(elem.ns(), Some("jabber:client".to_owned()));
|
||||
let child = elem.children().next().unwrap();
|
||||
assert_eq!(child.name(), "message");
|
||||
assert_eq!(child.ns(), Some("jabber:client".to_owned()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn namespace_inherited_prefixed1() {
|
||||
let elem: Element = "<stream:features xmlns:stream='http://etherx.jabber.org/streams' xmlns='jabber:client'><message/></stream:features>"
|
||||
.parse().unwrap();
|
||||
assert_eq!(elem.name(), "features");
|
||||
assert_eq!(elem.ns(), Some("http://etherx.jabber.org/streams".to_owned()));
|
||||
let child = elem.children().next().unwrap();
|
||||
assert_eq!(child.name(), "message");
|
||||
assert_eq!(child.ns(), Some("jabber:client".to_owned()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn namespace_inherited_prefixed2() {
|
||||
let elem: Element = "<stream xmlns='http://etherx.jabber.org/streams' xmlns:jabber='jabber:client'><jabber:message/></stream>"
|
||||
.parse().unwrap();
|
||||
assert_eq!(elem.name(), "stream");
|
||||
assert_eq!(elem.ns(), Some("http://etherx.jabber.org/streams".to_owned()));
|
||||
let child = elem.children().next().unwrap();
|
||||
assert_eq!(child.name(), "message");
|
||||
assert_eq!(child.ns(), Some("jabber:client".to_owned()));
|
||||
}
|
||||
|
||||
#[cfg(feature = "comments")]
|
||||
#[test]
|
||||
fn read_comments() {
|
||||
let mut reader = Reader::from_str(COMMENT_TEST_STRING);
|
||||
assert_eq!(Element::from_reader(&mut reader).unwrap(), build_comment_test_tree());
|
||||
}
|
||||
|
||||
#[cfg(feature = "comments")]
|
||||
#[test]
|
||||
fn write_comments() {
|
||||
let root = build_comment_test_tree();
|
||||
let mut writer = Vec::new();
|
||||
{
|
||||
root.write_to(&mut writer).unwrap();
|
||||
}
|
||||
assert_eq!(String::from_utf8(writer).unwrap(), COMMENT_TEST_STRING);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xml_error() {
|
||||
match "<a></b>".parse::<Element>() {
|
||||
Err(crate::error::Error::XmlError(_)) => (),
|
||||
err => panic!("No or wrong error: {:?}", err)
|
||||
}
|
||||
|
||||
match "<a></".parse::<Element>() {
|
||||
Err(crate::error::Error::XmlError(_)) => (),
|
||||
err => panic!("No or wrong error: {:?}", err)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_element_error() {
|
||||
match "<a:b:c>".parse::<Element>() {
|
||||
Err(crate::error::Error::InvalidElement) => (),
|
||||
err => panic!("No or wrong error: {:?}", err)
|
||||
}
|
||||
}
|
||||
1
tokio-xmpp/.gitignore
vendored
Normal file
1
tokio-xmpp/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
target
|
||||
14
tokio-xmpp/.gitlab-ci.yml
Normal file
14
tokio-xmpp/.gitlab-ci.yml
Normal file
|
|
@ -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
|
||||
28
tokio-xmpp/Cargo.toml
Normal file
28
tokio-xmpp/Cargo.toml
Normal file
|
|
@ -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"
|
||||
6
tokio-xmpp/README.md
Normal file
6
tokio-xmpp/README.md
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# TODO
|
||||
|
||||
- [ ] minidom ns
|
||||
- [ ] replace debug output with log crate
|
||||
- [ ] customize tls verify?
|
||||
- [ ] more tests
|
||||
130
tokio-xmpp/examples/contact_addr.rs
Normal file
130
tokio-xmpp/examples/contact_addr.rs
Normal file
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
232
tokio-xmpp/examples/download_avatars.rs
Normal file
232
tokio-xmpp/examples/download_avatars.rs
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
use futures::{future, Future, Sink, Stream};
|
||||
use std::convert::TryFrom;
|
||||
use std::env::args;
|
||||
use std::fs::{create_dir_all, File};
|
||||
use std::io::{self, Write};
|
||||
use std::process::exit;
|
||||
use std::str::FromStr;
|
||||
use tokio::runtime::current_thread::Runtime;
|
||||
use tokio_xmpp::{Client, Packet};
|
||||
use xmpp_parsers::{
|
||||
avatar::{Data as AvatarData, Metadata as AvatarMetadata},
|
||||
caps::{compute_disco, hash_caps, Caps},
|
||||
disco::{DiscoInfoQuery, DiscoInfoResult, Feature, Identity},
|
||||
hashes::Algo,
|
||||
iq::{Iq, IqType},
|
||||
message::Message,
|
||||
ns,
|
||||
presence::{Presence, Type as PresenceType},
|
||||
pubsub::{
|
||||
event::PubSubEvent,
|
||||
pubsub::{Items, PubSub},
|
||||
NodeName,
|
||||
},
|
||||
stanza_error::{StanzaError, ErrorType, DefinedCondition},
|
||||
Jid,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = args().collect();
|
||||
if args.len() != 3 {
|
||||
println!("Usage: {} <jid> <password>", args[0]);
|
||||
exit(1);
|
||||
}
|
||||
let jid = &args[1];
|
||||
let password = &args[2];
|
||||
|
||||
// tokio_core context
|
||||
let mut rt = Runtime::new().unwrap();
|
||||
// Client instance
|
||||
let client = Client::new(jid, password).unwrap();
|
||||
|
||||
// Make the two interfaces for sending and receiving independent
|
||||
// of each other so we can move one into a closure.
|
||||
let (sink, stream) = client.split();
|
||||
|
||||
// Create outgoing pipe
|
||||
let (mut tx, rx) = futures::unsync::mpsc::unbounded();
|
||||
rt.spawn(
|
||||
rx.forward(
|
||||
sink.sink_map_err(|_| panic!("Pipe"))
|
||||
)
|
||||
.map(|(rx, mut sink)| {
|
||||
drop(rx);
|
||||
let _ = sink.close();
|
||||
})
|
||||
.map_err(|e| {
|
||||
panic!("Send error: {:?}", e);
|
||||
})
|
||||
);
|
||||
|
||||
let disco_info = make_disco();
|
||||
|
||||
// Main loop, processes events
|
||||
let mut wait_for_stream_end = false;
|
||||
let done = stream.for_each(move |event| {
|
||||
// Helper function to send an iq error.
|
||||
let mut send_error = |to, id, type_, condition, text: &str| {
|
||||
let error = StanzaError::new(type_, condition, "en", text);
|
||||
let iq = Iq::from_error(id, error)
|
||||
.with_to(to);
|
||||
tx.start_send(Packet::Stanza(iq.into())).unwrap();
|
||||
};
|
||||
|
||||
if wait_for_stream_end {
|
||||
/* Do nothing */
|
||||
} else if event.is_online() {
|
||||
println!("Online!");
|
||||
|
||||
let caps = get_disco_caps(&disco_info, "https://gitlab.com/xmpp-rs/tokio-xmpp");
|
||||
let presence = make_presence(caps);
|
||||
tx.start_send(Packet::Stanza(presence.into())).unwrap();
|
||||
} else if let Some(stanza) = event.into_stanza() {
|
||||
if stanza.is("iq", "jabber:client") {
|
||||
let iq = Iq::try_from(stanza).unwrap();
|
||||
if let IqType::Get(payload) = iq.payload {
|
||||
if payload.is("query", ns::DISCO_INFO) {
|
||||
let query = DiscoInfoQuery::try_from(payload);
|
||||
match query {
|
||||
Ok(query) => {
|
||||
let mut disco = disco_info.clone();
|
||||
disco.node = query.node;
|
||||
let iq = Iq::from_result(iq.id, Some(disco))
|
||||
.with_to(iq.from.unwrap());
|
||||
tx.start_send(Packet::Stanza(iq.into())).unwrap();
|
||||
},
|
||||
Err(err) => {
|
||||
send_error(iq.from.unwrap(), iq.id, ErrorType::Modify, DefinedCondition::BadRequest, &format!("{}", err));
|
||||
},
|
||||
}
|
||||
} else {
|
||||
// We MUST answer unhandled get iqs with a service-unavailable error.
|
||||
send_error(iq.from.unwrap(), iq.id, ErrorType::Cancel, DefinedCondition::ServiceUnavailable, "No handler defined for this kind of iq.");
|
||||
}
|
||||
} else if let IqType::Result(Some(payload)) = iq.payload {
|
||||
if payload.is("pubsub", ns::PUBSUB) {
|
||||
let pubsub = PubSub::try_from(payload).unwrap();
|
||||
let from =
|
||||
iq.from.clone().unwrap_or(Jid::from_str(jid).unwrap());
|
||||
handle_iq_result(pubsub, &from);
|
||||
}
|
||||
} else if let IqType::Set(_) = iq.payload {
|
||||
// We MUST answer unhandled set iqs with a service-unavailable error.
|
||||
send_error(iq.from.unwrap(), iq.id, ErrorType::Cancel, DefinedCondition::ServiceUnavailable, "No handler defined for this kind of iq.");
|
||||
}
|
||||
} else if stanza.is("message", "jabber:client") {
|
||||
let message = Message::try_from(stanza).unwrap();
|
||||
let from = message.from.clone().unwrap();
|
||||
if let Some(body) = message.get_best_body(vec!["en"]) {
|
||||
if body.1 .0 == "die" {
|
||||
println!("Secret die command triggered by {}", from);
|
||||
wait_for_stream_end = true;
|
||||
tx.start_send(Packet::StreamEnd).unwrap();
|
||||
}
|
||||
}
|
||||
for child in message.payloads {
|
||||
if child.is("event", ns::PUBSUB_EVENT) {
|
||||
let event = PubSubEvent::try_from(child).unwrap();
|
||||
if let PubSubEvent::PublishedItems { node, items } = event {
|
||||
if node.0 == ns::AVATAR_METADATA {
|
||||
for item in items.into_iter() {
|
||||
let payload = item.payload.clone().unwrap();
|
||||
if payload.is("metadata", ns::AVATAR_METADATA) {
|
||||
// TODO: do something with these metadata.
|
||||
let _metadata = AvatarMetadata::try_from(payload).unwrap();
|
||||
println!(
|
||||
"[1m{}[0m has published an avatar, downloading...",
|
||||
from.clone()
|
||||
);
|
||||
let iq = download_avatar(from.clone());
|
||||
tx.start_send(Packet::Stanza(iq.into())).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if stanza.is("presence", "jabber:client") {
|
||||
// Nothing to do here.
|
||||
} else {
|
||||
panic!("Unknown stanza: {}", String::from(&stanza));
|
||||
}
|
||||
}
|
||||
|
||||
future::ok(())
|
||||
});
|
||||
|
||||
// Start polling `done`
|
||||
match rt.block_on(done) {
|
||||
Ok(_) => (),
|
||||
Err(e) => {
|
||||
println!("Fatal: {}", e);
|
||||
()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn make_disco() -> DiscoInfoResult {
|
||||
let identities = vec![Identity::new("client", "bot", "en", "tokio-xmpp")];
|
||||
let features = vec![
|
||||
Feature::new(ns::DISCO_INFO),
|
||||
Feature::new(format!("{}+notify", ns::AVATAR_METADATA)),
|
||||
];
|
||||
DiscoInfoResult {
|
||||
node: None,
|
||||
identities,
|
||||
features,
|
||||
extensions: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn get_disco_caps(disco: &DiscoInfoResult, node: &str) -> Caps {
|
||||
let caps_data = compute_disco(disco);
|
||||
let hash = hash_caps(&caps_data, Algo::Sha_1).unwrap();
|
||||
Caps::new(node, hash)
|
||||
}
|
||||
|
||||
// Construct a <presence/>
|
||||
fn make_presence(caps: Caps) -> Presence {
|
||||
let mut presence = Presence::new(PresenceType::None)
|
||||
.with_priority(-1);
|
||||
presence.set_status("en", "Downloading avatars.");
|
||||
presence.add_payload(caps);
|
||||
presence
|
||||
}
|
||||
|
||||
fn download_avatar(from: Jid) -> Iq {
|
||||
Iq::from_get("coucou", PubSub::Items(Items {
|
||||
max_items: None,
|
||||
node: NodeName(String::from(ns::AVATAR_DATA)),
|
||||
subid: None,
|
||||
items: Vec::new(),
|
||||
}))
|
||||
.with_to(from)
|
||||
}
|
||||
|
||||
fn handle_iq_result(pubsub: PubSub, from: &Jid) {
|
||||
if let PubSub::Items(items) = pubsub {
|
||||
if items.node.0 == ns::AVATAR_DATA {
|
||||
for item in items.items {
|
||||
match (item.id.clone(), item.payload.clone()) {
|
||||
(Some(id), Some(payload)) => {
|
||||
let data = AvatarData::try_from(payload).unwrap();
|
||||
save_avatar(from, id.0, &data.data).unwrap();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn save_avatar(from: &Jid, id: String, data: &[u8]) -> io::Result<()> {
|
||||
let directory = format!("data/{}", from);
|
||||
let filename = format!("data/{}/{}", from, id);
|
||||
println!(
|
||||
"Saving avatar from [1m{}[0m to [4m{}[0m.",
|
||||
from, filename
|
||||
);
|
||||
create_dir_all(directory)?;
|
||||
let mut file = File::create(filename)?;
|
||||
file.write_all(data)
|
||||
}
|
||||
106
tokio-xmpp/examples/echo_bot.rs
Normal file
106
tokio-xmpp/examples/echo_bot.rs
Normal file
|
|
@ -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()
|
||||
}
|
||||
99
tokio-xmpp/examples/echo_component.rs
Normal file
99
tokio-xmpp/examples/echo_component.rs
Normal file
|
|
@ -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()
|
||||
}
|
||||
172
tokio-xmpp/logo.svg
Normal file
172
tokio-xmpp/logo.svg
Normal file
|
|
@ -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"
|
||||
d="m 314.30911,1061.7683 c -8.97073,-5.2395 -16.48089,-10.0377 -16.68926,-10.6628 -0.26982,-0.8095 166.28356,-288.11102 202.75565,-349.74973 l 2.54893,-4.30777 -9.36559,-8.39055 c -11.144,-9.98383 -22.35733,-21.92978 -28.0257,-29.85676 -2.25924,-3.15946 -4.2669,-5.74448 -4.46148,-5.74448 -0.19457,0 -30.6124,17.4714 -67.59521,38.82531 -36.98279,21.35393 -67.38191,38.63271 -67.55361,38.39729 -3.29619,-4.51935 -18.81062,-33.32415 -18.43899,-34.23468 0.28394,-0.69568 26.01362,-15.92783 57.17709,-33.84921 31.16345,-17.92139 61.09657,-35.1893 66.51803,-38.37317 l 9.85719,-5.78884 -4.65622,-13.21887 c -4.07486,-11.56836 -8.04326,-28.12263 -10.25106,-42.76242 l -0.68393,-4.53511 H 224.14076 22.836563 v -19.9545 -19.9545 H 224.14076 425.44495 l 0.68393,-4.5351 c 2.20688,-14.63373 6.17521,-31.19125 10.24412,-42.74274 l 4.64929,-13.19918 -9.85025,-5.77551 c -5.41765,-3.17654 -35.35061,-20.45332 -66.51769,-38.39286 -31.16709,-17.93955 -56.89677,-33.18655 -57.17709,-33.88223 -0.36614,-0.90866 15.08023,-29.58386 18.43579,-34.22491 0.16633,-0.23003 30.60498,17.07925 67.64147,38.4651 37.03649,21.38584 67.48667,38.71678 67.66708,38.51319 0.18041,-0.20358 3.5815,-4.45175 7.55796,-9.44037 8.55901,-10.73754 23.43746,-25.50319 33.56957,-33.31503 3.99089,-3.07699 7.42447,-6.0522 7.63019,-6.6116 0.20571,-0.55942 -45.66778,-80.05268 -101.94109,-176.65173 C 349.23634,94.87494 306.17509,19.72792 306.78588,19.11713 308.19161,17.71141 338.56097,0 339.56561,0 c 0.4291,0 47.02676,79.44412 103.55039,176.54249 78.60729,135.03455 103.19671,176.3654 104.58425,175.78931 14.01846,-5.82028 30.16056,-10.04645 55.32837,-14.48557 l 2.72106,-0.47993 v -75.56217 -75.56217 h 19.9545 19.9545 v 75.56217 75.56217 l 2.72106,0.54898 c 1.49659,0.30194 7.8911,1.43066 14.21003,2.50826 6.31892,1.07761 18.17561,4.17686 26.3482,6.88724 8.1726,2.71038 15.0185,4.72127 15.21314,4.46861 0.19462,-0.25264 46.29667,-79.48313 102.44897,-176.06777 C 862.75239,79.12698 909.11172,0.07996 909.62079,0.05159 c 0.50906,-0.0284 8.26445,4.16607 17.23414,9.32099 11.34048,6.51737 16.17929,9.93831 15.88418,11.22974 -0.23343,1.02145 -46.19384,80.42165 -102.13425,176.4449 l -101.70985,174.58771 8.10553,6.13315 c 11.47436,8.68226 26.67576,23.56058 35.4381,34.68493 6.76288,8.58593 7.73722,9.38055 10.03334,8.18278 1.39869,-0.72964 30.57007,-17.49239 64.82528,-37.25058 34.25522,-19.75818 63.46697,-36.5502 64.91498,-37.3156 2.49056,-1.31647 3.16664,-0.47069 12.51549,15.65711 5.43546,9.37681 9.42076,17.4631 8.85617,17.96951 -0.5646,0.5064 -30.14195,17.6627 -65.72746,38.12508 -35.58552,20.46239 -65.35304,37.81597 -66.15006,38.56352 -1.11691,1.04762 -0.36064,4.54738 3.29904,15.26687 4.30684,12.61506 9.00358,32.4176 9.89584,41.72304 l 0.34788,3.62809 201.66136,0.30589 201.6614,0.30588 v 19.94741 19.94741 l -201.6614,0.30589 -201.66136,0.30588 -0.34788,3.62809 c -0.89828,9.3683 -5.60703,29.15966 -10.02924,42.15394 -2.68469,7.88877 -4.71343,14.45948 -4.50826,14.60158 0.20516,0.14208 30.03265,17.30357 66.28331,38.13661 36.25066,20.83304 66.37223,38.28975 66.93683,38.7927 0.56459,0.50293 -3.42071,8.58638 -8.85617,17.96319 -9.34354,16.11862 -10.02635,16.97321 -12.51549,15.66392 -1.44801,-0.76167 -30.65976,-17.55164 -64.91498,-37.31105 -34.25521,-19.75941 -63.42659,-36.52524 -64.82528,-37.25737 -2.2992,-1.20352 -3.27874,-0.39721 -10.21442,8.40809 -4.21924,5.3566 -13.12221,14.88134 -19.78439,21.16609 -10.1228,9.54933 -11.90518,11.76555 -10.84787,13.48828 1.04335,1.69992 192.71096,330.83154 202.00872,346.88934 l 2.89322,4.9967 -17.11682,9.9052 c -9.41425,5.4479 -17.52207,9.4578 -18.01738,8.9109 -0.49531,-0.5468 -46.48965,-79.36099 -102.20961,-175.14256 -55.71998,-95.78159 -101.69517,-174.5851 -102.16708,-175.11893 -0.47193,-0.53385 -4.0675,0.47777 -7.99015,2.24805 -16.83432,7.59718 -33.19012,12.19105 -60.64646,17.03382 l -2.72106,0.47994 v 75.56218 75.56216 h -19.9545 -19.9545 V 813.3199 737.75772 l -2.72106,-0.56168 c -1.49659,-0.30894 -5.44214,-0.91495 -8.76789,-1.34671 -10.96151,-1.42303 -32.49085,-7.42089 -44.73737,-12.4634 -6.62528,-2.72794 -12.43791,-4.56618 -12.91696,-4.08498 -0.47905,0.4812 -46.66546,79.51377 -102.63647,175.62793 -55.971,96.11415 -102.12208,175.11582 -102.55794,175.55932 -0.43585,0.4435 -8.13215,-3.4805 -17.10288,-8.7199 z"
|
||||
id="path4674" /><path
|
||||
style="fill:url(#radialGradient4654);fill-opacity:1;stroke-width:1.20936334"
|
||||
d="m 615.24584,987.5771 c -16.03866,-5.72964 -23.70609,-24.21313 -16.75096,-40.38075 3.16819,-7.36465 7.25094,-11.42494 14.88068,-14.79874 11.24336,-4.97174 24.69485,-2.54323 33.23901,6.00096 5.52442,5.5244 7.6728,10.22156 8.41406,18.39619 1.94732,21.47547 -19.46247,38.04153 -39.78279,30.78234 z"
|
||||
id="path4668" /><path
|
||||
style="fill:url(#radialGradient4654);fill-opacity:1;stroke-width:1.20936334"
|
||||
d="m 254.0458,776.59921 c -20.23221,-7.20535 -26.62109,-33.06165 -11.95085,-48.3661 18.83855,-19.65296 51.66062,-6.70059 51.60062,20.36281 -0.0456,20.59195 -20.2976,34.89522 -39.64977,28.00329 z"
|
||||
id="path4666" /><path
|
||||
style="fill:url(#radialGradient4654);fill-opacity:1;stroke-width:1.20936334"
|
||||
d="m 977.24507,776.53001 c -12.06564,-4.16718 -19.66881,-14.82142 -19.73015,-27.64767 -0.0405,-8.47044 2.45519,-15.09499 7.77931,-20.64927 14.76548,-15.4038 39.87537,-11.50094 48.75977,7.5788 7.7005,16.53727 -0.9274,36.08511 -18.2018,41.23878 -7.65886,2.28498 -10.73282,2.19896 -18.60713,-0.52064 z"
|
||||
id="path4664" /><path
|
||||
style="fill:url(#radialGradient4654);fill-opacity:1;stroke-width:1.20936334"
|
||||
d="m 259.07812,360.23187 c -2.76453,-0.48737 -7.11823,-1.95868 -9.6749,-3.26957 -16.90031,-8.66541 -20.31807,-33.3237 -6.52993,-47.11183 5.52441,-5.5244 10.22156,-7.67278 18.39619,-8.41404 18.41005,-1.66937 33.63973,13.36629 32.16375,31.75398 -0.67162,8.36694 -2.80945,13.19288 -8.25786,18.6413 -6.98389,6.98389 -16.59066,10.07611 -26.09725,8.40016 z"
|
||||
id="path4662" /><path
|
||||
style="fill:url(#radialGradient4654);fill-opacity:1;stroke-width:1.20936334"
|
||||
d="m 982.2774,360.23187 c -2.76454,-0.48737 -7.11825,-1.95868 -9.67491,-3.26957 -16.90031,-8.66541 -20.31806,-33.3237 -6.52993,-47.11183 5.5244,-5.5244 10.22156,-7.67278 18.3962,-8.41404 18.41004,-1.66937 33.63974,13.36629 32.16374,31.75398 -0.6716,8.36694 -2.8094,13.19288 -8.2578,18.6413 -6.9839,6.98389 -16.59071,10.07611 -26.0973,8.40016 z"
|
||||
id="path4660" /><path
|
||||
style="fill:url(#radialGradient4654);fill-opacity:1;stroke-width:1.20936334"
|
||||
d="m 620.67777,144.9652 c -2.76453,-0.48737 -7.11824,-1.95868 -9.67491,-3.26957 -16.90031,-8.66541 -20.31807,-33.3237 -6.52994,-47.11183 5.52441,-5.52441 10.22157,-7.6728 18.3962,-8.41404 18.41005,-1.66937 33.63973,13.36628 32.16375,31.75398 -0.67161,8.36694 -2.80945,13.19288 -8.25786,18.6413 -6.98388,6.98389 -16.59066,10.07611 -26.09724,8.40016 z"
|
||||
id="path4616" /><g
|
||||
id="g4646"
|
||||
transform="matrix(2.5779309,0,0,2.5779309,481.32066,2207.8469)"><path
|
||||
id="path9"
|
||||
d="m 105.84704,-690.34808 c 0.077,1.313 -1.786,0.968 -1.786,2.293 0,38.551 -44.72,96.831 -89.847,108.19 v 1.182 c 59.957,-5.51 126.73,-66.8 128.24,-125.85 l -36.6,14.189 z"
|
||||
style="fill:url(#linearGradient4656)"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
id="path11"
|
||||
d="m 89.78704,-686.57708 c 0.077,1.313 0.121,2.633 0.121,3.958 0,38.551 -30.7,90.497 -75.827,101.86 v 1.637 c 59.065,-3.823 105.81,-63.023 105.81,-109.2 0,-2.375 -0.125,-4.729 -0.371,-7.056 l -29.73,8.796 z"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#e96d1f" /><path
|
||||
id="path20"
|
||||
d="m 119.89704,-695.77708 -7.833,2.625 c 0.041,0.963 0.191,2.203 0.191,3.173 0,41.219 -37.272,98.205 -87.274,107.12 -3.243,1.089 -7.538,2.077 -10.93,2.932 v 1.639 c 68.344,-8.66 111.18,-71.719 105.84,-117.49 z"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#d9541e" /><path
|
||||
id="path22"
|
||||
d="m 6.15604,-690.34808 c -0.077,1.313 1.787,0.968 1.787,2.293 0,38.551 46.558,97.366 91.688,108.73 v 1.639 c -59.953,-5.52 -128.56,-67.8 -130.07,-126.85 l 36.599,14.189 z"
|
||||
style="fill:url(#SVGID_2_)"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
id="path24"
|
||||
d="m 24.28804,-685.60508 c -0.076,1.313 -0.12,2.63 -0.12,3.957 0,38.551 30.699,90.497 75.827,101.86 v 1.639 c -59.044,-2.79 -105.81,-63.024 -105.81,-109.2 0,-2.375 0.128,-4.729 0.371,-7.056 l 29.73,8.798 z"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#a0ce67" /><path
|
||||
id="path26"
|
||||
d="m -5.72996,-694.95408 7.617,2.722 c -0.041,0.962 -0.066,2.254 -0.066,3.225 0,41.219 37.271,98.204 87.272,107.12 3.245,1.088 7.538,2.077 10.932,2.931 v 1.638 c -65.254,-5.56 -111.1,-71.866 -105.76,-117.64 z"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#439639" /></g></svg>
|
||||
|
After Width: | Height: | Size: 12 KiB |
116
tokio-xmpp/src/client/auth.rs
Normal file
116
tokio-xmpp/src/client/auth.rs
Normal file
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
102
tokio-xmpp/src/client/bind.rs
Normal file
102
tokio-xmpp/src/client/bind.rs
Normal file
|
|
@ -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!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
236
tokio-xmpp/src/client/mod.rs
Normal file
236
tokio-xmpp/src/client/mod.rs
Normal file
|
|
@ -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(())),
|
||||
}
|
||||
}
|
||||
}
|
||||
89
tokio-xmpp/src/component/auth.rs
Normal file
89
tokio-xmpp/src/component/auth.rs
Normal file
|
|
@ -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!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
163
tokio-xmpp/src/component/mod.rs
Normal file
163
tokio-xmpp/src/component/mod.rs
Normal file
|
|
@ -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(())),
|
||||
}
|
||||
}
|
||||
}
|
||||
224
tokio-xmpp/src/error.rs
Normal file
224
tokio-xmpp/src/error.rs
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
54
tokio-xmpp/src/event.rs
Normal file
54
tokio-xmpp/src/event.rs
Normal file
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
196
tokio-xmpp/src/happy_eyeballs.rs
Normal file
196
tokio-xmpp/src/happy_eyeballs.rs
Normal file
|
|
@ -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!(""),
|
||||
}
|
||||
}
|
||||
}
|
||||
19
tokio-xmpp/src/lib.rs
Normal file
19
tokio-xmpp/src/lib.rs
Normal file
|
|
@ -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};
|
||||
114
tokio-xmpp/src/starttls.rs
Normal file
114
tokio-xmpp/src/starttls.rs
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
125
tokio-xmpp/src/stream_start.rs
Normal file
125
tokio-xmpp/src/stream_start.rs
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
532
tokio-xmpp/src/xmpp_codec.rs
Normal file
532
tokio-xmpp/src/xmpp_codec.rs
Normal file
|
|
@ -0,0 +1,532 @@
|
|||
//! XML stream parser for XMPP
|
||||
|
||||
use crate::{ParseError, ParserError};
|
||||
use bytes::{BufMut, BytesMut};
|
||||
use xmpp_parsers::Element;
|
||||
use quick_xml::Writer as EventWriter;
|
||||
use std;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::vec_deque::VecDeque;
|
||||
use std::collections::HashMap;
|
||||
use std::default::Default;
|
||||
use std::fmt::Write;
|
||||
use std::io;
|
||||
use std::iter::FromIterator;
|
||||
use std::rc::Rc;
|
||||
use std::str::from_utf8;
|
||||
use std::borrow::Cow;
|
||||
use tokio_codec::{Decoder, Encoder};
|
||||
use xml5ever::interface::Attribute;
|
||||
use xml5ever::tokenizer::{Tag, TagKind, Token, TokenSink, XmlTokenizer};
|
||||
use xml5ever::buffer_queue::BufferQueue;
|
||||
|
||||
/// Anything that can be sent or received on an XMPP/XML stream
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Packet {
|
||||
/// `<stream:stream>` start tag
|
||||
StreamStart(HashMap<String, String>),
|
||||
/// A complete stanza or nonza
|
||||
Stanza(Element),
|
||||
/// Plain text (think whitespace keep-alive)
|
||||
Text(String),
|
||||
/// `</stream:stream>` closing tag
|
||||
StreamEnd,
|
||||
}
|
||||
|
||||
type QueueItem = Result<Packet, ParserError>;
|
||||
|
||||
/// Parser state
|
||||
struct ParserSink {
|
||||
// Ready stanzas, shared with XMPPCodec
|
||||
queue: Rc<RefCell<VecDeque<QueueItem>>>,
|
||||
// Parsing stack
|
||||
stack: Vec<Element>,
|
||||
ns_stack: Vec<HashMap<Option<String>, String>>,
|
||||
}
|
||||
|
||||
impl ParserSink {
|
||||
pub fn new(queue: Rc<RefCell<VecDeque<QueueItem>>>) -> Self {
|
||||
ParserSink {
|
||||
queue,
|
||||
stack: vec![],
|
||||
ns_stack: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn push_queue(&self, pkt: Packet) {
|
||||
self.queue.borrow_mut().push_back(Ok(pkt));
|
||||
}
|
||||
|
||||
fn push_queue_error(&self, e: ParserError) {
|
||||
self.queue.borrow_mut().push_back(Err(e));
|
||||
}
|
||||
|
||||
/// Lookup XML namespace declaration for given prefix (or no prefix)
|
||||
fn lookup_ns(&self, prefix: &Option<String>) -> Option<&str> {
|
||||
for nss in self.ns_stack.iter().rev() {
|
||||
if let Some(ns) = nss.get(prefix) {
|
||||
return Some(ns);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn handle_start_tag(&mut self, tag: Tag) {
|
||||
let mut nss = HashMap::new();
|
||||
let is_prefix_xmlns = |attr: &Attribute| {
|
||||
attr.name
|
||||
.prefix
|
||||
.as_ref()
|
||||
.map(|prefix| prefix.eq_str_ignore_ascii_case("xmlns"))
|
||||
.unwrap_or(false)
|
||||
};
|
||||
for attr in &tag.attrs {
|
||||
match attr.name.local.as_ref() {
|
||||
"xmlns" => {
|
||||
nss.insert(None, attr.value.as_ref().to_owned());
|
||||
}
|
||||
prefix if is_prefix_xmlns(attr) => {
|
||||
nss.insert(Some(prefix.to_owned()), attr.value.as_ref().to_owned());
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
self.ns_stack.push(nss);
|
||||
|
||||
let el = {
|
||||
let mut el_builder = Element::builder(tag.name.local.as_ref());
|
||||
if let Some(el_ns) =
|
||||
self.lookup_ns(&tag.name.prefix.map(|prefix| prefix.as_ref().to_owned()))
|
||||
{
|
||||
el_builder = el_builder.ns(el_ns);
|
||||
}
|
||||
for attr in &tag.attrs {
|
||||
match attr.name.local.as_ref() {
|
||||
"xmlns" => (),
|
||||
_ if is_prefix_xmlns(attr) => (),
|
||||
_ => {
|
||||
let attr_name = if let Some(ref prefix) = attr.name.prefix {
|
||||
Cow::Owned(format!("{}:{}", prefix, attr.name.local))
|
||||
} else {
|
||||
Cow::Borrowed(attr.name.local.as_ref())
|
||||
};
|
||||
el_builder = el_builder.attr(attr_name, attr.value.as_ref());
|
||||
}
|
||||
}
|
||||
}
|
||||
el_builder.build()
|
||||
};
|
||||
|
||||
if self.stack.is_empty() {
|
||||
let attrs = HashMap::from_iter(tag.attrs.iter().map(|attr| {
|
||||
(
|
||||
attr.name.local.as_ref().to_owned(),
|
||||
attr.value.as_ref().to_owned(),
|
||||
)
|
||||
}));
|
||||
self.push_queue(Packet::StreamStart(attrs));
|
||||
}
|
||||
|
||||
self.stack.push(el);
|
||||
}
|
||||
|
||||
fn handle_end_tag(&mut self) {
|
||||
let el = self.stack.pop().unwrap();
|
||||
self.ns_stack.pop();
|
||||
|
||||
match self.stack.len() {
|
||||
// </stream:stream>
|
||||
0 => self.push_queue(Packet::StreamEnd),
|
||||
// </stanza>
|
||||
1 => self.push_queue(Packet::Stanza(el)),
|
||||
len => {
|
||||
let parent = &mut self.stack[len - 1];
|
||||
parent.append_child(el);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TokenSink for ParserSink {
|
||||
fn process_token(&mut self, token: Token) {
|
||||
match token {
|
||||
Token::TagToken(tag) => match tag.kind {
|
||||
TagKind::StartTag => self.handle_start_tag(tag),
|
||||
TagKind::EndTag => self.handle_end_tag(),
|
||||
TagKind::EmptyTag => {
|
||||
self.handle_start_tag(tag);
|
||||
self.handle_end_tag();
|
||||
}
|
||||
TagKind::ShortTag => self.push_queue_error(ParserError::ShortTag),
|
||||
},
|
||||
Token::CharacterTokens(tendril) => match self.stack.len() {
|
||||
0 | 1 => self.push_queue(Packet::Text(tendril.into())),
|
||||
len => {
|
||||
let el = &mut self.stack[len - 1];
|
||||
el.append_text_node(tendril);
|
||||
}
|
||||
},
|
||||
Token::EOFToken => self.push_queue(Packet::StreamEnd),
|
||||
Token::ParseError(s) => {
|
||||
// println!("ParseError: {:?}", s);
|
||||
self.push_queue_error(ParserError::Parse(ParseError(s)));
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
// fn end(&mut self) {
|
||||
// }
|
||||
}
|
||||
|
||||
/// Stateful encoder/decoder for a bytestream from/to XMPP `Packet`
|
||||
pub struct XMPPCodec {
|
||||
/// Outgoing
|
||||
ns: Option<String>,
|
||||
/// Incoming
|
||||
parser: XmlTokenizer<ParserSink>,
|
||||
/// For handling incoming truncated utf8
|
||||
// TODO: optimize using tendrils?
|
||||
buf: Vec<u8>,
|
||||
/// Shared with ParserSink
|
||||
queue: Rc<RefCell<VecDeque<QueueItem>>>,
|
||||
}
|
||||
|
||||
impl XMPPCodec {
|
||||
/// Constructor
|
||||
pub fn new() -> Self {
|
||||
let queue = Rc::new(RefCell::new(VecDeque::new()));
|
||||
let sink = ParserSink::new(queue.clone());
|
||||
// TODO: configure parser?
|
||||
let parser = XmlTokenizer::new(sink, Default::default());
|
||||
XMPPCodec {
|
||||
ns: None,
|
||||
parser,
|
||||
queue,
|
||||
buf: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for XMPPCodec {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Decoder for XMPPCodec {
|
||||
type Item = Packet;
|
||||
type Error = ParserError;
|
||||
|
||||
fn decode(&mut self, buf: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
|
||||
let buf1: Box<dyn AsRef<[u8]>> = if !self.buf.is_empty() && !buf.is_empty() {
|
||||
let mut prefix = std::mem::replace(&mut self.buf, vec![]);
|
||||
prefix.extend_from_slice(buf.take().as_ref());
|
||||
Box::new(prefix)
|
||||
} else {
|
||||
Box::new(buf.take())
|
||||
};
|
||||
let buf1 = buf1.as_ref().as_ref();
|
||||
match from_utf8(buf1) {
|
||||
Ok(mut s) => {
|
||||
s = s.trim();
|
||||
if !s.is_empty() {
|
||||
// println!("<< {}", s);
|
||||
let mut buffer_queue = BufferQueue::new();
|
||||
let tendril = FromIterator::from_iter(s.chars());
|
||||
buffer_queue.push_back(tendril);
|
||||
self.parser.feed(&mut buffer_queue);
|
||||
}
|
||||
}
|
||||
// Remedies for truncated utf8
|
||||
Err(e) if e.valid_up_to() >= buf1.len() - 3 => {
|
||||
// Prepare all the valid data
|
||||
let mut b = BytesMut::with_capacity(e.valid_up_to());
|
||||
b.put(&buf1[0..e.valid_up_to()]);
|
||||
|
||||
// Retry
|
||||
let result = self.decode(&mut b);
|
||||
|
||||
// Keep the tail back in
|
||||
self.buf.extend_from_slice(&buf1[e.valid_up_to()..]);
|
||||
|
||||
return result;
|
||||
}
|
||||
Err(e) => {
|
||||
// println!("error {} at {}/{} in {:?}", e, e.valid_up_to(), buf1.len(), buf1);
|
||||
return Err(ParserError::Utf8(e));
|
||||
}
|
||||
}
|
||||
|
||||
match self.queue.borrow_mut().pop_front() {
|
||||
None => Ok(None),
|
||||
Some(result) => result.map(|pkt| Some(pkt)),
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_eof(&mut self, buf: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
|
||||
self.decode(buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl Encoder for XMPPCodec {
|
||||
type Item = Packet;
|
||||
type Error = io::Error;
|
||||
|
||||
fn encode(&mut self, item: Self::Item, dst: &mut BytesMut) -> Result<(), Self::Error> {
|
||||
let remaining = dst.capacity() - dst.len();
|
||||
let max_stanza_size: usize = 2usize.pow(16);
|
||||
if remaining < max_stanza_size {
|
||||
dst.reserve(max_stanza_size - remaining);
|
||||
}
|
||||
|
||||
fn to_io_err<E: Into<Box<dyn std::error::Error + Send + Sync>>>(e: E) -> io::Error {
|
||||
io::Error::new(io::ErrorKind::InvalidInput, e)
|
||||
}
|
||||
|
||||
match item {
|
||||
Packet::StreamStart(start_attrs) => {
|
||||
let mut buf = String::new();
|
||||
write!(buf, "<stream:stream")
|
||||
.map_err(to_io_err)?;
|
||||
for (name, value) in start_attrs {
|
||||
write!(buf, " {}=\"{}\"", escape(&name), escape(&value))
|
||||
.map_err(to_io_err)?;
|
||||
if name == "xmlns" {
|
||||
self.ns = Some(value);
|
||||
}
|
||||
}
|
||||
write!(buf, ">\n")
|
||||
.map_err(to_io_err)?;
|
||||
|
||||
// print!(">> {}", buf);
|
||||
write!(dst, "{}", buf)
|
||||
.map_err(to_io_err)
|
||||
}
|
||||
Packet::Stanza(stanza) => {
|
||||
stanza
|
||||
.write_to_inner(&mut EventWriter::new(WriteBytes::new(dst)))
|
||||
.and_then(|_| {
|
||||
// println!(">> {:?}", dst);
|
||||
Ok(())
|
||||
})
|
||||
.map_err(|e| to_io_err(format!("{}", e)))
|
||||
}
|
||||
Packet::Text(text) => {
|
||||
write_text(&text, dst)
|
||||
.and_then(|_| {
|
||||
// println!(">> {:?}", dst);
|
||||
Ok(())
|
||||
})
|
||||
.map_err(to_io_err)
|
||||
}
|
||||
Packet::StreamEnd => {
|
||||
write!(dst, "</stream:stream>\n")
|
||||
.map_err(to_io_err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Write XML-escaped text string
|
||||
pub fn write_text<W: Write>(text: &str, writer: &mut W) -> Result<(), std::fmt::Error> {
|
||||
write!(writer, "{}", escape(text))
|
||||
}
|
||||
|
||||
/// Copied from `RustyXML` for now
|
||||
pub fn escape(input: &str) -> String {
|
||||
let mut result = String::with_capacity(input.len());
|
||||
|
||||
for c in input.chars() {
|
||||
match c {
|
||||
'&' => result.push_str("&"),
|
||||
'<' => result.push_str("<"),
|
||||
'>' => result.push_str(">"),
|
||||
'\'' => result.push_str("'"),
|
||||
'"' => result.push_str("""),
|
||||
o => result.push(o),
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// BytesMut impl only std::fmt::Write but not std::io::Write. The
|
||||
/// latter trait is required for minidom's
|
||||
/// `Element::write_to_inner()`.
|
||||
struct WriteBytes<'a> {
|
||||
dst: &'a mut BytesMut,
|
||||
}
|
||||
|
||||
impl<'a> WriteBytes<'a> {
|
||||
fn new(dst: &'a mut BytesMut) -> Self {
|
||||
WriteBytes { dst }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> std::io::Write for WriteBytes<'a> {
|
||||
fn write(&mut self, buf: &[u8]) -> std::result::Result<usize, std::io::Error> {
|
||||
self.dst.put_slice(buf);
|
||||
Ok(buf.len())
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> std::result::Result<(), std::io::Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use bytes::BytesMut;
|
||||
|
||||
#[test]
|
||||
fn test_stream_start() {
|
||||
let mut c = XMPPCodec::new();
|
||||
let mut b = BytesMut::with_capacity(1024);
|
||||
b.put(r"<?xml version='1.0'?><stream:stream xmlns:stream='http://etherx.jabber.org/streams' version='1.0' xmlns='jabber:client'>");
|
||||
let r = c.decode(&mut b);
|
||||
assert!(match r {
|
||||
Ok(Some(Packet::StreamStart(_))) => true,
|
||||
_ => false,
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stream_end() {
|
||||
let mut c = XMPPCodec::new();
|
||||
let mut b = BytesMut::with_capacity(1024);
|
||||
b.put(r"<?xml version='1.0'?><stream:stream xmlns:stream='http://etherx.jabber.org/streams' version='1.0' xmlns='jabber:client'>");
|
||||
let r = c.decode(&mut b);
|
||||
assert!(match r {
|
||||
Ok(Some(Packet::StreamStart(_))) => true,
|
||||
_ => false,
|
||||
});
|
||||
b.clear();
|
||||
b.put(r"</stream:stream>");
|
||||
let r = c.decode(&mut b);
|
||||
assert!(match r {
|
||||
Ok(Some(Packet::StreamEnd)) => true,
|
||||
_ => false,
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_truncated_stanza() {
|
||||
let mut c = XMPPCodec::new();
|
||||
let mut b = BytesMut::with_capacity(1024);
|
||||
b.put(r"<?xml version='1.0'?><stream:stream xmlns:stream='http://etherx.jabber.org/streams' version='1.0' xmlns='jabber:client'>");
|
||||
let r = c.decode(&mut b);
|
||||
assert!(match r {
|
||||
Ok(Some(Packet::StreamStart(_))) => true,
|
||||
_ => false,
|
||||
});
|
||||
|
||||
b.clear();
|
||||
b.put(r"<test>ß</test");
|
||||
let r = c.decode(&mut b);
|
||||
assert!(match r {
|
||||
Ok(None) => true,
|
||||
_ => false,
|
||||
});
|
||||
|
||||
b.clear();
|
||||
b.put(r">");
|
||||
let r = c.decode(&mut b);
|
||||
assert!(match r {
|
||||
Ok(Some(Packet::Stanza(ref el))) if el.name() == "test" && el.text() == "ß" => true,
|
||||
_ => false,
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_truncated_utf8() {
|
||||
let mut c = XMPPCodec::new();
|
||||
let mut b = BytesMut::with_capacity(1024);
|
||||
b.put(r"<?xml version='1.0'?><stream:stream xmlns:stream='http://etherx.jabber.org/streams' version='1.0' xmlns='jabber:client'>");
|
||||
let r = c.decode(&mut b);
|
||||
assert!(match r {
|
||||
Ok(Some(Packet::StreamStart(_))) => true,
|
||||
_ => false,
|
||||
});
|
||||
|
||||
b.clear();
|
||||
b.put(&b"<test>\xc3"[..]);
|
||||
let r = c.decode(&mut b);
|
||||
assert!(match r {
|
||||
Ok(None) => true,
|
||||
_ => false,
|
||||
});
|
||||
|
||||
b.clear();
|
||||
b.put(&b"\x9f</test>"[..]);
|
||||
let r = c.decode(&mut b);
|
||||
assert!(match r {
|
||||
Ok(Some(Packet::Stanza(ref el))) if el.name() == "test" && el.text() == "ß" => true,
|
||||
_ => false,
|
||||
});
|
||||
}
|
||||
|
||||
/// test case for https://gitlab.com/xmpp-rs/tokio-xmpp/issues/3
|
||||
#[test]
|
||||
fn test_atrribute_prefix() {
|
||||
let mut c = XMPPCodec::new();
|
||||
let mut b = BytesMut::with_capacity(1024);
|
||||
b.put(r"<?xml version='1.0'?><stream:stream xmlns:stream='http://etherx.jabber.org/streams' version='1.0' xmlns='jabber:client'>");
|
||||
let r = c.decode(&mut b);
|
||||
assert!(match r {
|
||||
Ok(Some(Packet::StreamStart(_))) => true,
|
||||
_ => false,
|
||||
});
|
||||
|
||||
b.clear();
|
||||
b.put(r"<status xml:lang='en'>Test status</status>");
|
||||
let r = c.decode(&mut b);
|
||||
assert!(match r {
|
||||
Ok(Some(Packet::Stanza(ref el))) if el.name() == "status" && el.text() == "Test status" && el.attr("xml:lang").map_or(false, |a| a == "en") => true,
|
||||
_ => false,
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/// By default, encode() only get's a BytesMut that has 8kb space reserved.
|
||||
#[test]
|
||||
fn test_large_stanza() {
|
||||
use futures::{Future, Sink};
|
||||
use std::io::Cursor;
|
||||
use tokio_codec::FramedWrite;
|
||||
let framed = FramedWrite::new(Cursor::new(vec![]), XMPPCodec::new());
|
||||
let mut text = "".to_owned();
|
||||
for _ in 0..2usize.pow(15) {
|
||||
text = text + "A";
|
||||
}
|
||||
let stanza = Element::builder("message")
|
||||
.append(Element::builder("body").append(text.as_ref()).build())
|
||||
.build();
|
||||
let framed = framed.send(Packet::Stanza(stanza)).wait().expect("send");
|
||||
assert_eq!(
|
||||
framed.get_ref().get_ref(),
|
||||
&("<message><body>".to_owned() + &text + "</body></message>").as_bytes()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lone_whitespace() {
|
||||
let mut c = XMPPCodec::new();
|
||||
let mut b = BytesMut::with_capacity(1024);
|
||||
b.put(r"<?xml version='1.0'?><stream:stream xmlns:stream='http://etherx.jabber.org/streams' version='1.0' xmlns='jabber:client'>");
|
||||
let r = c.decode(&mut b);
|
||||
assert!(match r {
|
||||
Ok(Some(Packet::StreamStart(_))) => true,
|
||||
_ => false,
|
||||
});
|
||||
|
||||
b.clear();
|
||||
b.put(r" ");
|
||||
let r = c.decode(&mut b);
|
||||
assert!(match r {
|
||||
Ok(None) => true,
|
||||
_ => false,
|
||||
});
|
||||
}
|
||||
}
|
||||
92
tokio-xmpp/src/xmpp_stream.rs
Normal file
92
tokio-xmpp/src/xmpp_stream.rs
Normal file
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
62
xmpp-parsers/.gitlab-ci.yml
Normal file
62
xmpp-parsers/.gitlab-ci.yml
Normal file
|
|
@ -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"
|
||||
34
xmpp-parsers/Cargo.toml
Normal file
34
xmpp-parsers/Cargo.toml
Normal file
|
|
@ -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" ]
|
||||
325
xmpp-parsers/ChangeLog
Normal file
325
xmpp-parsers/ChangeLog
Normal file
|
|
@ -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.
|
||||
373
xmpp-parsers/LICENSE
Normal file
373
xmpp-parsers/LICENSE
Normal file
|
|
@ -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.
|
||||
643
xmpp-parsers/doap.xml
Normal file
643
xmpp-parsers/doap.xml
Normal file
|
|
@ -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>
|
||||
62
xmpp-parsers/examples/generate-caps.rs
Normal file
62
xmpp-parsers/examples/generate-caps.rs
Normal file
|
|
@ -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(())
|
||||
}
|
||||
72
xmpp-parsers/src/attention.rs
Normal file
72
xmpp-parsers/src/attention.rs
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
use crate::message::MessagePayload;
|
||||
|
||||
generate_empty_element!(
|
||||
/// Requests the attention of the recipient.
|
||||
Attention,
|
||||
"attention",
|
||||
ATTENTION
|
||||
);
|
||||
|
||||
impl MessagePayload for Attention {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[cfg(not(feature = "disable-validation"))]
|
||||
use crate::util::error::Error;
|
||||
use crate::Element;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(Attention, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple() {
|
||||
let elem: Element = "<attention xmlns='urn:xmpp:attention:0'/>".parse().unwrap();
|
||||
Attention::try_from(elem).unwrap();
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "disable-validation"))]
|
||||
#[test]
|
||||
fn test_invalid_child() {
|
||||
let elem: Element = "<attention xmlns='urn:xmpp:attention:0'><coucou/></attention>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let error = Attention::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "Unknown child in attention element.");
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "disable-validation"))]
|
||||
#[test]
|
||||
fn test_invalid_attribute() {
|
||||
let elem: Element = "<attention xmlns='urn:xmpp:attention:0' coucou=''/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let error = Attention::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "Unknown attribute in attention element.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialise() {
|
||||
let elem: Element = "<attention xmlns='urn:xmpp:attention:0'/>".parse().unwrap();
|
||||
let attention = Attention;
|
||||
let elem2: Element = attention.into();
|
||||
assert_eq!(elem, elem2);
|
||||
}
|
||||
}
|
||||
128
xmpp-parsers/src/avatar.rs
Normal file
128
xmpp-parsers/src/avatar.rs
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
use crate::hashes::Sha1HexAttribute;
|
||||
use crate::pubsub::PubSubPayload;
|
||||
use crate::util::helpers::WhitespaceAwareBase64;
|
||||
|
||||
generate_element!(
|
||||
/// Communicates information about an avatar.
|
||||
Metadata, "metadata", AVATAR_METADATA,
|
||||
children: [
|
||||
/// List of information elements describing this avatar.
|
||||
infos: Vec<Info> = ("info", AVATAR_METADATA) => Info
|
||||
]
|
||||
);
|
||||
|
||||
impl PubSubPayload for Metadata {}
|
||||
|
||||
generate_element!(
|
||||
/// Communicates avatar metadata.
|
||||
Info, "info", AVATAR_METADATA,
|
||||
attributes: [
|
||||
/// The size of the image data in bytes.
|
||||
bytes: Required<u16> = "bytes",
|
||||
|
||||
/// The width of the image in pixels.
|
||||
width: Option<u16> = "width",
|
||||
|
||||
/// The height of the image in pixels.
|
||||
height: Option<u16> = "height",
|
||||
|
||||
/// The SHA-1 hash of the image data for the specified content-type.
|
||||
id: Required<Sha1HexAttribute> = "id",
|
||||
|
||||
/// The IANA-registered content type of the image data.
|
||||
type_: Required<String> = "type",
|
||||
|
||||
/// The http: or https: URL at which the image data file is hosted.
|
||||
url: Option<String> = "url",
|
||||
]
|
||||
);
|
||||
|
||||
generate_element!(
|
||||
/// The actual avatar data.
|
||||
Data, "data", AVATAR_DATA,
|
||||
text: (
|
||||
/// Vector of bytes representing the 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.")
|
||||
}
|
||||
}
|
||||
198
xmpp-parsers/src/bind.rs
Normal file
198
xmpp-parsers/src/bind.rs
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
// Copyright (c) 2018 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
use crate::util::error::Error;
|
||||
use crate::iq::{IqResultPayload, IqSetPayload};
|
||||
use crate::ns;
|
||||
use jid::{FullJid, Jid};
|
||||
use crate::Element;
|
||||
use std::str::FromStr;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
/// The request for resource binding, which is the process by which a client
|
||||
/// can obtain a full JID and start exchanging on the XMPP network.
|
||||
///
|
||||
/// See https://xmpp.org/rfcs/rfc6120.html#bind
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct BindQuery {
|
||||
/// Requests this resource, the server may associate another one though.
|
||||
///
|
||||
/// If this is None, we request no particular resource, and a random one
|
||||
/// will be affected by the server.
|
||||
resource: Option<String>,
|
||||
}
|
||||
|
||||
impl BindQuery {
|
||||
/// Creates a resource binding request.
|
||||
pub fn new(resource: Option<String>) -> BindQuery {
|
||||
BindQuery { resource }
|
||||
}
|
||||
}
|
||||
|
||||
impl IqSetPayload for BindQuery {}
|
||||
|
||||
impl TryFrom<Element> for BindQuery {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(elem: Element) -> Result<BindQuery, Error> {
|
||||
check_self!(elem, "bind", BIND);
|
||||
check_no_attributes!(elem, "bind");
|
||||
|
||||
let mut resource = None;
|
||||
for child in elem.children() {
|
||||
if resource.is_some() {
|
||||
return Err(Error::ParseError("Bind can only have one child."));
|
||||
}
|
||||
if child.is("resource", ns::BIND) {
|
||||
check_no_attributes!(child, "resource");
|
||||
check_no_children!(child, "resource");
|
||||
resource = Some(child.text());
|
||||
} else {
|
||||
return Err(Error::ParseError("Unknown element in bind request."));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(BindQuery { resource })
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BindQuery> for Element {
|
||||
fn from(bind: BindQuery) -> Element {
|
||||
Element::builder("bind")
|
||||
.ns(ns::BIND)
|
||||
.append_all(bind.resource.map(|resource|
|
||||
Element::builder("resource")
|
||||
.ns(ns::BIND)
|
||||
.append(resource)))
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
/// The response for resource binding, containing the 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.");
|
||||
}
|
||||
}
|
||||
223
xmpp-parsers/src/blocking.rs
Normal file
223
xmpp-parsers/src/blocking.rs
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
use crate::util::error::Error;
|
||||
use crate::iq::{IqGetPayload, IqResultPayload, IqSetPayload};
|
||||
use crate::ns;
|
||||
use jid::Jid;
|
||||
use crate::Element;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
generate_empty_element!(
|
||||
/// The element requesting the blocklist, the result iq will contain a
|
||||
/// [BlocklistResult].
|
||||
BlocklistRequest,
|
||||
"blocklist",
|
||||
BLOCKING
|
||||
);
|
||||
|
||||
impl IqGetPayload for BlocklistRequest {}
|
||||
|
||||
macro_rules! generate_blocking_element {
|
||||
($(#[$meta:meta])* $elem:ident, $name:tt) => (
|
||||
$(#[$meta])*
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct $elem {
|
||||
/// List of JIDs affected by this command.
|
||||
pub items: Vec<Jid>,
|
||||
}
|
||||
|
||||
impl TryFrom<Element> for $elem {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(elem: Element) -> Result<$elem, Error> {
|
||||
check_self!(elem, $name, BLOCKING);
|
||||
check_no_attributes!(elem, $name);
|
||||
let mut items = vec!();
|
||||
for child in elem.children() {
|
||||
check_self!(child, "item", BLOCKING);
|
||||
check_no_unknown_attributes!(child, "item", ["jid"]);
|
||||
check_no_children!(child, "item");
|
||||
items.push(get_attr!(child, "jid", Required));
|
||||
}
|
||||
Ok($elem { items })
|
||||
}
|
||||
}
|
||||
|
||||
impl From<$elem> for Element {
|
||||
fn from(elem: $elem) -> Element {
|
||||
Element::builder($name)
|
||||
.ns(ns::BLOCKING)
|
||||
.append_all(elem.items.into_iter().map(|jid| {
|
||||
Element::builder("item")
|
||||
.ns(ns::BLOCKING)
|
||||
.attr("jid", jid)
|
||||
}))
|
||||
.build()
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
generate_blocking_element!(
|
||||
/// The element containing the current blocklist, as a reply from
|
||||
/// [BlocklistRequest].
|
||||
BlocklistResult,
|
||||
"blocklist"
|
||||
);
|
||||
|
||||
impl IqResultPayload for BlocklistResult {}
|
||||
|
||||
// TODO: Prevent zero elements from being allowed.
|
||||
generate_blocking_element!(
|
||||
/// A query to block one or more JIDs.
|
||||
Block,
|
||||
"block"
|
||||
);
|
||||
|
||||
impl IqSetPayload for Block {}
|
||||
|
||||
generate_blocking_element!(
|
||||
/// A query to unblock one or more JIDs, or all of them.
|
||||
///
|
||||
/// Warning: not putting any JID there means clearing out the blocklist.
|
||||
Unblock,
|
||||
"unblock"
|
||||
);
|
||||
|
||||
impl IqSetPayload for Unblock {}
|
||||
|
||||
generate_empty_element!(
|
||||
/// The application-specific error condition when a message is blocked.
|
||||
Blocked,
|
||||
"blocked",
|
||||
BLOCKING_ERRORS
|
||||
);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use jid::BareJid;
|
||||
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(BlocklistRequest, 0);
|
||||
assert_size!(BlocklistResult, 12);
|
||||
assert_size!(Block, 12);
|
||||
assert_size!(Unblock, 12);
|
||||
}
|
||||
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(BlocklistRequest, 0);
|
||||
assert_size!(BlocklistResult, 24);
|
||||
assert_size!(Block, 24);
|
||||
assert_size!(Unblock, 24);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple() {
|
||||
let elem: Element = "<blocklist xmlns='urn:xmpp:blocking'/>".parse().unwrap();
|
||||
let request_elem = elem.clone();
|
||||
BlocklistRequest::try_from(request_elem).unwrap();
|
||||
|
||||
let result_elem = elem.clone();
|
||||
let result = BlocklistResult::try_from(result_elem).unwrap();
|
||||
assert_eq!(result.items, vec!());
|
||||
|
||||
let elem: Element = "<block xmlns='urn:xmpp:blocking'/>".parse().unwrap();
|
||||
let block = Block::try_from(elem).unwrap();
|
||||
assert_eq!(block.items, vec!());
|
||||
|
||||
let elem: Element = "<unblock xmlns='urn:xmpp:blocking'/>".parse().unwrap();
|
||||
let unblock = Unblock::try_from(elem).unwrap();
|
||||
assert_eq!(unblock.items, vec!());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_items() {
|
||||
let elem: Element = "<blocklist xmlns='urn:xmpp:blocking'><item jid='coucou@coucou'/><item jid='domain'/></blocklist>".parse().unwrap();
|
||||
let two_items = vec![
|
||||
Jid::Bare(BareJid {
|
||||
node: Some(String::from("coucou")),
|
||||
domain: String::from("coucou"),
|
||||
}),
|
||||
Jid::Bare(BareJid {
|
||||
node: None,
|
||||
domain: String::from("domain"),
|
||||
}),
|
||||
];
|
||||
|
||||
let result_elem = elem.clone();
|
||||
let result = BlocklistResult::try_from(result_elem).unwrap();
|
||||
assert_eq!(result.items, two_items);
|
||||
|
||||
let elem: Element = "<block xmlns='urn:xmpp:blocking'><item jid='coucou@coucou'/><item jid='domain'/></block>".parse().unwrap();
|
||||
let block = Block::try_from(elem).unwrap();
|
||||
assert_eq!(block.items, two_items);
|
||||
|
||||
let elem: Element = "<unblock xmlns='urn:xmpp:blocking'><item jid='coucou@coucou'/><item jid='domain'/></unblock>".parse().unwrap();
|
||||
let unblock = Unblock::try_from(elem).unwrap();
|
||||
assert_eq!(unblock.items, two_items);
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "disable-validation"))]
|
||||
#[test]
|
||||
fn test_invalid() {
|
||||
let elem: Element = "<blocklist xmlns='urn:xmpp:blocking' coucou=''/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let request_elem = elem.clone();
|
||||
let error = BlocklistRequest::try_from(request_elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "Unknown attribute in blocklist element.");
|
||||
|
||||
let result_elem = elem.clone();
|
||||
let error = BlocklistResult::try_from(result_elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "Unknown attribute in blocklist element.");
|
||||
|
||||
let elem: Element = "<block xmlns='urn:xmpp:blocking' coucou=''/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let error = Block::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "Unknown attribute in block element.");
|
||||
|
||||
let elem: Element = "<unblock xmlns='urn:xmpp:blocking' coucou=''/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let error = Unblock::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "Unknown attribute in unblock element.");
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "disable-validation"))]
|
||||
#[test]
|
||||
fn test_non_empty_blocklist_request() {
|
||||
let elem: Element = "<blocklist xmlns='urn:xmpp:blocking'><item jid='coucou@coucou'/><item jid='domain'/></blocklist>".parse().unwrap();
|
||||
let error = BlocklistRequest::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "Unknown child in blocklist element.");
|
||||
}
|
||||
}
|
||||
168
xmpp-parsers/src/bob.rs
Normal file
168
xmpp-parsers/src/bob.rs
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
use crate::hashes::{Hash, Algo};
|
||||
use crate::util::helpers::Base64;
|
||||
use crate::util::error::Error;
|
||||
use minidom::IntoAttributeValue;
|
||||
use std::str::FromStr;
|
||||
|
||||
/// A Content-ID, as defined in RFC2111.
|
||||
///
|
||||
/// The text value SHOULD be of the form algo+hash@bob.xmpp.org, this struct
|
||||
/// enforces that format.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ContentId {
|
||||
hash: Hash,
|
||||
}
|
||||
|
||||
impl FromStr for ContentId {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Error> {
|
||||
let temp: Vec<_> = s.splitn(2, '@').collect();
|
||||
let temp: Vec<_> = match temp[..] {
|
||||
[lhs, rhs] => {
|
||||
if rhs != "bob.xmpp.org" {
|
||||
return Err(Error::ParseError("Wrong domain for cid URI."))
|
||||
}
|
||||
lhs.splitn(2, '+').collect()
|
||||
},
|
||||
_ => return Err(Error::ParseError("Missing @ in cid URI."))
|
||||
};
|
||||
let (algo, hex) = match temp[..] {
|
||||
[lhs, rhs] => {
|
||||
let algo = match lhs {
|
||||
"sha1" => Algo::Sha_1,
|
||||
"sha256" => Algo::Sha_256,
|
||||
_ => unimplemented!(),
|
||||
};
|
||||
(algo, rhs)
|
||||
},
|
||||
_ => return Err(Error::ParseError("Missing + in cid URI."))
|
||||
};
|
||||
let hash = Hash::from_hex(algo, hex)?;
|
||||
Ok(ContentId { hash })
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoAttributeValue for ContentId {
|
||||
fn into_attribute_value(self) -> Option<String> {
|
||||
let algo = match self.hash.algo {
|
||||
Algo::Sha_1 => "sha1",
|
||||
Algo::Sha_256 => "sha256",
|
||||
_ => unimplemented!(),
|
||||
};
|
||||
Some(format!("{}+{}@bob.xmpp.org", algo, self.hash.to_hex()))
|
||||
}
|
||||
}
|
||||
|
||||
generate_element!(
|
||||
/// Request for an uncached cid file.
|
||||
Data, "data", BOB,
|
||||
attributes: [
|
||||
/// The cid in question.
|
||||
cid: Required<ContentId> = "cid",
|
||||
|
||||
/// How long to cache it (in seconds).
|
||||
max_age: Option<usize> = "max-age",
|
||||
|
||||
/// The MIME type of the data being transmitted.
|
||||
///
|
||||
/// See the [IANA MIME Media Types Registry][1] for a list of
|
||||
/// registered types, but unregistered or yet-to-be-registered are
|
||||
/// accepted too.
|
||||
///
|
||||
/// [1]: https://www.iana.org/assignments/media-types/media-types.xhtml
|
||||
type_: Option<String> = "type"
|
||||
],
|
||||
text: (
|
||||
/// The actual data.
|
||||
data: Base64<Vec<u8>>
|
||||
)
|
||||
);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::Element;
|
||||
use std::convert::TryFrom;
|
||||
use std::error::Error as StdError;
|
||||
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(ContentId, 28);
|
||||
assert_size!(Data, 60);
|
||||
}
|
||||
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(ContentId, 56);
|
||||
assert_size!(Data, 120);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple() {
|
||||
let cid: ContentId = "sha1+8f35fef110ffc5df08d579a50083ff9308fb6242@bob.xmpp.org".parse().unwrap();
|
||||
assert_eq!(cid.hash.algo, Algo::Sha_1);
|
||||
assert_eq!(cid.hash.hash, b"\x8f\x35\xfe\xf1\x10\xff\xc5\xdf\x08\xd5\x79\xa5\x00\x83\xff\x93\x08\xfb\x62\x42");
|
||||
assert_eq!(cid.into_attribute_value().unwrap(), "sha1+8f35fef110ffc5df08d579a50083ff9308fb6242@bob.xmpp.org");
|
||||
|
||||
let elem: Element = "<data xmlns='urn:xmpp:bob' cid='sha1+8f35fef110ffc5df08d579a50083ff9308fb6242@bob.xmpp.org'/>".parse().unwrap();
|
||||
let data = Data::try_from(elem).unwrap();
|
||||
assert_eq!(data.cid.hash.algo, Algo::Sha_1);
|
||||
assert_eq!(data.cid.hash.hash, b"\x8f\x35\xfe\xf1\x10\xff\xc5\xdf\x08\xd5\x79\xa5\x00\x83\xff\x93\x08\xfb\x62\x42");
|
||||
assert!(data.max_age.is_none());
|
||||
assert!(data.type_.is_none());
|
||||
assert!(data.data.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_cid() {
|
||||
let error = "Hello world!".parse::<ContentId>().unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "Missing @ in cid URI.");
|
||||
|
||||
let error = "Hello world@bob.xmpp.org".parse::<ContentId>().unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "Missing + in cid URI.");
|
||||
|
||||
let error = "sha1+1234@coucou.linkmauve.fr".parse::<ContentId>().unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "Wrong domain for cid URI.");
|
||||
|
||||
let error = "sha1+invalid@bob.xmpp.org".parse::<ContentId>().unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseIntError(error) => error,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message.description(), "invalid digit found in string");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_child() {
|
||||
let elem: Element = "<data xmlns='urn:xmpp:bob'><coucou/></data>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let error = Data::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "Unknown child in data element.");
|
||||
}
|
||||
}
|
||||
122
xmpp-parsers/src/bookmarks.rs
Normal file
122
xmpp-parsers/src/bookmarks.rs
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
// Copyright (c) 2018 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
use jid::BareJid;
|
||||
|
||||
generate_attribute!(
|
||||
/// Whether a conference bookmark should be joined automatically.
|
||||
Autojoin,
|
||||
"autojoin",
|
||||
bool
|
||||
);
|
||||
|
||||
generate_element!(
|
||||
/// A conference bookmark.
|
||||
Conference, "conference", BOOKMARKS,
|
||||
attributes: [
|
||||
/// Whether a conference bookmark should be joined automatically.
|
||||
autojoin: Default<Autojoin> = "autojoin",
|
||||
|
||||
/// The JID of the conference.
|
||||
jid: Required<BareJid> = "jid",
|
||||
|
||||
/// A user-defined name for this conference.
|
||||
name: Required<String> = "name",
|
||||
],
|
||||
children: [
|
||||
/// The nick the user will use to join this conference.
|
||||
nick: Option<String> = ("nick", BOOKMARKS) => String,
|
||||
|
||||
/// The password required to join this conference.
|
||||
password: Option<String> = ("password", BOOKMARKS) => String
|
||||
]
|
||||
);
|
||||
|
||||
generate_element!(
|
||||
/// An URL bookmark.
|
||||
Url, "url", BOOKMARKS,
|
||||
attributes: [
|
||||
/// A user-defined name for this URL.
|
||||
name: Required<String> = "name",
|
||||
|
||||
/// The URL of this bookmark.
|
||||
url: Required<String> = "url",
|
||||
]
|
||||
);
|
||||
|
||||
generate_element!(
|
||||
/// Container element for multiple bookmarks.
|
||||
#[derive(Default)]
|
||||
Storage, "storage", BOOKMARKS,
|
||||
children: [
|
||||
/// Conferences the user has expressed an interest in.
|
||||
conferences: Vec<Conference> = ("conference", BOOKMARKS) => Conference,
|
||||
|
||||
/// URLs the user is interested in.
|
||||
urls: Vec<Url> = ("url", BOOKMARKS) => Url
|
||||
]
|
||||
);
|
||||
|
||||
impl Storage {
|
||||
/// Create an empty bookmarks storage.
|
||||
pub fn new() -> Storage {
|
||||
Storage::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::util::compare_elements::NamespaceAwareCompare;
|
||||
use crate::Element;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(Conference, 64);
|
||||
assert_size!(Url, 24);
|
||||
assert_size!(Storage, 24);
|
||||
}
|
||||
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(Conference, 128);
|
||||
assert_size!(Url, 48);
|
||||
assert_size!(Storage, 48);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
let elem: Element = "<storage xmlns='storage:bookmarks'/>".parse().unwrap();
|
||||
let elem1 = elem.clone();
|
||||
let storage = Storage::try_from(elem).unwrap();
|
||||
assert_eq!(storage.conferences.len(), 0);
|
||||
assert_eq!(storage.urls.len(), 0);
|
||||
|
||||
let elem2 = Element::from(Storage::new());
|
||||
assert!(elem1.compare_to(&elem2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete() {
|
||||
let elem: Element = "<storage xmlns='storage:bookmarks'><url name='Example' url='https://example.org/'/><conference autojoin='true' jid='test-muc@muc.localhost' name='Test MUC'><nick>Coucou</nick><password>secret</password></conference></storage>".parse().unwrap();
|
||||
let storage = Storage::try_from(elem).unwrap();
|
||||
assert_eq!(storage.urls.len(), 1);
|
||||
assert_eq!(storage.urls[0].name, "Example");
|
||||
assert_eq!(storage.urls[0].url, "https://example.org/");
|
||||
assert_eq!(storage.conferences.len(), 1);
|
||||
assert_eq!(storage.conferences[0].autojoin, Autojoin::True);
|
||||
assert_eq!(
|
||||
storage.conferences[0].jid,
|
||||
BareJid::new("test-muc", "muc.localhost")
|
||||
);
|
||||
assert_eq!(storage.conferences[0].name, "Test MUC");
|
||||
assert_eq!(storage.conferences[0].clone().nick.unwrap(), "Coucou");
|
||||
assert_eq!(storage.conferences[0].clone().password.unwrap(), "secret");
|
||||
}
|
||||
}
|
||||
119
xmpp-parsers/src/bookmarks2.rs
Normal file
119
xmpp-parsers/src/bookmarks2.rs
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
generate_attribute!(
|
||||
/// Whether a conference bookmark should be joined automatically.
|
||||
Autojoin,
|
||||
"autojoin",
|
||||
bool
|
||||
);
|
||||
|
||||
generate_element!(
|
||||
/// A conference bookmark.
|
||||
Conference, "conference", BOOKMARKS2,
|
||||
attributes: [
|
||||
/// Whether a conference bookmark should be joined automatically.
|
||||
autojoin: Default<Autojoin> = "autojoin",
|
||||
|
||||
/// A user-defined name for this conference.
|
||||
name: Option<String> = "name",
|
||||
],
|
||||
children: [
|
||||
/// The nick the user will use to join this conference.
|
||||
nick: Option<String> = ("nick", BOOKMARKS2) => String,
|
||||
|
||||
/// The password required to join this conference.
|
||||
password: Option<String> = ("password", BOOKMARKS2) => String
|
||||
]
|
||||
);
|
||||
|
||||
impl Conference {
|
||||
/// Create a new conference.
|
||||
pub fn new() -> Conference {
|
||||
Conference {
|
||||
autojoin: Autojoin::False,
|
||||
name: None,
|
||||
nick: None,
|
||||
password: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::util::compare_elements::NamespaceAwareCompare;
|
||||
use crate::Element;
|
||||
use std::convert::TryFrom;
|
||||
use crate::pubsub::pubsub::Item as PubSubItem;
|
||||
use crate::pubsub::event::PubSubEvent;
|
||||
use crate::ns;
|
||||
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(Conference, 40);
|
||||
}
|
||||
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(Conference, 80);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple() {
|
||||
let elem: Element = "<conference xmlns='urn:xmpp:bookmarks:0'/>".parse().unwrap();
|
||||
let elem1 = elem.clone();
|
||||
let conference = Conference::try_from(elem).unwrap();
|
||||
assert_eq!(conference.autojoin, Autojoin::False);
|
||||
assert_eq!(conference.name, None);
|
||||
assert_eq!(conference.nick, None);
|
||||
assert_eq!(conference.password, None);
|
||||
|
||||
let elem2 = Element::from(Conference::new());
|
||||
assert!(elem1.compare_to(&elem2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete() {
|
||||
let elem: Element = "<conference xmlns='urn:xmpp:bookmarks:0' autojoin='true' name='Test MUC'><nick>Coucou</nick><password>secret</password></conference>".parse().unwrap();
|
||||
let conference = Conference::try_from(elem).unwrap();
|
||||
assert_eq!(conference.autojoin, Autojoin::True);
|
||||
assert_eq!(conference.name, Some(String::from("Test MUC")));
|
||||
assert_eq!(conference.clone().nick.unwrap(), "Coucou");
|
||||
assert_eq!(conference.clone().password.unwrap(), "secret");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrapped() {
|
||||
let elem: Element = "<item xmlns='http://jabber.org/protocol/pubsub' id='test-muc@muc.localhost'><conference xmlns='urn:xmpp:bookmarks:0' autojoin='true' name='Test MUC'><nick>Coucou</nick><password>secret</password></conference></item>".parse().unwrap();
|
||||
let item = PubSubItem::try_from(elem).unwrap();
|
||||
let payload = item.payload.clone().unwrap();
|
||||
let conference = Conference::try_from(payload).unwrap();
|
||||
assert_eq!(conference.autojoin, Autojoin::True);
|
||||
assert_eq!(conference.name, Some(String::from("Test MUC")));
|
||||
assert_eq!(conference.clone().nick.unwrap(), "Coucou");
|
||||
assert_eq!(conference.clone().password.unwrap(), "secret");
|
||||
|
||||
let elem: Element = "<event xmlns='http://jabber.org/protocol/pubsub#event'><items node='urn:xmpp:bookmarks:0'><item xmlns='http://jabber.org/protocol/pubsub#event' id='test-muc@muc.localhost'><conference xmlns='urn:xmpp:bookmarks:0' autojoin='true' name='Test MUC'><nick>Coucou</nick><password>secret</password></conference></item></items></event>".parse().unwrap();
|
||||
let mut items = match PubSubEvent::try_from(elem) {
|
||||
Ok(PubSubEvent::PublishedItems { node, items }) => {
|
||||
assert_eq!(&node.0, ns::BOOKMARKS2);
|
||||
items
|
||||
},
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(items.len(), 1);
|
||||
let item = items.pop().unwrap();
|
||||
let payload = item.payload.clone().unwrap();
|
||||
let conference = Conference::try_from(payload).unwrap();
|
||||
assert_eq!(conference.autojoin, Autojoin::True);
|
||||
assert_eq!(conference.name, Some(String::from("Test MUC")));
|
||||
assert_eq!(conference.clone().nick.unwrap(), "Coucou");
|
||||
assert_eq!(conference.clone().password.unwrap(), "secret");
|
||||
}
|
||||
}
|
||||
341
xmpp-parsers/src/caps.rs
Normal file
341
xmpp-parsers/src/caps.rs
Normal file
|
|
@ -0,0 +1,341 @@
|
|||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
use crate::data_forms::DataForm;
|
||||
use crate::disco::{DiscoInfoQuery, DiscoInfoResult, Feature, Identity};
|
||||
use crate::util::error::Error;
|
||||
use crate::hashes::{Algo, Hash};
|
||||
use crate::ns;
|
||||
use crate::presence::PresencePayload;
|
||||
use blake2::VarBlake2b;
|
||||
use digest::{Digest, Input, VariableOutput};
|
||||
use crate::Element;
|
||||
use sha1::Sha1;
|
||||
use sha2::{Sha256, Sha512};
|
||||
use sha3::{Sha3_256, Sha3_512};
|
||||
use std::convert::TryFrom;
|
||||
|
||||
/// Represents a capability hash for a given client.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Caps {
|
||||
/// Deprecated list of additional feature bundles.
|
||||
pub ext: Option<String>,
|
||||
|
||||
/// A URI identifying an XMPP application.
|
||||
pub node: String,
|
||||
|
||||
/// The hash of that 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()
|
||||
);
|
||||
}
|
||||
}
|
||||
127
xmpp-parsers/src/carbons.rs
Normal file
127
xmpp-parsers/src/carbons.rs
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
use crate::forwarding::Forwarded;
|
||||
use crate::iq::IqSetPayload;
|
||||
use crate::message::MessagePayload;
|
||||
|
||||
generate_empty_element!(
|
||||
/// Enable carbons for this session.
|
||||
Enable,
|
||||
"enable",
|
||||
CARBONS
|
||||
);
|
||||
|
||||
impl IqSetPayload for Enable {}
|
||||
|
||||
generate_empty_element!(
|
||||
/// Disable a previously-enabled carbons.
|
||||
Disable,
|
||||
"disable",
|
||||
CARBONS
|
||||
);
|
||||
|
||||
impl IqSetPayload for Disable {}
|
||||
|
||||
generate_empty_element!(
|
||||
/// Request the enclosing message to not be copied to other carbons-enabled
|
||||
/// resources of the user.
|
||||
Private,
|
||||
"private",
|
||||
CARBONS
|
||||
);
|
||||
|
||||
impl MessagePayload for Private {}
|
||||
|
||||
generate_element!(
|
||||
/// Wrapper for a message received on another resource.
|
||||
Received, "received", CARBONS,
|
||||
|
||||
children: [
|
||||
/// Wrapper for the enclosed message.
|
||||
forwarded: Required<Forwarded> = ("forwarded", FORWARD) => Forwarded
|
||||
]
|
||||
);
|
||||
|
||||
impl MessagePayload for Received {}
|
||||
|
||||
generate_element!(
|
||||
/// Wrapper for a message sent from another resource.
|
||||
Sent, "sent", CARBONS,
|
||||
|
||||
children: [
|
||||
/// Wrapper for the enclosed message.
|
||||
forwarded: Required<Forwarded> = ("forwarded", FORWARD) => Forwarded
|
||||
]
|
||||
);
|
||||
|
||||
impl MessagePayload for Sent {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::Element;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(Enable, 0);
|
||||
assert_size!(Disable, 0);
|
||||
assert_size!(Private, 0);
|
||||
assert_size!(Received, 212);
|
||||
assert_size!(Sent, 212);
|
||||
}
|
||||
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(Enable, 0);
|
||||
assert_size!(Disable, 0);
|
||||
assert_size!(Private, 0);
|
||||
assert_size!(Received, 408);
|
||||
assert_size!(Sent, 408);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_elements() {
|
||||
let elem: Element = "<enable xmlns='urn:xmpp:carbons:2'/>".parse().unwrap();
|
||||
Enable::try_from(elem).unwrap();
|
||||
|
||||
let elem: Element = "<disable xmlns='urn:xmpp:carbons:2'/>".parse().unwrap();
|
||||
Disable::try_from(elem).unwrap();
|
||||
|
||||
let elem: Element = "<private xmlns='urn:xmpp:carbons:2'/>".parse().unwrap();
|
||||
Private::try_from(elem).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn forwarded_elements() {
|
||||
let elem: Element = "<received xmlns='urn:xmpp:carbons:2'>
|
||||
<forwarded xmlns='urn:xmpp:forward:0'>
|
||||
<message xmlns='jabber:client'
|
||||
to='juliet@capulet.example/balcony'
|
||||
from='romeo@montague.example/home'/>
|
||||
</forwarded>
|
||||
</received>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let received = Received::try_from(elem).unwrap();
|
||||
assert!(received.forwarded.stanza.is_some());
|
||||
|
||||
let elem: Element = "<sent xmlns='urn:xmpp:carbons:2'>
|
||||
<forwarded xmlns='urn:xmpp:forward:0'>
|
||||
<message xmlns='jabber:client'
|
||||
to='juliet@capulet.example/balcony'
|
||||
from='romeo@montague.example/home'/>
|
||||
</forwarded>
|
||||
</sent>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let sent = Sent::try_from(elem).unwrap();
|
||||
assert!(sent.forwarded.stanza.is_some());
|
||||
}
|
||||
}
|
||||
214
xmpp-parsers/src/cert_management.rs
Normal file
214
xmpp-parsers/src/cert_management.rs
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
use crate::iq::{IqSetPayload, IqGetPayload, IqResultPayload};
|
||||
use crate::util::helpers::Base64;
|
||||
|
||||
generate_elem_id!(
|
||||
/// The name of a certificate.
|
||||
Name, "name", SASL_CERT
|
||||
);
|
||||
|
||||
generate_element!(
|
||||
/// An X.509 certificate.
|
||||
Cert, "x509cert", SASL_CERT,
|
||||
text: (
|
||||
/// The BER X.509 data.
|
||||
data: Base64<Vec<u8>>
|
||||
)
|
||||
);
|
||||
|
||||
generate_element!(
|
||||
/// For the client to upload an X.509 certificate.
|
||||
Append, "append", SASL_CERT,
|
||||
children: [
|
||||
/// The name of this certificate.
|
||||
name: Required<Name> = ("name", SASL_CERT) => Name,
|
||||
|
||||
/// The X.509 certificate to set.
|
||||
cert: Required<Cert> = ("x509cert", SASL_CERT) => Cert,
|
||||
|
||||
/// This client is forbidden from managing certificates.
|
||||
no_cert_management: Present<_> = ("no-cert-management", SASL_CERT) => bool
|
||||
]
|
||||
);
|
||||
|
||||
impl IqSetPayload for Append {}
|
||||
|
||||
generate_empty_element!(
|
||||
/// Client requests the current list of X.509 certificates.
|
||||
ListCertsQuery, "items", SASL_CERT
|
||||
);
|
||||
|
||||
impl IqGetPayload for ListCertsQuery {}
|
||||
|
||||
generate_elem_id!(
|
||||
/// One resource currently using a certificate.
|
||||
Resource, "resource", SASL_CERT
|
||||
);
|
||||
|
||||
generate_element!(
|
||||
/// A list of resources currently using this certificate.
|
||||
Users, "users", SASL_CERT,
|
||||
children: [
|
||||
/// Resources currently using this certificate.
|
||||
resources: Vec<Resource> = ("resource", SASL_CERT) => Resource
|
||||
]
|
||||
);
|
||||
|
||||
generate_element!(
|
||||
/// An X.509 certificate being set for this user.
|
||||
Item, "item", SASL_CERT,
|
||||
children: [
|
||||
/// The name of this certificate.
|
||||
name: Required<Name> = ("name", SASL_CERT) => Name,
|
||||
|
||||
/// The X.509 certificate to set.
|
||||
cert: Required<Cert> = ("x509cert", SASL_CERT) => Cert,
|
||||
|
||||
/// This client is forbidden from managing certificates.
|
||||
no_cert_management: Present<_> = ("no-cert-management", SASL_CERT) => bool,
|
||||
|
||||
/// List of resources currently using this certificate.
|
||||
users: Option<Users> = ("users", SASL_CERT) => Users
|
||||
]
|
||||
);
|
||||
|
||||
generate_element!(
|
||||
/// Server answers with the current list of X.509 certificates.
|
||||
ListCertsResponse, "items", SASL_CERT,
|
||||
children: [
|
||||
/// List of certificates.
|
||||
items: Vec<Item> = ("item", SASL_CERT) => Item
|
||||
]
|
||||
);
|
||||
|
||||
impl IqResultPayload for ListCertsResponse {}
|
||||
|
||||
generate_element!(
|
||||
/// Client disables an X.509 certificate.
|
||||
Disable, "disable", SASL_CERT,
|
||||
children: [
|
||||
/// Name of the certificate to disable.
|
||||
name: Required<Name> = ("name", SASL_CERT) => Name
|
||||
]
|
||||
);
|
||||
|
||||
impl IqSetPayload for Disable {}
|
||||
|
||||
generate_element!(
|
||||
/// Client revokes an X.509 certificate.
|
||||
Revoke, "revoke", SASL_CERT,
|
||||
children: [
|
||||
/// Name of the certificate to revoke.
|
||||
name: Required<Name> = ("name", SASL_CERT) => Name
|
||||
]
|
||||
);
|
||||
|
||||
impl IqSetPayload for Revoke {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::Element;
|
||||
use std::convert::TryFrom;
|
||||
use std::str::FromStr;
|
||||
use crate::ns;
|
||||
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(Append, 28);
|
||||
assert_size!(Disable, 12);
|
||||
assert_size!(Revoke, 12);
|
||||
assert_size!(ListCertsQuery, 0);
|
||||
assert_size!(ListCertsResponse, 12);
|
||||
assert_size!(Item, 40);
|
||||
assert_size!(Resource, 12);
|
||||
assert_size!(Users, 12);
|
||||
assert_size!(Cert, 12);
|
||||
}
|
||||
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(Append, 56);
|
||||
assert_size!(Disable, 24);
|
||||
assert_size!(Revoke, 24);
|
||||
assert_size!(ListCertsQuery, 0);
|
||||
assert_size!(ListCertsResponse, 24);
|
||||
assert_size!(Item, 80);
|
||||
assert_size!(Resource, 24);
|
||||
assert_size!(Users, 24);
|
||||
assert_size!(Cert, 24);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple() {
|
||||
let elem: Element = "<append xmlns='urn:xmpp:saslcert:1'><name>Mobile Client</name><x509cert>AAAA</x509cert></append>".parse().unwrap();
|
||||
let append = Append::try_from(elem).unwrap();
|
||||
assert_eq!(append.name.0, "Mobile Client");
|
||||
assert_eq!(append.cert.data, b"\0\0\0");
|
||||
|
||||
let elem: Element = "<disable xmlns='urn:xmpp:saslcert:1'><name>Mobile Client</name></disable>".parse().unwrap();
|
||||
let disable = Disable::try_from(elem).unwrap();
|
||||
assert_eq!(disable.name.0, "Mobile Client");
|
||||
|
||||
let elem: Element = "<revoke xmlns='urn:xmpp:saslcert:1'><name>Mobile Client</name></revoke>".parse().unwrap();
|
||||
let revoke = Revoke::try_from(elem).unwrap();
|
||||
assert_eq!(revoke.name.0, "Mobile Client");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list() {
|
||||
let elem: Element = r#"
|
||||
<items xmlns='urn:xmpp:saslcert:1'>
|
||||
<item>
|
||||
<name>Mobile Client</name>
|
||||
<x509cert>AAAA</x509cert>
|
||||
<users>
|
||||
<resource>Phone</resource>
|
||||
</users>
|
||||
</item>
|
||||
<item>
|
||||
<name>Laptop</name>
|
||||
<x509cert>BBBB</x509cert>
|
||||
</item>
|
||||
</items>"#.parse().unwrap();
|
||||
let mut list = ListCertsResponse::try_from(elem).unwrap();
|
||||
assert_eq!(list.items.len(), 2);
|
||||
|
||||
let item = list.items.pop().unwrap();
|
||||
assert_eq!(item.name.0, "Laptop");
|
||||
assert_eq!(item.cert.data, [4, 16, 65]);
|
||||
assert!(item.users.is_none());
|
||||
|
||||
let item = list.items.pop().unwrap();
|
||||
assert_eq!(item.name.0, "Mobile Client");
|
||||
assert_eq!(item.cert.data, b"\0\0\0");
|
||||
assert_eq!(item.users.unwrap().resources.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialise() {
|
||||
let append = Append {
|
||||
name: Name::from_str("Mobile Client").unwrap(),
|
||||
cert: Cert { data: b"\0\0\0".to_vec() },
|
||||
no_cert_management: false,
|
||||
};
|
||||
let elem: Element = append.into();
|
||||
assert!(elem.is("append", ns::SASL_CERT));
|
||||
|
||||
let disable = Disable {
|
||||
name: Name::from_str("Mobile Client").unwrap(),
|
||||
};
|
||||
let elem: Element = disable.into();
|
||||
assert!(elem.is("disable", ns::SASL_CERT));
|
||||
let elem = elem.children().cloned().collect::<Vec<_>>().pop().unwrap();
|
||||
assert!(elem.is("name", ns::SASL_CERT));
|
||||
assert_eq!(elem.text(), "Mobile Client");
|
||||
}
|
||||
}
|
||||
100
xmpp-parsers/src/chatstates.rs
Normal file
100
xmpp-parsers/src/chatstates.rs
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
use crate::message::MessagePayload;
|
||||
|
||||
generate_element_enum!(
|
||||
/// Enum representing chatstate elements part of the
|
||||
/// `http://jabber.org/protocol/chatstates` namespace.
|
||||
ChatState, "chatstate", CHATSTATES, {
|
||||
/// `<active xmlns='http://jabber.org/protocol/chatstates'/>`
|
||||
Active => "active",
|
||||
|
||||
/// `<composing xmlns='http://jabber.org/protocol/chatstates'/>`
|
||||
Composing => "composing",
|
||||
|
||||
/// `<gone xmlns='http://jabber.org/protocol/chatstates'/>`
|
||||
Gone => "gone",
|
||||
|
||||
/// `<inactive xmlns='http://jabber.org/protocol/chatstates'/>`
|
||||
Inactive => "inactive",
|
||||
|
||||
/// `<paused xmlns='http://jabber.org/protocol/chatstates'/>`
|
||||
Paused => "paused",
|
||||
}
|
||||
);
|
||||
|
||||
impl MessagePayload for ChatState {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::util::error::Error;
|
||||
use crate::ns;
|
||||
use crate::Element;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(ChatState, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple() {
|
||||
let elem: Element = "<active xmlns='http://jabber.org/protocol/chatstates'/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
ChatState::try_from(elem).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid() {
|
||||
let elem: Element = "<coucou xmlns='http://jabber.org/protocol/chatstates'/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let error = ChatState::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "This is not a chatstate element.");
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "disable-validation"))]
|
||||
#[test]
|
||||
fn test_invalid_child() {
|
||||
let elem: Element = "<gone xmlns='http://jabber.org/protocol/chatstates'><coucou/></gone>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let error = ChatState::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "Unknown child in chatstate element.");
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "disable-validation"))]
|
||||
#[test]
|
||||
fn test_invalid_attribute() {
|
||||
let elem: Element = "<inactive xmlns='http://jabber.org/protocol/chatstates' coucou=''/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let error = ChatState::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "Unknown attribute in chatstate element.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialise() {
|
||||
let chatstate = ChatState::Active;
|
||||
let elem: Element = chatstate.into();
|
||||
assert!(elem.is("active", ns::CHATSTATES));
|
||||
}
|
||||
}
|
||||
87
xmpp-parsers/src/component.rs
Normal file
87
xmpp-parsers/src/component.rs
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
// Copyright (c) 2018 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
use crate::util::helpers::PlainText;
|
||||
use digest::Digest;
|
||||
use sha1::Sha1;
|
||||
|
||||
generate_element!(
|
||||
/// The main authentication mechanism for components.
|
||||
#[derive(Default)]
|
||||
Handshake, "handshake", COMPONENT,
|
||||
text: (
|
||||
/// If Some, contains the hex-encoded SHA-1 of the concatenation of the
|
||||
/// stream id and the password, and is used to authenticate against the
|
||||
/// server.
|
||||
///
|
||||
/// If None, it is the successful reply from the server, the stream is now
|
||||
/// fully established and both sides can now exchange stanzas.
|
||||
data: PlainText<Option<String>>
|
||||
)
|
||||
);
|
||||
|
||||
impl Handshake {
|
||||
/// Creates a successful reply from a server.
|
||||
pub fn new() -> Handshake {
|
||||
Handshake::default()
|
||||
}
|
||||
|
||||
/// Creates an authentication request from the component.
|
||||
pub fn from_password_and_stream_id(password: &str, stream_id: &str) -> Handshake {
|
||||
let input = String::from(stream_id) + password;
|
||||
let hash = Sha1::digest(input.as_bytes());
|
||||
let content = format!("{:x}", hash);
|
||||
Handshake {
|
||||
data: Some(content),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::Element;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(Handshake, 12);
|
||||
}
|
||||
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(Handshake, 24);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple() {
|
||||
let elem: Element = "<handshake xmlns='jabber:component:accept'/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let handshake = Handshake::try_from(elem).unwrap();
|
||||
assert_eq!(handshake.data, None);
|
||||
|
||||
let elem: Element = "<handshake xmlns='jabber:component:accept'>Coucou</handshake>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let handshake = Handshake::try_from(elem).unwrap();
|
||||
assert_eq!(handshake.data, Some(String::from("Coucou")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_constructors() {
|
||||
let handshake = Handshake::new();
|
||||
assert_eq!(handshake.data, None);
|
||||
|
||||
let handshake = Handshake::from_password_and_stream_id("123456", "sid");
|
||||
assert_eq!(
|
||||
handshake.data,
|
||||
Some(String::from("9accec263ab84a43c6037ccf7cd48cb1d3f6df8e"))
|
||||
);
|
||||
}
|
||||
}
|
||||
59
xmpp-parsers/src/csi.rs
Normal file
59
xmpp-parsers/src/csi.rs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
generate_empty_element!(
|
||||
/// Stream:feature sent by the server to advertise it supports CSI.
|
||||
Feature, "csi", CSI
|
||||
);
|
||||
|
||||
generate_empty_element!(
|
||||
/// Client indicates it is inactive.
|
||||
Inactive, "inactive", CSI
|
||||
);
|
||||
|
||||
generate_empty_element!(
|
||||
/// Client indicates it is active again.
|
||||
Active, "active", CSI
|
||||
);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::Element;
|
||||
use std::convert::TryFrom;
|
||||
use crate::ns;
|
||||
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(Feature, 0);
|
||||
assert_size!(Inactive, 0);
|
||||
assert_size!(Active, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parsing() {
|
||||
let elem: Element = "<csi xmlns='urn:xmpp:csi:0'/>".parse().unwrap();
|
||||
Feature::try_from(elem).unwrap();
|
||||
|
||||
let elem: Element = "<inactive xmlns='urn:xmpp:csi:0'/>".parse().unwrap();
|
||||
Inactive::try_from(elem).unwrap();
|
||||
|
||||
let elem: Element = "<active xmlns='urn:xmpp:csi:0'/>".parse().unwrap();
|
||||
Active::try_from(elem).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialising() {
|
||||
let elem: Element = Feature.into();
|
||||
assert!(elem.is("csi", ns::CSI));
|
||||
|
||||
let elem: Element = Inactive.into();
|
||||
assert!(elem.is("inactive", ns::CSI));
|
||||
|
||||
let elem: Element = Active.into();
|
||||
assert!(elem.is("active", ns::CSI));
|
||||
}
|
||||
}
|
||||
395
xmpp-parsers/src/data_forms.rs
Normal file
395
xmpp-parsers/src/data_forms.rs
Normal file
|
|
@ -0,0 +1,395 @@
|
|||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
use crate::util::error::Error;
|
||||
use crate::media_element::MediaElement;
|
||||
use crate::ns;
|
||||
use crate::Element;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
generate_element!(
|
||||
/// Represents one of the possible values for a list- field.
|
||||
Option_, "option", DATA_FORMS,
|
||||
attributes: [
|
||||
/// The optional label to be displayed to the user for this option.
|
||||
label: Option<String> = "label"
|
||||
],
|
||||
children: [
|
||||
/// The value returned to the server when selecting this option.
|
||||
value: Required<String> = ("value", DATA_FORMS) => String
|
||||
]
|
||||
);
|
||||
|
||||
generate_attribute!(
|
||||
/// The type of a [field](struct.Field.html) element.
|
||||
FieldType, "type", {
|
||||
/// This field can only take the values "0" or "false" for a false
|
||||
/// value, and "1" or "true" for a true value.
|
||||
Boolean => "boolean",
|
||||
|
||||
/// This field describes data, it must not be sent back to the
|
||||
/// requester.
|
||||
Fixed => "fixed",
|
||||
|
||||
/// This field is hidden, it should not be displayed to the user but
|
||||
/// should be sent back to the requester.
|
||||
Hidden => "hidden",
|
||||
|
||||
/// This field accepts one or more [JIDs](../../jid/struct.Jid.html).
|
||||
/// A client may want to let the user autocomplete them based on their
|
||||
/// contacts list for instance.
|
||||
JidMulti => "jid-multi",
|
||||
|
||||
/// This field accepts one [JID](../../jid/struct.Jid.html). A client
|
||||
/// may want to let the user autocomplete it based on their contacts
|
||||
/// list for instance.
|
||||
JidSingle => "jid-single",
|
||||
|
||||
/// This field accepts one or more values from the list provided as
|
||||
/// [options](struct.Option_.html).
|
||||
ListMulti => "list-multi",
|
||||
|
||||
/// This field accepts one value from the list provided as
|
||||
/// [options](struct.Option_.html).
|
||||
ListSingle => "list-single",
|
||||
|
||||
/// This field accepts one or more free form text lines.
|
||||
TextMulti => "text-multi",
|
||||
|
||||
/// This field accepts one free form password, a client should hide it
|
||||
/// in its user interface.
|
||||
TextPrivate => "text-private",
|
||||
|
||||
/// This field accepts one free form text line.
|
||||
TextSingle => "text-single",
|
||||
}, Default = TextSingle
|
||||
);
|
||||
|
||||
/// Represents a field in a [data form](struct.DataForm.html).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Field {
|
||||
/// The unique identifier for this field, in the form.
|
||||
pub var: String,
|
||||
|
||||
/// The type of this field.
|
||||
pub type_: FieldType,
|
||||
|
||||
/// The label to be possibly displayed to the user for this field.
|
||||
pub label: Option<String>,
|
||||
|
||||
/// The form will be rejected if this field 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."
|
||||
);
|
||||
}
|
||||
}
|
||||
138
xmpp-parsers/src/date.rs
Normal file
138
xmpp-parsers/src/date.rs
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
use crate::util::error::Error;
|
||||
use chrono::{DateTime as ChronoDateTime, FixedOffset};
|
||||
use minidom::{IntoAttributeValue, Node};
|
||||
use std::str::FromStr;
|
||||
|
||||
/// Implements the DateTime profile of XEP-0082, which represents a
|
||||
/// non-recurring moment in time, with an accuracy of seconds or fraction of
|
||||
/// seconds, and includes a timezone.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct DateTime(ChronoDateTime<FixedOffset>);
|
||||
|
||||
impl DateTime {
|
||||
/// Retrieves the associated timezone.
|
||||
pub fn timezone(&self) -> FixedOffset {
|
||||
self.0.timezone()
|
||||
}
|
||||
|
||||
/// Returns a new `DateTime` with a different timezone.
|
||||
pub fn with_timezone(&self, tz: FixedOffset) -> DateTime {
|
||||
DateTime(self.0.with_timezone(&tz))
|
||||
}
|
||||
|
||||
/// Formats this `DateTime` with the specified format string.
|
||||
pub fn format(&self, fmt: &str) -> String {
|
||||
format!("{}", self.0.format(fmt))
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for DateTime {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<DateTime, Error> {
|
||||
Ok(DateTime(ChronoDateTime::parse_from_rfc3339(s)?))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoAttributeValue for DateTime {
|
||||
fn into_attribute_value(self) -> Option<String> {
|
||||
Some(self.0.to_rfc3339())
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<Node> for DateTime {
|
||||
fn into(self) -> Node {
|
||||
Node::Text(self.0.to_rfc3339())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use chrono::{Datelike, Timelike};
|
||||
use std::error::Error as StdError;
|
||||
|
||||
// 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")));
|
||||
}
|
||||
}
|
||||
118
xmpp-parsers/src/delay.rs
Normal file
118
xmpp-parsers/src/delay.rs
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
use crate::date::DateTime;
|
||||
use crate::util::helpers::PlainText;
|
||||
use crate::message::MessagePayload;
|
||||
use crate::presence::PresencePayload;
|
||||
use jid::Jid;
|
||||
|
||||
generate_element!(
|
||||
/// Notes when and by whom a message got stored for later delivery.
|
||||
Delay, "delay", DELAY,
|
||||
attributes: [
|
||||
/// The entity which delayed this message.
|
||||
from: Option<Jid> = "from",
|
||||
|
||||
/// The time at which this message got stored.
|
||||
stamp: Required<DateTime> = "stamp"
|
||||
],
|
||||
text: (
|
||||
/// The optional reason this message got delayed.
|
||||
data: PlainText<Option<String>>
|
||||
)
|
||||
);
|
||||
|
||||
impl MessagePayload for Delay {}
|
||||
impl PresencePayload for Delay {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::util::error::Error;
|
||||
use crate::Element;
|
||||
use std::str::FromStr;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(Delay, 68);
|
||||
}
|
||||
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(Delay, 120);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple() {
|
||||
let elem: Element =
|
||||
"<delay xmlns='urn:xmpp:delay' from='capulet.com' stamp='2002-09-10T23:08:25Z'/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let delay = Delay::try_from(elem).unwrap();
|
||||
assert_eq!(delay.from, Some(Jid::from_str("capulet.com").unwrap()));
|
||||
assert_eq!(
|
||||
delay.stamp,
|
||||
DateTime::from_str("2002-09-10T23:08:25Z").unwrap()
|
||||
);
|
||||
assert_eq!(delay.data, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unknown() {
|
||||
let elem: Element = "<replace xmlns='urn:xmpp:message-correct:0'/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let error = Delay::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "This is not a delay element.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_child() {
|
||||
let elem: Element = "<delay xmlns='urn:xmpp:delay'><coucou/></delay>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let error = Delay::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "Unknown child in delay element.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialise() {
|
||||
let elem: Element = "<delay xmlns='urn:xmpp:delay' stamp='2002-09-10T23:08:25+00:00'/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let delay = Delay {
|
||||
from: None,
|
||||
stamp: DateTime::from_str("2002-09-10T23:08:25Z").unwrap(),
|
||||
data: None,
|
||||
};
|
||||
let elem2 = delay.into();
|
||||
assert_eq!(elem, elem2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialise_data() {
|
||||
let elem: Element = "<delay xmlns='urn:xmpp:delay' from='juliet@example.org' stamp='2002-09-10T23:08:25+00:00'>Reason</delay>".parse().unwrap();
|
||||
let delay = Delay {
|
||||
from: Some(Jid::from_str("juliet@example.org").unwrap()),
|
||||
stamp: DateTime::from_str("2002-09-10T23:08:25Z").unwrap(),
|
||||
data: Some(String::from("Reason")),
|
||||
};
|
||||
let elem2 = delay.into();
|
||||
assert_eq!(elem, elem2);
|
||||
}
|
||||
}
|
||||
457
xmpp-parsers/src/disco.rs
Normal file
457
xmpp-parsers/src/disco.rs
Normal file
|
|
@ -0,0 +1,457 @@
|
|||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
use crate::data_forms::{DataForm, DataFormType};
|
||||
use crate::util::error::Error;
|
||||
use crate::iq::{IqGetPayload, IqResultPayload};
|
||||
use crate::ns;
|
||||
use jid::Jid;
|
||||
use crate::Element;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
generate_element!(
|
||||
/// Structure representing a `<query xmlns='http://jabber.org/protocol/disco#info'/>` element.
|
||||
///
|
||||
/// It should only be used in an `<iq type='get'/>`, as it can only represent
|
||||
/// the request, and not a result.
|
||||
DiscoInfoQuery, "query", DISCO_INFO,
|
||||
attributes: [
|
||||
/// Node on which we are doing the discovery.
|
||||
node: Option<String> = "node",
|
||||
]);
|
||||
|
||||
impl IqGetPayload for DiscoInfoQuery {}
|
||||
|
||||
generate_element!(
|
||||
/// Structure representing a `<feature xmlns='http://jabber.org/protocol/disco#info'/>` element.
|
||||
#[derive(PartialEq)]
|
||||
Feature, "feature", DISCO_INFO,
|
||||
attributes: [
|
||||
/// Namespace of the feature we want to represent.
|
||||
var: Required<String> = "var",
|
||||
]);
|
||||
|
||||
impl Feature {
|
||||
/// Create a new `<feature/>` with the according `@var`.
|
||||
pub fn new<S: Into<String>>(var: S) -> Feature {
|
||||
Feature {
|
||||
var: var.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
generate_element!(
|
||||
/// Structure representing an `<identity xmlns='http://jabber.org/protocol/disco#info'/>` element.
|
||||
Identity, "identity", DISCO_INFO,
|
||||
attributes: [
|
||||
/// Category of this identity.
|
||||
// TODO: use an enum here.
|
||||
category: RequiredNonEmpty<String> = "category",
|
||||
|
||||
/// Type of this identity.
|
||||
// TODO: use an enum here.
|
||||
type_: RequiredNonEmpty<String> = "type",
|
||||
|
||||
/// Lang of the name of this identity.
|
||||
lang: Option<String> = "xml:lang",
|
||||
|
||||
/// Name of this identity.
|
||||
name: Option<String> = "name",
|
||||
]
|
||||
);
|
||||
|
||||
impl Identity {
|
||||
/// Create a new `<identity/>`.
|
||||
pub fn new<C, T, L, N>(category: C, type_: T, lang: L, name: N) -> Identity
|
||||
where C: Into<String>,
|
||||
T: Into<String>,
|
||||
L: Into<String>,
|
||||
N: Into<String>,
|
||||
{
|
||||
Identity {
|
||||
category: category.into(),
|
||||
type_: type_.into(),
|
||||
lang: Some(lang.into()),
|
||||
name: Some(name.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new `<identity/>` without a name.
|
||||
pub fn new_anonymous<C, T, L, N>(category: C, type_: T) -> Identity
|
||||
where C: Into<String>,
|
||||
T: Into<String>,
|
||||
{
|
||||
Identity {
|
||||
category: category.into(),
|
||||
type_: type_.into(),
|
||||
lang: None,
|
||||
name: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Structure representing a `<query xmlns='http://jabber.org/protocol/disco#info'/>` element.
|
||||
///
|
||||
/// It should only be used in an `<iq type='result'/>`, as it can only
|
||||
/// represent the result, and not a request.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DiscoInfoResult {
|
||||
/// Node on which we have done this discovery.
|
||||
pub node: Option<String>,
|
||||
|
||||
/// List of identities exposed by this entity.
|
||||
pub identities: Vec<Identity>,
|
||||
|
||||
/// List of features supported by this entity.
|
||||
pub features: Vec<Feature>,
|
||||
|
||||
/// List of extensions reported by this entity.
|
||||
pub extensions: Vec<DataForm>,
|
||||
}
|
||||
|
||||
impl IqResultPayload for DiscoInfoResult {}
|
||||
|
||||
impl TryFrom<Element> for DiscoInfoResult {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(elem: Element) -> Result<DiscoInfoResult, Error> {
|
||||
check_self!(elem, "query", DISCO_INFO, "disco#info result");
|
||||
check_no_unknown_attributes!(elem, "disco#info result", ["node"]);
|
||||
|
||||
let mut result = DiscoInfoResult {
|
||||
node: get_attr!(elem, "node", Option),
|
||||
identities: vec![],
|
||||
features: vec![],
|
||||
extensions: vec![],
|
||||
};
|
||||
|
||||
for child in elem.children() {
|
||||
if child.is("identity", ns::DISCO_INFO) {
|
||||
let identity = Identity::try_from(child.clone())?;
|
||||
result.identities.push(identity);
|
||||
} else if child.is("feature", ns::DISCO_INFO) {
|
||||
let feature = Feature::try_from(child.clone())?;
|
||||
result.features.push(feature);
|
||||
} else if child.is("x", ns::DATA_FORMS) {
|
||||
let data_form = DataForm::try_from(child.clone())?;
|
||||
if data_form.type_ != DataFormType::Result_ {
|
||||
return Err(Error::ParseError(
|
||||
"Data form must have a 'result' type in disco#info.",
|
||||
));
|
||||
}
|
||||
if data_form.form_type.is_none() {
|
||||
return Err(Error::ParseError("Data form found without a FORM_TYPE."));
|
||||
}
|
||||
result.extensions.push(data_form);
|
||||
} else {
|
||||
return Err(Error::ParseError("Unknown element in disco#info."));
|
||||
}
|
||||
}
|
||||
|
||||
if result.identities.is_empty() {
|
||||
return Err(Error::ParseError(
|
||||
"There must be at least one identity in disco#info.",
|
||||
));
|
||||
}
|
||||
if result.features.is_empty() {
|
||||
return Err(Error::ParseError(
|
||||
"There must be at least one feature in disco#info.",
|
||||
));
|
||||
}
|
||||
if !result.features.contains(&Feature {
|
||||
var: ns::DISCO_INFO.to_owned(),
|
||||
}) {
|
||||
return Err(Error::ParseError(
|
||||
"disco#info feature not present in disco#info.",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DiscoInfoResult> for Element {
|
||||
fn from(disco: DiscoInfoResult) -> Element {
|
||||
Element::builder("query")
|
||||
.ns(ns::DISCO_INFO)
|
||||
.attr("node", disco.node)
|
||||
.append_all(disco.identities.into_iter())
|
||||
.append_all(disco.features.into_iter())
|
||||
.append_all(disco.extensions.iter().cloned().map(Element::from))
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
generate_element!(
|
||||
/// Structure representing a `<query xmlns='http://jabber.org/protocol/disco#items'/>` element.
|
||||
///
|
||||
/// It should only be used in an `<iq type='get'/>`, as it can only represent
|
||||
/// the request, and not a result.
|
||||
DiscoItemsQuery, "query", DISCO_ITEMS,
|
||||
attributes: [
|
||||
/// Node on which we are doing the discovery.
|
||||
node: Option<String> = "node",
|
||||
]);
|
||||
|
||||
impl IqGetPayload for DiscoItemsQuery {}
|
||||
|
||||
generate_element!(
|
||||
/// Structure representing an `<item xmlns='http://jabber.org/protocol/disco#items'/>` element.
|
||||
Item, "item", DISCO_ITEMS,
|
||||
attributes: [
|
||||
/// JID of the entity pointed by this item.
|
||||
jid: Required<Jid> = "jid",
|
||||
/// Node of the entity pointed by this item.
|
||||
node: Option<String> = "node",
|
||||
/// Name of the entity pointed by this item.
|
||||
name: Option<String> = "name",
|
||||
]);
|
||||
|
||||
generate_element!(
|
||||
/// Structure representing a `<query
|
||||
/// xmlns='http://jabber.org/protocol/disco#items'/>` element.
|
||||
///
|
||||
/// It should only be used in an `<iq type='result'/>`, as it can only
|
||||
/// represent the result, and not a request.
|
||||
DiscoItemsResult, "query", DISCO_ITEMS,
|
||||
attributes: [
|
||||
/// Node on which we have done this discovery.
|
||||
node: Option<String> = "node"
|
||||
],
|
||||
children: [
|
||||
/// List of items pointed by this entity.
|
||||
items: Vec<Item> = ("item", DISCO_ITEMS) => Item
|
||||
]
|
||||
);
|
||||
|
||||
impl IqResultPayload for DiscoItemsResult {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::util::compare_elements::NamespaceAwareCompare;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(Identity, 48);
|
||||
assert_size!(Feature, 12);
|
||||
assert_size!(DiscoInfoQuery, 12);
|
||||
assert_size!(DiscoInfoResult, 48);
|
||||
|
||||
assert_size!(Item, 64);
|
||||
assert_size!(DiscoItemsQuery, 12);
|
||||
assert_size!(DiscoItemsResult, 24);
|
||||
}
|
||||
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(Identity, 96);
|
||||
assert_size!(Feature, 24);
|
||||
assert_size!(DiscoInfoQuery, 24);
|
||||
assert_size!(DiscoInfoResult, 96);
|
||||
|
||||
assert_size!(Item, 128);
|
||||
assert_size!(DiscoItemsQuery, 24);
|
||||
assert_size!(DiscoItemsResult, 48);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple() {
|
||||
let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='client' type='pc'/><feature var='http://jabber.org/protocol/disco#info'/></query>".parse().unwrap();
|
||||
let query = DiscoInfoResult::try_from(elem).unwrap();
|
||||
assert!(query.node.is_none());
|
||||
assert_eq!(query.identities.len(), 1);
|
||||
assert_eq!(query.features.len(), 1);
|
||||
assert!(query.extensions.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_identity_after_feature() {
|
||||
let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><feature var='http://jabber.org/protocol/disco#info'/><identity category='client' type='pc'/></query>".parse().unwrap();
|
||||
let query = DiscoInfoResult::try_from(elem).unwrap();
|
||||
assert_eq!(query.identities.len(), 1);
|
||||
assert_eq!(query.features.len(), 1);
|
||||
assert!(query.extensions.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_feature_after_dataform() {
|
||||
let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='client' type='pc'/><x xmlns='jabber:x:data' type='result'><field var='FORM_TYPE' type='hidden'><value>coucou</value></field></x><feature var='http://jabber.org/protocol/disco#info'/></query>".parse().unwrap();
|
||||
let query = DiscoInfoResult::try_from(elem).unwrap();
|
||||
assert_eq!(query.identities.len(), 1);
|
||||
assert_eq!(query.features.len(), 1);
|
||||
assert_eq!(query.extensions.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extension() {
|
||||
let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='client' type='pc'/><feature var='http://jabber.org/protocol/disco#info'/><x xmlns='jabber:x:data' type='result'><field var='FORM_TYPE' type='hidden'><value>example</value></field></x></query>".parse().unwrap();
|
||||
let elem1 = elem.clone();
|
||||
let query = DiscoInfoResult::try_from(elem).unwrap();
|
||||
assert!(query.node.is_none());
|
||||
assert_eq!(query.identities.len(), 1);
|
||||
assert_eq!(query.features.len(), 1);
|
||||
assert_eq!(query.extensions.len(), 1);
|
||||
assert_eq!(query.extensions[0].form_type, Some(String::from("example")));
|
||||
|
||||
let elem2 = query.into();
|
||||
assert!(elem1.compare_to(&elem2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid() {
|
||||
let elem: Element =
|
||||
"<query xmlns='http://jabber.org/protocol/disco#info'><coucou/></query>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let error = DiscoInfoResult::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "Unknown element in disco#info.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_identity() {
|
||||
let elem: Element =
|
||||
"<query xmlns='http://jabber.org/protocol/disco#info'><identity/></query>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let error = DiscoInfoResult::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "Required attribute 'category' missing.");
|
||||
|
||||
let elem: Element =
|
||||
"<query xmlns='http://jabber.org/protocol/disco#info'><identity category=''/></query>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let error = DiscoInfoResult::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(
|
||||
message,
|
||||
"Required attribute 'category' must not be empty."
|
||||
);
|
||||
|
||||
let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='coucou'/></query>".parse().unwrap();
|
||||
let error = DiscoInfoResult::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "Required attribute 'type' missing.");
|
||||
|
||||
let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='coucou' type=''/></query>".parse().unwrap();
|
||||
let error = DiscoInfoResult::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "Required attribute 'type' must not be empty.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_feature() {
|
||||
let elem: Element =
|
||||
"<query xmlns='http://jabber.org/protocol/disco#info'><feature/></query>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let error = DiscoInfoResult::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "Required attribute 'var' missing.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_result() {
|
||||
let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let error = DiscoInfoResult::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(
|
||||
message,
|
||||
"There must be at least one identity in disco#info."
|
||||
);
|
||||
|
||||
let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='client' type='pc'/></query>".parse().unwrap();
|
||||
let error = DiscoInfoResult::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "There must be at least one feature in disco#info.");
|
||||
|
||||
let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='client' type='pc'/><feature var='http://jabber.org/protocol/disco#items'/></query>".parse().unwrap();
|
||||
let error = DiscoInfoResult::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "disco#info feature not present in disco#info.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple_items() {
|
||||
let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#items'/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let query = DiscoItemsQuery::try_from(elem).unwrap();
|
||||
assert!(query.node.is_none());
|
||||
|
||||
let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#items' node='coucou'/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let query = DiscoItemsQuery::try_from(elem).unwrap();
|
||||
assert_eq!(query.node, Some(String::from("coucou")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple_items_result() {
|
||||
let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#items'/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let query = DiscoItemsResult::try_from(elem).unwrap();
|
||||
assert!(query.node.is_none());
|
||||
assert!(query.items.is_empty());
|
||||
|
||||
let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#items' node='coucou'/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let query = DiscoItemsResult::try_from(elem).unwrap();
|
||||
assert_eq!(query.node, Some(String::from("coucou")));
|
||||
assert!(query.items.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_answers_items_result() {
|
||||
let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#items'><item jid='component'/><item jid='component2' node='test' name='A component'/></query>".parse().unwrap();
|
||||
let query = DiscoItemsResult::try_from(elem).unwrap();
|
||||
let elem2 = Element::from(query);
|
||||
let query = DiscoItemsResult::try_from(elem2).unwrap();
|
||||
assert_eq!(query.items.len(), 2);
|
||||
assert_eq!(query.items[0].jid, Jid::from_str("component").unwrap());
|
||||
assert_eq!(query.items[0].node, None);
|
||||
assert_eq!(query.items[0].name, None);
|
||||
assert_eq!(query.items[1].jid, Jid::from_str("component2").unwrap());
|
||||
assert_eq!(query.items[1].node, Some(String::from("test")));
|
||||
assert_eq!(query.items[1].name, Some(String::from("A component")));
|
||||
}
|
||||
}
|
||||
472
xmpp-parsers/src/ecaps2.rs
Normal file
472
xmpp-parsers/src/ecaps2.rs
Normal file
|
|
@ -0,0 +1,472 @@
|
|||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
use crate::data_forms::DataForm;
|
||||
use crate::disco::{DiscoInfoQuery, DiscoInfoResult, Feature, Identity};
|
||||
use crate::hashes::{Algo, Hash};
|
||||
use crate::ns;
|
||||
use crate::presence::PresencePayload;
|
||||
use blake2::VarBlake2b;
|
||||
use digest::{Digest, Input, VariableOutput};
|
||||
use sha2::{Sha256, Sha512};
|
||||
use sha3::{Sha3_256, Sha3_512};
|
||||
|
||||
generate_element!(
|
||||
/// Represents a set of capability hashes, all of them must correspond to
|
||||
/// the same input [disco#info](../disco/struct.DiscoInfoResult.html),
|
||||
/// using different [algorithms](../hashes/enum.Algo.html).
|
||||
ECaps2, "c", ECAPS2,
|
||||
children: [
|
||||
/// Hashes of the [disco#info](../disco/struct.DiscoInfoResult.html).
|
||||
hashes: Vec<Hash> = ("hash", HASHES) => Hash
|
||||
]
|
||||
);
|
||||
|
||||
impl PresencePayload for ECaps2 {}
|
||||
|
||||
impl ECaps2 {
|
||||
/// Create an ECaps2 element from a list of hashes.
|
||||
pub fn new(hashes: Vec<Hash>) -> ECaps2 {
|
||||
ECaps2 {
|
||||
hashes,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_item(field: &str) -> Vec<u8> {
|
||||
let mut bytes = field.as_bytes().to_vec();
|
||||
bytes.push(0x1f);
|
||||
bytes
|
||||
}
|
||||
|
||||
fn compute_items<T, F: Fn(&T) -> Vec<u8>>(things: &[T], separator: u8, encode: F) -> Vec<u8> {
|
||||
let mut string: Vec<u8> = vec![];
|
||||
let mut accumulator: Vec<Vec<u8>> = vec![];
|
||||
for thing in things {
|
||||
let bytes = encode(thing);
|
||||
accumulator.push(bytes);
|
||||
}
|
||||
// This works using the expected i;octet collation.
|
||||
accumulator.sort();
|
||||
for mut bytes in accumulator {
|
||||
string.append(&mut bytes);
|
||||
}
|
||||
string.push(separator);
|
||||
string
|
||||
}
|
||||
|
||||
fn compute_features(features: &[Feature]) -> Vec<u8> {
|
||||
compute_items(features, 0x1c, |feature| compute_item(&feature.var))
|
||||
}
|
||||
|
||||
fn compute_identities(identities: &[Identity]) -> Vec<u8> {
|
||||
compute_items(identities, 0x1c, |identity| {
|
||||
let mut bytes = compute_item(&identity.category);
|
||||
bytes.append(&mut compute_item(&identity.type_));
|
||||
bytes.append(&mut compute_item(
|
||||
&identity.lang.clone().unwrap_or_default(),
|
||||
));
|
||||
bytes.append(&mut compute_item(
|
||||
&identity.name.clone().unwrap_or_default(),
|
||||
));
|
||||
bytes.push(0x1e);
|
||||
bytes
|
||||
})
|
||||
}
|
||||
|
||||
fn compute_extensions(extensions: &[DataForm]) -> Result<Vec<u8>, ()> {
|
||||
for extension in extensions {
|
||||
if extension.form_type.is_none() {
|
||||
return Err(());
|
||||
}
|
||||
}
|
||||
Ok(compute_items(extensions, 0x1c, |extension| {
|
||||
let mut bytes = compute_item("FORM_TYPE");
|
||||
bytes.append(&mut compute_item(if let Some(ref form_type) = extension.form_type { form_type } else { unreachable!() }));
|
||||
bytes.push(0x1e);
|
||||
bytes.append(&mut compute_items(&extension.fields, 0x1d, |field| {
|
||||
let mut bytes = compute_item(&field.var);
|
||||
bytes.append(&mut compute_items(&field.values, 0x1e, |value| {
|
||||
compute_item(value)
|
||||
}));
|
||||
bytes
|
||||
}));
|
||||
bytes
|
||||
}))
|
||||
}
|
||||
|
||||
/// Applies the [algorithm from
|
||||
/// XEP-0390](https://xmpp.org/extensions/xep-0390.html#algorithm-input) on a
|
||||
/// [disco#info query element](../disco/struct.DiscoInfoResult.html).
|
||||
pub fn compute_disco(disco: &DiscoInfoResult) -> Result<Vec<u8>, ()> {
|
||||
let features_string = compute_features(&disco.features);
|
||||
let identities_string = compute_identities(&disco.identities);
|
||||
let extensions_string = compute_extensions(&disco.extensions)?;
|
||||
|
||||
let mut final_string = vec![];
|
||||
final_string.extend(features_string);
|
||||
final_string.extend(identities_string);
|
||||
final_string.extend(extensions_string);
|
||||
Ok(final_string)
|
||||
}
|
||||
|
||||
fn get_hash_vec(hash: &[u8]) -> Vec<u8> {
|
||||
let mut vec = Vec::with_capacity(hash.len());
|
||||
vec.extend_from_slice(hash);
|
||||
vec
|
||||
}
|
||||
|
||||
/// Hashes the result of [compute_disco()] with one of the supported [hash
|
||||
/// algorithms](../hashes/enum.Algo.html).
|
||||
pub fn hash_ecaps2(data: &[u8], algo: Algo) -> Result<Hash, String> {
|
||||
Ok(Hash {
|
||||
hash: match algo {
|
||||
Algo::Sha_256 => {
|
||||
let hash = Sha256::digest(data);
|
||||
get_hash_vec(hash.as_slice())
|
||||
}
|
||||
Algo::Sha_512 => {
|
||||
let hash = Sha512::digest(data);
|
||||
get_hash_vec(hash.as_slice())
|
||||
}
|
||||
Algo::Sha3_256 => {
|
||||
let hash = Sha3_256::digest(data);
|
||||
get_hash_vec(hash.as_slice())
|
||||
}
|
||||
Algo::Sha3_512 => {
|
||||
let hash = Sha3_512::digest(data);
|
||||
get_hash_vec(hash.as_slice())
|
||||
}
|
||||
Algo::Blake2b_256 => {
|
||||
let mut hasher = VarBlake2b::new(32).unwrap();
|
||||
hasher.input(data);
|
||||
hasher.vec_result()
|
||||
}
|
||||
Algo::Blake2b_512 => {
|
||||
let mut hasher = VarBlake2b::new(64).unwrap();
|
||||
hasher.input(data);
|
||||
hasher.vec_result()
|
||||
}
|
||||
Algo::Sha_1 => return Err(String::from("Disabled algorithm sha-1: unsafe.")),
|
||||
Algo::Unknown(algo) => return Err(format!("Unknown algorithm: {}.", algo)),
|
||||
},
|
||||
algo,
|
||||
})
|
||||
}
|
||||
|
||||
/// Helper function to create the query for the disco#info corresponding to an
|
||||
/// ecaps2 hash.
|
||||
pub fn query_ecaps2(hash: Hash) -> DiscoInfoQuery {
|
||||
DiscoInfoQuery {
|
||||
node: Some(format!(
|
||||
"{}#{}.{}",
|
||||
ns::ECAPS2,
|
||||
String::from(hash.algo),
|
||||
base64::encode(&hash.hash)
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::util::error::Error;
|
||||
use crate::Element;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(ECaps2, 12);
|
||||
}
|
||||
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(ECaps2, 24);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse() {
|
||||
let elem: Element = "<c xmlns='urn:xmpp:caps'><hash xmlns='urn:xmpp:hashes:2' algo='sha-256'>K1Njy3HZBThlo4moOD5gBGhn0U0oK7/CbfLlIUDi6o4=</hash><hash xmlns='urn:xmpp:hashes:2' algo='sha3-256'>+sDTQqBmX6iG/X3zjt06fjZMBBqL/723knFIyRf0sg8=</hash></c>".parse().unwrap();
|
||||
let ecaps2 = ECaps2::try_from(elem).unwrap();
|
||||
assert_eq!(ecaps2.hashes.len(), 2);
|
||||
assert_eq!(ecaps2.hashes[0].algo, Algo::Sha_256);
|
||||
assert_eq!(
|
||||
ecaps2.hashes[0].hash,
|
||||
base64::decode("K1Njy3HZBThlo4moOD5gBGhn0U0oK7/CbfLlIUDi6o4=").unwrap()
|
||||
);
|
||||
assert_eq!(ecaps2.hashes[1].algo, Algo::Sha3_256);
|
||||
assert_eq!(
|
||||
ecaps2.hashes[1].hash,
|
||||
base64::decode("+sDTQqBmX6iG/X3zjt06fjZMBBqL/723knFIyRf0sg8=").unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_child() {
|
||||
let elem: Element = "<c xmlns='urn:xmpp:caps'><hash xmlns='urn:xmpp:hashes:2' algo='sha-256'>K1Njy3HZBThlo4moOD5gBGhn0U0oK7/CbfLlIUDi6o4=</hash><hash xmlns='urn:xmpp:hashes:1' algo='sha3-256'>+sDTQqBmX6iG/X3zjt06fjZMBBqL/723knFIyRf0sg8=</hash></c>".parse().unwrap();
|
||||
let error = ECaps2::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "Unknown child in c element.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple() {
|
||||
let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='client' type='pc'/><feature var='http://jabber.org/protocol/disco#info'/></query>".parse().unwrap();
|
||||
let disco = DiscoInfoResult::try_from(elem).unwrap();
|
||||
let ecaps2 = compute_disco(&disco).unwrap();
|
||||
assert_eq!(ecaps2.len(), 54);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_xep_ex1() {
|
||||
let elem: Element = r#"
|
||||
<query xmlns="http://jabber.org/protocol/disco#info">
|
||||
<identity category="client" name="BombusMod" type="mobile"/>
|
||||
<feature var="http://jabber.org/protocol/si"/>
|
||||
<feature var="http://jabber.org/protocol/bytestreams"/>
|
||||
<feature var="http://jabber.org/protocol/chatstates"/>
|
||||
<feature var="http://jabber.org/protocol/disco#info"/>
|
||||
<feature var="http://jabber.org/protocol/disco#items"/>
|
||||
<feature var="urn:xmpp:ping"/>
|
||||
<feature var="jabber:iq:time"/>
|
||||
<feature var="jabber:iq:privacy"/>
|
||||
<feature var="jabber:iq:version"/>
|
||||
<feature var="http://jabber.org/protocol/rosterx"/>
|
||||
<feature var="urn:xmpp:time"/>
|
||||
<feature var="jabber:x:oob"/>
|
||||
<feature var="http://jabber.org/protocol/ibb"/>
|
||||
<feature var="http://jabber.org/protocol/si/profile/file-transfer"/>
|
||||
<feature var="urn:xmpp:receipts"/>
|
||||
<feature var="jabber:iq:roster"/>
|
||||
<feature var="jabber:iq:last"/>
|
||||
</query>
|
||||
"#
|
||||
.parse()
|
||||
.unwrap();
|
||||
let expected = vec![
|
||||
104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112,
|
||||
114, 111, 116, 111, 99, 111, 108, 47, 98, 121, 116, 101, 115, 116, 114, 101, 97, 109,
|
||||
115, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103,
|
||||
47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 99, 104, 97, 116, 115, 116, 97, 116,
|
||||
101, 115, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114,
|
||||
103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 100, 105, 115, 99, 111, 35, 105,
|
||||
110, 102, 111, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111,
|
||||
114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 100, 105, 115, 99, 111, 35,
|
||||
105, 116, 101, 109, 115, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114,
|
||||
46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 105, 98, 98, 31, 104,
|
||||
116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114,
|
||||
111, 116, 111, 99, 111, 108, 47, 114, 111, 115, 116, 101, 114, 120, 31, 104, 116, 116,
|
||||
112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116,
|
||||
111, 99, 111, 108, 47, 115, 105, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98,
|
||||
101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 115, 105,
|
||||
47, 112, 114, 111, 102, 105, 108, 101, 47, 102, 105, 108, 101, 45, 116, 114, 97, 110,
|
||||
115, 102, 101, 114, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 108, 97, 115, 116,
|
||||
31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 112, 114, 105, 118, 97, 99, 121, 31,
|
||||
106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 114, 111, 115, 116, 101, 114, 31, 106, 97,
|
||||
98, 98, 101, 114, 58, 105, 113, 58, 116, 105, 109, 101, 31, 106, 97, 98, 98, 101, 114,
|
||||
58, 105, 113, 58, 118, 101, 114, 115, 105, 111, 110, 31, 106, 97, 98, 98, 101, 114, 58,
|
||||
120, 58, 111, 111, 98, 31, 117, 114, 110, 58, 120, 109, 112, 112, 58, 112, 105, 110,
|
||||
103, 31, 117, 114, 110, 58, 120, 109, 112, 112, 58, 114, 101, 99, 101, 105, 112, 116,
|
||||
115, 31, 117, 114, 110, 58, 120, 109, 112, 112, 58, 116, 105, 109, 101, 31, 28, 99,
|
||||
108, 105, 101, 110, 116, 31, 109, 111, 98, 105, 108, 101, 31, 31, 66, 111, 109, 98,
|
||||
117, 115, 77, 111, 100, 31, 30, 28, 28,
|
||||
];
|
||||
let disco = DiscoInfoResult::try_from(elem).unwrap();
|
||||
let ecaps2 = compute_disco(&disco).unwrap();
|
||||
assert_eq!(ecaps2.len(), 0x1d9);
|
||||
assert_eq!(ecaps2, expected);
|
||||
|
||||
let sha_256 = hash_ecaps2(&ecaps2, Algo::Sha_256).unwrap();
|
||||
assert_eq!(
|
||||
sha_256.hash,
|
||||
base64::decode("kzBZbkqJ3ADrj7v08reD1qcWUwNGHaidNUgD7nHpiw8=").unwrap()
|
||||
);
|
||||
let sha3_256 = hash_ecaps2(&ecaps2, Algo::Sha3_256).unwrap();
|
||||
assert_eq!(
|
||||
sha3_256.hash,
|
||||
base64::decode("79mdYAfU9rEdTOcWDO7UEAt6E56SUzk/g6TnqUeuD9Q=").unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_xep_ex2() {
|
||||
let elem: Element = r#"
|
||||
<query xmlns="http://jabber.org/protocol/disco#info">
|
||||
<identity category="client" name="Tkabber" type="pc" xml:lang="en"/>
|
||||
<identity category="client" name="Ткаббер" type="pc" xml:lang="ru"/>
|
||||
<feature var="games:board"/>
|
||||
<feature var="http://jabber.org/protocol/activity"/>
|
||||
<feature var="http://jabber.org/protocol/activity+notify"/>
|
||||
<feature var="http://jabber.org/protocol/bytestreams"/>
|
||||
<feature var="http://jabber.org/protocol/chatstates"/>
|
||||
<feature var="http://jabber.org/protocol/commands"/>
|
||||
<feature var="http://jabber.org/protocol/disco#info"/>
|
||||
<feature var="http://jabber.org/protocol/disco#items"/>
|
||||
<feature var="http://jabber.org/protocol/evil"/>
|
||||
<feature var="http://jabber.org/protocol/feature-neg"/>
|
||||
<feature var="http://jabber.org/protocol/geoloc"/>
|
||||
<feature var="http://jabber.org/protocol/geoloc+notify"/>
|
||||
<feature var="http://jabber.org/protocol/ibb"/>
|
||||
<feature var="http://jabber.org/protocol/iqibb"/>
|
||||
<feature var="http://jabber.org/protocol/mood"/>
|
||||
<feature var="http://jabber.org/protocol/mood+notify"/>
|
||||
<feature var="http://jabber.org/protocol/rosterx"/>
|
||||
<feature var="http://jabber.org/protocol/si"/>
|
||||
<feature var="http://jabber.org/protocol/si/profile/file-transfer"/>
|
||||
<feature var="http://jabber.org/protocol/tune"/>
|
||||
<feature var="http://www.facebook.com/xmpp/messages"/>
|
||||
<feature var="http://www.xmpp.org/extensions/xep-0084.html#ns-metadata+notify"/>
|
||||
<feature var="jabber:iq:avatar"/>
|
||||
<feature var="jabber:iq:browse"/>
|
||||
<feature var="jabber:iq:dtcp"/>
|
||||
<feature var="jabber:iq:filexfer"/>
|
||||
<feature var="jabber:iq:ibb"/>
|
||||
<feature var="jabber:iq:inband"/>
|
||||
<feature var="jabber:iq:jidlink"/>
|
||||
<feature var="jabber:iq:last"/>
|
||||
<feature var="jabber:iq:oob"/>
|
||||
<feature var="jabber:iq:privacy"/>
|
||||
<feature var="jabber:iq:roster"/>
|
||||
<feature var="jabber:iq:time"/>
|
||||
<feature var="jabber:iq:version"/>
|
||||
<feature var="jabber:x:data"/>
|
||||
<feature var="jabber:x:event"/>
|
||||
<feature var="jabber:x:oob"/>
|
||||
<feature var="urn:xmpp:avatar:metadata+notify"/>
|
||||
<feature var="urn:xmpp:ping"/>
|
||||
<feature var="urn:xmpp:receipts"/>
|
||||
<feature var="urn:xmpp:time"/>
|
||||
<x xmlns="jabber:x:data" type="result">
|
||||
<field type="hidden" var="FORM_TYPE">
|
||||
<value>urn:xmpp:dataforms:softwareinfo</value>
|
||||
</field>
|
||||
<field var="software">
|
||||
<value>Tkabber</value>
|
||||
</field>
|
||||
<field var="software_version">
|
||||
<value>0.11.1-svn-20111216-mod (Tcl/Tk 8.6b2)</value>
|
||||
</field>
|
||||
<field var="os">
|
||||
<value>Windows</value>
|
||||
</field>
|
||||
<field var="os_version">
|
||||
<value>XP</value>
|
||||
</field>
|
||||
</x>
|
||||
</query>
|
||||
"#
|
||||
.parse()
|
||||
.unwrap();
|
||||
let expected = vec![
|
||||
103, 97, 109, 101, 115, 58, 98, 111, 97, 114, 100, 31, 104, 116, 116, 112, 58, 47, 47,
|
||||
106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111,
|
||||
108, 47, 97, 99, 116, 105, 118, 105, 116, 121, 31, 104, 116, 116, 112, 58, 47, 47, 106,
|
||||
97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47,
|
||||
97, 99, 116, 105, 118, 105, 116, 121, 43, 110, 111, 116, 105, 102, 121, 31, 104, 116,
|
||||
116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111,
|
||||
116, 111, 99, 111, 108, 47, 98, 121, 116, 101, 115, 116, 114, 101, 97, 109, 115, 31,
|
||||
104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112,
|
||||
114, 111, 116, 111, 99, 111, 108, 47, 99, 104, 97, 116, 115, 116, 97, 116, 101, 115,
|
||||
31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47,
|
||||
112, 114, 111, 116, 111, 99, 111, 108, 47, 99, 111, 109, 109, 97, 110, 100, 115, 31,
|
||||
104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112,
|
||||
114, 111, 116, 111, 99, 111, 108, 47, 100, 105, 115, 99, 111, 35, 105, 110, 102, 111,
|
||||
31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47,
|
||||
112, 114, 111, 116, 111, 99, 111, 108, 47, 100, 105, 115, 99, 111, 35, 105, 116, 101,
|
||||
109, 115, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114,
|
||||
103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 101, 118, 105, 108, 31, 104, 116,
|
||||
116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111,
|
||||
116, 111, 99, 111, 108, 47, 102, 101, 97, 116, 117, 114, 101, 45, 110, 101, 103, 31,
|
||||
104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112,
|
||||
114, 111, 116, 111, 99, 111, 108, 47, 103, 101, 111, 108, 111, 99, 31, 104, 116, 116,
|
||||
112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116,
|
||||
111, 99, 111, 108, 47, 103, 101, 111, 108, 111, 99, 43, 110, 111, 116, 105, 102, 121,
|
||||
31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47,
|
||||
112, 114, 111, 116, 111, 99, 111, 108, 47, 105, 98, 98, 31, 104, 116, 116, 112, 58, 47,
|
||||
47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111,
|
||||
108, 47, 105, 113, 105, 98, 98, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98,
|
||||
101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 109, 111,
|
||||
111, 100, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114,
|
||||
103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 109, 111, 111, 100, 43, 110, 111,
|
||||
116, 105, 102, 121, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46,
|
||||
111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 114, 111, 115, 116, 101,
|
||||
114, 120, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114,
|
||||
103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 115, 105, 31, 104, 116, 116, 112,
|
||||
58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111,
|
||||
99, 111, 108, 47, 115, 105, 47, 112, 114, 111, 102, 105, 108, 101, 47, 102, 105, 108,
|
||||
101, 45, 116, 114, 97, 110, 115, 102, 101, 114, 31, 104, 116, 116, 112, 58, 47, 47,
|
||||
106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111,
|
||||
108, 47, 116, 117, 110, 101, 31, 104, 116, 116, 112, 58, 47, 47, 119, 119, 119, 46,
|
||||
102, 97, 99, 101, 98, 111, 111, 107, 46, 99, 111, 109, 47, 120, 109, 112, 112, 47, 109,
|
||||
101, 115, 115, 97, 103, 101, 115, 31, 104, 116, 116, 112, 58, 47, 47, 119, 119, 119,
|
||||
46, 120, 109, 112, 112, 46, 111, 114, 103, 47, 101, 120, 116, 101, 110, 115, 105, 111,
|
||||
110, 115, 47, 120, 101, 112, 45, 48, 48, 56, 52, 46, 104, 116, 109, 108, 35, 110, 115,
|
||||
45, 109, 101, 116, 97, 100, 97, 116, 97, 43, 110, 111, 116, 105, 102, 121, 31, 106, 97,
|
||||
98, 98, 101, 114, 58, 105, 113, 58, 97, 118, 97, 116, 97, 114, 31, 106, 97, 98, 98,
|
||||
101, 114, 58, 105, 113, 58, 98, 114, 111, 119, 115, 101, 31, 106, 97, 98, 98, 101, 114,
|
||||
58, 105, 113, 58, 100, 116, 99, 112, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58,
|
||||
102, 105, 108, 101, 120, 102, 101, 114, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113,
|
||||
58, 105, 98, 98, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 105, 110, 98, 97,
|
||||
110, 100, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 106, 105, 100, 108, 105,
|
||||
110, 107, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 108, 97, 115, 116, 31, 106,
|
||||
97, 98, 98, 101, 114, 58, 105, 113, 58, 111, 111, 98, 31, 106, 97, 98, 98, 101, 114,
|
||||
58, 105, 113, 58, 112, 114, 105, 118, 97, 99, 121, 31, 106, 97, 98, 98, 101, 114, 58,
|
||||
105, 113, 58, 114, 111, 115, 116, 101, 114, 31, 106, 97, 98, 98, 101, 114, 58, 105,
|
||||
113, 58, 116, 105, 109, 101, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 118, 101,
|
||||
114, 115, 105, 111, 110, 31, 106, 97, 98, 98, 101, 114, 58, 120, 58, 100, 97, 116, 97,
|
||||
31, 106, 97, 98, 98, 101, 114, 58, 120, 58, 101, 118, 101, 110, 116, 31, 106, 97, 98,
|
||||
98, 101, 114, 58, 120, 58, 111, 111, 98, 31, 117, 114, 110, 58, 120, 109, 112, 112, 58,
|
||||
97, 118, 97, 116, 97, 114, 58, 109, 101, 116, 97, 100, 97, 116, 97, 43, 110, 111, 116,
|
||||
105, 102, 121, 31, 117, 114, 110, 58, 120, 109, 112, 112, 58, 112, 105, 110, 103, 31,
|
||||
117, 114, 110, 58, 120, 109, 112, 112, 58, 114, 101, 99, 101, 105, 112, 116, 115, 31,
|
||||
117, 114, 110, 58, 120, 109, 112, 112, 58, 116, 105, 109, 101, 31, 28, 99, 108, 105,
|
||||
101, 110, 116, 31, 112, 99, 31, 101, 110, 31, 84, 107, 97, 98, 98, 101, 114, 31, 30,
|
||||
99, 108, 105, 101, 110, 116, 31, 112, 99, 31, 114, 117, 31, 208, 162, 208, 186, 208,
|
||||
176, 208, 177, 208, 177, 208, 181, 209, 128, 31, 30, 28, 70, 79, 82, 77, 95, 84, 89,
|
||||
80, 69, 31, 117, 114, 110, 58, 120, 109, 112, 112, 58, 100, 97, 116, 97, 102, 111, 114,
|
||||
109, 115, 58, 115, 111, 102, 116, 119, 97, 114, 101, 105, 110, 102, 111, 31, 30, 111,
|
||||
115, 31, 87, 105, 110, 100, 111, 119, 115, 31, 30, 111, 115, 95, 118, 101, 114, 115,
|
||||
105, 111, 110, 31, 88, 80, 31, 30, 115, 111, 102, 116, 119, 97, 114, 101, 31, 84, 107,
|
||||
97, 98, 98, 101, 114, 31, 30, 115, 111, 102, 116, 119, 97, 114, 101, 95, 118, 101, 114,
|
||||
115, 105, 111, 110, 31, 48, 46, 49, 49, 46, 49, 45, 115, 118, 110, 45, 50, 48, 49, 49,
|
||||
49, 50, 49, 54, 45, 109, 111, 100, 32, 40, 84, 99, 108, 47, 84, 107, 32, 56, 46, 54,
|
||||
98, 50, 41, 31, 30, 29, 28,
|
||||
];
|
||||
let disco = DiscoInfoResult::try_from(elem).unwrap();
|
||||
let ecaps2 = compute_disco(&disco).unwrap();
|
||||
assert_eq!(ecaps2.len(), 0x543);
|
||||
assert_eq!(ecaps2, expected);
|
||||
|
||||
let sha_256 = hash_ecaps2(&ecaps2, Algo::Sha_256).unwrap();
|
||||
assert_eq!(
|
||||
sha_256.hash,
|
||||
base64::decode("u79ZroNJbdSWhdSp311mddz44oHHPsEBntQ5b1jqBSY=").unwrap()
|
||||
);
|
||||
let sha3_256 = hash_ecaps2(&ecaps2, Algo::Sha3_256).unwrap();
|
||||
assert_eq!(
|
||||
sha3_256.hash,
|
||||
base64::decode("XpUJzLAc93258sMECZ3FJpebkzuyNXDzRNwQog8eycg=").unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_blake2b_512() {
|
||||
let hash = hash_ecaps2("abc".as_bytes(), Algo::Blake2b_512).unwrap();
|
||||
let known_hash: Vec<u8> = vec![
|
||||
0xBA, 0x80, 0xA5, 0x3F, 0x98, 0x1C, 0x4D, 0x0D, 0x6A, 0x27, 0x97, 0xB6, 0x9F, 0x12,
|
||||
0xF6, 0xE9, 0x4C, 0x21, 0x2F, 0x14, 0x68, 0x5A, 0xC4, 0xB7, 0x4B, 0x12, 0xBB, 0x6F,
|
||||
0xDB, 0xFF, 0xA2, 0xD1, 0x7D, 0x87, 0xC5, 0x39, 0x2A, 0xAB, 0x79, 0x2D, 0xC2, 0x52,
|
||||
0xD5, 0xDE, 0x45, 0x33, 0xCC, 0x95, 0x18, 0xD3, 0x8A, 0xA8, 0xDB, 0xF1, 0x92, 0x5A,
|
||||
0xB9, 0x23, 0x86, 0xED, 0xD4, 0x00, 0x99, 0x23,
|
||||
];
|
||||
assert_eq!(hash.hash, known_hash);
|
||||
}
|
||||
}
|
||||
96
xmpp-parsers/src/eme.rs
Normal file
96
xmpp-parsers/src/eme.rs
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
use crate::message::MessagePayload;
|
||||
|
||||
generate_element!(
|
||||
/// Structure representing an `<encryption xmlns='urn:xmpp:eme:0'/>` element.
|
||||
ExplicitMessageEncryption, "encryption", EME,
|
||||
attributes: [
|
||||
/// Namespace of the encryption scheme used.
|
||||
namespace: Required<String> = "namespace",
|
||||
|
||||
/// User-friendly name for the encryption scheme, should be `None` for OTR,
|
||||
/// legacy OpenPGP and OX.
|
||||
name: Option<String> = "name",
|
||||
]
|
||||
);
|
||||
|
||||
impl MessagePayload for ExplicitMessageEncryption {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::util::error::Error;
|
||||
use crate::Element;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(ExplicitMessageEncryption, 24);
|
||||
}
|
||||
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(ExplicitMessageEncryption, 48);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple() {
|
||||
let elem: Element = "<encryption xmlns='urn:xmpp:eme:0' namespace='urn:xmpp:otr:0'/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let encryption = ExplicitMessageEncryption::try_from(elem).unwrap();
|
||||
assert_eq!(encryption.namespace, "urn:xmpp:otr:0");
|
||||
assert_eq!(encryption.name, None);
|
||||
|
||||
let elem: Element = "<encryption xmlns='urn:xmpp:eme:0' namespace='some.unknown.mechanism' name='SuperMechanism'/>".parse().unwrap();
|
||||
let encryption = ExplicitMessageEncryption::try_from(elem).unwrap();
|
||||
assert_eq!(encryption.namespace, "some.unknown.mechanism");
|
||||
assert_eq!(encryption.name, Some(String::from("SuperMechanism")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unknown() {
|
||||
let elem: Element = "<replace xmlns='urn:xmpp:message-correct:0'/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let error = ExplicitMessageEncryption::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "This is not a encryption element.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_child() {
|
||||
let elem: Element = "<encryption xmlns='urn:xmpp:eme:0'><coucou/></encryption>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let error = ExplicitMessageEncryption::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "Unknown child in encryption element.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialise() {
|
||||
let elem: Element = "<encryption xmlns='urn:xmpp:eme:0' namespace='coucou'/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let eme = ExplicitMessageEncryption {
|
||||
namespace: String::from("coucou"),
|
||||
name: None,
|
||||
};
|
||||
let elem2 = eme.into();
|
||||
assert_eq!(elem, elem2);
|
||||
}
|
||||
}
|
||||
74
xmpp-parsers/src/forwarding.rs
Normal file
74
xmpp-parsers/src/forwarding.rs
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
use crate::delay::Delay;
|
||||
use crate::message::Message;
|
||||
|
||||
generate_element!(
|
||||
/// Contains a forwarded stanza, either standalone or part of another
|
||||
/// extension (such as carbons).
|
||||
Forwarded, "forwarded", FORWARD,
|
||||
children: [
|
||||
/// When the stanza originally got sent.
|
||||
delay: Option<Delay> = ("delay", DELAY) => Delay,
|
||||
|
||||
// XXX: really? Option?
|
||||
/// The stanza being forwarded.
|
||||
stanza: Option<Message> = ("message", DEFAULT_NS) => Message
|
||||
|
||||
// TODO: also handle the two other stanza possibilities.
|
||||
]
|
||||
);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::util::error::Error;
|
||||
use crate::Element;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(Forwarded, 212);
|
||||
}
|
||||
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(Forwarded, 408);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple() {
|
||||
let elem: Element = "<forwarded xmlns='urn:xmpp:forward:0'/>".parse().unwrap();
|
||||
Forwarded::try_from(elem).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_child() {
|
||||
let elem: Element = "<forwarded xmlns='urn:xmpp:forward:0'><coucou/></forwarded>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let error = Forwarded::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "Unknown child in forwarded element.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialise() {
|
||||
let elem: Element = "<forwarded xmlns='urn:xmpp:forward:0'/>".parse().unwrap();
|
||||
let forwarded = Forwarded {
|
||||
delay: None,
|
||||
stanza: None,
|
||||
};
|
||||
let elem2 = forwarded.into();
|
||||
assert_eq!(elem, elem2);
|
||||
}
|
||||
}
|
||||
269
xmpp-parsers/src/hashes.rs
Normal file
269
xmpp-parsers/src/hashes.rs
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
use crate::util::error::Error;
|
||||
use crate::util::helpers::Base64;
|
||||
use minidom::IntoAttributeValue;
|
||||
use std::num::ParseIntError;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::str::FromStr;
|
||||
|
||||
/// List of the algorithms we support, or Unknown.
|
||||
#[allow(non_camel_case_types)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum Algo {
|
||||
/// The Secure Hash Algorithm 1, with known vulnerabilities, do not use it.
|
||||
///
|
||||
/// See https://tools.ietf.org/html/rfc3174
|
||||
Sha_1,
|
||||
|
||||
/// The Secure Hash Algorithm 2, in its 256-bit version.
|
||||
///
|
||||
/// See https://tools.ietf.org/html/rfc6234
|
||||
Sha_256,
|
||||
|
||||
/// The Secure Hash Algorithm 2, in its 512-bit version.
|
||||
///
|
||||
/// See https://tools.ietf.org/html/rfc6234
|
||||
Sha_512,
|
||||
|
||||
/// The Secure Hash Algorithm 3, based on Keccak, in its 256-bit version.
|
||||
///
|
||||
/// See https://keccak.team/files/Keccak-submission-3.pdf
|
||||
Sha3_256,
|
||||
|
||||
/// The Secure Hash Algorithm 3, based on Keccak, in its 512-bit version.
|
||||
///
|
||||
/// See https://keccak.team/files/Keccak-submission-3.pdf
|
||||
Sha3_512,
|
||||
|
||||
/// The BLAKE2 hash algorithm, for a 256-bit output.
|
||||
///
|
||||
/// See https://tools.ietf.org/html/rfc7693
|
||||
Blake2b_256,
|
||||
|
||||
/// The BLAKE2 hash algorithm, for a 512-bit output.
|
||||
///
|
||||
/// See https://tools.ietf.org/html/rfc7693
|
||||
Blake2b_512,
|
||||
|
||||
/// An unknown hash not in this list, you can probably reject it.
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
impl FromStr for Algo {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Algo, Error> {
|
||||
Ok(match s {
|
||||
"" => return Err(Error::ParseError("'algo' argument 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.");
|
||||
}
|
||||
}
|
||||
172
xmpp-parsers/src/ibb.rs
Normal file
172
xmpp-parsers/src/ibb.rs
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
use crate::util::helpers::Base64;
|
||||
use crate::iq::IqSetPayload;
|
||||
|
||||
generate_id!(
|
||||
/// An identifier matching a stream.
|
||||
StreamId
|
||||
);
|
||||
|
||||
generate_attribute!(
|
||||
/// Which stanza type to use to exchange data.
|
||||
Stanza, "stanza", {
|
||||
/// `<iq/>` gives a feedback on whether the chunk has been received or not,
|
||||
/// which is useful in the case the recipient might not receive them in a
|
||||
/// timely manner, or to do your own throttling based on the results.
|
||||
Iq => "iq",
|
||||
|
||||
/// `<message/>` can be faster, since it 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.");
|
||||
}
|
||||
}
|
||||
247
xmpp-parsers/src/ibr.rs
Normal file
247
xmpp-parsers/src/ibr.rs
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
use crate::data_forms::DataForm;
|
||||
use crate::util::error::Error;
|
||||
use crate::iq::{IqGetPayload, IqResultPayload, IqSetPayload};
|
||||
use crate::ns;
|
||||
use crate::Element;
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
/// Query for registering against a service.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Query {
|
||||
/// Deprecated fixed list of possible fields to fill before the user can
|
||||
/// register.
|
||||
pub fields: HashMap<String, String>,
|
||||
|
||||
/// Whether this account is already registered.
|
||||
pub registered: bool,
|
||||
|
||||
/// Whether to remove this account.
|
||||
pub remove: bool,
|
||||
|
||||
/// A data form the user must fill before being allowed to register.
|
||||
pub form: Option<DataForm>,
|
||||
// Not yet implemented.
|
||||
//pub oob: Option<Oob>,
|
||||
}
|
||||
|
||||
impl IqGetPayload for Query {}
|
||||
impl IqSetPayload for Query {}
|
||||
impl IqResultPayload for Query {}
|
||||
|
||||
impl TryFrom<Element> for Query {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(elem: Element) -> Result<Query, Error> {
|
||||
check_self!(elem, "query", REGISTER, "IBR query");
|
||||
let mut query = Query {
|
||||
registered: false,
|
||||
fields: HashMap::new(),
|
||||
remove: false,
|
||||
form: None,
|
||||
};
|
||||
for child in elem.children() {
|
||||
let namespace = child.ns().unwrap();
|
||||
if namespace == ns::REGISTER {
|
||||
let name = child.name();
|
||||
let fields = vec![
|
||||
"address",
|
||||
"city",
|
||||
"date",
|
||||
"email",
|
||||
"first",
|
||||
"instructions",
|
||||
"key",
|
||||
"last",
|
||||
"misc",
|
||||
"name",
|
||||
"nick",
|
||||
"password",
|
||||
"phone",
|
||||
"state",
|
||||
"text",
|
||||
"url",
|
||||
"username",
|
||||
"zip",
|
||||
];
|
||||
if fields.binary_search(&name).is_ok() {
|
||||
query.fields.insert(name.to_owned(), child.text());
|
||||
} else if name == "registered" {
|
||||
query.registered = true;
|
||||
} else if name == "remove" {
|
||||
query.remove = true;
|
||||
} else {
|
||||
return Err(Error::ParseError("Wrong field in ibr element."));
|
||||
}
|
||||
} else if child.is("x", ns::DATA_FORMS) {
|
||||
query.form = Some(DataForm::try_from(child.clone())?);
|
||||
} else {
|
||||
return Err(Error::ParseError("Unknown child in ibr element."));
|
||||
}
|
||||
}
|
||||
Ok(query)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Query> for Element {
|
||||
fn from(query: Query) -> Element {
|
||||
Element::builder("query")
|
||||
.ns(ns::REGISTER)
|
||||
.append_all(if query.registered {
|
||||
Some(Element::builder("registered").ns(ns::REGISTER))
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.append_all(
|
||||
query
|
||||
.fields
|
||||
.into_iter()
|
||||
.map(|(name, value)| Element::builder(name).ns(ns::REGISTER).append(value))
|
||||
)
|
||||
.append_all(if query.remove {
|
||||
Some(Element::builder("remove").ns(ns::REGISTER))
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.append_all(query.form.map(Element::from))
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::util::compare_elements::NamespaceAwareCompare;
|
||||
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(Query, 96);
|
||||
}
|
||||
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(Query, 168);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple() {
|
||||
let elem: Element = "<query xmlns='jabber:iq:register'/>".parse().unwrap();
|
||||
Query::try_from(elem).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ex2() {
|
||||
let elem: Element = r#"
|
||||
<query xmlns='jabber:iq:register'>
|
||||
<instructions>
|
||||
Choose a username and password for use with this service.
|
||||
Please also provide your email address.
|
||||
</instructions>
|
||||
<username/>
|
||||
<password/>
|
||||
<email/>
|
||||
</query>
|
||||
"#
|
||||
.parse()
|
||||
.unwrap();
|
||||
let query = Query::try_from(elem).unwrap();
|
||||
assert_eq!(query.registered, false);
|
||||
assert_eq!(query.fields["instructions"], "\n Choose a username and password for use with this service.\n Please also provide your email address.\n ");
|
||||
assert_eq!(query.fields["username"], "");
|
||||
assert_eq!(query.fields["password"], "");
|
||||
assert_eq!(query.fields["email"], "");
|
||||
assert_eq!(query.fields.contains_key("name"), false);
|
||||
|
||||
// FIXME: HashMap 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));
|
||||
}
|
||||
}
|
||||
147
xmpp-parsers/src/idle.rs
Normal file
147
xmpp-parsers/src/idle.rs
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
use crate::date::DateTime;
|
||||
use crate::presence::PresencePayload;
|
||||
|
||||
generate_element!(
|
||||
/// Represents the last time the user interacted with their system.
|
||||
Idle, "idle", IDLE,
|
||||
attributes: [
|
||||
/// The time at which the user stopped interacting.
|
||||
since: Required<DateTime> = "since",
|
||||
]
|
||||
);
|
||||
|
||||
impl PresencePayload for Idle {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::util::error::Error;
|
||||
use crate::Element;
|
||||
use std::error::Error as StdError;
|
||||
use std::str::FromStr;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(Idle, 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple() {
|
||||
let elem: Element = "<idle xmlns='urn:xmpp:idle:1' since='2017-05-21T20:19:55+01:00'/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
Idle::try_from(elem).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_child() {
|
||||
let elem: Element = "<idle xmlns='urn:xmpp:idle:1'><coucou/></idle>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let error = Idle::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "Unknown child in idle element.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_id() {
|
||||
let elem: Element = "<idle xmlns='urn:xmpp:idle:1'/>".parse().unwrap();
|
||||
let error = Idle::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "Required attribute 'since' missing.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_date() {
|
||||
// There is no thirteenth month.
|
||||
let elem: Element = "<idle xmlns='urn:xmpp:idle:1' since='2017-13-01T12:23:34Z'/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let error = Idle::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ChronoParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message.description(), "input is out of range");
|
||||
|
||||
// Timezone ≥24:00 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);
|
||||
}
|
||||
}
|
||||
447
xmpp-parsers/src/iq.rs
Normal file
447
xmpp-parsers/src/iq.rs
Normal file
|
|
@ -0,0 +1,447 @@
|
|||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||
// Copyright (c) 2017 Maxime “pep” Buquet <pep@bouah.net>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
use crate::util::error::Error;
|
||||
use crate::ns;
|
||||
use crate::stanza_error::StanzaError;
|
||||
use jid::Jid;
|
||||
use crate::Element;
|
||||
use minidom::IntoAttributeValue;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
/// Should be implemented on every known payload of an `<iq type='get'/>`.
|
||||
pub trait IqGetPayload: TryFrom<Element> + Into<Element> {}
|
||||
|
||||
/// Should be implemented on every known payload of an `<iq type='set'/>`.
|
||||
pub trait IqSetPayload: TryFrom<Element> + Into<Element> {}
|
||||
|
||||
/// Should be implemented on every known payload of an `<iq type='result'/>`.
|
||||
pub trait IqResultPayload: TryFrom<Element> + Into<Element> {}
|
||||
|
||||
/// Represents one of the four possible iq types.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum IqType {
|
||||
/// This is a request for accessing some data.
|
||||
Get(Element),
|
||||
|
||||
/// This is a request for modifying some data.
|
||||
Set(Element),
|
||||
|
||||
/// This is a result containing some data.
|
||||
Result(Option<Element>),
|
||||
|
||||
/// A get or set request failed.
|
||||
Error(StanzaError),
|
||||
}
|
||||
|
||||
impl<'a> IntoAttributeValue for &'a IqType {
|
||||
fn into_attribute_value(self) -> Option<String> {
|
||||
Some(
|
||||
match *self {
|
||||
IqType::Get(_) => "get",
|
||||
IqType::Set(_) => "set",
|
||||
IqType::Result(_) => "result",
|
||||
IqType::Error(_) => "error",
|
||||
}
|
||||
.to_owned(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// The main structure representing the `<iq/>` stanza.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Iq {
|
||||
/// The JID emitting this stanza.
|
||||
pub from: Option<Jid>,
|
||||
|
||||
/// The recipient of this stanza.
|
||||
pub to: Option<Jid>,
|
||||
|
||||
/// The @id attribute of this stanza, which is required in order to match a
|
||||
/// request with its result/error.
|
||||
pub id: String,
|
||||
|
||||
/// The payload content of this stanza.
|
||||
pub payload: IqType,
|
||||
}
|
||||
|
||||
impl Iq {
|
||||
/// Creates an `<iq/>` stanza containing a get request.
|
||||
pub fn from_get<S: Into<String>>(id: S, payload: impl IqGetPayload) -> Iq {
|
||||
Iq {
|
||||
from: None,
|
||||
to: None,
|
||||
id: id.into(),
|
||||
payload: IqType::Get(payload.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an `<iq/>` stanza containing a set request.
|
||||
pub fn from_set<S: Into<String>>(id: S, payload: impl IqSetPayload) -> Iq {
|
||||
Iq {
|
||||
from: None,
|
||||
to: None,
|
||||
id: id.into(),
|
||||
payload: IqType::Set(payload.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an `<iq/>` stanza containing a result.
|
||||
pub fn from_result<S: Into<String>>(id: S, payload: Option<impl IqResultPayload>) -> Iq {
|
||||
Iq {
|
||||
from: None,
|
||||
to: None,
|
||||
id: id.into(),
|
||||
payload: IqType::Result(payload.map(Into::into)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an `<iq/>` stanza containing an error.
|
||||
pub fn from_error<S: Into<String>>(id: S, payload: StanzaError) -> Iq {
|
||||
Iq {
|
||||
from: None,
|
||||
to: None,
|
||||
id: id.into(),
|
||||
payload: IqType::Error(payload),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the recipient of this stanza.
|
||||
pub fn with_to(mut self, to: Jid) -> Iq {
|
||||
self.to = Some(to);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the emitter of this stanza.
|
||||
pub fn with_from(mut self, from: Jid) -> Iq {
|
||||
self.from = Some(from);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the id of this stanza, in order to later match its response.
|
||||
pub fn with_id(mut self, id: String) -> Iq {
|
||||
self.id = id;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Element> for Iq {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(root: Element) -> Result<Iq, Error> {
|
||||
check_self!(root, "iq", DEFAULT_NS);
|
||||
let from = get_attr!(root, "from", Option);
|
||||
let to = get_attr!(root, "to", Option);
|
||||
let id = get_attr!(root, "id", Required);
|
||||
let type_: String = get_attr!(root, "type", Required);
|
||||
|
||||
let mut payload = None;
|
||||
let mut error_payload = None;
|
||||
for elem in root.children() {
|
||||
if payload.is_some() {
|
||||
return Err(Error::ParseError("Wrong number of children in iq element."));
|
||||
}
|
||||
if type_ == "error" {
|
||||
if elem.is("error", ns::DEFAULT_NS) {
|
||||
if error_payload.is_some() {
|
||||
return Err(Error::ParseError("Wrong number of children in iq element."));
|
||||
}
|
||||
error_payload = Some(StanzaError::try_from(elem.clone())?);
|
||||
} else if root.children().count() != 2 {
|
||||
return Err(Error::ParseError("Wrong number of children in iq element."));
|
||||
}
|
||||
} else {
|
||||
payload = Some(elem.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let type_ = if type_ == "get" {
|
||||
if let Some(payload) = payload {
|
||||
IqType::Get(payload)
|
||||
} else {
|
||||
return Err(Error::ParseError("Wrong number of children in iq element."));
|
||||
}
|
||||
} else if type_ == "set" {
|
||||
if let Some(payload) = payload {
|
||||
IqType::Set(payload)
|
||||
} else {
|
||||
return Err(Error::ParseError("Wrong number of children in iq element."));
|
||||
}
|
||||
} else if type_ == "result" {
|
||||
if let Some(payload) = payload {
|
||||
IqType::Result(Some(payload))
|
||||
} else {
|
||||
IqType::Result(None)
|
||||
}
|
||||
} else if type_ == "error" {
|
||||
if let Some(payload) = error_payload {
|
||||
IqType::Error(payload)
|
||||
} else {
|
||||
return Err(Error::ParseError("Wrong number of children in iq element."));
|
||||
}
|
||||
} else {
|
||||
return Err(Error::ParseError("Unknown iq type."));
|
||||
};
|
||||
|
||||
Ok(Iq {
|
||||
from,
|
||||
to,
|
||||
id,
|
||||
payload: type_,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Iq> for Element {
|
||||
fn from(iq: Iq) -> Element {
|
||||
let mut stanza = Element::builder("iq")
|
||||
.ns(ns::DEFAULT_NS)
|
||||
.attr("from", iq.from)
|
||||
.attr("to", iq.to)
|
||||
.attr("id", iq.id)
|
||||
.attr("type", &iq.payload)
|
||||
.build();
|
||||
let elem = match iq.payload {
|
||||
IqType::Get(elem) | IqType::Set(elem) | IqType::Result(Some(elem)) => elem,
|
||||
IqType::Error(error) => error.into(),
|
||||
IqType::Result(None) => return stanza,
|
||||
};
|
||||
stanza.append_child(elem);
|
||||
stanza
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::util::compare_elements::NamespaceAwareCompare;
|
||||
use crate::disco::DiscoInfoQuery;
|
||||
use crate::stanza_error::{DefinedCondition, ErrorType};
|
||||
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(IqType, 112);
|
||||
assert_size!(Iq, 204);
|
||||
}
|
||||
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(IqType, 224);
|
||||
assert_size!(Iq, 408);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_require_type() {
|
||||
#[cfg(not(feature = "component"))]
|
||||
let elem: Element = "<iq xmlns='jabber:client'/>".parse().unwrap();
|
||||
#[cfg(feature = "component")]
|
||||
let elem: Element = "<iq xmlns='jabber:component:accept'/>".parse().unwrap();
|
||||
let error = Iq::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "Required attribute 'id' missing.");
|
||||
|
||||
#[cfg(not(feature = "component"))]
|
||||
let elem: Element = "<iq xmlns='jabber:client' id='coucou'/>".parse().unwrap();
|
||||
#[cfg(feature = "component")]
|
||||
let elem: Element = "<iq xmlns='jabber:component:accept' id='coucou'/>".parse().unwrap();
|
||||
let error = Iq::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "Required attribute 'type' missing.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get() {
|
||||
#[cfg(not(feature = "component"))]
|
||||
let elem: Element = "<iq xmlns='jabber:client' type='get' id='foo'>
|
||||
<foo xmlns='bar'/>
|
||||
</iq>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
#[cfg(feature = "component")]
|
||||
let elem: Element = "<iq xmlns='jabber:component:accept' type='get' id='foo'>
|
||||
<foo xmlns='bar'/>
|
||||
</iq>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let iq = Iq::try_from(elem).unwrap();
|
||||
let query: Element = "<foo xmlns='bar'/>".parse().unwrap();
|
||||
assert_eq!(iq.from, None);
|
||||
assert_eq!(iq.to, None);
|
||||
assert_eq!(&iq.id, "foo");
|
||||
assert!(match iq.payload {
|
||||
IqType::Get(element) => element.compare_to(&query),
|
||||
_ => false,
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set() {
|
||||
#[cfg(not(feature = "component"))]
|
||||
let elem: Element = "<iq xmlns='jabber:client' type='set' id='vcard'>
|
||||
<vCard xmlns='vcard-temp'/>
|
||||
</iq>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
#[cfg(feature = "component")]
|
||||
let elem: Element = "<iq xmlns='jabber:component:accept' type='set' id='vcard'>
|
||||
<vCard xmlns='vcard-temp'/>
|
||||
</iq>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let iq = Iq::try_from(elem).unwrap();
|
||||
let vcard: Element = "<vCard xmlns='vcard-temp'/>".parse().unwrap();
|
||||
assert_eq!(iq.from, None);
|
||||
assert_eq!(iq.to, None);
|
||||
assert_eq!(&iq.id, "vcard");
|
||||
assert!(match iq.payload {
|
||||
IqType::Set(element) => element.compare_to(&vcard),
|
||||
_ => false,
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_result_empty() {
|
||||
#[cfg(not(feature = "component"))]
|
||||
let elem: Element = "<iq xmlns='jabber:client' type='result' id='res'/>".parse().unwrap();
|
||||
#[cfg(feature = "component")]
|
||||
let elem: Element = "<iq xmlns='jabber:component:accept' type='result' id='res'/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let iq = Iq::try_from(elem).unwrap();
|
||||
assert_eq!(iq.from, None);
|
||||
assert_eq!(iq.to, None);
|
||||
assert_eq!(&iq.id, "res");
|
||||
assert!(match iq.payload {
|
||||
IqType::Result(None) => true,
|
||||
_ => false,
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_result() {
|
||||
#[cfg(not(feature = "component"))]
|
||||
let elem: Element = "<iq xmlns='jabber:client' type='result' id='res'>
|
||||
<query xmlns='http://jabber.org/protocol/disco#items'/>
|
||||
</iq>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
#[cfg(feature = "component")]
|
||||
let elem: Element = "<iq xmlns='jabber:component:accept' type='result' id='res'>
|
||||
<query xmlns='http://jabber.org/protocol/disco#items'/>
|
||||
</iq>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let iq = Iq::try_from(elem).unwrap();
|
||||
let query: Element = "<query xmlns='http://jabber.org/protocol/disco#items'/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
assert_eq!(iq.from, None);
|
||||
assert_eq!(iq.to, None);
|
||||
assert_eq!(&iq.id, "res");
|
||||
assert!(match iq.payload {
|
||||
IqType::Result(Some(element)) => element.compare_to(&query),
|
||||
_ => false,
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error() {
|
||||
#[cfg(not(feature = "component"))]
|
||||
let elem: Element = "<iq xmlns='jabber:client' type='error' id='err1'>
|
||||
<ping xmlns='urn:xmpp:ping'/>
|
||||
<error type='cancel'>
|
||||
<service-unavailable xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
|
||||
</error>
|
||||
</iq>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
#[cfg(feature = "component")]
|
||||
let elem: Element = "<iq xmlns='jabber:component:accept' type='error' id='err1'>
|
||||
<ping xmlns='urn:xmpp:ping'/>
|
||||
<error type='cancel'>
|
||||
<service-unavailable xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
|
||||
</error>
|
||||
</iq>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let iq = Iq::try_from(elem).unwrap();
|
||||
assert_eq!(iq.from, None);
|
||||
assert_eq!(iq.to, None);
|
||||
assert_eq!(iq.id, "err1");
|
||||
match iq.payload {
|
||||
IqType::Error(error) => {
|
||||
assert_eq!(error.type_, ErrorType::Cancel);
|
||||
assert_eq!(error.by, None);
|
||||
assert_eq!(
|
||||
error.defined_condition,
|
||||
DefinedCondition::ServiceUnavailable
|
||||
);
|
||||
assert_eq!(error.texts.len(), 0);
|
||||
assert_eq!(error.other, None);
|
||||
}
|
||||
_ => panic!(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_children_invalid() {
|
||||
#[cfg(not(feature = "component"))]
|
||||
let elem: Element = "<iq xmlns='jabber:client' type='error' id='error'/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
#[cfg(feature = "component")]
|
||||
let elem: Element = "<iq xmlns='jabber:component:accept' type='error' id='error'/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let error = Iq::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "Wrong number of children in iq element.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialise() {
|
||||
#[cfg(not(feature = "component"))]
|
||||
let elem: Element = "<iq xmlns='jabber:client' type='result' id='res'/>".parse().unwrap();
|
||||
#[cfg(feature = "component")]
|
||||
let elem: Element = "<iq xmlns='jabber:component:accept' type='result' id='res'/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let iq2 = Iq {
|
||||
from: None,
|
||||
to: None,
|
||||
id: String::from("res"),
|
||||
payload: IqType::Result(None),
|
||||
};
|
||||
let elem2 = iq2.into();
|
||||
assert_eq!(elem, elem2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_disco() {
|
||||
#[cfg(not(feature = "component"))]
|
||||
let elem: Element = "<iq xmlns='jabber:client' type='get' id='disco'><query xmlns='http://jabber.org/protocol/disco#info'/></iq>".parse().unwrap();
|
||||
#[cfg(feature = "component")]
|
||||
let elem: Element = "<iq xmlns='jabber:component:accept' type='get' id='disco'><query xmlns='http://jabber.org/protocol/disco#info'/></iq>".parse().unwrap();
|
||||
let iq = Iq::try_from(elem).unwrap();
|
||||
let disco_info = match iq.payload {
|
||||
IqType::Get(payload) => DiscoInfoQuery::try_from(payload).unwrap(),
|
||||
_ => panic!(),
|
||||
};
|
||||
assert!(disco_info.node.is_none());
|
||||
}
|
||||
}
|
||||
73
xmpp-parsers/src/jid_prep.rs
Normal file
73
xmpp-parsers/src/jid_prep.rs
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
use crate::iq::{IqGetPayload, IqResultPayload};
|
||||
use crate::util::helpers::{Text, JidCodec};
|
||||
use jid::Jid;
|
||||
|
||||
generate_element!(
|
||||
/// Request from a client to stringprep/PRECIS a string into a JID.
|
||||
JidPrepQuery, "jid", JID_PREP,
|
||||
text: (
|
||||
/// The potential JID.
|
||||
data: Text<String>
|
||||
)
|
||||
);
|
||||
|
||||
impl IqGetPayload for JidPrepQuery {}
|
||||
|
||||
impl JidPrepQuery {
|
||||
/// Create a new JID Prep query.
|
||||
pub fn new<J: Into<String>>(jid: J) -> JidPrepQuery {
|
||||
JidPrepQuery {
|
||||
data: jid.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
generate_element!(
|
||||
/// Response from the server with the 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());
|
||||
}
|
||||
}
|
||||
867
xmpp-parsers/src/jingle.rs
Normal file
867
xmpp-parsers/src/jingle.rs
Normal file
|
|
@ -0,0 +1,867 @@
|
|||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
use crate::util::error::Error;
|
||||
use crate::iq::IqSetPayload;
|
||||
use crate::jingle_rtp::Description as RtpDescription;
|
||||
use crate::jingle_ice_udp::Transport as IceUdpTransport;
|
||||
use crate::jingle_ibb::Transport as IbbTransport;
|
||||
use crate::jingle_s5b::Transport as Socks5Transport;
|
||||
use crate::ns;
|
||||
use jid::Jid;
|
||||
use crate::Element;
|
||||
use std::collections::BTreeMap;
|
||||
use std::str::FromStr;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
generate_attribute!(
|
||||
/// The action attribute.
|
||||
Action, "action", {
|
||||
/// Accept a content-add action received from another party.
|
||||
ContentAccept => "content-accept",
|
||||
|
||||
/// Add one or more new content definitions to the session.
|
||||
ContentAdd => "content-add",
|
||||
|
||||
/// Change the directionality of media sending.
|
||||
ContentModify => "content-modify",
|
||||
|
||||
/// Reject a content-add action received from another party.
|
||||
ContentReject => "content-reject",
|
||||
|
||||
/// Remove one or more content definitions from the session.
|
||||
ContentRemove => "content-remove",
|
||||
|
||||
/// Exchange information about parameters for an application type.
|
||||
DescriptionInfo => "description-info",
|
||||
|
||||
/// Exchange information about security preconditions.
|
||||
SecurityInfo => "security-info",
|
||||
|
||||
/// Definitively accept a session negotiation.
|
||||
SessionAccept => "session-accept",
|
||||
|
||||
/// Send session-level information, such as a ping or a ringing message.
|
||||
SessionInfo => "session-info",
|
||||
|
||||
/// Request negotiation of a new Jingle session.
|
||||
SessionInitiate => "session-initiate",
|
||||
|
||||
/// End an existing session.
|
||||
SessionTerminate => "session-terminate",
|
||||
|
||||
/// Accept a transport-replace action received from another party.
|
||||
TransportAccept => "transport-accept",
|
||||
|
||||
/// Exchange transport candidates.
|
||||
TransportInfo => "transport-info",
|
||||
|
||||
/// Reject a transport-replace action received from another party.
|
||||
TransportReject => "transport-reject",
|
||||
|
||||
/// Redefine a transport method or replace it with a different method.
|
||||
TransportReplace => "transport-replace",
|
||||
}
|
||||
);
|
||||
|
||||
generate_attribute!(
|
||||
/// Which party originally generated the content type.
|
||||
Creator, "creator", {
|
||||
/// This content was created by the initiator of this session.
|
||||
Initiator => "initiator",
|
||||
|
||||
/// This content was created by the responder of this session.
|
||||
Responder => "responder",
|
||||
}
|
||||
);
|
||||
|
||||
generate_attribute!(
|
||||
/// Which parties in the session will be generating content.
|
||||
Senders, "senders", {
|
||||
/// Both parties can send for this content.
|
||||
Both => "both",
|
||||
|
||||
/// Only the initiator can send for this content.
|
||||
Initiator => "initiator",
|
||||
|
||||
/// No one can send for this content.
|
||||
None => "none",
|
||||
|
||||
/// Only the responder can send for this content.
|
||||
Responder => "responder",
|
||||
}, Default = Both
|
||||
);
|
||||
|
||||
generate_attribute!(
|
||||
/// How the content definition is to be interpreted by the recipient. The
|
||||
/// meaning of this attribute matches the "Content-Disposition" header as
|
||||
/// defined in RFC 2183 and applied to SIP by RFC 3261.
|
||||
///
|
||||
/// Possible values are defined here:
|
||||
/// https://www.iana.org/assignments/cont-disp/cont-disp.xhtml
|
||||
Disposition, "disposition", {
|
||||
/// Displayed automatically.
|
||||
Inline => "inline",
|
||||
|
||||
/// User controlled display.
|
||||
Attachment => "attachment",
|
||||
|
||||
/// Process as form response.
|
||||
FormData => "form-data",
|
||||
|
||||
/// Tunneled content to be processed silently.
|
||||
Signal => "signal",
|
||||
|
||||
/// The body is a custom ring tone to alert the user.
|
||||
Alert => "alert",
|
||||
|
||||
/// The body is displayed as an icon to the user.
|
||||
Icon => "icon",
|
||||
|
||||
/// The body should be displayed to the user.
|
||||
Render => "render",
|
||||
|
||||
/// The body contains a list of URIs that indicates the recipients of
|
||||
/// the request.
|
||||
RecipientListHistory => "recipient-list-history",
|
||||
|
||||
/// The body describes a communications session, for example, an
|
||||
/// RFC2327 SDP body.
|
||||
Session => "session",
|
||||
|
||||
/// Authenticated Identity Body.
|
||||
Aib => "aib",
|
||||
|
||||
/// The body describes an early communications session, for example,
|
||||
/// and [RFC2327] SDP body.
|
||||
EarlySession => "early-session",
|
||||
|
||||
/// The body includes a list of URIs to which URI-list services are to
|
||||
/// be applied.
|
||||
RecipientList => "recipient-list",
|
||||
|
||||
/// The payload of the message carrying this Content-Disposition header
|
||||
/// field value is an Instant Message Disposition Notification as
|
||||
/// requested in the corresponding Instant Message.
|
||||
Notification => "notification",
|
||||
|
||||
/// The body needs to be handled according to a reference to the body
|
||||
/// that is located in the same SIP message as the body.
|
||||
ByReference => "by-reference",
|
||||
|
||||
/// The body contains information associated with an Info Package.
|
||||
InfoPackage => "info-package",
|
||||
|
||||
/// The body describes either metadata about the RS or the reason for
|
||||
/// the metadata snapshot request as determined by the MIME value
|
||||
/// indicated in the Content-Type.
|
||||
RecordingSession => "recording-session",
|
||||
}, Default = Session
|
||||
);
|
||||
|
||||
generate_id!(
|
||||
/// An unique identifier in a session, referencing a
|
||||
/// [struct.Content.html](Content element).
|
||||
ContentId
|
||||
);
|
||||
|
||||
/// Enum wrapping all of the various supported descriptions of a Content.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Description {
|
||||
/// Jingle RTP Sessions (XEP-0167) description.
|
||||
Rtp(RtpDescription),
|
||||
|
||||
/// To be used for any description that 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.");
|
||||
}
|
||||
}
|
||||
98
xmpp-parsers/src/jingle_dtls_srtp.rs
Normal file
98
xmpp-parsers/src/jingle_dtls_srtp.rs
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
use crate::util::helpers::ColonSeparatedHex;
|
||||
use crate::util::error::Error;
|
||||
use crate::hashes::{Hash, Algo};
|
||||
|
||||
generate_attribute!(
|
||||
/// Indicates which of the end points should initiate the TCP connection establishment.
|
||||
Setup, "setup", {
|
||||
/// The endpoint will initiate an outgoing connection.
|
||||
Active => "active",
|
||||
|
||||
/// The endpoint will accept an incoming connection.
|
||||
Passive => "passive",
|
||||
|
||||
/// The endpoint is willing to accept an incoming connection or to initiate an outgoing
|
||||
/// connection.
|
||||
Actpass => "actpass",
|
||||
|
||||
/*
|
||||
/// The endpoint does not want the connection to be established for the time being.
|
||||
///
|
||||
/// Note that this value 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]);
|
||||
}
|
||||
}
|
||||
616
xmpp-parsers/src/jingle_ft.rs
Normal file
616
xmpp-parsers/src/jingle_ft.rs
Normal file
|
|
@ -0,0 +1,616 @@
|
|||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
use crate::date::DateTime;
|
||||
use crate::util::error::Error;
|
||||
use crate::hashes::Hash;
|
||||
use crate::jingle::{ContentId, Creator};
|
||||
use crate::ns;
|
||||
use minidom::{Element, Node};
|
||||
use std::collections::BTreeMap;
|
||||
use std::str::FromStr;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
generate_element!(
|
||||
/// Represents a range in a file.
|
||||
#[derive(PartialEq, Default)]
|
||||
Range, "range", JINGLE_FT,
|
||||
attributes: [
|
||||
/// The offset in bytes from the beginning of the file.
|
||||
offset: Default<u64> = "offset",
|
||||
|
||||
/// The length in bytes of the range, or None to be the entire
|
||||
/// remaining of the file.
|
||||
length: Option<u64> = "length"
|
||||
],
|
||||
children: [
|
||||
/// List of hashes for this range.
|
||||
hashes: Vec<Hash> = ("hash", HASHES) => Hash
|
||||
]
|
||||
);
|
||||
|
||||
impl Range {
|
||||
/// Creates a new range.
|
||||
pub fn new() -> Range {
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
type Lang = String;
|
||||
|
||||
generate_id!(
|
||||
/// Wrapper for a file description.
|
||||
Desc
|
||||
);
|
||||
|
||||
/// Represents a file to be transferred.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct File {
|
||||
/// The date of last modification of this file.
|
||||
pub date: Option<DateTime>,
|
||||
|
||||
/// The MIME type of this file.
|
||||
pub media_type: Option<String>,
|
||||
|
||||
/// The name of this file.
|
||||
pub name: Option<String>,
|
||||
|
||||
/// The description of this file, possibly localised.
|
||||
pub descs: BTreeMap<Lang, Desc>,
|
||||
|
||||
/// The size of this file, in bytes.
|
||||
pub size: Option<u64>,
|
||||
|
||||
/// Used to request only a part of this file.
|
||||
pub range: Option<Range>,
|
||||
|
||||
/// A list of hashes matching this entire file.
|
||||
pub hashes: Vec<Hash>,
|
||||
}
|
||||
|
||||
impl File {
|
||||
/// Creates a new file descriptor.
|
||||
pub fn new() -> File {
|
||||
File::default()
|
||||
}
|
||||
|
||||
/// Sets the date of last modification on this file.
|
||||
pub fn with_date(mut self, date: DateTime) -> File {
|
||||
self.date = Some(date);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the date of last modification on this file from an ISO-8601
|
||||
/// string.
|
||||
pub fn with_date_str(mut self, date: &str) -> Result<File, Error> {
|
||||
self.date = Some(DateTime::from_str(date)?);
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Sets the MIME type of this file.
|
||||
pub fn with_media_type(mut self, media_type: String) -> File {
|
||||
self.media_type = Some(media_type);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the name of this file.
|
||||
pub fn with_name(mut self, name: String) -> File {
|
||||
self.name = Some(name);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets a description for this file.
|
||||
pub fn add_desc(mut self, lang: &str, desc: Desc) -> File {
|
||||
self.descs.insert(Lang::from(lang), desc);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the file size of this file, in bytes.
|
||||
pub fn with_size(mut self, size: u64) -> File {
|
||||
self.size = Some(size);
|
||||
self
|
||||
}
|
||||
|
||||
/// Request only a range of this file.
|
||||
pub fn with_range(mut self, range: Range) -> File {
|
||||
self.range = Some(range);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a hash on this file.
|
||||
pub fn add_hash(mut self, hash: Hash) -> File {
|
||||
self.hashes.push(hash);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Element> for File {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(elem: Element) -> Result<File, Error> {
|
||||
check_self!(elem, "file", JINGLE_FT);
|
||||
check_no_attributes!(elem, "file");
|
||||
|
||||
let mut file = File {
|
||||
date: None,
|
||||
media_type: None,
|
||||
name: None,
|
||||
descs: BTreeMap::new(),
|
||||
size: None,
|
||||
range: None,
|
||||
hashes: vec![],
|
||||
};
|
||||
|
||||
for child in elem.children() {
|
||||
if child.is("date", ns::JINGLE_FT) {
|
||||
if file.date.is_some() {
|
||||
return Err(Error::ParseError("File must not have more than one date."));
|
||||
}
|
||||
file.date = Some(child.text().parse()?);
|
||||
} else if child.is("media-type", ns::JINGLE_FT) {
|
||||
if file.media_type.is_some() {
|
||||
return Err(Error::ParseError(
|
||||
"File must not have more than one media-type.",
|
||||
));
|
||||
}
|
||||
file.media_type = Some(child.text());
|
||||
} else if child.is("name", ns::JINGLE_FT) {
|
||||
if file.name.is_some() {
|
||||
return Err(Error::ParseError("File must not have more than one name."));
|
||||
}
|
||||
file.name = Some(child.text());
|
||||
} else if child.is("desc", ns::JINGLE_FT) {
|
||||
let lang = get_attr!(child, "xml:lang", Default);
|
||||
let desc = Desc(child.text());
|
||||
if file.descs.insert(lang, desc).is_some() {
|
||||
return Err(Error::ParseError(
|
||||
"Desc element present twice for the same xml:lang.",
|
||||
));
|
||||
}
|
||||
} else if child.is("size", ns::JINGLE_FT) {
|
||||
if file.size.is_some() {
|
||||
return Err(Error::ParseError("File must not have more than one size."));
|
||||
}
|
||||
file.size = Some(child.text().parse()?);
|
||||
} else if child.is("range", ns::JINGLE_FT) {
|
||||
if file.range.is_some() {
|
||||
return Err(Error::ParseError("File must not have more than one range."));
|
||||
}
|
||||
file.range = Some(Range::try_from(child.clone())?);
|
||||
} else if child.is("hash", ns::HASHES) {
|
||||
file.hashes.push(Hash::try_from(child.clone())?);
|
||||
} else {
|
||||
return Err(Error::ParseError("Unknown element in JingleFT file."));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(file)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<File> for Element {
|
||||
fn from(file: File) -> Element {
|
||||
Element::builder("file")
|
||||
.ns(ns::JINGLE_FT)
|
||||
.append_all(file.date.map(|date|
|
||||
Element::builder("date")
|
||||
.append(date)))
|
||||
.append_all(file.media_type.map(|media_type|
|
||||
Element::builder("media-type")
|
||||
.append(media_type)))
|
||||
.append_all(file.name.map(|name|
|
||||
Element::builder("name")
|
||||
.append(name)))
|
||||
.append_all(file.descs.into_iter().map(|(lang, desc)|
|
||||
Element::builder("desc")
|
||||
.attr("xml:lang", lang)
|
||||
.append(desc.0)))
|
||||
.append_all(file.size.map(|size|
|
||||
Element::builder("size")
|
||||
.append(format!("{}", size))))
|
||||
.append_all(file.range)
|
||||
.append_all(file.hashes)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper element for a file.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Description {
|
||||
/// The actual file descriptor.
|
||||
pub file: File,
|
||||
}
|
||||
|
||||
impl TryFrom<Element> for Description {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(elem: Element) -> Result<Description, Error> {
|
||||
check_self!(elem, "description", JINGLE_FT, "JingleFT description");
|
||||
check_no_attributes!(elem, "JingleFT description");
|
||||
let mut file = None;
|
||||
for child in elem.children() {
|
||||
if file.is_some() {
|
||||
return Err(Error::ParseError(
|
||||
"JingleFT description element must have exactly one child.",
|
||||
));
|
||||
}
|
||||
file = Some(File::try_from(child.clone())?);
|
||||
}
|
||||
if file.is_none() {
|
||||
return Err(Error::ParseError(
|
||||
"JingleFT description element must have exactly one child.",
|
||||
));
|
||||
}
|
||||
Ok(Description {
|
||||
file: file.unwrap(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Description> for Element {
|
||||
fn from(description: Description) -> Element {
|
||||
Element::builder("description")
|
||||
.ns(ns::JINGLE_FT)
|
||||
.append(Node::Element(description.file.into()))
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
/// A checksum for checking that the file has been transferred correctly.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Checksum {
|
||||
/// The identifier of the file transfer content.
|
||||
pub name: ContentId,
|
||||
|
||||
/// The creator of this file transfer.
|
||||
pub creator: Creator,
|
||||
|
||||
/// The file being checksummed.
|
||||
pub file: File,
|
||||
}
|
||||
|
||||
impl TryFrom<Element> for Checksum {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(elem: Element) -> Result<Checksum, Error> {
|
||||
check_self!(elem, "checksum", JINGLE_FT);
|
||||
check_no_unknown_attributes!(elem, "checksum", ["name", "creator"]);
|
||||
let mut file = None;
|
||||
for child in elem.children() {
|
||||
if file.is_some() {
|
||||
return Err(Error::ParseError(
|
||||
"JingleFT checksum element must have exactly one child.",
|
||||
));
|
||||
}
|
||||
file = Some(File::try_from(child.clone())?);
|
||||
}
|
||||
if file.is_none() {
|
||||
return Err(Error::ParseError(
|
||||
"JingleFT checksum element must have exactly one child.",
|
||||
));
|
||||
}
|
||||
Ok(Checksum {
|
||||
name: get_attr!(elem, "name", Required),
|
||||
creator: get_attr!(elem, "creator", Required),
|
||||
file: file.unwrap(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Checksum> for Element {
|
||||
fn from(checksum: Checksum) -> Element {
|
||||
Element::builder("checksum")
|
||||
.ns(ns::JINGLE_FT)
|
||||
.attr("name", checksum.name)
|
||||
.attr("creator", checksum.creator)
|
||||
.append(Node::Element(checksum.file.into()))
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
generate_element!(
|
||||
/// A notice that the file transfer has been completed.
|
||||
Received, "received", JINGLE_FT,
|
||||
attributes: [
|
||||
/// The content identifier of this Jingle session.
|
||||
name: Required<ContentId> = "name",
|
||||
|
||||
/// The creator of this file transfer.
|
||||
creator: Required<Creator> = "creator",
|
||||
]
|
||||
);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::hashes::Algo;
|
||||
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(Range, 40);
|
||||
assert_size!(File, 128);
|
||||
assert_size!(Description, 128);
|
||||
assert_size!(Checksum, 144);
|
||||
assert_size!(Received, 16);
|
||||
}
|
||||
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(Range, 48);
|
||||
assert_size!(File, 184);
|
||||
assert_size!(Description, 184);
|
||||
assert_size!(Checksum, 216);
|
||||
assert_size!(Received, 32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_description() {
|
||||
let elem: Element = r#"
|
||||
<description xmlns='urn:xmpp:jingle:apps:file-transfer:5'>
|
||||
<file>
|
||||
<media-type>text/plain</media-type>
|
||||
<name>test.txt</name>
|
||||
<date>2015-07-26T21:46:00+01:00</date>
|
||||
<size>6144</size>
|
||||
<hash xmlns='urn:xmpp:hashes:2'
|
||||
algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash>
|
||||
</file>
|
||||
</description>
|
||||
"#
|
||||
.parse()
|
||||
.unwrap();
|
||||
let desc = Description::try_from(elem).unwrap();
|
||||
assert_eq!(desc.file.media_type, Some(String::from("text/plain")));
|
||||
assert_eq!(desc.file.name, Some(String::from("test.txt")));
|
||||
assert_eq!(desc.file.descs, BTreeMap::new());
|
||||
assert_eq!(
|
||||
desc.file.date,
|
||||
DateTime::from_str("2015-07-26T21:46:00+01:00").ok()
|
||||
);
|
||||
assert_eq!(desc.file.size, Some(6144u64));
|
||||
assert_eq!(desc.file.range, None);
|
||||
assert_eq!(desc.file.hashes[0].algo, Algo::Sha_1);
|
||||
assert_eq!(
|
||||
desc.file.hashes[0].hash,
|
||||
base64::decode("w0mcJylzCn+AfvuGdqkty2+KP48=").unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_request() {
|
||||
let elem: Element = r#"
|
||||
<description xmlns='urn:xmpp:jingle:apps:file-transfer:5'>
|
||||
<file>
|
||||
<hash xmlns='urn:xmpp:hashes:2'
|
||||
algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash>
|
||||
</file>
|
||||
</description>
|
||||
"#
|
||||
.parse()
|
||||
.unwrap();
|
||||
let desc = Description::try_from(elem).unwrap();
|
||||
assert_eq!(desc.file.media_type, None);
|
||||
assert_eq!(desc.file.name, None);
|
||||
assert_eq!(desc.file.descs, BTreeMap::new());
|
||||
assert_eq!(desc.file.date, None);
|
||||
assert_eq!(desc.file.size, None);
|
||||
assert_eq!(desc.file.range, None);
|
||||
assert_eq!(desc.file.hashes[0].algo, Algo::Sha_1);
|
||||
assert_eq!(
|
||||
desc.file.hashes[0].hash,
|
||||
base64::decode("w0mcJylzCn+AfvuGdqkty2+KP48=").unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_descs() {
|
||||
let elem: Element = r#"
|
||||
<description xmlns='urn:xmpp:jingle:apps:file-transfer:5'>
|
||||
<file>
|
||||
<media-type>text/plain</media-type>
|
||||
<desc xml:lang='fr'>Fichier secret !</desc>
|
||||
<desc xml:lang='en'>Secret file!</desc>
|
||||
<hash xmlns='urn:xmpp:hashes:2'
|
||||
algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash>
|
||||
</file>
|
||||
</description>
|
||||
"#
|
||||
.parse()
|
||||
.unwrap();
|
||||
let desc = Description::try_from(elem).unwrap();
|
||||
assert_eq!(
|
||||
desc.file.descs.keys().cloned().collect::<Vec<_>>(),
|
||||
["en", "fr"]
|
||||
);
|
||||
assert_eq!(desc.file.descs["en"], Desc(String::from("Secret file!")));
|
||||
assert_eq!(
|
||||
desc.file.descs["fr"],
|
||||
Desc(String::from("Fichier secret !"))
|
||||
);
|
||||
|
||||
let elem: Element = r#"
|
||||
<description xmlns='urn:xmpp:jingle:apps:file-transfer:5'>
|
||||
<file>
|
||||
<media-type>text/plain</media-type>
|
||||
<desc xml:lang='fr'>Fichier secret !</desc>
|
||||
<desc xml:lang='fr'>Secret file!</desc>
|
||||
<hash xmlns='urn:xmpp:hashes:2'
|
||||
algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash>
|
||||
</file>
|
||||
</description>
|
||||
"#
|
||||
.parse()
|
||||
.unwrap();
|
||||
let error = Description::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "Desc element present twice for the same xml:lang.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_received() {
|
||||
let elem: Element = "<received xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='initiator'/>".parse().unwrap();
|
||||
let received = Received::try_from(elem).unwrap();
|
||||
assert_eq!(received.name, ContentId(String::from("coucou")));
|
||||
assert_eq!(received.creator, Creator::Initiator);
|
||||
let elem2 = Element::from(received.clone());
|
||||
let received2 = Received::try_from(elem2).unwrap();
|
||||
assert_eq!(received2.name, ContentId(String::from("coucou")));
|
||||
assert_eq!(received2.creator, Creator::Initiator);
|
||||
|
||||
let elem: Element = "<received xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='initiator'><coucou/></received>".parse().unwrap();
|
||||
let error = Received::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "Unknown child in received element.");
|
||||
|
||||
let elem: Element =
|
||||
"<received xmlns='urn:xmpp:jingle:apps:file-transfer:5' creator='initiator'/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let error = Received::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "Required attribute 'name' missing.");
|
||||
|
||||
let elem: Element = "<received xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='coucou'/>".parse().unwrap();
|
||||
let error = Received::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "Unknown value for 'creator' attribute.");
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "disable-validation"))]
|
||||
#[test]
|
||||
fn test_invalid_received() {
|
||||
let elem: Element = "<received xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='initiator' coucou=''/>".parse().unwrap();
|
||||
let error = Received::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "Unknown attribute in received element.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_checksum() {
|
||||
let elem: Element = "<checksum xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='initiator'><file><hash xmlns='urn:xmpp:hashes:2' algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash></file></checksum>".parse().unwrap();
|
||||
let hash = vec![
|
||||
195, 73, 156, 39, 41, 115, 10, 127, 128, 126, 251, 134, 118, 169, 45, 203, 111, 138,
|
||||
63, 143,
|
||||
];
|
||||
let checksum = Checksum::try_from(elem).unwrap();
|
||||
assert_eq!(checksum.name, ContentId(String::from("coucou")));
|
||||
assert_eq!(checksum.creator, Creator::Initiator);
|
||||
assert_eq!(
|
||||
checksum.file.hashes,
|
||||
vec!(Hash {
|
||||
algo: Algo::Sha_1,
|
||||
hash: hash.clone()
|
||||
})
|
||||
);
|
||||
let elem2 = Element::from(checksum);
|
||||
let checksum2 = Checksum::try_from(elem2).unwrap();
|
||||
assert_eq!(checksum2.name, ContentId(String::from("coucou")));
|
||||
assert_eq!(checksum2.creator, Creator::Initiator);
|
||||
assert_eq!(
|
||||
checksum2.file.hashes,
|
||||
vec!(Hash {
|
||||
algo: Algo::Sha_1,
|
||||
hash: hash.clone()
|
||||
})
|
||||
);
|
||||
|
||||
let elem: Element = "<checksum xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='initiator'><coucou/></checksum>".parse().unwrap();
|
||||
let error = Checksum::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "This is not a file element.");
|
||||
|
||||
let elem: Element = "<checksum xmlns='urn:xmpp:jingle:apps:file-transfer:5' creator='initiator'><file><hash xmlns='urn:xmpp:hashes:2' algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash></file></checksum>".parse().unwrap();
|
||||
let error = Checksum::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "Required attribute 'name' missing.");
|
||||
|
||||
let elem: Element = "<checksum xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='coucou'><file><hash xmlns='urn:xmpp:hashes:2' algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash></file></checksum>".parse().unwrap();
|
||||
let error = Checksum::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "Unknown value for 'creator' attribute.");
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "disable-validation"))]
|
||||
#[test]
|
||||
fn test_invalid_checksum() {
|
||||
let elem: Element = "<checksum xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='initiator' coucou=''><file><hash xmlns='urn:xmpp:hashes:2' algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash></file></checksum>".parse().unwrap();
|
||||
let error = Checksum::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "Unknown attribute in checksum element.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_range() {
|
||||
let elem: Element = "<range xmlns='urn:xmpp:jingle:apps:file-transfer:5'/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let range = Range::try_from(elem).unwrap();
|
||||
assert_eq!(range.offset, 0);
|
||||
assert_eq!(range.length, None);
|
||||
assert_eq!(range.hashes, vec!());
|
||||
|
||||
let elem: Element = "<range xmlns='urn:xmpp:jingle:apps:file-transfer:5' offset='2048' length='1024'><hash xmlns='urn:xmpp:hashes:2' algo='sha-1'>kHp5RSzW/h7Gm1etSf90Mr5PC/k=</hash></range>".parse().unwrap();
|
||||
let hashes = vec![Hash {
|
||||
algo: Algo::Sha_1,
|
||||
hash: vec![
|
||||
144, 122, 121, 69, 44, 214, 254, 30, 198, 155, 87, 173, 73, 255, 116, 50, 190, 79,
|
||||
11, 249,
|
||||
],
|
||||
}];
|
||||
let range = Range::try_from(elem).unwrap();
|
||||
assert_eq!(range.offset, 2048);
|
||||
assert_eq!(range.length, Some(1024));
|
||||
assert_eq!(range.hashes, hashes);
|
||||
let elem2 = Element::from(range);
|
||||
let range2 = Range::try_from(elem2).unwrap();
|
||||
assert_eq!(range2.offset, 2048);
|
||||
assert_eq!(range2.length, Some(1024));
|
||||
assert_eq!(range2.hashes, hashes);
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "disable-validation"))]
|
||||
#[test]
|
||||
fn test_invalid_range() {
|
||||
let elem: Element = "<range xmlns='urn:xmpp:jingle:apps:file-transfer:5' coucou=''/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let error = Range::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "Unknown attribute in range element.");
|
||||
}
|
||||
}
|
||||
114
xmpp-parsers/src/jingle_ibb.rs
Normal file
114
xmpp-parsers/src/jingle_ibb.rs
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
use crate::ibb::{Stanza, StreamId};
|
||||
|
||||
generate_element!(
|
||||
/// Describes an [In-Band Bytestream](https://xmpp.org/extensions/xep-0047.html)
|
||||
/// Jingle transport, see also the [IBB module](../ibb.rs).
|
||||
Transport, "transport", JINGLE_IBB,
|
||||
attributes: [
|
||||
/// Maximum size in bytes for each chunk.
|
||||
block_size: Required<u16> = "block-size",
|
||||
|
||||
/// The identifier to be used to create a stream.
|
||||
sid: Required<StreamId> = "sid",
|
||||
|
||||
/// Which stanza type to use to exchange data.
|
||||
stanza: Default<Stanza> = "stanza",
|
||||
]);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::util::error::Error;
|
||||
use crate::Element;
|
||||
use std::error::Error as StdError;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(Transport, 16);
|
||||
}
|
||||
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(Transport, 32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple() {
|
||||
let elem: Element =
|
||||
"<transport xmlns='urn:xmpp:jingle:transports:ibb:1' block-size='3' sid='coucou'/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let transport = Transport::try_from(elem).unwrap();
|
||||
assert_eq!(transport.block_size, 3);
|
||||
assert_eq!(transport.sid, StreamId(String::from("coucou")));
|
||||
assert_eq!(transport.stanza, Stanza::Iq);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid() {
|
||||
let elem: Element = "<transport xmlns='urn:xmpp:jingle:transports:ibb:1'/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let error = Transport::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "Required attribute 'block-size' missing.");
|
||||
|
||||
let elem: Element =
|
||||
"<transport xmlns='urn:xmpp:jingle:transports:ibb:1' block-size='65536'/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let error = Transport::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseIntError(error) => error,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(
|
||||
message.description(),
|
||||
"number too large to fit in target type"
|
||||
);
|
||||
|
||||
let elem: Element = "<transport xmlns='urn:xmpp:jingle:transports:ibb:1' block-size='-5'/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let error = Transport::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseIntError(error) => error,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message.description(), "invalid digit found in string");
|
||||
|
||||
let elem: Element =
|
||||
"<transport xmlns='urn:xmpp:jingle:transports:ibb:1' block-size='128'/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let error = Transport::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "Required attribute 'sid' missing.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_stanza() {
|
||||
let elem: Element = "<transport xmlns='urn:xmpp:jingle:transports:ibb:1' block-size='128' sid='coucou' stanza='fdsq'/>".parse().unwrap();
|
||||
let error = Transport::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "Unknown value for 'stanza' attribute.");
|
||||
}
|
||||
}
|
||||
189
xmpp-parsers/src/jingle_ice_udp.rs
Normal file
189
xmpp-parsers/src/jingle_ice_udp.rs
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
use crate::jingle_dtls_srtp::Fingerprint;
|
||||
use std::net::IpAddr;
|
||||
|
||||
generate_element!(
|
||||
/// Wrapper element for an ICE-UDP transport.
|
||||
Transport, "transport", JINGLE_ICE_UDP,
|
||||
attributes: [
|
||||
/// A Password as defined in ICE-CORE.
|
||||
pwd: Option<String> = "pwd",
|
||||
|
||||
/// A User Fragment as defined in ICE-CORE.
|
||||
ufrag: Option<String> = "ufrag",
|
||||
],
|
||||
children: [
|
||||
/// List of candidates for this ICE-UDP session.
|
||||
candidates: Vec<Candidate> = ("candidate", JINGLE_ICE_UDP) => Candidate,
|
||||
|
||||
/// Fingerprint of the key used for the DTLS handshake.
|
||||
fingerprint: Option<Fingerprint> = ("fingerprint", JINGLE_DTLS) => Fingerprint
|
||||
]
|
||||
);
|
||||
|
||||
impl Transport {
|
||||
/// Create a new ICE-UDP transport.
|
||||
pub fn new() -> Transport {
|
||||
Transport {
|
||||
pwd: None,
|
||||
ufrag: None,
|
||||
candidates: Vec::new(),
|
||||
fingerprint: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a candidate to this transport.
|
||||
pub fn add_candidate(mut self, candidate: Candidate) -> Self {
|
||||
self.candidates.push(candidate);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the DTLS-SRTP fingerprint of this transport.
|
||||
pub fn with_fingerprint(mut self, fingerprint: Fingerprint) -> Self {
|
||||
self.fingerprint = Some(fingerprint);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
generate_attribute!(
|
||||
/// A Candidate Type as defined in ICE-CORE.
|
||||
Type, "type", {
|
||||
/// Host candidate.
|
||||
Host => "host",
|
||||
|
||||
/// Peer reflexive candidate.
|
||||
Prflx => "prflx",
|
||||
|
||||
/// Relayed candidate.
|
||||
Relay => "relay",
|
||||
|
||||
/// Server reflexive candidate.
|
||||
Srflx => "srflx",
|
||||
}
|
||||
);
|
||||
|
||||
generate_element!(
|
||||
/// A candidate for an ICE-UDP session.
|
||||
Candidate, "candidate", JINGLE_ICE_UDP,
|
||||
attributes: [
|
||||
/// A Component ID as defined in ICE-CORE.
|
||||
component: Required<u8> = "component",
|
||||
|
||||
/// A Foundation as defined in ICE-CORE.
|
||||
foundation: Required<u8> = "foundation",
|
||||
|
||||
/// An index, starting at 0, that enables the parties to keep track of updates to the
|
||||
/// candidate throughout the life of the session.
|
||||
generation: Required<u8> = "generation",
|
||||
|
||||
/// A unique identifier for the candidate.
|
||||
id: Required<String> = "id",
|
||||
|
||||
/// The Internet Protocol (IP) address for the candidate transport mechanism; this can be
|
||||
/// either an IPv4 address or an IPv6 address.
|
||||
ip: Required<IpAddr> = "ip",
|
||||
|
||||
/// An index, starting at 0, referencing which network this candidate is on for a given
|
||||
/// peer.
|
||||
network: Required<u8> = "network",
|
||||
|
||||
/// The port at the candidate IP address.
|
||||
port: Required<u16> = "port",
|
||||
|
||||
/// A Priority as defined in ICE-CORE.
|
||||
priority: Required<u32> = "priority",
|
||||
|
||||
/// The protocol to be used. The only value defined by this specification is "udp".
|
||||
protocol: Required<String> = "protocol",
|
||||
|
||||
/// A related address as defined in ICE-CORE.
|
||||
rel_addr: Option<IpAddr> = "rel-addr",
|
||||
|
||||
/// A related port as defined in ICE-CORE.
|
||||
rel_port: Option<u16> = "rel-port",
|
||||
|
||||
/// A Candidate Type as defined in ICE-CORE.
|
||||
type_: Required<Type> = "type",
|
||||
]
|
||||
);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::Element;
|
||||
use std::convert::TryFrom;
|
||||
use crate::hashes::Algo;
|
||||
use crate::jingle_dtls_srtp::Setup;
|
||||
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(Transport, 68);
|
||||
assert_size!(Type, 1);
|
||||
assert_size!(Candidate, 80);
|
||||
}
|
||||
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(Transport, 136);
|
||||
assert_size!(Type, 1);
|
||||
assert_size!(Candidate, 104);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gajim() {
|
||||
let elem: Element = "
|
||||
<transport xmlns='urn:xmpp:jingle:transports:ice-udp:1' pwd='wakMJ8Ydd5rqnPaFerws5o' ufrag='aeXX'>
|
||||
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='2' foundation='1' generation='0' id='11b72719-6a1b-4c51-8ae6-9f1538047568' ip='192.168.0.12' network='0' port='56715' priority='1010828030' protocol='tcp' type='host'/>
|
||||
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='2' foundation='1' generation='0' id='7e07b22d-db50-4e17-9ed9-eafeb96f4f63' ip='192.168.0.12' network='0' port='0' priority='1015022334' protocol='tcp' type='host'/>
|
||||
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='2' foundation='1' generation='0' id='431de362-c45f-40a8-bf10-9ed898a71d86' ip='192.168.0.12' network='0' port='36480' priority='2013266428' protocol='udp' type='host'/>
|
||||
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='1' foundation='1' generation='0' id='b1197df3-abca-413b-99ee-3660d91bcfa7' ip='192.168.0.12' network='0' port='50387' priority='1010828031' protocol='tcp' type='host'/>
|
||||
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='1' foundation='1' generation='0' id='adaf3a85-3a57-4df0-a2d8-0c7d28d3ca01' ip='192.168.0.12' network='0' port='0' priority='1015022335' protocol='tcp' type='host'/>
|
||||
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='1' foundation='1' generation='0' id='ef4e0a62-81f2-4fe3-87ae-46cb5d1d1e1d' ip='192.168.0.12' network='0' port='43132' priority='2013266429' protocol='udp' type='host'/>
|
||||
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='1' foundation='1' generation='0' id='51891e8a-4c1e-4540-b173-8637aeb0143c' ip='fe80::24eb:646f:7d78:cb6' network='0' port='38881' priority='2013266431' protocol='udp' type='host'/>
|
||||
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='1' foundation='1' generation='0' id='73f82655-eb84-4fa1-b05c-1ea76f695d32' ip='fe80::24eb:646f:7d78:cb6' network='0' port='0' priority='1015023103' protocol='tcp' type='host'/>
|
||||
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='1' foundation='1' generation='0' id='a2a8fa62-6f2e-41e8-b218-ba095540d60f' ip='fe80::24eb:646f:7d78:cb6' network='0' port='55819' priority='1010828799' protocol='tcp' type='host'/>
|
||||
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='1' foundation='1' generation='0' id='23e66735-9515-414c-81ad-2455569a57f8' ip='2a01:e35:2e2f:fbb0:43aa:33b5:5535:8905' network='0' port='39967' priority='2013266430' protocol='udp' type='host'/>
|
||||
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='1' foundation='1' generation='0' id='9a8dff18-e138-4fb2-a956-89d71216da84' ip='2a01:e35:2e2f:fbb0:43aa:33b5:5535:8905' network='0' port='0' priority='1015022079' protocol='tcp' type='host'/>
|
||||
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='1' foundation='1' generation='0' id='f0c73ac3-9b7d-4032-abe3-6dd9a57d0f03' ip='2a01:e35:2e2f:fbb0:43aa:33b5:5535:8905' network='0' port='37487' priority='1010827775' protocol='tcp' type='host'/>
|
||||
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='2' foundation='1' generation='0' id='a6199a00-34df-46f5-a608-847b75c5250e' ip='fe80::24eb:646f:7d78:cb6' network='0' port='43521' priority='2013266430' protocol='udp' type='host'/>
|
||||
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='2' foundation='1' generation='0' id='83bc2600-39ce-4c9e-8b0b-cc7aa7e6a293' ip='fe80::24eb:646f:7d78:cb6' network='0' port='0' priority='1015023102' protocol='tcp' type='host'/>
|
||||
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='2' foundation='1' generation='0' id='7e3606ca-46de-4de8-8802-068dd69ef01a' ip='fe80::24eb:646f:7d78:cb6' network='0' port='52279' priority='1010828798' protocol='tcp' type='host'/>
|
||||
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='2' foundation='1' generation='0' id='a7c2472a-8462-412c-a64c-d3528f0abfa4' ip='2a01:e35:2e2f:fbb0:43aa:33b5:5535:8905' network='0' port='34088' priority='2013266429' protocol='udp' type='host'/>
|
||||
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='2' foundation='1' generation='0' id='5a12c345-9643-4d2c-b770-695ec6affcaf' ip='2a01:e35:2e2f:fbb0:43aa:33b5:5535:8905' network='0' port='0' priority='1015022078' protocol='tcp' type='host'/>
|
||||
<candidate xmlns='urn:xmpp:jingle:transports:ice-udp:1' component='2' foundation='1' generation='0' id='67f65b0b-8cee-421a-9f37-1f2ca2211c87' ip='2a01:e35:2e2f:fbb0:43aa:33b5:5535:8905' network='0' port='39431' priority='1010827774' protocol='tcp' type='host'/>
|
||||
</transport>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let transport = Transport::try_from(elem).unwrap();
|
||||
assert_eq!(transport.pwd.unwrap(), "wakMJ8Ydd5rqnPaFerws5o");
|
||||
assert_eq!(transport.ufrag.unwrap(), "aeXX");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_jitsi_meet() {
|
||||
let elem: Element = "
|
||||
<transport ufrag='2acq51d4p07v2m' pwd='7lk9uul39gckit6t02oavv2r9j' xmlns='urn:xmpp:jingle:transports:ice-udp:1'>
|
||||
<fingerprint hash='sha-1' setup='actpass' xmlns='urn:xmpp:jingle:apps:dtls:0'>97:F2:B5:BE:DB:A6:00:B1:3E:40:B2:41:3C:0D:FC:E0:BD:B2:A0:E8</fingerprint>
|
||||
<candidate type='host' protocol='udp' id='186cb069513c2bbe546192c93cc4ab3b05ab0d426' ip='2a05:d014:fc7:54a1:8bfc:7248:3d1c:51a4' component='1' port='10000' foundation='1' generation='0' priority='2130706431' network='0'/>
|
||||
<candidate type='host' protocol='udp' id='186cb069513c2bbe546192c93cc4ab3b063daeefd' ip='10.15.1.120' component='1' port='10000' foundation='2' generation='0' priority='2130706431' network='0'/>
|
||||
<candidate rel-port='10000' type='srflx' protocol='udp' id='186cb069513c2bbe546192c93cc4ab3b05d449db8' ip='3.120.176.51' component='1' port='10000' foundation='3' generation='0' network='0' priority='1677724415' rel-addr='10.15.1.120'/>
|
||||
</transport>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let transport = Transport::try_from(elem).unwrap();
|
||||
assert_eq!(transport.pwd.unwrap(), "7lk9uul39gckit6t02oavv2r9j");
|
||||
assert_eq!(transport.ufrag.unwrap(), "2acq51d4p07v2m");
|
||||
|
||||
let fingerprint = transport.fingerprint.unwrap();
|
||||
assert_eq!(fingerprint.hash, Algo::Sha_1);
|
||||
assert_eq!(fingerprint.setup, Setup::Actpass);
|
||||
assert_eq!(fingerprint.value, [151, 242, 181, 190, 219, 166, 0, 177, 62, 64, 178, 65, 60, 13, 252, 224, 189, 178, 160, 232]);
|
||||
}
|
||||
}
|
||||
146
xmpp-parsers/src/jingle_message.rs
Normal file
146
xmpp-parsers/src/jingle_message.rs
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
use crate::util::error::Error;
|
||||
use crate::jingle::SessionId;
|
||||
use crate::ns;
|
||||
use crate::Element;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
/// Defines a protocol for broadcasting Jingle requests to all of the clients
|
||||
/// of a user.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum JingleMI {
|
||||
/// Indicates we want to start a Jingle session.
|
||||
Propose {
|
||||
/// The generated session identifier, must be unique between two users.
|
||||
sid: SessionId,
|
||||
|
||||
/// The application description of the proposed session.
|
||||
// TODO: Use a more specialised type here.
|
||||
description: Element,
|
||||
},
|
||||
|
||||
/// Cancels a previously proposed session.
|
||||
Retract(SessionId),
|
||||
|
||||
/// Accepts a session proposed by the other party.
|
||||
Accept(SessionId),
|
||||
|
||||
/// Proceed with a previously proposed session.
|
||||
Proceed(SessionId),
|
||||
|
||||
/// Rejects a session proposed by the other party.
|
||||
Reject(SessionId),
|
||||
}
|
||||
|
||||
fn get_sid(elem: Element) -> Result<SessionId, Error> {
|
||||
check_no_unknown_attributes!(elem, "Jingle message", ["id"]);
|
||||
Ok(SessionId(get_attr!(elem, "id", Required)))
|
||||
}
|
||||
|
||||
fn check_empty_and_get_sid(elem: Element) -> Result<SessionId, Error> {
|
||||
check_no_children!(elem, "Jingle message");
|
||||
get_sid(elem)
|
||||
}
|
||||
|
||||
impl TryFrom<Element> for JingleMI {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(elem: Element) -> Result<JingleMI, Error> {
|
||||
if !elem.has_ns(ns::JINGLE_MESSAGE) {
|
||||
return Err(Error::ParseError("This is not a Jingle message element."));
|
||||
}
|
||||
Ok(match elem.name() {
|
||||
"propose" => {
|
||||
let mut description = None;
|
||||
for child in elem.children() {
|
||||
if child.name() != "description" {
|
||||
return Err(Error::ParseError("Unknown child in propose element."));
|
||||
}
|
||||
if description.is_some() {
|
||||
return Err(Error::ParseError("Too many children in propose element."));
|
||||
}
|
||||
description = Some(child.clone());
|
||||
}
|
||||
JingleMI::Propose {
|
||||
sid: get_sid(elem)?,
|
||||
description: description.ok_or(Error::ParseError(
|
||||
"Propose element 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.");
|
||||
}
|
||||
}
|
||||
46
xmpp-parsers/src/jingle_rtcp_fb.rs
Normal file
46
xmpp-parsers/src/jingle_rtcp_fb.rs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
generate_element!(
|
||||
/// Wrapper element for a rtcp-fb.
|
||||
RtcpFb, "rtcp-fb", JINGLE_RTCP_FB,
|
||||
attributes: [
|
||||
/// Type of this rtcp-fb.
|
||||
type_: Required<String> = "type",
|
||||
|
||||
/// Subtype of this rtcp-fb, if relevant.
|
||||
subtype: Option<String> = "subtype",
|
||||
]
|
||||
);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::Element;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(RtcpFb, 24);
|
||||
}
|
||||
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(RtcpFb, 48);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_simple() {
|
||||
let elem: Element = "<rtcp-fb xmlns='urn:xmpp:jingle:apps:rtp:rtcp-fb:0' type='nack' subtype='sli'/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let rtcp_fb = RtcpFb::try_from(elem).unwrap();
|
||||
assert_eq!(rtcp_fb.type_, "nack");
|
||||
assert_eq!(rtcp_fb.subtype.unwrap(), "sli");
|
||||
}
|
||||
}
|
||||
173
xmpp-parsers/src/jingle_rtp.rs
Normal file
173
xmpp-parsers/src/jingle_rtp.rs
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
use crate::jingle_ssma::{Source, Group};
|
||||
use crate::jingle_rtcp_fb::RtcpFb;
|
||||
|
||||
generate_element!(
|
||||
/// Wrapper element describing an RTP session.
|
||||
Description, "description", JINGLE_RTP,
|
||||
attributes: [
|
||||
/// Namespace of the encryption scheme used.
|
||||
media: Required<String> = "media",
|
||||
|
||||
/// User-friendly name for the encryption scheme, should be `None` for OTR,
|
||||
/// legacy OpenPGP and OX.
|
||||
// XXX: is this a String or an u32?! Refer to RFC 3550.
|
||||
ssrc: Option<String> = "ssrc",
|
||||
],
|
||||
children: [
|
||||
/// List of encodings that can be used for this RTP stream.
|
||||
payload_types: Vec<PayloadType> = ("payload-type", JINGLE_RTP) => PayloadType,
|
||||
|
||||
/// List of ssrc-group.
|
||||
ssrc_groups: Vec<Group> = ("ssrc-group", JINGLE_SSMA) => Group,
|
||||
|
||||
/// List of ssrc.
|
||||
ssrcs: Vec<Source> = ("source", JINGLE_SSMA) => Source
|
||||
|
||||
// TODO: Add support for <encryption/> and <bandwidth/>.
|
||||
]
|
||||
);
|
||||
|
||||
impl Description {
|
||||
/// Create a new RTP description.
|
||||
pub fn new(media: String) -> Description {
|
||||
Description {
|
||||
media,
|
||||
ssrc: None,
|
||||
payload_types: Vec::new(),
|
||||
ssrc_groups: Vec::new(),
|
||||
ssrcs: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
generate_attribute!(
|
||||
/// The number of channels.
|
||||
Channels, "channels", u8, Default = 1
|
||||
);
|
||||
|
||||
generate_element!(
|
||||
/// An encoding that can be used for an RTP stream.
|
||||
PayloadType, "payload-type", JINGLE_RTP,
|
||||
attributes: [
|
||||
/// The number of channels.
|
||||
channels: Default<Channels> = "channels",
|
||||
|
||||
/// The sampling frequency in Hertz.
|
||||
clockrate: Option<u32> = "clockrate",
|
||||
|
||||
/// The payload identifier.
|
||||
id: Required<u8> = "id",
|
||||
|
||||
/// Maximum packet time as specified in RFC 4566.
|
||||
maxptime: Option<u32> = "maxptime",
|
||||
|
||||
/// The appropriate subtype of the MIME type.
|
||||
name: Option<String> = "name",
|
||||
|
||||
/// Packet time as specified in RFC 4566.
|
||||
ptime: Option<u32> = "ptime",
|
||||
],
|
||||
children: [
|
||||
/// List of parameters specifying this payload-type.
|
||||
///
|
||||
/// Their order MUST be ignored.
|
||||
parameters: Vec<Parameter> = ("parameter", JINGLE_RTP) => Parameter,
|
||||
|
||||
/// List of rtcp-fb parameters from XEP-0293.
|
||||
rtcp_fbs: Vec<RtcpFb> = ("rtcp-fb", JINGLE_RTCP_FB) => RtcpFb
|
||||
]
|
||||
);
|
||||
|
||||
impl PayloadType {
|
||||
/// Create a new RTP payload-type.
|
||||
pub fn new(id: u8, name: String) -> PayloadType {
|
||||
PayloadType {
|
||||
channels: Default::default(),
|
||||
clockrate: None,
|
||||
id,
|
||||
maxptime: None,
|
||||
name: Some(name),
|
||||
ptime: None,
|
||||
parameters: Vec::new(),
|
||||
rtcp_fbs: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
generate_element!(
|
||||
/// Parameter related to a payload.
|
||||
Parameter, "parameter", JINGLE_RTP,
|
||||
attributes: [
|
||||
/// The name of the parameter, from the list at
|
||||
/// https://www.iana.org/assignments/sdp-parameters/sdp-parameters.xhtml
|
||||
name: Required<String> = "name",
|
||||
|
||||
/// The value of this parameter.
|
||||
value: Required<String> = "value",
|
||||
]
|
||||
);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::Element;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(Description, 36);
|
||||
assert_size!(Channels, 1);
|
||||
assert_size!(PayloadType, 52);
|
||||
assert_size!(Parameter, 24);
|
||||
}
|
||||
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(Description, 120);
|
||||
assert_size!(Channels, 1);
|
||||
assert_size!(PayloadType, 104);
|
||||
assert_size!(Parameter, 48);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple() {
|
||||
let elem: Element = "
|
||||
<description xmlns='urn:xmpp:jingle:apps:rtp:1' media='audio'>
|
||||
<payload-type xmlns='urn:xmpp:jingle:apps:rtp:1' channels='2' clockrate='48000' id='96' name='OPUS'/>
|
||||
<payload-type xmlns='urn:xmpp:jingle:apps:rtp:1' channels='1' clockrate='32000' id='105' name='SPEEX'/>
|
||||
<payload-type xmlns='urn:xmpp:jingle:apps:rtp:1' channels='1' clockrate='8000' id='9' name='G722'/>
|
||||
<payload-type xmlns='urn:xmpp:jingle:apps:rtp:1' channels='1' clockrate='16000' id='106' name='SPEEX'/>
|
||||
<payload-type xmlns='urn:xmpp:jingle:apps:rtp:1' clockrate='8000' id='8' name='PCMA'/>
|
||||
<payload-type xmlns='urn:xmpp:jingle:apps:rtp:1' clockrate='8000' id='0' name='PCMU'/>
|
||||
<payload-type xmlns='urn:xmpp:jingle:apps:rtp:1' channels='1' clockrate='8000' id='107' name='SPEEX'/>
|
||||
<payload-type xmlns='urn:xmpp:jingle:apps:rtp:1' channels='1' clockrate='8000' id='99' name='AMR'>
|
||||
<parameter xmlns='urn:xmpp:jingle:apps:rtp:1' name='octet-align' value='1'/>
|
||||
<parameter xmlns='urn:xmpp:jingle:apps:rtp:1' name='crc' value='0'/>
|
||||
<parameter xmlns='urn:xmpp:jingle:apps:rtp:1' name='robust-sorting' value='0'/>
|
||||
<parameter xmlns='urn:xmpp:jingle:apps:rtp:1' name='interleaving' value='0'/>
|
||||
</payload-type>
|
||||
<payload-type xmlns='urn:xmpp:jingle:apps:rtp:1' clockrate='48000' id='100' name='telephone-event'>
|
||||
<parameter xmlns='urn:xmpp:jingle:apps:rtp:1' name='events' value='0-15'/>
|
||||
</payload-type>
|
||||
<payload-type xmlns='urn:xmpp:jingle:apps:rtp:1' clockrate='16000' id='101' name='telephone-event'>
|
||||
<parameter xmlns='urn:xmpp:jingle:apps:rtp:1' name='events' value='0-15'/>
|
||||
</payload-type>
|
||||
<payload-type xmlns='urn:xmpp:jingle:apps:rtp:1' clockrate='8000' id='102' name='telephone-event'>
|
||||
<parameter xmlns='urn:xmpp:jingle:apps:rtp:1' name='events' value='0-15'/>
|
||||
</payload-type>
|
||||
</description>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let desc = Description::try_from(elem).unwrap();
|
||||
assert_eq!(desc.media, "audio");
|
||||
assert_eq!(desc.ssrc, None);
|
||||
}
|
||||
}
|
||||
352
xmpp-parsers/src/jingle_s5b.rs
Normal file
352
xmpp-parsers/src/jingle_s5b.rs
Normal file
|
|
@ -0,0 +1,352 @@
|
|||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
use crate::util::error::Error;
|
||||
use crate::ns;
|
||||
use jid::Jid;
|
||||
use crate::Element;
|
||||
use std::net::IpAddr;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
generate_attribute!(
|
||||
/// The type of the connection being proposed by this candidate.
|
||||
Type, "type", {
|
||||
/// Direct connection using NAT assisting technologies like NAT-PMP or
|
||||
/// UPnP-IGD.
|
||||
Assisted => "assisted",
|
||||
|
||||
/// Direct connection using the given interface.
|
||||
Direct => "direct",
|
||||
|
||||
/// SOCKS5 relay.
|
||||
Proxy => "proxy",
|
||||
|
||||
/// Tunnel protocol such as Teredo.
|
||||
Tunnel => "tunnel",
|
||||
}, Default = Direct
|
||||
);
|
||||
|
||||
generate_attribute!(
|
||||
/// Which mode to use for the connection.
|
||||
Mode, "mode", {
|
||||
/// Use TCP, which is the default.
|
||||
Tcp => "tcp",
|
||||
|
||||
/// Use UDP.
|
||||
Udp => "udp",
|
||||
}, Default = Tcp
|
||||
);
|
||||
|
||||
generate_id!(
|
||||
/// An identifier for a candidate.
|
||||
CandidateId
|
||||
);
|
||||
|
||||
generate_id!(
|
||||
/// An identifier for a stream.
|
||||
StreamId
|
||||
);
|
||||
|
||||
generate_element!(
|
||||
/// A candidate for a connection.
|
||||
Candidate, "candidate", JINGLE_S5B,
|
||||
attributes: [
|
||||
/// The identifier for this candidate.
|
||||
cid: Required<CandidateId> = "cid",
|
||||
|
||||
/// The host to connect to.
|
||||
host: Required<IpAddr> = "host",
|
||||
|
||||
/// The JID to request at the given end.
|
||||
jid: Required<Jid> = "jid",
|
||||
|
||||
/// The port to connect to.
|
||||
port: Option<u16> = "port",
|
||||
|
||||
/// The priority of this candidate, computed using this formula:
|
||||
/// priority = (2^16)*(type preference) + (local preference)
|
||||
priority: Required<u32> = "priority",
|
||||
|
||||
/// The type of the connection being proposed by this candidate.
|
||||
type_: Default<Type> = "type",
|
||||
]
|
||||
);
|
||||
|
||||
impl Candidate {
|
||||
/// Creates a new candidate with the given parameters.
|
||||
pub fn new(cid: CandidateId, host: IpAddr, jid: Jid, priority: u32) -> Candidate {
|
||||
Candidate {
|
||||
cid,
|
||||
host,
|
||||
jid,
|
||||
priority,
|
||||
port: Default::default(),
|
||||
type_: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the port of this candidate.
|
||||
pub fn with_port(mut self, port: u16) -> Candidate {
|
||||
self.port = Some(port);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the type of this candidate.
|
||||
pub fn with_type(mut self, type_: Type) -> Candidate {
|
||||
self.type_ = type_;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// The payload of a transport.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum TransportPayload {
|
||||
/// The responder informs the initiator that the bytestream pointed by this
|
||||
/// candidate has been activated.
|
||||
Activated(CandidateId),
|
||||
|
||||
/// A list of suggested candidates.
|
||||
Candidates(Vec<Candidate>),
|
||||
|
||||
/// Both parties failed to use a candidate, they should fallback to another
|
||||
/// transport.
|
||||
CandidateError,
|
||||
|
||||
/// The candidate pointed here should be used by both parties.
|
||||
CandidateUsed(CandidateId),
|
||||
|
||||
/// This entity 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));
|
||||
}
|
||||
}
|
||||
114
xmpp-parsers/src/jingle_ssma.rs
Normal file
114
xmpp-parsers/src/jingle_ssma.rs
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
generate_element!(
|
||||
/// Source element for the ssrc SDP attribute.
|
||||
Source, "source", JINGLE_SSMA,
|
||||
attributes: [
|
||||
/// Maps to the ssrc-id parameter.
|
||||
id: Required<String> = "ssrc",
|
||||
],
|
||||
children: [
|
||||
/// List of attributes for this source.
|
||||
parameters: Vec<Parameter> = ("parameter", JINGLE_SSMA) => Parameter
|
||||
]
|
||||
);
|
||||
|
||||
impl Source {
|
||||
/// Create a new SSMA Source element.
|
||||
pub fn new(id: String) -> Source {
|
||||
Source {
|
||||
id,
|
||||
parameters: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
generate_element!(
|
||||
/// Parameter associated with a ssrc.
|
||||
Parameter, "parameter", JINGLE_SSMA,
|
||||
attributes: [
|
||||
/// The name of the parameter.
|
||||
name: Required<String> = "name",
|
||||
|
||||
/// The optional value of the parameter.
|
||||
value: Option<String> = "value",
|
||||
]
|
||||
);
|
||||
|
||||
generate_element!(
|
||||
/// Element grouping multiple ssrc.
|
||||
Group, "ssrc-group", JINGLE_SSMA,
|
||||
attributes: [
|
||||
/// The semantics of this group.
|
||||
semantics: Required<String> = "semantics",
|
||||
],
|
||||
children: [
|
||||
/// The various ssrc concerned by this group.
|
||||
sources: Vec<Source> = ("source", JINGLE_SSMA) => Source
|
||||
]
|
||||
);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::Element;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(Source, 24);
|
||||
assert_size!(Parameter, 24);
|
||||
assert_size!(Group, 24);
|
||||
}
|
||||
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(Source, 48);
|
||||
assert_size!(Parameter, 48);
|
||||
assert_size!(Group, 48);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_source() {
|
||||
let elem: Element = "
|
||||
<source ssrc='1656081975' xmlns='urn:xmpp:jingle:apps:rtp:ssma:0'>
|
||||
<parameter name='cname' value='Yv/wvbCdsDW2Prgd'/>
|
||||
<parameter name='msid' value='MLTJKIHilGn71fNQoszkQ4jlPTuS5vJyKVIv MLTJKIHilGn71fNQoszkQ4jlPTuS5vJyKVIva0'/>
|
||||
</source>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let mut ssrc = Source::try_from(elem).unwrap();
|
||||
assert_eq!(ssrc.id, "1656081975");
|
||||
assert_eq!(ssrc.parameters.len(), 2);
|
||||
let parameter = ssrc.parameters.pop().unwrap();
|
||||
assert_eq!(parameter.name, "msid");
|
||||
assert_eq!(parameter.value.unwrap(), "MLTJKIHilGn71fNQoszkQ4jlPTuS5vJyKVIv MLTJKIHilGn71fNQoszkQ4jlPTuS5vJyKVIva0");
|
||||
let parameter = ssrc.parameters.pop().unwrap();
|
||||
assert_eq!(parameter.name, "cname");
|
||||
assert_eq!(parameter.value.unwrap(), "Yv/wvbCdsDW2Prgd");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_source_group() {
|
||||
let elem: Element = "
|
||||
<ssrc-group semantics='FID' xmlns='urn:xmpp:jingle:apps:rtp:ssma:0'>
|
||||
<source ssrc='2301230316'/>
|
||||
<source ssrc='386328120'/>
|
||||
</ssrc-group>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let mut group = Group::try_from(elem).unwrap();
|
||||
assert_eq!(group.semantics, "FID");
|
||||
assert_eq!(group.sources.len(), 2);
|
||||
let source = group.sources.pop().unwrap();
|
||||
assert_eq!(source.id, "386328120");
|
||||
let source = group.sources.pop().unwrap();
|
||||
assert_eq!(source.id, "2301230316");
|
||||
}
|
||||
}
|
||||
214
xmpp-parsers/src/lib.rs
Normal file
214
xmpp-parsers/src/lib.rs
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
//! A crate parsing common XMPP elements into Rust structures.
|
||||
//!
|
||||
//! Each module implements the `TryFrom<Element>` trait, which takes a
|
||||
//! minidom [`Element`] and returns a `Result` whose value is `Ok` if the
|
||||
//! element parsed correctly, `Err(error::Error)` otherwise.
|
||||
//!
|
||||
//! The returned structure can be manipuled as any Rust structure, with each
|
||||
//! field being public. You can also create the same structure manually, with
|
||||
//! some having `new()` and `with_*()` helper methods to create them.
|
||||
//!
|
||||
//! Once you are happy with your structure, you can serialise it back to an
|
||||
//! [`Element`], using either `From` or `Into<Element>`, which give you what
|
||||
//! you want to be sending on the wire.
|
||||
//!
|
||||
//! [`Element`]: ../minidom/element/struct.Element.html
|
||||
|
||||
// Copyright (c) 2017-2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||
// Copyright (c) 2017-2019 Maxime “pep” Buquet <pep@bouah.net>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
#![deny(missing_docs)]
|
||||
|
||||
pub use minidom::Element;
|
||||
pub use jid::{BareJid, FullJid, Jid, JidParseError};
|
||||
pub use crate::util::error::Error;
|
||||
|
||||
/// XML namespace definitions used through XMPP.
|
||||
pub mod ns;
|
||||
|
||||
#[macro_use]
|
||||
mod util;
|
||||
|
||||
/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core
|
||||
pub mod bind;
|
||||
/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core
|
||||
pub mod iq;
|
||||
/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core
|
||||
pub mod message;
|
||||
/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core
|
||||
pub mod presence;
|
||||
/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core
|
||||
pub mod sasl;
|
||||
/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core
|
||||
pub mod stanza_error;
|
||||
/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core
|
||||
pub mod stream;
|
||||
|
||||
/// RFC 6121: Extensible Messaging and Presence Protocol (XMPP): Instant Messaging and Presence
|
||||
pub mod roster;
|
||||
|
||||
/// RFC 7395: An Extensible Messaging and Presence Protocol (XMPP) Subprotocol for WebSocket
|
||||
pub mod websocket;
|
||||
|
||||
/// XEP-0004: Data Forms
|
||||
pub mod data_forms;
|
||||
|
||||
/// XEP-0030: Service Discovery
|
||||
pub mod disco;
|
||||
|
||||
/// XEP-0045: Multi-User Chat
|
||||
pub mod muc;
|
||||
|
||||
/// XEP-0047: In-Band Bytestreams
|
||||
pub mod ibb;
|
||||
|
||||
/// XEP-0048: Bookmarks
|
||||
pub mod bookmarks;
|
||||
|
||||
/// XEP-0059: Result Set Management
|
||||
pub mod rsm;
|
||||
|
||||
/// XEP-0060: Publish-Subscribe
|
||||
pub mod pubsub;
|
||||
|
||||
/// XEP-0071: XHTML-IM
|
||||
pub mod xhtml;
|
||||
|
||||
/// XEP-0077: In-Band Registration
|
||||
pub mod ibr;
|
||||
|
||||
/// XEP-0082: XMPP Date and Time Profiles
|
||||
pub mod date;
|
||||
|
||||
/// XEP-0084: User Avatar
|
||||
pub mod avatar;
|
||||
|
||||
/// XEP-0085: Chat State Notifications
|
||||
pub mod chatstates;
|
||||
|
||||
/// XEP-0092: Software Version
|
||||
pub mod version;
|
||||
|
||||
/// XEP-0107: User Mood
|
||||
pub mod mood;
|
||||
|
||||
/// XEP-0114: Jabber Component Protocol
|
||||
pub mod component;
|
||||
|
||||
/// XEP-0115: Entity Capabilities
|
||||
pub mod caps;
|
||||
|
||||
/// XEP-0118: User Tune
|
||||
pub mod tune;
|
||||
|
||||
/// XEP-0157: Contact Addresses for XMPP Services
|
||||
pub mod server_info;
|
||||
|
||||
/// XEP-0166: Jingle
|
||||
pub mod jingle;
|
||||
|
||||
/// XEP-0167: Jingle RTP Sessions
|
||||
pub mod jingle_rtp;
|
||||
|
||||
/// XEP-0172: User Nickname
|
||||
pub mod nick;
|
||||
|
||||
/// XEP-0176: Jingle ICE-UDP Transport Method
|
||||
pub mod jingle_ice_udp;
|
||||
|
||||
/// XEP-0184: Message Delivery Receipts
|
||||
pub mod receipts;
|
||||
|
||||
/// XEP-0191: Blocking Command
|
||||
pub mod blocking;
|
||||
|
||||
/// XEP-0198: Stream Management
|
||||
pub mod sm;
|
||||
|
||||
/// XEP-0199: XMPP Ping
|
||||
pub mod ping;
|
||||
|
||||
/// XEP-0202: Entity Time
|
||||
pub mod time;
|
||||
|
||||
/// XEP-0203: Delayed Delivery
|
||||
pub mod delay;
|
||||
|
||||
/// XEP-0221: Data Forms Media Element
|
||||
pub mod media_element;
|
||||
|
||||
/// XEP-0224: Attention
|
||||
pub mod attention;
|
||||
|
||||
/// XEP-0231: Bits of Binary
|
||||
pub mod bob;
|
||||
|
||||
/// XEP-0234: Jingle File Transfer
|
||||
pub mod jingle_ft;
|
||||
|
||||
/// XEP-0257: Client Certificate Management for SASL EXTERNAL
|
||||
pub mod cert_management;
|
||||
|
||||
/// XEP-0260: Jingle SOCKS5 Bytestreams Transport Method
|
||||
pub mod jingle_s5b;
|
||||
|
||||
/// XEP-0261: Jingle In-Band Bytestreams Transport Method
|
||||
pub mod jingle_ibb;
|
||||
|
||||
/// XEP-0280: Message Carbons
|
||||
pub mod carbons;
|
||||
|
||||
/// XEP-0293: Jingle RTP Feedback Negotiation
|
||||
pub mod jingle_rtcp_fb;
|
||||
|
||||
/// XEP-0297: Stanza Forwarding
|
||||
pub mod forwarding;
|
||||
|
||||
/// XEP-0300: Use of Cryptographic Hash Functions in XMPP
|
||||
pub mod hashes;
|
||||
|
||||
/// XEP-0308: Last Message Correction
|
||||
pub mod message_correct;
|
||||
|
||||
/// XEP-0313: Message Archive Management
|
||||
pub mod mam;
|
||||
|
||||
/// XEP-0319: Last User Interaction in Presence
|
||||
pub mod idle;
|
||||
|
||||
/// XEP-0320: Use of DTLS-SRTP in Jingle Sessions
|
||||
pub mod jingle_dtls_srtp;
|
||||
|
||||
/// XEP-0328: JID Prep
|
||||
pub mod jid_prep;
|
||||
|
||||
/// XEP-0339: Source-Specific Media Attributes in Jingle
|
||||
pub mod jingle_ssma;
|
||||
|
||||
/// XEP-0352: Client State Indication
|
||||
pub mod csi;
|
||||
|
||||
/// XEP-0353: Jingle Message Initiation
|
||||
pub mod jingle_message;
|
||||
|
||||
/// XEP-0359: Unique and Stable Stanza IDs
|
||||
pub mod stanza_id;
|
||||
|
||||
/// XEP-0373: OpenPGP for XMPP
|
||||
pub mod openpgp;
|
||||
|
||||
/// XEP-0380: Explicit Message Encryption
|
||||
pub mod eme;
|
||||
|
||||
/// XEP-0390: Entity Capabilities 2.0
|
||||
pub mod ecaps2;
|
||||
|
||||
/// XEP-0402: Bookmarks 2 (This Time it's Serious)
|
||||
pub mod bookmarks2;
|
||||
|
||||
/// XEP-0421: Anonymous unique occupant identifiers for MUCs
|
||||
pub mod occupant_id;
|
||||
392
xmpp-parsers/src/mam.rs
Normal file
392
xmpp-parsers/src/mam.rs
Normal file
|
|
@ -0,0 +1,392 @@
|
|||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
use crate::data_forms::DataForm;
|
||||
use crate::util::error::Error;
|
||||
use crate::forwarding::Forwarded;
|
||||
use crate::iq::{IqGetPayload, IqResultPayload, IqSetPayload};
|
||||
use crate::message::MessagePayload;
|
||||
use crate::ns;
|
||||
use crate::pubsub::NodeName;
|
||||
use crate::rsm::{SetQuery, SetResult};
|
||||
use jid::Jid;
|
||||
use minidom::{Element, Node};
|
||||
use std::convert::TryFrom;
|
||||
|
||||
generate_id!(
|
||||
/// An identifier matching a result message to the query requesting it.
|
||||
QueryId
|
||||
);
|
||||
|
||||
generate_element!(
|
||||
/// Starts a query to the archive.
|
||||
Query, "query", MAM,
|
||||
attributes: [
|
||||
/// An optional identifier for matching forwarded messages to this
|
||||
/// query.
|
||||
queryid: Option<QueryId> = "queryid",
|
||||
|
||||
/// Must be set to Some when querying a PubSub 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);
|
||||
}
|
||||
}
|
||||
256
xmpp-parsers/src/media_element.rs
Normal file
256
xmpp-parsers/src/media_element.rs
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
use crate::util::helpers::TrimmedPlainText;
|
||||
|
||||
generate_element!(
|
||||
/// Represents an URI used in a media element.
|
||||
URI, "uri", MEDIA_ELEMENT,
|
||||
attributes: [
|
||||
/// The MIME type of the URI referenced.
|
||||
///
|
||||
/// See the [IANA MIME Media Types Registry][1] for a list of
|
||||
/// registered types, but unregistered or yet-to-be-registered are
|
||||
/// accepted too.
|
||||
///
|
||||
/// [1]: https://www.iana.org/assignments/media-types/media-types.xhtml
|
||||
type_: Required<String> = "type"
|
||||
],
|
||||
text: (
|
||||
/// The actual URI contained.
|
||||
uri: TrimmedPlainText<String>
|
||||
)
|
||||
);
|
||||
|
||||
generate_element!(
|
||||
/// References a media element, to be used in [data
|
||||
/// forms](../data_forms/index.html).
|
||||
MediaElement, "media", MEDIA_ELEMENT,
|
||||
attributes: [
|
||||
/// The recommended display width in pixels.
|
||||
width: Option<usize> = "width",
|
||||
|
||||
/// The recommended display height in pixels.
|
||||
height: Option<usize> = "height"
|
||||
],
|
||||
children: [
|
||||
/// A list of URIs referencing this media.
|
||||
uris: Vec<URI> = ("uri", MEDIA_ELEMENT) => URI
|
||||
]
|
||||
);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::data_forms::DataForm;
|
||||
use crate::util::error::Error;
|
||||
use crate::Element;
|
||||
use std::error::Error as StdError;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(URI, 24);
|
||||
assert_size!(MediaElement, 28);
|
||||
}
|
||||
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(URI, 48);
|
||||
assert_size!(MediaElement, 56);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple() {
|
||||
let elem: Element = "<media xmlns='urn:xmpp:media-element'/>".parse().unwrap();
|
||||
let media = MediaElement::try_from(elem).unwrap();
|
||||
assert!(media.width.is_none());
|
||||
assert!(media.height.is_none());
|
||||
assert!(media.uris.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_width_height() {
|
||||
let elem: Element = "<media xmlns='urn:xmpp:media-element' width='32' height='32'/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let media = MediaElement::try_from(elem).unwrap();
|
||||
assert_eq!(media.width.unwrap(), 32);
|
||||
assert_eq!(media.height.unwrap(), 32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_uri() {
|
||||
let elem: Element = "<media xmlns='urn:xmpp:media-element'><uri type='text/html'>https://example.org/</uri></media>".parse().unwrap();
|
||||
let media = MediaElement::try_from(elem).unwrap();
|
||||
assert_eq!(media.uris.len(), 1);
|
||||
assert_eq!(media.uris[0].type_, "text/html");
|
||||
assert_eq!(media.uris[0].uri, "https://example.org/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_width_height() {
|
||||
let elem: Element = "<media xmlns='urn:xmpp:media-element' width=''/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let error = MediaElement::try_from(elem).unwrap_err();
|
||||
let error = match error {
|
||||
Error::ParseIntError(error) => error,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(
|
||||
error.description(),
|
||||
"cannot parse integer from empty string"
|
||||
);
|
||||
|
||||
let elem: Element = "<media xmlns='urn:xmpp:media-element' width='coucou'/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let error = MediaElement::try_from(elem).unwrap_err();
|
||||
let error = match error {
|
||||
Error::ParseIntError(error) => error,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(error.description(), "invalid digit found in string");
|
||||
|
||||
let elem: Element = "<media xmlns='urn:xmpp:media-element' height=''/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let error = MediaElement::try_from(elem).unwrap_err();
|
||||
let error = match error {
|
||||
Error::ParseIntError(error) => error,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(
|
||||
error.description(),
|
||||
"cannot parse integer from empty string"
|
||||
);
|
||||
|
||||
let elem: Element = "<media xmlns='urn:xmpp:media-element' height='-10'/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let error = MediaElement::try_from(elem).unwrap_err();
|
||||
let error = match error {
|
||||
Error::ParseIntError(error) => error,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(error.description(), "invalid digit found in string");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unknown_child() {
|
||||
let elem: Element = "<media xmlns='urn:xmpp:media-element'><coucou/></media>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let error = MediaElement::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "Unknown child in media element.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bad_uri() {
|
||||
let elem: Element =
|
||||
"<media xmlns='urn:xmpp:media-element'><uri>https://example.org/</uri></media>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let error = MediaElement::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "Required attribute 'type' missing.");
|
||||
|
||||
let elem: Element = "<media xmlns='urn:xmpp:media-element'><uri type='text/html'/></media>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let error = MediaElement::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "URI missing in uri.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_xep_ex1() {
|
||||
let elem: Element = r#"
|
||||
<media xmlns='urn:xmpp:media-element'>
|
||||
<uri type='audio/x-wav'>
|
||||
http://victim.example.com/challenges/speech.wav?F3A6292C
|
||||
</uri>
|
||||
<uri type='audio/ogg; codecs=speex'>
|
||||
cid:sha1+a15a505e360702b79c75a5f67773072ed392f52a@bob.xmpp.org
|
||||
</uri>
|
||||
<uri type='audio/mpeg'>
|
||||
http://victim.example.com/challenges/speech.mp3?F3A6292C
|
||||
</uri>
|
||||
</media>"#
|
||||
.parse()
|
||||
.unwrap();
|
||||
let media = MediaElement::try_from(elem).unwrap();
|
||||
assert!(media.width.is_none());
|
||||
assert!(media.height.is_none());
|
||||
assert_eq!(media.uris.len(), 3);
|
||||
assert_eq!(media.uris[0].type_, "audio/x-wav");
|
||||
assert_eq!(
|
||||
media.uris[0].uri,
|
||||
"http://victim.example.com/challenges/speech.wav?F3A6292C"
|
||||
);
|
||||
assert_eq!(media.uris[1].type_, "audio/ogg; codecs=speex");
|
||||
assert_eq!(
|
||||
media.uris[1].uri,
|
||||
"cid:sha1+a15a505e360702b79c75a5f67773072ed392f52a@bob.xmpp.org"
|
||||
);
|
||||
assert_eq!(media.uris[2].type_, "audio/mpeg");
|
||||
assert_eq!(
|
||||
media.uris[2].uri,
|
||||
"http://victim.example.com/challenges/speech.mp3?F3A6292C"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_xep_ex2() {
|
||||
let elem: Element = r#"
|
||||
<x xmlns='jabber:x:data' type='form'>
|
||||
[ ... ]
|
||||
<field var='ocr'>
|
||||
<media xmlns='urn:xmpp:media-element'
|
||||
height='80'
|
||||
width='290'>
|
||||
<uri type='image/jpeg'>
|
||||
http://www.victim.com/challenges/ocr.jpeg?F3A6292C
|
||||
</uri>
|
||||
<uri type='image/jpeg'>
|
||||
cid:sha1+f24030b8d91d233bac14777be5ab531ca3b9f102@bob.xmpp.org
|
||||
</uri>
|
||||
</media>
|
||||
</field>
|
||||
[ ... ]
|
||||
</x>"#
|
||||
.parse()
|
||||
.unwrap();
|
||||
let form = DataForm::try_from(elem).unwrap();
|
||||
assert_eq!(form.fields.len(), 1);
|
||||
assert_eq!(form.fields[0].var, "ocr");
|
||||
assert_eq!(form.fields[0].media[0].width, Some(290));
|
||||
assert_eq!(form.fields[0].media[0].height, Some(80));
|
||||
assert_eq!(form.fields[0].media[0].uris[0].type_, "image/jpeg");
|
||||
assert_eq!(
|
||||
form.fields[0].media[0].uris[0].uri,
|
||||
"http://www.victim.com/challenges/ocr.jpeg?F3A6292C"
|
||||
);
|
||||
assert_eq!(form.fields[0].media[0].uris[1].type_, "image/jpeg");
|
||||
assert_eq!(
|
||||
form.fields[0].media[0].uris[1].uri,
|
||||
"cid:sha1+f24030b8d91d233bac14777be5ab531ca3b9f102@bob.xmpp.org"
|
||||
);
|
||||
}
|
||||
}
|
||||
418
xmpp-parsers/src/message.rs
Normal file
418
xmpp-parsers/src/message.rs
Normal file
|
|
@ -0,0 +1,418 @@
|
|||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
use crate::util::error::Error;
|
||||
use crate::ns;
|
||||
use jid::Jid;
|
||||
use crate::Element;
|
||||
use std::collections::BTreeMap;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
/// Should be implemented on every known payload of a `<message/>`.
|
||||
pub trait MessagePayload: TryFrom<Element> + Into<Element> {}
|
||||
|
||||
generate_attribute!(
|
||||
/// The type of a message.
|
||||
MessageType, "type", {
|
||||
/// Standard instant messaging message.
|
||||
Chat => "chat",
|
||||
|
||||
/// Notifies that an error happened.
|
||||
Error => "error",
|
||||
|
||||
/// Standard group instant messaging message.
|
||||
Groupchat => "groupchat",
|
||||
|
||||
/// Used by servers to notify users when things happen.
|
||||
Headline => "headline",
|
||||
|
||||
/// This is an email-like message, it usually contains a
|
||||
/// [subject](struct.Subject.html).
|
||||
Normal => "normal",
|
||||
}, Default = Normal
|
||||
);
|
||||
|
||||
type Lang = String;
|
||||
|
||||
generate_elem_id!(
|
||||
/// Represents one `<body/>` element, that is the free form text content of
|
||||
/// a message.
|
||||
Body,
|
||||
"body",
|
||||
DEFAULT_NS
|
||||
);
|
||||
|
||||
generate_elem_id!(
|
||||
/// Defines the subject of a room, or of an email-like normal message.
|
||||
Subject,
|
||||
"subject",
|
||||
DEFAULT_NS
|
||||
);
|
||||
|
||||
generate_elem_id!(
|
||||
/// A thread identifier, so that other people can specify to which message
|
||||
/// they are replying.
|
||||
Thread,
|
||||
"thread",
|
||||
DEFAULT_NS
|
||||
);
|
||||
|
||||
/// The main structure representing the `<message/>` stanza.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Message {
|
||||
/// The JID emitting this stanza.
|
||||
pub from: Option<Jid>,
|
||||
|
||||
/// The recipient of this stanza.
|
||||
pub to: Option<Jid>,
|
||||
|
||||
/// The @id attribute of this stanza, which is required in order to match a
|
||||
/// request with its response.
|
||||
pub id: Option<String>,
|
||||
|
||||
/// The type of this message.
|
||||
pub type_: MessageType,
|
||||
|
||||
/// A list of bodies, sorted per language. Use
|
||||
/// [get_best_body()](#method.get_best_body) to access them on reception.
|
||||
pub bodies: BTreeMap<Lang, Body>,
|
||||
|
||||
/// A list of subjects, sorted per language. Use
|
||||
/// [get_best_subject()](#method.get_best_subject) to access them on
|
||||
/// reception.
|
||||
pub subjects: BTreeMap<Lang, Subject>,
|
||||
|
||||
/// An optional thread identifier, so that other people can reply directly
|
||||
/// to this message.
|
||||
pub thread: Option<Thread>,
|
||||
|
||||
/// A list of the extension payloads contained in this stanza.
|
||||
pub payloads: Vec<Element>,
|
||||
}
|
||||
|
||||
impl Message {
|
||||
/// Creates a new `<message/>` stanza for the given recipient.
|
||||
pub fn new(to: Option<Jid>) -> Message {
|
||||
Message {
|
||||
from: None,
|
||||
to,
|
||||
id: None,
|
||||
type_: MessageType::Chat,
|
||||
bodies: BTreeMap::new(),
|
||||
subjects: BTreeMap::new(),
|
||||
thread: None,
|
||||
payloads: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn get_best<'a, T>(
|
||||
map: &'a BTreeMap<Lang, T>,
|
||||
preferred_langs: Vec<&str>,
|
||||
) -> Option<(Lang, &'a T)> {
|
||||
if map.is_empty() {
|
||||
return None;
|
||||
}
|
||||
for lang in preferred_langs {
|
||||
if let Some(value) = map.get(lang) {
|
||||
return Some((Lang::from(lang), value));
|
||||
}
|
||||
}
|
||||
if let Some(value) = map.get("") {
|
||||
return Some((Lang::new(), value));
|
||||
}
|
||||
map.iter().map(|(lang, value)| (lang.clone(), value)).next()
|
||||
}
|
||||
|
||||
/// Returns the best matching body from a list of languages.
|
||||
///
|
||||
/// For instance, if a message contains both an xml:lang='de', an xml:lang='fr' and an English
|
||||
/// body without an xml:lang attribute, and you pass ["fr", "en"] as your preferred languages,
|
||||
/// `Some(("fr", the_second_body))` will be returned.
|
||||
///
|
||||
/// If no body matches, an undefined body will be returned.
|
||||
pub fn get_best_body(&self, preferred_langs: Vec<&str>) -> Option<(Lang, &Body)> {
|
||||
Message::get_best::<Body>(&self.bodies, preferred_langs)
|
||||
}
|
||||
|
||||
/// Returns the best matching subject from a list of languages.
|
||||
///
|
||||
/// For instance, if a message contains both an xml:lang='de', an xml:lang='fr' and an English
|
||||
/// subject without an xml:lang attribute, and you pass ["fr", "en"] as your preferred
|
||||
/// languages, `Some(("fr", the_second_subject))` will be returned.
|
||||
///
|
||||
/// If no subject matches, an undefined subject will be returned.
|
||||
pub fn get_best_subject(&self, preferred_langs: Vec<&str>) -> Option<(Lang, &Subject)> {
|
||||
Message::get_best::<Subject>(&self.subjects, preferred_langs)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Element> for Message {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(root: Element) -> Result<Message, Error> {
|
||||
check_self!(root, "message", DEFAULT_NS);
|
||||
let from = get_attr!(root, "from", Option);
|
||||
let to = get_attr!(root, "to", Option);
|
||||
let id = get_attr!(root, "id", Option);
|
||||
let type_ = get_attr!(root, "type", Default);
|
||||
let mut bodies = BTreeMap::new();
|
||||
let mut subjects = BTreeMap::new();
|
||||
let mut thread = None;
|
||||
let mut payloads = vec![];
|
||||
for elem in root.children() {
|
||||
if elem.is("body", ns::DEFAULT_NS) {
|
||||
check_no_children!(elem, "body");
|
||||
let lang = get_attr!(elem, "xml:lang", Default);
|
||||
let body = Body(elem.text());
|
||||
if bodies.insert(lang, body).is_some() {
|
||||
return Err(Error::ParseError(
|
||||
"Body element present twice for the same xml:lang.",
|
||||
));
|
||||
}
|
||||
} else if elem.is("subject", ns::DEFAULT_NS) {
|
||||
check_no_children!(elem, "subject");
|
||||
let lang = get_attr!(elem, "xml:lang", Default);
|
||||
let subject = Subject(elem.text());
|
||||
if subjects.insert(lang, subject).is_some() {
|
||||
return Err(Error::ParseError(
|
||||
"Subject element present twice for the same xml:lang.",
|
||||
));
|
||||
}
|
||||
} else if elem.is("thread", ns::DEFAULT_NS) {
|
||||
if thread.is_some() {
|
||||
return Err(Error::ParseError("Thread element present twice."));
|
||||
}
|
||||
check_no_children!(elem, "thread");
|
||||
thread = Some(Thread(elem.text()));
|
||||
} else {
|
||||
payloads.push(elem.clone())
|
||||
}
|
||||
}
|
||||
Ok(Message {
|
||||
from,
|
||||
to,
|
||||
id,
|
||||
type_,
|
||||
bodies,
|
||||
subjects,
|
||||
thread,
|
||||
payloads,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Message> for Element {
|
||||
fn from(message: Message) -> Element {
|
||||
Element::builder("message")
|
||||
.ns(ns::DEFAULT_NS)
|
||||
.attr("from", message.from)
|
||||
.attr("to", message.to)
|
||||
.attr("id", message.id)
|
||||
.attr("type", message.type_)
|
||||
.append_all(
|
||||
message
|
||||
.subjects
|
||||
.into_iter()
|
||||
.map(|(lang, subject)| {
|
||||
let mut subject = Element::from(subject);
|
||||
subject.set_attr(
|
||||
"xml:lang",
|
||||
match lang.as_ref() {
|
||||
"" => None,
|
||||
lang => Some(lang),
|
||||
},
|
||||
);
|
||||
subject
|
||||
})
|
||||
)
|
||||
.append_all(
|
||||
message
|
||||
.bodies
|
||||
.into_iter()
|
||||
.map(|(lang, body)| {
|
||||
let mut body = Element::from(body);
|
||||
body.set_attr(
|
||||
"xml:lang",
|
||||
match lang.as_ref() {
|
||||
"" => None,
|
||||
lang => Some(lang),
|
||||
},
|
||||
);
|
||||
body
|
||||
})
|
||||
)
|
||||
.append_all(message.payloads.into_iter())
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::util::compare_elements::NamespaceAwareCompare;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(MessageType, 1);
|
||||
assert_size!(Body, 12);
|
||||
assert_size!(Subject, 12);
|
||||
assert_size!(Thread, 12);
|
||||
assert_size!(Message, 144);
|
||||
}
|
||||
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(MessageType, 1);
|
||||
assert_size!(Body, 24);
|
||||
assert_size!(Subject, 24);
|
||||
assert_size!(Thread, 24);
|
||||
assert_size!(Message, 288);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple() {
|
||||
#[cfg(not(feature = "component"))]
|
||||
let elem: Element = "<message xmlns='jabber:client'/>".parse().unwrap();
|
||||
#[cfg(feature = "component")]
|
||||
let elem: Element = "<message xmlns='jabber:component:accept'/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let message = Message::try_from(elem).unwrap();
|
||||
assert_eq!(message.from, None);
|
||||
assert_eq!(message.to, None);
|
||||
assert_eq!(message.id, None);
|
||||
assert_eq!(message.type_, MessageType::Normal);
|
||||
assert!(message.payloads.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialise() {
|
||||
#[cfg(not(feature = "component"))]
|
||||
let elem: Element = "<message xmlns='jabber:client'/>".parse().unwrap();
|
||||
#[cfg(feature = "component")]
|
||||
let elem: Element = "<message xmlns='jabber:component:accept'/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let mut message = Message::new(None);
|
||||
message.type_ = MessageType::Normal;
|
||||
let elem2 = message.into();
|
||||
assert_eq!(elem, elem2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_body() {
|
||||
#[cfg(not(feature = "component"))]
|
||||
let elem: Element = "<message xmlns='jabber:client' to='coucou@example.org' type='chat'><body>Hello world!</body></message>".parse().unwrap();
|
||||
#[cfg(feature = "component")]
|
||||
let elem: Element = "<message xmlns='jabber:component:accept' to='coucou@example.org' type='chat'><body>Hello world!</body></message>".parse().unwrap();
|
||||
let elem1 = elem.clone();
|
||||
let message = Message::try_from(elem).unwrap();
|
||||
assert_eq!(message.bodies[""], Body::from_str("Hello world!").unwrap());
|
||||
|
||||
{
|
||||
let (lang, body) = message.get_best_body(vec!["en"]).unwrap();
|
||||
assert_eq!(lang, "");
|
||||
assert_eq!(body, &Body::from_str("Hello world!").unwrap());
|
||||
}
|
||||
|
||||
let elem2 = message.into();
|
||||
assert!(elem1.compare_to(&elem2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialise_body() {
|
||||
#[cfg(not(feature = "component"))]
|
||||
let elem: Element = "<message xmlns='jabber:client' to='coucou@example.org' type='chat'><body>Hello world!</body></message>".parse().unwrap();
|
||||
#[cfg(feature = "component")]
|
||||
let elem: Element = "<message xmlns='jabber:component:accept' to='coucou@example.org' type='chat'><body>Hello world!</body></message>".parse().unwrap();
|
||||
let mut message = Message::new(Some(Jid::from_str("coucou@example.org").unwrap()));
|
||||
message
|
||||
.bodies
|
||||
.insert(String::from(""), Body::from_str("Hello world!").unwrap());
|
||||
let elem2 = message.into();
|
||||
assert!(elem.compare_to(&elem2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_subject() {
|
||||
#[cfg(not(feature = "component"))]
|
||||
let elem: Element = "<message xmlns='jabber:client' to='coucou@example.org' type='chat'><subject>Hello world!</subject></message>".parse().unwrap();
|
||||
#[cfg(feature = "component")]
|
||||
let elem: Element = "<message xmlns='jabber:component:accept' to='coucou@example.org' type='chat'><subject>Hello world!</subject></message>".parse().unwrap();
|
||||
let elem1 = elem.clone();
|
||||
let message = Message::try_from(elem).unwrap();
|
||||
assert_eq!(
|
||||
message.subjects[""],
|
||||
Subject::from_str("Hello world!").unwrap()
|
||||
);
|
||||
|
||||
{
|
||||
let (lang, subject) = message.get_best_subject(vec!["en"]).unwrap();
|
||||
assert_eq!(lang, "");
|
||||
assert_eq!(subject, &Subject::from_str("Hello world!").unwrap());
|
||||
}
|
||||
|
||||
let elem2 = message.into();
|
||||
assert!(elem1.compare_to(&elem2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_best_body() {
|
||||
#[cfg(not(feature = "component"))]
|
||||
let elem: Element = "<message xmlns='jabber:client' to='coucou@example.org' type='chat'><body xml:lang='de'>Hallo Welt!</body><body xml:lang='fr'>Salut le monde !</body><body>Hello world!</body></message>".parse().unwrap();
|
||||
#[cfg(feature = "component")]
|
||||
let elem: Element = "<message xmlns='jabber:component:accept' to='coucou@example.org' type='chat'><body>Hello world!</body></message>".parse().unwrap();
|
||||
let message = Message::try_from(elem).unwrap();
|
||||
|
||||
// Tests basic feature.
|
||||
{
|
||||
let (lang, body) = message.get_best_body(vec!["fr"]).unwrap();
|
||||
assert_eq!(lang, "fr");
|
||||
assert_eq!(body, &Body::from_str("Salut le monde !").unwrap());
|
||||
}
|
||||
|
||||
// Tests order.
|
||||
{
|
||||
let (lang, body) = message.get_best_body(vec!["en", "de"]).unwrap();
|
||||
assert_eq!(lang, "de");
|
||||
assert_eq!(body, &Body::from_str("Hallo Welt!").unwrap());
|
||||
}
|
||||
|
||||
// Tests fallback.
|
||||
{
|
||||
let (lang, body) = message.get_best_body(vec![]).unwrap();
|
||||
assert_eq!(lang, "");
|
||||
assert_eq!(body, &Body::from_str("Hello world!").unwrap());
|
||||
}
|
||||
|
||||
// Tests fallback.
|
||||
{
|
||||
let (lang, body) = message.get_best_body(vec!["ja"]).unwrap();
|
||||
assert_eq!(lang, "");
|
||||
assert_eq!(body, &Body::from_str("Hello world!").unwrap());
|
||||
}
|
||||
|
||||
let message = Message::new(None);
|
||||
|
||||
// Tests without a body.
|
||||
assert_eq!(message.get_best_body(vec!("ja")), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_attention() {
|
||||
#[cfg(not(feature = "component"))]
|
||||
let elem: Element = "<message xmlns='jabber:client' to='coucou@example.org' type='chat'><attention xmlns='urn:xmpp:attention:0'/></message>".parse().unwrap();
|
||||
#[cfg(feature = "component")]
|
||||
let elem: Element = "<message xmlns='jabber:component:accept' to='coucou@example.org' type='chat'><attention xmlns='urn:xmpp:attention:0'/></message>".parse().unwrap();
|
||||
let elem1 = elem.clone();
|
||||
let message = Message::try_from(elem).unwrap();
|
||||
let elem2 = message.into();
|
||||
assert_eq!(elem1, elem2);
|
||||
}
|
||||
}
|
||||
99
xmpp-parsers/src/message_correct.rs
Normal file
99
xmpp-parsers/src/message_correct.rs
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
use crate::message::MessagePayload;
|
||||
|
||||
generate_element!(
|
||||
/// Defines that the message containing this payload should replace a
|
||||
/// previous message, identified by the id.
|
||||
Replace, "replace", MESSAGE_CORRECT,
|
||||
attributes: [
|
||||
/// The 'id' attribute of the message getting corrected.
|
||||
id: Required<String> = "id",
|
||||
]
|
||||
);
|
||||
|
||||
impl MessagePayload for Replace {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::util::error::Error;
|
||||
use crate::Element;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(Replace, 12);
|
||||
}
|
||||
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(Replace, 24);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple() {
|
||||
let elem: Element = "<replace xmlns='urn:xmpp:message-correct:0' id='coucou'/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
Replace::try_from(elem).unwrap();
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "disable-validation"))]
|
||||
#[test]
|
||||
fn test_invalid_attribute() {
|
||||
let elem: Element = "<replace xmlns='urn:xmpp:message-correct:0' coucou=''/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let error = Replace::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "Unknown attribute in replace element.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_child() {
|
||||
let elem: Element = "<replace xmlns='urn:xmpp:message-correct:0'><coucou/></replace>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let error = Replace::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "Unknown child in replace element.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_id() {
|
||||
let elem: Element = "<replace xmlns='urn:xmpp:message-correct:0'/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let error = Replace::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "Required attribute 'id' missing.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialise() {
|
||||
let elem: Element = "<replace xmlns='urn:xmpp:message-correct:0' id='coucou'/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let replace = Replace {
|
||||
id: String::from("coucou"),
|
||||
};
|
||||
let elem2 = replace.into();
|
||||
assert_eq!(elem, elem2);
|
||||
}
|
||||
}
|
||||
312
xmpp-parsers/src/mood.rs
Normal file
312
xmpp-parsers/src/mood.rs
Normal file
|
|
@ -0,0 +1,312 @@
|
|||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
generate_element_enum!(
|
||||
/// Enum representing all of the possible values of the XEP-0107 moods.
|
||||
MoodEnum, "mood", MOOD, {
|
||||
/// Impressed with fear or apprehension; in fear; apprehensive.
|
||||
Afraid => "afraid",
|
||||
|
||||
/// Astonished; confounded with fear, surprise or wonder.
|
||||
Amazed => "amazed",
|
||||
|
||||
/// Inclined to love; having a propensity to love, or to sexual enjoyment; loving, fond, affectionate, passionate, lustful, sexual, etc.
|
||||
Amorous => "amorous",
|
||||
|
||||
/// Displaying or feeling anger, i.e., a strong feeling of displeasure, hostility or antagonism towards someone or something, usually combined with an urge to harm.
|
||||
Angry => "angry",
|
||||
|
||||
/// To be disturbed or irritated, especially by continued or repeated acts.
|
||||
Annoyed => "annoyed",
|
||||
|
||||
/// Full of anxiety or disquietude; greatly concerned or solicitous, esp. respecting something future or unknown; being in painful suspense.
|
||||
Anxious => "anxious",
|
||||
|
||||
/// To be stimulated in one's feelings, especially to be sexually stimulated.
|
||||
Aroused => "aroused",
|
||||
|
||||
/// Feeling shame or guilt.
|
||||
Ashamed => "ashamed",
|
||||
|
||||
/// Suffering from boredom; uninterested, without attention.
|
||||
Bored => "bored",
|
||||
|
||||
/// Strong in the face of fear; courageous.
|
||||
Brave => "brave",
|
||||
|
||||
/// Peaceful, quiet.
|
||||
Calm => "calm",
|
||||
|
||||
/// Taking care or caution; tentative.
|
||||
Cautious => "cautious",
|
||||
|
||||
/// Feeling the sensation of coldness, especially to the point of discomfort.
|
||||
Cold => "cold",
|
||||
|
||||
/// Feeling very sure of or positive about something, especially about one's own capabilities.
|
||||
Confident => "confident",
|
||||
|
||||
/// Chaotic, jumbled or muddled.
|
||||
Confused => "confused",
|
||||
|
||||
/// Feeling introspective or thoughtful.
|
||||
Contemplative => "contemplative",
|
||||
|
||||
/// Pleased at the satisfaction of a want or desire; satisfied.
|
||||
Contented => "contented",
|
||||
|
||||
/// Grouchy, irritable; easily upset.
|
||||
Cranky => "cranky",
|
||||
|
||||
/// Feeling out of control; feeling overly excited or enthusiastic.
|
||||
Crazy => "crazy",
|
||||
|
||||
/// Feeling original, expressive, or imaginative.
|
||||
Creative => "creative",
|
||||
|
||||
/// Inquisitive; tending to ask questions, investigate, or explore.
|
||||
Curious => "curious",
|
||||
|
||||
/// Feeling sad and dispirited.
|
||||
Dejected => "dejected",
|
||||
|
||||
/// Severely despondent and unhappy.
|
||||
Depressed => "depressed",
|
||||
|
||||
/// Defeated of expectation or hope; let down.
|
||||
Disappointed => "disappointed",
|
||||
|
||||
/// Filled with disgust; irritated and out of patience.
|
||||
Disgusted => "disgusted",
|
||||
|
||||
/// Feeling a sudden or complete loss of courage in the face of trouble or danger.
|
||||
Dismayed => "dismayed",
|
||||
|
||||
/// Having one's attention diverted; preoccupied.
|
||||
Distracted => "distracted",
|
||||
|
||||
/// Having a feeling of shameful discomfort.
|
||||
Embarrassed => "embarrassed",
|
||||
|
||||
/// Feeling pain by the excellence or good fortune of another.
|
||||
Envious => "envious",
|
||||
|
||||
/// Having great enthusiasm.
|
||||
Excited => "excited",
|
||||
|
||||
/// In the mood for flirting.
|
||||
Flirtatious => "flirtatious",
|
||||
|
||||
/// Suffering from frustration; dissatisfied, agitated, or discontented because one is unable to perform an action or fulfill a desire.
|
||||
Frustrated => "frustrated",
|
||||
|
||||
/// Feeling appreciation or thanks.
|
||||
Grateful => "grateful",
|
||||
|
||||
/// Feeling very sad about something, especially something lost; mournful; sorrowful.
|
||||
Grieving => "grieving",
|
||||
|
||||
/// Unhappy and irritable.
|
||||
Grumpy => "grumpy",
|
||||
|
||||
/// Feeling responsible for wrongdoing; feeling blameworthy.
|
||||
Guilty => "guilty",
|
||||
|
||||
/// Experiencing the effect of favourable fortune; having the feeling arising from the consciousness of well-being or of enjoyment; enjoying good of any kind, as peace, tranquillity, comfort; contented; joyous.
|
||||
Happy => "happy",
|
||||
|
||||
/// Having a positive feeling, belief, or expectation that something wished for can or will happen.
|
||||
Hopeful => "hopeful",
|
||||
|
||||
/// Feeling the sensation of heat, especially to the point of discomfort.
|
||||
Hot => "hot",
|
||||
|
||||
/// Having or showing a modest or low estimate of one's own importance; feeling lowered in dignity or importance.
|
||||
Humbled => "humbled",
|
||||
|
||||
/// Feeling deprived of dignity or self-respect.
|
||||
Humiliated => "humiliated",
|
||||
|
||||
/// Having a physical need for food.
|
||||
Hungry => "hungry",
|
||||
|
||||
/// Wounded, injured, or pained, whether physically or emotionally.
|
||||
Hurt => "hurt",
|
||||
|
||||
/// Favourably affected by something or someone.
|
||||
Impressed => "impressed",
|
||||
|
||||
/// Feeling amazement at something or someone; or feeling a combination of fear and reverence.
|
||||
InAwe => "in_awe",
|
||||
|
||||
/// Feeling strong affection, care, liking, or attraction..
|
||||
InLove => "in_love",
|
||||
|
||||
/// Showing anger or indignation, especially at something unjust or wrong.
|
||||
Indignant => "indignant",
|
||||
|
||||
/// Showing great attention to something or someone; having or showing interest.
|
||||
Interested => "interested",
|
||||
|
||||
/// Under the influence of alcohol; drunk.
|
||||
Intoxicated => "intoxicated",
|
||||
|
||||
/// Feeling as if one cannot be defeated, overcome or denied.
|
||||
Invincible => "invincible",
|
||||
|
||||
/// Fearful of being replaced in position or affection.
|
||||
Jealous => "jealous",
|
||||
|
||||
/// Feeling isolated, empty, or abandoned.
|
||||
Lonely => "lonely",
|
||||
|
||||
/// Unable to find one's way, either physically or emotionally.
|
||||
Lost => "lost",
|
||||
|
||||
/// Feeling as if one will be favored by luck.
|
||||
Lucky => "lucky",
|
||||
|
||||
/// Causing or intending to cause intentional harm; bearing ill will towards another; cruel; malicious.
|
||||
Mean => "mean",
|
||||
|
||||
/// Given to sudden or frequent changes of mind or feeling; temperamental.
|
||||
Moody => "moody",
|
||||
|
||||
/// Easily agitated or alarmed; apprehensive or anxious.
|
||||
Nervous => "nervous",
|
||||
|
||||
/// Not having a strong mood or emotional state.
|
||||
Neutral => "neutral",
|
||||
|
||||
/// Feeling emotionally hurt, displeased, or insulted.
|
||||
Offended => "offended",
|
||||
|
||||
/// Feeling resentful anger caused by an extremely violent or vicious attack, or by an offensive, immoral, or indecent act.
|
||||
Outraged => "outraged",
|
||||
|
||||
/// Interested in play; fun, recreational, unserious, lighthearted; joking, silly.
|
||||
Playful => "playful",
|
||||
|
||||
/// Feeling a sense of one's own worth or accomplishment.
|
||||
Proud => "proud",
|
||||
|
||||
/// Having an easy-going mood; not stressed; calm.
|
||||
Relaxed => "relaxed",
|
||||
|
||||
/// Feeling uplifted because of the removal of stress or discomfort.
|
||||
Relieved => "relieved",
|
||||
|
||||
/// Feeling regret or sadness for doing something wrong.
|
||||
Remorseful => "remorseful",
|
||||
|
||||
/// Without rest; unable to be still or quiet; uneasy; continually moving.
|
||||
Restless => "restless",
|
||||
|
||||
/// Feeling sorrow; sorrowful, mournful.
|
||||
Sad => "sad",
|
||||
|
||||
/// Mocking and ironical.
|
||||
Sarcastic => "sarcastic",
|
||||
|
||||
/// Pleased at the fulfillment of a need or desire.
|
||||
Satisfied => "satisfied",
|
||||
|
||||
/// Without humor or expression of happiness; grave in manner or disposition; earnest; thoughtful; solemn.
|
||||
Serious => "serious",
|
||||
|
||||
/// Surprised, startled, confused, or taken aback.
|
||||
Shocked => "shocked",
|
||||
|
||||
/// Feeling easily frightened or scared; timid; reserved or coy.
|
||||
Shy => "shy",
|
||||
|
||||
/// Feeling in poor health; ill.
|
||||
Sick => "sick",
|
||||
|
||||
/// Feeling the need for sleep.
|
||||
Sleepy => "sleepy",
|
||||
|
||||
/// Acting without planning; natural; impulsive.
|
||||
Spontaneous => "spontaneous",
|
||||
|
||||
/// Suffering emotional pressure.
|
||||
Stressed => "stressed",
|
||||
|
||||
/// Capable of producing great physical force; or, emotionally forceful, able, determined, unyielding.
|
||||
Strong => "strong",
|
||||
|
||||
/// Experiencing a feeling caused by something unexpected.
|
||||
Surprised => "surprised",
|
||||
|
||||
/// Showing appreciation or gratitude.
|
||||
Thankful => "thankful",
|
||||
|
||||
/// Feeling the need to drink.
|
||||
Thirsty => "thirsty",
|
||||
|
||||
/// In need of rest or sleep.
|
||||
Tired => "tired",
|
||||
|
||||
/// [Feeling any emotion not defined here.]
|
||||
Undefined => "undefined",
|
||||
|
||||
/// Lacking in force or ability, either physical or emotional.
|
||||
Weak => "weak",
|
||||
|
||||
/// Thinking about unpleasant things that have happened or that might happen; feeling afraid and unhappy.
|
||||
Worried => "worried",
|
||||
}
|
||||
);
|
||||
|
||||
generate_elem_id!(
|
||||
/// Free-form text description of the mood.
|
||||
Text,
|
||||
"text",
|
||||
MOOD
|
||||
);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::Element;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(MoodEnum, 1);
|
||||
assert_size!(Text, 12);
|
||||
}
|
||||
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(MoodEnum, 1);
|
||||
assert_size!(Text, 24);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple() {
|
||||
let elem: Element = "<happy xmlns='http://jabber.org/protocol/mood'/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let mood = MoodEnum::try_from(elem).unwrap();
|
||||
assert_eq!(mood, MoodEnum::Happy);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_text() {
|
||||
let elem: Element = "<text xmlns='http://jabber.org/protocol/mood'>Yay!</text>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let elem2 = elem.clone();
|
||||
let text = Text::try_from(elem).unwrap();
|
||||
assert_eq!(text.0, String::from("Yay!"));
|
||||
|
||||
let elem3 = text.into();
|
||||
assert_eq!(elem2, elem3);
|
||||
}
|
||||
}
|
||||
14
xmpp-parsers/src/muc/mod.rs
Normal file
14
xmpp-parsers/src/muc/mod.rs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
// Copyright (c) 2017 Maxime “pep” Buquet <pep@bouah.net>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
/// The http://jabber.org/protocol/muc protocol.
|
||||
pub mod muc;
|
||||
|
||||
/// The http://jabber.org/protocol/muc#user protocol.
|
||||
pub mod user;
|
||||
|
||||
pub use self::muc::Muc;
|
||||
pub use self::user::MucUser;
|
||||
197
xmpp-parsers/src/muc/muc.rs
Normal file
197
xmpp-parsers/src/muc/muc.rs
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
// Copyright (c) 2017 Maxime “pep” Buquet <pep@bouah.net>
|
||||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
use crate::date::DateTime;
|
||||
use crate::presence::PresencePayload;
|
||||
|
||||
generate_element!(
|
||||
/// Represents the query for messages before our join.
|
||||
#[derive(PartialEq, Default)]
|
||||
History, "history", MUC,
|
||||
attributes: [
|
||||
/// How many characters of history to send, in XML characters.
|
||||
maxchars: Option<u32> = "maxchars",
|
||||
|
||||
/// How many messages to send.
|
||||
maxstanzas: Option<u32> = "maxstanzas",
|
||||
|
||||
/// Only send messages received in these last seconds.
|
||||
seconds: Option<u32> = "seconds",
|
||||
|
||||
/// Only send messages after this date.
|
||||
since: Option<DateTime> = "since",
|
||||
]
|
||||
);
|
||||
|
||||
impl History {
|
||||
/// Create a new empty history element.
|
||||
pub fn new() -> Self {
|
||||
History::default()
|
||||
}
|
||||
|
||||
/// Set how many characters of history to send.
|
||||
pub fn with_maxchars(mut self, maxchars: u32) -> Self {
|
||||
self.maxchars = Some(maxchars);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set how many messages to send.
|
||||
pub fn with_maxstanzas(mut self, maxstanzas: u32) -> Self {
|
||||
self.maxstanzas = Some(maxstanzas);
|
||||
self
|
||||
}
|
||||
|
||||
/// Only send messages received in these last seconds.
|
||||
pub fn with_seconds(mut self, seconds: u32) -> Self {
|
||||
self.seconds = Some(seconds);
|
||||
self
|
||||
}
|
||||
|
||||
/// Only send messages received since this date.
|
||||
pub fn with_since(mut self, since: DateTime) -> Self {
|
||||
self.since = Some(since);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
generate_element!(
|
||||
/// Represents a room join request.
|
||||
#[derive(PartialEq, Default)]
|
||||
Muc, "x", MUC, children: [
|
||||
/// Password to use when the room is protected by a password.
|
||||
password: Option<String> = ("password", MUC) => String,
|
||||
|
||||
/// Controls how much and how old we want to receive history on join.
|
||||
history: Option<History> = ("history", MUC) => History
|
||||
]
|
||||
);
|
||||
|
||||
impl PresencePayload for Muc {}
|
||||
|
||||
impl Muc {
|
||||
/// Create a new MUC join element.
|
||||
pub fn new() -> Self {
|
||||
Muc::default()
|
||||
}
|
||||
|
||||
/// Join a room with this password.
|
||||
pub fn with_password(mut self, password: String) -> Self {
|
||||
self.password = Some(password);
|
||||
self
|
||||
}
|
||||
|
||||
/// Join a room with only that much history.
|
||||
pub fn with_history(mut self, history: History) -> Self {
|
||||
self.history = Some(history);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::util::compare_elements::NamespaceAwareCompare;
|
||||
use crate::util::error::Error;
|
||||
use crate::Element;
|
||||
use std::str::FromStr;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
#[test]
|
||||
fn test_muc_simple() {
|
||||
let elem: Element = "<x xmlns='http://jabber.org/protocol/muc'/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
Muc::try_from(elem).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_muc_invalid_child() {
|
||||
let elem: Element = "<x xmlns='http://jabber.org/protocol/muc'><coucou/></x>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let error = Muc::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "Unknown child in x element.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_muc_serialise() {
|
||||
let elem: Element = "<x xmlns='http://jabber.org/protocol/muc'/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let muc = Muc {
|
||||
password: None,
|
||||
history: None,
|
||||
};
|
||||
let elem2 = muc.into();
|
||||
assert_eq!(elem, elem2);
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "disable-validation"))]
|
||||
#[test]
|
||||
fn test_muc_invalid_attribute() {
|
||||
let elem: Element = "<x xmlns='http://jabber.org/protocol/muc' coucou=''/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let error = Muc::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "Unknown attribute in x element.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_muc_simple_password() {
|
||||
let elem: Element = "
|
||||
<x xmlns='http://jabber.org/protocol/muc'>
|
||||
<password>coucou</password>
|
||||
</x>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let elem1 = elem.clone();
|
||||
let muc = Muc::try_from(elem).unwrap();
|
||||
assert_eq!(muc.password, Some("coucou".to_owned()));
|
||||
|
||||
let elem2 = Element::from(muc);
|
||||
assert!(elem1.compare_to(&elem2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history() {
|
||||
let elem: Element = "
|
||||
<x xmlns='http://jabber.org/protocol/muc'>
|
||||
<history maxstanzas='0'/>
|
||||
</x>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let muc = Muc::try_from(elem).unwrap();
|
||||
let muc2 = Muc::new().with_history(History::new().with_maxstanzas(0));
|
||||
assert_eq!(muc, muc2);
|
||||
|
||||
let history = muc.history.unwrap();
|
||||
assert_eq!(history.maxstanzas, Some(0));
|
||||
assert_eq!(history.maxchars, None);
|
||||
assert_eq!(history.seconds, None);
|
||||
assert_eq!(history.since, None);
|
||||
|
||||
let elem: Element = "
|
||||
<x xmlns='http://jabber.org/protocol/muc'>
|
||||
<history since='1970-01-01T00:00:00Z'/>
|
||||
</x>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let muc = Muc::try_from(elem).unwrap();
|
||||
assert_eq!(
|
||||
muc.history.unwrap().since.unwrap(),
|
||||
DateTime::from_str("1970-01-01T00:00:00+00:00").unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
693
xmpp-parsers/src/muc/user.rs
Normal file
693
xmpp-parsers/src/muc/user.rs
Normal file
|
|
@ -0,0 +1,693 @@
|
|||
// Copyright (c) 2017 Maxime “pep” Buquet <pep@bouah.net>
|
||||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
use crate::util::error::Error;
|
||||
use crate::ns;
|
||||
use jid::FullJid;
|
||||
use crate::Element;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
generate_attribute_enum!(
|
||||
/// Lists all of the possible status codes used in MUC presences.
|
||||
Status, "status", MUC_USER, "code", {
|
||||
/// Inform user that any occupant is allowed to see the user's full JID
|
||||
NonAnonymousRoom => 100,
|
||||
|
||||
/// Inform user that his or her affiliation changed while not in the room
|
||||
AffiliationChange => 101,
|
||||
|
||||
/// Inform occupants that room now shows unavailable members
|
||||
ConfigShowsUnavailableMembers => 102,
|
||||
|
||||
/// Inform occupants that room now does not show unavailable members
|
||||
ConfigHidesUnavailableMembers => 103,
|
||||
|
||||
/// Inform occupants that a non-privacy-related room configuration change has occurred
|
||||
ConfigNonPrivacyRelated => 104,
|
||||
|
||||
/// Inform user that presence refers to itself
|
||||
SelfPresence => 110,
|
||||
|
||||
/// Inform occupants that room logging is now enabled
|
||||
ConfigRoomLoggingEnabled => 170,
|
||||
|
||||
/// Inform occupants that room logging is now disabled
|
||||
ConfigRoomLoggingDisabled => 171,
|
||||
|
||||
/// Inform occupants that the room is now non-anonymous
|
||||
ConfigRoomNonAnonymous => 172,
|
||||
|
||||
/// Inform occupants that the room is now semi-anonymous
|
||||
ConfigRoomSemiAnonymous => 173,
|
||||
|
||||
/// Inform user that a new room has been created
|
||||
RoomHasBeenCreated => 201,
|
||||
|
||||
/// Inform user that service has assigned or modified occupant's roomnick
|
||||
AssignedNick => 210,
|
||||
|
||||
/// Inform user that he or she has been banned from the room
|
||||
Banned => 301,
|
||||
|
||||
/// Inform all occupants of new room nickname
|
||||
NewNick => 303,
|
||||
|
||||
/// Inform user that he or she has been kicked from the room
|
||||
Kicked => 307,
|
||||
|
||||
/// Inform user that he or she is being removed from the room
|
||||
/// because of an affiliation change
|
||||
RemovalFromRoom => 321,
|
||||
|
||||
/// Inform user that he or she is being removed from the room
|
||||
/// because the room has been changed to members-only and the
|
||||
/// user is not a member
|
||||
ConfigMembersOnly => 322,
|
||||
|
||||
/// Inform user that he or she is being removed from the room
|
||||
/// because the MUC service is being shut down
|
||||
ServiceShutdown => 332,
|
||||
});
|
||||
|
||||
/// Optional <actor/> element used in <item/> elements inside presence stanzas of type
|
||||
/// "unavailable" that are sent to users who are kick or banned, as well as within IQs for tracking
|
||||
/// purposes. -- CHANGELOG 0.17 (2002-10-23)
|
||||
///
|
||||
/// Possesses a 'jid' and a 'nick' attribute, so that an action can be attributed either to a real
|
||||
/// JID or to a roomnick. -- CHANGELOG 1.25 (2012-02-08)
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Actor {
|
||||
/// The full JID associated with this user.
|
||||
Jid(FullJid),
|
||||
|
||||
/// The nickname of this user.
|
||||
Nick(String),
|
||||
}
|
||||
|
||||
impl TryFrom<Element> for Actor {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(elem: Element) -> Result<Actor, Error> {
|
||||
check_self!(elem, "actor", MUC_USER);
|
||||
check_no_unknown_attributes!(elem, "actor", ["jid", "nick"]);
|
||||
check_no_children!(elem, "actor");
|
||||
let jid: Option<FullJid> = get_attr!(elem, "jid", Option);
|
||||
let nick = get_attr!(elem, "nick", Option);
|
||||
|
||||
match (jid, nick) {
|
||||
(Some(_), Some(_)) | (None, None) => {
|
||||
Err(Error::ParseError(
|
||||
"Either 'jid' or 'nick' attribute is required.",
|
||||
))
|
||||
}
|
||||
(Some(jid), _) => Ok(Actor::Jid(jid)),
|
||||
(_, Some(nick)) => Ok(Actor::Nick(nick)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Actor> for Element {
|
||||
fn from(actor: Actor) -> Element {
|
||||
let elem = Element::builder("actor").ns(ns::MUC_USER);
|
||||
|
||||
(match actor {
|
||||
Actor::Jid(jid) => elem.attr("jid", jid),
|
||||
Actor::Nick(nick) => elem.attr("nick", nick),
|
||||
})
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
generate_element!(
|
||||
/// Used to continue a one-to-one discussion in a room, with more than one
|
||||
/// participant.
|
||||
Continue, "continue", MUC_USER,
|
||||
attributes: [
|
||||
/// The thread to continue in this room.
|
||||
thread: Option<String> = "thread",
|
||||
]
|
||||
);
|
||||
|
||||
generate_elem_id!(
|
||||
/// A reason for inviting, declining, etc. a request.
|
||||
Reason,
|
||||
"reason",
|
||||
MUC_USER
|
||||
);
|
||||
|
||||
generate_attribute!(
|
||||
/// The affiliation of an entity with a room, which 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()))),
|
||||
}
|
||||
}
|
||||
}
|
||||
79
xmpp-parsers/src/nick.rs
Normal file
79
xmpp-parsers/src/nick.rs
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
// Copyright (c) 2018 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
generate_elem_id!(
|
||||
/// Represents a global, memorable, friendly or informal name chosen by a user.
|
||||
Nick,
|
||||
"nick",
|
||||
NICK
|
||||
);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[cfg(not(feature = "disable-validation"))]
|
||||
use crate::util::error::Error;
|
||||
use crate::Element;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(Nick, 12);
|
||||
}
|
||||
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(Nick, 24);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple() {
|
||||
let elem: Element = "<nick xmlns='http://jabber.org/protocol/nick'>Link Mauve</nick>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let nick = Nick::try_from(elem).unwrap();
|
||||
assert_eq!(&nick.0, "Link Mauve");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialise() {
|
||||
let elem1 = Element::from(Nick(String::from("Link Mauve")));
|
||||
let elem2: Element = "<nick xmlns='http://jabber.org/protocol/nick'>Link Mauve</nick>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
assert_eq!(elem1, elem2);
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "disable-validation"))]
|
||||
#[test]
|
||||
fn test_invalid() {
|
||||
let elem: Element = "<nick xmlns='http://jabber.org/protocol/nick'><coucou/></nick>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let error = Nick::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "Unknown child in nick element.");
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "disable-validation"))]
|
||||
#[test]
|
||||
fn test_invalid_attribute() {
|
||||
let elem: Element = "<nick xmlns='http://jabber.org/protocol/nick' coucou=''/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let error = Nick::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "Unknown attribute in nick element.");
|
||||
}
|
||||
}
|
||||
235
xmpp-parsers/src/ns.rs
Normal file
235
xmpp-parsers/src/ns.rs
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
// Copyright (c) 2017-2018 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||
// Copyright (c) 2017 Maxime “pep” Buquet <pep@bouah.net>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core
|
||||
pub const JABBER_CLIENT: &str = "jabber:client";
|
||||
/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core
|
||||
pub const XMPP_STANZAS: &str = "urn:ietf:params:xml:ns:xmpp-stanzas";
|
||||
/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core
|
||||
pub const STREAM: &str = "http://etherx.jabber.org/streams";
|
||||
/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core
|
||||
pub const SASL: &str = "urn:ietf:params:xml:ns:xmpp-sasl";
|
||||
/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core
|
||||
pub const BIND: &str = "urn:ietf:params:xml:ns:xmpp-bind";
|
||||
|
||||
/// RFC 6121: Extensible Messaging and Presence Protocol (XMPP): Instant Messaging and Presence
|
||||
pub const ROSTER: &str = "jabber:iq:roster";
|
||||
|
||||
/// RFC 7395: An Extensible Messaging and Presence Protocol (XMPP) Subprotocol for WebSocket
|
||||
pub const WEBSOCKET: &str = "urn:ietf:params:xml:ns:xmpp-framing";
|
||||
|
||||
/// XEP-0004: Data Forms
|
||||
pub const DATA_FORMS: &str = "jabber:x:data";
|
||||
|
||||
/// XEP-0030: Service Discovery
|
||||
pub const DISCO_INFO: &str = "http://jabber.org/protocol/disco#info";
|
||||
/// XEP-0030: Service Discovery
|
||||
pub const DISCO_ITEMS: &str = "http://jabber.org/protocol/disco#items";
|
||||
|
||||
/// XEP-0045: Multi-User Chat
|
||||
pub const MUC: &str = "http://jabber.org/protocol/muc";
|
||||
/// XEP-0045: Multi-User Chat
|
||||
pub const MUC_USER: &str = "http://jabber.org/protocol/muc#user";
|
||||
|
||||
/// XEP-0047: In-Band Bytestreams
|
||||
pub const IBB: &str = "http://jabber.org/protocol/ibb";
|
||||
|
||||
/// XEP-0048: Bookmarks
|
||||
pub const BOOKMARKS: &str = "storage:bookmarks";
|
||||
|
||||
/// XEP-0059: Result Set Management
|
||||
pub const RSM: &str = "http://jabber.org/protocol/rsm";
|
||||
|
||||
/// XEP-0060: Publish-Subscribe
|
||||
pub const PUBSUB: &str = "http://jabber.org/protocol/pubsub";
|
||||
/// XEP-0060: Publish-Subscribe
|
||||
pub const PUBSUB_ERRORS: &str = "http://jabber.org/protocol/pubsub#errors";
|
||||
/// XEP-0060: Publish-Subscribe
|
||||
pub const PUBSUB_EVENT: &str = "http://jabber.org/protocol/pubsub#event";
|
||||
/// XEP-0060: Publish-Subscribe
|
||||
pub const PUBSUB_OWNER: &str = "http://jabber.org/protocol/pubsub#owner";
|
||||
|
||||
/// XEP-0071: XHTML-IM
|
||||
pub const XHTML_IM: &str = "http://jabber.org/protocol/xhtml-im";
|
||||
/// XEP-0071: XHTML-IM
|
||||
pub const XHTML: &str = "http://www.w3.org/1999/xhtml";
|
||||
|
||||
/// XEP-0077: In-Band Registration
|
||||
pub const REGISTER: &str = "jabber:iq:register";
|
||||
|
||||
/// XEP-0084: User Avatar
|
||||
pub const AVATAR_DATA: &str = "urn:xmpp:avatar:data";
|
||||
/// XEP-0084: User Avatar
|
||||
pub const AVATAR_METADATA: &str = "urn:xmpp:avatar:metadata";
|
||||
|
||||
/// XEP-0085: Chat State Notifications
|
||||
pub const CHATSTATES: &str = "http://jabber.org/protocol/chatstates";
|
||||
|
||||
/// XEP-0092: Software Version
|
||||
pub const VERSION: &str = "jabber:iq:version";
|
||||
|
||||
/// XEP-0107: User Mood
|
||||
pub const MOOD: &str = "http://jabber.org/protocol/mood";
|
||||
|
||||
/// XEP-0114: Jabber Component Protocol
|
||||
pub const COMPONENT_ACCEPT: &str = "jabber:component:accept";
|
||||
|
||||
/// XEP-0114: Jabber Component Protocol
|
||||
pub const COMPONENT: &str = "jabber:component:accept";
|
||||
|
||||
/// XEP-0115: Entity Capabilities
|
||||
pub const CAPS: &str = "http://jabber.org/protocol/caps";
|
||||
|
||||
/// XEP-0118: User Tune
|
||||
pub const TUNE: &str = "http://jabber.org/protocol/tune";
|
||||
|
||||
/// XEP-0157: Contact Addresses for XMPP Services
|
||||
pub const SERVER_INFO: &str = "http://jabber.org/network/serverinfo";
|
||||
|
||||
/// XEP-0166: Jingle
|
||||
pub const JINGLE: &str = "urn:xmpp:jingle:1";
|
||||
|
||||
/// XEP-0167: Jingle RTP Sessions
|
||||
pub const JINGLE_RTP: &str = "urn:xmpp:jingle:apps:rtp:1";
|
||||
/// XEP-0167: Jingle RTP Sessions
|
||||
pub const JINGLE_RTP_AUDIO: &str = "urn:xmpp:jingle:apps:rtp:audio";
|
||||
/// XEP-0167: Jingle RTP Sessions
|
||||
pub const JINGLE_RTP_VIDEO: &str = "urn:xmpp:jingle:apps:rtp:video";
|
||||
|
||||
/// XEP-0172: User Nickname
|
||||
pub const NICK: &str = "http://jabber.org/protocol/nick";
|
||||
|
||||
/// XEP-0176: Jingle ICE-UDP Transport Method
|
||||
pub const JINGLE_ICE_UDP: &str = "urn:xmpp:jingle:transports:ice-udp:1";
|
||||
|
||||
/// XEP-0184: Message Delivery Receipts
|
||||
pub const RECEIPTS: &str = "urn:xmpp:receipts";
|
||||
|
||||
/// XEP-0191: Blocking Command
|
||||
pub const BLOCKING: &str = "urn:xmpp:blocking";
|
||||
/// XEP-0191: Blocking Command
|
||||
pub const BLOCKING_ERRORS: &str = "urn:xmpp:blocking:errors";
|
||||
|
||||
/// XEP-0198: Stream Management
|
||||
pub const SM: &str = "urn:xmpp:sm:3";
|
||||
|
||||
/// XEP-0199: XMPP Ping
|
||||
pub const PING: &str = "urn:xmpp:ping";
|
||||
|
||||
/// XEP-0202: Entity Time
|
||||
pub const TIME: &str = "urn:xmpp:time";
|
||||
|
||||
/// XEP-0203: Delayed Delivery
|
||||
pub const DELAY: &str = "urn:xmpp:delay";
|
||||
|
||||
/// XEP-0221: Data Forms Media Element
|
||||
pub const MEDIA_ELEMENT: &str = "urn:xmpp:media-element";
|
||||
|
||||
/// XEP-0224: Attention
|
||||
pub const ATTENTION: &str = "urn:xmpp:attention:0";
|
||||
|
||||
/// XEP-0231: Bits of Binary
|
||||
pub const BOB: &str = "urn:xmpp:bob";
|
||||
|
||||
/// XEP-0234: Jingle File Transfer
|
||||
pub const JINGLE_FT: &str = "urn:xmpp:jingle:apps:file-transfer:5";
|
||||
/// XEP-0234: Jingle File Transfer
|
||||
pub const JINGLE_FT_ERROR: &str = "urn:xmpp:jingle:apps:file-transfer:errors:0";
|
||||
|
||||
/// XEP-0257: Client Certificate Management for SASL EXTERNAL
|
||||
pub const SASL_CERT: &str = "urn:xmpp:saslcert:1";
|
||||
|
||||
/// XEP-0260: Jingle SOCKS5 Bytestreams Transport Method
|
||||
pub const JINGLE_S5B: &str = "urn:xmpp:jingle:transports:s5b:1";
|
||||
|
||||
/// XEP-0261: Jingle In-Band Bytestreams Transport Method
|
||||
pub const JINGLE_IBB: &str = "urn:xmpp:jingle:transports:ibb:1";
|
||||
|
||||
/// XEP-0277: Microblogging over XMPP
|
||||
pub const MICROBLOG: &str = "urn:xmpp:microblog:0";
|
||||
|
||||
/// XEP-0280: Message Carbons
|
||||
pub const CARBONS: &str = "urn:xmpp:carbons:2";
|
||||
|
||||
/// XEP-0293: Jingle RTP Feedback Negotiation
|
||||
pub const JINGLE_RTCP_FB: &str = "urn:xmpp:jingle:apps:rtp:rtcp-fb:0";
|
||||
|
||||
/// XEP-0297: Stanza Forwarding
|
||||
pub const FORWARD: &str = "urn:xmpp:forward:0";
|
||||
|
||||
/// XEP-0300: Use of Cryptographic Hash Functions in XMPP
|
||||
pub const HASHES: &str = "urn:xmpp:hashes:2";
|
||||
/// XEP-0300: Use of Cryptographic Hash Functions in XMPP
|
||||
pub const HASH_ALGO_SHA_256: &str = "urn:xmpp:hash-function-text-names:sha-256";
|
||||
/// XEP-0300: Use of Cryptographic Hash Functions in XMPP
|
||||
pub const HASH_ALGO_SHA_512: &str = "urn:xmpp:hash-function-text-names:sha-512";
|
||||
/// XEP-0300: Use of Cryptographic Hash Functions in XMPP
|
||||
pub const HASH_ALGO_SHA3_256: &str = "urn:xmpp:hash-function-text-names:sha3-256";
|
||||
/// XEP-0300: Use of Cryptographic Hash Functions in XMPP
|
||||
pub const HASH_ALGO_SHA3_512: &str = "urn:xmpp:hash-function-text-names:sha3-512";
|
||||
/// XEP-0300: Use of Cryptographic Hash Functions in XMPP
|
||||
pub const HASH_ALGO_BLAKE2B_256: &str = "urn:xmpp:hash-function-text-names:id-blake2b256";
|
||||
/// XEP-0300: Use of Cryptographic Hash Functions in XMPP
|
||||
pub const HASH_ALGO_BLAKE2B_512: &str = "urn:xmpp:hash-function-text-names:id-blake2b512";
|
||||
|
||||
/// XEP-0308: Last Message Correction
|
||||
pub const MESSAGE_CORRECT: &str = "urn:xmpp:message-correct:0";
|
||||
|
||||
/// XEP-0313: Message Archive Management
|
||||
pub const MAM: &str = "urn:xmpp:mam:2";
|
||||
|
||||
/// XEP-0319: Last User Interaction in Presence
|
||||
pub const IDLE: &str = "urn:xmpp:idle:1";
|
||||
|
||||
/// XEP-0320: Use of DTLS-SRTP in Jingle Sessions
|
||||
pub const JINGLE_DTLS: &str = "urn:xmpp:jingle:apps:dtls:0";
|
||||
|
||||
/// XEP-0328: JID Prep
|
||||
pub const JID_PREP: &str = "urn:xmpp:jidprep:0";
|
||||
|
||||
/// XEP-0339: Source-Specific Media Attributes in Jingle
|
||||
pub const JINGLE_SSMA: &str = "urn:xmpp:jingle:apps:rtp:ssma:0";
|
||||
|
||||
/// XEP-0352: Client State Indication
|
||||
pub const CSI: &str = "urn:xmpp:csi:0";
|
||||
|
||||
/// XEP-0353: Jingle Message Initiation
|
||||
pub const JINGLE_MESSAGE: &str = "urn:xmpp:jingle-message:0";
|
||||
|
||||
/// XEP-0359: Unique and Stable Stanza IDs
|
||||
pub const SID: &str = "urn:xmpp:sid:0";
|
||||
|
||||
/// XEP-0373: OpenPGP for XMPP
|
||||
pub const OX: &str = "urn:xmpp:openpgp:0";
|
||||
/// XEP-0373: OpenPGP for XMPP
|
||||
pub const OX_PUBKEYS: &str = "urn:xmpp:openpgp:0:public-keys";
|
||||
|
||||
/// XEP-0380: Explicit Message Encryption
|
||||
pub const EME: &str = "urn:xmpp:eme:0";
|
||||
|
||||
/// XEP-0390: Entity Capabilities 2.0
|
||||
pub const ECAPS2: &str = "urn:xmpp:caps";
|
||||
/// XEP-0390: Entity Capabilities 2.0
|
||||
pub const ECAPS2_OPTIMIZE: &str = "urn:xmpp:caps:optimize";
|
||||
|
||||
/// XEP-0402: Bookmarks 2 (This Time it's Serious)
|
||||
pub const BOOKMARKS2: &str = "urn:xmpp:bookmarks:0";
|
||||
/// XEP-0402: Bookmarks 2 (This Time it's Serious)
|
||||
pub const BOOKMARKS2_COMPAT: &str = "urn:xmpp:bookmarks:0#compat";
|
||||
|
||||
/// XEP-0421: Anonymous unique occupant identifiers for MUCs
|
||||
pub const OID: &str = "urn:xmpp:occupant-id:0";
|
||||
|
||||
/// Alias for the main namespace of the stream, that is "jabber:client" when
|
||||
/// the component feature 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;
|
||||
89
xmpp-parsers/src/occupant_id.rs
Normal file
89
xmpp-parsers/src/occupant_id.rs
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
use crate::message::MessagePayload;
|
||||
use crate::presence::PresencePayload;
|
||||
|
||||
generate_element!(
|
||||
/// Unique identifier given to a MUC participant.
|
||||
///
|
||||
/// It allows clients to identify a MUC participant across reconnects and
|
||||
/// renames. It thus prevents impersonification of anonymous users.
|
||||
OccupantId, "occupant-id", OID,
|
||||
|
||||
attributes: [
|
||||
/// The id associated to the sending user by the MUC service.
|
||||
id: Required<String> = "id",
|
||||
]
|
||||
);
|
||||
|
||||
impl MessagePayload for OccupantId {}
|
||||
impl PresencePayload for OccupantId {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::util::error::Error;
|
||||
use crate::Element;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(OccupantId, 12);
|
||||
}
|
||||
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(OccupantId, 24);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple() {
|
||||
let elem: Element = "<occupant-id xmlns='urn:xmpp:occupant-id:0' id='coucou'/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let origin_id = OccupantId::try_from(elem).unwrap();
|
||||
assert_eq!(origin_id.id, "coucou");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_child() {
|
||||
let elem: Element = "<occupant-id xmlns='urn:xmpp:occupant-id:0'><coucou/></occupant-id>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let error = OccupantId::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "Unknown child in occupant-id element.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_id() {
|
||||
let elem: Element = "<occupant-id xmlns='urn:xmpp:occupant-id:0'/>".parse().unwrap();
|
||||
let error = OccupantId::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "Required attribute 'id' missing.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialise() {
|
||||
let elem: Element = "<occupant-id xmlns='urn:xmpp:occupant-id:0' id='coucou'/>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let occupant_id = OccupantId {
|
||||
id: String::from("coucou"),
|
||||
};
|
||||
let elem2 = occupant_id.into();
|
||||
assert_eq!(elem, elem2);
|
||||
}
|
||||
}
|
||||
104
xmpp-parsers/src/openpgp.rs
Normal file
104
xmpp-parsers/src/openpgp.rs
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
// Copyright (c) 2019 Maxime “pep” Buquet <pep@bouah.net>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
use crate::date::DateTime;
|
||||
use crate::util::helpers::Base64;
|
||||
use crate::pubsub::PubSubPayload;
|
||||
|
||||
// TODO: Merge this container with the PubKey struct
|
||||
generate_element!(
|
||||
/// Data contained in the PubKey element
|
||||
PubKeyData, "data", OX,
|
||||
text: (
|
||||
/// Base64 data
|
||||
data: Base64<Vec<u8>>
|
||||
)
|
||||
);
|
||||
|
||||
generate_element!(
|
||||
/// Pubkey element to be used in PubSub publish payloads.
|
||||
PubKey, "pubkey", OX,
|
||||
attributes: [
|
||||
/// Last updated date
|
||||
date: Option<DateTime> = "date"
|
||||
],
|
||||
children: [
|
||||
/// Public key as base64 data
|
||||
data: Required<PubKeyData> = ("data", OX) => PubKeyData
|
||||
]
|
||||
);
|
||||
|
||||
impl PubSubPayload for PubKey {}
|
||||
|
||||
generate_element!(
|
||||
/// Public key metadata
|
||||
PubKeyMeta, "pubkey-metadata", OX,
|
||||
attributes: [
|
||||
/// OpenPGP v4 fingerprint
|
||||
v4fingerprint: Required<String> = "v4-fingerprint",
|
||||
/// Time the key was published or updated
|
||||
date: Required<DateTime> = "date",
|
||||
]
|
||||
);
|
||||
|
||||
generate_element!(
|
||||
/// List of public key metadata
|
||||
PubKeysMeta, "public-key-list", OX,
|
||||
children: [
|
||||
/// Public keys
|
||||
pubkeys: Vec<PubKeyMeta> = ("pubkey-metadata", OX) => PubKeyMeta
|
||||
]
|
||||
);
|
||||
|
||||
impl PubSubPayload for PubKeysMeta {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::ns;
|
||||
use std::str::FromStr;
|
||||
use crate::pubsub::{NodeName, Item, pubsub::{Item as PubSubItem, Publish}};
|
||||
|
||||
#[test]
|
||||
fn pubsub_publish_pubkey_data() {
|
||||
let pubkey = PubKey {
|
||||
date: None,
|
||||
data: PubKeyData {
|
||||
data: (&"Foo").as_bytes().to_vec(),
|
||||
}
|
||||
};
|
||||
println!("Foo1: {:?}", pubkey);
|
||||
|
||||
let pubsub = Publish {
|
||||
node: NodeName(format!("{}:{}", ns::OX_PUBKEYS, "some-fingerprint")),
|
||||
items: vec![
|
||||
PubSubItem(Item::new(None, None, Some(pubkey))),
|
||||
],
|
||||
};
|
||||
println!("Foo2: {:?}", pubsub);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pubsub_publish_pubkey_meta() {
|
||||
let pubkeymeta = PubKeysMeta {
|
||||
pubkeys: vec![
|
||||
PubKeyMeta {
|
||||
v4fingerprint: "some-fingerprint".to_owned(),
|
||||
date: DateTime::from_str("2019-03-30T18:30:25Z").unwrap(),
|
||||
},
|
||||
],
|
||||
};
|
||||
println!("Foo1: {:?}", pubkeymeta);
|
||||
|
||||
let pubsub = Publish {
|
||||
node: NodeName("foo".to_owned()),
|
||||
items: vec![
|
||||
PubSubItem(Item::new(None, None, Some(pubkeymeta))),
|
||||
],
|
||||
};
|
||||
println!("Foo2: {:?}", pubsub);
|
||||
}
|
||||
}
|
||||
71
xmpp-parsers/src/ping.rs
Normal file
71
xmpp-parsers/src/ping.rs
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||
// Copyright (c) 2017 Maxime “pep” Buquet <pep@bouah.net>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
use crate::iq::IqGetPayload;
|
||||
|
||||
generate_empty_element!(
|
||||
/// Represents a ping to the recipient, which must be answered with an
|
||||
/// empty `<iq/>` or with an error.
|
||||
Ping,
|
||||
"ping",
|
||||
PING
|
||||
);
|
||||
|
||||
impl IqGetPayload for Ping {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[cfg(not(feature = "disable-validation"))]
|
||||
use crate::util::error::Error;
|
||||
use crate::Element;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
#[test]
|
||||
fn test_size() {
|
||||
assert_size!(Ping, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple() {
|
||||
let elem: Element = "<ping xmlns='urn:xmpp:ping'/>".parse().unwrap();
|
||||
Ping::try_from(elem).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialise() {
|
||||
let elem1 = Element::from(Ping);
|
||||
let elem2: Element = "<ping xmlns='urn:xmpp:ping'/>".parse().unwrap();
|
||||
assert_eq!(elem1, elem2);
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "disable-validation"))]
|
||||
#[test]
|
||||
fn test_invalid() {
|
||||
let elem: Element = "<ping xmlns='urn:xmpp:ping'><coucou/></ping>"
|
||||
.parse()
|
||||
.unwrap();
|
||||
let error = Ping::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "Unknown child in ping element.");
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "disable-validation"))]
|
||||
#[test]
|
||||
fn test_invalid_attribute() {
|
||||
let elem: Element = "<ping xmlns='urn:xmpp:ping' coucou=''/>".parse().unwrap();
|
||||
let error = Ping::try_from(elem).unwrap_err();
|
||||
let message = match error {
|
||||
Error::ParseError(string) => string,
|
||||
_ => panic!(),
|
||||
};
|
||||
assert_eq!(message, "Unknown attribute in ping element.");
|
||||
}
|
||||
}
|
||||
661
xmpp-parsers/src/presence.rs
Normal file
661
xmpp-parsers/src/presence.rs
Normal file
|
|
@ -0,0 +1,661 @@
|
|||
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||
// Copyright (c) 2017 Maxime “pep” Buquet <pep@bouah.net>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
use crate::util::error::Error;
|
||||
use crate::ns;
|
||||
use jid::Jid;
|
||||
use minidom::{Element, IntoAttributeValue, Node};
|
||||
use std::collections::BTreeMap;
|
||||
use std::str::FromStr;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
/// Should be implemented on every known payload of a `<presence/>`.
|
||||
pub trait PresencePayload: TryFrom<Element> + Into<Element> {}
|
||||
|
||||
/// Specifies the availability of an entity or resource.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Show {
|
||||
/// The entity or resource is temporarily away.
|
||||
Away,
|
||||
|
||||
/// The entity or resource is actively interested in chatting.
|
||||
Chat,
|
||||
|
||||
/// The entity or resource is busy (dnd = "Do Not Disturb").
|
||||
Dnd,
|
||||
|
||||
/// The entity or resource is away for an extended period (xa = "eXtended
|
||||
/// Away").
|
||||
Xa,
|
||||
}
|
||||
|
||||
impl FromStr for Show {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Show, Error> {
|
||||
Ok(match s {
|
||||
"away" => Show::Away,
|
||||
"chat" => Show::Chat,
|
||||
"dnd" => Show::Dnd,
|
||||
"xa" => Show::Xa,
|
||||
|
||||
_ => return Err(Error::ParseError("Invalid value for show.")),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<Node> for Show {
|
||||
fn into(self) -> Node {
|
||||
Element::builder("show")
|
||||
.append(match self {
|
||||
Show::Away => "away",
|
||||
Show::Chat => "chat",
|
||||
Show::Dnd => "dnd",
|
||||
Show::Xa => "xa",
|
||||
})
|
||||
.build()
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
type Lang = String;
|
||||
type Status = String;
|
||||
|
||||
type Priority = i8;
|
||||
|
||||
///
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Type {
|
||||
/// This value is not an acceptable 'type' attribute, it is only used
|
||||
/// internally to signal the absence of 'type'.
|
||||
None,
|
||||
|
||||
/// An error has occurred regarding processing of a previously sent
|
||||
/// presence stanza; if the presence stanza is of type "error", it MUST
|
||||
/// include an <error/> child element (refer to [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"));
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue