vendored xmpp-parsers into the repository for now
This commit is contained in:
parent
55aa89010c
commit
a6089293a6
|
@ -0,0 +1,35 @@
|
||||||
|
[package]
|
||||||
|
name = "xmpp-parsers-gst-meet"
|
||||||
|
version = "0.18.2"
|
||||||
|
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-rs"
|
||||||
|
repository = "https://gitlab.com/xmpp-rs/xmpp-rs"
|
||||||
|
keywords = ["xmpp", "jabber", "xml"]
|
||||||
|
categories = ["parsing", "network-programming"]
|
||||||
|
license = "MPL-2.0"
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
minidom = { package = "minidom-gst-meet", version = "0.13" }
|
||||||
|
jid = { package = "jid-gst-meet", version = "0.9", features = ["minidom"] }
|
||||||
|
base64 = "0.13"
|
||||||
|
digest = "0.9"
|
||||||
|
sha-1 = "0.9"
|
||||||
|
sha2 = "0.9"
|
||||||
|
sha3 = "0.9"
|
||||||
|
blake2 = "0.9"
|
||||||
|
chrono = { version = "0.4.5", default-features = false, features = ["std"] }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
# Build xmpp-parsers to make components instead of clients.
|
||||||
|
component = []
|
||||||
|
# Disable validation of unknown attributes.
|
||||||
|
disable-validation = []
|
||||||
|
serde = ["jid/serde"]
|
||||||
|
|
||||||
|
[package.metadata.docs.rs]
|
||||||
|
rustdoc-args = [ "--sort-modules-by-appearance", "-Zunstable-options" ]
|
|
@ -0,0 +1,365 @@
|
||||||
|
Version 0.18.0:
|
||||||
|
2021-01-13 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
* Bugfixes:
|
||||||
|
- Bump minidom to 0.13, as 0.12.1 got yanked.
|
||||||
|
|
||||||
|
Version 0.18.0:
|
||||||
|
2021-01-13 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
* New parsers/serialisers:
|
||||||
|
- Jingle Raw UDP Transport Method (XEP-0177).
|
||||||
|
- Jingle RTP Header Extensions Negotiation (XEP-0294).
|
||||||
|
- Jingle Grouping Framework (XEP-0338).
|
||||||
|
- Mediated Information eXchange (MIX) (XEP-0369).
|
||||||
|
* Improvements:
|
||||||
|
- Everything is now PartialEq!
|
||||||
|
- Add "serde" feature to enable "jid/serde".
|
||||||
|
- Implement more of XEP-0060.
|
||||||
|
- Bump XEP-0167 to version 1.2.0, adding rtcp-mux.
|
||||||
|
- Bump XEP-0176 to version 1.1, fixing interoperability with other
|
||||||
|
clients.
|
||||||
|
- Bump XEP-0402 to version 1.1.1, bumping its namespace and adding
|
||||||
|
support for extension data.
|
||||||
|
- Bump all dependencies to their latest version.
|
||||||
|
- Some more helper constructors.
|
||||||
|
- Make public some stuff that should have been public from the very
|
||||||
|
beginning.
|
||||||
|
* Bugfixes:
|
||||||
|
- Jingle::set_reason() does what it says now (copy/paste error).
|
||||||
|
- Bookmarks’ names are now optional like they should.
|
||||||
|
|
||||||
|
Version 0.17.0:
|
||||||
|
2020-02-15 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>, Maxime “pep” Buquet <pep@bouah.net>, Paul Fariello <paul@fariello.eu>
|
||||||
|
* Improvements:
|
||||||
|
- Add serialization tests where possible
|
||||||
|
- Use minidom's NSChoice API for Jingle parser
|
||||||
|
- Remove NamespaceAwareCompare. Move to minidom
|
||||||
|
* Breaking changes:
|
||||||
|
- Prevent generate_serializer macro from adding another layer of Node.
|
||||||
|
Fixes some serializers.
|
||||||
|
- ecaps2: Use the Error type instead of ()
|
||||||
|
|
||||||
|
Version 0.16.0:
|
||||||
|
2019-10-15 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
* New parsers/serialisers:
|
||||||
|
- Client Certificate Management for SASL EXTERNAL (XEP-0257)
|
||||||
|
- JID Prep (XEP-0328)
|
||||||
|
- Client State Indication (XEP-0352)
|
||||||
|
- OpenPGP for XMPP (XEP-0373)
|
||||||
|
- Bookmarks 2 (This Time it's Serious) (XEP-0402)
|
||||||
|
- Anonymous unique occupant identifiers for MUCs (XEP-0421)
|
||||||
|
- Source-Specific Media Attributes in Jingle (XEP-0339)
|
||||||
|
- Jingle RTP Feedback Negotiation (XEP-0293)
|
||||||
|
* Breaking changes:
|
||||||
|
- Presence constructors now take Into<Jid> and assume Some.
|
||||||
|
* Improvements:
|
||||||
|
- CI: refactor, add caching
|
||||||
|
- Update jid-rs to 0.8
|
||||||
|
|
||||||
|
Version 0.15.0:
|
||||||
|
2019-09-06 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
* New parsers/serialisers:
|
||||||
|
- XHTML-IM (XEP-0071)
|
||||||
|
- User Tune (XEP-0118)
|
||||||
|
- Bits of Binary (XEP-0231)
|
||||||
|
- Message Carbons (XEP-0280)
|
||||||
|
* Breaking changes:
|
||||||
|
- Stop reexporting TryFrom and TryInto, they are available in
|
||||||
|
std::convert nowadays.
|
||||||
|
- Bind has been split into BindQuery and BindResponse.
|
||||||
|
* Improvements:
|
||||||
|
- New DOAP file for a machine-readable description of the features.
|
||||||
|
- Add various parser and formatter helpers on Hash.
|
||||||
|
|
||||||
|
Version 0.14.0:
|
||||||
|
2019-07-13 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>, Maxime “pep” Buquet <pep@bouah.net>
|
||||||
|
* New parsers/serialisers:
|
||||||
|
- Entity Time (XEP-0202).
|
||||||
|
* Improvements:
|
||||||
|
- Microblog NS (XEP-0227).
|
||||||
|
- Update jid-rs dependency with jid split change (Jid, FullJid,
|
||||||
|
BareJid) and reexport them.
|
||||||
|
- Fix rustdoc options in Cargo.toml for docs.rs
|
||||||
|
* Breaking changes:
|
||||||
|
- Presence's show attribute is now Option<Show> and Show::None is no
|
||||||
|
more.
|
||||||
|
|
||||||
|
Version 0.13.1:
|
||||||
|
2019-04-12 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
* Bugfixes:
|
||||||
|
- Fix invalid serialisation of priority in presence.
|
||||||
|
- Bump image size to u16 from u8, as per XEP-0084 version 1.1.2.
|
||||||
|
* Improvements:
|
||||||
|
- Drop try_from dependency, as std::convert::TryFrom got
|
||||||
|
stabilised.
|
||||||
|
|
||||||
|
Version 0.13.0:
|
||||||
|
2019-03-20 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
* New parsers/serialisers:
|
||||||
|
- User Avatar (XEP-0084).
|
||||||
|
- Contact Addresses for XMPP Services (XEP-0157).
|
||||||
|
- Jingle RTP Sessions (XEP-0167).
|
||||||
|
- Jingle ICE-UDP Transport Method (XEP-0176).
|
||||||
|
- Use of DTLS-SRTP in Jingle Sessions (XEP-0320).
|
||||||
|
* Breaking changes:
|
||||||
|
- Make 'id' required on iq, as per RFC6120 §8.1.3.
|
||||||
|
- Refactor PubSub to have more type-safety.
|
||||||
|
- Treat FORM_TYPE as a special case in data forms, to avoid
|
||||||
|
duplicating it into a field.
|
||||||
|
- Add forgotten i18n to Jingle text element.
|
||||||
|
* Improvements:
|
||||||
|
- Add various helpers for hash representations.
|
||||||
|
- Add helpers constructors for multiple extensions (disco, caps,
|
||||||
|
pubsub, stanza_error).
|
||||||
|
- Use Into<String> in more constructors.
|
||||||
|
- Internal change on attribute declaration in macros.
|
||||||
|
- Reexport missing try_from::TryInto.
|
||||||
|
|
||||||
|
Version 0.12.2:
|
||||||
|
2019-01-16 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
* Improvements:
|
||||||
|
- Reexport missing util::error::Error and try_from::TryFrom.
|
||||||
|
|
||||||
|
Version 0.12.1:
|
||||||
|
2019-01-16 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
* Improvements:
|
||||||
|
- Reexport missing JidParseError from the jid crate.
|
||||||
|
|
||||||
|
Version 0.12.0:
|
||||||
|
2019-01-16 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
* Breaking changes:
|
||||||
|
- Update dependencies.
|
||||||
|
- Switch to git, upstream is now available at
|
||||||
|
https://gitlab.com/xmpp-rs/xmpp-parsers
|
||||||
|
- Switch to Edition 2018, this removes support for rustc
|
||||||
|
versions older than 1.31.
|
||||||
|
- Implement support for XEP-0030 2.5rc3, relaxing the ordering
|
||||||
|
of children in disco#info.
|
||||||
|
* Improvements:
|
||||||
|
- Test for struct size, to keep them known and avoid bloat.
|
||||||
|
- Add various constructors to make the API easier to use.
|
||||||
|
- Reexport Jid from the jid crate, to avoid any weird issue on
|
||||||
|
using different incompatible versions of the same crate.
|
||||||
|
- Add forgotten 'ask' attribute on roster item (thanks O01eg!).
|
||||||
|
- Use cargo-fmt on the codebase, to lower the barrier of entry.
|
||||||
|
- Add a disable-validation feature, disabling many checks
|
||||||
|
xmpp-parsers is doing. This should be used for software
|
||||||
|
which want to let invalid XMPP pass through instead of being
|
||||||
|
rejected as invalid (thanks Astro-!).
|
||||||
|
|
||||||
|
Version 0.11.1:
|
||||||
|
2018-09-20 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
* Improvements:
|
||||||
|
- Document all of the modules.
|
||||||
|
|
||||||
|
Version 0.11.0:
|
||||||
|
2018-08-03 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
* Breaking changes:
|
||||||
|
- Split Software Version (XEP-0092) into a query and response
|
||||||
|
elements.
|
||||||
|
- Split RSM (XEP-0059) into a query and response elements.
|
||||||
|
- Fix type safety and spec issues in RSM and MAM (XEP-0313).
|
||||||
|
- Remove item@node and EmptyItems from PubSub events
|
||||||
|
(XEP-0060).
|
||||||
|
* Improvements:
|
||||||
|
- Document many additional modules.
|
||||||
|
- Add the <failure/> SASL nonza, as well as the SCRAM-SHA-256
|
||||||
|
and the two -PLUS mechanisms.
|
||||||
|
|
||||||
|
Version 0.10.0:
|
||||||
|
2018-07-31 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
* New parsers/serialisers:
|
||||||
|
- Added <stream:stream>, SASL and bind (RFC6120) parsers.
|
||||||
|
- Added a WebSocket <open/> (RFC7395) implementation.
|
||||||
|
- Added a Jabber Component <handshake/> (XEP-0114).
|
||||||
|
- Added support for User Nickname (XEP-0172).
|
||||||
|
- Added support for Stream Management (XEP-0198).
|
||||||
|
- Added support for Bookmarks (XEP-0048).
|
||||||
|
- Publish-Subscribe (XEP-0060) now supports requests in
|
||||||
|
addition to events.
|
||||||
|
* Breaking changes:
|
||||||
|
- Switch from std::error to failure to report better errors.
|
||||||
|
- Bump to minidom 0.9.1, and reexport minidom::Element.
|
||||||
|
* Improvements:
|
||||||
|
- Add getters for the best body and subject in message, to make
|
||||||
|
it easier to determine which one the user wants based on
|
||||||
|
their language preferences.
|
||||||
|
- Add constructors and setters for most Jingle elements, to
|
||||||
|
ease their creation.
|
||||||
|
- Add constructors for hash, MUC item, iq and more.
|
||||||
|
- Use more macros to simplify and factorise the code.
|
||||||
|
- Use traits to define iq payloads.
|
||||||
|
- Document more modules.
|
||||||
|
|
||||||
|
Version 0.9.0:
|
||||||
|
2017-10-31 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
* New parsers/serialisers:
|
||||||
|
- Blocking Command (XEP-0191) has been added.
|
||||||
|
- Date and Time Profiles (XEP-0082) has been added, replacing
|
||||||
|
ad-hoc use of chrono in various places.
|
||||||
|
- User Mood (XEP-0107) has been added.
|
||||||
|
* Breaking changes:
|
||||||
|
- Fix subscription="none" not being the default.
|
||||||
|
- Add more type safety to pubsub#event.
|
||||||
|
- Reuse Jingle’s ContentId type in JingleFT.
|
||||||
|
- Import the disposition attribute values in Jingle.
|
||||||
|
* Improvements:
|
||||||
|
- Refactor a good part of the code using macros.
|
||||||
|
- Simplify the parsing code wherever it makes sense.
|
||||||
|
- Check for children ordering in disco#info result.
|
||||||
|
- Finish implementation of <received/>, <checksum/> and
|
||||||
|
<range/> in JingleFT.
|
||||||
|
- Correctly serialise <ping/>, and test it.
|
||||||
|
|
||||||
|
Version 0.8.0:
|
||||||
|
2017-08-27 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
* New parsers/serialisers:
|
||||||
|
- iq:version (XEP-0092) has been added.
|
||||||
|
- Finally implement extension serialisation in disco.
|
||||||
|
* Breaking changes:
|
||||||
|
- Wrap even more elements into their own type, in jingle,
|
||||||
|
jingle_ft, roster, message.
|
||||||
|
- Split loose enums into multiple structs where it makes sense,
|
||||||
|
such as for IBB, StanzaId, Receipts.
|
||||||
|
- Split disco query and answer elements into their own struct,
|
||||||
|
to enforce more guarantees on both.
|
||||||
|
* Improvements:
|
||||||
|
- Use Vec::into_iter() more to avoid references and clones.
|
||||||
|
- Make data_forms propagate a media_element error.
|
||||||
|
- Document more of disco, roster, chatstates.
|
||||||
|
- Use the minidom feature of jid, for IntoAttributeValue.
|
||||||
|
- Add a component feature, changing the default namespace to
|
||||||
|
jabber:component:accept.
|
||||||
|
- Add support for indicating ranged transfers in jingle_ft.
|
||||||
|
|
||||||
|
Version 0.7.1:
|
||||||
|
2017-07-24 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
* Hotfixes:
|
||||||
|
- Stub out blake2 support, since the blake2 crate broke its API
|
||||||
|
between their 0.6.0 and 0.6.1 releases…
|
||||||
|
|
||||||
|
Version 0.7.0:
|
||||||
|
2017-07-23 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
* New parsers/serialisers:
|
||||||
|
- Jingle Message Initialisation (XEP-0353) was added.
|
||||||
|
- The disco#items query (XEP-0030) is now supported, in
|
||||||
|
addition to the existing disco#info one.
|
||||||
|
* Breaking changes:
|
||||||
|
- Replaced many type aliases with proper wrapping structs.
|
||||||
|
- Split Disco into a query and a result part, since they have
|
||||||
|
very different constraints.
|
||||||
|
- Split IqPayload in three to avoid parsing queries as results
|
||||||
|
for example.
|
||||||
|
* Improvements:
|
||||||
|
- Use TryFrom from the try_from crate, thus removing the
|
||||||
|
dependency on nightly!
|
||||||
|
- Always implement From instead of Into, the latter is
|
||||||
|
generated anyway.
|
||||||
|
- Add helpers to construct your Presence stanza.
|
||||||
|
|
||||||
|
Version 0.6.0:
|
||||||
|
2017-06-27 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
* New parsers/serialisers:
|
||||||
|
- In-Band Registration (XEP-0077) was added.
|
||||||
|
- Multi-User Chat (XEP-0045) got expanded a lot, thanks pep.!
|
||||||
|
* Breaking changes:
|
||||||
|
- Added wrappers for Strings used as identifiers, to add type
|
||||||
|
safety.
|
||||||
|
- Use chrono’s DateTime for JingleFT’s date element.
|
||||||
|
- Use Jid for JingleS5B’s jid attribute.
|
||||||
|
* Improvements:
|
||||||
|
- Use more macros for common tasks.
|
||||||
|
- Add a constructor for Message and Presence.
|
||||||
|
- Implement std::fmt::Display and std::error::Error on our
|
||||||
|
error type.
|
||||||
|
- Fix DataForms serialisation.
|
||||||
|
- Fix roster group serialisation.
|
||||||
|
- Update libraries, notably chrono whose version 0.3.1 got
|
||||||
|
yanked.
|
||||||
|
|
||||||
|
Version 0.5.0:
|
||||||
|
2017-06-11 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
* New parsers/serialisers:
|
||||||
|
- Implementation of the roster management protocol defined in
|
||||||
|
RFC 6121 §2.
|
||||||
|
- Implementation of PubSub events (except collections).
|
||||||
|
- Early implementation of MUC.
|
||||||
|
* Breaking changes:
|
||||||
|
- Rename presence enums to make them easier to use.
|
||||||
|
* Improvements:
|
||||||
|
- Make hashes comparable and hashable.
|
||||||
|
- Make data forms embeddable easily into minidom
|
||||||
|
Element::builder.
|
||||||
|
|
||||||
|
Version 0.4.0:
|
||||||
|
2017-05-28 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
* Incompatible changes:
|
||||||
|
- Receipts now make the id optional, as per the specification.
|
||||||
|
- Hashes now expose their raw binary value, instead of staying
|
||||||
|
base64-encoded.
|
||||||
|
- Parse dates (XEP-0082) in delayed delivery (XEP-0203) and
|
||||||
|
last user interaction (XEP-0319), using the chrono crate.
|
||||||
|
* Improvements:
|
||||||
|
- Removal of most of the remaining clones, the only ones left
|
||||||
|
are due to minidom not exposing a draining iterator over the
|
||||||
|
children.
|
||||||
|
- Finish to parse all of the attributes using get_attr!().
|
||||||
|
- More attribute checks.
|
||||||
|
- Split more parsers into one parser per element.
|
||||||
|
- Rely on minidom 0.4.3 to serialise more standard types
|
||||||
|
automatically.
|
||||||
|
- Implement forgotten serialisation for data forms (XEP-0004).
|
||||||
|
- Implement legacy capabilities (XEP-0115) for compatibility
|
||||||
|
with older software.
|
||||||
|
|
||||||
|
Version 0.3.0:
|
||||||
|
2017-05-23 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
* Big changes:
|
||||||
|
- All parsers and serialisers now consume their argument, this
|
||||||
|
makes the API way more efficient, but you will have to clone
|
||||||
|
before passing your structs in it if you want to keep them.
|
||||||
|
- Payloads of stanzas are not parsed automatically anymore, to
|
||||||
|
let applications which want to forward them as-is do so more
|
||||||
|
easily. Parsing now always succeeds on unknown payloads, it
|
||||||
|
just puts them into an Unknown value containing the existing
|
||||||
|
minidom Element.
|
||||||
|
* New parsers/serialisers:
|
||||||
|
- Last User Interaction in Presence, XEP-0319.
|
||||||
|
* Improved parsers/serialisers:
|
||||||
|
- Message now supports subject, bodies and threads as per
|
||||||
|
RFC 6121 §5.2.
|
||||||
|
- Replace most attribute reads with a nice macro.
|
||||||
|
- Use enums for more enum-like things, for example Algo in
|
||||||
|
Hash, or FieldType in DataForm.
|
||||||
|
- Wire up stanza-id and origin-id to MessagePayload.
|
||||||
|
- Wire up MAM elements to message and iq payloads.
|
||||||
|
- Changes in the RSM API.
|
||||||
|
- Add support for more data forms elements, but still not the
|
||||||
|
complete set.
|
||||||
|
- Thanks to minidom 0.3.1, check for explicitly disallowed
|
||||||
|
extra attributes in some elements.
|
||||||
|
* Crate updates:
|
||||||
|
- minidom 0.4.1
|
||||||
|
|
||||||
|
Version 0.2.0:
|
||||||
|
2017-05-06 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
* New parsers/serialisers:
|
||||||
|
- Stanza error, as per RFC 6120 §8.3.
|
||||||
|
- Jingle SOCKS5 Transport, XEP-0260.
|
||||||
|
* Incompatible changes:
|
||||||
|
- Parsers and serialisers now all implement TryFrom<Element>
|
||||||
|
and Into<Element>, instead of the old parse_* and serialise_*
|
||||||
|
functions.
|
||||||
|
- Presence has got an overhaul, it now hosts show, statuses and
|
||||||
|
priority in its struct. The status module has also been
|
||||||
|
dropped.
|
||||||
|
- Message now supports multiple bodies, each in a different
|
||||||
|
language. The body module has also been dropped.
|
||||||
|
- Iq now gets a proper StanzaError when the type is error.
|
||||||
|
- Fix bogus Jingle payload, which was requiring both
|
||||||
|
description and transport.
|
||||||
|
* Crate updates:
|
||||||
|
- minidom 0.3.0
|
||||||
|
|
||||||
|
Version 0.1.0:
|
||||||
|
2017-04-29 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
* Implement many extensions.
|
|
@ -0,0 +1,373 @@
|
||||||
|
Mozilla Public License Version 2.0
|
||||||
|
==================================
|
||||||
|
|
||||||
|
1. Definitions
|
||||||
|
--------------
|
||||||
|
|
||||||
|
1.1. "Contributor"
|
||||||
|
means each individual or legal entity that creates, contributes to
|
||||||
|
the creation of, or owns Covered Software.
|
||||||
|
|
||||||
|
1.2. "Contributor Version"
|
||||||
|
means the combination of the Contributions of others (if any) used
|
||||||
|
by a Contributor and that particular Contributor's Contribution.
|
||||||
|
|
||||||
|
1.3. "Contribution"
|
||||||
|
means Covered Software of a particular Contributor.
|
||||||
|
|
||||||
|
1.4. "Covered Software"
|
||||||
|
means Source Code Form to which the initial Contributor has attached
|
||||||
|
the notice in Exhibit A, the Executable Form of such Source Code
|
||||||
|
Form, and Modifications of such Source Code Form, in each case
|
||||||
|
including portions thereof.
|
||||||
|
|
||||||
|
1.5. "Incompatible With Secondary Licenses"
|
||||||
|
means
|
||||||
|
|
||||||
|
(a) that the initial Contributor has attached the notice described
|
||||||
|
in Exhibit B to the Covered Software; or
|
||||||
|
|
||||||
|
(b) that the Covered Software was made available under the terms of
|
||||||
|
version 1.1 or earlier of the License, but not also under the
|
||||||
|
terms of a Secondary License.
|
||||||
|
|
||||||
|
1.6. "Executable Form"
|
||||||
|
means any form of the work other than Source Code Form.
|
||||||
|
|
||||||
|
1.7. "Larger Work"
|
||||||
|
means a work that combines Covered Software with other material, in
|
||||||
|
a separate file or files, that is not Covered Software.
|
||||||
|
|
||||||
|
1.8. "License"
|
||||||
|
means this document.
|
||||||
|
|
||||||
|
1.9. "Licensable"
|
||||||
|
means having the right to grant, to the maximum extent possible,
|
||||||
|
whether at the time of the initial grant or subsequently, any and
|
||||||
|
all of the rights conveyed by this License.
|
||||||
|
|
||||||
|
1.10. "Modifications"
|
||||||
|
means any of the following:
|
||||||
|
|
||||||
|
(a) any file in Source Code Form that results from an addition to,
|
||||||
|
deletion from, or modification of the contents of Covered
|
||||||
|
Software; or
|
||||||
|
|
||||||
|
(b) any new file in Source Code Form that contains any Covered
|
||||||
|
Software.
|
||||||
|
|
||||||
|
1.11. "Patent Claims" of a Contributor
|
||||||
|
means any patent claim(s), including without limitation, method,
|
||||||
|
process, and apparatus claims, in any patent Licensable by such
|
||||||
|
Contributor that would be infringed, but for the grant of the
|
||||||
|
License, by the making, using, selling, offering for sale, having
|
||||||
|
made, import, or transfer of either its Contributions or its
|
||||||
|
Contributor Version.
|
||||||
|
|
||||||
|
1.12. "Secondary License"
|
||||||
|
means either the GNU General Public License, Version 2.0, the GNU
|
||||||
|
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||||
|
Public License, Version 3.0, or any later versions of those
|
||||||
|
licenses.
|
||||||
|
|
||||||
|
1.13. "Source Code Form"
|
||||||
|
means the form of the work preferred for making modifications.
|
||||||
|
|
||||||
|
1.14. "You" (or "Your")
|
||||||
|
means an individual or a legal entity exercising rights under this
|
||||||
|
License. For legal entities, "You" includes any entity that
|
||||||
|
controls, is controlled by, or is under common control with You. For
|
||||||
|
purposes of this definition, "control" means (a) the power, direct
|
||||||
|
or indirect, to cause the direction or management of such entity,
|
||||||
|
whether by contract or otherwise, or (b) ownership of more than
|
||||||
|
fifty percent (50%) of the outstanding shares or beneficial
|
||||||
|
ownership of such entity.
|
||||||
|
|
||||||
|
2. License Grants and Conditions
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
2.1. Grants
|
||||||
|
|
||||||
|
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||||
|
non-exclusive license:
|
||||||
|
|
||||||
|
(a) under intellectual property rights (other than patent or trademark)
|
||||||
|
Licensable by such Contributor to use, reproduce, make available,
|
||||||
|
modify, display, perform, distribute, and otherwise exploit its
|
||||||
|
Contributions, either on an unmodified basis, with Modifications, or
|
||||||
|
as part of a Larger Work; and
|
||||||
|
|
||||||
|
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||||
|
for sale, have made, import, and otherwise transfer either its
|
||||||
|
Contributions or its Contributor Version.
|
||||||
|
|
||||||
|
2.2. Effective Date
|
||||||
|
|
||||||
|
The licenses granted in Section 2.1 with respect to any Contribution
|
||||||
|
become effective for each Contribution on the date the Contributor first
|
||||||
|
distributes such Contribution.
|
||||||
|
|
||||||
|
2.3. Limitations on Grant Scope
|
||||||
|
|
||||||
|
The licenses granted in this Section 2 are the only rights granted under
|
||||||
|
this License. No additional rights or licenses will be implied from the
|
||||||
|
distribution or licensing of Covered Software under this License.
|
||||||
|
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||||
|
Contributor:
|
||||||
|
|
||||||
|
(a) for any code that a Contributor has removed from Covered Software;
|
||||||
|
or
|
||||||
|
|
||||||
|
(b) for infringements caused by: (i) Your and any other third party's
|
||||||
|
modifications of Covered Software, or (ii) the combination of its
|
||||||
|
Contributions with other software (except as part of its Contributor
|
||||||
|
Version); or
|
||||||
|
|
||||||
|
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||||
|
its Contributions.
|
||||||
|
|
||||||
|
This License does not grant any rights in the trademarks, service marks,
|
||||||
|
or logos of any Contributor (except as may be necessary to comply with
|
||||||
|
the notice requirements in Section 3.4).
|
||||||
|
|
||||||
|
2.4. Subsequent Licenses
|
||||||
|
|
||||||
|
No Contributor makes additional grants as a result of Your choice to
|
||||||
|
distribute the Covered Software under a subsequent version of this
|
||||||
|
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||||
|
permitted under the terms of Section 3.3).
|
||||||
|
|
||||||
|
2.5. Representation
|
||||||
|
|
||||||
|
Each Contributor represents that the Contributor believes its
|
||||||
|
Contributions are its original creation(s) or it has sufficient rights
|
||||||
|
to grant the rights to its Contributions conveyed by this License.
|
||||||
|
|
||||||
|
2.6. Fair Use
|
||||||
|
|
||||||
|
This License is not intended to limit any rights You have under
|
||||||
|
applicable copyright doctrines of fair use, fair dealing, or other
|
||||||
|
equivalents.
|
||||||
|
|
||||||
|
2.7. Conditions
|
||||||
|
|
||||||
|
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||||
|
in Section 2.1.
|
||||||
|
|
||||||
|
3. Responsibilities
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
3.1. Distribution of Source Form
|
||||||
|
|
||||||
|
All distribution of Covered Software in Source Code Form, including any
|
||||||
|
Modifications that You create or to which You contribute, must be under
|
||||||
|
the terms of this License. You must inform recipients that the Source
|
||||||
|
Code Form of the Covered Software is governed by the terms of this
|
||||||
|
License, and how they can obtain a copy of this License. You may not
|
||||||
|
attempt to alter or restrict the recipients' rights in the Source Code
|
||||||
|
Form.
|
||||||
|
|
||||||
|
3.2. Distribution of Executable Form
|
||||||
|
|
||||||
|
If You distribute Covered Software in Executable Form then:
|
||||||
|
|
||||||
|
(a) such Covered Software must also be made available in Source Code
|
||||||
|
Form, as described in Section 3.1, and You must inform recipients of
|
||||||
|
the Executable Form how they can obtain a copy of such Source Code
|
||||||
|
Form by reasonable means in a timely manner, at a charge no more
|
||||||
|
than the cost of distribution to the recipient; and
|
||||||
|
|
||||||
|
(b) You may distribute such Executable Form under the terms of this
|
||||||
|
License, or sublicense it under different terms, provided that the
|
||||||
|
license for the Executable Form does not attempt to limit or alter
|
||||||
|
the recipients' rights in the Source Code Form under this License.
|
||||||
|
|
||||||
|
3.3. Distribution of a Larger Work
|
||||||
|
|
||||||
|
You may create and distribute a Larger Work under terms of Your choice,
|
||||||
|
provided that You also comply with the requirements of this License for
|
||||||
|
the Covered Software. If the Larger Work is a combination of Covered
|
||||||
|
Software with a work governed by one or more Secondary Licenses, and the
|
||||||
|
Covered Software is not Incompatible With Secondary Licenses, this
|
||||||
|
License permits You to additionally distribute such Covered Software
|
||||||
|
under the terms of such Secondary License(s), so that the recipient of
|
||||||
|
the Larger Work may, at their option, further distribute the Covered
|
||||||
|
Software under the terms of either this License or such Secondary
|
||||||
|
License(s).
|
||||||
|
|
||||||
|
3.4. Notices
|
||||||
|
|
||||||
|
You may not remove or alter the substance of any license notices
|
||||||
|
(including copyright notices, patent notices, disclaimers of warranty,
|
||||||
|
or limitations of liability) contained within the Source Code Form of
|
||||||
|
the Covered Software, except that You may alter any license notices to
|
||||||
|
the extent required to remedy known factual inaccuracies.
|
||||||
|
|
||||||
|
3.5. Application of Additional Terms
|
||||||
|
|
||||||
|
You may choose to offer, and to charge a fee for, warranty, support,
|
||||||
|
indemnity or liability obligations to one or more recipients of Covered
|
||||||
|
Software. However, You may do so only on Your own behalf, and not on
|
||||||
|
behalf of any Contributor. You must make it absolutely clear that any
|
||||||
|
such warranty, support, indemnity, or liability obligation is offered by
|
||||||
|
You alone, and You hereby agree to indemnify every Contributor for any
|
||||||
|
liability incurred by such Contributor as a result of warranty, support,
|
||||||
|
indemnity or liability terms You offer. You may include additional
|
||||||
|
disclaimers of warranty and limitations of liability specific to any
|
||||||
|
jurisdiction.
|
||||||
|
|
||||||
|
4. Inability to Comply Due to Statute or Regulation
|
||||||
|
---------------------------------------------------
|
||||||
|
|
||||||
|
If it is impossible for You to comply with any of the terms of this
|
||||||
|
License with respect to some or all of the Covered Software due to
|
||||||
|
statute, judicial order, or regulation then You must: (a) comply with
|
||||||
|
the terms of this License to the maximum extent possible; and (b)
|
||||||
|
describe the limitations and the code they affect. Such description must
|
||||||
|
be placed in a text file included with all distributions of the Covered
|
||||||
|
Software under this License. Except to the extent prohibited by statute
|
||||||
|
or regulation, such description must be sufficiently detailed for a
|
||||||
|
recipient of ordinary skill to be able to understand it.
|
||||||
|
|
||||||
|
5. Termination
|
||||||
|
--------------
|
||||||
|
|
||||||
|
5.1. The rights granted under this License will terminate automatically
|
||||||
|
if You fail to comply with any of its terms. However, if You become
|
||||||
|
compliant, then the rights granted under this License from a particular
|
||||||
|
Contributor are reinstated (a) provisionally, unless and until such
|
||||||
|
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||||
|
ongoing basis, if such Contributor fails to notify You of the
|
||||||
|
non-compliance by some reasonable means prior to 60 days after You have
|
||||||
|
come back into compliance. Moreover, Your grants from a particular
|
||||||
|
Contributor are reinstated on an ongoing basis if such Contributor
|
||||||
|
notifies You of the non-compliance by some reasonable means, this is the
|
||||||
|
first time You have received notice of non-compliance with this License
|
||||||
|
from such Contributor, and You become compliant prior to 30 days after
|
||||||
|
Your receipt of the notice.
|
||||||
|
|
||||||
|
5.2. If You initiate litigation against any entity by asserting a patent
|
||||||
|
infringement claim (excluding declaratory judgment actions,
|
||||||
|
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||||
|
directly or indirectly infringes any patent, then the rights granted to
|
||||||
|
You by any and all Contributors for the Covered Software under Section
|
||||||
|
2.1 of this License shall terminate.
|
||||||
|
|
||||||
|
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||||
|
end user license agreements (excluding distributors and resellers) which
|
||||||
|
have been validly granted by You or Your distributors under this License
|
||||||
|
prior to termination shall survive termination.
|
||||||
|
|
||||||
|
************************************************************************
|
||||||
|
* *
|
||||||
|
* 6. Disclaimer of Warranty *
|
||||||
|
* ------------------------- *
|
||||||
|
* *
|
||||||
|
* Covered Software is provided under this License on an "as is" *
|
||||||
|
* basis, without warranty of any kind, either expressed, implied, or *
|
||||||
|
* statutory, including, without limitation, warranties that the *
|
||||||
|
* Covered Software is free of defects, merchantable, fit for a *
|
||||||
|
* particular purpose or non-infringing. The entire risk as to the *
|
||||||
|
* quality and performance of the Covered Software is with You. *
|
||||||
|
* Should any Covered Software prove defective in any respect, You *
|
||||||
|
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||||
|
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||||
|
* essential part of this License. No use of any Covered Software is *
|
||||||
|
* authorized under this License except under this disclaimer. *
|
||||||
|
* *
|
||||||
|
************************************************************************
|
||||||
|
|
||||||
|
************************************************************************
|
||||||
|
* *
|
||||||
|
* 7. Limitation of Liability *
|
||||||
|
* -------------------------- *
|
||||||
|
* *
|
||||||
|
* Under no circumstances and under no legal theory, whether tort *
|
||||||
|
* (including negligence), contract, or otherwise, shall any *
|
||||||
|
* Contributor, or anyone who distributes Covered Software as *
|
||||||
|
* permitted above, be liable to You for any direct, indirect, *
|
||||||
|
* special, incidental, or consequential damages of any character *
|
||||||
|
* including, without limitation, damages for lost profits, loss of *
|
||||||
|
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||||
|
* and all other commercial damages or losses, even if such party *
|
||||||
|
* shall have been informed of the possibility of such damages. This *
|
||||||
|
* limitation of liability shall not apply to liability for death or *
|
||||||
|
* personal injury resulting from such party's negligence to the *
|
||||||
|
* extent applicable law prohibits such limitation. Some *
|
||||||
|
* jurisdictions do not allow the exclusion or limitation of *
|
||||||
|
* incidental or consequential damages, so this exclusion and *
|
||||||
|
* limitation may not apply to You. *
|
||||||
|
* *
|
||||||
|
************************************************************************
|
||||||
|
|
||||||
|
8. Litigation
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Any litigation relating to this License may be brought only in the
|
||||||
|
courts of a jurisdiction where the defendant maintains its principal
|
||||||
|
place of business and such litigation shall be governed by laws of that
|
||||||
|
jurisdiction, without reference to its conflict-of-law provisions.
|
||||||
|
Nothing in this Section shall prevent a party's ability to bring
|
||||||
|
cross-claims or counter-claims.
|
||||||
|
|
||||||
|
9. Miscellaneous
|
||||||
|
----------------
|
||||||
|
|
||||||
|
This License represents the complete agreement concerning the subject
|
||||||
|
matter hereof. If any provision of this License is held to be
|
||||||
|
unenforceable, such provision shall be reformed only to the extent
|
||||||
|
necessary to make it enforceable. Any law or regulation which provides
|
||||||
|
that the language of a contract shall be construed against the drafter
|
||||||
|
shall not be used to construe this License against a Contributor.
|
||||||
|
|
||||||
|
10. Versions of the License
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
10.1. New Versions
|
||||||
|
|
||||||
|
Mozilla Foundation is the license steward. Except as provided in Section
|
||||||
|
10.3, no one other than the license steward has the right to modify or
|
||||||
|
publish new versions of this License. Each version will be given a
|
||||||
|
distinguishing version number.
|
||||||
|
|
||||||
|
10.2. Effect of New Versions
|
||||||
|
|
||||||
|
You may distribute the Covered Software under the terms of the version
|
||||||
|
of the License under which You originally received the Covered Software,
|
||||||
|
or under the terms of any subsequent version published by the license
|
||||||
|
steward.
|
||||||
|
|
||||||
|
10.3. Modified Versions
|
||||||
|
|
||||||
|
If you create software not governed by this License, and you want to
|
||||||
|
create a new license for such software, you may create and use a
|
||||||
|
modified version of this License if you rename the license and remove
|
||||||
|
any references to the name of the license steward (except to note that
|
||||||
|
such modified license differs from this License).
|
||||||
|
|
||||||
|
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||||
|
Licenses
|
||||||
|
|
||||||
|
If You choose to distribute Source Code Form that is Incompatible With
|
||||||
|
Secondary Licenses under the terms of this version of the License, the
|
||||||
|
notice described in Exhibit B of this License must be attached.
|
||||||
|
|
||||||
|
Exhibit A - Source Code Form License Notice
|
||||||
|
-------------------------------------------
|
||||||
|
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
If it is not possible or desirable to put the notice in a particular
|
||||||
|
file, then You may include the notice in a location (such as a LICENSE
|
||||||
|
file in a relevant directory) where a recipient would be likely to look
|
||||||
|
for such a notice.
|
||||||
|
|
||||||
|
You may add additional accurate notices of copyright ownership.
|
||||||
|
|
||||||
|
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||||
|
---------------------------------------------------------
|
||||||
|
|
||||||
|
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||||
|
defined by the Mozilla Public License, v. 2.0.
|
|
@ -0,0 +1,684 @@
|
||||||
|
<?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.2.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-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.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-0177.html"/>
|
||||||
|
<xmpp:status>complete</xmpp:status>
|
||||||
|
<xmpp:version>1.1</xmpp:version>
|
||||||
|
<xmpp:since>NEXT</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-0294.html"/>
|
||||||
|
<xmpp:status>partial</xmpp:status>
|
||||||
|
<xmpp:version>1.0</xmpp:version>
|
||||||
|
<xmpp:since>NEXT</xmpp:since>
|
||||||
|
<xmpp:note>Parameters aren’t yet implemented.</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.7.5</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-0338.html"/>
|
||||||
|
<xmpp:status>complete</xmpp:status>
|
||||||
|
<xmpp:version>1.0.0</xmpp:version>
|
||||||
|
<xmpp:since>NEXT</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-0369.html"/>
|
||||||
|
<xmpp:status>complete</xmpp:status>
|
||||||
|
<xmpp:version>0.14.3</xmpp:version>
|
||||||
|
<xmpp:since>NEXT</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>1.1.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-0421.html"/>
|
||||||
|
<xmpp:status>complete</xmpp:status>
|
||||||
|
<xmpp:version>0.1.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-0441.html"/>
|
||||||
|
<xmpp:status>complete</xmpp:status>
|
||||||
|
<xmpp:version>0.2.0</xmpp:version>
|
||||||
|
<xmpp:since>0.1.0</xmpp:since>
|
||||||
|
</xmpp:SupportedXep>
|
||||||
|
</implements>
|
||||||
|
|
||||||
|
<release>
|
||||||
|
<Version>
|
||||||
|
<revision>0.15.0</revision>
|
||||||
|
<created>2019-09-06</created>
|
||||||
|
<file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.15.0/download"/>
|
||||||
|
</Version>
|
||||||
|
</release>
|
||||||
|
<release>
|
||||||
|
<Version>
|
||||||
|
<revision>0.14.0</revision>
|
||||||
|
<created>2019-07-13</created>
|
||||||
|
<file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.14.0/download"/>
|
||||||
|
</Version>
|
||||||
|
</release>
|
||||||
|
<release>
|
||||||
|
<Version>
|
||||||
|
<revision>0.13.1</revision>
|
||||||
|
<created>2019-04-12</created>
|
||||||
|
<file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.13.1/download"/>
|
||||||
|
</Version>
|
||||||
|
</release>
|
||||||
|
<release>
|
||||||
|
<Version>
|
||||||
|
<revision>0.13.0</revision>
|
||||||
|
<created>2019-03-20</created>
|
||||||
|
<file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.13.0/download"/>
|
||||||
|
</Version>
|
||||||
|
</release>
|
||||||
|
<release>
|
||||||
|
<Version>
|
||||||
|
<revision>0.12.2</revision>
|
||||||
|
<created>2019-01-16</created>
|
||||||
|
<file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.12.2/download"/>
|
||||||
|
</Version>
|
||||||
|
</release>
|
||||||
|
<release>
|
||||||
|
<Version>
|
||||||
|
<revision>0.12.1</revision>
|
||||||
|
<created>2019-01-16</created>
|
||||||
|
<file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.12.1/download"/>
|
||||||
|
</Version>
|
||||||
|
</release>
|
||||||
|
<release>
|
||||||
|
<Version>
|
||||||
|
<revision>0.12.0</revision>
|
||||||
|
<created>2019-01-16</created>
|
||||||
|
<file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.12.0/download"/>
|
||||||
|
</Version>
|
||||||
|
</release>
|
||||||
|
<release>
|
||||||
|
<Version>
|
||||||
|
<revision>0.11.1</revision>
|
||||||
|
<created>2018-09-20</created>
|
||||||
|
<file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.11.1/download"/>
|
||||||
|
</Version>
|
||||||
|
</release>
|
||||||
|
<release>
|
||||||
|
<Version>
|
||||||
|
<revision>0.11.0</revision>
|
||||||
|
<created>2018-08-02</created>
|
||||||
|
<file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.11.0/download"/>
|
||||||
|
</Version>
|
||||||
|
</release>
|
||||||
|
<release>
|
||||||
|
<Version>
|
||||||
|
<revision>0.10.0</revision>
|
||||||
|
<created>2018-07-31</created>
|
||||||
|
<file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.10.0/download"/>
|
||||||
|
</Version>
|
||||||
|
</release>
|
||||||
|
<release>
|
||||||
|
<Version>
|
||||||
|
<revision>0.9.0</revision>
|
||||||
|
<created>2017-12-27</created>
|
||||||
|
<file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.9.0/download"/>
|
||||||
|
</Version>
|
||||||
|
</release>
|
||||||
|
<release>
|
||||||
|
<Version>
|
||||||
|
<revision>0.8.0</revision>
|
||||||
|
<created>2017-11-30</created>
|
||||||
|
<file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.8.0/download"/>
|
||||||
|
</Version>
|
||||||
|
</release>
|
||||||
|
<release>
|
||||||
|
<Version>
|
||||||
|
<revision>0.7.1</revision>
|
||||||
|
<created>2017-11-30</created>
|
||||||
|
<file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.7.1/download"/>
|
||||||
|
</Version>
|
||||||
|
</release>
|
||||||
|
<release>
|
||||||
|
<Version>
|
||||||
|
<revision>0.7.0</revision>
|
||||||
|
<created>2017-11-30</created>
|
||||||
|
<file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.7.0/download"/>
|
||||||
|
</Version>
|
||||||
|
</release>
|
||||||
|
<release>
|
||||||
|
<Version>
|
||||||
|
<revision>0.6.0</revision>
|
||||||
|
<created>2017-11-30</created>
|
||||||
|
<file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.6.0/download"/>
|
||||||
|
</Version>
|
||||||
|
</release>
|
||||||
|
<release>
|
||||||
|
<Version>
|
||||||
|
<revision>0.5.0</revision>
|
||||||
|
<created>2017-11-30</created>
|
||||||
|
<file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.5.0/download"/>
|
||||||
|
</Version>
|
||||||
|
</release>
|
||||||
|
<release>
|
||||||
|
<Version>
|
||||||
|
<revision>0.4.0</revision>
|
||||||
|
<created>2017-11-30</created>
|
||||||
|
<file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.4.0/download"/>
|
||||||
|
</Version>
|
||||||
|
</release>
|
||||||
|
<release>
|
||||||
|
<Version>
|
||||||
|
<revision>0.3.0</revision>
|
||||||
|
<created>2017-11-30</created>
|
||||||
|
<file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.3.0/download"/>
|
||||||
|
</Version>
|
||||||
|
</release>
|
||||||
|
<release>
|
||||||
|
<Version>
|
||||||
|
<revision>0.2.0</revision>
|
||||||
|
<created>2017-11-30</created>
|
||||||
|
<file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.2.0/download"/>
|
||||||
|
</Version>
|
||||||
|
</release>
|
||||||
|
<release>
|
||||||
|
<Version>
|
||||||
|
<revision>0.1.0</revision>
|
||||||
|
<created>2017-11-30</created>
|
||||||
|
<file-release rdf:resource="https://crates.io/api/v1/crates/xmpp-parsers/0.1.0/download"/>
|
||||||
|
</Version>
|
||||||
|
</release>
|
||||||
|
</Project>
|
||||||
|
</rdf:RDF>
|
|
@ -0,0 +1,67 @@
|
||||||
|
// 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::env;
|
||||||
|
use std::io::{self, Read};
|
||||||
|
use xmpp_parsers::{
|
||||||
|
caps::{compute_disco as compute_disco_caps, hash_caps, Caps},
|
||||||
|
disco::DiscoInfoResult,
|
||||||
|
ecaps2::{compute_disco as compute_disco_ecaps2, hash_ecaps2, ECaps2},
|
||||||
|
hashes::Algo,
|
||||||
|
Element, Error,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn get_caps(disco: &DiscoInfoResult, node: String) -> Result<Caps, String> {
|
||||||
|
let data = compute_disco_caps(&disco);
|
||||||
|
let hash = hash_caps(&data, Algo::Sha_1)?;
|
||||||
|
Ok(Caps::new(node, hash))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_ecaps2(disco: &DiscoInfoResult) -> Result<ECaps2, Error> {
|
||||||
|
let data = compute_disco_ecaps2(&disco)?;
|
||||||
|
let hash_sha256 = hash_ecaps2(&data, Algo::Sha_256)?;
|
||||||
|
let hash_sha3_256 = hash_ecaps2(&data, Algo::Sha3_256)?;
|
||||||
|
let hash_blake2b_256 = hash_ecaps2(&data, Algo::Blake2b_256)?;
|
||||||
|
Ok(ECaps2::new(vec![
|
||||||
|
hash_sha256,
|
||||||
|
hash_sha3_256,
|
||||||
|
hash_blake2b_256,
|
||||||
|
]))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let args: Vec<_> = env::args().collect();
|
||||||
|
if args.len() != 2 {
|
||||||
|
println!("Usage: {} <node>", args[0]);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
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)?;
|
||||||
|
|
||||||
|
// Parse the payload into a DiscoInfoResult.
|
||||||
|
let elem: Element = data.parse()?;
|
||||||
|
let disco = DiscoInfoResult::try_from(elem)?;
|
||||||
|
|
||||||
|
// Compute both kinds of caps.
|
||||||
|
let caps = get_caps(&disco, node)?;
|
||||||
|
let ecaps2 = get_ecaps2(&disco)?;
|
||||||
|
|
||||||
|
// Print them.
|
||||||
|
let caps_elem = Element::from(caps);
|
||||||
|
let ecaps2_elem = Element::from(ecaps2);
|
||||||
|
println!("{}", String::from(&caps_elem));
|
||||||
|
println!("{}", String::from(&ecaps2_elem));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
use crate::message::MessagePayload;
|
||||||
|
|
||||||
|
generate_empty_element!(
|
||||||
|
/// Requests the attention of the recipient.
|
||||||
|
Attention,
|
||||||
|
"attention",
|
||||||
|
ATTENTION
|
||||||
|
);
|
||||||
|
|
||||||
|
impl MessagePayload for Attention {}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
#[cfg(not(feature = "disable-validation"))]
|
||||||
|
use crate::util::error::Error;
|
||||||
|
use crate::Element;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(Attention, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_simple() {
|
||||||
|
let elem: Element = "<attention xmlns='urn:xmpp:attention:0'/>".parse().unwrap();
|
||||||
|
Attention::try_from(elem).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "disable-validation"))]
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_child() {
|
||||||
|
let elem: Element = "<attention xmlns='urn:xmpp:attention:0'><coucou/></attention>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = Attention::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Unknown child in attention element.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "disable-validation"))]
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_attribute() {
|
||||||
|
let elem: Element = "<attention xmlns='urn:xmpp:attention:0' coucou=''/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = Attention::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Unknown attribute in attention element.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialise() {
|
||||||
|
let elem: Element = "<attention xmlns='urn:xmpp:attention:0'/>".parse().unwrap();
|
||||||
|
let attention = Attention;
|
||||||
|
let elem2: Element = attention.into();
|
||||||
|
assert_eq!(elem, elem2);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,128 @@
|
||||||
|
// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
use crate::hashes::Sha1HexAttribute;
|
||||||
|
use crate::pubsub::PubSubPayload;
|
||||||
|
use crate::util::helpers::WhitespaceAwareBase64;
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// Communicates information about an avatar.
|
||||||
|
Metadata, "metadata", AVATAR_METADATA,
|
||||||
|
children: [
|
||||||
|
/// List of information elements describing this avatar.
|
||||||
|
infos: Vec<Info> = ("info", AVATAR_METADATA) => Info
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
impl PubSubPayload for Metadata {}
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// Communicates avatar metadata.
|
||||||
|
Info, "info", AVATAR_METADATA,
|
||||||
|
attributes: [
|
||||||
|
/// The size of the image data in bytes.
|
||||||
|
bytes: Required<u16> = "bytes",
|
||||||
|
|
||||||
|
/// The width of the image in pixels.
|
||||||
|
width: Option<u16> = "width",
|
||||||
|
|
||||||
|
/// The height of the image in pixels.
|
||||||
|
height: Option<u16> = "height",
|
||||||
|
|
||||||
|
/// The SHA-1 hash of the image data for the specified content-type.
|
||||||
|
id: Required<Sha1HexAttribute> = "id",
|
||||||
|
|
||||||
|
/// The IANA-registered content type of the image data.
|
||||||
|
type_: Required<String> = "type",
|
||||||
|
|
||||||
|
/// The http: or https: URL at which the image data file is hosted.
|
||||||
|
url: Option<String> = "url",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// The actual avatar data.
|
||||||
|
Data, "data", AVATAR_DATA,
|
||||||
|
text: (
|
||||||
|
/// Vector of bytes representing the avatar’s image.
|
||||||
|
data: WhitespaceAwareBase64<Vec<u8>>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
impl PubSubPayload for Data {}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::hashes::Algo;
|
||||||
|
#[cfg(not(feature = "disable-validation"))]
|
||||||
|
use crate::util::error::Error;
|
||||||
|
use crate::Element;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "32")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(Metadata, 12);
|
||||||
|
assert_size!(Info, 64);
|
||||||
|
assert_size!(Data, 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "64")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(Metadata, 24);
|
||||||
|
assert_size!(Info, 120);
|
||||||
|
assert_size!(Data, 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_simple() {
|
||||||
|
let elem: Element = "<metadata xmlns='urn:xmpp:avatar:metadata'>
|
||||||
|
<info bytes='12345' width='64' height='64'
|
||||||
|
id='111f4b3c50d7b0df729d299bc6f8e9ef9066971f'
|
||||||
|
type='image/png'/>
|
||||||
|
</metadata>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let metadata = Metadata::try_from(elem).unwrap();
|
||||||
|
assert_eq!(metadata.infos.len(), 1);
|
||||||
|
let info = &metadata.infos[0];
|
||||||
|
assert_eq!(info.bytes, 12345);
|
||||||
|
assert_eq!(info.width, Some(64));
|
||||||
|
assert_eq!(info.height, Some(64));
|
||||||
|
assert_eq!(info.id.algo, Algo::Sha_1);
|
||||||
|
assert_eq!(info.type_, "image/png");
|
||||||
|
assert_eq!(info.url, None);
|
||||||
|
assert_eq!(
|
||||||
|
info.id.hash,
|
||||||
|
[
|
||||||
|
17, 31, 75, 60, 80, 215, 176, 223, 114, 157, 41, 155, 198, 248, 233, 239, 144, 102,
|
||||||
|
151, 31
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
let elem: Element = "<data xmlns='urn:xmpp:avatar:data'>AAAA</data>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let data = Data::try_from(elem).unwrap();
|
||||||
|
assert_eq!(data.data, b"\0\0\0");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "disable-validation"))]
|
||||||
|
#[test]
|
||||||
|
fn test_invalid() {
|
||||||
|
let elem: Element = "<data xmlns='urn:xmpp:avatar:data' id='coucou'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = Data::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Unknown attribute in data element.")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,203 @@
|
||||||
|
// 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::iq::{IqResultPayload, IqSetPayload};
|
||||||
|
use crate::ns;
|
||||||
|
use crate::util::error::Error;
|
||||||
|
use crate::Element;
|
||||||
|
use jid::{FullJid, Jid};
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
/// 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::BIND)
|
||||||
|
.append_all(
|
||||||
|
bind.resource
|
||||||
|
.map(|resource| Element::builder("resource", 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::BIND)
|
||||||
|
.append(Element::builder("jid", ns::BIND).append(bind.jid))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "32")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(BindQuery, 12);
|
||||||
|
assert_size!(BindResponse, 36);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "64")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(BindQuery, 24);
|
||||||
|
assert_size!(BindResponse, 72);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_simple() {
|
||||||
|
let elem: Element = "<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let bind = BindQuery::try_from(elem).unwrap();
|
||||||
|
assert_eq!(bind.resource, None);
|
||||||
|
|
||||||
|
let elem: Element =
|
||||||
|
"<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'><resource>Hello™</resource></bind>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let bind = BindQuery::try_from(elem).unwrap();
|
||||||
|
// FIXME: “™” should be resourceprep’d into “TM” here…
|
||||||
|
//assert_eq!(bind.resource.unwrap(), "HelloTM");
|
||||||
|
assert_eq!(bind.resource.unwrap(), "Hello™");
|
||||||
|
|
||||||
|
let elem: Element = "<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'><jid>coucou@linkmauve.fr/HelloTM</jid></bind>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let bind = BindResponse::try_from(elem).unwrap();
|
||||||
|
assert_eq!(bind.jid, FullJid::new("coucou", "linkmauve.fr", "HelloTM"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "disable-validation"))]
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_resource() {
|
||||||
|
let elem: Element = "<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'><resource attr='coucou'>resource</resource></bind>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = BindQuery::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Unknown attribute in resource element.");
|
||||||
|
|
||||||
|
let elem: Element = "<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'><resource><hello-world/>resource</resource></bind>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = BindQuery::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Unknown child in resource element.");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,221 @@
|
||||||
|
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
use crate::iq::{IqGetPayload, IqResultPayload, IqSetPayload};
|
||||||
|
use crate::ns;
|
||||||
|
use crate::util::error::Error;
|
||||||
|
use crate::Element;
|
||||||
|
use jid::Jid;
|
||||||
|
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::BLOCKING)
|
||||||
|
.append_all(elem.items.into_iter().map(|jid| {
|
||||||
|
Element::builder("item", 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!(result.items.is_empty());
|
||||||
|
|
||||||
|
let elem: Element = "<block xmlns='urn:xmpp:blocking'/>".parse().unwrap();
|
||||||
|
let block = Block::try_from(elem).unwrap();
|
||||||
|
assert!(block.items.is_empty());
|
||||||
|
|
||||||
|
let elem: Element = "<unblock xmlns='urn:xmpp:blocking'/>".parse().unwrap();
|
||||||
|
let unblock = Unblock::try_from(elem).unwrap();
|
||||||
|
assert!(unblock.items.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_items() {
|
||||||
|
let elem: Element = "<blocklist xmlns='urn:xmpp:blocking'><item jid='coucou@coucou'/><item jid='domain'/></blocklist>".parse().unwrap();
|
||||||
|
let two_items = vec![
|
||||||
|
Jid::Bare(BareJid {
|
||||||
|
node: Some(String::from("coucou")),
|
||||||
|
domain: String::from("coucou"),
|
||||||
|
}),
|
||||||
|
Jid::Bare(BareJid {
|
||||||
|
node: None,
|
||||||
|
domain: String::from("domain"),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
let result_elem = elem.clone();
|
||||||
|
let result = BlocklistResult::try_from(result_elem).unwrap();
|
||||||
|
assert_eq!(result.items, two_items);
|
||||||
|
|
||||||
|
let elem: Element = "<block xmlns='urn:xmpp:blocking'><item jid='coucou@coucou'/><item jid='domain'/></block>".parse().unwrap();
|
||||||
|
let block = Block::try_from(elem).unwrap();
|
||||||
|
assert_eq!(block.items, two_items);
|
||||||
|
|
||||||
|
let elem: Element = "<unblock xmlns='urn:xmpp:blocking'><item jid='coucou@coucou'/><item jid='domain'/></unblock>".parse().unwrap();
|
||||||
|
let unblock = Unblock::try_from(elem).unwrap();
|
||||||
|
assert_eq!(unblock.items, two_items);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "disable-validation"))]
|
||||||
|
#[test]
|
||||||
|
fn test_invalid() {
|
||||||
|
let elem: Element = "<blocklist xmlns='urn:xmpp:blocking' coucou=''/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let request_elem = elem.clone();
|
||||||
|
let error = BlocklistRequest::try_from(request_elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Unknown attribute in blocklist element.");
|
||||||
|
|
||||||
|
let result_elem = elem.clone();
|
||||||
|
let error = BlocklistResult::try_from(result_elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Unknown attribute in blocklist element.");
|
||||||
|
|
||||||
|
let elem: Element = "<block xmlns='urn:xmpp:blocking' coucou=''/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = Block::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Unknown attribute in block element.");
|
||||||
|
|
||||||
|
let elem: Element = "<unblock xmlns='urn:xmpp:blocking' coucou=''/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = Unblock::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Unknown attribute in unblock element.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "disable-validation"))]
|
||||||
|
#[test]
|
||||||
|
fn test_non_empty_blocklist_request() {
|
||||||
|
let elem: Element = "<blocklist xmlns='urn:xmpp:blocking'><item jid='coucou@coucou'/><item jid='domain'/></blocklist>".parse().unwrap();
|
||||||
|
let error = BlocklistRequest::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Unknown child in blocklist element.");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,182 @@
|
||||||
|
// 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::{Algo, Hash};
|
||||||
|
use crate::util::error::Error;
|
||||||
|
use crate::util::helpers::Base64;
|
||||||
|
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, PartialEq)]
|
||||||
|
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;
|
||||||
|
|
||||||
|
#[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.to_string(), "invalid digit found in string");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unknown_child() {
|
||||||
|
let elem: Element = "<data xmlns='urn:xmpp:bob'><coucou/></data>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = Data::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Unknown child in data element.");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,121 @@
|
||||||
|
// 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: Option<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: Option<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::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_eq!(elem1, 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].clone().name.unwrap(), "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].clone().name.unwrap(), "Test MUC");
|
||||||
|
assert_eq!(storage.conferences[0].clone().nick.unwrap(), "Coucou");
|
||||||
|
assert_eq!(storage.conferences[0].clone().password.unwrap(), "secret");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,201 @@
|
||||||
|
// 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::ns;
|
||||||
|
use crate::util::error::Error;
|
||||||
|
use crate::Element;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
generate_attribute!(
|
||||||
|
/// Whether a conference bookmark should be joined automatically.
|
||||||
|
Autojoin,
|
||||||
|
"autojoin",
|
||||||
|
bool
|
||||||
|
);
|
||||||
|
|
||||||
|
/// A conference bookmark.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Conference {
|
||||||
|
/// Whether a conference bookmark should be joined automatically.
|
||||||
|
pub autojoin: Autojoin,
|
||||||
|
|
||||||
|
/// A user-defined name for this conference.
|
||||||
|
pub name: Option<String>,
|
||||||
|
|
||||||
|
/// The nick the user will use to join this conference.
|
||||||
|
pub nick: Option<String>,
|
||||||
|
|
||||||
|
/// The password required to join this conference.
|
||||||
|
pub password: Option<String>,
|
||||||
|
|
||||||
|
/// Extensions elements.
|
||||||
|
pub extensions: Option<Vec<Element>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Conference {
|
||||||
|
/// Create a new conference.
|
||||||
|
pub fn new() -> Conference {
|
||||||
|
Conference {
|
||||||
|
autojoin: Autojoin::False,
|
||||||
|
name: None,
|
||||||
|
nick: None,
|
||||||
|
password: None,
|
||||||
|
extensions: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<Element> for Conference {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(root: Element) -> Result<Conference, Error> {
|
||||||
|
check_self!(root, "conference", BOOKMARKS2, "Conference");
|
||||||
|
check_no_unknown_attributes!(root, "Conference", ["autojoin", "name"]);
|
||||||
|
|
||||||
|
let mut conference = Conference {
|
||||||
|
autojoin: get_attr!(root, "autojoin", Default),
|
||||||
|
name: get_attr!(root, "name", Option),
|
||||||
|
nick: None,
|
||||||
|
password: None,
|
||||||
|
extensions: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
for child in root.children().cloned() {
|
||||||
|
if child.is("extensions", ns::BOOKMARKS2) {
|
||||||
|
if conference.extensions.is_some() {
|
||||||
|
return Err(Error::ParseError(
|
||||||
|
"Conference must not have more than one extensions element.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
conference.extensions = Some(child.children().cloned().collect());
|
||||||
|
} else if child.is("nick", ns::BOOKMARKS2) {
|
||||||
|
if conference.nick.is_some() {
|
||||||
|
return Err(Error::ParseError(
|
||||||
|
"Conference must not have more than one nick.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
check_no_children!(child, "nick");
|
||||||
|
check_no_attributes!(child, "nick");
|
||||||
|
conference.nick = Some(child.text());
|
||||||
|
} else if child.is("password", ns::BOOKMARKS2) {
|
||||||
|
if conference.password.is_some() {
|
||||||
|
return Err(Error::ParseError(
|
||||||
|
"Conference must not have more than one password.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
check_no_children!(child, "password");
|
||||||
|
check_no_attributes!(child, "password");
|
||||||
|
conference.password = Some(child.text());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(conference)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Conference> for Element {
|
||||||
|
fn from(conference: Conference) -> Element {
|
||||||
|
Element::builder("conference", ns::BOOKMARKS2)
|
||||||
|
.attr("autojoin", conference.autojoin)
|
||||||
|
.attr("name", conference.name)
|
||||||
|
.append_all(
|
||||||
|
conference
|
||||||
|
.nick
|
||||||
|
.map(|nick| Element::builder("nick", ns::BOOKMARKS2).append(nick)),
|
||||||
|
)
|
||||||
|
.append_all(
|
||||||
|
conference
|
||||||
|
.password
|
||||||
|
.map(|password| Element::builder("password", ns::BOOKMARKS2).append(password)),
|
||||||
|
)
|
||||||
|
.append_all(match conference.extensions {
|
||||||
|
Some(extensions) => extensions,
|
||||||
|
None => vec![],
|
||||||
|
})
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::pubsub::pubsub::Item as PubSubItem;
|
||||||
|
use crate::Element;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "32")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(Conference, 52);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "64")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(Conference, 104);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn simple() {
|
||||||
|
let elem: Element = "<conference xmlns='urn:xmpp:bookmarks:1'/>"
|
||||||
|
.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_eq!(elem1, elem2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn complete() {
|
||||||
|
let elem: Element = "<conference xmlns='urn:xmpp:bookmarks:1' autojoin='true' name='Test MUC'><nick>Coucou</nick><password>secret</password><extensions><test xmlns='urn:xmpp:unknown' /></extensions></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");
|
||||||
|
assert_eq!(conference.clone().extensions.unwrap().len(), 1);
|
||||||
|
assert!(conference.clone().extensions.unwrap()[0].is("test", "urn:xmpp:unknown"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wrapped() {
|
||||||
|
let elem: Element = "<item xmlns='http://jabber.org/protocol/pubsub' id='test-muc@muc.localhost'><conference xmlns='urn:xmpp:bookmarks:1' 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();
|
||||||
|
println!("FOO: payload: {:?}", payload);
|
||||||
|
// let conference = Conference::try_from(payload).unwrap();
|
||||||
|
let conference = Conference::try_from(payload);
|
||||||
|
println!("FOO: conference: {:?}", conference);
|
||||||
|
/*
|
||||||
|
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:1'><item xmlns='http://jabber.org/protocol/pubsub#event' id='test-muc@muc.localhost'><conference xmlns='urn:xmpp:bookmarks:1' autojoin='true' name='Test MUC'><nick>Coucou</nick><password>secret</password></conference></item></items></event>".parse().unwrap();
|
||||||
|
let mut items = match PubSubEvent::try_from(elem) {
|
||||||
|
Ok(PubSubEvent::PublishedItems { node, items }) => {
|
||||||
|
assert_eq!(&node.0, ns::BOOKMARKS2);
|
||||||
|
items
|
||||||
|
}
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(items.len(), 1);
|
||||||
|
let item = items.pop().unwrap();
|
||||||
|
let payload = item.payload.clone().unwrap();
|
||||||
|
let conference = Conference::try_from(payload).unwrap();
|
||||||
|
assert_eq!(conference.autojoin, Autojoin::True);
|
||||||
|
assert_eq!(conference.name, Some(String::from("Test MUC")));
|
||||||
|
assert_eq!(conference.clone().nick.unwrap(), "Coucou");
|
||||||
|
assert_eq!(conference.clone().password.unwrap(), "secret");
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,342 @@
|
||||||
|
// 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 crate::util::error::Error;
|
||||||
|
use crate::Element;
|
||||||
|
use blake2::VarBlake2b;
|
||||||
|
use digest::{Digest, Update, VariableOutput};
|
||||||
|
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::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> {
|
||||||
|
hash.to_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.update(data);
|
||||||
|
let mut vec = Vec::with_capacity(32);
|
||||||
|
hasher.finalize_variable(|slice| vec.extend_from_slice(slice));
|
||||||
|
vec
|
||||||
|
}
|
||||||
|
Algo::Blake2b_512 => {
|
||||||
|
let mut hasher = VarBlake2b::new(64).unwrap();
|
||||||
|
hasher.update(data);
|
||||||
|
let mut vec = Vec::with_capacity(64);
|
||||||
|
hasher.finalize_variable(|slice| vec.extend_from_slice(slice));
|
||||||
|
vec
|
||||||
|
}
|
||||||
|
Algo::Unknown(algo) => return Err(format!("Unknown algorithm: {}.", algo)),
|
||||||
|
},
|
||||||
|
algo,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper function to create the query for the disco#info corresponding to a
|
||||||
|
/// caps hash.
|
||||||
|
pub fn query_caps(caps: Caps) -> DiscoInfoQuery {
|
||||||
|
DiscoInfoQuery {
|
||||||
|
node: Some(format!("{}#{}", caps.node, base64::encode(&caps.hash.hash))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::caps;
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "32")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(Caps, 52);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "64")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(Caps, 104);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse() {
|
||||||
|
let elem: Element = "<c xmlns='http://jabber.org/protocol/caps' hash='sha-256' node='coucou' ver='K1Njy3HZBThlo4moOD5gBGhn0U0oK7/CbfLlIUDi6o4='/>".parse().unwrap();
|
||||||
|
let caps = Caps::try_from(elem).unwrap();
|
||||||
|
assert_eq!(caps.node, String::from("coucou"));
|
||||||
|
assert_eq!(caps.hash.algo, Algo::Sha_256);
|
||||||
|
assert_eq!(
|
||||||
|
caps.hash.hash,
|
||||||
|
base64::decode("K1Njy3HZBThlo4moOD5gBGhn0U0oK7/CbfLlIUDi6o4=").unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "disable-validation"))]
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_child() {
|
||||||
|
let elem: Element = "<c xmlns='http://jabber.org/protocol/caps'><hash xmlns='urn:xmpp:hashes:2' algo='sha-256'>K1Njy3HZBThlo4moOD5gBGhn0U0oK7/CbfLlIUDi6o4=</hash></c>".parse().unwrap();
|
||||||
|
let error = Caps::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Unknown child in caps element.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_simple() {
|
||||||
|
let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='client' type='pc'/><feature var='http://jabber.org/protocol/disco#info'/></query>".parse().unwrap();
|
||||||
|
let disco = DiscoInfoResult::try_from(elem).unwrap();
|
||||||
|
let caps = caps::compute_disco(&disco);
|
||||||
|
assert_eq!(caps.len(), 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_xep_5_2() {
|
||||||
|
let elem: Element = r#"
|
||||||
|
<query xmlns='http://jabber.org/protocol/disco#info'
|
||||||
|
node='http://psi-im.org#q07IKJEyjvHSyhy//CH0CxmKi8w='>
|
||||||
|
<identity category='client' name='Exodus 0.9.1' type='pc'/>
|
||||||
|
<feature var='http://jabber.org/protocol/caps'/>
|
||||||
|
<feature var='http://jabber.org/protocol/disco#info'/>
|
||||||
|
<feature var='http://jabber.org/protocol/disco#items'/>
|
||||||
|
<feature var='http://jabber.org/protocol/muc'/>
|
||||||
|
</query>
|
||||||
|
"#
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let data = b"client/pc//Exodus 0.9.1<http://jabber.org/protocol/caps<http://jabber.org/protocol/disco#info<http://jabber.org/protocol/disco#items<http://jabber.org/protocol/muc<";
|
||||||
|
let mut expected = Vec::with_capacity(data.len());
|
||||||
|
expected.extend_from_slice(data);
|
||||||
|
let disco = DiscoInfoResult::try_from(elem).unwrap();
|
||||||
|
let caps = caps::compute_disco(&disco);
|
||||||
|
assert_eq!(caps, expected);
|
||||||
|
|
||||||
|
let sha_1 = caps::hash_caps(&caps, Algo::Sha_1).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
sha_1.hash,
|
||||||
|
base64::decode("QgayPKawpkPSDYmwT/WM94uAlu0=").unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_xep_5_3() {
|
||||||
|
let elem: Element = r#"
|
||||||
|
<query xmlns='http://jabber.org/protocol/disco#info'
|
||||||
|
node='http://psi-im.org#q07IKJEyjvHSyhy//CH0CxmKi8w='>
|
||||||
|
<identity xml:lang='en' category='client' name='Psi 0.11' type='pc'/>
|
||||||
|
<identity xml:lang='el' category='client' name='Ψ 0.11' type='pc'/>
|
||||||
|
<feature var='http://jabber.org/protocol/caps'/>
|
||||||
|
<feature var='http://jabber.org/protocol/disco#info'/>
|
||||||
|
<feature var='http://jabber.org/protocol/disco#items'/>
|
||||||
|
<feature var='http://jabber.org/protocol/muc'/>
|
||||||
|
<x xmlns='jabber:x:data' type='result'>
|
||||||
|
<field var='FORM_TYPE' type='hidden'>
|
||||||
|
<value>urn:xmpp:dataforms:softwareinfo</value>
|
||||||
|
</field>
|
||||||
|
<field var='ip_version'>
|
||||||
|
<value>ipv4</value>
|
||||||
|
<value>ipv6</value>
|
||||||
|
</field>
|
||||||
|
<field var='os'>
|
||||||
|
<value>Mac</value>
|
||||||
|
</field>
|
||||||
|
<field var='os_version'>
|
||||||
|
<value>10.5.1</value>
|
||||||
|
</field>
|
||||||
|
<field var='software'>
|
||||||
|
<value>Psi</value>
|
||||||
|
</field>
|
||||||
|
<field var='software_version'>
|
||||||
|
<value>0.11</value>
|
||||||
|
</field>
|
||||||
|
</x>
|
||||||
|
</query>
|
||||||
|
"#
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let expected = b"client/pc/el/\xce\xa8 0.11<client/pc/en/Psi 0.11<http://jabber.org/protocol/caps<http://jabber.org/protocol/disco#info<http://jabber.org/protocol/disco#items<http://jabber.org/protocol/muc<urn:xmpp:dataforms:softwareinfo<ip_version<ipv4<ipv6<os<Mac<os_version<10.5.1<software<Psi<software_version<0.11<".to_vec();
|
||||||
|
let disco = DiscoInfoResult::try_from(elem).unwrap();
|
||||||
|
let caps = caps::compute_disco(&disco);
|
||||||
|
assert_eq!(caps, expected);
|
||||||
|
|
||||||
|
let sha_1 = caps::hash_caps(&caps, Algo::Sha_1).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
sha_1.hash,
|
||||||
|
base64::decode("q07IKJEyjvHSyhy//CH0CxmKi8w=").unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,165 @@
|
||||||
|
// 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());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialize_received() {
|
||||||
|
let reference: 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 elem: Element = "<forwarded xmlns='urn:xmpp:forward:0'><message xmlns='jabber:client' to='juliet@capulet.example/balcony' from='romeo@montague.example/home'/></forwarded>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let forwarded = Forwarded::try_from(elem).unwrap();
|
||||||
|
|
||||||
|
let received = Received {
|
||||||
|
forwarded: forwarded,
|
||||||
|
};
|
||||||
|
|
||||||
|
let serialized: Element = received.into();
|
||||||
|
assert_eq!(serialized, reference);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialize_sent() {
|
||||||
|
let reference: 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 elem: Element = "<forwarded xmlns='urn:xmpp:forward:0'><message xmlns='jabber:client' to='juliet@capulet.example/balcony' from='romeo@montague.example/home'/></forwarded>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let forwarded = Forwarded::try_from(elem).unwrap();
|
||||||
|
|
||||||
|
let sent = Sent {
|
||||||
|
forwarded: forwarded,
|
||||||
|
};
|
||||||
|
|
||||||
|
let serialized: Element = sent.into();
|
||||||
|
assert_eq!(serialized, reference);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,297 @@
|
||||||
|
// 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, IqSetPayload};
|
||||||
|
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::ns;
|
||||||
|
use crate::Element;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
#[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");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialize_item() {
|
||||||
|
let reference: Element = "<item xmlns='urn:xmpp:saslcert:1'><name>Mobile Client</name><x509cert>AAAA</x509cert></item>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let item = Item {
|
||||||
|
name: Name::from_str("Mobile Client").unwrap(),
|
||||||
|
cert: Cert {
|
||||||
|
data: b"\0\0\0".to_vec(),
|
||||||
|
},
|
||||||
|
no_cert_management: false,
|
||||||
|
users: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let serialized: Element = item.into();
|
||||||
|
assert_eq!(serialized, reference);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialize_append() {
|
||||||
|
let reference: Element = "<append xmlns='urn:xmpp:saslcert:1'><name>Mobile Client</name><x509cert>AAAA</x509cert></append>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let append = Append {
|
||||||
|
name: Name::from_str("Mobile Client").unwrap(),
|
||||||
|
cert: Cert {
|
||||||
|
data: b"\0\0\0".to_vec(),
|
||||||
|
},
|
||||||
|
no_cert_management: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let serialized: Element = append.into();
|
||||||
|
assert_eq!(serialized, reference);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialize_disable() {
|
||||||
|
let reference: Element =
|
||||||
|
"<disable xmlns='urn:xmpp:saslcert:1'><name>Mobile Client</name></disable>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let disable = Disable {
|
||||||
|
name: Name::from_str("Mobile Client").unwrap(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let serialized: Element = disable.into();
|
||||||
|
assert_eq!(serialized, reference);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialize_revoke() {
|
||||||
|
let reference: Element =
|
||||||
|
"<revoke xmlns='urn:xmpp:saslcert:1'><name>Mobile Client</name></revoke>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let revoke = Revoke {
|
||||||
|
name: Name::from_str("Mobile Client").unwrap(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let serialized: Element = revoke.into();
|
||||||
|
assert_eq!(serialized, reference);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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::ns;
|
||||||
|
use crate::util::error::Error;
|
||||||
|
use crate::Element;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(ChatState, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_simple() {
|
||||||
|
let elem: Element = "<active xmlns='http://jabber.org/protocol/chatstates'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
ChatState::try_from(elem).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid() {
|
||||||
|
let elem: Element = "<coucou xmlns='http://jabber.org/protocol/chatstates'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = ChatState::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "This is not a chatstate element.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "disable-validation"))]
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_child() {
|
||||||
|
let elem: Element = "<gone xmlns='http://jabber.org/protocol/chatstates'><coucou/></gone>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = ChatState::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Unknown child in chatstate element.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "disable-validation"))]
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_attribute() {
|
||||||
|
let elem: Element = "<inactive xmlns='http://jabber.org/protocol/chatstates' coucou=''/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = ChatState::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Unknown attribute in chatstate element.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialise() {
|
||||||
|
let chatstate = ChatState::Active;
|
||||||
|
let elem: Element = chatstate.into();
|
||||||
|
assert!(elem.is("active", ns::CHATSTATES));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
// Copyright (c) 2018 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
use crate::util::helpers::PlainText;
|
||||||
|
use digest::Digest;
|
||||||
|
use sha1::Sha1;
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// The main authentication mechanism for components.
|
||||||
|
#[derive(Default)]
|
||||||
|
Handshake, "handshake", COMPONENT,
|
||||||
|
text: (
|
||||||
|
/// If Some, contains the hex-encoded SHA-1 of the concatenation of the
|
||||||
|
/// stream id and the password, and is used to authenticate against the
|
||||||
|
/// server.
|
||||||
|
///
|
||||||
|
/// If None, it is the successful reply from the server, the stream is now
|
||||||
|
/// fully established and both sides can now exchange stanzas.
|
||||||
|
data: PlainText<Option<String>>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
impl Handshake {
|
||||||
|
/// Creates a successful reply from a server.
|
||||||
|
pub fn new() -> Handshake {
|
||||||
|
Handshake::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates an authentication request from the component.
|
||||||
|
pub fn from_password_and_stream_id(password: &str, stream_id: &str) -> Handshake {
|
||||||
|
let input = String::from(stream_id) + password;
|
||||||
|
let hash = Sha1::digest(input.as_bytes());
|
||||||
|
let content = format!("{:x}", hash);
|
||||||
|
Handshake {
|
||||||
|
data: Some(content),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::Element;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "32")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(Handshake, 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "64")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(Handshake, 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_simple() {
|
||||||
|
let elem: Element = "<handshake xmlns='jabber:component:accept'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let handshake = Handshake::try_from(elem).unwrap();
|
||||||
|
assert_eq!(handshake.data, None);
|
||||||
|
|
||||||
|
let elem: Element = "<handshake xmlns='jabber:component:accept'>Coucou</handshake>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let handshake = Handshake::try_from(elem).unwrap();
|
||||||
|
assert_eq!(handshake.data, Some(String::from("Coucou")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_constructors() {
|
||||||
|
let handshake = Handshake::new();
|
||||||
|
assert_eq!(handshake.data, None);
|
||||||
|
|
||||||
|
let handshake = Handshake::from_password_and_stream_id("123456", "sid");
|
||||||
|
assert_eq!(
|
||||||
|
handshake.data,
|
||||||
|
Some(String::from("9accec263ab84a43c6037ccf7cd48cb1d3f6df8e"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
// 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::ns;
|
||||||
|
use crate::Element;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(Feature, 0);
|
||||||
|
assert_size!(Inactive, 0);
|
||||||
|
assert_size!(Active, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parsing() {
|
||||||
|
let elem: Element = "<csi xmlns='urn:xmpp:csi:0'/>".parse().unwrap();
|
||||||
|
Feature::try_from(elem).unwrap();
|
||||||
|
|
||||||
|
let elem: Element = "<inactive xmlns='urn:xmpp:csi:0'/>".parse().unwrap();
|
||||||
|
Inactive::try_from(elem).unwrap();
|
||||||
|
|
||||||
|
let elem: Element = "<active xmlns='urn:xmpp:csi:0'/>".parse().unwrap();
|
||||||
|
Active::try_from(elem).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn serialising() {
|
||||||
|
let elem: Element = Feature.into();
|
||||||
|
assert!(elem.is("csi", ns::CSI));
|
||||||
|
|
||||||
|
let elem: Element = Inactive.into();
|
||||||
|
assert!(elem.is("inactive", ns::CSI));
|
||||||
|
|
||||||
|
let elem: Element = Active.into();
|
||||||
|
assert!(elem.is("active", ns::CSI));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,385 @@
|
||||||
|
// 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::media_element::MediaElement;
|
||||||
|
use crate::ns;
|
||||||
|
use crate::util::error::Error;
|
||||||
|
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, PartialEq)]
|
||||||
|
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::DATA_FORMS)
|
||||||
|
.attr("var", field.var)
|
||||||
|
.attr("type", field.type_)
|
||||||
|
.attr("label", field.label)
|
||||||
|
.append_all(if field.required {
|
||||||
|
Some(Element::builder("required", 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::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, PartialEq)]
|
||||||
|
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::DATA_FORMS)
|
||||||
|
.attr("type", form.type_)
|
||||||
|
.append_all(
|
||||||
|
form.title
|
||||||
|
.map(|title| Element::builder("title", ns::DATA_FORMS).append(title)),
|
||||||
|
)
|
||||||
|
.append_all(
|
||||||
|
form.instructions
|
||||||
|
.map(|text| Element::builder("instructions", ns::DATA_FORMS).append(text)),
|
||||||
|
)
|
||||||
|
.append_all(form.form_type.map(|form_type| {
|
||||||
|
Element::builder("field", ns::DATA_FORMS)
|
||||||
|
.attr("var", "FORM_TYPE")
|
||||||
|
.attr("type", "hidden")
|
||||||
|
.append(Element::builder("value", ns::DATA_FORMS).append(form_type))
|
||||||
|
}))
|
||||||
|
.append_all(form.fields.iter().cloned().map(Element::from))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "32")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(Option_, 24);
|
||||||
|
assert_size!(FieldType, 1);
|
||||||
|
assert_size!(Field, 64);
|
||||||
|
assert_size!(DataFormType, 1);
|
||||||
|
assert_size!(DataForm, 52);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "64")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(Option_, 48);
|
||||||
|
assert_size!(FieldType, 1);
|
||||||
|
assert_size!(Field, 128);
|
||||||
|
assert_size!(DataFormType, 1);
|
||||||
|
assert_size!(DataForm, 104);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_simple() {
|
||||||
|
let elem: Element = "<x xmlns='jabber:x:data' type='result'/>".parse().unwrap();
|
||||||
|
let form = DataForm::try_from(elem).unwrap();
|
||||||
|
assert_eq!(form.type_, DataFormType::Result_);
|
||||||
|
assert!(form.form_type.is_none());
|
||||||
|
assert!(form.fields.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid() {
|
||||||
|
let elem: Element = "<x xmlns='jabber:x:data'/>".parse().unwrap();
|
||||||
|
let error = DataForm::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Required attribute 'type' missing.");
|
||||||
|
|
||||||
|
let elem: Element = "<x xmlns='jabber:x:data' type='coucou'/>".parse().unwrap();
|
||||||
|
let error = DataForm::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Unknown value for 'type' attribute.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_wrong_child() {
|
||||||
|
let elem: Element = "<x xmlns='jabber:x:data' type='cancel'><coucou/></x>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = DataForm::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Unknown child in data form element.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn option() {
|
||||||
|
let elem: Element =
|
||||||
|
"<option xmlns='jabber:x:data' label='Coucou !'><value>coucou</value></option>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let option = Option_::try_from(elem).unwrap();
|
||||||
|
assert_eq!(&option.label.unwrap(), "Coucou !");
|
||||||
|
assert_eq!(&option.value, "coucou");
|
||||||
|
|
||||||
|
let elem: Element = "<option xmlns='jabber:x:data' label='Coucou !'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = Option_::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Missing child value in option element.");
|
||||||
|
|
||||||
|
let elem: Element = "<option xmlns='jabber:x:data' label='Coucou !'><value>coucou</value><value>error</value></option>".parse().unwrap();
|
||||||
|
let error = Option_::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
message,
|
||||||
|
"Element option must not have more than one value child."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,137 @@
|
||||||
|
// 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(pub 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};
|
||||||
|
|
||||||
|
// 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.to_string(), "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.to_string(), "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.to_string(), "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.to_string(), "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.to_string(), "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.to_string(), "premature end of input");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialise() {
|
||||||
|
let date =
|
||||||
|
DateTime(ChronoDateTime::parse_from_rfc3339("2017-05-21T20:19:55+01:00").unwrap());
|
||||||
|
let attr = date.into_attribute_value();
|
||||||
|
assert_eq!(attr, Some(String::from("2017-05-21T20:19:55+01:00")));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,119 @@
|
||||||
|
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
use crate::date::DateTime;
|
||||||
|
use crate::message::MessagePayload;
|
||||||
|
use crate::presence::PresencePayload;
|
||||||
|
use crate::util::helpers::PlainText;
|
||||||
|
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 jid::BareJid;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
#[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.unwrap(), BareJid::domain("capulet.com"));
|
||||||
|
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::Bare(BareJid::new("juliet", "example.org"))),
|
||||||
|
stamp: DateTime::from_str("2002-09-10T23:08:25Z").unwrap(),
|
||||||
|
data: Some(String::from("Reason")),
|
||||||
|
};
|
||||||
|
let elem2 = delay.into();
|
||||||
|
assert_eq!(elem, elem2);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,452 @@
|
||||||
|
// 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::iq::{IqGetPayload, IqResultPayload};
|
||||||
|
use crate::ns;
|
||||||
|
use crate::util::error::Error;
|
||||||
|
use crate::Element;
|
||||||
|
use jid::Jid;
|
||||||
|
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!(
|
||||||
|
#[derive(Eq, Hash)]
|
||||||
|
/// Structure representing a `<feature xmlns='http://jabber.org/protocol/disco#info'/>` element.
|
||||||
|
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::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 jid::BareJid;
|
||||||
|
|
||||||
|
#[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_eq!(elem1, 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, BareJid::domain("component"));
|
||||||
|
assert_eq!(query.items[0].node, None);
|
||||||
|
assert_eq!(query.items[0].name, None);
|
||||||
|
assert_eq!(query.items[1].jid, BareJid::domain("component2"));
|
||||||
|
assert_eq!(query.items[1].node, Some(String::from("test")));
|
||||||
|
assert_eq!(query.items[1].name, Some(String::from("A component")));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,481 @@
|
||||||
|
// 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 crate::util::error::Error;
|
||||||
|
use blake2::VarBlake2b;
|
||||||
|
use digest::{Digest, Update, 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>, Error> {
|
||||||
|
for extension in extensions {
|
||||||
|
if extension.form_type.is_none() {
|
||||||
|
return Err(Error::ParseError("Missing FORM_TYPE in extension."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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>, Error> {
|
||||||
|
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, Error> {
|
||||||
|
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.update(data);
|
||||||
|
let mut vec = Vec::with_capacity(32);
|
||||||
|
hasher.finalize_variable(|slice| vec.extend_from_slice(slice));
|
||||||
|
vec
|
||||||
|
}
|
||||||
|
Algo::Blake2b_512 => {
|
||||||
|
let mut hasher = VarBlake2b::new(64).unwrap();
|
||||||
|
hasher.update(data);
|
||||||
|
let mut vec = Vec::with_capacity(64);
|
||||||
|
hasher.finalize_variable(|slice| vec.extend_from_slice(slice));
|
||||||
|
vec
|
||||||
|
}
|
||||||
|
Algo::Sha_1 => return Err(Error::ParseError("Disabled algorithm sha-1: unsafe.")),
|
||||||
|
Algo::Unknown(_algo) => return Err(Error::ParseError("Unknown algorithm in ecaps2.")),
|
||||||
|
},
|
||||||
|
algo,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper function to create the query for the disco#info corresponding to an
|
||||||
|
/// ecaps2 hash.
|
||||||
|
pub fn query_ecaps2(hash: Hash) -> DiscoInfoQuery {
|
||||||
|
DiscoInfoQuery {
|
||||||
|
node: Some(format!(
|
||||||
|
"{}#{}.{}",
|
||||||
|
ns::ECAPS2,
|
||||||
|
String::from(hash.algo),
|
||||||
|
base64::encode(&hash.hash)
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::util::error::Error;
|
||||||
|
use crate::Element;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "32")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(ECaps2, 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "64")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(ECaps2, 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse() {
|
||||||
|
let elem: Element = "<c xmlns='urn:xmpp:caps'><hash xmlns='urn:xmpp:hashes:2' algo='sha-256'>K1Njy3HZBThlo4moOD5gBGhn0U0oK7/CbfLlIUDi6o4=</hash><hash xmlns='urn:xmpp:hashes:2' algo='sha3-256'>+sDTQqBmX6iG/X3zjt06fjZMBBqL/723knFIyRf0sg8=</hash></c>".parse().unwrap();
|
||||||
|
let ecaps2 = ECaps2::try_from(elem).unwrap();
|
||||||
|
assert_eq!(ecaps2.hashes.len(), 2);
|
||||||
|
assert_eq!(ecaps2.hashes[0].algo, Algo::Sha_256);
|
||||||
|
assert_eq!(
|
||||||
|
ecaps2.hashes[0].hash,
|
||||||
|
base64::decode("K1Njy3HZBThlo4moOD5gBGhn0U0oK7/CbfLlIUDi6o4=").unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(ecaps2.hashes[1].algo, Algo::Sha3_256);
|
||||||
|
assert_eq!(
|
||||||
|
ecaps2.hashes[1].hash,
|
||||||
|
base64::decode("+sDTQqBmX6iG/X3zjt06fjZMBBqL/723knFIyRf0sg8=").unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_child() {
|
||||||
|
let elem: Element = "<c xmlns='urn:xmpp:caps'><hash xmlns='urn:xmpp:hashes:2' algo='sha-256'>K1Njy3HZBThlo4moOD5gBGhn0U0oK7/CbfLlIUDi6o4=</hash><hash xmlns='urn:xmpp:hashes:1' algo='sha3-256'>+sDTQqBmX6iG/X3zjt06fjZMBBqL/723knFIyRf0sg8=</hash></c>".parse().unwrap();
|
||||||
|
let error = ECaps2::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Unknown child in c element.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_simple() {
|
||||||
|
let elem: Element = "<query xmlns='http://jabber.org/protocol/disco#info'><identity category='client' type='pc'/><feature var='http://jabber.org/protocol/disco#info'/></query>".parse().unwrap();
|
||||||
|
let disco = DiscoInfoResult::try_from(elem).unwrap();
|
||||||
|
let ecaps2 = compute_disco(&disco).unwrap();
|
||||||
|
assert_eq!(ecaps2.len(), 54);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_xep_ex1() {
|
||||||
|
let elem: Element = r#"
|
||||||
|
<query xmlns="http://jabber.org/protocol/disco#info">
|
||||||
|
<identity category="client" name="BombusMod" type="mobile"/>
|
||||||
|
<feature var="http://jabber.org/protocol/si"/>
|
||||||
|
<feature var="http://jabber.org/protocol/bytestreams"/>
|
||||||
|
<feature var="http://jabber.org/protocol/chatstates"/>
|
||||||
|
<feature var="http://jabber.org/protocol/disco#info"/>
|
||||||
|
<feature var="http://jabber.org/protocol/disco#items"/>
|
||||||
|
<feature var="urn:xmpp:ping"/>
|
||||||
|
<feature var="jabber:iq:time"/>
|
||||||
|
<feature var="jabber:iq:privacy"/>
|
||||||
|
<feature var="jabber:iq:version"/>
|
||||||
|
<feature var="http://jabber.org/protocol/rosterx"/>
|
||||||
|
<feature var="urn:xmpp:time"/>
|
||||||
|
<feature var="jabber:x:oob"/>
|
||||||
|
<feature var="http://jabber.org/protocol/ibb"/>
|
||||||
|
<feature var="http://jabber.org/protocol/si/profile/file-transfer"/>
|
||||||
|
<feature var="urn:xmpp:receipts"/>
|
||||||
|
<feature var="jabber:iq:roster"/>
|
||||||
|
<feature var="jabber:iq:last"/>
|
||||||
|
</query>
|
||||||
|
"#
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let expected = vec![
|
||||||
|
104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112,
|
||||||
|
114, 111, 116, 111, 99, 111, 108, 47, 98, 121, 116, 101, 115, 116, 114, 101, 97, 109,
|
||||||
|
115, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103,
|
||||||
|
47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 99, 104, 97, 116, 115, 116, 97, 116,
|
||||||
|
101, 115, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114,
|
||||||
|
103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 100, 105, 115, 99, 111, 35, 105,
|
||||||
|
110, 102, 111, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111,
|
||||||
|
114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 100, 105, 115, 99, 111, 35,
|
||||||
|
105, 116, 101, 109, 115, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114,
|
||||||
|
46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 105, 98, 98, 31, 104,
|
||||||
|
116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114,
|
||||||
|
111, 116, 111, 99, 111, 108, 47, 114, 111, 115, 116, 101, 114, 120, 31, 104, 116, 116,
|
||||||
|
112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116,
|
||||||
|
111, 99, 111, 108, 47, 115, 105, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98,
|
||||||
|
101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 115, 105,
|
||||||
|
47, 112, 114, 111, 102, 105, 108, 101, 47, 102, 105, 108, 101, 45, 116, 114, 97, 110,
|
||||||
|
115, 102, 101, 114, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 108, 97, 115, 116,
|
||||||
|
31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 112, 114, 105, 118, 97, 99, 121, 31,
|
||||||
|
106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 114, 111, 115, 116, 101, 114, 31, 106, 97,
|
||||||
|
98, 98, 101, 114, 58, 105, 113, 58, 116, 105, 109, 101, 31, 106, 97, 98, 98, 101, 114,
|
||||||
|
58, 105, 113, 58, 118, 101, 114, 115, 105, 111, 110, 31, 106, 97, 98, 98, 101, 114, 58,
|
||||||
|
120, 58, 111, 111, 98, 31, 117, 114, 110, 58, 120, 109, 112, 112, 58, 112, 105, 110,
|
||||||
|
103, 31, 117, 114, 110, 58, 120, 109, 112, 112, 58, 114, 101, 99, 101, 105, 112, 116,
|
||||||
|
115, 31, 117, 114, 110, 58, 120, 109, 112, 112, 58, 116, 105, 109, 101, 31, 28, 99,
|
||||||
|
108, 105, 101, 110, 116, 31, 109, 111, 98, 105, 108, 101, 31, 31, 66, 111, 109, 98,
|
||||||
|
117, 115, 77, 111, 100, 31, 30, 28, 28,
|
||||||
|
];
|
||||||
|
let disco = DiscoInfoResult::try_from(elem).unwrap();
|
||||||
|
let ecaps2 = compute_disco(&disco).unwrap();
|
||||||
|
assert_eq!(ecaps2.len(), 0x1d9);
|
||||||
|
assert_eq!(ecaps2, expected);
|
||||||
|
|
||||||
|
let sha_256 = hash_ecaps2(&ecaps2, Algo::Sha_256).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
sha_256.hash,
|
||||||
|
base64::decode("kzBZbkqJ3ADrj7v08reD1qcWUwNGHaidNUgD7nHpiw8=").unwrap()
|
||||||
|
);
|
||||||
|
let sha3_256 = hash_ecaps2(&ecaps2, Algo::Sha3_256).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
sha3_256.hash,
|
||||||
|
base64::decode("79mdYAfU9rEdTOcWDO7UEAt6E56SUzk/g6TnqUeuD9Q=").unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_xep_ex2() {
|
||||||
|
let elem: Element = r#"
|
||||||
|
<query xmlns="http://jabber.org/protocol/disco#info">
|
||||||
|
<identity category="client" name="Tkabber" type="pc" xml:lang="en"/>
|
||||||
|
<identity category="client" name="Ткаббер" type="pc" xml:lang="ru"/>
|
||||||
|
<feature var="games:board"/>
|
||||||
|
<feature var="http://jabber.org/protocol/activity"/>
|
||||||
|
<feature var="http://jabber.org/protocol/activity+notify"/>
|
||||||
|
<feature var="http://jabber.org/protocol/bytestreams"/>
|
||||||
|
<feature var="http://jabber.org/protocol/chatstates"/>
|
||||||
|
<feature var="http://jabber.org/protocol/commands"/>
|
||||||
|
<feature var="http://jabber.org/protocol/disco#info"/>
|
||||||
|
<feature var="http://jabber.org/protocol/disco#items"/>
|
||||||
|
<feature var="http://jabber.org/protocol/evil"/>
|
||||||
|
<feature var="http://jabber.org/protocol/feature-neg"/>
|
||||||
|
<feature var="http://jabber.org/protocol/geoloc"/>
|
||||||
|
<feature var="http://jabber.org/protocol/geoloc+notify"/>
|
||||||
|
<feature var="http://jabber.org/protocol/ibb"/>
|
||||||
|
<feature var="http://jabber.org/protocol/iqibb"/>
|
||||||
|
<feature var="http://jabber.org/protocol/mood"/>
|
||||||
|
<feature var="http://jabber.org/protocol/mood+notify"/>
|
||||||
|
<feature var="http://jabber.org/protocol/rosterx"/>
|
||||||
|
<feature var="http://jabber.org/protocol/si"/>
|
||||||
|
<feature var="http://jabber.org/protocol/si/profile/file-transfer"/>
|
||||||
|
<feature var="http://jabber.org/protocol/tune"/>
|
||||||
|
<feature var="http://www.facebook.com/xmpp/messages"/>
|
||||||
|
<feature var="http://www.xmpp.org/extensions/xep-0084.html#ns-metadata+notify"/>
|
||||||
|
<feature var="jabber:iq:avatar"/>
|
||||||
|
<feature var="jabber:iq:browse"/>
|
||||||
|
<feature var="jabber:iq:dtcp"/>
|
||||||
|
<feature var="jabber:iq:filexfer"/>
|
||||||
|
<feature var="jabber:iq:ibb"/>
|
||||||
|
<feature var="jabber:iq:inband"/>
|
||||||
|
<feature var="jabber:iq:jidlink"/>
|
||||||
|
<feature var="jabber:iq:last"/>
|
||||||
|
<feature var="jabber:iq:oob"/>
|
||||||
|
<feature var="jabber:iq:privacy"/>
|
||||||
|
<feature var="jabber:iq:roster"/>
|
||||||
|
<feature var="jabber:iq:time"/>
|
||||||
|
<feature var="jabber:iq:version"/>
|
||||||
|
<feature var="jabber:x:data"/>
|
||||||
|
<feature var="jabber:x:event"/>
|
||||||
|
<feature var="jabber:x:oob"/>
|
||||||
|
<feature var="urn:xmpp:avatar:metadata+notify"/>
|
||||||
|
<feature var="urn:xmpp:ping"/>
|
||||||
|
<feature var="urn:xmpp:receipts"/>
|
||||||
|
<feature var="urn:xmpp:time"/>
|
||||||
|
<x xmlns="jabber:x:data" type="result">
|
||||||
|
<field type="hidden" var="FORM_TYPE">
|
||||||
|
<value>urn:xmpp:dataforms:softwareinfo</value>
|
||||||
|
</field>
|
||||||
|
<field var="software">
|
||||||
|
<value>Tkabber</value>
|
||||||
|
</field>
|
||||||
|
<field var="software_version">
|
||||||
|
<value>0.11.1-svn-20111216-mod (Tcl/Tk 8.6b2)</value>
|
||||||
|
</field>
|
||||||
|
<field var="os">
|
||||||
|
<value>Windows</value>
|
||||||
|
</field>
|
||||||
|
<field var="os_version">
|
||||||
|
<value>XP</value>
|
||||||
|
</field>
|
||||||
|
</x>
|
||||||
|
</query>
|
||||||
|
"#
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let expected = vec![
|
||||||
|
103, 97, 109, 101, 115, 58, 98, 111, 97, 114, 100, 31, 104, 116, 116, 112, 58, 47, 47,
|
||||||
|
106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111,
|
||||||
|
108, 47, 97, 99, 116, 105, 118, 105, 116, 121, 31, 104, 116, 116, 112, 58, 47, 47, 106,
|
||||||
|
97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47,
|
||||||
|
97, 99, 116, 105, 118, 105, 116, 121, 43, 110, 111, 116, 105, 102, 121, 31, 104, 116,
|
||||||
|
116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111,
|
||||||
|
116, 111, 99, 111, 108, 47, 98, 121, 116, 101, 115, 116, 114, 101, 97, 109, 115, 31,
|
||||||
|
104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112,
|
||||||
|
114, 111, 116, 111, 99, 111, 108, 47, 99, 104, 97, 116, 115, 116, 97, 116, 101, 115,
|
||||||
|
31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47,
|
||||||
|
112, 114, 111, 116, 111, 99, 111, 108, 47, 99, 111, 109, 109, 97, 110, 100, 115, 31,
|
||||||
|
104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112,
|
||||||
|
114, 111, 116, 111, 99, 111, 108, 47, 100, 105, 115, 99, 111, 35, 105, 110, 102, 111,
|
||||||
|
31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47,
|
||||||
|
112, 114, 111, 116, 111, 99, 111, 108, 47, 100, 105, 115, 99, 111, 35, 105, 116, 101,
|
||||||
|
109, 115, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114,
|
||||||
|
103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 101, 118, 105, 108, 31, 104, 116,
|
||||||
|
116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111,
|
||||||
|
116, 111, 99, 111, 108, 47, 102, 101, 97, 116, 117, 114, 101, 45, 110, 101, 103, 31,
|
||||||
|
104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112,
|
||||||
|
114, 111, 116, 111, 99, 111, 108, 47, 103, 101, 111, 108, 111, 99, 31, 104, 116, 116,
|
||||||
|
112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116,
|
||||||
|
111, 99, 111, 108, 47, 103, 101, 111, 108, 111, 99, 43, 110, 111, 116, 105, 102, 121,
|
||||||
|
31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47,
|
||||||
|
112, 114, 111, 116, 111, 99, 111, 108, 47, 105, 98, 98, 31, 104, 116, 116, 112, 58, 47,
|
||||||
|
47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111,
|
||||||
|
108, 47, 105, 113, 105, 98, 98, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98,
|
||||||
|
101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 109, 111,
|
||||||
|
111, 100, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114,
|
||||||
|
103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 109, 111, 111, 100, 43, 110, 111,
|
||||||
|
116, 105, 102, 121, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46,
|
||||||
|
111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 114, 111, 115, 116, 101,
|
||||||
|
114, 120, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114,
|
||||||
|
103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 115, 105, 31, 104, 116, 116, 112,
|
||||||
|
58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111,
|
||||||
|
99, 111, 108, 47, 115, 105, 47, 112, 114, 111, 102, 105, 108, 101, 47, 102, 105, 108,
|
||||||
|
101, 45, 116, 114, 97, 110, 115, 102, 101, 114, 31, 104, 116, 116, 112, 58, 47, 47,
|
||||||
|
106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111,
|
||||||
|
108, 47, 116, 117, 110, 101, 31, 104, 116, 116, 112, 58, 47, 47, 119, 119, 119, 46,
|
||||||
|
102, 97, 99, 101, 98, 111, 111, 107, 46, 99, 111, 109, 47, 120, 109, 112, 112, 47, 109,
|
||||||
|
101, 115, 115, 97, 103, 101, 115, 31, 104, 116, 116, 112, 58, 47, 47, 119, 119, 119,
|
||||||
|
46, 120, 109, 112, 112, 46, 111, 114, 103, 47, 101, 120, 116, 101, 110, 115, 105, 111,
|
||||||
|
110, 115, 47, 120, 101, 112, 45, 48, 48, 56, 52, 46, 104, 116, 109, 108, 35, 110, 115,
|
||||||
|
45, 109, 101, 116, 97, 100, 97, 116, 97, 43, 110, 111, 116, 105, 102, 121, 31, 106, 97,
|
||||||
|
98, 98, 101, 114, 58, 105, 113, 58, 97, 118, 97, 116, 97, 114, 31, 106, 97, 98, 98,
|
||||||
|
101, 114, 58, 105, 113, 58, 98, 114, 111, 119, 115, 101, 31, 106, 97, 98, 98, 101, 114,
|
||||||
|
58, 105, 113, 58, 100, 116, 99, 112, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58,
|
||||||
|
102, 105, 108, 101, 120, 102, 101, 114, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113,
|
||||||
|
58, 105, 98, 98, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 105, 110, 98, 97,
|
||||||
|
110, 100, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 106, 105, 100, 108, 105,
|
||||||
|
110, 107, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 108, 97, 115, 116, 31, 106,
|
||||||
|
97, 98, 98, 101, 114, 58, 105, 113, 58, 111, 111, 98, 31, 106, 97, 98, 98, 101, 114,
|
||||||
|
58, 105, 113, 58, 112, 114, 105, 118, 97, 99, 121, 31, 106, 97, 98, 98, 101, 114, 58,
|
||||||
|
105, 113, 58, 114, 111, 115, 116, 101, 114, 31, 106, 97, 98, 98, 101, 114, 58, 105,
|
||||||
|
113, 58, 116, 105, 109, 101, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 118, 101,
|
||||||
|
114, 115, 105, 111, 110, 31, 106, 97, 98, 98, 101, 114, 58, 120, 58, 100, 97, 116, 97,
|
||||||
|
31, 106, 97, 98, 98, 101, 114, 58, 120, 58, 101, 118, 101, 110, 116, 31, 106, 97, 98,
|
||||||
|
98, 101, 114, 58, 120, 58, 111, 111, 98, 31, 117, 114, 110, 58, 120, 109, 112, 112, 58,
|
||||||
|
97, 118, 97, 116, 97, 114, 58, 109, 101, 116, 97, 100, 97, 116, 97, 43, 110, 111, 116,
|
||||||
|
105, 102, 121, 31, 117, 114, 110, 58, 120, 109, 112, 112, 58, 112, 105, 110, 103, 31,
|
||||||
|
117, 114, 110, 58, 120, 109, 112, 112, 58, 114, 101, 99, 101, 105, 112, 116, 115, 31,
|
||||||
|
117, 114, 110, 58, 120, 109, 112, 112, 58, 116, 105, 109, 101, 31, 28, 99, 108, 105,
|
||||||
|
101, 110, 116, 31, 112, 99, 31, 101, 110, 31, 84, 107, 97, 98, 98, 101, 114, 31, 30,
|
||||||
|
99, 108, 105, 101, 110, 116, 31, 112, 99, 31, 114, 117, 31, 208, 162, 208, 186, 208,
|
||||||
|
176, 208, 177, 208, 177, 208, 181, 209, 128, 31, 30, 28, 70, 79, 82, 77, 95, 84, 89,
|
||||||
|
80, 69, 31, 117, 114, 110, 58, 120, 109, 112, 112, 58, 100, 97, 116, 97, 102, 111, 114,
|
||||||
|
109, 115, 58, 115, 111, 102, 116, 119, 97, 114, 101, 105, 110, 102, 111, 31, 30, 111,
|
||||||
|
115, 31, 87, 105, 110, 100, 111, 119, 115, 31, 30, 111, 115, 95, 118, 101, 114, 115,
|
||||||
|
105, 111, 110, 31, 88, 80, 31, 30, 115, 111, 102, 116, 119, 97, 114, 101, 31, 84, 107,
|
||||||
|
97, 98, 98, 101, 114, 31, 30, 115, 111, 102, 116, 119, 97, 114, 101, 95, 118, 101, 114,
|
||||||
|
115, 105, 111, 110, 31, 48, 46, 49, 49, 46, 49, 45, 115, 118, 110, 45, 50, 48, 49, 49,
|
||||||
|
49, 50, 49, 54, 45, 109, 111, 100, 32, 40, 84, 99, 108, 47, 84, 107, 32, 56, 46, 54,
|
||||||
|
98, 50, 41, 31, 30, 29, 28,
|
||||||
|
];
|
||||||
|
let disco = DiscoInfoResult::try_from(elem).unwrap();
|
||||||
|
let ecaps2 = compute_disco(&disco).unwrap();
|
||||||
|
assert_eq!(ecaps2.len(), 0x543);
|
||||||
|
assert_eq!(ecaps2, expected);
|
||||||
|
|
||||||
|
let sha_256 = hash_ecaps2(&ecaps2, Algo::Sha_256).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
sha_256.hash,
|
||||||
|
base64::decode("u79ZroNJbdSWhdSp311mddz44oHHPsEBntQ5b1jqBSY=").unwrap()
|
||||||
|
);
|
||||||
|
let sha3_256 = hash_ecaps2(&ecaps2, Algo::Sha3_256).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
sha3_256.hash,
|
||||||
|
base64::decode("XpUJzLAc93258sMECZ3FJpebkzuyNXDzRNwQog8eycg=").unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_blake2b_512() {
|
||||||
|
let hash = hash_ecaps2("abc".as_bytes(), Algo::Blake2b_512).unwrap();
|
||||||
|
let known_hash: Vec<u8> = vec![
|
||||||
|
0xBA, 0x80, 0xA5, 0x3F, 0x98, 0x1C, 0x4D, 0x0D, 0x6A, 0x27, 0x97, 0xB6, 0x9F, 0x12,
|
||||||
|
0xF6, 0xE9, 0x4C, 0x21, 0x2F, 0x14, 0x68, 0x5A, 0xC4, 0xB7, 0x4B, 0x12, 0xBB, 0x6F,
|
||||||
|
0xDB, 0xFF, 0xA2, 0xD1, 0x7D, 0x87, 0xC5, 0x39, 0x2A, 0xAB, 0x79, 0x2D, 0xC2, 0x52,
|
||||||
|
0xD5, 0xDE, 0x45, 0x33, 0xCC, 0x95, 0x18, 0xD3, 0x8A, 0xA8, 0xDB, 0xF1, 0x92, 0x5A,
|
||||||
|
0xB9, 0x23, 0x86, 0xED, 0xD4, 0x00, 0x99, 0x23,
|
||||||
|
];
|
||||||
|
assert_eq!(hash.hash, known_hash);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,96 @@
|
||||||
|
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
use crate::message::MessagePayload;
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// Structure representing an `<encryption xmlns='urn:xmpp:eme:0'/>` element.
|
||||||
|
ExplicitMessageEncryption, "encryption", EME,
|
||||||
|
attributes: [
|
||||||
|
/// Namespace of the encryption scheme used.
|
||||||
|
namespace: Required<String> = "namespace",
|
||||||
|
|
||||||
|
/// User-friendly name for the encryption scheme, should be `None` for OTR,
|
||||||
|
/// legacy OpenPGP and OX.
|
||||||
|
name: Option<String> = "name",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
impl MessagePayload for ExplicitMessageEncryption {}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::util::error::Error;
|
||||||
|
use crate::Element;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "32")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(ExplicitMessageEncryption, 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "64")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(ExplicitMessageEncryption, 48);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_simple() {
|
||||||
|
let elem: Element = "<encryption xmlns='urn:xmpp:eme:0' namespace='urn:xmpp:otr:0'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let encryption = ExplicitMessageEncryption::try_from(elem).unwrap();
|
||||||
|
assert_eq!(encryption.namespace, "urn:xmpp:otr:0");
|
||||||
|
assert_eq!(encryption.name, None);
|
||||||
|
|
||||||
|
let elem: Element = "<encryption xmlns='urn:xmpp:eme:0' namespace='some.unknown.mechanism' name='SuperMechanism'/>".parse().unwrap();
|
||||||
|
let encryption = ExplicitMessageEncryption::try_from(elem).unwrap();
|
||||||
|
assert_eq!(encryption.namespace, "some.unknown.mechanism");
|
||||||
|
assert_eq!(encryption.name, Some(String::from("SuperMechanism")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_unknown() {
|
||||||
|
let elem: Element = "<replace xmlns='urn:xmpp:message-correct:0'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = ExplicitMessageEncryption::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "This is not a encryption element.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_child() {
|
||||||
|
let elem: Element = "<encryption xmlns='urn:xmpp:eme:0'><coucou/></encryption>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = ExplicitMessageEncryption::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Unknown child in encryption element.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialise() {
|
||||||
|
let elem: Element = "<encryption xmlns='urn:xmpp:eme:0' namespace='coucou'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let eme = ExplicitMessageEncryption {
|
||||||
|
namespace: String::from("coucou"),
|
||||||
|
name: None,
|
||||||
|
};
|
||||||
|
let elem2 = eme.into();
|
||||||
|
assert_eq!(elem, elem2);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,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::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);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialize_with_delay_and_stanza() {
|
||||||
|
let reference: Element = "<forwarded xmlns='urn:xmpp:forward:0'><delay xmlns='urn:xmpp:delay' from='capulet.com' stamp='2002-09-10T23:08:25+00:00'/><message xmlns='jabber:client' to='juliet@capulet.example/balcony' from='romeo@montague.example/home'/></forwarded>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let elem: Element = "<message xmlns='jabber:client' to='juliet@capulet.example/balcony' from='romeo@montague.example/home'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let message = Message::try_from(elem).unwrap();
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
let forwarded = Forwarded {
|
||||||
|
delay: Some(delay),
|
||||||
|
stanza: Some(message),
|
||||||
|
};
|
||||||
|
|
||||||
|
let serialized: Element = forwarded.into();
|
||||||
|
assert_eq!(serialized, reference);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,274 @@
|
||||||
|
// 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.
|
||||||
|
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 {
|
||||||
|
self.hash
|
||||||
|
.iter()
|
||||||
|
.map(|byte| format!("{:02x}", byte))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Formats this hash into colon-separated hexadecimal.
|
||||||
|
pub fn to_colon_separated_hex(&self) -> String {
|
||||||
|
self.hash
|
||||||
|
.iter()
|
||||||
|
.map(|byte| format!("{:02x}", byte))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(":")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper for parsing and serialising a SHA-1 attribute.
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct Sha1HexAttribute(Hash);
|
||||||
|
|
||||||
|
impl FromStr for Sha1HexAttribute {
|
||||||
|
type Err = ParseIntError;
|
||||||
|
|
||||||
|
fn from_str(hex: &str) -> Result<Self, Self::Err> {
|
||||||
|
let hash = Hash::from_hex(Algo::Sha_1, hex)?;
|
||||||
|
Ok(Sha1HexAttribute(hash))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoAttributeValue for Sha1HexAttribute {
|
||||||
|
fn into_attribute_value(self) -> Option<String> {
|
||||||
|
Some(self.to_hex())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DerefMut for Sha1HexAttribute {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for Sha1HexAttribute {
|
||||||
|
type Target = Hash;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::Element;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "32")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(Algo, 16);
|
||||||
|
assert_size!(Hash, 28);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "64")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(Algo, 32);
|
||||||
|
assert_size!(Hash, 56);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_simple() {
|
||||||
|
let elem: Element = "<hash xmlns='urn:xmpp:hashes:2' algo='sha-256'>2XarmwTlNxDAMkvymloX3S5+VbylNrJt/l5QyPa+YoU=</hash>".parse().unwrap();
|
||||||
|
let hash = Hash::try_from(elem).unwrap();
|
||||||
|
assert_eq!(hash.algo, Algo::Sha_256);
|
||||||
|
assert_eq!(
|
||||||
|
hash.hash,
|
||||||
|
base64::decode("2XarmwTlNxDAMkvymloX3S5+VbylNrJt/l5QyPa+YoU=").unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn value_serialisation() {
|
||||||
|
let elem: Element = "<hash xmlns='urn:xmpp:hashes:2' algo='sha-256'>2XarmwTlNxDAMkvymloX3S5+VbylNrJt/l5QyPa+YoU=</hash>".parse().unwrap();
|
||||||
|
let hash = Hash::try_from(elem).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
hash.to_base64(),
|
||||||
|
"2XarmwTlNxDAMkvymloX3S5+VbylNrJt/l5QyPa+YoU="
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
hash.to_hex(),
|
||||||
|
"d976ab9b04e53710c0324bf29a5a17dd2e7e55bca536b26dfe5e50c8f6be6285"
|
||||||
|
);
|
||||||
|
assert_eq!(hash.to_colon_separated_hex(), "d9:76:ab:9b:04:e5:37:10:c0:32:4b:f2:9a:5a:17:dd:2e:7e:55:bc:a5:36:b2:6d:fe:5e:50:c8:f6:be:62:85");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_unknown() {
|
||||||
|
let elem: Element = "<replace xmlns='urn:xmpp:message-correct:0'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = Hash::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "This is not a hash element.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_child() {
|
||||||
|
let elem: Element = "<hash xmlns='urn:xmpp:hashes:2'><coucou/></hash>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = Hash::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Unknown child in hash element.");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,171 @@
|
||||||
|
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
use crate::iq::IqSetPayload;
|
||||||
|
use crate::util::helpers::Base64;
|
||||||
|
|
||||||
|
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::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.to_string(), "invalid digit found in string");
|
||||||
|
|
||||||
|
let elem: Element = "<open xmlns='http://jabber.org/protocol/ibb' block-size='128'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = Open::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(error) => error,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Required attribute 'sid' missing.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_stanza() {
|
||||||
|
let elem: Element = "<open xmlns='http://jabber.org/protocol/ibb' block-size='128' sid='coucou' stanza='fdsq'/>".parse().unwrap();
|
||||||
|
let error = Open::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Unknown value for 'stanza' attribute.");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,195 @@
|
||||||
|
// 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::iq::{IqGetPayload, IqResultPayload, IqSetPayload};
|
||||||
|
use crate::ns;
|
||||||
|
use crate::util::error::Error;
|
||||||
|
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();
|
||||||
|
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::REGISTER)
|
||||||
|
.append_all(if query.registered {
|
||||||
|
Some(Element::builder("registered", ns::REGISTER))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
})
|
||||||
|
.append_all(
|
||||||
|
query
|
||||||
|
.fields
|
||||||
|
.into_iter()
|
||||||
|
.map(|(name, value)| Element::builder(name, ns::REGISTER).append(value)),
|
||||||
|
)
|
||||||
|
.append_all(if query.remove {
|
||||||
|
Some(Element::builder("remove", ns::REGISTER))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
})
|
||||||
|
.append_all(query.form.map(Element::from))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "32")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(Query, 88);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "64")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(Query, 160);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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 = "<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_eq!(elem1, elem2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ex10() {
|
||||||
|
let elem: Element = "<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_eq!(elem1, elem2);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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::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::convert::TryFrom;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
#[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.to_string(), "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.to_string(), "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.to_string(), "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.to_string(), "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.to_string(), "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.to_string(), "premature end of input");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialise() {
|
||||||
|
let elem: Element = "<idle xmlns='urn:xmpp:idle:1' since='2017-05-21T20:19:55+01:00'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let idle = Idle {
|
||||||
|
since: DateTime::from_str("2017-05-21T20:19:55+01:00").unwrap(),
|
||||||
|
};
|
||||||
|
let elem2 = idle.into();
|
||||||
|
assert_eq!(elem, elem2);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,461 @@
|
||||||
|
// 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::ns;
|
||||||
|
use crate::stanza_error::StanzaError;
|
||||||
|
use crate::util::error::Error;
|
||||||
|
use crate::Element;
|
||||||
|
use jid::Jid;
|
||||||
|
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 empty `<iq type="result"/>` stanza.
|
||||||
|
pub fn empty_result<S: Into<String>>(to: Jid, id: S) -> Iq {
|
||||||
|
Iq {
|
||||||
|
from: None,
|
||||||
|
to: Some(to),
|
||||||
|
id: id.into(),
|
||||||
|
payload: IqType::Result(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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::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::disco::DiscoInfoQuery;
|
||||||
|
use crate::stanza_error::{DefinedCondition, ErrorType};
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "32")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(IqType, 136);
|
||||||
|
assert_size!(Iq, 228);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "64")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(IqType, 272);
|
||||||
|
assert_size!(Iq, 456);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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 == 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 == 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 == query,
|
||||||
|
_ => false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_error() {
|
||||||
|
#[cfg(not(feature = "component"))]
|
||||||
|
let elem: Element = "<iq xmlns='jabber:client' type='error' id='err1'>
|
||||||
|
<ping xmlns='urn:xmpp:ping'/>
|
||||||
|
<error type='cancel'>
|
||||||
|
<service-unavailable xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
|
||||||
|
</error>
|
||||||
|
</iq>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
#[cfg(feature = "component")]
|
||||||
|
let elem: Element = "<iq xmlns='jabber:component:accept' type='error' id='err1'>
|
||||||
|
<ping xmlns='urn:xmpp:ping'/>
|
||||||
|
<error type='cancel'>
|
||||||
|
<service-unavailable xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
|
||||||
|
</error>
|
||||||
|
</iq>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let iq = Iq::try_from(elem).unwrap();
|
||||||
|
assert_eq!(iq.from, None);
|
||||||
|
assert_eq!(iq.to, None);
|
||||||
|
assert_eq!(iq.id, "err1");
|
||||||
|
match iq.payload {
|
||||||
|
IqType::Error(error) => {
|
||||||
|
assert_eq!(error.type_, ErrorType::Cancel);
|
||||||
|
assert_eq!(error.by, None);
|
||||||
|
assert_eq!(
|
||||||
|
error.defined_condition,
|
||||||
|
DefinedCondition::ServiceUnavailable
|
||||||
|
);
|
||||||
|
assert_eq!(error.texts.len(), 0);
|
||||||
|
assert_eq!(error.other, None);
|
||||||
|
}
|
||||||
|
_ => panic!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_children_invalid() {
|
||||||
|
#[cfg(not(feature = "component"))]
|
||||||
|
let elem: Element = "<iq xmlns='jabber:client' type='error' id='error'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
#[cfg(feature = "component")]
|
||||||
|
let elem: Element = "<iq xmlns='jabber:component:accept' type='error' id='error'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = Iq::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Wrong number of children in iq element.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialise() {
|
||||||
|
#[cfg(not(feature = "component"))]
|
||||||
|
let elem: Element = "<iq xmlns='jabber:client' type='result' id='res'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
#[cfg(feature = "component")]
|
||||||
|
let elem: Element = "<iq xmlns='jabber:component:accept' type='result' id='res'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let iq2 = Iq {
|
||||||
|
from: None,
|
||||||
|
to: None,
|
||||||
|
id: String::from("res"),
|
||||||
|
payload: IqType::Result(None),
|
||||||
|
};
|
||||||
|
let elem2 = iq2.into();
|
||||||
|
assert_eq!(elem, elem2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_disco() {
|
||||||
|
#[cfg(not(feature = "component"))]
|
||||||
|
let elem: Element = "<iq xmlns='jabber:client' type='get' id='disco'><query xmlns='http://jabber.org/protocol/disco#info'/></iq>".parse().unwrap();
|
||||||
|
#[cfg(feature = "component")]
|
||||||
|
let elem: Element = "<iq xmlns='jabber:component:accept' type='get' id='disco'><query xmlns='http://jabber.org/protocol/disco#info'/></iq>".parse().unwrap();
|
||||||
|
let iq = Iq::try_from(elem).unwrap();
|
||||||
|
let disco_info = match iq.payload {
|
||||||
|
IqType::Get(payload) => DiscoInfoQuery::try_from(payload).unwrap(),
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert!(disco_info.node.is_none());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
// 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::{JidCodec, Text};
|
||||||
|
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 jid::FullJid;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
#[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,
|
||||||
|
FullJid::new("romeo", "montague.lit", "orchard")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,911 @@
|
||||||
|
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
use crate::iq::IqSetPayload;
|
||||||
|
use crate::jingle_grouping::Group;
|
||||||
|
use crate::jingle_ibb::Transport as IbbTransport;
|
||||||
|
use crate::jingle_ice_udp::Transport as IceUdpTransport;
|
||||||
|
use crate::jingle_rtp::Description as RtpDescription;
|
||||||
|
use crate::jingle_s5b::Transport as Socks5Transport;
|
||||||
|
use crate::ns;
|
||||||
|
use crate::util::error::Error;
|
||||||
|
use crate::Element;
|
||||||
|
use jid::Jid;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
use std::fmt;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
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",
|
||||||
|
|
||||||
|
/// --- Non-standard messages used by Jitsi Meet:
|
||||||
|
|
||||||
|
/// Add a source to existing content.
|
||||||
|
SourceAdd => "source-add",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
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",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
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, PartialEq)]
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enum wrapping all of the various supported transports of a Content.
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// Describes a session’s content, there can be multiple content in one
|
||||||
|
/// session.
|
||||||
|
Content, "content", JINGLE,
|
||||||
|
attributes: [
|
||||||
|
/// Who created this content.
|
||||||
|
creator: Option<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: Option<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: Some(creator),
|
||||||
|
name,
|
||||||
|
disposition: Disposition::Session,
|
||||||
|
senders: Some(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 = Some(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::JINGLE,
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Lang = String;
|
||||||
|
|
||||||
|
/// Informs the recipient of something.
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
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 fmt::Display for ReasonElement {
|
||||||
|
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(fmt, "{}", Element::from(self.reason.clone()).name())?;
|
||||||
|
if let Some(text) = self.texts.get("en") {
|
||||||
|
write!(fmt, ": {}", text)?;
|
||||||
|
} else if let Some(text) = self.texts.get("") {
|
||||||
|
write!(fmt, ": {}", text)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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::JINGLE)
|
||||||
|
.append(Element::from(reason.reason))
|
||||||
|
.append_all(reason.texts.into_iter().map(|(lang, text)| {
|
||||||
|
Element::builder("text", 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, PartialEq)]
|
||||||
|
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>,
|
||||||
|
|
||||||
|
/// An optional grouping.
|
||||||
|
pub group: Option<Group>,
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
group: 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, reason: ReasonElement) -> Jingle {
|
||||||
|
self.reason = Some(reason);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the grouping in this Jingle container.
|
||||||
|
pub fn set_group(mut self, group: Group) -> Jingle {
|
||||||
|
self.group = Some(group);
|
||||||
|
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,
|
||||||
|
group: 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 if child.is("group", ns::JINGLE_GROUPING) {
|
||||||
|
if jingle.group.is_some() {
|
||||||
|
return Err(Error::ParseError(
|
||||||
|
"Jingle must not have more than one grouping.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let group = Group::try_from(child)?;
|
||||||
|
jingle.group = Some(group);
|
||||||
|
} else {
|
||||||
|
jingle.other.push(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(jingle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Jingle> for Element {
|
||||||
|
fn from(jingle: Jingle) -> Element {
|
||||||
|
Element::builder("jingle", 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))
|
||||||
|
.append_all(jingle.group.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, 252);
|
||||||
|
assert_size!(Reason, 1);
|
||||||
|
assert_size!(ReasonElement, 16);
|
||||||
|
assert_size!(SessionId, 12);
|
||||||
|
assert_size!(Jingle, 152);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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, 504);
|
||||||
|
assert_size!(Reason, 1);
|
||||||
|
assert_size!(ReasonElement, 32);
|
||||||
|
assert_size!(SessionId, 24);
|
||||||
|
assert_size!(Jingle, 304);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialize_jingle() {
|
||||||
|
let reference: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='a73sjjvkla37jfea'><content xmlns='urn:xmpp:jingle:1' creator='initiator' name='this-is-a-stub'><description xmlns='urn:xmpp:jingle:apps:stub:0'/><transport xmlns='urn:xmpp:jingle:transports:stub:0'/></content></jingle>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let jingle = Jingle {
|
||||||
|
action: Action::SessionInitiate,
|
||||||
|
initiator: None,
|
||||||
|
responder: None,
|
||||||
|
sid: SessionId(String::from("a73sjjvkla37jfea")),
|
||||||
|
contents: vec![Content {
|
||||||
|
creator: Creator::Initiator,
|
||||||
|
disposition: Disposition::default(),
|
||||||
|
name: ContentId(String::from("this-is-a-stub")),
|
||||||
|
senders: Senders::default(),
|
||||||
|
description: Some(Description::Unknown(
|
||||||
|
Element::builder("description", "urn:xmpp:jingle:apps:stub:0").build(),
|
||||||
|
)),
|
||||||
|
transport: Some(Transport::Unknown(
|
||||||
|
Element::builder("transport", "urn:xmpp:jingle:transports:stub:0").build(),
|
||||||
|
)),
|
||||||
|
security: None,
|
||||||
|
}],
|
||||||
|
reason: None,
|
||||||
|
group: None,
|
||||||
|
other: vec![],
|
||||||
|
};
|
||||||
|
let serialized: Element = jingle.into();
|
||||||
|
assert_eq!(serialized, reference);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,112 @@
|
||||||
|
// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
use crate::hashes::{Algo, Hash};
|
||||||
|
use crate::util::error::Error;
|
||||||
|
use crate::util::helpers::ColonSeparatedHex;
|
||||||
|
|
||||||
|
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: Option<Setup> = "setup",
|
||||||
|
|
||||||
|
/// Indicates whether DTLS is mandatory
|
||||||
|
required: Option<String> = "required"
|
||||||
|
],
|
||||||
|
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: Some(setup),
|
||||||
|
value: hash.hash,
|
||||||
|
required: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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, Some(Setup::Actpass));
|
||||||
|
assert_eq!(fingerprint.hash, Algo::Sha_256);
|
||||||
|
assert_eq!(
|
||||||
|
fingerprint.value,
|
||||||
|
[
|
||||||
|
2, 26, 204, 84, 39, 171, 235, 156, 83, 63, 62, 75, 101, 46, 125, 70, 63, 84, 66,
|
||||||
|
205, 84, 241, 122, 3, 162, 125, 249, 176, 127, 70, 25, 178
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,620 @@
|
||||||
|
// 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::hashes::Hash;
|
||||||
|
use crate::jingle::{ContentId, Creator};
|
||||||
|
use crate::ns;
|
||||||
|
use crate::util::error::Error;
|
||||||
|
use minidom::{Element, Node};
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// Represents a range in a file.
|
||||||
|
#[derive(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::JINGLE_FT)
|
||||||
|
.append_all(
|
||||||
|
file.date
|
||||||
|
.map(|date| Element::builder("date", ns::JINGLE_FT).append(date)),
|
||||||
|
)
|
||||||
|
.append_all(
|
||||||
|
file.media_type.map(|media_type| {
|
||||||
|
Element::builder("media-type", ns::JINGLE_FT).append(media_type)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.append_all(
|
||||||
|
file.name
|
||||||
|
.map(|name| Element::builder("name", ns::JINGLE_FT).append(name)),
|
||||||
|
)
|
||||||
|
.append_all(file.descs.into_iter().map(|(lang, desc)| {
|
||||||
|
Element::builder("desc", ns::JINGLE_FT)
|
||||||
|
.attr("xml:lang", lang)
|
||||||
|
.append(desc.0)
|
||||||
|
}))
|
||||||
|
.append_all(
|
||||||
|
file.size.map(|size| {
|
||||||
|
Element::builder("size", ns::JINGLE_FT).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::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::JINGLE_FT)
|
||||||
|
.attr("name", checksum.name)
|
||||||
|
.attr("creator", checksum.creator)
|
||||||
|
.append(Node::Element(checksum.file.into()))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// A notice that the file transfer has been completed.
|
||||||
|
Received, "received", JINGLE_FT,
|
||||||
|
attributes: [
|
||||||
|
/// The content identifier of this Jingle session.
|
||||||
|
name: Required<ContentId> = "name",
|
||||||
|
|
||||||
|
/// The creator of this file transfer.
|
||||||
|
creator: Required<Creator> = "creator",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::hashes::Algo;
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "32")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(Range, 40);
|
||||||
|
assert_size!(File, 128);
|
||||||
|
assert_size!(Description, 128);
|
||||||
|
assert_size!(Checksum, 144);
|
||||||
|
assert_size!(Received, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "64")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(Range, 48);
|
||||||
|
assert_size!(File, 184);
|
||||||
|
assert_size!(Description, 184);
|
||||||
|
assert_size!(Checksum, 216);
|
||||||
|
assert_size!(Received, 32);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_description() {
|
||||||
|
let elem: Element = r#"
|
||||||
|
<description xmlns='urn:xmpp:jingle:apps:file-transfer:5'>
|
||||||
|
<file>
|
||||||
|
<media-type>text/plain</media-type>
|
||||||
|
<name>test.txt</name>
|
||||||
|
<date>2015-07-26T21:46:00+01:00</date>
|
||||||
|
<size>6144</size>
|
||||||
|
<hash xmlns='urn:xmpp:hashes:2'
|
||||||
|
algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash>
|
||||||
|
</file>
|
||||||
|
</description>
|
||||||
|
"#
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let desc = Description::try_from(elem).unwrap();
|
||||||
|
assert_eq!(desc.file.media_type, Some(String::from("text/plain")));
|
||||||
|
assert_eq!(desc.file.name, Some(String::from("test.txt")));
|
||||||
|
assert_eq!(desc.file.descs, BTreeMap::new());
|
||||||
|
assert_eq!(
|
||||||
|
desc.file.date,
|
||||||
|
DateTime::from_str("2015-07-26T21:46:00+01:00").ok()
|
||||||
|
);
|
||||||
|
assert_eq!(desc.file.size, Some(6144u64));
|
||||||
|
assert_eq!(desc.file.range, None);
|
||||||
|
assert_eq!(desc.file.hashes[0].algo, Algo::Sha_1);
|
||||||
|
assert_eq!(
|
||||||
|
desc.file.hashes[0].hash,
|
||||||
|
base64::decode("w0mcJylzCn+AfvuGdqkty2+KP48=").unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_request() {
|
||||||
|
let elem: Element = r#"
|
||||||
|
<description xmlns='urn:xmpp:jingle:apps:file-transfer:5'>
|
||||||
|
<file>
|
||||||
|
<hash xmlns='urn:xmpp:hashes:2'
|
||||||
|
algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash>
|
||||||
|
</file>
|
||||||
|
</description>
|
||||||
|
"#
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let desc = Description::try_from(elem).unwrap();
|
||||||
|
assert_eq!(desc.file.media_type, None);
|
||||||
|
assert_eq!(desc.file.name, None);
|
||||||
|
assert_eq!(desc.file.descs, BTreeMap::new());
|
||||||
|
assert_eq!(desc.file.date, None);
|
||||||
|
assert_eq!(desc.file.size, None);
|
||||||
|
assert_eq!(desc.file.range, None);
|
||||||
|
assert_eq!(desc.file.hashes[0].algo, Algo::Sha_1);
|
||||||
|
assert_eq!(
|
||||||
|
desc.file.hashes[0].hash,
|
||||||
|
base64::decode("w0mcJylzCn+AfvuGdqkty2+KP48=").unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_descs() {
|
||||||
|
let elem: Element = r#"
|
||||||
|
<description xmlns='urn:xmpp:jingle:apps:file-transfer:5'>
|
||||||
|
<file>
|
||||||
|
<media-type>text/plain</media-type>
|
||||||
|
<desc xml:lang='fr'>Fichier secret !</desc>
|
||||||
|
<desc xml:lang='en'>Secret file!</desc>
|
||||||
|
<hash xmlns='urn:xmpp:hashes:2'
|
||||||
|
algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash>
|
||||||
|
</file>
|
||||||
|
</description>
|
||||||
|
"#
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let desc = Description::try_from(elem).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
desc.file.descs.keys().cloned().collect::<Vec<_>>(),
|
||||||
|
["en", "fr"]
|
||||||
|
);
|
||||||
|
assert_eq!(desc.file.descs["en"], Desc(String::from("Secret file!")));
|
||||||
|
assert_eq!(
|
||||||
|
desc.file.descs["fr"],
|
||||||
|
Desc(String::from("Fichier secret !"))
|
||||||
|
);
|
||||||
|
|
||||||
|
let elem: Element = r#"
|
||||||
|
<description xmlns='urn:xmpp:jingle:apps:file-transfer:5'>
|
||||||
|
<file>
|
||||||
|
<media-type>text/plain</media-type>
|
||||||
|
<desc xml:lang='fr'>Fichier secret !</desc>
|
||||||
|
<desc xml:lang='fr'>Secret file!</desc>
|
||||||
|
<hash xmlns='urn:xmpp:hashes:2'
|
||||||
|
algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash>
|
||||||
|
</file>
|
||||||
|
</description>
|
||||||
|
"#
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = Description::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Desc element present twice for the same xml:lang.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_received() {
|
||||||
|
let elem: Element = "<received xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='initiator'/>".parse().unwrap();
|
||||||
|
let received = Received::try_from(elem).unwrap();
|
||||||
|
assert_eq!(received.name, ContentId(String::from("coucou")));
|
||||||
|
assert_eq!(received.creator, Creator::Initiator);
|
||||||
|
let elem2 = Element::from(received.clone());
|
||||||
|
let received2 = Received::try_from(elem2).unwrap();
|
||||||
|
assert_eq!(received2.name, ContentId(String::from("coucou")));
|
||||||
|
assert_eq!(received2.creator, Creator::Initiator);
|
||||||
|
|
||||||
|
let elem: Element = "<received xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='initiator'><coucou/></received>".parse().unwrap();
|
||||||
|
let error = Received::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Unknown child in received element.");
|
||||||
|
|
||||||
|
let elem: Element =
|
||||||
|
"<received xmlns='urn:xmpp:jingle:apps:file-transfer:5' creator='initiator'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = Received::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Required attribute 'name' missing.");
|
||||||
|
|
||||||
|
let elem: Element = "<received xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='coucou'/>".parse().unwrap();
|
||||||
|
let error = Received::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Unknown value for 'creator' attribute.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "disable-validation"))]
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_received() {
|
||||||
|
let elem: Element = "<received xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='initiator' coucou=''/>".parse().unwrap();
|
||||||
|
let error = Received::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Unknown attribute in received element.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_checksum() {
|
||||||
|
let elem: Element = "<checksum xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='initiator'><file><hash xmlns='urn:xmpp:hashes:2' algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash></file></checksum>".parse().unwrap();
|
||||||
|
let hash = vec![
|
||||||
|
195, 73, 156, 39, 41, 115, 10, 127, 128, 126, 251, 134, 118, 169, 45, 203, 111, 138,
|
||||||
|
63, 143,
|
||||||
|
];
|
||||||
|
let checksum = Checksum::try_from(elem).unwrap();
|
||||||
|
assert_eq!(checksum.name, ContentId(String::from("coucou")));
|
||||||
|
assert_eq!(checksum.creator, Creator::Initiator);
|
||||||
|
assert_eq!(
|
||||||
|
checksum.file.hashes,
|
||||||
|
vec!(Hash {
|
||||||
|
algo: Algo::Sha_1,
|
||||||
|
hash: hash.clone()
|
||||||
|
})
|
||||||
|
);
|
||||||
|
let elem2 = Element::from(checksum);
|
||||||
|
let checksum2 = Checksum::try_from(elem2).unwrap();
|
||||||
|
assert_eq!(checksum2.name, ContentId(String::from("coucou")));
|
||||||
|
assert_eq!(checksum2.creator, Creator::Initiator);
|
||||||
|
assert_eq!(
|
||||||
|
checksum2.file.hashes,
|
||||||
|
vec!(Hash {
|
||||||
|
algo: Algo::Sha_1,
|
||||||
|
hash: hash.clone()
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
let elem: Element = "<checksum xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='initiator'><coucou/></checksum>".parse().unwrap();
|
||||||
|
let error = Checksum::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "This is not a file element.");
|
||||||
|
|
||||||
|
let elem: Element = "<checksum xmlns='urn:xmpp:jingle:apps:file-transfer:5' creator='initiator'><file><hash xmlns='urn:xmpp:hashes:2' algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash></file></checksum>".parse().unwrap();
|
||||||
|
let error = Checksum::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Required attribute 'name' missing.");
|
||||||
|
|
||||||
|
let elem: Element = "<checksum xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='coucou'><file><hash xmlns='urn:xmpp:hashes:2' algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash></file></checksum>".parse().unwrap();
|
||||||
|
let error = Checksum::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Unknown value for 'creator' attribute.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "disable-validation"))]
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_checksum() {
|
||||||
|
let elem: Element = "<checksum xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='initiator' coucou=''><file><hash xmlns='urn:xmpp:hashes:2' algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash></file></checksum>".parse().unwrap();
|
||||||
|
let error = Checksum::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Unknown attribute in checksum element.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_range() {
|
||||||
|
let elem: Element = "<range xmlns='urn:xmpp:jingle:apps:file-transfer:5'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let range = Range::try_from(elem).unwrap();
|
||||||
|
assert_eq!(range.offset, 0);
|
||||||
|
assert_eq!(range.length, None);
|
||||||
|
assert_eq!(range.hashes, vec!());
|
||||||
|
|
||||||
|
let elem: Element = "<range xmlns='urn:xmpp:jingle:apps:file-transfer:5' offset='2048' length='1024'><hash xmlns='urn:xmpp:hashes:2' algo='sha-1'>kHp5RSzW/h7Gm1etSf90Mr5PC/k=</hash></range>".parse().unwrap();
|
||||||
|
let hashes = vec![Hash {
|
||||||
|
algo: Algo::Sha_1,
|
||||||
|
hash: vec![
|
||||||
|
144, 122, 121, 69, 44, 214, 254, 30, 198, 155, 87, 173, 73, 255, 116, 50, 190, 79,
|
||||||
|
11, 249,
|
||||||
|
],
|
||||||
|
}];
|
||||||
|
let range = Range::try_from(elem).unwrap();
|
||||||
|
assert_eq!(range.offset, 2048);
|
||||||
|
assert_eq!(range.length, Some(1024));
|
||||||
|
assert_eq!(range.hashes, hashes);
|
||||||
|
let elem2 = Element::from(range);
|
||||||
|
let range2 = Range::try_from(elem2).unwrap();
|
||||||
|
assert_eq!(range2.offset, 2048);
|
||||||
|
assert_eq!(range2.length, Some(1024));
|
||||||
|
assert_eq!(range2.hashes, hashes);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "disable-validation"))]
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_range() {
|
||||||
|
let elem: Element = "<range xmlns='urn:xmpp:jingle:apps:file-transfer:5' coucou=''/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = Range::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Unknown attribute in range element.");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,90 @@
|
||||||
|
// Copyright (c) 2020 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::ContentId;
|
||||||
|
|
||||||
|
generate_attribute!(
|
||||||
|
/// The semantics of the grouping.
|
||||||
|
Semantics, "semantics", {
|
||||||
|
/// Lip synchronsation.
|
||||||
|
Ls => "LS",
|
||||||
|
|
||||||
|
/// Bundle.
|
||||||
|
Bundle => "BUNDLE",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// Describes a content that should be grouped with other ones.
|
||||||
|
Content, "content", JINGLE_GROUPING,
|
||||||
|
attributes: [
|
||||||
|
/// The name of the matching [`Content`](crate::jingle::Content).
|
||||||
|
name: Required<ContentId> = "name",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
impl Content {
|
||||||
|
/// Creates a new <content/> element.
|
||||||
|
pub fn new(name: &str) -> Content {
|
||||||
|
Content {
|
||||||
|
name: ContentId(name.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// A semantic group of contents.
|
||||||
|
Group, "group", JINGLE_GROUPING,
|
||||||
|
attributes: [
|
||||||
|
/// Semantics of the grouping.
|
||||||
|
semantics: Required<Semantics> = "semantics",
|
||||||
|
],
|
||||||
|
children: [
|
||||||
|
/// List of contents that should be grouped with each other.
|
||||||
|
contents: Vec<Content> = ("content", JINGLE_GROUPING) => 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!(Semantics, 1);
|
||||||
|
assert_size!(Content, 12);
|
||||||
|
assert_size!(Group, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "64")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(Semantics, 1);
|
||||||
|
assert_size!(Content, 24);
|
||||||
|
assert_size!(Group, 32);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_group() {
|
||||||
|
let elem: Element = "
|
||||||
|
<group xmlns='urn:xmpp:jingle:apps:grouping:0' semantics='BUNDLE'>
|
||||||
|
<content name='voice'/>
|
||||||
|
<content name='webcam'/>
|
||||||
|
</group>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let group = Group::try_from(elem).unwrap();
|
||||||
|
assert_eq!(group.semantics, Semantics::Bundle);
|
||||||
|
assert_eq!(group.contents.len(), 2);
|
||||||
|
assert_eq!(
|
||||||
|
group.contents,
|
||||||
|
&[Content::new("voice"), Content::new("webcam")]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,113 @@
|
||||||
|
// 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::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.to_string(),
|
||||||
|
"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.to_string(), "invalid digit found in string");
|
||||||
|
|
||||||
|
let elem: Element =
|
||||||
|
"<transport xmlns='urn:xmpp:jingle:transports:ibb:1' block-size='128'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = Transport::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Required attribute 'sid' missing.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_stanza() {
|
||||||
|
let elem: Element = "<transport xmlns='urn:xmpp:jingle:transports:ibb:1' block-size='128' sid='coucou' stanza='fdsq'/>".parse().unwrap();
|
||||||
|
let error = Transport::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Unknown value for 'stanza' attribute.");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,243 @@
|
||||||
|
// 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_empty_element!(
|
||||||
|
/// Specifies the ability to multiplex RTP Data and Control Packets on a single port as
|
||||||
|
/// described in RFC 5761.
|
||||||
|
RtcpMux,
|
||||||
|
"rtcp-mux",
|
||||||
|
JINGLE_ICE_UDP
|
||||||
|
);
|
||||||
|
|
||||||
|
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,
|
||||||
|
|
||||||
|
/// Indicates that RTCP can be muxed
|
||||||
|
rtcp_mux: Option<RtcpMux> = ("rtcp-mux", JINGLE_ICE_UDP) => RtcpMux,
|
||||||
|
|
||||||
|
/// Details of the Colibri WebSocket
|
||||||
|
web_socket: Option<WebSocket> = ("web-socket", JITSI_COLIBRI) => WebSocket
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
impl Transport {
|
||||||
|
/// Create a new ICE-UDP transport.
|
||||||
|
pub fn new() -> Transport {
|
||||||
|
Transport {
|
||||||
|
pwd: None,
|
||||||
|
ufrag: None,
|
||||||
|
candidates: Vec::new(),
|
||||||
|
fingerprint: None,
|
||||||
|
rtcp_mux: None,
|
||||||
|
web_socket: 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_element!(
|
||||||
|
/// Colibri WebSocket details
|
||||||
|
WebSocket, "web-socket", JITSI_COLIBRI,
|
||||||
|
attributes: [
|
||||||
|
/// The WebSocket URL
|
||||||
|
url: Required<String> = "url",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
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<String> = "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",
|
||||||
|
|
||||||
|
/// 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",
|
||||||
|
|
||||||
|
/// An index, starting at 0, referencing which network this candidate is on for a given
|
||||||
|
/// peer.
|
||||||
|
network: Option<u8> = "network",
|
||||||
|
|
||||||
|
/// A Candidate Type as defined in ICE-CORE.
|
||||||
|
type_: Required<Type> = "type",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::hashes::Algo;
|
||||||
|
use crate::jingle_dtls_srtp::Setup;
|
||||||
|
use crate::Element;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "32")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(Transport, 68);
|
||||||
|
assert_size!(Type, 1);
|
||||||
|
assert_size!(Candidate, 92);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "64")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(Transport, 136);
|
||||||
|
assert_size!(Type, 1);
|
||||||
|
assert_size!(Candidate, 128);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialize_transport() {
|
||||||
|
let reference: Element =
|
||||||
|
"<transport xmlns='urn:xmpp:jingle:transports:ice-udp:1'><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></transport>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
let transport = Transport {
|
||||||
|
pwd: None,
|
||||||
|
ufrag: None,
|
||||||
|
candidates: vec![],
|
||||||
|
fingerprint: Some(fingerprint),
|
||||||
|
};
|
||||||
|
|
||||||
|
let serialized: Element = transport.into();
|
||||||
|
assert_eq!(serialized, reference);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,143 @@
|
||||||
|
// 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::jingle::SessionId;
|
||||||
|
use crate::ns;
|
||||||
|
use crate::util::error::Error;
|
||||||
|
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::JINGLE_MESSAGE)
|
||||||
|
.attr("id", sid)
|
||||||
|
.append(description)
|
||||||
|
}
|
||||||
|
JingleMI::Retract(sid) => {
|
||||||
|
Element::builder("retract", ns::JINGLE_MESSAGE).attr("id", sid)
|
||||||
|
}
|
||||||
|
JingleMI::Accept(sid) => Element::builder("accept", ns::JINGLE_MESSAGE).attr("id", sid),
|
||||||
|
JingleMI::Proceed(sid) => {
|
||||||
|
Element::builder("proceed", ns::JINGLE_MESSAGE).attr("id", sid)
|
||||||
|
}
|
||||||
|
JingleMI::Reject(sid) => Element::builder("reject", 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, 92);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "64")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(JingleMI, 184);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_simple() {
|
||||||
|
let elem: Element = "<accept xmlns='urn:xmpp:jingle-message:0' id='coucou'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
JingleMI::try_from(elem).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_child() {
|
||||||
|
let elem: Element =
|
||||||
|
"<propose xmlns='urn:xmpp:jingle-message:0' id='coucou'><coucou/></propose>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = JingleMI::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Unknown child in propose element.");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,102 @@
|
||||||
|
// Copyright (c) 2020 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_ice_udp::Type;
|
||||||
|
use std::net::IpAddr;
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// Wrapper element for an raw UDP transport.
|
||||||
|
Transport, "transport", JINGLE_RAW_UDP,
|
||||||
|
children: [
|
||||||
|
/// List of candidates for this raw UDP session.
|
||||||
|
candidates: Vec<Candidate> = ("candidate", JINGLE_RAW_UDP) => Candidate
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
impl Transport {
|
||||||
|
/// Create a new ICE-UDP transport.
|
||||||
|
pub fn new() -> Transport {
|
||||||
|
Transport {
|
||||||
|
candidates: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a candidate to this transport.
|
||||||
|
pub fn add_candidate(mut self, candidate: Candidate) -> Self {
|
||||||
|
self.candidates.push(candidate);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// A candidate for an ICE-UDP session.
|
||||||
|
Candidate, "candidate", JINGLE_RAW_UDP,
|
||||||
|
attributes: [
|
||||||
|
/// A Component ID as defined in ICE-CORE.
|
||||||
|
component: Required<u8> = "component",
|
||||||
|
|
||||||
|
/// 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",
|
||||||
|
|
||||||
|
/// The port at the candidate IP address.
|
||||||
|
port: Required<u16> = "port",
|
||||||
|
|
||||||
|
/// A Candidate Type as defined in ICE-CORE.
|
||||||
|
type_: Option<Type> = "type",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::Element;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "32")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(Transport, 12);
|
||||||
|
assert_size!(Candidate, 40);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "64")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(Transport, 24);
|
||||||
|
assert_size!(Candidate, 56);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn example_1() {
|
||||||
|
let elem: Element = "
|
||||||
|
<transport xmlns='urn:xmpp:jingle:transports:raw-udp:1'>
|
||||||
|
<candidate component='1'
|
||||||
|
generation='0'
|
||||||
|
id='a9j3mnbtu1'
|
||||||
|
ip='10.1.1.104'
|
||||||
|
port='13540'/>
|
||||||
|
</transport>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let mut transport = Transport::try_from(elem).unwrap();
|
||||||
|
assert_eq!(transport.candidates.len(), 1);
|
||||||
|
let candidate = transport.candidates.pop().unwrap();
|
||||||
|
assert_eq!(candidate.component, 1);
|
||||||
|
assert_eq!(candidate.generation, 0);
|
||||||
|
assert_eq!(candidate.id, "a9j3mnbtu1");
|
||||||
|
assert_eq!(candidate.ip, "10.1.1.104".parse::<IpAddr>().unwrap());
|
||||||
|
assert_eq!(candidate.port, 13540u16);
|
||||||
|
assert!(candidate.type_.is_none());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// Wrapper element for a rtcp-fb.
|
||||||
|
RtcpFb, "rtcp-fb", JINGLE_RTCP_FB,
|
||||||
|
attributes: [
|
||||||
|
/// Type of this rtcp-fb.
|
||||||
|
type_: Required<String> = "type",
|
||||||
|
|
||||||
|
/// Subtype of this rtcp-fb, if relevant.
|
||||||
|
subtype: Option<String> = "subtype",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::Element;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "32")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(RtcpFb, 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "64")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(RtcpFb, 48);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_simple() {
|
||||||
|
let elem: Element =
|
||||||
|
"<rtcp-fb xmlns='urn:xmpp:jingle:apps:rtp:rtcp-fb:0' type='nack' subtype='sli'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let rtcp_fb = RtcpFb::try_from(elem).unwrap();
|
||||||
|
assert_eq!(rtcp_fb.type_, "nack");
|
||||||
|
assert_eq!(rtcp_fb.subtype.unwrap(), "sli");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,211 @@
|
||||||
|
// Copyright (c) 2019-2020 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_rtcp_fb::RtcpFb;
|
||||||
|
use crate::jingle_rtp_hdrext::RtpHdrext;
|
||||||
|
use crate::jingle_ssma::{Group, Source};
|
||||||
|
|
||||||
|
generate_empty_element!(
|
||||||
|
/// Specifies the ability to multiplex RTP Data and Control Packets on a single port as
|
||||||
|
/// described in RFC 5761.
|
||||||
|
RtcpMux,
|
||||||
|
"rtcp-mux",
|
||||||
|
JINGLE_RTP
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// Wrapper element describing an RTP session.
|
||||||
|
Description, "description", JINGLE_RTP,
|
||||||
|
attributes: [
|
||||||
|
/// Namespace of the encryption scheme used.
|
||||||
|
media: Required<String> = "media",
|
||||||
|
|
||||||
|
/// ssrc?
|
||||||
|
ssrc: Option<String> = "ssrc",
|
||||||
|
|
||||||
|
/// maximum packet time
|
||||||
|
maxptime: Option<u32> = "maxptime",
|
||||||
|
],
|
||||||
|
children: [
|
||||||
|
/// List of encodings that can be used for this RTP stream.
|
||||||
|
payload_types: Vec<PayloadType> = ("payload-type", JINGLE_RTP) => PayloadType,
|
||||||
|
|
||||||
|
/// Specifies the ability to multiplex RTP Data and Control Packets on a single port as
|
||||||
|
/// described in RFC 5761.
|
||||||
|
rtcp_mux: Option<RtcpMux> = ("rtcp-mux", JINGLE_RTP) => RtcpMux,
|
||||||
|
|
||||||
|
/// List of ssrc-group.
|
||||||
|
ssrc_groups: Vec<Group> = ("ssrc-group", JINGLE_SSMA) => Group,
|
||||||
|
|
||||||
|
/// List of ssrc.
|
||||||
|
ssrcs: Vec<Source> = ("source", JINGLE_SSMA) => Source,
|
||||||
|
|
||||||
|
/// List of header extensions.
|
||||||
|
hdrexts: Vec<RtpHdrext> = ("rtp-hdrext", JINGLE_RTP_HDREXT) => RtpHdrext
|
||||||
|
|
||||||
|
// TODO: Add support for <encryption/> and <bandwidth/>.
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
impl Description {
|
||||||
|
/// Create a new RTP description.
|
||||||
|
pub fn new(media: String) -> Description {
|
||||||
|
Description {
|
||||||
|
media,
|
||||||
|
ssrc: None,
|
||||||
|
maxptime: None,
|
||||||
|
payload_types: Vec::new(),
|
||||||
|
rtcp_mux: None,
|
||||||
|
ssrc_groups: Vec::new(),
|
||||||
|
ssrcs: Vec::new(),
|
||||||
|
hdrexts: 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, clockrate: u32, channels: u8) -> PayloadType {
|
||||||
|
PayloadType {
|
||||||
|
channels: Channels(channels),
|
||||||
|
clockrate: Some(clockrate),
|
||||||
|
id,
|
||||||
|
maxptime: None,
|
||||||
|
name: Some(name),
|
||||||
|
ptime: None,
|
||||||
|
parameters: Vec::new(),
|
||||||
|
rtcp_fbs: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new RTP payload-type without a clockrate. Warning: this is invalid as per
|
||||||
|
/// RFC 4566!
|
||||||
|
pub fn without_clockrate(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, 76);
|
||||||
|
assert_size!(Channels, 1);
|
||||||
|
assert_size!(PayloadType, 64);
|
||||||
|
assert_size!(Parameter, 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "64")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(Description, 152);
|
||||||
|
assert_size!(Channels, 1);
|
||||||
|
assert_size!(PayloadType, 104);
|
||||||
|
assert_size!(Parameter, 48);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_simple() {
|
||||||
|
let elem: Element = "
|
||||||
|
<description xmlns='urn:xmpp:jingle:apps:rtp:1' media='audio'>
|
||||||
|
<payload-type xmlns='urn:xmpp:jingle:apps:rtp:1' channels='2' clockrate='48000' id='96' name='OPUS'/>
|
||||||
|
<payload-type xmlns='urn:xmpp:jingle:apps:rtp:1' channels='1' clockrate='32000' id='105' name='SPEEX'/>
|
||||||
|
<payload-type xmlns='urn:xmpp:jingle:apps:rtp:1' channels='1' clockrate='8000' id='9' name='G722'/>
|
||||||
|
<payload-type xmlns='urn:xmpp:jingle:apps:rtp:1' channels='1' clockrate='16000' id='106' name='SPEEX'/>
|
||||||
|
<payload-type xmlns='urn:xmpp:jingle:apps:rtp:1' clockrate='8000' id='8' name='PCMA'/>
|
||||||
|
<payload-type xmlns='urn:xmpp:jingle:apps:rtp:1' clockrate='8000' id='0' name='PCMU'/>
|
||||||
|
<payload-type xmlns='urn:xmpp:jingle:apps:rtp:1' channels='1' clockrate='8000' id='107' name='SPEEX'/>
|
||||||
|
<payload-type xmlns='urn:xmpp:jingle:apps:rtp:1' channels='1' clockrate='8000' id='99' name='AMR'>
|
||||||
|
<parameter xmlns='urn:xmpp:jingle:apps:rtp:1' name='octet-align' value='1'/>
|
||||||
|
<parameter xmlns='urn:xmpp:jingle:apps:rtp:1' name='crc' value='0'/>
|
||||||
|
<parameter xmlns='urn:xmpp:jingle:apps:rtp:1' name='robust-sorting' value='0'/>
|
||||||
|
<parameter xmlns='urn:xmpp:jingle:apps:rtp:1' name='interleaving' value='0'/>
|
||||||
|
</payload-type>
|
||||||
|
<payload-type xmlns='urn:xmpp:jingle:apps:rtp:1' clockrate='48000' id='100' name='telephone-event'>
|
||||||
|
<parameter xmlns='urn:xmpp:jingle:apps:rtp:1' name='events' value='0-15'/>
|
||||||
|
</payload-type>
|
||||||
|
<payload-type xmlns='urn:xmpp:jingle:apps:rtp:1' clockrate='16000' id='101' name='telephone-event'>
|
||||||
|
<parameter xmlns='urn:xmpp:jingle:apps:rtp:1' name='events' value='0-15'/>
|
||||||
|
</payload-type>
|
||||||
|
<payload-type xmlns='urn:xmpp:jingle:apps:rtp:1' clockrate='8000' id='102' name='telephone-event'>
|
||||||
|
<parameter xmlns='urn:xmpp:jingle:apps:rtp:1' name='events' value='0-15'/>
|
||||||
|
</payload-type>
|
||||||
|
</description>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let desc = Description::try_from(elem).unwrap();
|
||||||
|
assert_eq!(desc.media, "audio");
|
||||||
|
assert_eq!(desc.ssrc, None);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
// Copyright (c) 2020 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!(
|
||||||
|
/// Which party is allowed to send the negotiated RTP Header Extensions.
|
||||||
|
Senders, "senders", {
|
||||||
|
/// Both parties can send them.
|
||||||
|
Both => "both",
|
||||||
|
|
||||||
|
/// Only the initiator can send them.
|
||||||
|
Initiator => "initiator",
|
||||||
|
|
||||||
|
/// Only the responder can send them.
|
||||||
|
Responder => "responder",
|
||||||
|
}, Default = Both
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// Header extensions to be used in a RTP description.
|
||||||
|
RtpHdrext, "rtp-hdrext", JINGLE_RTP_HDREXT,
|
||||||
|
attributes: [
|
||||||
|
/// The ID of the extensions.
|
||||||
|
id: Required<String> = "id",
|
||||||
|
|
||||||
|
/// The URI that defines the extension.
|
||||||
|
uri: Required<String> = "uri",
|
||||||
|
|
||||||
|
/// Which party is allowed to send the negotiated RTP Header Extensions.
|
||||||
|
senders: Default<Senders> = "senders",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
impl RtpHdrext {
|
||||||
|
/// Create a new RTP header extension element.
|
||||||
|
pub fn new(id: String, uri: String) -> RtpHdrext {
|
||||||
|
RtpHdrext {
|
||||||
|
id,
|
||||||
|
uri,
|
||||||
|
senders: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the senders.
|
||||||
|
pub fn with_senders(mut self, senders: Senders) -> RtpHdrext {
|
||||||
|
self.senders = senders;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::Element;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "32")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(Senders, 1);
|
||||||
|
assert_size!(RtpHdrext, 28);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "64")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(Senders, 1);
|
||||||
|
assert_size!(RtpHdrext, 56);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_exthdr() {
|
||||||
|
let elem: Element = "
|
||||||
|
<rtp-hdrext xmlns='urn:xmpp:jingle:apps:rtp:rtp-hdrext:0'
|
||||||
|
uri='urn:ietf:params:rtp-hdrext:toffset'
|
||||||
|
id='1'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let rtp_hdrext = RtpHdrext::try_from(elem).unwrap();
|
||||||
|
assert_eq!(rtp_hdrext.id, "1");
|
||||||
|
assert_eq!(rtp_hdrext.uri, "urn:ietf:params:rtp-hdrext:toffset");
|
||||||
|
assert_eq!(rtp_hdrext.senders, Senders::Both);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,353 @@
|
||||||
|
// 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::ns;
|
||||||
|
use crate::util::error::Error;
|
||||||
|
use crate::Element;
|
||||||
|
use jid::Jid;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
use std::net::IpAddr;
|
||||||
|
|
||||||
|
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, PartialEq)]
|
||||||
|
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, PartialEq)]
|
||||||
|
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::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::JINGLE_S5B)
|
||||||
|
.attr("cid", cid)
|
||||||
|
.build()]
|
||||||
|
}
|
||||||
|
TransportPayload::CandidateError => {
|
||||||
|
vec![Element::builder("candidate-error", ns::JINGLE_S5B).build()]
|
||||||
|
}
|
||||||
|
TransportPayload::CandidateUsed(cid) => {
|
||||||
|
vec![Element::builder("candidate-used", ns::JINGLE_S5B)
|
||||||
|
.attr("cid", cid)
|
||||||
|
.build()]
|
||||||
|
}
|
||||||
|
TransportPayload::ProxyError => {
|
||||||
|
vec![Element::builder("proxy-error", ns::JINGLE_S5B).build()]
|
||||||
|
}
|
||||||
|
TransportPayload::None => vec![],
|
||||||
|
})
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use jid::BareJid;
|
||||||
|
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_eq!(elem, 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::Bare(BareJid::new("coucou", "coucou")),
|
||||||
|
port: None,
|
||||||
|
priority: 0u32,
|
||||||
|
type_: Type::Direct,
|
||||||
|
}]),
|
||||||
|
};
|
||||||
|
let elem2: Element = transport.into();
|
||||||
|
assert_eq!(elem, elem2);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,130 @@
|
||||||
|
// 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,
|
||||||
|
|
||||||
|
/// ssrc-info for this source.
|
||||||
|
info: Option<SsrcInfo> = ("ssrc-info", JITSI_MEET) => SsrcInfo
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
impl Source {
|
||||||
|
/// Create a new SSMA Source element.
|
||||||
|
pub fn new(id: String) -> Source {
|
||||||
|
Source {
|
||||||
|
id,
|
||||||
|
parameters: Vec::new(),
|
||||||
|
info: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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!(
|
||||||
|
/// ssrc-info associated with a ssrc.
|
||||||
|
SsrcInfo, "ssrc-info", JITSI_MEET,
|
||||||
|
attributes: [
|
||||||
|
/// The owner of the ssrc.
|
||||||
|
owner: Required<String> = "owner"
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// Element grouping multiple ssrc.
|
||||||
|
Group, "ssrc-group", JINGLE_SSMA,
|
||||||
|
attributes: [
|
||||||
|
/// The semantics of this group.
|
||||||
|
semantics: Required<String> = "semantics",
|
||||||
|
],
|
||||||
|
children: [
|
||||||
|
/// The various ssrc concerned by this group.
|
||||||
|
sources: Vec<Source> = ("source", JINGLE_SSMA) => Source
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::Element;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "32")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(Source, 24);
|
||||||
|
assert_size!(Parameter, 24);
|
||||||
|
assert_size!(Group, 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "64")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(Source, 48);
|
||||||
|
assert_size!(Parameter, 48);
|
||||||
|
assert_size!(Group, 48);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_source() {
|
||||||
|
let elem: Element = "
|
||||||
|
<source ssrc='1656081975' xmlns='urn:xmpp:jingle:apps:rtp:ssma:0'>
|
||||||
|
<parameter name='cname' value='Yv/wvbCdsDW2Prgd'/>
|
||||||
|
<parameter name='msid' value='MLTJKIHilGn71fNQoszkQ4jlPTuS5vJyKVIv MLTJKIHilGn71fNQoszkQ4jlPTuS5vJyKVIva0'/>
|
||||||
|
</source>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let mut ssrc = Source::try_from(elem).unwrap();
|
||||||
|
assert_eq!(ssrc.id, "1656081975");
|
||||||
|
assert_eq!(ssrc.parameters.len(), 2);
|
||||||
|
let parameter = ssrc.parameters.pop().unwrap();
|
||||||
|
assert_eq!(parameter.name, "msid");
|
||||||
|
assert_eq!(
|
||||||
|
parameter.value.unwrap(),
|
||||||
|
"MLTJKIHilGn71fNQoszkQ4jlPTuS5vJyKVIv MLTJKIHilGn71fNQoszkQ4jlPTuS5vJyKVIva0"
|
||||||
|
);
|
||||||
|
let parameter = ssrc.parameters.pop().unwrap();
|
||||||
|
assert_eq!(parameter.name, "cname");
|
||||||
|
assert_eq!(parameter.value.unwrap(), "Yv/wvbCdsDW2Prgd");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_source_group() {
|
||||||
|
let elem: Element = "
|
||||||
|
<ssrc-group semantics='FID' xmlns='urn:xmpp:jingle:apps:rtp:ssma:0'>
|
||||||
|
<source ssrc='2301230316'/>
|
||||||
|
<source ssrc='386328120'/>
|
||||||
|
</ssrc-group>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let mut group = Group::try_from(elem).unwrap();
|
||||||
|
assert_eq!(group.semantics, "FID");
|
||||||
|
assert_eq!(group.sources.len(), 2);
|
||||||
|
let source = group.sources.pop().unwrap();
|
||||||
|
assert_eq!(source.id, "386328120");
|
||||||
|
let source = group.sources.pop().unwrap();
|
||||||
|
assert_eq!(source.id, "2301230316");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,229 @@
|
||||||
|
//! 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 crate::util::error::Error;
|
||||||
|
pub use jid::{BareJid, FullJid, Jid, JidParseError};
|
||||||
|
pub use minidom::Element;
|
||||||
|
|
||||||
|
/// 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-0177: Jingle Raw UDP Transport Method
|
||||||
|
pub mod jingle_raw_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-0294: Jingle RTP Header Extensions Negociation
|
||||||
|
pub mod jingle_rtp_hdrext;
|
||||||
|
|
||||||
|
/// 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-0338: Jingle Grouping Framework
|
||||||
|
pub mod jingle_grouping;
|
||||||
|
|
||||||
|
/// 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-0369: Mediated Information eXchange (MIX)
|
||||||
|
pub mod mix;
|
||||||
|
|
||||||
|
/// 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;
|
||||||
|
|
||||||
|
/// XEP-0441: Message Archive Management Preferences
|
||||||
|
pub mod mam_prefs;
|
|
@ -0,0 +1,296 @@
|
||||||
|
// Copyright (c) 2017-2021 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::forwarding::Forwarded;
|
||||||
|
use crate::iq::{IqGetPayload, IqResultPayload, IqSetPayload};
|
||||||
|
use crate::message::MessagePayload;
|
||||||
|
use crate::pubsub::NodeName;
|
||||||
|
use crate::rsm::{SetQuery, SetResult};
|
||||||
|
|
||||||
|
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 {}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::util::error::Error;
|
||||||
|
use minidom::Element;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
#[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);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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_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_empty() {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialize_query_with_form() {
|
||||||
|
let reference: Element = "<query xmlns='urn:xmpp:mam:2'><x xmlns='jabber:x:data' type='submit'><field xmlns='jabber:x:data' var='FORM_TYPE' type='hidden'><value xmlns='jabber:x:data'>urn:xmpp:mam:2</value></field><field xmlns='jabber:x:data' var='with'><value xmlns='jabber:x:data'>juliet@capulet.lit</value></field></x></query>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let elem: Element = "<x xmlns='jabber:x:data' type='submit'><field xmlns='jabber:x:data' var='FORM_TYPE' type='hidden'><value xmlns='jabber:x:data'>urn:xmpp:mam:2</value></field><field xmlns='jabber:x:data' var='with'><value xmlns='jabber:x:data'>juliet@capulet.lit</value></field></x>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let form = DataForm::try_from(elem).unwrap();
|
||||||
|
|
||||||
|
let query = Query {
|
||||||
|
queryid: None,
|
||||||
|
node: None,
|
||||||
|
set: None,
|
||||||
|
form: Some(form),
|
||||||
|
};
|
||||||
|
let serialized: Element = query.into();
|
||||||
|
assert_eq!(serialized, reference);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialize_result() {
|
||||||
|
let reference: Element = "<result xmlns='urn:xmpp:mam:2' queryid='f27' id='28482-98726-73623'><forwarded xmlns='urn:xmpp:forward:0'><delay xmlns='urn:xmpp:delay' stamp='2002-09-10T23:08:25+00:00'/><message xmlns='jabber:client' to='juliet@capulet.example/balcony' from='romeo@montague.example/home'/></forwarded></result>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let elem: Element = "<forwarded xmlns='urn:xmpp:forward:0'><delay xmlns='urn:xmpp:delay' stamp='2002-09-10T23:08:25+00:00'/><message xmlns='jabber:client' to='juliet@capulet.example/balcony' from='romeo@montague.example/home'/></forwarded>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let forwarded = Forwarded::try_from(elem).unwrap();
|
||||||
|
|
||||||
|
let result = Result_ {
|
||||||
|
id: String::from("28482-98726-73623"),
|
||||||
|
queryid: Some(QueryId(String::from("f27"))),
|
||||||
|
forwarded: forwarded,
|
||||||
|
};
|
||||||
|
let serialized: Element = result.into();
|
||||||
|
assert_eq!(serialized, reference);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialize_fin() {
|
||||||
|
let reference: Element = "<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();
|
||||||
|
|
||||||
|
let elem: Element = "<set xmlns='http://jabber.org/protocol/rsm'><first index='0'>28482-98726-73623</first><last>09af3-cc343-b409f</last></set>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let set = SetResult::try_from(elem).unwrap();
|
||||||
|
|
||||||
|
let fin = Fin {
|
||||||
|
set: set,
|
||||||
|
complete: Complete::default(),
|
||||||
|
};
|
||||||
|
let serialized: Element = fin.into();
|
||||||
|
assert_eq!(serialized, reference);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,175 @@
|
||||||
|
// Copyright (c) 2021 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
use crate::iq::{IqGetPayload, IqResultPayload, IqSetPayload};
|
||||||
|
use crate::ns;
|
||||||
|
use crate::util::error::Error;
|
||||||
|
use jid::Jid;
|
||||||
|
use minidom::{Element, Node};
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
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::MAM)
|
||||||
|
.append_all(
|
||||||
|
jids.into_iter()
|
||||||
|
.map(|jid| Element::builder("jid", ns::MAM).append(String::from(jid))),
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
|
.into_iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Prefs> for Element {
|
||||||
|
fn from(prefs: Prefs) -> Element {
|
||||||
|
Element::builder("prefs", 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 jid::BareJid;
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "32")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(DefaultPrefs, 1);
|
||||||
|
assert_size!(Prefs, 28);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "64")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(DefaultPrefs, 1);
|
||||||
|
assert_size!(Prefs, 56);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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!(prefs.always.is_empty());
|
||||||
|
assert!(prefs.never.is_empty());
|
||||||
|
|
||||||
|
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!(prefs.always.is_empty());
|
||||||
|
assert!(prefs.never.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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, [BareJid::new("romeo", "montague.lit")]);
|
||||||
|
assert_eq!(prefs.never, [BareJid::new("montague", "montague.lit")]);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,249 @@
|
||||||
|
// 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::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.to_string(), "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.to_string(), "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.to_string(), "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.to_string(), "invalid digit found in string");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_unknown_child() {
|
||||||
|
let elem: Element = "<media xmlns='urn:xmpp:media-element'><coucou/></media>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = MediaElement::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Unknown child in media element.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bad_uri() {
|
||||||
|
let elem: Element =
|
||||||
|
"<media xmlns='urn:xmpp:media-element'><uri>https://example.org/</uri></media>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = MediaElement::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Required attribute 'type' missing.");
|
||||||
|
|
||||||
|
let elem: Element = "<media xmlns='urn:xmpp:media-element'><uri type='text/html'/></media>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = MediaElement::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "URI missing in uri.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_xep_ex1() {
|
||||||
|
let elem: Element = r#"
|
||||||
|
<media xmlns='urn:xmpp:media-element'>
|
||||||
|
<uri type='audio/x-wav'>
|
||||||
|
http://victim.example.com/challenges/speech.wav?F3A6292C
|
||||||
|
</uri>
|
||||||
|
<uri type='audio/ogg; codecs=speex'>
|
||||||
|
cid:sha1+a15a505e360702b79c75a5f67773072ed392f52a@bob.xmpp.org
|
||||||
|
</uri>
|
||||||
|
<uri type='audio/mpeg'>
|
||||||
|
http://victim.example.com/challenges/speech.mp3?F3A6292C
|
||||||
|
</uri>
|
||||||
|
</media>"#
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let media = MediaElement::try_from(elem).unwrap();
|
||||||
|
assert!(media.width.is_none());
|
||||||
|
assert!(media.height.is_none());
|
||||||
|
assert_eq!(media.uris.len(), 3);
|
||||||
|
assert_eq!(media.uris[0].type_, "audio/x-wav");
|
||||||
|
assert_eq!(
|
||||||
|
media.uris[0].uri,
|
||||||
|
"http://victim.example.com/challenges/speech.wav?F3A6292C"
|
||||||
|
);
|
||||||
|
assert_eq!(media.uris[1].type_, "audio/ogg; codecs=speex");
|
||||||
|
assert_eq!(
|
||||||
|
media.uris[1].uri,
|
||||||
|
"cid:sha1+a15a505e360702b79c75a5f67773072ed392f52a@bob.xmpp.org"
|
||||||
|
);
|
||||||
|
assert_eq!(media.uris[2].type_, "audio/mpeg");
|
||||||
|
assert_eq!(
|
||||||
|
media.uris[2].uri,
|
||||||
|
"http://victim.example.com/challenges/speech.mp3?F3A6292C"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_xep_ex2() {
|
||||||
|
let elem: Element = r#"
|
||||||
|
<x xmlns='jabber:x:data' type='form'>
|
||||||
|
[ ... ]
|
||||||
|
<field var='ocr'>
|
||||||
|
<media xmlns='urn:xmpp:media-element'
|
||||||
|
height='80'
|
||||||
|
width='290'>
|
||||||
|
<uri type='image/jpeg'>
|
||||||
|
http://www.victim.com/challenges/ocr.jpeg?F3A6292C
|
||||||
|
</uri>
|
||||||
|
<uri type='image/jpeg'>
|
||||||
|
cid:sha1+f24030b8d91d233bac14777be5ab531ca3b9f102@bob.xmpp.org
|
||||||
|
</uri>
|
||||||
|
</media>
|
||||||
|
</field>
|
||||||
|
[ ... ]
|
||||||
|
</x>"#
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let form = DataForm::try_from(elem).unwrap();
|
||||||
|
assert_eq!(form.fields.len(), 1);
|
||||||
|
assert_eq!(form.fields[0].var, "ocr");
|
||||||
|
assert_eq!(form.fields[0].media[0].width, Some(290));
|
||||||
|
assert_eq!(form.fields[0].media[0].height, Some(80));
|
||||||
|
assert_eq!(form.fields[0].media[0].uris[0].type_, "image/jpeg");
|
||||||
|
assert_eq!(
|
||||||
|
form.fields[0].media[0].uris[0].uri,
|
||||||
|
"http://www.victim.com/challenges/ocr.jpeg?F3A6292C"
|
||||||
|
);
|
||||||
|
assert_eq!(form.fields[0].media[0].uris[1].type_, "image/jpeg");
|
||||||
|
assert_eq!(
|
||||||
|
form.fields[0].media[0].uris[1].uri,
|
||||||
|
"cid:sha1+f24030b8d91d233bac14777be5ab531ca3b9f102@bob.xmpp.org"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,407 @@
|
||||||
|
// 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::ns;
|
||||||
|
use crate::util::error::Error;
|
||||||
|
use crate::Element;
|
||||||
|
use jid::Jid;
|
||||||
|
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, PartialEq)]
|
||||||
|
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<J: Into<Option<Jid>>>(to: J) -> Message {
|
||||||
|
Message {
|
||||||
|
from: None,
|
||||||
|
to: to.into(),
|
||||||
|
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::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 jid::BareJid;
|
||||||
|
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_eq!(elem1, 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(Jid::Bare(BareJid::new("coucou", "example.org")));
|
||||||
|
message
|
||||||
|
.bodies
|
||||||
|
.insert(String::from(""), Body::from_str("Hello world!").unwrap());
|
||||||
|
let elem2 = message.into();
|
||||||
|
assert_eq!(elem, 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_eq!(elem1, elem2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_best_body() {
|
||||||
|
#[cfg(not(feature = "component"))]
|
||||||
|
let elem: Element = "<message xmlns='jabber:client' to='coucou@example.org' type='chat'><body xml:lang='de'>Hallo Welt!</body><body xml:lang='fr'>Salut le monde !</body><body>Hello world!</body></message>".parse().unwrap();
|
||||||
|
#[cfg(feature = "component")]
|
||||||
|
let elem: Element = "<message xmlns='jabber:component:accept' to='coucou@example.org' type='chat'><body>Hello world!</body></message>".parse().unwrap();
|
||||||
|
let message = Message::try_from(elem).unwrap();
|
||||||
|
|
||||||
|
// Tests basic feature.
|
||||||
|
{
|
||||||
|
let (lang, body) = message.get_best_body(vec!["fr"]).unwrap();
|
||||||
|
assert_eq!(lang, "fr");
|
||||||
|
assert_eq!(body, &Body::from_str("Salut le monde !").unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests order.
|
||||||
|
{
|
||||||
|
let (lang, body) = message.get_best_body(vec!["en", "de"]).unwrap();
|
||||||
|
assert_eq!(lang, "de");
|
||||||
|
assert_eq!(body, &Body::from_str("Hallo Welt!").unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests fallback.
|
||||||
|
{
|
||||||
|
let (lang, body) = message.get_best_body(vec![]).unwrap();
|
||||||
|
assert_eq!(lang, "");
|
||||||
|
assert_eq!(body, &Body::from_str("Hello world!").unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests fallback.
|
||||||
|
{
|
||||||
|
let (lang, body) = message.get_best_body(vec!["ja"]).unwrap();
|
||||||
|
assert_eq!(lang, "");
|
||||||
|
assert_eq!(body, &Body::from_str("Hello world!").unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
let message = Message::new(None);
|
||||||
|
|
||||||
|
// Tests without a body.
|
||||||
|
assert_eq!(message.get_best_body(vec!("ja")), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_attention() {
|
||||||
|
#[cfg(not(feature = "component"))]
|
||||||
|
let elem: Element = "<message xmlns='jabber:client' to='coucou@example.org' type='chat'><attention xmlns='urn:xmpp:attention:0'/></message>".parse().unwrap();
|
||||||
|
#[cfg(feature = "component")]
|
||||||
|
let elem: Element = "<message xmlns='jabber:component:accept' to='coucou@example.org' type='chat'><attention xmlns='urn:xmpp:attention:0'/></message>".parse().unwrap();
|
||||||
|
let elem1 = elem.clone();
|
||||||
|
let message = Message::try_from(elem).unwrap();
|
||||||
|
let elem2 = message.into();
|
||||||
|
assert_eq!(elem1, elem2);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,99 @@
|
||||||
|
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
use crate::message::MessagePayload;
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// Defines that the message containing this payload should replace a
|
||||||
|
/// previous message, identified by the id.
|
||||||
|
Replace, "replace", MESSAGE_CORRECT,
|
||||||
|
attributes: [
|
||||||
|
/// The 'id' attribute of the message getting corrected.
|
||||||
|
id: Required<String> = "id",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
impl MessagePayload for Replace {}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::util::error::Error;
|
||||||
|
use crate::Element;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "32")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(Replace, 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "64")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(Replace, 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_simple() {
|
||||||
|
let elem: Element = "<replace xmlns='urn:xmpp:message-correct:0' id='coucou'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
Replace::try_from(elem).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "disable-validation"))]
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_attribute() {
|
||||||
|
let elem: Element = "<replace xmlns='urn:xmpp:message-correct:0' coucou=''/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = Replace::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Unknown attribute in replace element.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_child() {
|
||||||
|
let elem: Element = "<replace xmlns='urn:xmpp:message-correct:0'><coucou/></replace>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = Replace::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Unknown child in replace element.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_id() {
|
||||||
|
let elem: Element = "<replace xmlns='urn:xmpp:message-correct:0'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = Replace::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Required attribute 'id' missing.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialise() {
|
||||||
|
let elem: Element = "<replace xmlns='urn:xmpp:message-correct:0' id='coucou'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let replace = Replace {
|
||||||
|
id: String::from("coucou"),
|
||||||
|
};
|
||||||
|
let elem2 = replace.into();
|
||||||
|
assert_eq!(elem, elem2);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,398 @@
|
||||||
|
// Copyright (c) 2020 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/.
|
||||||
|
|
||||||
|
// TODO: validate nicks by applying the “nickname” profile of the PRECIS OpaqueString class, as
|
||||||
|
// defined in RFC 7700.
|
||||||
|
|
||||||
|
use crate::iq::{IqResultPayload, IqSetPayload};
|
||||||
|
use crate::message::MessagePayload;
|
||||||
|
use crate::pubsub::{NodeName, PubSubPayload};
|
||||||
|
use jid::BareJid;
|
||||||
|
|
||||||
|
generate_id!(
|
||||||
|
/// The identifier a participant receives when joining a channel.
|
||||||
|
ParticipantId
|
||||||
|
);
|
||||||
|
|
||||||
|
impl ParticipantId {
|
||||||
|
/// Create a new ParticipantId.
|
||||||
|
pub fn new<P: Into<String>>(participant: P) -> ParticipantId {
|
||||||
|
ParticipantId(participant.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_id!(
|
||||||
|
/// A MIX channel identifier.
|
||||||
|
ChannelId
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// Represents a participant in a MIX channel, usually returned on the
|
||||||
|
/// urn:xmpp:mix:nodes:participants PubSub node.
|
||||||
|
Participant, "participant", MIX_CORE,
|
||||||
|
children: [
|
||||||
|
/// The nick of this participant.
|
||||||
|
nick: Required<String> = ("nick", MIX_CORE) => String,
|
||||||
|
|
||||||
|
/// The bare JID of this participant.
|
||||||
|
// TODO: should be a BareJid!
|
||||||
|
jid: Required<String> = ("jid", MIX_CORE) => String
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
impl PubSubPayload for Participant {}
|
||||||
|
|
||||||
|
impl Participant {
|
||||||
|
/// Create a new MIX participant.
|
||||||
|
pub fn new<J: Into<String>, N: Into<String>>(jid: J, nick: N) -> Participant {
|
||||||
|
Participant {
|
||||||
|
nick: nick.into(),
|
||||||
|
jid: jid.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// A node to subscribe to.
|
||||||
|
Subscribe, "subscribe", MIX_CORE,
|
||||||
|
attributes: [
|
||||||
|
/// The PubSub node to subscribe to.
|
||||||
|
node: Required<NodeName> = "node",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
impl Subscribe {
|
||||||
|
/// Create a new Subscribe element.
|
||||||
|
pub fn new<N: Into<String>>(node: N) -> Subscribe {
|
||||||
|
Subscribe {
|
||||||
|
node: NodeName(node.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// A request from a user’s server to join a MIX channel.
|
||||||
|
Join, "join", MIX_CORE,
|
||||||
|
attributes: [
|
||||||
|
/// The participant identifier returned by the MIX service on successful join.
|
||||||
|
id: Option<ParticipantId> = "id",
|
||||||
|
],
|
||||||
|
children: [
|
||||||
|
/// The nick requested by the user or set by the service.
|
||||||
|
nick: Required<String> = ("nick", MIX_CORE) => String,
|
||||||
|
|
||||||
|
/// Which MIX nodes to subscribe to.
|
||||||
|
subscribes: Vec<Subscribe> = ("subscribe", MIX_CORE) => Subscribe
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
impl IqSetPayload for Join {}
|
||||||
|
impl IqResultPayload for Join {}
|
||||||
|
|
||||||
|
impl Join {
|
||||||
|
/// Create a new Join element.
|
||||||
|
pub fn from_nick_and_nodes<N: Into<String>>(nick: N, nodes: &[&str]) -> Join {
|
||||||
|
let subscribes = nodes
|
||||||
|
.into_iter()
|
||||||
|
.cloned()
|
||||||
|
.map(|n| Subscribe::new(n))
|
||||||
|
.collect();
|
||||||
|
Join {
|
||||||
|
id: None,
|
||||||
|
nick: nick.into(),
|
||||||
|
subscribes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the JID on this update-subscription.
|
||||||
|
pub fn with_id<I: Into<String>>(mut self, id: I) -> Self {
|
||||||
|
self.id = Some(ParticipantId(id.into()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// Update a given subscription.
|
||||||
|
UpdateSubscription, "update-subscription", MIX_CORE,
|
||||||
|
attributes: [
|
||||||
|
/// The JID of the user to be affected.
|
||||||
|
// TODO: why is it not a participant id instead?
|
||||||
|
jid: Option<BareJid> = "jid",
|
||||||
|
],
|
||||||
|
children: [
|
||||||
|
/// The list of additional nodes to subscribe to.
|
||||||
|
// TODO: what happens when we are already subscribed? Also, how do we unsubscribe from
|
||||||
|
// just one?
|
||||||
|
subscribes: Vec<Subscribe> = ("subscribe", MIX_CORE) => Subscribe
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
impl IqSetPayload for UpdateSubscription {}
|
||||||
|
impl IqResultPayload for UpdateSubscription {}
|
||||||
|
|
||||||
|
impl UpdateSubscription {
|
||||||
|
/// Create a new UpdateSubscription element.
|
||||||
|
pub fn from_nodes(nodes: &[&str]) -> UpdateSubscription {
|
||||||
|
let subscribes = nodes
|
||||||
|
.into_iter()
|
||||||
|
.cloned()
|
||||||
|
.map(|n| Subscribe::new(n))
|
||||||
|
.collect();
|
||||||
|
UpdateSubscription {
|
||||||
|
jid: None,
|
||||||
|
subscribes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the JID on this update-subscription.
|
||||||
|
pub fn with_jid(mut self, jid: BareJid) -> Self {
|
||||||
|
self.jid = Some(jid);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_empty_element!(
|
||||||
|
/// Request to leave a given MIX channel. It will automatically unsubscribe the user from all
|
||||||
|
/// nodes on this channel.
|
||||||
|
Leave,
|
||||||
|
"leave",
|
||||||
|
MIX_CORE
|
||||||
|
);
|
||||||
|
|
||||||
|
impl IqSetPayload for Leave {}
|
||||||
|
impl IqResultPayload for Leave {}
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// A request to change the user’s nick.
|
||||||
|
SetNick, "setnick", MIX_CORE,
|
||||||
|
children: [
|
||||||
|
/// The new requested nick.
|
||||||
|
nick: Required<String> = ("nick", MIX_CORE) => String
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
impl IqSetPayload for SetNick {}
|
||||||
|
impl IqResultPayload for SetNick {}
|
||||||
|
|
||||||
|
impl SetNick {
|
||||||
|
/// Create a new SetNick element.
|
||||||
|
pub fn new<N: Into<String>>(nick: N) -> SetNick {
|
||||||
|
SetNick { nick: nick.into() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// Message payload describing who actually sent the message, since unlike in MUC, all messages
|
||||||
|
/// are sent from the channel’s JID.
|
||||||
|
Mix, "mix", MIX_CORE,
|
||||||
|
children: [
|
||||||
|
/// The nick of the user who said something.
|
||||||
|
nick: Required<String> = ("nick", MIX_CORE) => String,
|
||||||
|
|
||||||
|
/// The JID of the user who said something.
|
||||||
|
// TODO: should be a BareJid!
|
||||||
|
jid: Required<String> = ("jid", MIX_CORE) => String
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
impl MessagePayload for Mix {}
|
||||||
|
|
||||||
|
impl Mix {
|
||||||
|
/// Create a new Mix element.
|
||||||
|
pub fn new<N: Into<String>, J: Into<String>>(nick: N, jid: J) -> Mix {
|
||||||
|
Mix {
|
||||||
|
nick: nick.into(),
|
||||||
|
jid: jid.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// Create a new MIX channel.
|
||||||
|
Create, "create", MIX_CORE,
|
||||||
|
attributes: [
|
||||||
|
/// The requested channel identifier.
|
||||||
|
channel: Option<ChannelId> = "channel",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
impl IqSetPayload for Create {}
|
||||||
|
impl IqResultPayload for Create {}
|
||||||
|
|
||||||
|
impl Create {
|
||||||
|
/// Create a new ad-hoc Create element.
|
||||||
|
pub fn new() -> Create {
|
||||||
|
Create { channel: None }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new Create element with a channel identifier.
|
||||||
|
pub fn from_channel_id<C: Into<String>>(channel: C) -> Create {
|
||||||
|
Create {
|
||||||
|
channel: Some(ChannelId(channel.into())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// Destroy a given MIX channel.
|
||||||
|
Destroy, "destroy", MIX_CORE,
|
||||||
|
attributes: [
|
||||||
|
/// The channel identifier to be destroyed.
|
||||||
|
channel: Required<ChannelId> = "channel",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO: section 7.3.4, example 33, doesn’t mirror the <destroy/> in the iq result unlike every
|
||||||
|
// other section so far.
|
||||||
|
impl IqSetPayload for Destroy {}
|
||||||
|
|
||||||
|
impl Destroy {
|
||||||
|
/// Create a new Destroy element.
|
||||||
|
pub fn new<C: Into<String>>(channel: C) -> Destroy {
|
||||||
|
Destroy {
|
||||||
|
channel: ChannelId(channel.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::Element;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn participant() {
|
||||||
|
let elem: Element = "<participant xmlns='urn:xmpp:mix:core:1'><jid>foo@bar</jid><nick>coucou</nick></participant>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let participant = Participant::try_from(elem).unwrap();
|
||||||
|
assert_eq!(participant.nick, "coucou");
|
||||||
|
assert_eq!(participant.jid, "foo@bar");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn join() {
|
||||||
|
let elem: Element = "<join xmlns='urn:xmpp:mix:core:1'><subscribe node='urn:xmpp:mix:nodes:messages'/><subscribe node='urn:xmpp:mix:nodes:info'/><nick>coucou</nick></join>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let join = Join::try_from(elem).unwrap();
|
||||||
|
assert_eq!(join.nick, "coucou");
|
||||||
|
assert_eq!(join.id, None);
|
||||||
|
assert_eq!(join.subscribes.len(), 2);
|
||||||
|
assert_eq!(join.subscribes[0].node.0, "urn:xmpp:mix:nodes:messages");
|
||||||
|
assert_eq!(join.subscribes[1].node.0, "urn:xmpp:mix:nodes:info");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn update_subscription() {
|
||||||
|
let elem: Element = "<update-subscription xmlns='urn:xmpp:mix:core:1'><subscribe node='urn:xmpp:mix:nodes:participants'/></update-subscription>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let update_subscription = UpdateSubscription::try_from(elem).unwrap();
|
||||||
|
assert_eq!(update_subscription.jid, None);
|
||||||
|
assert_eq!(update_subscription.subscribes.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
update_subscription.subscribes[0].node.0,
|
||||||
|
"urn:xmpp:mix:nodes:participants"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn leave() {
|
||||||
|
let elem: Element = "<leave xmlns='urn:xmpp:mix:core:1'/>".parse().unwrap();
|
||||||
|
Leave::try_from(elem).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn setnick() {
|
||||||
|
let elem: Element = "<setnick xmlns='urn:xmpp:mix:core:1'><nick>coucou</nick></setnick>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let setnick = SetNick::try_from(elem).unwrap();
|
||||||
|
assert_eq!(setnick.nick, "coucou");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn message_mix() {
|
||||||
|
let elem: Element =
|
||||||
|
"<mix xmlns='urn:xmpp:mix:core:1'><jid>foo@bar</jid><nick>coucou</nick></mix>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let mix = Mix::try_from(elem).unwrap();
|
||||||
|
assert_eq!(mix.nick, "coucou");
|
||||||
|
assert_eq!(mix.jid, "foo@bar");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create() {
|
||||||
|
let elem: Element = "<create xmlns='urn:xmpp:mix:core:1' channel='coucou'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let create = Create::try_from(elem).unwrap();
|
||||||
|
assert_eq!(create.channel.unwrap().0, "coucou");
|
||||||
|
|
||||||
|
let elem: Element = "<create xmlns='urn:xmpp:mix:core:1'/>".parse().unwrap();
|
||||||
|
let create = Create::try_from(elem).unwrap();
|
||||||
|
assert_eq!(create.channel, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn destroy() {
|
||||||
|
let elem: Element = "<destroy xmlns='urn:xmpp:mix:core:1' channel='coucou'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let destroy = Destroy::try_from(elem).unwrap();
|
||||||
|
assert_eq!(destroy.channel.0, "coucou");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn serialise() {
|
||||||
|
let elem: Element = Join::from_nick_and_nodes("coucou", &["foo", "bar"]).into();
|
||||||
|
let xml = String::from(&elem);
|
||||||
|
assert_eq!(xml, "<join xmlns=\"urn:xmpp:mix:core:1\"><nick>coucou</nick><subscribe node=\"foo\"/><subscribe node=\"bar\"/></join>");
|
||||||
|
|
||||||
|
let elem: Element = UpdateSubscription::from_nodes(&["foo", "bar"]).into();
|
||||||
|
let xml = String::from(&elem);
|
||||||
|
assert_eq!(xml, "<update-subscription xmlns=\"urn:xmpp:mix:core:1\"><subscribe node=\"foo\"/><subscribe node=\"bar\"/></update-subscription>");
|
||||||
|
|
||||||
|
let elem: Element = Leave.into();
|
||||||
|
let xml = String::from(&elem);
|
||||||
|
assert_eq!(xml, "<leave xmlns=\"urn:xmpp:mix:core:1\"/>");
|
||||||
|
|
||||||
|
let elem: Element = SetNick::new("coucou").into();
|
||||||
|
let xml = String::from(&elem);
|
||||||
|
assert_eq!(
|
||||||
|
xml,
|
||||||
|
"<setnick xmlns=\"urn:xmpp:mix:core:1\"><nick>coucou</nick></setnick>"
|
||||||
|
);
|
||||||
|
|
||||||
|
let elem: Element = Mix::new("coucou", "coucou@example").into();
|
||||||
|
let xml = String::from(&elem);
|
||||||
|
assert_eq!(
|
||||||
|
xml,
|
||||||
|
"<mix xmlns=\"urn:xmpp:mix:core:1\"><nick>coucou</nick><jid>coucou@example</jid></mix>"
|
||||||
|
);
|
||||||
|
|
||||||
|
let elem: Element = Create::new().into();
|
||||||
|
let xml = String::from(&elem);
|
||||||
|
assert_eq!(xml, "<create xmlns=\"urn:xmpp:mix:core:1\"/>");
|
||||||
|
|
||||||
|
let elem: Element = Create::from_channel_id("coucou").into();
|
||||||
|
let xml = String::from(&elem);
|
||||||
|
assert_eq!(
|
||||||
|
xml,
|
||||||
|
"<create xmlns=\"urn:xmpp:mix:core:1\" channel=\"coucou\"/>"
|
||||||
|
);
|
||||||
|
|
||||||
|
let elem: Element = Destroy::new("coucou").into();
|
||||||
|
let xml = String::from(&elem);
|
||||||
|
assert_eq!(
|
||||||
|
xml,
|
||||||
|
"<destroy xmlns=\"urn:xmpp:mix:core:1\" channel=\"coucou\"/>"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,312 @@
|
||||||
|
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
generate_element_enum!(
|
||||||
|
/// Enum representing all of the possible values of the XEP-0107 moods.
|
||||||
|
MoodEnum, "mood", MOOD, {
|
||||||
|
/// Impressed with fear or apprehension; in fear; apprehensive.
|
||||||
|
Afraid => "afraid",
|
||||||
|
|
||||||
|
/// Astonished; confounded with fear, surprise or wonder.
|
||||||
|
Amazed => "amazed",
|
||||||
|
|
||||||
|
/// Inclined to love; having a propensity to love, or to sexual enjoyment; loving, fond, affectionate, passionate, lustful, sexual, etc.
|
||||||
|
Amorous => "amorous",
|
||||||
|
|
||||||
|
/// Displaying or feeling anger, i.e., a strong feeling of displeasure, hostility or antagonism towards someone or something, usually combined with an urge to harm.
|
||||||
|
Angry => "angry",
|
||||||
|
|
||||||
|
/// To be disturbed or irritated, especially by continued or repeated acts.
|
||||||
|
Annoyed => "annoyed",
|
||||||
|
|
||||||
|
/// Full of anxiety or disquietude; greatly concerned or solicitous, esp. respecting something future or unknown; being in painful suspense.
|
||||||
|
Anxious => "anxious",
|
||||||
|
|
||||||
|
/// To be stimulated in one's feelings, especially to be sexually stimulated.
|
||||||
|
Aroused => "aroused",
|
||||||
|
|
||||||
|
/// Feeling shame or guilt.
|
||||||
|
Ashamed => "ashamed",
|
||||||
|
|
||||||
|
/// Suffering from boredom; uninterested, without attention.
|
||||||
|
Bored => "bored",
|
||||||
|
|
||||||
|
/// Strong in the face of fear; courageous.
|
||||||
|
Brave => "brave",
|
||||||
|
|
||||||
|
/// Peaceful, quiet.
|
||||||
|
Calm => "calm",
|
||||||
|
|
||||||
|
/// Taking care or caution; tentative.
|
||||||
|
Cautious => "cautious",
|
||||||
|
|
||||||
|
/// Feeling the sensation of coldness, especially to the point of discomfort.
|
||||||
|
Cold => "cold",
|
||||||
|
|
||||||
|
/// Feeling very sure of or positive about something, especially about one's own capabilities.
|
||||||
|
Confident => "confident",
|
||||||
|
|
||||||
|
/// Chaotic, jumbled or muddled.
|
||||||
|
Confused => "confused",
|
||||||
|
|
||||||
|
/// Feeling introspective or thoughtful.
|
||||||
|
Contemplative => "contemplative",
|
||||||
|
|
||||||
|
/// Pleased at the satisfaction of a want or desire; satisfied.
|
||||||
|
Contented => "contented",
|
||||||
|
|
||||||
|
/// Grouchy, irritable; easily upset.
|
||||||
|
Cranky => "cranky",
|
||||||
|
|
||||||
|
/// Feeling out of control; feeling overly excited or enthusiastic.
|
||||||
|
Crazy => "crazy",
|
||||||
|
|
||||||
|
/// Feeling original, expressive, or imaginative.
|
||||||
|
Creative => "creative",
|
||||||
|
|
||||||
|
/// Inquisitive; tending to ask questions, investigate, or explore.
|
||||||
|
Curious => "curious",
|
||||||
|
|
||||||
|
/// Feeling sad and dispirited.
|
||||||
|
Dejected => "dejected",
|
||||||
|
|
||||||
|
/// Severely despondent and unhappy.
|
||||||
|
Depressed => "depressed",
|
||||||
|
|
||||||
|
/// Defeated of expectation or hope; let down.
|
||||||
|
Disappointed => "disappointed",
|
||||||
|
|
||||||
|
/// Filled with disgust; irritated and out of patience.
|
||||||
|
Disgusted => "disgusted",
|
||||||
|
|
||||||
|
/// Feeling a sudden or complete loss of courage in the face of trouble or danger.
|
||||||
|
Dismayed => "dismayed",
|
||||||
|
|
||||||
|
/// Having one's attention diverted; preoccupied.
|
||||||
|
Distracted => "distracted",
|
||||||
|
|
||||||
|
/// Having a feeling of shameful discomfort.
|
||||||
|
Embarrassed => "embarrassed",
|
||||||
|
|
||||||
|
/// Feeling pain by the excellence or good fortune of another.
|
||||||
|
Envious => "envious",
|
||||||
|
|
||||||
|
/// Having great enthusiasm.
|
||||||
|
Excited => "excited",
|
||||||
|
|
||||||
|
/// In the mood for flirting.
|
||||||
|
Flirtatious => "flirtatious",
|
||||||
|
|
||||||
|
/// Suffering from frustration; dissatisfied, agitated, or discontented because one is unable to perform an action or fulfill a desire.
|
||||||
|
Frustrated => "frustrated",
|
||||||
|
|
||||||
|
/// Feeling appreciation or thanks.
|
||||||
|
Grateful => "grateful",
|
||||||
|
|
||||||
|
/// Feeling very sad about something, especially something lost; mournful; sorrowful.
|
||||||
|
Grieving => "grieving",
|
||||||
|
|
||||||
|
/// Unhappy and irritable.
|
||||||
|
Grumpy => "grumpy",
|
||||||
|
|
||||||
|
/// Feeling responsible for wrongdoing; feeling blameworthy.
|
||||||
|
Guilty => "guilty",
|
||||||
|
|
||||||
|
/// Experiencing the effect of favourable fortune; having the feeling arising from the consciousness of well-being or of enjoyment; enjoying good of any kind, as peace, tranquillity, comfort; contented; joyous.
|
||||||
|
Happy => "happy",
|
||||||
|
|
||||||
|
/// Having a positive feeling, belief, or expectation that something wished for can or will happen.
|
||||||
|
Hopeful => "hopeful",
|
||||||
|
|
||||||
|
/// Feeling the sensation of heat, especially to the point of discomfort.
|
||||||
|
Hot => "hot",
|
||||||
|
|
||||||
|
/// Having or showing a modest or low estimate of one's own importance; feeling lowered in dignity or importance.
|
||||||
|
Humbled => "humbled",
|
||||||
|
|
||||||
|
/// Feeling deprived of dignity or self-respect.
|
||||||
|
Humiliated => "humiliated",
|
||||||
|
|
||||||
|
/// Having a physical need for food.
|
||||||
|
Hungry => "hungry",
|
||||||
|
|
||||||
|
/// Wounded, injured, or pained, whether physically or emotionally.
|
||||||
|
Hurt => "hurt",
|
||||||
|
|
||||||
|
/// Favourably affected by something or someone.
|
||||||
|
Impressed => "impressed",
|
||||||
|
|
||||||
|
/// Feeling amazement at something or someone; or feeling a combination of fear and reverence.
|
||||||
|
InAwe => "in_awe",
|
||||||
|
|
||||||
|
/// Feeling strong affection, care, liking, or attraction..
|
||||||
|
InLove => "in_love",
|
||||||
|
|
||||||
|
/// Showing anger or indignation, especially at something unjust or wrong.
|
||||||
|
Indignant => "indignant",
|
||||||
|
|
||||||
|
/// Showing great attention to something or someone; having or showing interest.
|
||||||
|
Interested => "interested",
|
||||||
|
|
||||||
|
/// Under the influence of alcohol; drunk.
|
||||||
|
Intoxicated => "intoxicated",
|
||||||
|
|
||||||
|
/// Feeling as if one cannot be defeated, overcome or denied.
|
||||||
|
Invincible => "invincible",
|
||||||
|
|
||||||
|
/// Fearful of being replaced in position or affection.
|
||||||
|
Jealous => "jealous",
|
||||||
|
|
||||||
|
/// Feeling isolated, empty, or abandoned.
|
||||||
|
Lonely => "lonely",
|
||||||
|
|
||||||
|
/// Unable to find one's way, either physically or emotionally.
|
||||||
|
Lost => "lost",
|
||||||
|
|
||||||
|
/// Feeling as if one will be favored by luck.
|
||||||
|
Lucky => "lucky",
|
||||||
|
|
||||||
|
/// Causing or intending to cause intentional harm; bearing ill will towards another; cruel; malicious.
|
||||||
|
Mean => "mean",
|
||||||
|
|
||||||
|
/// Given to sudden or frequent changes of mind or feeling; temperamental.
|
||||||
|
Moody => "moody",
|
||||||
|
|
||||||
|
/// Easily agitated or alarmed; apprehensive or anxious.
|
||||||
|
Nervous => "nervous",
|
||||||
|
|
||||||
|
/// Not having a strong mood or emotional state.
|
||||||
|
Neutral => "neutral",
|
||||||
|
|
||||||
|
/// Feeling emotionally hurt, displeased, or insulted.
|
||||||
|
Offended => "offended",
|
||||||
|
|
||||||
|
/// Feeling resentful anger caused by an extremely violent or vicious attack, or by an offensive, immoral, or indecent act.
|
||||||
|
Outraged => "outraged",
|
||||||
|
|
||||||
|
/// Interested in play; fun, recreational, unserious, lighthearted; joking, silly.
|
||||||
|
Playful => "playful",
|
||||||
|
|
||||||
|
/// Feeling a sense of one's own worth or accomplishment.
|
||||||
|
Proud => "proud",
|
||||||
|
|
||||||
|
/// Having an easy-going mood; not stressed; calm.
|
||||||
|
Relaxed => "relaxed",
|
||||||
|
|
||||||
|
/// Feeling uplifted because of the removal of stress or discomfort.
|
||||||
|
Relieved => "relieved",
|
||||||
|
|
||||||
|
/// Feeling regret or sadness for doing something wrong.
|
||||||
|
Remorseful => "remorseful",
|
||||||
|
|
||||||
|
/// Without rest; unable to be still or quiet; uneasy; continually moving.
|
||||||
|
Restless => "restless",
|
||||||
|
|
||||||
|
/// Feeling sorrow; sorrowful, mournful.
|
||||||
|
Sad => "sad",
|
||||||
|
|
||||||
|
/// Mocking and ironical.
|
||||||
|
Sarcastic => "sarcastic",
|
||||||
|
|
||||||
|
/// Pleased at the fulfillment of a need or desire.
|
||||||
|
Satisfied => "satisfied",
|
||||||
|
|
||||||
|
/// Without humor or expression of happiness; grave in manner or disposition; earnest; thoughtful; solemn.
|
||||||
|
Serious => "serious",
|
||||||
|
|
||||||
|
/// Surprised, startled, confused, or taken aback.
|
||||||
|
Shocked => "shocked",
|
||||||
|
|
||||||
|
/// Feeling easily frightened or scared; timid; reserved or coy.
|
||||||
|
Shy => "shy",
|
||||||
|
|
||||||
|
/// Feeling in poor health; ill.
|
||||||
|
Sick => "sick",
|
||||||
|
|
||||||
|
/// Feeling the need for sleep.
|
||||||
|
Sleepy => "sleepy",
|
||||||
|
|
||||||
|
/// Acting without planning; natural; impulsive.
|
||||||
|
Spontaneous => "spontaneous",
|
||||||
|
|
||||||
|
/// Suffering emotional pressure.
|
||||||
|
Stressed => "stressed",
|
||||||
|
|
||||||
|
/// Capable of producing great physical force; or, emotionally forceful, able, determined, unyielding.
|
||||||
|
Strong => "strong",
|
||||||
|
|
||||||
|
/// Experiencing a feeling caused by something unexpected.
|
||||||
|
Surprised => "surprised",
|
||||||
|
|
||||||
|
/// Showing appreciation or gratitude.
|
||||||
|
Thankful => "thankful",
|
||||||
|
|
||||||
|
/// Feeling the need to drink.
|
||||||
|
Thirsty => "thirsty",
|
||||||
|
|
||||||
|
/// In need of rest or sleep.
|
||||||
|
Tired => "tired",
|
||||||
|
|
||||||
|
/// [Feeling any emotion not defined here.]
|
||||||
|
Undefined => "undefined",
|
||||||
|
|
||||||
|
/// Lacking in force or ability, either physical or emotional.
|
||||||
|
Weak => "weak",
|
||||||
|
|
||||||
|
/// Thinking about unpleasant things that have happened or that might happen; feeling afraid and unhappy.
|
||||||
|
Worried => "worried",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_elem_id!(
|
||||||
|
/// Free-form text description of the mood.
|
||||||
|
Text,
|
||||||
|
"text",
|
||||||
|
MOOD
|
||||||
|
);
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::Element;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "32")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(MoodEnum, 1);
|
||||||
|
assert_size!(Text, 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "64")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(MoodEnum, 1);
|
||||||
|
assert_size!(Text, 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_simple() {
|
||||||
|
let elem: Element = "<happy xmlns='http://jabber.org/protocol/mood'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let mood = MoodEnum::try_from(elem).unwrap();
|
||||||
|
assert_eq!(mood, MoodEnum::Happy);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_text() {
|
||||||
|
let elem: Element = "<text xmlns='http://jabber.org/protocol/mood'>Yay!</text>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let elem2 = elem.clone();
|
||||||
|
let text = Text::try_from(elem).unwrap();
|
||||||
|
assert_eq!(text.0, String::from("Yay!"));
|
||||||
|
|
||||||
|
let elem3 = text.into();
|
||||||
|
assert_eq!(elem2, elem3);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
// Copyright (c) 2017 Maxime “pep” Buquet <pep@bouah.net>
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
/// The http://jabber.org/protocol/muc protocol.
|
||||||
|
pub mod muc;
|
||||||
|
|
||||||
|
/// The http://jabber.org/protocol/muc#user protocol.
|
||||||
|
pub mod user;
|
||||||
|
|
||||||
|
pub use self::muc::Muc;
|
||||||
|
pub use self::user::MucUser;
|
|
@ -0,0 +1,194 @@
|
||||||
|
// 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(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(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::error::Error;
|
||||||
|
use crate::Element;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
#[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_eq!(elem1, elem2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn history() {
|
||||||
|
let elem: Element = "
|
||||||
|
<x xmlns='http://jabber.org/protocol/muc'>
|
||||||
|
<history maxstanzas='0'/>
|
||||||
|
</x>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let muc = Muc::try_from(elem).unwrap();
|
||||||
|
let muc2 = Muc::new().with_history(History::new().with_maxstanzas(0));
|
||||||
|
assert_eq!(muc, muc2);
|
||||||
|
|
||||||
|
let history = muc.history.unwrap();
|
||||||
|
assert_eq!(history.maxstanzas, Some(0));
|
||||||
|
assert_eq!(history.maxchars, None);
|
||||||
|
assert_eq!(history.seconds, None);
|
||||||
|
assert_eq!(history.since, None);
|
||||||
|
|
||||||
|
let elem: Element = "
|
||||||
|
<x xmlns='http://jabber.org/protocol/muc'>
|
||||||
|
<history since='1970-01-01T00:00:00Z'/>
|
||||||
|
</x>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let muc = Muc::try_from(elem).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
muc.history.unwrap().since.unwrap(),
|
||||||
|
DateTime::from_str("1970-01-01T00:00:00+00:00").unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,725 @@
|
||||||
|
// 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::ns;
|
||||||
|
use crate::util::error::Error;
|
||||||
|
use crate::Element;
|
||||||
|
use jid::FullJid;
|
||||||
|
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::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::*;
|
||||||
|
|
||||||
|
#[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_eq!(elem, 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.to_string(), "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()))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialize_item() {
|
||||||
|
let reference: Element = "<item xmlns='http://jabber.org/protocol/muc#user' affiliation='member' role='moderator'><actor nick='foobar'/><continue thread='foobar'/><reason>foobar</reason></item>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let elem: Element = "<actor xmlns='http://jabber.org/protocol/muc#user' nick='foobar'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let actor = Actor::try_from(elem).unwrap();
|
||||||
|
|
||||||
|
let elem: Element =
|
||||||
|
"<continue xmlns='http://jabber.org/protocol/muc#user' thread='foobar'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let continue_ = Continue::try_from(elem).unwrap();
|
||||||
|
|
||||||
|
let elem: Element = "<reason xmlns='http://jabber.org/protocol/muc#user'>foobar</reason>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let reason = Reason::try_from(elem).unwrap();
|
||||||
|
|
||||||
|
let item = Item {
|
||||||
|
affiliation: Affiliation::Member,
|
||||||
|
role: Role::Moderator,
|
||||||
|
jid: None,
|
||||||
|
nick: None,
|
||||||
|
actor: Some(actor),
|
||||||
|
reason: Some(reason),
|
||||||
|
continue_: Some(continue_),
|
||||||
|
};
|
||||||
|
|
||||||
|
let serialized: Element = item.into();
|
||||||
|
assert_eq!(serialized, reference);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
// Copyright (c) 2018 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
generate_elem_id!(
|
||||||
|
/// Represents a global, memorable, friendly or informal name chosen by a user.
|
||||||
|
Nick,
|
||||||
|
"nick",
|
||||||
|
NICK
|
||||||
|
);
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
#[cfg(not(feature = "disable-validation"))]
|
||||||
|
use crate::util::error::Error;
|
||||||
|
use crate::Element;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "32")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(Nick, 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "64")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(Nick, 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_simple() {
|
||||||
|
let elem: Element = "<nick xmlns='http://jabber.org/protocol/nick'>Link Mauve</nick>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let nick = Nick::try_from(elem).unwrap();
|
||||||
|
assert_eq!(&nick.0, "Link Mauve");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialise() {
|
||||||
|
let elem1 = Element::from(Nick(String::from("Link Mauve")));
|
||||||
|
let elem2: Element = "<nick xmlns='http://jabber.org/protocol/nick'>Link Mauve</nick>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(elem1, elem2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "disable-validation"))]
|
||||||
|
#[test]
|
||||||
|
fn test_invalid() {
|
||||||
|
let elem: Element = "<nick xmlns='http://jabber.org/protocol/nick'><coucou/></nick>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = Nick::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Unknown child in nick element.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "disable-validation"))]
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_attribute() {
|
||||||
|
let elem: Element = "<nick xmlns='http://jabber.org/protocol/nick' coucou=''/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = Nick::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Unknown attribute in nick element.");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,271 @@
|
||||||
|
// 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 TLS: &str = "urn:ietf:params:xml:ns:xmpp-tls";
|
||||||
|
/// 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-0060: Publish-Subscribe node configuration
|
||||||
|
pub const PUBSUB_CONFIGURE: &str = "http://jabber.org/protocol/pubsub#node_config";
|
||||||
|
|
||||||
|
/// 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-0177: Jingle Raw UDP Transport Method
|
||||||
|
pub const JINGLE_RAW_UDP: &str = "urn:xmpp:jingle:transports:raw-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-0294: Jingle RTP Header Extensions Negociation
|
||||||
|
pub const JINGLE_RTP_HDREXT: &str = "urn:xmpp:jingle:apps:rtp:rtp-hdrext: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-0338: Jingle Grouping Framework
|
||||||
|
pub const JINGLE_GROUPING: &str = "urn:xmpp:jingle:apps:grouping: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-0369: Mediated Information eXchange (MIX)
|
||||||
|
pub const MIX_CORE: &str = "urn:xmpp:mix:core:1";
|
||||||
|
/// XEP-0369: Mediated Information eXchange (MIX)
|
||||||
|
pub const MIX_CORE_SEARCHABLE: &str = "urn:xmpp:mix:core:1#searchable";
|
||||||
|
/// XEP-0369: Mediated Information eXchange (MIX)
|
||||||
|
pub const MIX_CORE_CREATE_CHANNEL: &str = "urn:xmpp:mix:core:1#create-channel";
|
||||||
|
/// XEP-0369: Mediated Information eXchange (MIX)
|
||||||
|
pub const MIX_NODES_PRESENCE: &str = "urn:xmpp:mix:nodes:presence";
|
||||||
|
/// XEP-0369: Mediated Information eXchange (MIX)
|
||||||
|
pub const MIX_NODES_PARTICIPANTS: &str = "urn:xmpp:mix:nodes:participants";
|
||||||
|
/// XEP-0369: Mediated Information eXchange (MIX)
|
||||||
|
pub const MIX_NODES_MESSAGES: &str = "urn:xmpp:mix:nodes:messages";
|
||||||
|
/// XEP-0369: Mediated Information eXchange (MIX)
|
||||||
|
pub const MIX_NODES_CONFIG: &str = "urn:xmpp:mix:nodes:config";
|
||||||
|
/// XEP-0369: Mediated Information eXchange (MIX)
|
||||||
|
pub const MIX_NODES_INFO: &str = "urn:xmpp:mix:nodes:info";
|
||||||
|
|
||||||
|
/// 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:1";
|
||||||
|
/// 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;
|
||||||
|
|
||||||
|
/// Jitsi Meet general namespace
|
||||||
|
pub const JITSI_MEET: &str = "http://jitsi.org/jitmeet";
|
||||||
|
|
||||||
|
/// Jitsi Meet Colibri namespace
|
||||||
|
pub const JITSI_COLIBRI: &str = "http://jitsi.org/protocol/colibri";
|
|
@ -0,0 +1,91 @@
|
||||||
|
// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
use crate::message::MessagePayload;
|
||||||
|
use crate::presence::PresencePayload;
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// Unique identifier given to a MUC participant.
|
||||||
|
///
|
||||||
|
/// It allows clients to identify a MUC participant across reconnects and
|
||||||
|
/// renames. It thus prevents impersonification of anonymous users.
|
||||||
|
OccupantId, "occupant-id", OID,
|
||||||
|
|
||||||
|
attributes: [
|
||||||
|
/// The id associated to the sending user by the MUC service.
|
||||||
|
id: Required<String> = "id",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
impl MessagePayload for OccupantId {}
|
||||||
|
impl PresencePayload for OccupantId {}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::util::error::Error;
|
||||||
|
use crate::Element;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "32")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(OccupantId, 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "64")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(OccupantId, 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_simple() {
|
||||||
|
let elem: Element = "<occupant-id xmlns='urn:xmpp:occupant-id:0' id='coucou'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let origin_id = OccupantId::try_from(elem).unwrap();
|
||||||
|
assert_eq!(origin_id.id, "coucou");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_child() {
|
||||||
|
let elem: Element = "<occupant-id xmlns='urn:xmpp:occupant-id:0'><coucou/></occupant-id>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = OccupantId::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Unknown child in occupant-id element.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_id() {
|
||||||
|
let elem: Element = "<occupant-id xmlns='urn:xmpp:occupant-id:0'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = OccupantId::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Required attribute 'id' missing.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialise() {
|
||||||
|
let elem: Element = "<occupant-id xmlns='urn:xmpp:occupant-id:0' id='coucou'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let occupant_id = OccupantId {
|
||||||
|
id: String::from("coucou"),
|
||||||
|
};
|
||||||
|
let elem2 = occupant_id.into();
|
||||||
|
assert_eq!(elem, elem2);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,119 @@
|
||||||
|
// 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::pubsub::PubSubPayload;
|
||||||
|
use crate::util::helpers::Base64;
|
||||||
|
|
||||||
|
// 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 crate::pubsub::{
|
||||||
|
pubsub::{Item as PubSubItem, Publish},
|
||||||
|
Item, NodeName,
|
||||||
|
};
|
||||||
|
use crate::Element;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
#[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);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialize_pubkey() {
|
||||||
|
let reference: Element = "<pubkey xmlns='urn:xmpp:openpgp:0'><data>AAAA</data></pubkey>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let pubkey = PubKey {
|
||||||
|
date: None,
|
||||||
|
data: PubKeyData {
|
||||||
|
data: b"\0\0\0".to_vec(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let serialized: Element = pubkey.into();
|
||||||
|
assert_eq!(serialized, reference);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
// Copyright (c) 2017 Maxime “pep” Buquet <pep@bouah.net>
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
use crate::iq::IqGetPayload;
|
||||||
|
|
||||||
|
generate_empty_element!(
|
||||||
|
/// Represents a ping to the recipient, which must be answered with an
|
||||||
|
/// empty `<iq/>` or with an error.
|
||||||
|
Ping,
|
||||||
|
"ping",
|
||||||
|
PING
|
||||||
|
);
|
||||||
|
|
||||||
|
impl IqGetPayload for Ping {}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
#[cfg(not(feature = "disable-validation"))]
|
||||||
|
use crate::util::error::Error;
|
||||||
|
use crate::Element;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(Ping, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_simple() {
|
||||||
|
let elem: Element = "<ping xmlns='urn:xmpp:ping'/>".parse().unwrap();
|
||||||
|
Ping::try_from(elem).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialise() {
|
||||||
|
let elem1 = Element::from(Ping);
|
||||||
|
let elem2: Element = "<ping xmlns='urn:xmpp:ping'/>".parse().unwrap();
|
||||||
|
assert_eq!(elem1, elem2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "disable-validation"))]
|
||||||
|
#[test]
|
||||||
|
fn test_invalid() {
|
||||||
|
let elem: Element = "<ping xmlns='urn:xmpp:ping'><coucou/></ping>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = Ping::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Unknown child in ping element.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "disable-validation"))]
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_attribute() {
|
||||||
|
let elem: Element = "<ping xmlns='urn:xmpp:ping' coucou=''/>".parse().unwrap();
|
||||||
|
let error = Ping::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Unknown attribute in ping element.");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,655 @@
|
||||||
|
// 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::ns;
|
||||||
|
use crate::util::error::Error;
|
||||||
|
use jid::Jid;
|
||||||
|
use minidom::{Element, IntoAttributeValue, Node};
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
/// 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", ns::DEFAULT_NS)
|
||||||
|
.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::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", ns::DEFAULT_NS)
|
||||||
|
.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", ns::DEFAULT_NS)
|
||||||
|
.append(format!("{}", presence.priority)),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.append_all(presence.payloads.into_iter())
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
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_eq!(elem, elem2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_show() {
|
||||||
|
#[cfg(not(feature = "component"))]
|
||||||
|
let elem: Element = "<presence xmlns='jabber:client'><show>chat</show></presence>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
#[cfg(feature = "component")]
|
||||||
|
let elem: Element =
|
||||||
|
"<presence xmlns='jabber:component:accept'><show>chat</show></presence>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let presence = Presence::try_from(elem).unwrap();
|
||||||
|
assert_eq!(presence.payloads.len(), 0);
|
||||||
|
assert_eq!(presence.show, Some(Show::Chat));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_empty_show_value() {
|
||||||
|
#[cfg(not(feature = "component"))]
|
||||||
|
let elem: Element = "<presence xmlns='jabber:client'/>".parse().unwrap();
|
||||||
|
#[cfg(feature = "component")]
|
||||||
|
let elem: Element = "<presence xmlns='jabber:component:accept'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let presence = Presence::try_from(elem).unwrap();
|
||||||
|
assert_eq!(presence.show, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_missing_show_value() {
|
||||||
|
#[cfg(not(feature = "component"))]
|
||||||
|
let elem: Element = "<presence xmlns='jabber:client'><show/></presence>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
#[cfg(feature = "component")]
|
||||||
|
let elem: Element = "<presence xmlns='jabber:component:accept'><show/></presence>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = Presence::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Invalid value for show.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_show() {
|
||||||
|
// "online" used to be a pretty common mistake.
|
||||||
|
#[cfg(not(feature = "component"))]
|
||||||
|
let elem: Element = "<presence xmlns='jabber:client'><show>online</show></presence>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
#[cfg(feature = "component")]
|
||||||
|
let elem: Element =
|
||||||
|
"<presence xmlns='jabber:component:accept'><show>online</show></presence>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = Presence::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Invalid value for show.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_empty_status() {
|
||||||
|
#[cfg(not(feature = "component"))]
|
||||||
|
let elem: Element = "<presence xmlns='jabber:client'><status/></presence>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
#[cfg(feature = "component")]
|
||||||
|
let elem: Element = "<presence xmlns='jabber:component:accept'><status/></presence>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let presence = Presence::try_from(elem).unwrap();
|
||||||
|
assert_eq!(presence.payloads.len(), 0);
|
||||||
|
assert_eq!(presence.statuses.len(), 1);
|
||||||
|
assert_eq!(presence.statuses[""], "");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_status() {
|
||||||
|
#[cfg(not(feature = "component"))]
|
||||||
|
let elem: Element = "<presence xmlns='jabber:client'><status>Here!</status></presence>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
#[cfg(feature = "component")]
|
||||||
|
let elem: Element =
|
||||||
|
"<presence xmlns='jabber:component:accept'><status>Here!</status></presence>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let presence = Presence::try_from(elem).unwrap();
|
||||||
|
assert_eq!(presence.payloads.len(), 0);
|
||||||
|
assert_eq!(presence.statuses.len(), 1);
|
||||||
|
assert_eq!(presence.statuses[""], "Here!");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_multiple_statuses() {
|
||||||
|
#[cfg(not(feature = "component"))]
|
||||||
|
let elem: Element = "<presence xmlns='jabber:client'><status>Here!</status><status xml:lang='fr'>Là!</status></presence>".parse().unwrap();
|
||||||
|
#[cfg(feature = "component")]
|
||||||
|
let elem: Element = "<presence xmlns='jabber:component:accept'><status>Here!</status><status xml:lang='fr'>Là!</status></presence>".parse().unwrap();
|
||||||
|
let presence = Presence::try_from(elem).unwrap();
|
||||||
|
assert_eq!(presence.payloads.len(), 0);
|
||||||
|
assert_eq!(presence.statuses.len(), 2);
|
||||||
|
assert_eq!(presence.statuses[""], "Here!");
|
||||||
|
assert_eq!(presence.statuses["fr"], "Là!");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_multiple_statuses() {
|
||||||
|
#[cfg(not(feature = "component"))]
|
||||||
|
let elem: Element = "<presence xmlns='jabber:client'><status xml:lang='fr'>Here!</status><status xml:lang='fr'>Là!</status></presence>".parse().unwrap();
|
||||||
|
#[cfg(feature = "component")]
|
||||||
|
let elem: Element = "<presence xmlns='jabber:component:accept'><status xml:lang='fr'>Here!</status><status xml:lang='fr'>Là!</status></presence>".parse().unwrap();
|
||||||
|
let error = Presence::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
message,
|
||||||
|
"Status element present twice for the same xml:lang."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_priority() {
|
||||||
|
#[cfg(not(feature = "component"))]
|
||||||
|
let elem: Element = "<presence xmlns='jabber:client'><priority>-1</priority></presence>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
#[cfg(feature = "component")]
|
||||||
|
let elem: Element =
|
||||||
|
"<presence xmlns='jabber:component:accept'><priority>-1</priority></presence>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let presence = Presence::try_from(elem).unwrap();
|
||||||
|
assert_eq!(presence.payloads.len(), 0);
|
||||||
|
assert_eq!(presence.priority, -1i8);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_priority() {
|
||||||
|
#[cfg(not(feature = "component"))]
|
||||||
|
let elem: Element = "<presence xmlns='jabber:client'><priority>128</priority></presence>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
#[cfg(feature = "component")]
|
||||||
|
let elem: Element =
|
||||||
|
"<presence xmlns='jabber:component:accept'><priority>128</priority></presence>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = Presence::try_from(elem).unwrap_err();
|
||||||
|
match error {
|
||||||
|
Error::ParseIntError(_) => (),
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_unknown_child() {
|
||||||
|
#[cfg(not(feature = "component"))]
|
||||||
|
let elem: Element = "<presence xmlns='jabber:client'><test xmlns='invalid'/></presence>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
#[cfg(feature = "component")]
|
||||||
|
let elem: Element =
|
||||||
|
"<presence xmlns='jabber:component:accept'><test xmlns='invalid'/></presence>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let presence = Presence::try_from(elem).unwrap();
|
||||||
|
let payload = &presence.payloads[0];
|
||||||
|
assert!(payload.is("test", "invalid"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "disable-validation"))]
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_status_child() {
|
||||||
|
#[cfg(not(feature = "component"))]
|
||||||
|
let elem: Element = "<presence xmlns='jabber:client'><status><coucou/></status></presence>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
#[cfg(feature = "component")]
|
||||||
|
let elem: Element =
|
||||||
|
"<presence xmlns='jabber:component:accept'><status><coucou/></status></presence>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = Presence::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Unknown child in status element.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "disable-validation"))]
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_attribute() {
|
||||||
|
#[cfg(not(feature = "component"))]
|
||||||
|
let elem: Element = "<presence xmlns='jabber:client'><status coucou=''/></presence>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
#[cfg(feature = "component")]
|
||||||
|
let elem: Element =
|
||||||
|
"<presence xmlns='jabber:component:accept'><status coucou=''/></presence>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = Presence::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Unknown attribute in status element.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialise_status() {
|
||||||
|
let status = Status::from("Hello world!");
|
||||||
|
let mut presence = Presence::new(Type::Unavailable);
|
||||||
|
presence.statuses.insert(String::from(""), status);
|
||||||
|
let elem: Element = presence.into();
|
||||||
|
assert!(elem.is("presence", ns::DEFAULT_NS));
|
||||||
|
assert!(elem.children().next().unwrap().is("status", ns::DEFAULT_NS));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialise_priority() {
|
||||||
|
let presence = Presence::new(Type::None).with_priority(42);
|
||||||
|
let elem: Element = presence.into();
|
||||||
|
assert!(elem.is("presence", ns::DEFAULT_NS));
|
||||||
|
let priority = elem.children().next().unwrap();
|
||||||
|
assert!(priority.is("priority", ns::DEFAULT_NS));
|
||||||
|
assert_eq!(priority.text(), "42");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn presence_with_to() {
|
||||||
|
let presence = Presence::new(Type::None);
|
||||||
|
let elem: Element = presence.into();
|
||||||
|
assert_eq!(elem.attr("to"), None);
|
||||||
|
|
||||||
|
let presence = Presence::new(Type::None).with_to(Jid::Bare(BareJid::domain("localhost")));
|
||||||
|
let elem: Element = presence.into();
|
||||||
|
assert_eq!(elem.attr("to"), Some("localhost"));
|
||||||
|
|
||||||
|
let presence = Presence::new(Type::None).with_to(BareJid::domain("localhost"));
|
||||||
|
let elem: Element = presence.into();
|
||||||
|
assert_eq!(elem.attr("to"), Some("localhost"));
|
||||||
|
|
||||||
|
let presence = Presence::new(Type::None).with_to(Jid::Full(FullJid::new(
|
||||||
|
"test",
|
||||||
|
"localhost",
|
||||||
|
"coucou",
|
||||||
|
)));
|
||||||
|
let elem: Element = presence.into();
|
||||||
|
assert_eq!(elem.attr("to"), Some("test@localhost/coucou"));
|
||||||
|
|
||||||
|
let presence =
|
||||||
|
Presence::new(Type::None).with_to(FullJid::new("test", "localhost", "coucou"));
|
||||||
|
let elem: Element = presence.into();
|
||||||
|
assert_eq!(elem.attr("to"), Some("test@localhost/coucou"));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,416 @@
|
||||||
|
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
use crate::data_forms::DataForm;
|
||||||
|
use crate::date::DateTime;
|
||||||
|
use crate::message::MessagePayload;
|
||||||
|
use crate::ns;
|
||||||
|
use crate::pubsub::{Item as PubSubItem, ItemId, NodeName, Subscription, SubscriptionId};
|
||||||
|
use crate::util::error::Error;
|
||||||
|
use crate::Element;
|
||||||
|
use jid::Jid;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
/// Event wrapper for a PubSub `<item/>`.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Item(pub PubSubItem);
|
||||||
|
|
||||||
|
impl_pubsub_item!(Item, PUBSUB_EVENT);
|
||||||
|
|
||||||
|
/// Represents an event happening to a PubSub node.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum PubSubEvent {
|
||||||
|
/*
|
||||||
|
Collection {
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
/// This node’s configuration changed.
|
||||||
|
Configuration {
|
||||||
|
/// The node affected.
|
||||||
|
node: NodeName,
|
||||||
|
|
||||||
|
/// The new configuration of this node.
|
||||||
|
form: Option<DataForm>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// This node has been deleted, with an optional redirect to another node.
|
||||||
|
Delete {
|
||||||
|
/// The node affected.
|
||||||
|
node: NodeName,
|
||||||
|
|
||||||
|
/// The xmpp: URI of another node replacing this one.
|
||||||
|
redirect: Option<String>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Some items have been published on this node.
|
||||||
|
PublishedItems {
|
||||||
|
/// The node affected.
|
||||||
|
node: NodeName,
|
||||||
|
|
||||||
|
/// The list of published items.
|
||||||
|
items: Vec<Item>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Some items have been removed from this node.
|
||||||
|
RetractedItems {
|
||||||
|
/// The node affected.
|
||||||
|
node: NodeName,
|
||||||
|
|
||||||
|
/// The list of retracted items.
|
||||||
|
items: Vec<ItemId>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// All items of this node just got removed at once.
|
||||||
|
Purge {
|
||||||
|
/// The node affected.
|
||||||
|
node: NodeName,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// The user’s subscription to this node has changed.
|
||||||
|
Subscription {
|
||||||
|
/// The node affected.
|
||||||
|
node: NodeName,
|
||||||
|
|
||||||
|
/// The time at which this subscription will expire.
|
||||||
|
expiry: Option<DateTime>,
|
||||||
|
|
||||||
|
/// The JID of the user affected.
|
||||||
|
jid: Option<Jid>,
|
||||||
|
|
||||||
|
/// An identifier for this subscription.
|
||||||
|
subid: Option<SubscriptionId>,
|
||||||
|
|
||||||
|
/// The state of this subscription.
|
||||||
|
subscription: Option<Subscription>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_items(elem: Element, node: NodeName) -> Result<PubSubEvent, Error> {
|
||||||
|
let mut is_retract = None;
|
||||||
|
let mut items = vec![];
|
||||||
|
let mut retracts = vec![];
|
||||||
|
for child in elem.children() {
|
||||||
|
if child.is("item", ns::PUBSUB_EVENT) {
|
||||||
|
match is_retract {
|
||||||
|
None => is_retract = Some(false),
|
||||||
|
Some(false) => (),
|
||||||
|
Some(true) => {
|
||||||
|
return Err(Error::ParseError(
|
||||||
|
"Mix of item and retract in items element.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
items.push(Item::try_from(child.clone())?);
|
||||||
|
} else if child.is("retract", ns::PUBSUB_EVENT) {
|
||||||
|
match is_retract {
|
||||||
|
None => is_retract = Some(true),
|
||||||
|
Some(true) => (),
|
||||||
|
Some(false) => {
|
||||||
|
return Err(Error::ParseError(
|
||||||
|
"Mix of item and retract in items element.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
check_no_children!(child, "retract");
|
||||||
|
check_no_unknown_attributes!(child, "retract", ["id"]);
|
||||||
|
let id = get_attr!(child, "id", Required);
|
||||||
|
retracts.push(id);
|
||||||
|
} else {
|
||||||
|
return Err(Error::ParseError("Invalid child in items element."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(match is_retract {
|
||||||
|
Some(false) => PubSubEvent::PublishedItems { node, items },
|
||||||
|
Some(true) => PubSubEvent::RetractedItems {
|
||||||
|
node,
|
||||||
|
items: retracts,
|
||||||
|
},
|
||||||
|
None => return Err(Error::ParseError("Missing children in items element.")),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<Element> for PubSubEvent {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(elem: Element) -> Result<PubSubEvent, Error> {
|
||||||
|
check_self!(elem, "event", PUBSUB_EVENT);
|
||||||
|
check_no_attributes!(elem, "event");
|
||||||
|
|
||||||
|
let mut payload = None;
|
||||||
|
for child in elem.children() {
|
||||||
|
let node = get_attr!(child, "node", Required);
|
||||||
|
if child.is("configuration", ns::PUBSUB_EVENT) {
|
||||||
|
let mut payloads = child.children().cloned().collect::<Vec<_>>();
|
||||||
|
let item = payloads.pop();
|
||||||
|
if !payloads.is_empty() {
|
||||||
|
return Err(Error::ParseError(
|
||||||
|
"More than a single payload in configuration element.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let form = match item {
|
||||||
|
None => None,
|
||||||
|
Some(payload) => Some(DataForm::try_from(payload)?),
|
||||||
|
};
|
||||||
|
payload = Some(PubSubEvent::Configuration { node, form });
|
||||||
|
} else if child.is("delete", ns::PUBSUB_EVENT) {
|
||||||
|
let mut redirect = None;
|
||||||
|
for item in child.children() {
|
||||||
|
if item.is("redirect", ns::PUBSUB_EVENT) {
|
||||||
|
if redirect.is_some() {
|
||||||
|
return Err(Error::ParseError(
|
||||||
|
"More than one redirect in delete element.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let uri = get_attr!(item, "uri", Required);
|
||||||
|
redirect = Some(uri);
|
||||||
|
} else {
|
||||||
|
return Err(Error::ParseError("Unknown child in delete element."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
payload = Some(PubSubEvent::Delete { node, redirect });
|
||||||
|
} else if child.is("items", ns::PUBSUB_EVENT) {
|
||||||
|
payload = Some(parse_items(child.clone(), node)?);
|
||||||
|
} else if child.is("purge", ns::PUBSUB_EVENT) {
|
||||||
|
check_no_children!(child, "purge");
|
||||||
|
payload = Some(PubSubEvent::Purge { node });
|
||||||
|
} else if child.is("subscription", ns::PUBSUB_EVENT) {
|
||||||
|
check_no_children!(child, "subscription");
|
||||||
|
payload = Some(PubSubEvent::Subscription {
|
||||||
|
node,
|
||||||
|
expiry: get_attr!(child, "expiry", Option),
|
||||||
|
jid: get_attr!(child, "jid", Option),
|
||||||
|
subid: get_attr!(child, "subid", Option),
|
||||||
|
subscription: get_attr!(child, "subscription", Option),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return Err(Error::ParseError("Unknown child in event element."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(payload.ok_or(Error::ParseError("No payload in event element."))?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PubSubEvent> for Element {
|
||||||
|
fn from(event: PubSubEvent) -> Element {
|
||||||
|
let payload = match event {
|
||||||
|
PubSubEvent::Configuration { node, form } => {
|
||||||
|
Element::builder("configuration", ns::PUBSUB_EVENT)
|
||||||
|
.attr("node", node)
|
||||||
|
.append_all(form.map(Element::from))
|
||||||
|
}
|
||||||
|
PubSubEvent::Delete { node, redirect } => Element::builder("purge", ns::PUBSUB_EVENT)
|
||||||
|
.attr("node", node)
|
||||||
|
.append_all(redirect.map(|redirect| {
|
||||||
|
Element::builder("redirect", ns::PUBSUB_EVENT).attr("uri", redirect)
|
||||||
|
})),
|
||||||
|
PubSubEvent::PublishedItems { node, items } => {
|
||||||
|
Element::builder("items", ns::PUBSUB_EVENT)
|
||||||
|
.attr("node", node)
|
||||||
|
.append_all(items.into_iter())
|
||||||
|
}
|
||||||
|
PubSubEvent::RetractedItems { node, items } => {
|
||||||
|
Element::builder("items", ns::PUBSUB_EVENT)
|
||||||
|
.attr("node", node)
|
||||||
|
.append_all(
|
||||||
|
items
|
||||||
|
.into_iter()
|
||||||
|
.map(|id| Element::builder("retract", ns::PUBSUB_EVENT).attr("id", id)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
PubSubEvent::Purge { node } => {
|
||||||
|
Element::builder("purge", ns::PUBSUB_EVENT).attr("node", node)
|
||||||
|
}
|
||||||
|
PubSubEvent::Subscription {
|
||||||
|
node,
|
||||||
|
expiry,
|
||||||
|
jid,
|
||||||
|
subid,
|
||||||
|
subscription,
|
||||||
|
} => Element::builder("subscription", ns::PUBSUB_EVENT)
|
||||||
|
.attr("node", node)
|
||||||
|
.attr("expiry", expiry)
|
||||||
|
.attr("jid", jid)
|
||||||
|
.attr("subid", subid)
|
||||||
|
.attr("subscription", subscription),
|
||||||
|
};
|
||||||
|
Element::builder("event", ns::PUBSUB_EVENT)
|
||||||
|
.append(payload)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessagePayload for PubSubEvent {}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use jid::BareJid;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_items() {
|
||||||
|
let elem: Element =
|
||||||
|
"<event xmlns='http://jabber.org/protocol/pubsub#event'><items node='coucou'/></event>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = PubSubEvent::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Missing children in items element.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_simple_items() {
|
||||||
|
let elem: Element = "<event xmlns='http://jabber.org/protocol/pubsub#event'><items node='coucou'><item id='test' publisher='test@coucou'/></items></event>".parse().unwrap();
|
||||||
|
let event = PubSubEvent::try_from(elem).unwrap();
|
||||||
|
match event {
|
||||||
|
PubSubEvent::PublishedItems { node, items } => {
|
||||||
|
assert_eq!(node, NodeName(String::from("coucou")));
|
||||||
|
assert_eq!(items[0].id, Some(ItemId(String::from("test"))));
|
||||||
|
assert_eq!(
|
||||||
|
items[0].publisher.clone().unwrap(),
|
||||||
|
BareJid::new("test", "coucou")
|
||||||
|
);
|
||||||
|
assert_eq!(items[0].payload, None);
|
||||||
|
}
|
||||||
|
_ => panic!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_simple_pep() {
|
||||||
|
let elem: Element = "<event xmlns='http://jabber.org/protocol/pubsub#event'><items node='something'><item><foreign xmlns='example:namespace'/></item></items></event>".parse().unwrap();
|
||||||
|
let event = PubSubEvent::try_from(elem).unwrap();
|
||||||
|
match event {
|
||||||
|
PubSubEvent::PublishedItems { node, items } => {
|
||||||
|
assert_eq!(node, NodeName(String::from("something")));
|
||||||
|
assert_eq!(items[0].id, None);
|
||||||
|
assert_eq!(items[0].publisher, None);
|
||||||
|
match items[0].payload {
|
||||||
|
Some(ref elem) => assert!(elem.is("foreign", "example:namespace")),
|
||||||
|
_ => panic!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => panic!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_simple_retract() {
|
||||||
|
let elem: Element = "<event xmlns='http://jabber.org/protocol/pubsub#event'><items node='something'><retract id='coucou'/><retract id='test'/></items></event>".parse().unwrap();
|
||||||
|
let event = PubSubEvent::try_from(elem).unwrap();
|
||||||
|
match event {
|
||||||
|
PubSubEvent::RetractedItems { node, items } => {
|
||||||
|
assert_eq!(node, NodeName(String::from("something")));
|
||||||
|
assert_eq!(items[0], ItemId(String::from("coucou")));
|
||||||
|
assert_eq!(items[1], ItemId(String::from("test")));
|
||||||
|
}
|
||||||
|
_ => panic!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_simple_delete() {
|
||||||
|
let elem: Element = "<event xmlns='http://jabber.org/protocol/pubsub#event'><delete node='coucou'><redirect uri='hello'/></delete></event>".parse().unwrap();
|
||||||
|
let event = PubSubEvent::try_from(elem).unwrap();
|
||||||
|
match event {
|
||||||
|
PubSubEvent::Delete { node, redirect } => {
|
||||||
|
assert_eq!(node, NodeName(String::from("coucou")));
|
||||||
|
assert_eq!(redirect, Some(String::from("hello")));
|
||||||
|
}
|
||||||
|
_ => panic!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_simple_purge() {
|
||||||
|
let elem: Element =
|
||||||
|
"<event xmlns='http://jabber.org/protocol/pubsub#event'><purge node='coucou'/></event>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let event = PubSubEvent::try_from(elem).unwrap();
|
||||||
|
match event {
|
||||||
|
PubSubEvent::Purge { node } => {
|
||||||
|
assert_eq!(node, NodeName(String::from("coucou")));
|
||||||
|
}
|
||||||
|
_ => panic!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_simple_configure() {
|
||||||
|
let elem: Element = "<event xmlns='http://jabber.org/protocol/pubsub#event'><configuration node='coucou'><x xmlns='jabber:x:data' type='result'><field var='FORM_TYPE' type='hidden'><value>http://jabber.org/protocol/pubsub#node_config</value></field></x></configuration></event>".parse().unwrap();
|
||||||
|
let event = PubSubEvent::try_from(elem).unwrap();
|
||||||
|
match event {
|
||||||
|
PubSubEvent::Configuration { node, form: _ } => {
|
||||||
|
assert_eq!(node, NodeName(String::from("coucou")));
|
||||||
|
//assert_eq!(form.type_, Result_);
|
||||||
|
}
|
||||||
|
_ => panic!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid() {
|
||||||
|
let elem: Element =
|
||||||
|
"<event xmlns='http://jabber.org/protocol/pubsub#event'><coucou node='test'/></event>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = PubSubEvent::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Unknown child in event element.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "disable-validation"))]
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_attribute() {
|
||||||
|
let elem: Element = "<event xmlns='http://jabber.org/protocol/pubsub#event' coucou=''/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = PubSubEvent::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Unknown attribute in event element.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ex221_subscription() {
|
||||||
|
let elem: Element = "<event xmlns='http://jabber.org/protocol/pubsub#event'><subscription expiry='2006-02-28T23:59:59+00:00' jid='francisco@denmark.lit' node='princely_musings' subid='ba49252aaa4f5d320c24d3766f0bdcade78c78d3' subscription='subscribed'/></event>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let event = PubSubEvent::try_from(elem.clone()).unwrap();
|
||||||
|
match event.clone() {
|
||||||
|
PubSubEvent::Subscription {
|
||||||
|
node,
|
||||||
|
expiry,
|
||||||
|
jid,
|
||||||
|
subid,
|
||||||
|
subscription,
|
||||||
|
} => {
|
||||||
|
assert_eq!(node, NodeName(String::from("princely_musings")));
|
||||||
|
assert_eq!(
|
||||||
|
subid,
|
||||||
|
Some(SubscriptionId(String::from(
|
||||||
|
"ba49252aaa4f5d320c24d3766f0bdcade78c78d3"
|
||||||
|
)))
|
||||||
|
);
|
||||||
|
assert_eq!(subscription, Some(Subscription::Subscribed));
|
||||||
|
assert_eq!(jid.unwrap(), BareJid::new("francisco", "denmark.lit"));
|
||||||
|
assert_eq!(expiry, Some("2006-02-28T23:59:59Z".parse().unwrap()));
|
||||||
|
}
|
||||||
|
_ => panic!(),
|
||||||
|
}
|
||||||
|
|
||||||
|
let elem2: Element = event.into();
|
||||||
|
assert_eq!(elem, elem2);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,107 @@
|
||||||
|
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
/// The `http://jabber.org/protocol/pubsub#event` protocol.
|
||||||
|
pub mod event;
|
||||||
|
|
||||||
|
/// The `http://jabber.org/protocol/pubsub#owner` protocol.
|
||||||
|
pub mod owner;
|
||||||
|
|
||||||
|
/// The `http://jabber.org/protocol/pubsub` protocol.
|
||||||
|
pub mod pubsub;
|
||||||
|
|
||||||
|
pub use self::event::PubSubEvent;
|
||||||
|
pub use self::owner::PubSubOwner;
|
||||||
|
pub use self::pubsub::PubSub;
|
||||||
|
|
||||||
|
use crate::{Element, Jid};
|
||||||
|
|
||||||
|
generate_id!(
|
||||||
|
/// The name of a PubSub node, used to identify it on a JID.
|
||||||
|
NodeName
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_id!(
|
||||||
|
/// The identifier of an item, which is unique per node.
|
||||||
|
ItemId
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_id!(
|
||||||
|
/// The identifier of a subscription to a PubSub node.
|
||||||
|
SubscriptionId
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_attribute!(
|
||||||
|
/// The state of a subscription to a node.
|
||||||
|
Subscription, "subscription", {
|
||||||
|
/// The user is not subscribed to this node.
|
||||||
|
None => "none",
|
||||||
|
|
||||||
|
/// The user’s subscription to this node is still pending.
|
||||||
|
Pending => "pending",
|
||||||
|
|
||||||
|
/// The user is subscribed to this node.
|
||||||
|
Subscribed => "subscribed",
|
||||||
|
|
||||||
|
/// The user’s subscription to this node will only be valid once
|
||||||
|
/// configured.
|
||||||
|
Unconfigured => "unconfigured",
|
||||||
|
}, Default = None
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_attribute!(
|
||||||
|
/// A list of possible affiliations to a node.
|
||||||
|
AffiliationAttribute, "affiliation", {
|
||||||
|
/// You are a member of this node, you can subscribe and retrieve items.
|
||||||
|
Member => "member",
|
||||||
|
|
||||||
|
/// You don’t have a specific affiliation with this node, you can only subscribe to it.
|
||||||
|
None => "none",
|
||||||
|
|
||||||
|
/// You are banned from this node.
|
||||||
|
Outcast => "outcast",
|
||||||
|
|
||||||
|
/// You are an owner of this node, and can do anything with it.
|
||||||
|
Owner => "owner",
|
||||||
|
|
||||||
|
/// You are a publisher on this node, you can publish and retract items to it.
|
||||||
|
Publisher => "publisher",
|
||||||
|
|
||||||
|
/// You can publish and retract items on this node, but not subscribe or retrieve items.
|
||||||
|
PublishOnly => "publish-only",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/// An item from a PubSub node.
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct Item {
|
||||||
|
/// The identifier for this item, unique per node.
|
||||||
|
pub id: Option<ItemId>,
|
||||||
|
|
||||||
|
/// The JID of the entity who published this item.
|
||||||
|
pub publisher: Option<Jid>,
|
||||||
|
|
||||||
|
/// The payload of this item, in an arbitrary namespace.
|
||||||
|
pub payload: Option<Element>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Item {
|
||||||
|
/// Create a new item, accepting only payloads implementing `PubSubPayload`.
|
||||||
|
pub fn new<P: PubSubPayload>(
|
||||||
|
id: Option<ItemId>,
|
||||||
|
publisher: Option<Jid>,
|
||||||
|
payload: Option<P>,
|
||||||
|
) -> Item {
|
||||||
|
Item {
|
||||||
|
id,
|
||||||
|
publisher,
|
||||||
|
payload: payload.map(Into::into),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This trait should be implemented on any element which can be included as a PubSub payload.
|
||||||
|
pub trait PubSubPayload: ::std::convert::TryFrom<crate::Element> + Into<crate::Element> {}
|
|
@ -0,0 +1,364 @@
|
||||||
|
// Copyright (c) 2020 Paul Fariello <paul@fariello.eu>
|
||||||
|
// Copyright (c) 2018 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
use crate::data_forms::DataForm;
|
||||||
|
use crate::iq::{IqGetPayload, IqResultPayload, IqSetPayload};
|
||||||
|
use crate::ns;
|
||||||
|
use crate::pubsub::{AffiliationAttribute, NodeName, Subscription};
|
||||||
|
use crate::util::error::Error;
|
||||||
|
use crate::Element;
|
||||||
|
use jid::Jid;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// A list of affiliations you have on a service, or on a node.
|
||||||
|
Affiliations, "affiliations", PUBSUB_OWNER,
|
||||||
|
attributes: [
|
||||||
|
/// The node name this request pertains to.
|
||||||
|
node: Required<NodeName> = "node",
|
||||||
|
],
|
||||||
|
children: [
|
||||||
|
/// The actual list of affiliation elements.
|
||||||
|
affiliations: Vec<Affiliation> = ("affiliation", PUBSUB_OWNER) => Affiliation
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// An affiliation element.
|
||||||
|
Affiliation, "affiliation", PUBSUB_OWNER,
|
||||||
|
attributes: [
|
||||||
|
/// The node this affiliation pertains to.
|
||||||
|
jid: Required<Jid> = "jid",
|
||||||
|
|
||||||
|
/// The affiliation you currently have on this node.
|
||||||
|
affiliation: Required<AffiliationAttribute> = "affiliation",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// Request to configure a node.
|
||||||
|
Configure, "configure", PUBSUB_OWNER,
|
||||||
|
attributes: [
|
||||||
|
/// The node to be configured.
|
||||||
|
node: Option<NodeName> = "node",
|
||||||
|
],
|
||||||
|
children: [
|
||||||
|
/// The form to configure it.
|
||||||
|
form: Option<DataForm> = ("x", DATA_FORMS) => DataForm
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// Request to change default configuration.
|
||||||
|
Default, "default", PUBSUB_OWNER,
|
||||||
|
children: [
|
||||||
|
/// The form to configure it.
|
||||||
|
form: Option<DataForm> = ("x", DATA_FORMS) => DataForm
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// Request to delete a node.
|
||||||
|
Delete, "delete", PUBSUB_OWNER,
|
||||||
|
attributes: [
|
||||||
|
/// The node to be configured.
|
||||||
|
node: Required<NodeName> = "node",
|
||||||
|
],
|
||||||
|
children: [
|
||||||
|
/// Redirection to replace the deleted node.
|
||||||
|
redirect: Option<Redirect> = ("redirect", PUBSUB_OWNER) => Redirect
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// A redirect element.
|
||||||
|
Redirect, "redirect", PUBSUB_OWNER,
|
||||||
|
attributes: [
|
||||||
|
/// The node this node will be redirected to.
|
||||||
|
uri: Required<String> = "uri",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// Request to delete a node.
|
||||||
|
Purge, "purge", PUBSUB_OWNER,
|
||||||
|
attributes: [
|
||||||
|
/// The node to be configured.
|
||||||
|
node: Required<NodeName> = "node",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// A request for current subscriptions.
|
||||||
|
Subscriptions, "subscriptions", PUBSUB_OWNER,
|
||||||
|
attributes: [
|
||||||
|
/// The node to query.
|
||||||
|
node: Required<NodeName> = "node",
|
||||||
|
],
|
||||||
|
children: [
|
||||||
|
/// The list of subscription elements returned.
|
||||||
|
subscriptions: Vec<SubscriptionElem> = ("subscription", PUBSUB_OWNER) => SubscriptionElem
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// A subscription element, describing the state of a subscription.
|
||||||
|
SubscriptionElem, "subscription", PUBSUB_OWNER,
|
||||||
|
attributes: [
|
||||||
|
/// The JID affected by this subscription.
|
||||||
|
jid: Required<Jid> = "jid",
|
||||||
|
|
||||||
|
/// The state of the subscription.
|
||||||
|
subscription: Required<Subscription> = "subscription",
|
||||||
|
|
||||||
|
/// Subscription unique id.
|
||||||
|
subid: Option<String> = "subid",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Main payload used to communicate with a PubSubOwner service.
|
||||||
|
///
|
||||||
|
/// `<pubsub xmlns="http://jabber.org/protocol/pubsub#owner"/>`
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum PubSubOwner {
|
||||||
|
/// Manage the affiliations of a node.
|
||||||
|
Affiliations(Affiliations),
|
||||||
|
/// Request to configure a node, with optional suggested name and suggested configuration.
|
||||||
|
Configure(Configure),
|
||||||
|
/// Request the default node configuration.
|
||||||
|
Default(Default),
|
||||||
|
/// Delete a node.
|
||||||
|
Delete(Delete),
|
||||||
|
/// Purge all items from node.
|
||||||
|
Purge(Purge),
|
||||||
|
/// Request subscriptions of a node.
|
||||||
|
Subscriptions(Subscriptions),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IqGetPayload for PubSubOwner {}
|
||||||
|
impl IqSetPayload for PubSubOwner {}
|
||||||
|
impl IqResultPayload for PubSubOwner {}
|
||||||
|
|
||||||
|
impl TryFrom<Element> for PubSubOwner {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(elem: Element) -> Result<PubSubOwner, Error> {
|
||||||
|
check_self!(elem, "pubsub", PUBSUB_OWNER);
|
||||||
|
check_no_attributes!(elem, "pubsub");
|
||||||
|
|
||||||
|
let mut payload = None;
|
||||||
|
for child in elem.children() {
|
||||||
|
if child.is("configure", ns::PUBSUB_OWNER) {
|
||||||
|
if payload.is_some() {
|
||||||
|
return Err(Error::ParseError(
|
||||||
|
"Payload is already defined in pubsub owner element.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let configure = Configure::try_from(child.clone())?;
|
||||||
|
payload = Some(PubSubOwner::Configure(configure));
|
||||||
|
} else {
|
||||||
|
return Err(Error::ParseError("Unknown child in pubsub element."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(payload.ok_or(Error::ParseError("No payload in pubsub element."))?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PubSubOwner> for Element {
|
||||||
|
fn from(pubsub: PubSubOwner) -> Element {
|
||||||
|
Element::builder("pubsub", ns::PUBSUB_OWNER)
|
||||||
|
.append_all(match pubsub {
|
||||||
|
PubSubOwner::Affiliations(affiliations) => vec![Element::from(affiliations)],
|
||||||
|
PubSubOwner::Configure(configure) => vec![Element::from(configure)],
|
||||||
|
PubSubOwner::Default(default) => vec![Element::from(default)],
|
||||||
|
PubSubOwner::Delete(delete) => vec![Element::from(delete)],
|
||||||
|
PubSubOwner::Purge(purge) => vec![Element::from(purge)],
|
||||||
|
PubSubOwner::Subscriptions(subscriptions) => vec![Element::from(subscriptions)],
|
||||||
|
})
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::data_forms::{DataForm, DataFormType, Field, FieldType};
|
||||||
|
use jid::BareJid;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn affiliations() {
|
||||||
|
let elem: Element = "<pubsub xmlns='http://jabber.org/protocol/pubsub#owner'><affiliations node='foo'><affiliation jid='hamlet@denmark.lit' affiliation='owner'/><affiliation jid='polonius@denmark.lit' affiliation='outcast'/></affiliations></pubsub>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let elem1 = elem.clone();
|
||||||
|
|
||||||
|
let pubsub = PubSubOwner::Affiliations(Affiliations {
|
||||||
|
node: NodeName(String::from("foo")),
|
||||||
|
affiliations: vec![
|
||||||
|
Affiliation {
|
||||||
|
jid: Jid::Bare(BareJid::from_str("hamlet@denmark.lit").unwrap()),
|
||||||
|
affiliation: AffiliationAttribute::Owner,
|
||||||
|
},
|
||||||
|
Affiliation {
|
||||||
|
jid: Jid::Bare(BareJid::from_str("polonius@denmark.lit").unwrap()),
|
||||||
|
affiliation: AffiliationAttribute::Outcast,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
let elem2 = Element::from(pubsub);
|
||||||
|
assert_eq!(elem1, elem2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn configure() {
|
||||||
|
let elem: Element = "<pubsub xmlns='http://jabber.org/protocol/pubsub#owner'><configure node='foo'><x xmlns='jabber:x:data' type='submit'><field var='FORM_TYPE' type='hidden'><value>http://jabber.org/protocol/pubsub#node_config</value></field><field var='pubsub#access_model' type='list-single'><value>whitelist</value></field></x></configure></pubsub>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let elem1 = elem.clone();
|
||||||
|
|
||||||
|
let pubsub = PubSubOwner::Configure(Configure {
|
||||||
|
node: Some(NodeName(String::from("foo"))),
|
||||||
|
form: Some(DataForm {
|
||||||
|
type_: DataFormType::Submit,
|
||||||
|
form_type: Some(String::from(ns::PUBSUB_CONFIGURE)),
|
||||||
|
title: None,
|
||||||
|
instructions: None,
|
||||||
|
fields: vec![Field {
|
||||||
|
var: String::from("pubsub#access_model"),
|
||||||
|
type_: FieldType::ListSingle,
|
||||||
|
label: None,
|
||||||
|
required: false,
|
||||||
|
options: vec![],
|
||||||
|
values: vec![String::from("whitelist")],
|
||||||
|
media: vec![],
|
||||||
|
}],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
let elem2 = Element::from(pubsub);
|
||||||
|
assert_eq!(elem1, elem2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialize_configure() {
|
||||||
|
let reference: Element = "<pubsub xmlns='http://jabber.org/protocol/pubsub#owner'><configure node='foo'><x xmlns='jabber:x:data' type='submit'/></configure></pubsub>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let elem: Element = "<x xmlns='jabber:x:data' type='submit'/>".parse().unwrap();
|
||||||
|
|
||||||
|
let form = DataForm::try_from(elem).unwrap();
|
||||||
|
|
||||||
|
let configure = PubSubOwner::Configure(Configure {
|
||||||
|
node: Some(NodeName(String::from("foo"))),
|
||||||
|
form: Some(form),
|
||||||
|
});
|
||||||
|
let serialized: Element = configure.into();
|
||||||
|
assert_eq!(serialized, reference);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default() {
|
||||||
|
let elem: Element = "<pubsub xmlns='http://jabber.org/protocol/pubsub#owner'><default><x xmlns='jabber:x:data' type='submit'><field var='FORM_TYPE' type='hidden'><value>http://jabber.org/protocol/pubsub#node_config</value></field><field var='pubsub#access_model' type='list-single'><value>whitelist</value></field></x></default></pubsub>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let elem1 = elem.clone();
|
||||||
|
|
||||||
|
let pubsub = PubSubOwner::Default(Default {
|
||||||
|
form: Some(DataForm {
|
||||||
|
type_: DataFormType::Submit,
|
||||||
|
form_type: Some(String::from(ns::PUBSUB_CONFIGURE)),
|
||||||
|
title: None,
|
||||||
|
instructions: None,
|
||||||
|
fields: vec![Field {
|
||||||
|
var: String::from("pubsub#access_model"),
|
||||||
|
type_: FieldType::ListSingle,
|
||||||
|
label: None,
|
||||||
|
required: false,
|
||||||
|
options: vec![],
|
||||||
|
values: vec![String::from("whitelist")],
|
||||||
|
media: vec![],
|
||||||
|
}],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
let elem2 = Element::from(pubsub);
|
||||||
|
assert_eq!(elem1, elem2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn delete() {
|
||||||
|
let elem: Element = "<pubsub xmlns='http://jabber.org/protocol/pubsub#owner'><delete node='foo'><redirect uri='xmpp:hamlet@denmark.lit?;node=blog'/></delete></pubsub>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let elem1 = elem.clone();
|
||||||
|
|
||||||
|
let pubsub = PubSubOwner::Delete(Delete {
|
||||||
|
node: NodeName(String::from("foo")),
|
||||||
|
redirect: Some(Redirect {
|
||||||
|
uri: String::from("xmpp:hamlet@denmark.lit?;node=blog"),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
let elem2 = Element::from(pubsub);
|
||||||
|
assert_eq!(elem1, elem2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn purge() {
|
||||||
|
let elem: Element = "<pubsub xmlns='http://jabber.org/protocol/pubsub#owner'><purge node='foo'></purge></pubsub>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let elem1 = elem.clone();
|
||||||
|
|
||||||
|
let pubsub = PubSubOwner::Purge(Purge {
|
||||||
|
node: NodeName(String::from("foo")),
|
||||||
|
});
|
||||||
|
|
||||||
|
let elem2 = Element::from(pubsub);
|
||||||
|
assert_eq!(elem1, elem2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn subscriptions() {
|
||||||
|
let elem: Element = "<pubsub xmlns='http://jabber.org/protocol/pubsub#owner'><subscriptions node='foo'><subscription jid='hamlet@denmark.lit' subscription='subscribed'/><subscription jid='polonius@denmark.lit' subscription='unconfigured'/><subscription jid='bernardo@denmark.lit' subscription='subscribed' subid='123-abc'/><subscription jid='bernardo@denmark.lit' subscription='subscribed' subid='004-yyy'/></subscriptions></pubsub>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let elem1 = elem.clone();
|
||||||
|
|
||||||
|
let pubsub = PubSubOwner::Subscriptions(Subscriptions {
|
||||||
|
node: NodeName(String::from("foo")),
|
||||||
|
subscriptions: vec![
|
||||||
|
SubscriptionElem {
|
||||||
|
jid: Jid::Bare(BareJid::from_str("hamlet@denmark.lit").unwrap()),
|
||||||
|
subscription: Subscription::Subscribed,
|
||||||
|
subid: None,
|
||||||
|
},
|
||||||
|
SubscriptionElem {
|
||||||
|
jid: Jid::Bare(BareJid::from_str("polonius@denmark.lit").unwrap()),
|
||||||
|
subscription: Subscription::Unconfigured,
|
||||||
|
subid: None,
|
||||||
|
},
|
||||||
|
SubscriptionElem {
|
||||||
|
jid: Jid::Bare(BareJid::from_str("bernardo@denmark.lit").unwrap()),
|
||||||
|
subscription: Subscription::Subscribed,
|
||||||
|
subid: Some(String::from("123-abc")),
|
||||||
|
},
|
||||||
|
SubscriptionElem {
|
||||||
|
jid: Jid::Bare(BareJid::from_str("bernardo@denmark.lit").unwrap()),
|
||||||
|
subscription: Subscription::Subscribed,
|
||||||
|
subid: Some(String::from("004-yyy")),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
let elem2 = Element::from(pubsub);
|
||||||
|
assert_eq!(elem1, elem2);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,772 @@
|
||||||
|
// Copyright (c) 2018 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
use crate::data_forms::DataForm;
|
||||||
|
use crate::iq::{IqGetPayload, IqResultPayload, IqSetPayload};
|
||||||
|
use crate::ns;
|
||||||
|
use crate::pubsub::{
|
||||||
|
AffiliationAttribute, Item as PubSubItem, NodeName, Subscription, SubscriptionId,
|
||||||
|
};
|
||||||
|
use crate::util::error::Error;
|
||||||
|
use crate::Element;
|
||||||
|
use jid::Jid;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
// TODO: a better solution would be to split this into a query and a result elements, like for
|
||||||
|
// XEP-0030.
|
||||||
|
generate_element!(
|
||||||
|
/// A list of affiliations you have on a service, or on a node.
|
||||||
|
Affiliations, "affiliations", PUBSUB,
|
||||||
|
attributes: [
|
||||||
|
/// The optional node name this request pertains to.
|
||||||
|
node: Option<NodeName> = "node",
|
||||||
|
],
|
||||||
|
children: [
|
||||||
|
/// The actual list of affiliation elements.
|
||||||
|
affiliations: Vec<Affiliation> = ("affiliation", PUBSUB) => Affiliation
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// An affiliation element.
|
||||||
|
Affiliation, "affiliation", PUBSUB,
|
||||||
|
attributes: [
|
||||||
|
/// The node this affiliation pertains to.
|
||||||
|
node: Required<NodeName> = "node",
|
||||||
|
|
||||||
|
/// The affiliation you currently have on this node.
|
||||||
|
affiliation: Required<AffiliationAttribute> = "affiliation",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// Request to configure a new node.
|
||||||
|
Configure, "configure", PUBSUB,
|
||||||
|
children: [
|
||||||
|
/// The form to configure it.
|
||||||
|
form: Option<DataForm> = ("x", DATA_FORMS) => DataForm
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// Request to create a new node.
|
||||||
|
Create, "create", PUBSUB,
|
||||||
|
attributes: [
|
||||||
|
/// The node name to create, if `None` the service will generate one.
|
||||||
|
node: Option<NodeName> = "node",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// Request for a default node configuration.
|
||||||
|
Default, "default", PUBSUB,
|
||||||
|
attributes: [
|
||||||
|
/// The node targeted by this request, otherwise the entire service.
|
||||||
|
node: Option<NodeName> = "node",
|
||||||
|
|
||||||
|
// TODO: do we really want to support collection nodes?
|
||||||
|
// type: Option<String> = "type",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// A request for a list of items.
|
||||||
|
Items, "items", PUBSUB,
|
||||||
|
attributes: [
|
||||||
|
// TODO: should be an xs:positiveInteger, that is, an unbounded int ≥ 1.
|
||||||
|
/// Maximum number of items returned.
|
||||||
|
max_items: Option<u32> = "max_items",
|
||||||
|
|
||||||
|
/// The node queried by this request.
|
||||||
|
node: Required<NodeName> = "node",
|
||||||
|
|
||||||
|
/// The subscription identifier related to this request.
|
||||||
|
subid: Option<SubscriptionId> = "subid",
|
||||||
|
],
|
||||||
|
children: [
|
||||||
|
/// The actual list of items returned.
|
||||||
|
items: Vec<Item> = ("item", PUBSUB) => Item
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
impl Items {
|
||||||
|
/// Create a new items request.
|
||||||
|
pub fn new(node: &str) -> Items {
|
||||||
|
Items {
|
||||||
|
node: NodeName(String::from(node)),
|
||||||
|
max_items: None,
|
||||||
|
subid: None,
|
||||||
|
items: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Response wrapper for a PubSub `<item/>`.
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct Item(pub PubSubItem);
|
||||||
|
|
||||||
|
impl_pubsub_item!(Item, PUBSUB);
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// The options associated to a subscription request.
|
||||||
|
Options, "options", PUBSUB,
|
||||||
|
attributes: [
|
||||||
|
/// The JID affected by this request.
|
||||||
|
jid: Required<Jid> = "jid",
|
||||||
|
|
||||||
|
/// The node affected by this request.
|
||||||
|
node: Option<NodeName> = "node",
|
||||||
|
|
||||||
|
/// The subscription identifier affected by this request.
|
||||||
|
subid: Option<SubscriptionId> = "subid",
|
||||||
|
],
|
||||||
|
children: [
|
||||||
|
/// The form describing the subscription.
|
||||||
|
form: Option<DataForm> = ("x", DATA_FORMS) => DataForm
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// Request to publish items to a node.
|
||||||
|
Publish, "publish", PUBSUB,
|
||||||
|
attributes: [
|
||||||
|
/// The target node for this operation.
|
||||||
|
node: Required<NodeName> = "node",
|
||||||
|
],
|
||||||
|
children: [
|
||||||
|
/// The items you want to publish.
|
||||||
|
items: Vec<Item> = ("item", PUBSUB) => Item
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// The options associated to a publish request.
|
||||||
|
PublishOptions, "publish-options", PUBSUB,
|
||||||
|
children: [
|
||||||
|
/// The form describing these options.
|
||||||
|
form: Option<DataForm> = ("x", DATA_FORMS) => DataForm
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_attribute!(
|
||||||
|
/// Whether a retract request should notify subscribers or not.
|
||||||
|
Notify,
|
||||||
|
"notify",
|
||||||
|
bool
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// A request to retract some items from a node.
|
||||||
|
Retract, "retract", PUBSUB,
|
||||||
|
attributes: [
|
||||||
|
/// The node affected by this request.
|
||||||
|
node: Required<NodeName> = "node",
|
||||||
|
|
||||||
|
/// Whether a retract request should notify subscribers or not.
|
||||||
|
notify: Default<Notify> = "notify",
|
||||||
|
],
|
||||||
|
children: [
|
||||||
|
/// The items affected by this request.
|
||||||
|
items: Vec<Item> = ("item", PUBSUB) => Item
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Indicate that the subscription can be configured.
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct SubscribeOptions {
|
||||||
|
/// If `true`, the configuration is actually required.
|
||||||
|
required: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<Element> for SubscribeOptions {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(elem: Element) -> Result<Self, Error> {
|
||||||
|
check_self!(elem, "subscribe-options", PUBSUB);
|
||||||
|
check_no_attributes!(elem, "subscribe-options");
|
||||||
|
let mut required = false;
|
||||||
|
for child in elem.children() {
|
||||||
|
if child.is("required", ns::PUBSUB) {
|
||||||
|
if required {
|
||||||
|
return Err(Error::ParseError(
|
||||||
|
"More than one required element in subscribe-options.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
required = true;
|
||||||
|
} else {
|
||||||
|
return Err(Error::ParseError(
|
||||||
|
"Unknown child in subscribe-options element.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(SubscribeOptions { required })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SubscribeOptions> for Element {
|
||||||
|
fn from(subscribe_options: SubscribeOptions) -> Element {
|
||||||
|
Element::builder("subscribe-options", ns::PUBSUB)
|
||||||
|
.append_all(if subscribe_options.required {
|
||||||
|
Some(Element::builder("required", ns::PUBSUB))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
})
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// A request to subscribe a JID to a node.
|
||||||
|
Subscribe, "subscribe", PUBSUB,
|
||||||
|
attributes: [
|
||||||
|
/// The JID being subscribed.
|
||||||
|
jid: Required<Jid> = "jid",
|
||||||
|
|
||||||
|
/// The node to subscribe to.
|
||||||
|
node: Option<NodeName> = "node",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// A request for current subscriptions.
|
||||||
|
Subscriptions, "subscriptions", PUBSUB,
|
||||||
|
attributes: [
|
||||||
|
/// The node to query.
|
||||||
|
node: Option<NodeName> = "node",
|
||||||
|
],
|
||||||
|
children: [
|
||||||
|
/// The list of subscription elements returned.
|
||||||
|
subscription: Vec<SubscriptionElem> = ("subscription", PUBSUB) => SubscriptionElem
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// A subscription element, describing the state of a subscription.
|
||||||
|
SubscriptionElem, "subscription", PUBSUB,
|
||||||
|
attributes: [
|
||||||
|
/// The JID affected by this subscription.
|
||||||
|
jid: Required<Jid> = "jid",
|
||||||
|
|
||||||
|
/// The node affected by this subscription.
|
||||||
|
node: Option<NodeName> = "node",
|
||||||
|
|
||||||
|
/// The subscription identifier for this subscription.
|
||||||
|
subid: Option<SubscriptionId> = "subid",
|
||||||
|
|
||||||
|
/// The state of the subscription.
|
||||||
|
subscription: Option<Subscription> = "subscription",
|
||||||
|
],
|
||||||
|
children: [
|
||||||
|
/// The options related to this subscription.
|
||||||
|
subscribe_options: Option<SubscribeOptions> = ("subscribe-options", PUBSUB) => SubscribeOptions
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// An unsubscribe request.
|
||||||
|
Unsubscribe, "unsubscribe", PUBSUB,
|
||||||
|
attributes: [
|
||||||
|
/// The JID affected by this request.
|
||||||
|
jid: Required<Jid> = "jid",
|
||||||
|
|
||||||
|
/// The node affected by this request.
|
||||||
|
node: Option<NodeName> = "node",
|
||||||
|
|
||||||
|
/// The subscription identifier for this subscription.
|
||||||
|
subid: Option<SubscriptionId> = "subid",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Main payload used to communicate with a PubSub service.
|
||||||
|
///
|
||||||
|
/// `<pubsub xmlns="http://jabber.org/protocol/pubsub"/>`
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum PubSub {
|
||||||
|
/// Request to create a new node, with optional suggested name and suggested configuration.
|
||||||
|
Create {
|
||||||
|
/// The create request.
|
||||||
|
create: Create,
|
||||||
|
|
||||||
|
/// The configure request for the new node.
|
||||||
|
configure: Option<Configure>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// A subcribe request.
|
||||||
|
Subscribe {
|
||||||
|
/// The subscribe request.
|
||||||
|
subscribe: Option<Subscribe>,
|
||||||
|
|
||||||
|
/// The options related to this subscribe request.
|
||||||
|
options: Option<Options>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Request to publish items to a node, with optional options.
|
||||||
|
Publish {
|
||||||
|
/// The publish request.
|
||||||
|
publish: Publish,
|
||||||
|
|
||||||
|
/// The options related to this publish request.
|
||||||
|
publish_options: Option<PublishOptions>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// A list of affiliations you have on a service, or on a node.
|
||||||
|
Affiliations(Affiliations),
|
||||||
|
|
||||||
|
/// Request for a default node configuration.
|
||||||
|
Default(Default),
|
||||||
|
|
||||||
|
/// A request for a list of items.
|
||||||
|
Items(Items),
|
||||||
|
|
||||||
|
/// A request to retract some items from a node.
|
||||||
|
Retract(Retract),
|
||||||
|
|
||||||
|
/// A request about a subscription.
|
||||||
|
Subscription(SubscriptionElem),
|
||||||
|
|
||||||
|
/// A request for current subscriptions.
|
||||||
|
Subscriptions(Subscriptions),
|
||||||
|
|
||||||
|
/// An unsubscribe request.
|
||||||
|
Unsubscribe(Unsubscribe),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IqGetPayload for PubSub {}
|
||||||
|
impl IqSetPayload for PubSub {}
|
||||||
|
impl IqResultPayload for PubSub {}
|
||||||
|
|
||||||
|
impl TryFrom<Element> for PubSub {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(elem: Element) -> Result<PubSub, Error> {
|
||||||
|
check_self!(elem, "pubsub", PUBSUB);
|
||||||
|
check_no_attributes!(elem, "pubsub");
|
||||||
|
|
||||||
|
let mut payload = None;
|
||||||
|
for child in elem.children() {
|
||||||
|
if child.is("create", ns::PUBSUB) {
|
||||||
|
if payload.is_some() {
|
||||||
|
return Err(Error::ParseError(
|
||||||
|
"Payload is already defined in pubsub element.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let create = Create::try_from(child.clone())?;
|
||||||
|
payload = Some(PubSub::Create {
|
||||||
|
create,
|
||||||
|
configure: None,
|
||||||
|
});
|
||||||
|
} else if child.is("subscribe", ns::PUBSUB) {
|
||||||
|
if payload.is_some() {
|
||||||
|
return Err(Error::ParseError(
|
||||||
|
"Payload is already defined in pubsub element.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let subscribe = Subscribe::try_from(child.clone())?;
|
||||||
|
payload = Some(PubSub::Subscribe {
|
||||||
|
subscribe: Some(subscribe),
|
||||||
|
options: None,
|
||||||
|
});
|
||||||
|
} else if child.is("options", ns::PUBSUB) {
|
||||||
|
if let Some(PubSub::Subscribe { subscribe, options }) = payload {
|
||||||
|
if options.is_some() {
|
||||||
|
return Err(Error::ParseError(
|
||||||
|
"Options is already defined in pubsub element.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let options = Some(Options::try_from(child.clone())?);
|
||||||
|
payload = Some(PubSub::Subscribe { subscribe, options });
|
||||||
|
} else if payload.is_none() {
|
||||||
|
let options = Options::try_from(child.clone())?;
|
||||||
|
payload = Some(PubSub::Subscribe {
|
||||||
|
subscribe: None,
|
||||||
|
options: Some(options),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return Err(Error::ParseError(
|
||||||
|
"Payload is already defined in pubsub element.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else if child.is("configure", ns::PUBSUB) {
|
||||||
|
if let Some(PubSub::Create { create, configure }) = payload {
|
||||||
|
if configure.is_some() {
|
||||||
|
return Err(Error::ParseError(
|
||||||
|
"Configure is already defined in pubsub element.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let configure = Some(Configure::try_from(child.clone())?);
|
||||||
|
payload = Some(PubSub::Create { create, configure });
|
||||||
|
} else {
|
||||||
|
return Err(Error::ParseError(
|
||||||
|
"Payload is already defined in pubsub element.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else if child.is("publish", ns::PUBSUB) {
|
||||||
|
if payload.is_some() {
|
||||||
|
return Err(Error::ParseError(
|
||||||
|
"Payload is already defined in pubsub element.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let publish = Publish::try_from(child.clone())?;
|
||||||
|
payload = Some(PubSub::Publish {
|
||||||
|
publish,
|
||||||
|
publish_options: None,
|
||||||
|
});
|
||||||
|
} else if child.is("publish-options", ns::PUBSUB) {
|
||||||
|
if let Some(PubSub::Publish {
|
||||||
|
publish,
|
||||||
|
publish_options,
|
||||||
|
}) = payload
|
||||||
|
{
|
||||||
|
if publish_options.is_some() {
|
||||||
|
return Err(Error::ParseError(
|
||||||
|
"Publish-options are already defined in pubsub element.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let publish_options = Some(PublishOptions::try_from(child.clone())?);
|
||||||
|
payload = Some(PubSub::Publish {
|
||||||
|
publish,
|
||||||
|
publish_options,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return Err(Error::ParseError(
|
||||||
|
"Payload is already defined in pubsub element.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else if child.is("affiliations", ns::PUBSUB) {
|
||||||
|
if payload.is_some() {
|
||||||
|
return Err(Error::ParseError(
|
||||||
|
"Payload is already defined in pubsub element.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let affiliations = Affiliations::try_from(child.clone())?;
|
||||||
|
payload = Some(PubSub::Affiliations(affiliations));
|
||||||
|
} else if child.is("default", ns::PUBSUB) {
|
||||||
|
if payload.is_some() {
|
||||||
|
return Err(Error::ParseError(
|
||||||
|
"Payload is already defined in pubsub element.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let default = Default::try_from(child.clone())?;
|
||||||
|
payload = Some(PubSub::Default(default));
|
||||||
|
} else if child.is("items", ns::PUBSUB) {
|
||||||
|
if payload.is_some() {
|
||||||
|
return Err(Error::ParseError(
|
||||||
|
"Payload is already defined in pubsub element.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let items = Items::try_from(child.clone())?;
|
||||||
|
payload = Some(PubSub::Items(items));
|
||||||
|
} else if child.is("retract", ns::PUBSUB) {
|
||||||
|
if payload.is_some() {
|
||||||
|
return Err(Error::ParseError(
|
||||||
|
"Payload is already defined in pubsub element.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let retract = Retract::try_from(child.clone())?;
|
||||||
|
payload = Some(PubSub::Retract(retract));
|
||||||
|
} else if child.is("subscription", ns::PUBSUB) {
|
||||||
|
if payload.is_some() {
|
||||||
|
return Err(Error::ParseError(
|
||||||
|
"Payload is already defined in pubsub element.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let subscription = SubscriptionElem::try_from(child.clone())?;
|
||||||
|
payload = Some(PubSub::Subscription(subscription));
|
||||||
|
} else if child.is("subscriptions", ns::PUBSUB) {
|
||||||
|
if payload.is_some() {
|
||||||
|
return Err(Error::ParseError(
|
||||||
|
"Payload is already defined in pubsub element.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let subscriptions = Subscriptions::try_from(child.clone())?;
|
||||||
|
payload = Some(PubSub::Subscriptions(subscriptions));
|
||||||
|
} else if child.is("unsubscribe", ns::PUBSUB) {
|
||||||
|
if payload.is_some() {
|
||||||
|
return Err(Error::ParseError(
|
||||||
|
"Payload is already defined in pubsub element.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let unsubscribe = Unsubscribe::try_from(child.clone())?;
|
||||||
|
payload = Some(PubSub::Unsubscribe(unsubscribe));
|
||||||
|
} else {
|
||||||
|
return Err(Error::ParseError("Unknown child in pubsub element."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(payload.ok_or(Error::ParseError("No payload in pubsub element."))?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PubSub> for Element {
|
||||||
|
fn from(pubsub: PubSub) -> Element {
|
||||||
|
Element::builder("pubsub", ns::PUBSUB)
|
||||||
|
.append_all(match pubsub {
|
||||||
|
PubSub::Create { create, configure } => {
|
||||||
|
let mut elems = vec![Element::from(create)];
|
||||||
|
if let Some(configure) = configure {
|
||||||
|
elems.push(Element::from(configure));
|
||||||
|
}
|
||||||
|
elems
|
||||||
|
}
|
||||||
|
PubSub::Subscribe { subscribe, options } => {
|
||||||
|
let mut elems = vec![];
|
||||||
|
if let Some(subscribe) = subscribe {
|
||||||
|
elems.push(Element::from(subscribe));
|
||||||
|
}
|
||||||
|
if let Some(options) = options {
|
||||||
|
elems.push(Element::from(options));
|
||||||
|
}
|
||||||
|
elems
|
||||||
|
}
|
||||||
|
PubSub::Publish {
|
||||||
|
publish,
|
||||||
|
publish_options,
|
||||||
|
} => {
|
||||||
|
let mut elems = vec![Element::from(publish)];
|
||||||
|
if let Some(publish_options) = publish_options {
|
||||||
|
elems.push(Element::from(publish_options));
|
||||||
|
}
|
||||||
|
elems
|
||||||
|
}
|
||||||
|
PubSub::Affiliations(affiliations) => vec![Element::from(affiliations)],
|
||||||
|
PubSub::Default(default) => vec![Element::from(default)],
|
||||||
|
PubSub::Items(items) => vec![Element::from(items)],
|
||||||
|
PubSub::Retract(retract) => vec![Element::from(retract)],
|
||||||
|
PubSub::Subscription(subscription) => vec![Element::from(subscription)],
|
||||||
|
PubSub::Subscriptions(subscriptions) => vec![Element::from(subscriptions)],
|
||||||
|
PubSub::Unsubscribe(unsubscribe) => vec![Element::from(unsubscribe)],
|
||||||
|
})
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::data_forms::{DataForm, DataFormType, Field, FieldType};
|
||||||
|
use jid::FullJid;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create() {
|
||||||
|
let elem: Element = "<pubsub xmlns='http://jabber.org/protocol/pubsub'><create/></pubsub>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let elem1 = elem.clone();
|
||||||
|
let pubsub = PubSub::try_from(elem).unwrap();
|
||||||
|
match pubsub.clone() {
|
||||||
|
PubSub::Create { create, configure } => {
|
||||||
|
assert!(create.node.is_none());
|
||||||
|
assert!(configure.is_none());
|
||||||
|
}
|
||||||
|
_ => panic!(),
|
||||||
|
}
|
||||||
|
|
||||||
|
let elem2 = Element::from(pubsub);
|
||||||
|
assert_eq!(elem1, elem2);
|
||||||
|
|
||||||
|
let elem: Element =
|
||||||
|
"<pubsub xmlns='http://jabber.org/protocol/pubsub'><create node='coucou'/></pubsub>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let elem1 = elem.clone();
|
||||||
|
let pubsub = PubSub::try_from(elem).unwrap();
|
||||||
|
match pubsub.clone() {
|
||||||
|
PubSub::Create { create, configure } => {
|
||||||
|
assert_eq!(&create.node.unwrap().0, "coucou");
|
||||||
|
assert!(configure.is_none());
|
||||||
|
}
|
||||||
|
_ => panic!(),
|
||||||
|
}
|
||||||
|
|
||||||
|
let elem2 = Element::from(pubsub);
|
||||||
|
assert_eq!(elem1, elem2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_and_configure_empty() {
|
||||||
|
let elem: Element =
|
||||||
|
"<pubsub xmlns='http://jabber.org/protocol/pubsub'><create/><configure/></pubsub>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let elem1 = elem.clone();
|
||||||
|
let pubsub = PubSub::try_from(elem).unwrap();
|
||||||
|
match pubsub.clone() {
|
||||||
|
PubSub::Create { create, configure } => {
|
||||||
|
assert!(create.node.is_none());
|
||||||
|
assert!(configure.unwrap().form.is_none());
|
||||||
|
}
|
||||||
|
_ => panic!(),
|
||||||
|
}
|
||||||
|
|
||||||
|
let elem2 = Element::from(pubsub);
|
||||||
|
assert_eq!(elem1, elem2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_and_configure_simple() {
|
||||||
|
// XXX: Do we want xmpp-parsers to always specify the field type in the output Element?
|
||||||
|
let elem: Element = "<pubsub xmlns='http://jabber.org/protocol/pubsub'><create node='foo'/><configure><x xmlns='jabber:x:data' type='submit'><field var='FORM_TYPE' type='hidden'><value>http://jabber.org/protocol/pubsub#node_config</value></field><field var='pubsub#access_model' type='list-single'><value>whitelist</value></field></x></configure></pubsub>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let elem1 = elem.clone();
|
||||||
|
|
||||||
|
let pubsub = PubSub::Create {
|
||||||
|
create: Create {
|
||||||
|
node: Some(NodeName(String::from("foo"))),
|
||||||
|
},
|
||||||
|
configure: Some(Configure {
|
||||||
|
form: Some(DataForm {
|
||||||
|
type_: DataFormType::Submit,
|
||||||
|
form_type: Some(String::from(ns::PUBSUB_CONFIGURE)),
|
||||||
|
title: None,
|
||||||
|
instructions: None,
|
||||||
|
fields: vec![Field {
|
||||||
|
var: String::from("pubsub#access_model"),
|
||||||
|
type_: FieldType::ListSingle,
|
||||||
|
label: None,
|
||||||
|
required: false,
|
||||||
|
options: vec![],
|
||||||
|
values: vec![String::from("whitelist")],
|
||||||
|
media: vec![],
|
||||||
|
}],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
let elem2 = Element::from(pubsub);
|
||||||
|
assert_eq!(elem1, elem2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn publish() {
|
||||||
|
let elem: Element =
|
||||||
|
"<pubsub xmlns='http://jabber.org/protocol/pubsub'><publish node='coucou'/></pubsub>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let elem1 = elem.clone();
|
||||||
|
let pubsub = PubSub::try_from(elem).unwrap();
|
||||||
|
match pubsub.clone() {
|
||||||
|
PubSub::Publish {
|
||||||
|
publish,
|
||||||
|
publish_options,
|
||||||
|
} => {
|
||||||
|
assert_eq!(&publish.node.0, "coucou");
|
||||||
|
assert!(publish_options.is_none());
|
||||||
|
}
|
||||||
|
_ => panic!(),
|
||||||
|
}
|
||||||
|
|
||||||
|
let elem2 = Element::from(pubsub);
|
||||||
|
assert_eq!(elem1, elem2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn publish_with_publish_options() {
|
||||||
|
let elem: Element = "<pubsub xmlns='http://jabber.org/protocol/pubsub'><publish node='coucou'/><publish-options/></pubsub>".parse().unwrap();
|
||||||
|
let elem1 = elem.clone();
|
||||||
|
let pubsub = PubSub::try_from(elem).unwrap();
|
||||||
|
match pubsub.clone() {
|
||||||
|
PubSub::Publish {
|
||||||
|
publish,
|
||||||
|
publish_options,
|
||||||
|
} => {
|
||||||
|
assert_eq!(&publish.node.0, "coucou");
|
||||||
|
assert!(publish_options.unwrap().form.is_none());
|
||||||
|
}
|
||||||
|
_ => panic!(),
|
||||||
|
}
|
||||||
|
|
||||||
|
let elem2 = Element::from(pubsub);
|
||||||
|
assert_eq!(elem1, elem2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_empty_pubsub() {
|
||||||
|
let elem: Element = "<pubsub xmlns='http://jabber.org/protocol/pubsub'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = PubSub::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "No payload in pubsub element.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn publish_option() {
|
||||||
|
let elem: Element = "<publish-options xmlns='http://jabber.org/protocol/pubsub'><x xmlns='jabber:x:data' type='submit'><field var='FORM_TYPE' type='hidden'><value>http://jabber.org/protocol/pubsub#publish-options</value></field></x></publish-options>".parse().unwrap();
|
||||||
|
let publish_options = PublishOptions::try_from(elem).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
&publish_options.form.unwrap().form_type.unwrap(),
|
||||||
|
"http://jabber.org/protocol/pubsub#publish-options"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn subscribe_options() {
|
||||||
|
let elem1: Element = "<subscribe-options xmlns='http://jabber.org/protocol/pubsub'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let subscribe_options1 = SubscribeOptions::try_from(elem1).unwrap();
|
||||||
|
assert_eq!(subscribe_options1.required, false);
|
||||||
|
|
||||||
|
let elem2: Element = "<subscribe-options xmlns='http://jabber.org/protocol/pubsub'><required/></subscribe-options>".parse().unwrap();
|
||||||
|
let subscribe_options2 = SubscribeOptions::try_from(elem2).unwrap();
|
||||||
|
assert_eq!(subscribe_options2.required, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_options_without_subscribe() {
|
||||||
|
let elem: Element = "<pubsub xmlns='http://jabber.org/protocol/pubsub'><options xmlns='http://jabber.org/protocol/pubsub' jid='juliet@capulet.lit/balcony'><x xmlns='jabber:x:data' type='submit'/></options></pubsub>".parse().unwrap();
|
||||||
|
let elem1 = elem.clone();
|
||||||
|
let pubsub = PubSub::try_from(elem).unwrap();
|
||||||
|
match pubsub.clone() {
|
||||||
|
PubSub::Subscribe { subscribe, options } => {
|
||||||
|
assert!(subscribe.is_none());
|
||||||
|
assert!(options.is_some());
|
||||||
|
}
|
||||||
|
_ => panic!(),
|
||||||
|
}
|
||||||
|
|
||||||
|
let elem2 = Element::from(pubsub);
|
||||||
|
assert_eq!(elem1, elem2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialize_options() {
|
||||||
|
let reference: Element = "<options xmlns='http://jabber.org/protocol/pubsub' jid='juliet@capulet.lit/balcony'><x xmlns='jabber:x:data' type='submit'/></options>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let elem: Element = "<x xmlns='jabber:x:data' type='submit'/>".parse().unwrap();
|
||||||
|
|
||||||
|
let form = DataForm::try_from(elem).unwrap();
|
||||||
|
|
||||||
|
let options = Options {
|
||||||
|
jid: Jid::Full(FullJid::new("juliet", "capulet.lit", "balcony")),
|
||||||
|
node: None,
|
||||||
|
subid: None,
|
||||||
|
form: Some(form),
|
||||||
|
};
|
||||||
|
let serialized: Element = options.into();
|
||||||
|
assert_eq!(serialized, reference);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialize_publish_options() {
|
||||||
|
let reference: Element = "<publish-options xmlns='http://jabber.org/protocol/pubsub'><x xmlns='jabber:x:data' type='submit'/></publish-options>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let elem: Element = "<x xmlns='jabber:x:data' type='submit'/>".parse().unwrap();
|
||||||
|
|
||||||
|
let form = DataForm::try_from(elem).unwrap();
|
||||||
|
|
||||||
|
let options = PublishOptions { form: Some(form) };
|
||||||
|
let serialized: Element = options.into();
|
||||||
|
assert_eq!(serialized, reference);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,89 @@
|
||||||
|
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
use crate::message::MessagePayload;
|
||||||
|
|
||||||
|
generate_empty_element!(
|
||||||
|
/// Requests that this message is acked by the final recipient once
|
||||||
|
/// received.
|
||||||
|
Request,
|
||||||
|
"request",
|
||||||
|
RECEIPTS
|
||||||
|
);
|
||||||
|
|
||||||
|
impl MessagePayload for Request {}
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// Notes that a previous message has correctly been received, it is
|
||||||
|
/// referenced by its 'id' attribute.
|
||||||
|
Received, "received", RECEIPTS,
|
||||||
|
attributes: [
|
||||||
|
/// The 'id' attribute of the received message.
|
||||||
|
id: Required<String> = "id",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
impl MessagePayload for Received {}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::ns;
|
||||||
|
use crate::util::error::Error;
|
||||||
|
use crate::Element;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "32")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(Request, 0);
|
||||||
|
assert_size!(Received, 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "64")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(Request, 0);
|
||||||
|
assert_size!(Received, 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_simple() {
|
||||||
|
let elem: Element = "<request xmlns='urn:xmpp:receipts'/>".parse().unwrap();
|
||||||
|
Request::try_from(elem).unwrap();
|
||||||
|
|
||||||
|
let elem: Element = "<received xmlns='urn:xmpp:receipts' id='coucou'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
Received::try_from(elem).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_missing_id() {
|
||||||
|
let elem: Element = "<received xmlns='urn:xmpp:receipts'/>".parse().unwrap();
|
||||||
|
let error = Received::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Required attribute 'id' missing.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialise() {
|
||||||
|
let receipt = Request;
|
||||||
|
let elem: Element = receipt.into();
|
||||||
|
assert!(elem.is("request", ns::RECEIPTS));
|
||||||
|
assert_eq!(elem.attrs().count(), 0);
|
||||||
|
|
||||||
|
let receipt = Received {
|
||||||
|
id: String::from("coucou"),
|
||||||
|
};
|
||||||
|
let elem: Element = receipt.into();
|
||||||
|
assert!(elem.is("received", ns::RECEIPTS));
|
||||||
|
assert_eq!(elem.attr("id"), Some("coucou"));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,313 @@
|
||||||
|
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
use crate::iq::{IqGetPayload, IqResultPayload, IqSetPayload};
|
||||||
|
use jid::BareJid;
|
||||||
|
|
||||||
|
generate_elem_id!(
|
||||||
|
/// Represents a group a contact is part of.
|
||||||
|
Group,
|
||||||
|
"group",
|
||||||
|
ROSTER
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_attribute!(
|
||||||
|
/// The state of your mutual subscription with a contact.
|
||||||
|
Subscription, "subscription", {
|
||||||
|
/// The user doesn’t have any subscription to this contact’s presence,
|
||||||
|
/// and neither does this contact.
|
||||||
|
None => "none",
|
||||||
|
|
||||||
|
/// Only this contact has a subscription with you, not the opposite.
|
||||||
|
From => "from",
|
||||||
|
|
||||||
|
/// Only you have a subscription with this contact, not the opposite.
|
||||||
|
To => "to",
|
||||||
|
|
||||||
|
/// Both you and your contact are subscribed to each other’s presence.
|
||||||
|
Both => "both",
|
||||||
|
|
||||||
|
/// In a roster set, this asks the server to remove this contact item
|
||||||
|
/// from your roster.
|
||||||
|
Remove => "remove",
|
||||||
|
}, Default = None
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_attribute!(
|
||||||
|
/// The sub-state of subscription with a contact.
|
||||||
|
Ask, "ask", (
|
||||||
|
/// Pending sub-state of the 'none' subscription state.
|
||||||
|
Subscribe => "subscribe"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// Contact from the user’s contact list.
|
||||||
|
Item, "item", ROSTER,
|
||||||
|
attributes: [
|
||||||
|
/// JID of this contact.
|
||||||
|
jid: Required<BareJid> = "jid",
|
||||||
|
|
||||||
|
/// Name of this contact.
|
||||||
|
name: OptionEmpty<String> = "name",
|
||||||
|
|
||||||
|
/// Subscription status of this contact.
|
||||||
|
subscription: Default<Subscription> = "subscription",
|
||||||
|
|
||||||
|
/// Indicates “Pending Out” sub-states for this contact.
|
||||||
|
ask: Default<Ask> = "ask",
|
||||||
|
],
|
||||||
|
|
||||||
|
children: [
|
||||||
|
/// Groups this contact is part of.
|
||||||
|
groups: Vec<Group> = ("group", ROSTER) => Group
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// The contact list of the user.
|
||||||
|
Roster, "query", ROSTER,
|
||||||
|
attributes: [
|
||||||
|
/// Version of the contact list.
|
||||||
|
///
|
||||||
|
/// This is an opaque string that should only be sent back to the server on
|
||||||
|
/// a new connection, if this client is storing the contact list between
|
||||||
|
/// connections.
|
||||||
|
ver: Option<String> = "ver"
|
||||||
|
],
|
||||||
|
children: [
|
||||||
|
/// List of the contacts of the user.
|
||||||
|
items: Vec<Item> = ("item", ROSTER) => Item
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
impl IqGetPayload for Roster {}
|
||||||
|
impl IqSetPayload for Roster {}
|
||||||
|
impl IqResultPayload for Roster {}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::util::error::Error;
|
||||||
|
use crate::Element;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "32")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(Group, 12);
|
||||||
|
assert_size!(Subscription, 1);
|
||||||
|
assert_size!(Ask, 1);
|
||||||
|
assert_size!(Item, 52);
|
||||||
|
assert_size!(Roster, 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "64")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(Group, 24);
|
||||||
|
assert_size!(Subscription, 1);
|
||||||
|
assert_size!(Ask, 1);
|
||||||
|
assert_size!(Item, 104);
|
||||||
|
assert_size!(Roster, 48);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get() {
|
||||||
|
let elem: Element = "<query xmlns='jabber:iq:roster'/>".parse().unwrap();
|
||||||
|
let roster = Roster::try_from(elem).unwrap();
|
||||||
|
assert!(roster.ver.is_none());
|
||||||
|
assert!(roster.items.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_result() {
|
||||||
|
let elem: Element = "<query xmlns='jabber:iq:roster' ver='ver7'><item jid='nurse@example.com'/><item jid='romeo@example.net'/></query>".parse().unwrap();
|
||||||
|
let roster = Roster::try_from(elem).unwrap();
|
||||||
|
assert_eq!(roster.ver, Some(String::from("ver7")));
|
||||||
|
assert_eq!(roster.items.len(), 2);
|
||||||
|
|
||||||
|
let elem2: Element = "<query xmlns='jabber:iq:roster' ver='ver7'><item jid='nurse@example.com'/><item jid='romeo@example.net' name=''/></query>".parse().unwrap();
|
||||||
|
let roster2 = Roster::try_from(elem2).unwrap();
|
||||||
|
assert_eq!(roster.items, roster2.items);
|
||||||
|
|
||||||
|
let elem: Element = "<query xmlns='jabber:iq:roster' ver='ver9'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let roster = Roster::try_from(elem).unwrap();
|
||||||
|
assert_eq!(roster.ver, Some(String::from("ver9")));
|
||||||
|
assert!(roster.items.is_empty());
|
||||||
|
|
||||||
|
let elem: Element = r#"
|
||||||
|
<query xmlns='jabber:iq:roster' ver='ver11'>
|
||||||
|
<item jid='romeo@example.net'
|
||||||
|
name='Romeo'
|
||||||
|
subscription='both'>
|
||||||
|
<group>Friends</group>
|
||||||
|
</item>
|
||||||
|
<item jid='mercutio@example.com'
|
||||||
|
name='Mercutio'
|
||||||
|
subscription='from'/>
|
||||||
|
<item jid='benvolio@example.net'
|
||||||
|
name='Benvolio'
|
||||||
|
subscription='both'/>
|
||||||
|
<item jid='contact@example.org'
|
||||||
|
subscription='none'
|
||||||
|
ask='subscribe'
|
||||||
|
name='MyContact'>
|
||||||
|
<group>MyBuddies</group>
|
||||||
|
</item>
|
||||||
|
</query>
|
||||||
|
"#
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let roster = Roster::try_from(elem).unwrap();
|
||||||
|
assert_eq!(roster.ver, Some(String::from("ver11")));
|
||||||
|
assert_eq!(roster.items.len(), 4);
|
||||||
|
assert_eq!(roster.items[0].jid, BareJid::new("romeo", "example.net"));
|
||||||
|
assert_eq!(roster.items[0].name, Some(String::from("Romeo")));
|
||||||
|
assert_eq!(roster.items[0].subscription, Subscription::Both);
|
||||||
|
assert_eq!(roster.items[0].ask, Ask::None);
|
||||||
|
assert_eq!(
|
||||||
|
roster.items[0].groups,
|
||||||
|
vec!(Group::from_str("Friends").unwrap())
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(roster.items[3].jid, BareJid::new("contact", "example.org"));
|
||||||
|
assert_eq!(roster.items[3].name, Some(String::from("MyContact")));
|
||||||
|
assert_eq!(roster.items[3].subscription, Subscription::None);
|
||||||
|
assert_eq!(roster.items[3].ask, Ask::Subscribe);
|
||||||
|
assert_eq!(
|
||||||
|
roster.items[3].groups,
|
||||||
|
vec!(Group::from_str("MyBuddies").unwrap())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_multiple_groups() {
|
||||||
|
let elem: Element = "<query xmlns='jabber:iq:roster'><item jid='test@example.org'><group>A</group><group>B</group></item></query>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let elem1 = elem.clone();
|
||||||
|
let roster = Roster::try_from(elem).unwrap();
|
||||||
|
assert!(roster.ver.is_none());
|
||||||
|
assert_eq!(roster.items.len(), 1);
|
||||||
|
assert_eq!(roster.items[0].jid, BareJid::new("test", "example.org"));
|
||||||
|
assert_eq!(roster.items[0].name, None);
|
||||||
|
assert_eq!(roster.items[0].groups.len(), 2);
|
||||||
|
assert_eq!(roster.items[0].groups[0], Group::from_str("A").unwrap());
|
||||||
|
assert_eq!(roster.items[0].groups[1], Group::from_str("B").unwrap());
|
||||||
|
let elem2 = roster.into();
|
||||||
|
assert_eq!(elem1, elem2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_set() {
|
||||||
|
let elem: Element =
|
||||||
|
"<query xmlns='jabber:iq:roster'><item jid='nurse@example.com'/></query>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let roster = Roster::try_from(elem).unwrap();
|
||||||
|
assert!(roster.ver.is_none());
|
||||||
|
assert_eq!(roster.items.len(), 1);
|
||||||
|
|
||||||
|
let elem: Element = r#"
|
||||||
|
<query xmlns='jabber:iq:roster'>
|
||||||
|
<item jid='nurse@example.com'
|
||||||
|
name='Nurse'>
|
||||||
|
<group>Servants</group>
|
||||||
|
</item>
|
||||||
|
</query>
|
||||||
|
"#
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let roster = Roster::try_from(elem).unwrap();
|
||||||
|
assert!(roster.ver.is_none());
|
||||||
|
assert_eq!(roster.items.len(), 1);
|
||||||
|
assert_eq!(roster.items[0].jid, BareJid::new("nurse", "example.com"));
|
||||||
|
assert_eq!(roster.items[0].name, Some(String::from("Nurse")));
|
||||||
|
assert_eq!(roster.items[0].groups.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
roster.items[0].groups[0],
|
||||||
|
Group::from_str("Servants").unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
let elem: Element = r#"
|
||||||
|
<query xmlns='jabber:iq:roster'>
|
||||||
|
<item jid='nurse@example.com'
|
||||||
|
subscription='remove'/>
|
||||||
|
</query>
|
||||||
|
"#
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let roster = Roster::try_from(elem).unwrap();
|
||||||
|
assert!(roster.ver.is_none());
|
||||||
|
assert_eq!(roster.items.len(), 1);
|
||||||
|
assert_eq!(roster.items[0].jid, BareJid::new("nurse", "example.com"));
|
||||||
|
assert!(roster.items[0].name.is_none());
|
||||||
|
assert!(roster.items[0].groups.is_empty());
|
||||||
|
assert_eq!(roster.items[0].subscription, Subscription::Remove);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "disable-validation"))]
|
||||||
|
#[test]
|
||||||
|
fn test_invalid() {
|
||||||
|
let elem: Element = "<query xmlns='jabber:iq:roster'><coucou/></query>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = Roster::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Unknown child in query element.");
|
||||||
|
|
||||||
|
let elem: Element = "<query xmlns='jabber:iq:roster' coucou=''/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = Roster::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Unknown attribute in query element.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_item() {
|
||||||
|
let elem: Element = "<query xmlns='jabber:iq:roster'><item/></query>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = Roster::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Required attribute 'jid' missing.");
|
||||||
|
|
||||||
|
/*
|
||||||
|
let elem: Element = "<query xmlns='jabber:iq:roster'><item jid=''/></query>".parse().unwrap();
|
||||||
|
let error = Roster::try_from(elem).unwrap_err();
|
||||||
|
let error = match error {
|
||||||
|
Error::JidParseError(error) => error,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(error.description(), "Invalid JID, I guess?");
|
||||||
|
*/
|
||||||
|
|
||||||
|
let elem: Element =
|
||||||
|
"<query xmlns='jabber:iq:roster'><item jid='coucou'><coucou/></item></query>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = Roster::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Unknown child in item element.");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,302 @@
|
||||||
|
// 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::ns;
|
||||||
|
use crate::util::error::Error;
|
||||||
|
use crate::Element;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
/// Requests paging through a potentially big set of items (represented by an
|
||||||
|
/// UID).
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct SetQuery {
|
||||||
|
/// Limit the number of items, or use the recipient’s defaults if None.
|
||||||
|
pub max: Option<usize>,
|
||||||
|
|
||||||
|
/// The UID after which to give results, or if None it is the element
|
||||||
|
/// “before” the first item, effectively an index of negative one.
|
||||||
|
pub after: Option<String>,
|
||||||
|
|
||||||
|
/// The UID before which to give results, or if None it starts with the
|
||||||
|
/// last page of the full set.
|
||||||
|
pub before: Option<String>,
|
||||||
|
|
||||||
|
/// Numerical index of the page (deprecated).
|
||||||
|
pub index: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<Element> for SetQuery {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(elem: Element) -> Result<SetQuery, Error> {
|
||||||
|
check_self!(elem, "set", RSM, "RSM set");
|
||||||
|
let mut set = SetQuery {
|
||||||
|
max: None,
|
||||||
|
after: None,
|
||||||
|
before: None,
|
||||||
|
index: None,
|
||||||
|
};
|
||||||
|
for child in elem.children() {
|
||||||
|
if child.is("max", ns::RSM) {
|
||||||
|
if set.max.is_some() {
|
||||||
|
return Err(Error::ParseError("Set can’t have more than one max."));
|
||||||
|
}
|
||||||
|
set.max = Some(child.text().parse()?);
|
||||||
|
} else if child.is("after", ns::RSM) {
|
||||||
|
if set.after.is_some() {
|
||||||
|
return Err(Error::ParseError("Set can’t have more than one after."));
|
||||||
|
}
|
||||||
|
set.after = Some(child.text());
|
||||||
|
} else if child.is("before", ns::RSM) {
|
||||||
|
if set.before.is_some() {
|
||||||
|
return Err(Error::ParseError("Set can’t have more than one before."));
|
||||||
|
}
|
||||||
|
set.before = Some(child.text());
|
||||||
|
} else if child.is("index", ns::RSM) {
|
||||||
|
if set.index.is_some() {
|
||||||
|
return Err(Error::ParseError("Set can’t have more than one index."));
|
||||||
|
}
|
||||||
|
set.index = Some(child.text().parse()?);
|
||||||
|
} else {
|
||||||
|
return Err(Error::ParseError("Unknown child in set element."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(set)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SetQuery> for Element {
|
||||||
|
fn from(set: SetQuery) -> Element {
|
||||||
|
Element::builder("set", ns::RSM)
|
||||||
|
.append_all(
|
||||||
|
set.max
|
||||||
|
.map(|max| Element::builder("max", ns::RSM).append(format!("{}", max))),
|
||||||
|
)
|
||||||
|
.append_all(
|
||||||
|
set.after
|
||||||
|
.map(|after| Element::builder("after", ns::RSM).append(after)),
|
||||||
|
)
|
||||||
|
.append_all(
|
||||||
|
set.before
|
||||||
|
.map(|before| Element::builder("before", ns::RSM).append(before)),
|
||||||
|
)
|
||||||
|
.append_all(
|
||||||
|
set.index
|
||||||
|
.map(|index| Element::builder("index", ns::RSM).append(format!("{}", index))),
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Describes the paging result of a [query](struct.SetQuery.html).
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct SetResult {
|
||||||
|
/// The UID of the first item of the page.
|
||||||
|
pub first: Option<String>,
|
||||||
|
|
||||||
|
/// The position of the [first item](#structfield.first) in the full set
|
||||||
|
/// (which may be approximate).
|
||||||
|
pub first_index: Option<usize>,
|
||||||
|
|
||||||
|
/// The UID of the last item of the page.
|
||||||
|
pub last: Option<String>,
|
||||||
|
|
||||||
|
/// How many items there are in the full set (which may be approximate).
|
||||||
|
pub count: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<Element> for SetResult {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(elem: Element) -> Result<SetResult, Error> {
|
||||||
|
check_self!(elem, "set", RSM, "RSM set");
|
||||||
|
let mut set = SetResult {
|
||||||
|
first: None,
|
||||||
|
first_index: None,
|
||||||
|
last: None,
|
||||||
|
count: None,
|
||||||
|
};
|
||||||
|
for child in elem.children() {
|
||||||
|
if child.is("first", ns::RSM) {
|
||||||
|
if set.first.is_some() {
|
||||||
|
return Err(Error::ParseError("Set can’t have more than one first."));
|
||||||
|
}
|
||||||
|
set.first_index = get_attr!(child, "index", Option);
|
||||||
|
set.first = Some(child.text());
|
||||||
|
} else if child.is("last", ns::RSM) {
|
||||||
|
if set.last.is_some() {
|
||||||
|
return Err(Error::ParseError("Set can’t have more than one last."));
|
||||||
|
}
|
||||||
|
set.last = Some(child.text());
|
||||||
|
} else if child.is("count", ns::RSM) {
|
||||||
|
if set.count.is_some() {
|
||||||
|
return Err(Error::ParseError("Set can’t have more than one count."));
|
||||||
|
}
|
||||||
|
set.count = Some(child.text().parse()?);
|
||||||
|
} else {
|
||||||
|
return Err(Error::ParseError("Unknown child in set element."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(set)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SetResult> for Element {
|
||||||
|
fn from(set: SetResult) -> Element {
|
||||||
|
let first = set.first.clone().map(|first| {
|
||||||
|
Element::builder("first", ns::RSM)
|
||||||
|
.attr("index", set.first_index)
|
||||||
|
.append(first)
|
||||||
|
});
|
||||||
|
Element::builder("set", ns::RSM)
|
||||||
|
.append_all(first)
|
||||||
|
.append_all(
|
||||||
|
set.last
|
||||||
|
.map(|last| Element::builder("last", ns::RSM).append(last)),
|
||||||
|
)
|
||||||
|
.append_all(
|
||||||
|
set.count
|
||||||
|
.map(|count| Element::builder("count", ns::RSM).append(format!("{}", count))),
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "32")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(SetQuery, 40);
|
||||||
|
assert_size!(SetResult, 40);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "64")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(SetQuery, 80);
|
||||||
|
assert_size!(SetResult, 80);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_simple() {
|
||||||
|
let elem: Element = "<set xmlns='http://jabber.org/protocol/rsm'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let set = SetQuery::try_from(elem).unwrap();
|
||||||
|
assert_eq!(set.max, None);
|
||||||
|
assert_eq!(set.after, None);
|
||||||
|
assert_eq!(set.before, None);
|
||||||
|
assert_eq!(set.index, None);
|
||||||
|
|
||||||
|
let elem: Element = "<set xmlns='http://jabber.org/protocol/rsm'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let set = SetResult::try_from(elem).unwrap();
|
||||||
|
match set.first {
|
||||||
|
Some(_) => panic!(),
|
||||||
|
None => (),
|
||||||
|
}
|
||||||
|
assert_eq!(set.last, None);
|
||||||
|
assert_eq!(set.count, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_unknown() {
|
||||||
|
let elem: Element = "<replace xmlns='urn:xmpp:message-correct:0'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = SetQuery::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "This is not a RSM set element.");
|
||||||
|
|
||||||
|
let elem: Element = "<replace xmlns='urn:xmpp:message-correct:0'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = SetResult::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "This is not a RSM set element.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_child() {
|
||||||
|
let elem: Element = "<set xmlns='http://jabber.org/protocol/rsm'><coucou/></set>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = SetQuery::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Unknown child in set element.");
|
||||||
|
|
||||||
|
let elem: Element = "<set xmlns='http://jabber.org/protocol/rsm'><coucou/></set>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = SetResult::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Unknown child in set element.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialise() {
|
||||||
|
let elem: Element = "<set xmlns='http://jabber.org/protocol/rsm'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let rsm = SetQuery {
|
||||||
|
max: None,
|
||||||
|
after: None,
|
||||||
|
before: None,
|
||||||
|
index: None,
|
||||||
|
};
|
||||||
|
let elem2 = rsm.into();
|
||||||
|
assert_eq!(elem, elem2);
|
||||||
|
|
||||||
|
let elem: Element = "<set xmlns='http://jabber.org/protocol/rsm'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let rsm = SetResult {
|
||||||
|
first: None,
|
||||||
|
first_index: None,
|
||||||
|
last: None,
|
||||||
|
count: None,
|
||||||
|
};
|
||||||
|
let elem2 = rsm.into();
|
||||||
|
assert_eq!(elem, elem2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_first_index() {
|
||||||
|
let elem: Element =
|
||||||
|
"<set xmlns='http://jabber.org/protocol/rsm'><first index='4'>coucou</first></set>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let elem1 = elem.clone();
|
||||||
|
let set = SetResult::try_from(elem).unwrap();
|
||||||
|
assert_eq!(set.first, Some(String::from("coucou")));
|
||||||
|
assert_eq!(set.first_index, Some(4));
|
||||||
|
|
||||||
|
let set2 = SetResult {
|
||||||
|
first: Some(String::from("coucou")),
|
||||||
|
first_index: Some(4),
|
||||||
|
last: None,
|
||||||
|
count: None,
|
||||||
|
};
|
||||||
|
let elem2 = set2.into();
|
||||||
|
assert_eq!(elem1, elem2);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,301 @@
|
||||||
|
// 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::ns;
|
||||||
|
use crate::util::error::Error;
|
||||||
|
use crate::util::helpers::Base64;
|
||||||
|
use crate::Element;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
generate_attribute!(
|
||||||
|
/// The list of available SASL mechanisms.
|
||||||
|
Mechanism, "mechanism", {
|
||||||
|
/// Uses no hashing mechanism and transmit the password in clear to the
|
||||||
|
/// server, using a single step.
|
||||||
|
Plain => "PLAIN",
|
||||||
|
|
||||||
|
/// Challenge-based mechanism using HMAC and SHA-1, allows both the
|
||||||
|
/// client and the server to avoid having to store the password in
|
||||||
|
/// clear.
|
||||||
|
///
|
||||||
|
/// See https://tools.ietf.org/html/rfc5802
|
||||||
|
ScramSha1 => "SCRAM-SHA-1",
|
||||||
|
|
||||||
|
/// Same as [ScramSha1](#structfield.ScramSha1), with the addition of
|
||||||
|
/// channel binding.
|
||||||
|
ScramSha1Plus => "SCRAM-SHA-1-PLUS",
|
||||||
|
|
||||||
|
/// Same as [ScramSha1](#structfield.ScramSha1), but using SHA-256
|
||||||
|
/// instead of SHA-1 as the hash function.
|
||||||
|
ScramSha256 => "SCRAM-SHA-256",
|
||||||
|
|
||||||
|
/// Same as [ScramSha256](#structfield.ScramSha256), with the addition
|
||||||
|
/// of channel binding.
|
||||||
|
ScramSha256Plus => "SCRAM-SHA-256-PLUS",
|
||||||
|
|
||||||
|
/// Creates a temporary JID on login, which will be destroyed on
|
||||||
|
/// disconnect.
|
||||||
|
Anonymous => "ANONYMOUS",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// The first step of the SASL process, selecting the mechanism and sending
|
||||||
|
/// the first part of the handshake.
|
||||||
|
Auth, "auth", SASL,
|
||||||
|
attributes: [
|
||||||
|
/// The mechanism used.
|
||||||
|
mechanism: Required<Mechanism> = "mechanism"
|
||||||
|
],
|
||||||
|
text: (
|
||||||
|
/// The content of the handshake.
|
||||||
|
data: Base64<Vec<u8>>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// In case the mechanism selected at the [auth](struct.Auth.html) step
|
||||||
|
/// requires a second step, the server sends this element with additional
|
||||||
|
/// data.
|
||||||
|
Challenge, "challenge", SASL,
|
||||||
|
text: (
|
||||||
|
/// The challenge data.
|
||||||
|
data: Base64<Vec<u8>>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// In case the mechanism selected at the [auth](struct.Auth.html) step
|
||||||
|
/// requires a second step, this contains the client’s response to the
|
||||||
|
/// server’s [challenge](struct.Challenge.html).
|
||||||
|
Response, "response", SASL,
|
||||||
|
text: (
|
||||||
|
/// The response data.
|
||||||
|
data: Base64<Vec<u8>>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_empty_element!(
|
||||||
|
/// Sent by the client at any point after [auth](struct.Auth.html) if it
|
||||||
|
/// wants to cancel the current authentication process.
|
||||||
|
Abort,
|
||||||
|
"abort",
|
||||||
|
SASL
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// Sent by the server on SASL success.
|
||||||
|
Success, "success", SASL,
|
||||||
|
text: (
|
||||||
|
/// Possible data sent on success.
|
||||||
|
data: Base64<Vec<u8>>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_element_enum!(
|
||||||
|
/// List of possible failure conditions for SASL.
|
||||||
|
DefinedCondition, "defined-condition", SASL, {
|
||||||
|
/// The client aborted the authentication with
|
||||||
|
/// [abort](struct.Abort.html).
|
||||||
|
Aborted => "aborted",
|
||||||
|
|
||||||
|
/// The account the client is trying to authenticate against has been
|
||||||
|
/// disabled.
|
||||||
|
AccountDisabled => "account-disabled",
|
||||||
|
|
||||||
|
/// The credentials for this account have expired.
|
||||||
|
CredentialsExpired => "credentials-expired",
|
||||||
|
|
||||||
|
/// You must enable StartTLS or use direct TLS before using this
|
||||||
|
/// authentication mechanism.
|
||||||
|
EncryptionRequired => "encryption-required",
|
||||||
|
|
||||||
|
/// The base64 data sent by the client is invalid.
|
||||||
|
IncorrectEncoding => "incorrect-encoding",
|
||||||
|
|
||||||
|
/// The authzid provided by the client is invalid.
|
||||||
|
InvalidAuthzid => "invalid-authzid",
|
||||||
|
|
||||||
|
/// The client tried to use an invalid mechanism, or none.
|
||||||
|
InvalidMechanism => "invalid-mechanism",
|
||||||
|
|
||||||
|
/// The client sent a bad request.
|
||||||
|
MalformedRequest => "malformed-request",
|
||||||
|
|
||||||
|
/// The mechanism selected is weaker than what the server allows.
|
||||||
|
MechanismTooWeak => "mechanism-too-weak",
|
||||||
|
|
||||||
|
/// The credentials provided are invalid.
|
||||||
|
NotAuthorized => "not-authorized",
|
||||||
|
|
||||||
|
/// The server encountered an issue which may be fixed later, the
|
||||||
|
/// client should retry at some point.
|
||||||
|
TemporaryAuthFailure => "temporary-auth-failure",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
type Lang = String;
|
||||||
|
|
||||||
|
/// Sent by the server on SASL failure.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Failure {
|
||||||
|
/// One of the allowed defined-conditions for SASL.
|
||||||
|
pub defined_condition: DefinedCondition,
|
||||||
|
|
||||||
|
/// A human-readable explanation for the failure.
|
||||||
|
pub texts: BTreeMap<Lang, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<Element> for Failure {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(root: Element) -> Result<Failure, Error> {
|
||||||
|
check_self!(root, "failure", SASL);
|
||||||
|
check_no_attributes!(root, "failure");
|
||||||
|
|
||||||
|
let mut defined_condition = None;
|
||||||
|
let mut texts = BTreeMap::new();
|
||||||
|
|
||||||
|
for child in root.children() {
|
||||||
|
if child.is("text", ns::SASL) {
|
||||||
|
check_no_unknown_attributes!(child, "text", ["xml:lang"]);
|
||||||
|
check_no_children!(child, "text");
|
||||||
|
let lang = get_attr!(child, "xml:lang", Default);
|
||||||
|
if texts.insert(lang, child.text()).is_some() {
|
||||||
|
return Err(Error::ParseError(
|
||||||
|
"Text element present twice for the same xml:lang in failure element.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else if child.has_ns(ns::SASL) {
|
||||||
|
if defined_condition.is_some() {
|
||||||
|
return Err(Error::ParseError(
|
||||||
|
"Failure must not have more than one defined-condition.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
check_no_attributes!(child, "defined-condition");
|
||||||
|
check_no_children!(child, "defined-condition");
|
||||||
|
let condition = match DefinedCondition::try_from(child.clone()) {
|
||||||
|
Ok(condition) => condition,
|
||||||
|
// TODO: do we really want to eat this error?
|
||||||
|
Err(_) => DefinedCondition::NotAuthorized,
|
||||||
|
};
|
||||||
|
defined_condition = Some(condition);
|
||||||
|
} else {
|
||||||
|
return Err(Error::ParseError("Unknown element in Failure."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let defined_condition =
|
||||||
|
defined_condition.ok_or(Error::ParseError("Failure must have a defined-condition."))?;
|
||||||
|
|
||||||
|
Ok(Failure {
|
||||||
|
defined_condition,
|
||||||
|
texts,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Failure> for Element {
|
||||||
|
fn from(failure: Failure) -> Element {
|
||||||
|
Element::builder("failure", ns::SASL)
|
||||||
|
.append(failure.defined_condition)
|
||||||
|
.append_all(failure.texts.into_iter().map(|(lang, text)| {
|
||||||
|
Element::builder("text", ns::SASL)
|
||||||
|
.attr("xml:lang", lang)
|
||||||
|
.append(text)
|
||||||
|
}))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::Element;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "32")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(Mechanism, 1);
|
||||||
|
assert_size!(Auth, 16);
|
||||||
|
assert_size!(Challenge, 12);
|
||||||
|
assert_size!(Response, 12);
|
||||||
|
assert_size!(Abort, 0);
|
||||||
|
assert_size!(Success, 12);
|
||||||
|
assert_size!(DefinedCondition, 1);
|
||||||
|
assert_size!(Failure, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "64")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(Mechanism, 1);
|
||||||
|
assert_size!(Auth, 32);
|
||||||
|
assert_size!(Challenge, 24);
|
||||||
|
assert_size!(Response, 24);
|
||||||
|
assert_size!(Abort, 0);
|
||||||
|
assert_size!(Success, 24);
|
||||||
|
assert_size!(DefinedCondition, 1);
|
||||||
|
assert_size!(Failure, 32);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_simple() {
|
||||||
|
let elem: Element = "<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let auth = Auth::try_from(elem).unwrap();
|
||||||
|
assert_eq!(auth.mechanism, Mechanism::Plain);
|
||||||
|
assert!(auth.data.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn section_6_5_1() {
|
||||||
|
let elem: Element =
|
||||||
|
"<failure xmlns='urn:ietf:params:xml:ns:xmpp-sasl'><aborted/></failure>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let failure = Failure::try_from(elem).unwrap();
|
||||||
|
assert_eq!(failure.defined_condition, DefinedCondition::Aborted);
|
||||||
|
assert!(failure.texts.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn section_6_5_2() {
|
||||||
|
let elem: Element = "<failure xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>
|
||||||
|
<account-disabled/>
|
||||||
|
<text xml:lang='en'>Call 212-555-1212 for assistance.</text>
|
||||||
|
</failure>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let failure = Failure::try_from(elem).unwrap();
|
||||||
|
assert_eq!(failure.defined_condition, DefinedCondition::AccountDisabled);
|
||||||
|
assert_eq!(
|
||||||
|
failure.texts["en"],
|
||||||
|
String::from("Call 212-555-1212 for assistance.")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Some servers apparently use a non-namespaced 'lang' attribute, which is invalid as not part
|
||||||
|
/// of the schema. This tests whether we can parse it when disabling validation.
|
||||||
|
#[cfg(feature = "disable-validation")]
|
||||||
|
#[test]
|
||||||
|
fn invalid_failure_with_non_prefixed_text_lang() {
|
||||||
|
let elem: Element = "<failure xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>
|
||||||
|
<not-authorized xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>
|
||||||
|
<text xmlns='urn:ietf:params:xml:ns:xmpp-sasl' lang='en'>Invalid username or password</text>
|
||||||
|
</failure>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let failure = Failure::try_from(elem).unwrap();
|
||||||
|
assert_eq!(failure.defined_condition, DefinedCondition::NotAuthorized);
|
||||||
|
assert_eq!(
|
||||||
|
failure.texts[""],
|
||||||
|
String::from("Invalid username or password")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,209 @@
|
||||||
|
// Copyright (C) 2019 Maxime “pep” Buquet <pep@bouah.net>
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
use crate::data_forms::{DataForm, DataFormType, Field, FieldType};
|
||||||
|
use crate::ns;
|
||||||
|
use crate::util::error::Error;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
/// Structure representing a `http://jabber.org/network/serverinfo` form type.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Default)]
|
||||||
|
pub struct ServerInfo {
|
||||||
|
/// Abuse addresses
|
||||||
|
pub abuse: Vec<String>,
|
||||||
|
|
||||||
|
/// Admin addresses
|
||||||
|
pub admin: Vec<String>,
|
||||||
|
|
||||||
|
/// Feedback addresses
|
||||||
|
pub feedback: Vec<String>,
|
||||||
|
|
||||||
|
/// Sales addresses
|
||||||
|
pub sales: Vec<String>,
|
||||||
|
|
||||||
|
/// Security addresses
|
||||||
|
pub security: Vec<String>,
|
||||||
|
|
||||||
|
/// Support addresses
|
||||||
|
pub support: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<DataForm> for ServerInfo {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(form: DataForm) -> Result<ServerInfo, Error> {
|
||||||
|
if form.type_ != DataFormType::Result_ {
|
||||||
|
return Err(Error::ParseError("Wrong type of form."));
|
||||||
|
}
|
||||||
|
if form.form_type != Some(String::from(ns::SERVER_INFO)) {
|
||||||
|
return Err(Error::ParseError("Wrong FORM_TYPE for form."));
|
||||||
|
}
|
||||||
|
let mut server_info = ServerInfo::default();
|
||||||
|
for field in form.fields {
|
||||||
|
if field.type_ != FieldType::ListMulti {
|
||||||
|
return Err(Error::ParseError("Field is not of the required type."));
|
||||||
|
}
|
||||||
|
if field.var == "abuse-addresses" {
|
||||||
|
server_info.abuse = field.values;
|
||||||
|
} else if field.var == "admin-addresses" {
|
||||||
|
server_info.admin = field.values;
|
||||||
|
} else if field.var == "feedback-addresses" {
|
||||||
|
server_info.feedback = field.values;
|
||||||
|
} else if field.var == "sales-addresses" {
|
||||||
|
server_info.sales = field.values;
|
||||||
|
} else if field.var == "security-addresses" {
|
||||||
|
server_info.security = field.values;
|
||||||
|
} else if field.var == "support-addresses" {
|
||||||
|
server_info.support = field.values;
|
||||||
|
} else {
|
||||||
|
return Err(Error::ParseError("Unknown form field var."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(server_info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ServerInfo> for DataForm {
|
||||||
|
fn from(server_info: ServerInfo) -> DataForm {
|
||||||
|
DataForm {
|
||||||
|
type_: DataFormType::Result_,
|
||||||
|
form_type: Some(String::from(ns::SERVER_INFO)),
|
||||||
|
title: None,
|
||||||
|
instructions: None,
|
||||||
|
fields: vec![
|
||||||
|
generate_address_field("abuse-addresses", server_info.abuse),
|
||||||
|
generate_address_field("admin-addresses", server_info.admin),
|
||||||
|
generate_address_field("feedback-addresses", server_info.feedback),
|
||||||
|
generate_address_field("sales-addresses", server_info.sales),
|
||||||
|
generate_address_field("security-addresses", server_info.security),
|
||||||
|
generate_address_field("support-addresses", server_info.support),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate `Field` for addresses
|
||||||
|
pub fn generate_address_field<S: Into<String>>(var: S, values: Vec<String>) -> Field {
|
||||||
|
Field {
|
||||||
|
var: var.into(),
|
||||||
|
type_: FieldType::ListMulti,
|
||||||
|
label: None,
|
||||||
|
required: false,
|
||||||
|
options: vec![],
|
||||||
|
values,
|
||||||
|
media: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::data_forms::{DataForm, DataFormType, Field, FieldType};
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "32")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(ServerInfo, 72);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "64")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(ServerInfo, 144);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_simple() {
|
||||||
|
let form = DataForm {
|
||||||
|
type_: DataFormType::Result_,
|
||||||
|
form_type: Some(String::from(ns::SERVER_INFO)),
|
||||||
|
title: None,
|
||||||
|
instructions: None,
|
||||||
|
fields: vec![
|
||||||
|
Field {
|
||||||
|
var: String::from("abuse-addresses"),
|
||||||
|
type_: FieldType::ListMulti,
|
||||||
|
label: None,
|
||||||
|
required: false,
|
||||||
|
options: vec![],
|
||||||
|
values: vec![],
|
||||||
|
media: vec![],
|
||||||
|
},
|
||||||
|
Field {
|
||||||
|
var: String::from("admin-addresses"),
|
||||||
|
type_: FieldType::ListMulti,
|
||||||
|
label: None,
|
||||||
|
required: false,
|
||||||
|
options: vec![],
|
||||||
|
values: vec![
|
||||||
|
String::from("xmpp:admin@foo.bar"),
|
||||||
|
String::from("https://foo.bar/chat/"),
|
||||||
|
String::from("mailto:admin@foo.bar"),
|
||||||
|
],
|
||||||
|
media: vec![],
|
||||||
|
},
|
||||||
|
Field {
|
||||||
|
var: String::from("feedback-addresses"),
|
||||||
|
type_: FieldType::ListMulti,
|
||||||
|
label: None,
|
||||||
|
required: false,
|
||||||
|
options: vec![],
|
||||||
|
values: vec![],
|
||||||
|
media: vec![],
|
||||||
|
},
|
||||||
|
Field {
|
||||||
|
var: String::from("sales-addresses"),
|
||||||
|
type_: FieldType::ListMulti,
|
||||||
|
label: None,
|
||||||
|
required: false,
|
||||||
|
options: vec![],
|
||||||
|
values: vec![],
|
||||||
|
media: vec![],
|
||||||
|
},
|
||||||
|
Field {
|
||||||
|
var: String::from("security-addresses"),
|
||||||
|
type_: FieldType::ListMulti,
|
||||||
|
label: None,
|
||||||
|
required: false,
|
||||||
|
options: vec![],
|
||||||
|
values: vec![
|
||||||
|
String::from("xmpp:security@foo.bar"),
|
||||||
|
String::from("mailto:security@foo.bar"),
|
||||||
|
],
|
||||||
|
media: vec![],
|
||||||
|
},
|
||||||
|
Field {
|
||||||
|
var: String::from("support-addresses"),
|
||||||
|
type_: FieldType::ListMulti,
|
||||||
|
label: None,
|
||||||
|
required: false,
|
||||||
|
options: vec![],
|
||||||
|
values: vec![String::from("mailto:support@foo.bar")],
|
||||||
|
media: vec![],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
let server_info = ServerInfo {
|
||||||
|
abuse: vec![],
|
||||||
|
admin: vec![
|
||||||
|
String::from("xmpp:admin@foo.bar"),
|
||||||
|
String::from("https://foo.bar/chat/"),
|
||||||
|
String::from("mailto:admin@foo.bar"),
|
||||||
|
],
|
||||||
|
feedback: vec![],
|
||||||
|
sales: vec![],
|
||||||
|
security: vec![
|
||||||
|
String::from("xmpp:security@foo.bar"),
|
||||||
|
String::from("mailto:security@foo.bar"),
|
||||||
|
],
|
||||||
|
support: vec![String::from("mailto:support@foo.bar")],
|
||||||
|
};
|
||||||
|
|
||||||
|
// assert_eq!(DataForm::from(server_info), form);
|
||||||
|
assert_eq!(ServerInfo::try_from(form).unwrap(), server_info);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,247 @@
|
||||||
|
// Copyright (c) 2018 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
use crate::stanza_error::DefinedCondition;
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// Acknowledgement of the currently received stanzas.
|
||||||
|
A, "a", SM,
|
||||||
|
attributes: [
|
||||||
|
/// The last handled stanza.
|
||||||
|
h: Required<u32> = "h",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
impl A {
|
||||||
|
/// Generates a new `<a/>` element.
|
||||||
|
pub fn new(h: u32) -> A {
|
||||||
|
A { h }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_attribute!(
|
||||||
|
/// Whether to allow resumption of a previous stream.
|
||||||
|
ResumeAttr,
|
||||||
|
"resume",
|
||||||
|
bool
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// Client request for enabling stream management.
|
||||||
|
#[derive(Default)]
|
||||||
|
Enable, "enable", SM,
|
||||||
|
attributes: [
|
||||||
|
/// The preferred resumption time in seconds by the client.
|
||||||
|
// TODO: should be the infinite integer set ≥ 1.
|
||||||
|
max: Option<u32> = "max",
|
||||||
|
|
||||||
|
/// Whether the client wants to be allowed to resume the stream.
|
||||||
|
resume: Default<ResumeAttr> = "resume",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
impl Enable {
|
||||||
|
/// Generates a new `<enable/>` element.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Enable::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the preferred resumption time in seconds.
|
||||||
|
pub fn with_max(mut self, max: u32) -> Self {
|
||||||
|
self.max = Some(max);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Asks for resumption to be possible.
|
||||||
|
pub fn with_resume(mut self) -> Self {
|
||||||
|
self.resume = ResumeAttr::True;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_id!(
|
||||||
|
/// A random identifier used for stream resumption.
|
||||||
|
StreamId
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// Server response once stream management is enabled.
|
||||||
|
Enabled, "enabled", SM,
|
||||||
|
attributes: [
|
||||||
|
/// A random identifier used for stream resumption.
|
||||||
|
id: Option<StreamId> = "id",
|
||||||
|
|
||||||
|
/// The preferred IP, domain, IP:port or domain:port location for
|
||||||
|
/// resumption.
|
||||||
|
location: Option<String> = "location",
|
||||||
|
|
||||||
|
/// The preferred resumption time in seconds by the server.
|
||||||
|
// TODO: should be the infinite integer set ≥ 1.
|
||||||
|
max: Option<u32> = "max",
|
||||||
|
|
||||||
|
/// Whether stream resumption is allowed.
|
||||||
|
resume: Default<ResumeAttr> = "resume",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// A stream management error happened.
|
||||||
|
Failed, "failed", SM,
|
||||||
|
attributes: [
|
||||||
|
/// The last handled stanza.
|
||||||
|
h: Option<u32> = "h",
|
||||||
|
],
|
||||||
|
children: [
|
||||||
|
/// The error returned.
|
||||||
|
// XXX: implement the * handling.
|
||||||
|
error: Option<DefinedCondition> = ("*", XMPP_STANZAS) => DefinedCondition
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_empty_element!(
|
||||||
|
/// Requests the currently received stanzas by the other party.
|
||||||
|
R,
|
||||||
|
"r",
|
||||||
|
SM
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// Requests a stream resumption.
|
||||||
|
Resume, "resume", SM,
|
||||||
|
attributes: [
|
||||||
|
/// The last handled stanza.
|
||||||
|
h: Required<u32> = "h",
|
||||||
|
|
||||||
|
/// The previous id given by the server on
|
||||||
|
/// [enabled](struct.Enabled.html).
|
||||||
|
previd: Required<StreamId> = "previd",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// The response by the server for a successfully resumed stream.
|
||||||
|
Resumed, "resumed", SM,
|
||||||
|
attributes: [
|
||||||
|
/// The last handled stanza.
|
||||||
|
h: Required<u32> = "h",
|
||||||
|
|
||||||
|
/// The previous id given by the server on
|
||||||
|
/// [enabled](struct.Enabled.html).
|
||||||
|
previd: Required<StreamId> = "previd",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO: add support for optional and required.
|
||||||
|
generate_empty_element!(
|
||||||
|
/// Represents availability of Stream Management in `<stream:features/>`.
|
||||||
|
StreamManagement,
|
||||||
|
"sm",
|
||||||
|
SM
|
||||||
|
);
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::Element;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "32")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(A, 4);
|
||||||
|
assert_size!(ResumeAttr, 1);
|
||||||
|
assert_size!(Enable, 12);
|
||||||
|
assert_size!(StreamId, 12);
|
||||||
|
assert_size!(Enabled, 36);
|
||||||
|
assert_size!(Failed, 12);
|
||||||
|
assert_size!(R, 0);
|
||||||
|
assert_size!(Resume, 16);
|
||||||
|
assert_size!(Resumed, 16);
|
||||||
|
assert_size!(StreamManagement, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "64")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(A, 4);
|
||||||
|
assert_size!(ResumeAttr, 1);
|
||||||
|
assert_size!(Enable, 12);
|
||||||
|
assert_size!(StreamId, 24);
|
||||||
|
assert_size!(Enabled, 64);
|
||||||
|
assert_size!(Failed, 12);
|
||||||
|
assert_size!(R, 0);
|
||||||
|
assert_size!(Resume, 32);
|
||||||
|
assert_size!(Resumed, 32);
|
||||||
|
assert_size!(StreamManagement, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a() {
|
||||||
|
let elem: Element = "<a xmlns='urn:xmpp:sm:3' h='5'".parse().unwrap();
|
||||||
|
let a = A::try_from(elem).unwrap();
|
||||||
|
assert_eq!(a.h, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stream_feature() {
|
||||||
|
let elem: Element = "<sm xmlns='urn:xmpp:sm:3'/>".parse().unwrap();
|
||||||
|
StreamManagement::try_from(elem).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resume() {
|
||||||
|
let elem: Element = "<enable xmlns='urn:xmpp:sm:3' resume='true'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let enable = Enable::try_from(elem).unwrap();
|
||||||
|
assert_eq!(enable.max, None);
|
||||||
|
assert_eq!(enable.resume, ResumeAttr::True);
|
||||||
|
|
||||||
|
let elem: Element = "<enabled xmlns='urn:xmpp:sm:3' resume='true' id='coucou' max='600'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let enabled = Enabled::try_from(elem).unwrap();
|
||||||
|
let previd = enabled.id.unwrap();
|
||||||
|
assert_eq!(enabled.resume, ResumeAttr::True);
|
||||||
|
assert_eq!(previd, StreamId(String::from("coucou")));
|
||||||
|
assert_eq!(enabled.max, Some(600));
|
||||||
|
assert_eq!(enabled.location, None);
|
||||||
|
|
||||||
|
let elem: Element = "<resume xmlns='urn:xmpp:sm:3' h='5' previd='coucou'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let resume = Resume::try_from(elem).unwrap();
|
||||||
|
assert_eq!(resume.h, 5);
|
||||||
|
assert_eq!(resume.previd, previd);
|
||||||
|
|
||||||
|
let elem: Element = "<resumed xmlns='urn:xmpp:sm:3' h='5' previd='coucou'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let resumed = Resumed::try_from(elem).unwrap();
|
||||||
|
assert_eq!(resumed.h, 5);
|
||||||
|
assert_eq!(resumed.previd, previd);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialize_failed() {
|
||||||
|
let reference: Element = "<failed xmlns='urn:xmpp:sm:3'><unexpected-request xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/></failed>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let elem: Element = "<unexpected-request xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let error = DefinedCondition::try_from(elem).unwrap();
|
||||||
|
|
||||||
|
let failed = Failed {
|
||||||
|
h: None,
|
||||||
|
error: Some(error),
|
||||||
|
};
|
||||||
|
let serialized: Element = failed.into();
|
||||||
|
assert_eq!(serialized, reference);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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::message::MessagePayload;
|
||||||
|
use crate::ns;
|
||||||
|
use crate::presence::PresencePayload;
|
||||||
|
use crate::util::error::Error;
|
||||||
|
use crate::Element;
|
||||||
|
use jid::Jid;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
generate_attribute!(
|
||||||
|
/// The type of the error.
|
||||||
|
ErrorType, "type", {
|
||||||
|
/// Retry after providing credentials.
|
||||||
|
Auth => "auth",
|
||||||
|
|
||||||
|
/// Do not retry (the error cannot be remedied).
|
||||||
|
Cancel => "cancel",
|
||||||
|
|
||||||
|
/// Proceed (the condition was only a warning).
|
||||||
|
Continue => "continue",
|
||||||
|
|
||||||
|
/// Retry after changing the data sent.
|
||||||
|
Modify => "modify",
|
||||||
|
|
||||||
|
/// Retry after waiting (the error is temporary).
|
||||||
|
Wait => "wait",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_element_enum!(
|
||||||
|
/// List of valid error conditions.
|
||||||
|
DefinedCondition, "condition", XMPP_STANZAS, {
|
||||||
|
/// The sender has sent a stanza containing XML that does not conform
|
||||||
|
/// to the appropriate schema or that cannot be processed (e.g., an IQ
|
||||||
|
/// stanza that includes an unrecognized value of the 'type' attribute,
|
||||||
|
/// or an element that is qualified by a recognized namespace but that
|
||||||
|
/// violates the defined syntax for the element); the associated error
|
||||||
|
/// type SHOULD be "modify".
|
||||||
|
BadRequest => "bad-request",
|
||||||
|
|
||||||
|
/// Access cannot be granted because an existing resource exists with
|
||||||
|
/// the same name or address; the associated error type SHOULD be
|
||||||
|
/// "cancel".
|
||||||
|
Conflict => "conflict",
|
||||||
|
|
||||||
|
/// The feature represented in the XML stanza is not implemented by the
|
||||||
|
/// intended recipient or an intermediate server and therefore the
|
||||||
|
/// stanza cannot be processed (e.g., the entity understands the
|
||||||
|
/// namespace but does not recognize the element name); the associated
|
||||||
|
/// error type SHOULD be "cancel" or "modify".
|
||||||
|
FeatureNotImplemented => "feature-not-implemented",
|
||||||
|
|
||||||
|
/// The requesting entity does not possess the necessary permissions to
|
||||||
|
/// perform an action that only certain authorized roles or individuals
|
||||||
|
/// are allowed to complete (i.e., it typically relates to
|
||||||
|
/// authorization rather than authentication); the associated error
|
||||||
|
/// type SHOULD be "auth".
|
||||||
|
Forbidden => "forbidden",
|
||||||
|
|
||||||
|
/// The recipient or server can no longer be contacted at this address,
|
||||||
|
/// typically on a permanent basis (as opposed to the <redirect/> error
|
||||||
|
/// condition, which is used for temporary addressing failures); the
|
||||||
|
/// associated error type SHOULD be "cancel" and the error stanza
|
||||||
|
/// SHOULD include a new address (if available) as the XML character
|
||||||
|
/// data of the <gone/> element (which MUST be a Uniform Resource
|
||||||
|
/// Identifier [URI] or Internationalized Resource Identifier [IRI] at
|
||||||
|
/// which the entity can be contacted, typically an XMPP IRI as
|
||||||
|
/// specified in [XMPP‑URI]).
|
||||||
|
Gone => "gone",
|
||||||
|
|
||||||
|
/// The server has experienced a misconfiguration or other internal
|
||||||
|
/// error that prevents it from processing the stanza; the associated
|
||||||
|
/// error type SHOULD be "cancel".
|
||||||
|
InternalServerError => "internal-server-error",
|
||||||
|
|
||||||
|
/// The addressed JID or item requested cannot be found; the associated
|
||||||
|
/// error type SHOULD be "cancel".
|
||||||
|
ItemNotFound => "item-not-found",
|
||||||
|
|
||||||
|
/// The sending entity has provided (e.g., during resource binding) or
|
||||||
|
/// communicated (e.g., in the 'to' address of a stanza) an XMPP
|
||||||
|
/// address or aspect thereof that violates the rules defined in
|
||||||
|
/// [XMPP‑ADDR]; the associated error type SHOULD be "modify".
|
||||||
|
JidMalformed => "jid-malformed",
|
||||||
|
|
||||||
|
/// The recipient or server understands the request but cannot process
|
||||||
|
/// it because the request does not meet criteria defined by the
|
||||||
|
/// recipient or server (e.g., a request to subscribe to information
|
||||||
|
/// that does not simultaneously include configuration parameters
|
||||||
|
/// needed by the recipient); the associated error type SHOULD be
|
||||||
|
/// "modify".
|
||||||
|
NotAcceptable => "not-acceptable",
|
||||||
|
|
||||||
|
/// The recipient or server does not allow any entity to perform the
|
||||||
|
/// action (e.g., sending to entities at a blacklisted domain); the
|
||||||
|
/// associated error type SHOULD be "cancel".
|
||||||
|
NotAllowed => "not-allowed",
|
||||||
|
|
||||||
|
/// The sender needs to provide credentials before being allowed to
|
||||||
|
/// perform the action, or has provided improper credentials (the name
|
||||||
|
/// "not-authorized", which was borrowed from the "401 Unauthorized"
|
||||||
|
/// error of [HTTP], might lead the reader to think that this condition
|
||||||
|
/// relates to authorization, but instead it is typically used in
|
||||||
|
/// relation to authentication); the associated error type SHOULD be
|
||||||
|
/// "auth".
|
||||||
|
NotAuthorized => "not-authorized",
|
||||||
|
|
||||||
|
/// The entity has violated some local service policy (e.g., a message
|
||||||
|
/// contains words that are prohibited by the service) and the server
|
||||||
|
/// MAY choose to specify the policy in the <text/> element or in an
|
||||||
|
/// application-specific condition element; the associated error type
|
||||||
|
/// SHOULD be "modify" or "wait" depending on the policy being
|
||||||
|
/// violated.
|
||||||
|
PolicyViolation => "policy-violation",
|
||||||
|
|
||||||
|
/// The intended recipient is temporarily unavailable, undergoing
|
||||||
|
/// maintenance, etc.; the associated error type SHOULD be "wait".
|
||||||
|
RecipientUnavailable => "recipient-unavailable",
|
||||||
|
|
||||||
|
/// The recipient or server is redirecting requests for this
|
||||||
|
/// information to another entity, typically in a temporary fashion (as
|
||||||
|
/// opposed to the <gone/> error condition, which is used for permanent
|
||||||
|
/// addressing failures); the associated error type SHOULD be "modify"
|
||||||
|
/// and the error stanza SHOULD contain the alternate address in the
|
||||||
|
/// XML character data of the <redirect/> element (which MUST be a URI
|
||||||
|
/// or IRI with which the sender can communicate, typically an XMPP IRI
|
||||||
|
/// as specified in [XMPP‑URI]).
|
||||||
|
Redirect => "redirect",
|
||||||
|
|
||||||
|
/// The requesting entity is not authorized to access the requested
|
||||||
|
/// service because prior registration is necessary (examples of prior
|
||||||
|
/// registration include members-only rooms in XMPP multi-user chat
|
||||||
|
/// [XEP‑0045] and gateways to non-XMPP instant messaging services,
|
||||||
|
/// which traditionally required registration in order to use the
|
||||||
|
/// gateway [XEP‑0100]); the associated error type SHOULD be "auth".
|
||||||
|
RegistrationRequired => "registration-required",
|
||||||
|
|
||||||
|
/// A remote server or service specified as part or all of the JID of
|
||||||
|
/// the intended recipient does not exist or cannot be resolved (e.g.,
|
||||||
|
/// there is no _xmpp-server._tcp DNS SRV record, the A or AAAA
|
||||||
|
/// fallback resolution fails, or A/AAAA lookups succeed but there is
|
||||||
|
/// no response on the IANA-registered port 5269); the associated error
|
||||||
|
/// type SHOULD be "cancel".
|
||||||
|
RemoteServerNotFound => "remote-server-not-found",
|
||||||
|
|
||||||
|
/// A remote server or service specified as part or all of the JID of
|
||||||
|
/// the intended recipient (or needed to fulfill a request) was
|
||||||
|
/// resolved but communications could not be established within a
|
||||||
|
/// reasonable amount of time (e.g., an XML stream cannot be
|
||||||
|
/// established at the resolved IP address and port, or an XML stream
|
||||||
|
/// can be established but stream negotiation fails because of problems
|
||||||
|
/// with TLS, SASL, Server Dialback, etc.); the associated error type
|
||||||
|
/// SHOULD be "wait" (unless the error is of a more permanent nature,
|
||||||
|
/// e.g., the remote server is found but it cannot be authenticated or
|
||||||
|
/// it violates security policies).
|
||||||
|
RemoteServerTimeout => "remote-server-timeout",
|
||||||
|
|
||||||
|
/// The server or recipient is busy or lacks the system resources
|
||||||
|
/// necessary to service the request; the associated error type SHOULD
|
||||||
|
/// be "wait".
|
||||||
|
ResourceConstraint => "resource-constraint",
|
||||||
|
|
||||||
|
/// The server or recipient does not currently provide the requested
|
||||||
|
/// service; the associated error type SHOULD be "cancel".
|
||||||
|
ServiceUnavailable => "service-unavailable",
|
||||||
|
|
||||||
|
/// The requesting entity is not authorized to access the requested
|
||||||
|
/// service because a prior subscription is necessary (examples of
|
||||||
|
/// prior subscription include authorization to receive presence
|
||||||
|
/// information as defined in [XMPP‑IM] and opt-in data feeds for XMPP
|
||||||
|
/// publish-subscribe as defined in [XEP‑0060]); the associated error
|
||||||
|
/// type SHOULD be "auth".
|
||||||
|
SubscriptionRequired => "subscription-required",
|
||||||
|
|
||||||
|
/// The error condition is not one of those defined by the other
|
||||||
|
/// conditions in this list; any error type can be associated with this
|
||||||
|
/// condition, and it SHOULD NOT be used except in conjunction with an
|
||||||
|
/// application-specific condition.
|
||||||
|
UndefinedCondition => "undefined-condition",
|
||||||
|
|
||||||
|
/// The recipient or server understood the request but was not
|
||||||
|
/// expecting it at this time (e.g., the request was out of order); the
|
||||||
|
/// associated error type SHOULD be "wait" or "modify".
|
||||||
|
UnexpectedRequest => "unexpected-request",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
type Lang = String;
|
||||||
|
|
||||||
|
/// The representation of a stanza error.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct StanzaError {
|
||||||
|
/// The type of this error.
|
||||||
|
pub type_: ErrorType,
|
||||||
|
|
||||||
|
/// The JID of the entity who set this error.
|
||||||
|
pub by: Option<Jid>,
|
||||||
|
|
||||||
|
/// One of the defined conditions for this error to happen.
|
||||||
|
pub defined_condition: DefinedCondition,
|
||||||
|
|
||||||
|
/// Human-readable description of this error.
|
||||||
|
pub texts: BTreeMap<Lang, String>,
|
||||||
|
|
||||||
|
/// A protocol-specific extension for this error.
|
||||||
|
pub other: Option<Element>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessagePayload for StanzaError {}
|
||||||
|
impl PresencePayload for StanzaError {}
|
||||||
|
|
||||||
|
impl StanzaError {
|
||||||
|
/// Create a new `<error/>` with the according content.
|
||||||
|
pub fn new<L, T>(
|
||||||
|
type_: ErrorType,
|
||||||
|
defined_condition: DefinedCondition,
|
||||||
|
lang: L,
|
||||||
|
text: T,
|
||||||
|
) -> StanzaError
|
||||||
|
where
|
||||||
|
L: Into<Lang>,
|
||||||
|
T: Into<String>,
|
||||||
|
{
|
||||||
|
StanzaError {
|
||||||
|
type_,
|
||||||
|
by: None,
|
||||||
|
defined_condition,
|
||||||
|
texts: {
|
||||||
|
let mut map = BTreeMap::new();
|
||||||
|
map.insert(lang.into(), text.into());
|
||||||
|
map
|
||||||
|
},
|
||||||
|
other: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<Element> for StanzaError {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(elem: Element) -> Result<StanzaError, Error> {
|
||||||
|
check_self!(elem, "error", DEFAULT_NS);
|
||||||
|
check_no_unknown_attributes!(elem, "error", ["type", "by"]);
|
||||||
|
|
||||||
|
let mut stanza_error = StanzaError {
|
||||||
|
type_: get_attr!(elem, "type", Required),
|
||||||
|
by: get_attr!(elem, "by", Option),
|
||||||
|
defined_condition: DefinedCondition::UndefinedCondition,
|
||||||
|
texts: BTreeMap::new(),
|
||||||
|
other: None,
|
||||||
|
};
|
||||||
|
let mut defined_condition = None;
|
||||||
|
|
||||||
|
for child in elem.children() {
|
||||||
|
if child.is("text", ns::XMPP_STANZAS) {
|
||||||
|
check_no_children!(child, "text");
|
||||||
|
check_no_unknown_attributes!(child, "text", ["xml:lang"]);
|
||||||
|
let lang = get_attr!(elem, "xml:lang", Default);
|
||||||
|
if stanza_error.texts.insert(lang, child.text()).is_some() {
|
||||||
|
return Err(Error::ParseError(
|
||||||
|
"Text element present twice for the same xml:lang.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else if child.has_ns(ns::XMPP_STANZAS) {
|
||||||
|
if defined_condition.is_some() {
|
||||||
|
return Err(Error::ParseError(
|
||||||
|
"Error must not have more than one defined-condition.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
check_no_children!(child, "defined-condition");
|
||||||
|
check_no_attributes!(child, "defined-condition");
|
||||||
|
let condition = DefinedCondition::try_from(child.clone())?;
|
||||||
|
defined_condition = Some(condition);
|
||||||
|
} else {
|
||||||
|
if stanza_error.other.is_some() {
|
||||||
|
return Err(Error::ParseError(
|
||||||
|
"Error must not have more than one other element.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
stanza_error.other = Some(child.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stanza_error.defined_condition =
|
||||||
|
defined_condition.ok_or(Error::ParseError("Error must have a defined-condition."))?;
|
||||||
|
|
||||||
|
Ok(stanza_error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<StanzaError> for Element {
|
||||||
|
fn from(err: StanzaError) -> Element {
|
||||||
|
Element::builder("error", ns::DEFAULT_NS)
|
||||||
|
.attr("type", err.type_)
|
||||||
|
.attr("by", err.by)
|
||||||
|
.append(err.defined_condition)
|
||||||
|
.append_all(err.texts.into_iter().map(|(lang, text)| {
|
||||||
|
Element::builder("text", ns::XMPP_STANZAS)
|
||||||
|
.attr("xml:lang", lang)
|
||||||
|
.append(text)
|
||||||
|
}))
|
||||||
|
.append_all(err.other)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "32")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(ErrorType, 1);
|
||||||
|
assert_size!(DefinedCondition, 1);
|
||||||
|
assert_size!(StanzaError, 132);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "64")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(ErrorType, 1);
|
||||||
|
assert_size!(DefinedCondition, 1);
|
||||||
|
assert_size!(StanzaError, 264);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_simple() {
|
||||||
|
#[cfg(not(feature = "component"))]
|
||||||
|
let elem: Element = "<error xmlns='jabber:client' type='cancel'><undefined-condition xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/></error>".parse().unwrap();
|
||||||
|
#[cfg(feature = "component")]
|
||||||
|
let elem: Element = "<error xmlns='jabber:component:accept' type='cancel'><undefined-condition xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/></error>".parse().unwrap();
|
||||||
|
let error = StanzaError::try_from(elem).unwrap();
|
||||||
|
assert_eq!(error.type_, ErrorType::Cancel);
|
||||||
|
assert_eq!(
|
||||||
|
error.defined_condition,
|
||||||
|
DefinedCondition::UndefinedCondition
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_type() {
|
||||||
|
#[cfg(not(feature = "component"))]
|
||||||
|
let elem: Element = "<error xmlns='jabber:client'/>".parse().unwrap();
|
||||||
|
#[cfg(feature = "component")]
|
||||||
|
let elem: Element = "<error xmlns='jabber:component:accept'/>".parse().unwrap();
|
||||||
|
let error = StanzaError::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Required attribute 'type' missing.");
|
||||||
|
|
||||||
|
#[cfg(not(feature = "component"))]
|
||||||
|
let elem: Element = "<error xmlns='jabber:client' type='coucou'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
#[cfg(feature = "component")]
|
||||||
|
let elem: Element = "<error xmlns='jabber:component:accept' type='coucou'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = StanzaError::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Unknown value for 'type' attribute.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_condition() {
|
||||||
|
#[cfg(not(feature = "component"))]
|
||||||
|
let elem: Element = "<error xmlns='jabber:client' type='cancel'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
#[cfg(feature = "component")]
|
||||||
|
let elem: Element = "<error xmlns='jabber:component:accept' type='cancel'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = StanzaError::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Error must have a defined-condition.");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,124 @@
|
||||||
|
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
use crate::message::MessagePayload;
|
||||||
|
use jid::Jid;
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// Gives the identifier a service has stamped on this stanza, often in
|
||||||
|
/// order to identify it inside of [an archive](../mam/index.html).
|
||||||
|
StanzaId, "stanza-id", SID,
|
||||||
|
attributes: [
|
||||||
|
/// The id associated to this stanza by another entity.
|
||||||
|
id: Required<String> = "id",
|
||||||
|
|
||||||
|
/// The entity who stamped this stanza-id.
|
||||||
|
by: Required<Jid> = "by",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
impl MessagePayload for StanzaId {}
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// A hack for MUC before version 1.31 to track a message which may have
|
||||||
|
/// its 'id' attribute changed.
|
||||||
|
OriginId, "origin-id", SID,
|
||||||
|
attributes: [
|
||||||
|
/// The id this client set for this stanza.
|
||||||
|
id: Required<String> = "id",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
impl MessagePayload for OriginId {}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::util::error::Error;
|
||||||
|
use crate::Element;
|
||||||
|
use jid::BareJid;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "32")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(StanzaId, 52);
|
||||||
|
assert_size!(OriginId, 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "64")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(StanzaId, 104);
|
||||||
|
assert_size!(OriginId, 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_simple() {
|
||||||
|
let elem: Element = "<stanza-id xmlns='urn:xmpp:sid:0' id='coucou' by='coucou@coucou'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let stanza_id = StanzaId::try_from(elem).unwrap();
|
||||||
|
assert_eq!(stanza_id.id, String::from("coucou"));
|
||||||
|
assert_eq!(stanza_id.by, BareJid::new("coucou", "coucou"));
|
||||||
|
|
||||||
|
let elem: Element = "<origin-id xmlns='urn:xmpp:sid:0' id='coucou'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let origin_id = OriginId::try_from(elem).unwrap();
|
||||||
|
assert_eq!(origin_id.id, String::from("coucou"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_child() {
|
||||||
|
let elem: Element = "<stanza-id xmlns='urn:xmpp:sid:0'><coucou/></stanza-id>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = StanzaId::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Unknown child in stanza-id element.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_id() {
|
||||||
|
let elem: Element = "<stanza-id xmlns='urn:xmpp:sid:0'/>".parse().unwrap();
|
||||||
|
let error = StanzaId::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Required attribute 'id' missing.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_by() {
|
||||||
|
let elem: Element = "<stanza-id xmlns='urn:xmpp:sid:0' id='coucou'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = StanzaId::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Required attribute 'by' missing.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialise() {
|
||||||
|
let elem: Element = "<stanza-id xmlns='urn:xmpp:sid:0' id='coucou' by='coucou@coucou'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let stanza_id = StanzaId {
|
||||||
|
id: String::from("coucou"),
|
||||||
|
by: Jid::Bare(BareJid::new("coucou", "coucou")),
|
||||||
|
};
|
||||||
|
let elem2 = stanza_id.into();
|
||||||
|
assert_eq!(elem, elem2);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,101 @@
|
||||||
|
// Copyright (c) 2018 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
use jid::BareJid;
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// The stream opening for client-server communications.
|
||||||
|
Stream, "stream", STREAM,
|
||||||
|
attributes: [
|
||||||
|
/// The JID of the entity opening this stream.
|
||||||
|
from: Option<BareJid> = "from",
|
||||||
|
|
||||||
|
/// The JID of the entity receiving this stream opening.
|
||||||
|
to: Option<BareJid> = "to",
|
||||||
|
|
||||||
|
/// The id of the stream, used for authentication challenges.
|
||||||
|
id: Option<String> = "id",
|
||||||
|
|
||||||
|
/// The XMPP version used during this stream.
|
||||||
|
version: Option<String> = "version",
|
||||||
|
|
||||||
|
/// The default human language for all subsequent stanzas, which will
|
||||||
|
/// be transmitted to other entities for better localisation.
|
||||||
|
xml_lang: Option<String> = "xml:lang",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
impl Stream {
|
||||||
|
/// Creates a simple client→server `<stream:stream>` element.
|
||||||
|
pub fn new(to: BareJid) -> Stream {
|
||||||
|
Stream {
|
||||||
|
from: None,
|
||||||
|
to: Some(to),
|
||||||
|
id: None,
|
||||||
|
version: Some(String::from("1.0")),
|
||||||
|
xml_lang: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the [@from](#structfield.from) attribute on this `<stream:stream>`
|
||||||
|
/// element.
|
||||||
|
pub fn with_from(mut self, from: BareJid) -> Stream {
|
||||||
|
self.from = Some(from);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the [@id](#structfield.id) attribute on this `<stream:stream>`
|
||||||
|
/// element.
|
||||||
|
pub fn with_id(mut self, id: String) -> Stream {
|
||||||
|
self.id = Some(id);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the [@xml:lang](#structfield.xml_lang) attribute on this
|
||||||
|
/// `<stream:stream>` element.
|
||||||
|
pub fn with_lang(mut self, xml_lang: String) -> Stream {
|
||||||
|
self.xml_lang = Some(xml_lang);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks whether the version matches the expected one.
|
||||||
|
pub fn is_version(&self, version: &str) -> bool {
|
||||||
|
match self.version {
|
||||||
|
None => false,
|
||||||
|
Some(ref self_version) => self_version == &String::from(version),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::Element;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "32")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(Stream, 84);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "64")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(Stream, 168);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_simple() {
|
||||||
|
let elem: Element = "<stream:stream xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' xml:lang='en' version='1.0' id='abc' from='some-server.example'/>".parse().unwrap();
|
||||||
|
let stream = Stream::try_from(elem).unwrap();
|
||||||
|
assert_eq!(stream.from, Some(BareJid::domain("some-server.example")));
|
||||||
|
assert_eq!(stream.to, None);
|
||||||
|
assert_eq!(stream.id, Some(String::from("abc")));
|
||||||
|
assert_eq!(stream.version, Some(String::from("1.0")));
|
||||||
|
assert_eq!(stream.xml_lang, Some(String::from("en")));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,115 @@
|
||||||
|
// 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::date::DateTime;
|
||||||
|
use crate::iq::{IqGetPayload, IqResultPayload};
|
||||||
|
use crate::ns;
|
||||||
|
use crate::util::error::Error;
|
||||||
|
use crate::Element;
|
||||||
|
use chrono::FixedOffset;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
generate_empty_element!(
|
||||||
|
/// An entity time query.
|
||||||
|
TimeQuery,
|
||||||
|
"time",
|
||||||
|
TIME
|
||||||
|
);
|
||||||
|
|
||||||
|
impl IqGetPayload for TimeQuery {}
|
||||||
|
|
||||||
|
/// An entity time result, containing an unique DateTime.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct TimeResult(pub DateTime);
|
||||||
|
|
||||||
|
impl IqResultPayload for TimeResult {}
|
||||||
|
|
||||||
|
impl TryFrom<Element> for TimeResult {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(elem: Element) -> Result<TimeResult, Error> {
|
||||||
|
check_self!(elem, "time", TIME);
|
||||||
|
check_no_attributes!(elem, "time");
|
||||||
|
|
||||||
|
let mut tzo = None;
|
||||||
|
let mut utc = None;
|
||||||
|
|
||||||
|
for child in elem.children() {
|
||||||
|
if child.is("tzo", ns::TIME) {
|
||||||
|
if tzo.is_some() {
|
||||||
|
return Err(Error::ParseError("More than one tzo element in time."));
|
||||||
|
}
|
||||||
|
check_no_children!(child, "tzo");
|
||||||
|
check_no_attributes!(child, "tzo");
|
||||||
|
// TODO: Add a FromStr implementation to FixedOffset to avoid this hack.
|
||||||
|
let fake_date = String::from("2019-04-22T11:38:00") + &child.text();
|
||||||
|
let date_time = DateTime::from_str(&fake_date)?;
|
||||||
|
tzo = Some(date_time.timezone());
|
||||||
|
} else if child.is("utc", ns::TIME) {
|
||||||
|
if utc.is_some() {
|
||||||
|
return Err(Error::ParseError("More than one utc element in time."));
|
||||||
|
}
|
||||||
|
check_no_children!(child, "utc");
|
||||||
|
check_no_attributes!(child, "utc");
|
||||||
|
let date_time = DateTime::from_str(&child.text())?;
|
||||||
|
if date_time.timezone() != FixedOffset::east(0) {
|
||||||
|
return Err(Error::ParseError("Non-UTC timezone for utc element."));
|
||||||
|
}
|
||||||
|
utc = Some(date_time);
|
||||||
|
} else {
|
||||||
|
return Err(Error::ParseError("Unknown child in time element."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let tzo = tzo.ok_or(Error::ParseError("Missing tzo child in time element."))?;
|
||||||
|
let utc = utc.ok_or(Error::ParseError("Missing utc child in time element."))?;
|
||||||
|
let date = utc.with_timezone(tzo);
|
||||||
|
|
||||||
|
Ok(TimeResult(date))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TimeResult> for Element {
|
||||||
|
fn from(time: TimeResult) -> Element {
|
||||||
|
Element::builder("time", ns::TIME)
|
||||||
|
.append(Element::builder("tzo", ns::TIME).append(format!("{}", time.0.timezone())))
|
||||||
|
.append(
|
||||||
|
Element::builder("utc", ns::TIME)
|
||||||
|
.append(time.0.with_timezone(FixedOffset::east(0)).format("%FT%TZ")),
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
// DateTime’s size doesn’t depend on the architecture.
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(TimeQuery, 0);
|
||||||
|
assert_size!(TimeResult, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_response() {
|
||||||
|
let elem: Element =
|
||||||
|
"<time xmlns='urn:xmpp:time'><tzo>-06:00</tzo><utc>2006-12-19T17:58:35Z</utc></time>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let elem1 = elem.clone();
|
||||||
|
let time = TimeResult::try_from(elem).unwrap();
|
||||||
|
assert_eq!(time.0.timezone(), FixedOffset::west(6 * 3600));
|
||||||
|
assert_eq!(
|
||||||
|
time.0,
|
||||||
|
DateTime::from_str("2006-12-19T12:58:35-05:00").unwrap()
|
||||||
|
);
|
||||||
|
let elem2 = Element::from(time);
|
||||||
|
assert_eq!(elem1, elem2);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,246 @@
|
||||||
|
// 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::ns;
|
||||||
|
use crate::pubsub::PubSubPayload;
|
||||||
|
use crate::util::error::Error;
|
||||||
|
use crate::Element;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
generate_elem_id!(
|
||||||
|
/// The artist or performer of the song or piece.
|
||||||
|
Artist,
|
||||||
|
"artist",
|
||||||
|
TUNE
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_elem_id!(
|
||||||
|
/// The duration of the song or piece in seconds.
|
||||||
|
Length,
|
||||||
|
"length",
|
||||||
|
TUNE,
|
||||||
|
u16
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_elem_id!(
|
||||||
|
/// The user's rating of the song or piece, from 1 (lowest) to 10 (highest).
|
||||||
|
Rating,
|
||||||
|
"rating",
|
||||||
|
TUNE,
|
||||||
|
u8
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_elem_id!(
|
||||||
|
/// The collection (e.g., album) or other source (e.g., a band website that hosts streams or
|
||||||
|
/// audio files).
|
||||||
|
Source,
|
||||||
|
"source",
|
||||||
|
TUNE
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_elem_id!(
|
||||||
|
/// The title of the song or piece.
|
||||||
|
Title,
|
||||||
|
"title",
|
||||||
|
TUNE
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_elem_id!(
|
||||||
|
/// A unique identifier for the tune; e.g., the track number within a collection or the
|
||||||
|
/// specific URI for the object (e.g., a stream or audio file).
|
||||||
|
Track,
|
||||||
|
"track",
|
||||||
|
TUNE
|
||||||
|
);
|
||||||
|
|
||||||
|
generate_elem_id!(
|
||||||
|
/// A URI or URL pointing to information about the song, collection, or artist.
|
||||||
|
Uri,
|
||||||
|
"uri",
|
||||||
|
TUNE
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Container for formatted text.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Tune {
|
||||||
|
/// The artist or performer of the song or piece.
|
||||||
|
artist: Option<Artist>,
|
||||||
|
|
||||||
|
/// The duration of the song or piece in seconds.
|
||||||
|
length: Option<Length>,
|
||||||
|
|
||||||
|
/// The user's rating of the song or piece, from 1 (lowest) to 10 (highest).
|
||||||
|
rating: Option<Rating>,
|
||||||
|
|
||||||
|
/// The collection (e.g., album) or other source (e.g., a band website that hosts streams or
|
||||||
|
/// audio files).
|
||||||
|
source: Option<Source>,
|
||||||
|
|
||||||
|
/// The title of the song or piece.
|
||||||
|
title: Option<Title>,
|
||||||
|
|
||||||
|
/// A unique identifier for the tune; e.g., the track number within a collection or the
|
||||||
|
/// specific URI for the object (e.g., a stream or audio file).
|
||||||
|
track: Option<Track>,
|
||||||
|
|
||||||
|
/// A URI or URL pointing to information about the song, collection, or artist.
|
||||||
|
uri: Option<Uri>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PubSubPayload for Tune {}
|
||||||
|
|
||||||
|
impl Tune {
|
||||||
|
fn new() -> Tune {
|
||||||
|
Tune {
|
||||||
|
artist: None,
|
||||||
|
length: None,
|
||||||
|
rating: None,
|
||||||
|
source: None,
|
||||||
|
title: None,
|
||||||
|
track: None,
|
||||||
|
uri: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<Element> for Tune {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(elem: Element) -> Result<Tune, Error> {
|
||||||
|
check_self!(elem, "tune", TUNE);
|
||||||
|
check_no_attributes!(elem, "tune");
|
||||||
|
|
||||||
|
let mut tune = Tune::new();
|
||||||
|
for child in elem.children() {
|
||||||
|
if child.is("artist", ns::TUNE) {
|
||||||
|
if tune.artist.is_some() {
|
||||||
|
return Err(Error::ParseError("Tune can’t have more than one artist."));
|
||||||
|
}
|
||||||
|
tune.artist = Some(Artist::try_from(child.clone())?);
|
||||||
|
} else if child.is("length", ns::TUNE) {
|
||||||
|
if tune.length.is_some() {
|
||||||
|
return Err(Error::ParseError("Tune can’t have more than one length."));
|
||||||
|
}
|
||||||
|
tune.length = Some(Length::try_from(child.clone())?);
|
||||||
|
} else if child.is("rating", ns::TUNE) {
|
||||||
|
if tune.rating.is_some() {
|
||||||
|
return Err(Error::ParseError("Tune can’t have more than one rating."));
|
||||||
|
}
|
||||||
|
tune.rating = Some(Rating::try_from(child.clone())?);
|
||||||
|
} else if child.is("source", ns::TUNE) {
|
||||||
|
if tune.source.is_some() {
|
||||||
|
return Err(Error::ParseError("Tune can’t have more than one source."));
|
||||||
|
}
|
||||||
|
tune.source = Some(Source::try_from(child.clone())?);
|
||||||
|
} else if child.is("title", ns::TUNE) {
|
||||||
|
if tune.title.is_some() {
|
||||||
|
return Err(Error::ParseError("Tune can’t have more than one title."));
|
||||||
|
}
|
||||||
|
tune.title = Some(Title::try_from(child.clone())?);
|
||||||
|
} else if child.is("track", ns::TUNE) {
|
||||||
|
if tune.track.is_some() {
|
||||||
|
return Err(Error::ParseError("Tune can’t have more than one track."));
|
||||||
|
}
|
||||||
|
tune.track = Some(Track::try_from(child.clone())?);
|
||||||
|
} else if child.is("uri", ns::TUNE) {
|
||||||
|
if tune.uri.is_some() {
|
||||||
|
return Err(Error::ParseError("Tune can’t have more than one uri."));
|
||||||
|
}
|
||||||
|
tune.uri = Some(Uri::try_from(child.clone())?);
|
||||||
|
} else {
|
||||||
|
return Err(Error::ParseError("Unknown element in User Tune."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(tune)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Tune> for Element {
|
||||||
|
fn from(tune: Tune) -> Element {
|
||||||
|
Element::builder("tune", ns::TUNE)
|
||||||
|
.append_all(tune.artist)
|
||||||
|
.append_all(tune.length)
|
||||||
|
.append_all(tune.rating)
|
||||||
|
.append_all(tune.source)
|
||||||
|
.append_all(tune.title)
|
||||||
|
.append_all(tune.track)
|
||||||
|
.append_all(tune.uri)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "32")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(Tune, 68);
|
||||||
|
assert_size!(Artist, 12);
|
||||||
|
assert_size!(Length, 2);
|
||||||
|
assert_size!(Rating, 1);
|
||||||
|
assert_size!(Source, 12);
|
||||||
|
assert_size!(Title, 12);
|
||||||
|
assert_size!(Track, 12);
|
||||||
|
assert_size!(Uri, 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "64")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(Tune, 128);
|
||||||
|
assert_size!(Artist, 24);
|
||||||
|
assert_size!(Length, 2);
|
||||||
|
assert_size!(Rating, 1);
|
||||||
|
assert_size!(Source, 24);
|
||||||
|
assert_size!(Title, 24);
|
||||||
|
assert_size!(Track, 24);
|
||||||
|
assert_size!(Uri, 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty() {
|
||||||
|
let elem: Element = "<tune xmlns='http://jabber.org/protocol/tune'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let elem2 = elem.clone();
|
||||||
|
let tune = Tune::try_from(elem).unwrap();
|
||||||
|
assert!(tune.artist.is_none());
|
||||||
|
assert!(tune.length.is_none());
|
||||||
|
assert!(tune.rating.is_none());
|
||||||
|
assert!(tune.source.is_none());
|
||||||
|
assert!(tune.title.is_none());
|
||||||
|
assert!(tune.track.is_none());
|
||||||
|
assert!(tune.uri.is_none());
|
||||||
|
|
||||||
|
let elem3 = tune.into();
|
||||||
|
assert_eq!(elem2, elem3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn full() {
|
||||||
|
let elem: Element = "<tune xmlns='http://jabber.org/protocol/tune'><artist>Yes</artist><length>686</length><rating>8</rating><source>Yessongs</source><title>Heart of the Sunrise</title><track>3</track><uri>http://www.yesworld.com/lyrics/Fragile.html#9</uri></tune>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let tune = Tune::try_from(elem).unwrap();
|
||||||
|
assert_eq!(tune.artist, Some(Artist::from_str("Yes").unwrap()));
|
||||||
|
assert_eq!(tune.length, Some(Length(686)));
|
||||||
|
assert_eq!(tune.rating, Some(Rating(8)));
|
||||||
|
assert_eq!(tune.source, Some(Source::from_str("Yessongs").unwrap()));
|
||||||
|
assert_eq!(
|
||||||
|
tune.title,
|
||||||
|
Some(Title::from_str("Heart of the Sunrise").unwrap())
|
||||||
|
);
|
||||||
|
assert_eq!(tune.track, Some(Track::from_str("3").unwrap()));
|
||||||
|
assert_eq!(
|
||||||
|
tune.uri,
|
||||||
|
Some(Uri::from_str("http://www.yesworld.com/lyrics/Fragile.html#9").unwrap())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,105 @@
|
||||||
|
// Copyright (c) 2017-2018 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
use std::error::Error as StdError;
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
/// Contains one of the potential errors triggered while parsing an
|
||||||
|
/// [Element](../struct.Element.html) into a specialised struct.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
/// The usual error when parsing something.
|
||||||
|
///
|
||||||
|
/// TODO: use a structured error so the user can report it better, instead
|
||||||
|
/// of a freeform string.
|
||||||
|
ParseError(&'static str),
|
||||||
|
|
||||||
|
/// Generated when some base64 content fails to decode, usually due to
|
||||||
|
/// extra characters.
|
||||||
|
Base64Error(base64::DecodeError),
|
||||||
|
|
||||||
|
/// Generated when text which should be an integer fails to parse.
|
||||||
|
ParseIntError(std::num::ParseIntError),
|
||||||
|
|
||||||
|
/// Generated when text which should be a string fails to parse.
|
||||||
|
ParseStringError(std::string::ParseError),
|
||||||
|
|
||||||
|
/// Generated when text which should be an IP address (IPv4 or IPv6) fails
|
||||||
|
/// to parse.
|
||||||
|
ParseAddrError(std::net::AddrParseError),
|
||||||
|
|
||||||
|
/// Generated when text which should be a [JID](../../jid/struct.Jid.html)
|
||||||
|
/// fails to parse.
|
||||||
|
JidParseError(jid::JidParseError),
|
||||||
|
|
||||||
|
/// Generated when text which should be a
|
||||||
|
/// [DateTime](../date/struct.DateTime.html) fails to parse.
|
||||||
|
ChronoParseError(chrono::ParseError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StdError for Error {
|
||||||
|
fn cause(&self) -> Option<&dyn StdError> {
|
||||||
|
match self {
|
||||||
|
Error::ParseError(_) => None,
|
||||||
|
Error::Base64Error(e) => Some(e),
|
||||||
|
Error::ParseIntError(e) => Some(e),
|
||||||
|
Error::ParseStringError(e) => Some(e),
|
||||||
|
Error::ParseAddrError(e) => Some(e),
|
||||||
|
Error::JidParseError(e) => Some(e),
|
||||||
|
Error::ChronoParseError(e) => Some(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Error {
|
||||||
|
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Error::ParseError(s) => write!(fmt, "parse error: {}", s),
|
||||||
|
Error::Base64Error(e) => write!(fmt, "base64 error: {}", e),
|
||||||
|
Error::ParseIntError(e) => write!(fmt, "integer parsing error: {}", e),
|
||||||
|
Error::ParseStringError(e) => write!(fmt, "string parsing error: {}", e),
|
||||||
|
Error::ParseAddrError(e) => write!(fmt, "IP address parsing error: {}", e),
|
||||||
|
Error::JidParseError(e) => write!(fmt, "JID parsing error: {}", e),
|
||||||
|
Error::ChronoParseError(e) => write!(fmt, "time parsing error: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<base64::DecodeError> for Error {
|
||||||
|
fn from(err: base64::DecodeError) -> Error {
|
||||||
|
Error::Base64Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::num::ParseIntError> for Error {
|
||||||
|
fn from(err: std::num::ParseIntError) -> Error {
|
||||||
|
Error::ParseIntError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::string::ParseError> for Error {
|
||||||
|
fn from(err: std::string::ParseError) -> Error {
|
||||||
|
Error::ParseStringError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::net::AddrParseError> for Error {
|
||||||
|
fn from(err: std::net::AddrParseError) -> Error {
|
||||||
|
Error::ParseAddrError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<jid::JidParseError> for Error {
|
||||||
|
fn from(err: jid::JidParseError) -> Error {
|
||||||
|
Error::JidParseError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<chrono::ParseError> for Error {
|
||||||
|
fn from(err: chrono::ParseError) -> Error {
|
||||||
|
Error::ChronoParseError(err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,122 @@
|
||||||
|
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
use crate::util::error::Error;
|
||||||
|
use jid::Jid;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
/// Codec for text content.
|
||||||
|
pub struct Text;
|
||||||
|
|
||||||
|
impl Text {
|
||||||
|
pub fn decode(s: &str) -> Result<String, Error> {
|
||||||
|
Ok(s.to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encode(string: &str) -> Option<String> {
|
||||||
|
Some(string.to_owned())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Codec for plain text content.
|
||||||
|
pub struct PlainText;
|
||||||
|
|
||||||
|
impl PlainText {
|
||||||
|
pub fn decode(s: &str) -> Result<Option<String>, Error> {
|
||||||
|
Ok(match s {
|
||||||
|
"" => None,
|
||||||
|
text => Some(text.to_owned()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encode(string: &Option<String>) -> Option<String> {
|
||||||
|
string.as_ref().map(ToOwned::to_owned)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Codec for trimmed plain text content.
|
||||||
|
pub struct TrimmedPlainText;
|
||||||
|
|
||||||
|
impl TrimmedPlainText {
|
||||||
|
pub fn decode(s: &str) -> Result<String, Error> {
|
||||||
|
Ok(match s.trim() {
|
||||||
|
"" => return Err(Error::ParseError("URI missing in uri.")),
|
||||||
|
text => text.to_owned(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encode(string: &str) -> Option<String> {
|
||||||
|
Some(string.to_owned())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Codec wrapping base64 encode/decode.
|
||||||
|
pub struct Base64;
|
||||||
|
|
||||||
|
impl Base64 {
|
||||||
|
pub fn decode(s: &str) -> Result<Vec<u8>, Error> {
|
||||||
|
Ok(base64::decode(s)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encode(b: &[u8]) -> Option<String> {
|
||||||
|
Some(base64::encode(b))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Codec wrapping base64 encode/decode, while ignoring whitespace characters.
|
||||||
|
pub struct WhitespaceAwareBase64;
|
||||||
|
|
||||||
|
impl WhitespaceAwareBase64 {
|
||||||
|
pub fn decode(s: &str) -> Result<Vec<u8>, Error> {
|
||||||
|
let s: String = s
|
||||||
|
.chars()
|
||||||
|
.filter(|ch| *ch != ' ' && *ch != '\n' && *ch != '\t')
|
||||||
|
.collect();
|
||||||
|
Ok(base64::decode(&s)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encode(b: &[u8]) -> Option<String> {
|
||||||
|
Some(base64::encode(b))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Codec for colon-separated bytes of uppercase hexadecimal.
|
||||||
|
pub struct ColonSeparatedHex;
|
||||||
|
|
||||||
|
impl ColonSeparatedHex {
|
||||||
|
pub fn decode(s: &str) -> Result<Vec<u8>, Error> {
|
||||||
|
let mut bytes = vec![];
|
||||||
|
for i in 0..(1 + s.len()) / 3 {
|
||||||
|
let byte = u8::from_str_radix(&s[3 * i..3 * i + 2], 16)?;
|
||||||
|
if 3 * i + 2 < s.len() {
|
||||||
|
assert_eq!(&s[3 * i + 2..3 * i + 3], ":");
|
||||||
|
}
|
||||||
|
bytes.push(byte);
|
||||||
|
}
|
||||||
|
Ok(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encode(b: &[u8]) -> Option<String> {
|
||||||
|
let mut bytes = vec![];
|
||||||
|
for byte in b {
|
||||||
|
bytes.push(format!("{:02X}", byte));
|
||||||
|
}
|
||||||
|
Some(bytes.join(":"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Codec for a JID.
|
||||||
|
pub struct JidCodec;
|
||||||
|
|
||||||
|
impl JidCodec {
|
||||||
|
pub fn decode(s: &str) -> Result<Jid, Error> {
|
||||||
|
Ok(Jid::from_str(s)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encode(jid: &Jid) -> Option<String> {
|
||||||
|
Some(jid.to_string())
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,767 @@
|
||||||
|
// Copyright (c) 2017-2018 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
macro_rules! get_attr {
|
||||||
|
($elem:ident, $attr:tt, $type:tt) => {
|
||||||
|
get_attr!($elem, $attr, $type, value, value.parse()?)
|
||||||
|
};
|
||||||
|
($elem:ident, $attr:tt, OptionEmpty, $value:ident, $func:expr) => {
|
||||||
|
match $elem.attr($attr) {
|
||||||
|
Some("") => None,
|
||||||
|
Some($value) => Some($func),
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
($elem:ident, $attr:tt, Option, $value:ident, $func:expr) => {
|
||||||
|
match $elem.attr($attr) {
|
||||||
|
Some($value) => Some($func),
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
($elem:ident, $attr:tt, Required, $value:ident, $func:expr) => {
|
||||||
|
match $elem.attr($attr) {
|
||||||
|
Some($value) => $func,
|
||||||
|
None => {
|
||||||
|
return Err(crate::util::error::Error::ParseError(concat!(
|
||||||
|
"Required attribute '",
|
||||||
|
$attr,
|
||||||
|
"' missing."
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
($elem:ident, $attr:tt, RequiredNonEmpty, $value:ident, $func:expr) => {
|
||||||
|
match $elem.attr($attr) {
|
||||||
|
Some("") => {
|
||||||
|
return Err(crate::util::error::Error::ParseError(concat!(
|
||||||
|
"Required attribute '",
|
||||||
|
$attr,
|
||||||
|
"' must not be empty."
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
Some($value) => $func,
|
||||||
|
None => {
|
||||||
|
return Err(crate::util::error::Error::ParseError(concat!(
|
||||||
|
"Required attribute '",
|
||||||
|
$attr,
|
||||||
|
"' missing."
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
($elem:ident, $attr:tt, Default, $value:ident, $func:expr) => {
|
||||||
|
match $elem.attr($attr) {
|
||||||
|
Some($value) => $func,
|
||||||
|
None => ::std::default::Default::default(),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! generate_attribute {
|
||||||
|
($(#[$meta:meta])* $elem:ident, $name:tt, {$($(#[$a_meta:meta])* $a:ident => $b:tt),+,}) => (
|
||||||
|
generate_attribute!($(#[$meta])* $elem, $name, {$($(#[$a_meta])* $a => $b),+});
|
||||||
|
);
|
||||||
|
($(#[$meta:meta])* $elem:ident, $name:tt, {$($(#[$a_meta:meta])* $a:ident => $b:tt),+,}, Default = $default:ident) => (
|
||||||
|
generate_attribute!($(#[$meta])* $elem, $name, {$($(#[$a_meta])* $a => $b),+}, Default = $default);
|
||||||
|
);
|
||||||
|
($(#[$meta:meta])* $elem:ident, $name:tt, {$($(#[$a_meta:meta])* $a:ident => $b:tt),+}) => (
|
||||||
|
$(#[$meta])*
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum $elem {
|
||||||
|
$(
|
||||||
|
$(#[$a_meta])*
|
||||||
|
$a
|
||||||
|
),+
|
||||||
|
}
|
||||||
|
impl ::std::str::FromStr for $elem {
|
||||||
|
type Err = crate::util::error::Error;
|
||||||
|
fn from_str(s: &str) -> Result<$elem, crate::util::error::Error> {
|
||||||
|
Ok(match s {
|
||||||
|
$($b => $elem::$a),+,
|
||||||
|
_ => return Err(crate::util::error::Error::ParseError(concat!("Unknown value for '", $name, "' attribute."))),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::fmt::Display for $elem {
|
||||||
|
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
|
||||||
|
write!(fmt, "{}", match self {
|
||||||
|
$($elem::$a => $b),+
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl ::minidom::IntoAttributeValue for $elem {
|
||||||
|
fn into_attribute_value(self) -> Option<String> {
|
||||||
|
Some(String::from(match self {
|
||||||
|
$($elem::$a => $b),+
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
($(#[$meta:meta])* $elem:ident, $name:tt, {$($(#[$a_meta:meta])* $a:ident => $b:tt),+}, Default = $default:ident) => (
|
||||||
|
$(#[$meta])*
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum $elem {
|
||||||
|
$(
|
||||||
|
$(#[$a_meta])*
|
||||||
|
$a
|
||||||
|
),+
|
||||||
|
}
|
||||||
|
impl ::std::str::FromStr for $elem {
|
||||||
|
type Err = crate::util::error::Error;
|
||||||
|
fn from_str(s: &str) -> Result<$elem, crate::util::error::Error> {
|
||||||
|
Ok(match s {
|
||||||
|
$($b => $elem::$a),+,
|
||||||
|
_ => return Err(crate::util::error::Error::ParseError(concat!("Unknown value for '", $name, "' attribute."))),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl ::minidom::IntoAttributeValue for $elem {
|
||||||
|
#[allow(unreachable_patterns)]
|
||||||
|
fn into_attribute_value(self) -> Option<String> {
|
||||||
|
Some(String::from(match self {
|
||||||
|
$elem::$default => return None,
|
||||||
|
$($elem::$a => $b),+
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl ::std::default::Default for $elem {
|
||||||
|
fn default() -> $elem {
|
||||||
|
$elem::$default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
($(#[$meta:meta])* $elem:ident, $name:tt, ($(#[$meta_symbol:meta])* $symbol:ident => $value:tt)) => (
|
||||||
|
$(#[$meta])*
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum $elem {
|
||||||
|
$(#[$meta_symbol])*
|
||||||
|
$symbol,
|
||||||
|
/// Value when absent.
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
impl ::std::str::FromStr for $elem {
|
||||||
|
type Err = crate::util::error::Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self, crate::util::error::Error> {
|
||||||
|
Ok(match s {
|
||||||
|
$value => $elem::$symbol,
|
||||||
|
_ => return Err(crate::util::error::Error::ParseError(concat!("Unknown value for '", $name, "' attribute."))),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl ::minidom::IntoAttributeValue for $elem {
|
||||||
|
fn into_attribute_value(self) -> Option<String> {
|
||||||
|
match self {
|
||||||
|
$elem::$symbol => Some(String::from($value)),
|
||||||
|
$elem::None => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl ::std::default::Default for $elem {
|
||||||
|
fn default() -> $elem {
|
||||||
|
$elem::None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
($(#[$meta:meta])* $elem:ident, $name:tt, bool) => (
|
||||||
|
$(#[$meta])*
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum $elem {
|
||||||
|
/// True value, represented by either 'true' or '1'.
|
||||||
|
True,
|
||||||
|
/// False value, represented by either 'false' or '0'.
|
||||||
|
False,
|
||||||
|
}
|
||||||
|
impl ::std::str::FromStr for $elem {
|
||||||
|
type Err = crate::util::error::Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self, crate::util::error::Error> {
|
||||||
|
Ok(match s {
|
||||||
|
"true" | "1" => $elem::True,
|
||||||
|
"false" | "0" => $elem::False,
|
||||||
|
_ => return Err(crate::util::error::Error::ParseError(concat!("Unknown value for '", $name, "' attribute."))),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl ::minidom::IntoAttributeValue for $elem {
|
||||||
|
fn into_attribute_value(self) -> Option<String> {
|
||||||
|
match self {
|
||||||
|
$elem::True => Some(String::from("true")),
|
||||||
|
$elem::False => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl ::std::default::Default for $elem {
|
||||||
|
fn default() -> $elem {
|
||||||
|
$elem::False
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
($(#[$meta:meta])* $elem:ident, $name:tt, $type:tt, Default = $default:expr) => (
|
||||||
|
$(#[$meta])*
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct $elem(pub $type);
|
||||||
|
impl ::std::str::FromStr for $elem {
|
||||||
|
type Err = crate::util::error::Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self, crate::util::error::Error> {
|
||||||
|
Ok($elem($type::from_str(s)?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl ::minidom::IntoAttributeValue for $elem {
|
||||||
|
fn into_attribute_value(self) -> Option<String> {
|
||||||
|
match self {
|
||||||
|
$elem($default) => None,
|
||||||
|
$elem(value) => Some(format!("{}", value)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl ::std::default::Default for $elem {
|
||||||
|
fn default() -> $elem {
|
||||||
|
$elem($default)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! generate_element_enum {
|
||||||
|
($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, {$($(#[$enum_meta:meta])* $enum:ident => $enum_name:tt),+,}) => (
|
||||||
|
generate_element_enum!($(#[$meta])* $elem, $name, $ns, {$($(#[$enum_meta])* $enum => $enum_name),+});
|
||||||
|
);
|
||||||
|
($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, {$($(#[$enum_meta:meta])* $enum:ident => $enum_name:tt),+}) => (
|
||||||
|
$(#[$meta])*
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum $elem {
|
||||||
|
$(
|
||||||
|
$(#[$enum_meta])*
|
||||||
|
$enum
|
||||||
|
),+
|
||||||
|
}
|
||||||
|
impl ::std::convert::TryFrom<crate::Element> for $elem {
|
||||||
|
type Error = crate::util::error::Error;
|
||||||
|
fn try_from(elem: crate::Element) -> Result<$elem, crate::util::error::Error> {
|
||||||
|
check_ns_only!(elem, $name, $ns);
|
||||||
|
check_no_children!(elem, $name);
|
||||||
|
check_no_attributes!(elem, $name);
|
||||||
|
Ok(match elem.name() {
|
||||||
|
$($enum_name => $elem::$enum,)+
|
||||||
|
_ => return Err(crate::util::error::Error::ParseError(concat!("This is not a ", $name, " element."))),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<$elem> for crate::Element {
|
||||||
|
fn from(elem: $elem) -> crate::Element {
|
||||||
|
crate::Element::builder(
|
||||||
|
match elem {
|
||||||
|
$($elem::$enum => $enum_name,)+
|
||||||
|
},
|
||||||
|
crate::ns::$ns,
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! generate_attribute_enum {
|
||||||
|
($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, $attr:tt, {$($(#[$enum_meta:meta])* $enum:ident => $enum_name:tt),+,}) => (
|
||||||
|
generate_attribute_enum!($(#[$meta])* $elem, $name, $ns, $attr, {$($(#[$enum_meta])* $enum => $enum_name),+});
|
||||||
|
);
|
||||||
|
($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, $attr:tt, {$($(#[$enum_meta:meta])* $enum:ident => $enum_name:tt),+}) => (
|
||||||
|
$(#[$meta])*
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum $elem {
|
||||||
|
$(
|
||||||
|
$(#[$enum_meta])*
|
||||||
|
$enum
|
||||||
|
),+
|
||||||
|
}
|
||||||
|
impl ::std::convert::TryFrom<crate::Element> for $elem {
|
||||||
|
type Error = crate::util::error::Error;
|
||||||
|
fn try_from(elem: crate::Element) -> Result<$elem, crate::util::error::Error> {
|
||||||
|
check_ns_only!(elem, $name, $ns);
|
||||||
|
check_no_children!(elem, $name);
|
||||||
|
check_no_unknown_attributes!(elem, $name, [$attr]);
|
||||||
|
Ok(match get_attr!(elem, $attr, Required) {
|
||||||
|
$($enum_name => $elem::$enum,)+
|
||||||
|
_ => return Err(crate::util::error::Error::ParseError(concat!("Invalid ", $name, " ", $attr, " value."))),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<$elem> for crate::Element {
|
||||||
|
fn from(elem: $elem) -> crate::Element {
|
||||||
|
crate::Element::builder($name, crate::ns::$ns)
|
||||||
|
.attr($attr, match elem {
|
||||||
|
$($elem::$enum => $enum_name,)+
|
||||||
|
})
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! check_self {
|
||||||
|
($elem:ident, $name:tt, $ns:ident) => {
|
||||||
|
check_self!($elem, $name, $ns, $name);
|
||||||
|
};
|
||||||
|
($elem:ident, $name:tt, $ns:ident, $pretty_name:tt) => {
|
||||||
|
if !$elem.is($name, crate::ns::$ns) {
|
||||||
|
return Err(crate::util::error::Error::ParseError(concat!(
|
||||||
|
"This is not a ",
|
||||||
|
$pretty_name,
|
||||||
|
" element."
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! check_ns_only {
|
||||||
|
($elem:ident, $name:tt, $ns:ident) => {
|
||||||
|
if !$elem.has_ns(crate::ns::$ns) {
|
||||||
|
return Err(crate::util::error::Error::ParseError(concat!(
|
||||||
|
"This is not a ",
|
||||||
|
$name,
|
||||||
|
" element."
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! check_no_children {
|
||||||
|
($elem:ident, $name:tt) => {
|
||||||
|
#[cfg(not(feature = "disable-validation"))]
|
||||||
|
for _ in $elem.children() {
|
||||||
|
return Err(crate::util::error::Error::ParseError(concat!(
|
||||||
|
"Unknown child in ",
|
||||||
|
$name,
|
||||||
|
" element."
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! check_no_attributes {
|
||||||
|
($elem:ident, $name:tt) => {
|
||||||
|
#[cfg(not(feature = "disable-validation"))]
|
||||||
|
for _ in $elem.attrs() {
|
||||||
|
return Err(crate::util::error::Error::ParseError(concat!(
|
||||||
|
"Unknown attribute in ",
|
||||||
|
$name,
|
||||||
|
" element."
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! check_no_unknown_attributes {
|
||||||
|
($elem:ident, $name:tt, [$($attr:tt),*]) => (
|
||||||
|
#[cfg(not(feature = "disable-validation"))]
|
||||||
|
for (_attr, _) in $elem.attrs() {
|
||||||
|
$(
|
||||||
|
if _attr == $attr {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
return Err(crate::util::error::Error::ParseError(concat!("Unknown attribute in ", $name, " element.")));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! generate_empty_element {
|
||||||
|
($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident) => (
|
||||||
|
$(#[$meta])*
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct $elem;
|
||||||
|
|
||||||
|
impl ::std::convert::TryFrom<crate::Element> for $elem {
|
||||||
|
type Error = crate::util::error::Error;
|
||||||
|
|
||||||
|
fn try_from(elem: crate::Element) -> Result<$elem, crate::util::error::Error> {
|
||||||
|
check_self!(elem, $name, $ns);
|
||||||
|
check_no_children!(elem, $name);
|
||||||
|
check_no_attributes!(elem, $name);
|
||||||
|
Ok($elem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<$elem> for crate::Element {
|
||||||
|
fn from(_: $elem) -> crate::Element {
|
||||||
|
crate::Element::builder($name, crate::ns::$ns)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! generate_id {
|
||||||
|
($(#[$meta:meta])* $elem:ident) => (
|
||||||
|
$(#[$meta])*
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub struct $elem(pub String);
|
||||||
|
impl ::std::str::FromStr for $elem {
|
||||||
|
type Err = crate::util::error::Error;
|
||||||
|
fn from_str(s: &str) -> Result<$elem, crate::util::error::Error> {
|
||||||
|
// TODO: add a way to parse that differently when needed.
|
||||||
|
Ok($elem(String::from(s)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl ::minidom::IntoAttributeValue for $elem {
|
||||||
|
fn into_attribute_value(self) -> Option<String> {
|
||||||
|
Some(self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! generate_elem_id {
|
||||||
|
($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident) => (
|
||||||
|
generate_elem_id!($(#[$meta])* $elem, $name, $ns, String);
|
||||||
|
impl ::std::str::FromStr for $elem {
|
||||||
|
type Err = crate::util::error::Error;
|
||||||
|
fn from_str(s: &str) -> Result<$elem, crate::util::error::Error> {
|
||||||
|
// TODO: add a way to parse that differently when needed.
|
||||||
|
Ok($elem(String::from(s)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, $type:ty) => (
|
||||||
|
$(#[$meta])*
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub struct $elem(pub $type);
|
||||||
|
impl ::std::convert::TryFrom<crate::Element> for $elem {
|
||||||
|
type Error = crate::util::error::Error;
|
||||||
|
fn try_from(elem: crate::Element) -> Result<$elem, crate::util::error::Error> {
|
||||||
|
check_self!(elem, $name, $ns);
|
||||||
|
check_no_children!(elem, $name);
|
||||||
|
check_no_attributes!(elem, $name);
|
||||||
|
// TODO: add a way to parse that differently when needed.
|
||||||
|
Ok($elem(elem.text().parse()?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<$elem> for crate::Element {
|
||||||
|
fn from(elem: $elem) -> crate::Element {
|
||||||
|
crate::Element::builder($name, crate::ns::$ns)
|
||||||
|
.append(elem.0.to_string())
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! decl_attr {
|
||||||
|
(OptionEmpty, $type:ty) => (
|
||||||
|
Option<$type>
|
||||||
|
);
|
||||||
|
(Option, $type:ty) => (
|
||||||
|
Option<$type>
|
||||||
|
);
|
||||||
|
(Required, $type:ty) => (
|
||||||
|
$type
|
||||||
|
);
|
||||||
|
(RequiredNonEmpty, $type:ty) => (
|
||||||
|
$type
|
||||||
|
);
|
||||||
|
(Default, $type:ty) => (
|
||||||
|
$type
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! start_decl {
|
||||||
|
(Vec, $type:ty) => (
|
||||||
|
Vec<$type>
|
||||||
|
);
|
||||||
|
(Option, $type:ty) => (
|
||||||
|
Option<$type>
|
||||||
|
);
|
||||||
|
(Required, $type:ty) => (
|
||||||
|
$type
|
||||||
|
);
|
||||||
|
(Present, $type:ty) => (
|
||||||
|
bool
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! start_parse_elem {
|
||||||
|
($temp:ident: Vec) => {
|
||||||
|
let mut $temp = Vec::new();
|
||||||
|
};
|
||||||
|
($temp:ident: Option) => {
|
||||||
|
let mut $temp = None;
|
||||||
|
};
|
||||||
|
($temp:ident: Required) => {
|
||||||
|
let mut $temp = None;
|
||||||
|
};
|
||||||
|
($temp:ident: Present) => {
|
||||||
|
let mut $temp = false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! do_parse {
|
||||||
|
($elem:ident, Element) => {
|
||||||
|
$elem.clone()
|
||||||
|
};
|
||||||
|
($elem:ident, String) => {
|
||||||
|
$elem.text()
|
||||||
|
};
|
||||||
|
($elem:ident, $constructor:ident) => {
|
||||||
|
$constructor::try_from($elem.clone())?
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! do_parse_elem {
|
||||||
|
($temp:ident: Vec = $constructor:ident => $elem:ident, $name:tt, $parent_name:tt) => {
|
||||||
|
$temp.push(do_parse!($elem, $constructor));
|
||||||
|
};
|
||||||
|
($temp:ident: Option = $constructor:ident => $elem:ident, $name:tt, $parent_name:tt) => {
|
||||||
|
if $temp.is_some() {
|
||||||
|
return Err(crate::util::error::Error::ParseError(concat!(
|
||||||
|
"Element ",
|
||||||
|
$parent_name,
|
||||||
|
" must not have more than one ",
|
||||||
|
$name,
|
||||||
|
" child."
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
$temp = Some(do_parse!($elem, $constructor));
|
||||||
|
};
|
||||||
|
($temp:ident: Required = $constructor:ident => $elem:ident, $name:tt, $parent_name:tt) => {
|
||||||
|
if $temp.is_some() {
|
||||||
|
return Err(crate::util::error::Error::ParseError(concat!(
|
||||||
|
"Element ",
|
||||||
|
$parent_name,
|
||||||
|
" must not have more than one ",
|
||||||
|
$name,
|
||||||
|
" child."
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
$temp = Some(do_parse!($elem, $constructor));
|
||||||
|
};
|
||||||
|
($temp:ident: Present = $constructor:ident => $elem:ident, $name:tt, $parent_name:tt) => {
|
||||||
|
if $temp {
|
||||||
|
return Err(crate::util::error::Error::ParseError(concat!(
|
||||||
|
"Element ",
|
||||||
|
$parent_name,
|
||||||
|
" must not have more than one ",
|
||||||
|
$name,
|
||||||
|
" child."
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
$temp = true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! finish_parse_elem {
|
||||||
|
($temp:ident: Vec = $name:tt, $parent_name:tt) => {
|
||||||
|
$temp
|
||||||
|
};
|
||||||
|
($temp:ident: Option = $name:tt, $parent_name:tt) => {
|
||||||
|
$temp
|
||||||
|
};
|
||||||
|
($temp:ident: Required = $name:tt, $parent_name:tt) => {
|
||||||
|
$temp.ok_or(crate::util::error::Error::ParseError(concat!(
|
||||||
|
"Missing child ",
|
||||||
|
$name,
|
||||||
|
" in ",
|
||||||
|
$parent_name,
|
||||||
|
" element."
|
||||||
|
)))?
|
||||||
|
};
|
||||||
|
($temp:ident: Present = $name:tt, $parent_name:tt) => {
|
||||||
|
$temp
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! generate_serialiser {
|
||||||
|
($builder:ident, $parent:ident, $elem:ident, Required, String, ($name:tt, $ns:ident)) => {
|
||||||
|
$builder.append(
|
||||||
|
crate::Element::builder($name, crate::ns::$ns)
|
||||||
|
.append(::minidom::Node::Text($parent.$elem)),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
($builder:ident, $parent:ident, $elem:ident, Option, String, ($name:tt, $ns:ident)) => {
|
||||||
|
$builder.append_all($parent.$elem.map(|elem| {
|
||||||
|
crate::Element::builder($name, crate::ns::$ns).append(::minidom::Node::Text(elem))
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
($builder:ident, $parent:ident, $elem:ident, Option, $constructor:ident, ($name:tt, *)) => {
|
||||||
|
$builder.append_all(
|
||||||
|
$parent
|
||||||
|
.$elem
|
||||||
|
.map(|elem| ::minidom::Node::Element(crate::Element::from(elem))),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
($builder:ident, $parent:ident, $elem:ident, Option, $constructor:ident, ($name:tt, $ns:ident)) => {
|
||||||
|
$builder.append_all(
|
||||||
|
$parent
|
||||||
|
.$elem
|
||||||
|
.map(|elem| ::minidom::Node::Element(crate::Element::from(elem))),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
($builder:ident, $parent:ident, $elem:ident, Vec, $constructor:ident, ($name:tt, $ns:ident)) => {
|
||||||
|
$builder.append_all($parent.$elem.into_iter())
|
||||||
|
};
|
||||||
|
($builder:ident, $parent:ident, $elem:ident, Present, $constructor:ident, ($name:tt, $ns:ident)) => {
|
||||||
|
$builder.append(::minidom::Node::Element(
|
||||||
|
crate::Element::builder($name, crate::ns::$ns).build(),
|
||||||
|
))
|
||||||
|
};
|
||||||
|
($builder:ident, $parent:ident, $elem:ident, $_:ident, $constructor:ident, ($name:tt, $ns:ident)) => {
|
||||||
|
$builder.append(::minidom::Node::Element(crate::Element::from(
|
||||||
|
$parent.$elem,
|
||||||
|
)))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! generate_child_test {
|
||||||
|
($child:ident, $name:tt, *) => {
|
||||||
|
$child.is($name, ::minidom::NSChoice::Any)
|
||||||
|
};
|
||||||
|
($child:ident, $name:tt, $ns:tt) => {
|
||||||
|
$child.is($name, crate::ns::$ns)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! generate_element {
|
||||||
|
($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, attributes: [$($(#[$attr_meta:meta])* $attr:ident: $attr_action:tt<$attr_type:ty> = $attr_name:tt),+,]) => (
|
||||||
|
generate_element!($(#[$meta])* $elem, $name, $ns, attributes: [$($(#[$attr_meta])* $attr: $attr_action<$attr_type> = $attr_name),*], children: []);
|
||||||
|
);
|
||||||
|
($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, attributes: [$($(#[$attr_meta:meta])* $attr:ident: $attr_action:tt<$attr_type:ty> = $attr_name:tt),+]) => (
|
||||||
|
generate_element!($(#[$meta])* $elem, $name, $ns, attributes: [$($(#[$attr_meta])* $attr: $attr_action<$attr_type> = $attr_name),*], children: []);
|
||||||
|
);
|
||||||
|
($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, children: [$($(#[$child_meta:meta])* $child_ident:ident: $coucou:tt<$child_type:ty> = ($child_name:tt, $child_ns:tt) => $child_constructor:ident),*]) => (
|
||||||
|
generate_element!($(#[$meta])* $elem, $name, $ns, attributes: [], children: [$($(#[$child_meta])* $child_ident: $coucou<$child_type> = ($child_name, $child_ns) => $child_constructor),*]);
|
||||||
|
);
|
||||||
|
($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, attributes: [$($(#[$attr_meta:meta])* $attr:ident: $attr_action:tt<$attr_type:ty> = $attr_name:tt),*,], children: [$($(#[$child_meta:meta])* $child_ident:ident: $coucou:tt<$child_type:ty> = ($child_name:tt, $child_ns:tt) => $child_constructor:ident),*]) => (
|
||||||
|
generate_element!($(#[$meta])* $elem, $name, $ns, attributes: [$($(#[$attr_meta])* $attr: $attr_action<$attr_type> = $attr_name),*], children: [$($(#[$child_meta])* $child_ident: $coucou<$child_type> = ($child_name, $child_ns) => $child_constructor),*]);
|
||||||
|
);
|
||||||
|
($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, text: ($(#[$text_meta:meta])* $text_ident:ident: $codec:ident < $text_type:ty >)) => (
|
||||||
|
generate_element!($(#[$meta])* $elem, $name, $ns, attributes: [], children: [], text: ($(#[$text_meta])* $text_ident: $codec<$text_type>));
|
||||||
|
);
|
||||||
|
($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, attributes: [$($(#[$attr_meta:meta])* $attr:ident: $attr_action:tt<$attr_type:ty> = $attr_name:tt),+], text: ($(#[$text_meta:meta])* $text_ident:ident: $codec:ident < $text_type:ty >)) => (
|
||||||
|
generate_element!($(#[$meta])* $elem, $name, $ns, attributes: [$($(#[$attr_meta])* $attr: $attr_action<$attr_type> = $attr_name),*], children: [], text: ($(#[$text_meta])* $text_ident: $codec<$text_type>));
|
||||||
|
);
|
||||||
|
($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, attributes: [$($(#[$attr_meta:meta])* $attr:ident: $attr_action:tt<$attr_type:ty> = $attr_name:tt),*], children: [$($(#[$child_meta:meta])* $child_ident:ident: $coucou:tt<$child_type:ty> = ($child_name:tt, $child_ns:tt) => $child_constructor:ident),*] $(, text: ($(#[$text_meta:meta])* $text_ident:ident: $codec:ident < $text_type:ty >))*) => (
|
||||||
|
$(#[$meta])*
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct $elem {
|
||||||
|
$(
|
||||||
|
$(#[$attr_meta])*
|
||||||
|
pub $attr: decl_attr!($attr_action, $attr_type),
|
||||||
|
)*
|
||||||
|
$(
|
||||||
|
$(#[$child_meta])*
|
||||||
|
pub $child_ident: start_decl!($coucou, $child_type),
|
||||||
|
)*
|
||||||
|
$(
|
||||||
|
$(#[$text_meta])*
|
||||||
|
pub $text_ident: $text_type,
|
||||||
|
)*
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ::std::convert::TryFrom<crate::Element> for $elem {
|
||||||
|
type Error = crate::util::error::Error;
|
||||||
|
|
||||||
|
fn try_from(elem: crate::Element) -> Result<$elem, crate::util::error::Error> {
|
||||||
|
check_self!(elem, $name, $ns);
|
||||||
|
check_no_unknown_attributes!(elem, $name, [$($attr_name),*]);
|
||||||
|
$(
|
||||||
|
start_parse_elem!($child_ident: $coucou);
|
||||||
|
)*
|
||||||
|
for _child in elem.children() {
|
||||||
|
$(
|
||||||
|
if generate_child_test!(_child, $child_name, $child_ns) {
|
||||||
|
do_parse_elem!($child_ident: $coucou = $child_constructor => _child, $child_name, $name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
return Err(crate::util::error::Error::ParseError(concat!("Unknown child in ", $name, " element.")));
|
||||||
|
}
|
||||||
|
Ok($elem {
|
||||||
|
$(
|
||||||
|
$attr: get_attr!(elem, $attr_name, $attr_action),
|
||||||
|
)*
|
||||||
|
$(
|
||||||
|
$child_ident: finish_parse_elem!($child_ident: $coucou = $child_name, $name),
|
||||||
|
)*
|
||||||
|
$(
|
||||||
|
$text_ident: $codec::decode(&elem.text())?,
|
||||||
|
)*
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<$elem> for crate::Element {
|
||||||
|
fn from(elem: $elem) -> crate::Element {
|
||||||
|
let mut builder = crate::Element::builder($name, crate::ns::$ns);
|
||||||
|
$(
|
||||||
|
builder = builder.attr($attr_name, elem.$attr);
|
||||||
|
)*
|
||||||
|
$(
|
||||||
|
builder = generate_serialiser!(builder, elem, $child_ident, $coucou, $child_constructor, ($child_name, $child_ns));
|
||||||
|
)*
|
||||||
|
$(
|
||||||
|
builder = builder.append_all($codec::encode(&elem.$text_ident).map(::minidom::Node::Text).into_iter());
|
||||||
|
)*
|
||||||
|
|
||||||
|
builder.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
macro_rules! assert_size (
|
||||||
|
($t:ty, $sz:expr) => (
|
||||||
|
assert_eq!(::std::mem::size_of::<$t>(), $sz);
|
||||||
|
);
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO: move that to src/pubsub/mod.rs, once we figure out how to use macros from there.
|
||||||
|
macro_rules! impl_pubsub_item {
|
||||||
|
($item:ident, $ns:ident) => {
|
||||||
|
impl ::std::convert::TryFrom<crate::Element> for $item {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(elem: crate::Element) -> Result<$item, Error> {
|
||||||
|
check_self!(elem, "item", $ns);
|
||||||
|
check_no_unknown_attributes!(elem, "item", ["id", "publisher"]);
|
||||||
|
let mut payloads = elem.children().cloned().collect::<Vec<_>>();
|
||||||
|
let payload = payloads.pop();
|
||||||
|
if !payloads.is_empty() {
|
||||||
|
return Err(Error::ParseError(
|
||||||
|
"More than a single payload in item element.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok($item(crate::pubsub::Item {
|
||||||
|
id: get_attr!(elem, "id", Option),
|
||||||
|
publisher: get_attr!(elem, "publisher", Option),
|
||||||
|
payload,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<$item> for crate::Element {
|
||||||
|
fn from(item: $item) -> crate::Element {
|
||||||
|
crate::Element::builder("item", ns::$ns)
|
||||||
|
.attr("id", item.0.id)
|
||||||
|
.attr("publisher", item.0.publisher)
|
||||||
|
.append_all(item.0.payload)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ::std::ops::Deref for $item {
|
||||||
|
type Target = crate::pubsub::Item;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ::std::ops::DerefMut for $item {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
/// Error type returned by every parser on failure.
|
||||||
|
pub mod error;
|
||||||
|
|
||||||
|
/// Various helpers.
|
||||||
|
pub(crate) mod helpers;
|
||||||
|
|
||||||
|
/// Helper macros to parse and serialise more easily.
|
||||||
|
#[macro_use]
|
||||||
|
mod macros;
|
|
@ -0,0 +1,88 @@
|
||||||
|
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
use crate::iq::{IqGetPayload, IqResultPayload};
|
||||||
|
|
||||||
|
generate_empty_element!(
|
||||||
|
/// Represents a query for the software version a remote entity is using.
|
||||||
|
///
|
||||||
|
/// It should only be used in an `<iq type='get'/>`, as it can only
|
||||||
|
/// represent the request, and not a result.
|
||||||
|
VersionQuery,
|
||||||
|
"query",
|
||||||
|
VERSION
|
||||||
|
);
|
||||||
|
|
||||||
|
impl IqGetPayload for VersionQuery {}
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// Represents the answer about the software version we are using.
|
||||||
|
///
|
||||||
|
/// It should only be used in an `<iq type='result'/>`, as it can only
|
||||||
|
/// represent the result, and not a request.
|
||||||
|
VersionResult, "query", VERSION,
|
||||||
|
children: [
|
||||||
|
/// The name of this client.
|
||||||
|
name: Required<String> = ("name", VERSION) => String,
|
||||||
|
|
||||||
|
/// The version of this client.
|
||||||
|
version: Required<String> = ("version", VERSION) => String,
|
||||||
|
|
||||||
|
/// The OS this client is running on.
|
||||||
|
os: Option<String> = ("os", VERSION) => String
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
impl IqResultPayload for VersionResult {}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::Element;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "32")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(VersionQuery, 0);
|
||||||
|
assert_size!(VersionResult, 36);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "64")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(VersionQuery, 0);
|
||||||
|
assert_size!(VersionResult, 72);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn simple() {
|
||||||
|
let elem: Element =
|
||||||
|
"<query xmlns='jabber:iq:version'><name>xmpp-rs</name><version>0.3.0</version></query>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let version = VersionResult::try_from(elem).unwrap();
|
||||||
|
assert_eq!(version.name, String::from("xmpp-rs"));
|
||||||
|
assert_eq!(version.version, String::from("0.3.0"));
|
||||||
|
assert_eq!(version.os, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn serialisation() {
|
||||||
|
let version = VersionResult {
|
||||||
|
name: String::from("xmpp-rs"),
|
||||||
|
version: String::from("0.3.0"),
|
||||||
|
os: None,
|
||||||
|
};
|
||||||
|
let elem1 = Element::from(version);
|
||||||
|
let elem2: Element =
|
||||||
|
"<query xmlns='jabber:iq:version'><name>xmpp-rs</name><version>0.3.0</version></query>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
println!("{:?}", elem1);
|
||||||
|
assert_eq!(elem1, elem2);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,102 @@
|
||||||
|
// Copyright (c) 2018 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
use jid::BareJid;
|
||||||
|
|
||||||
|
generate_element!(
|
||||||
|
/// The stream opening for WebSocket.
|
||||||
|
Open, "open", WEBSOCKET,
|
||||||
|
attributes: [
|
||||||
|
/// The JID of the entity opening this stream.
|
||||||
|
from: Option<BareJid> = "from",
|
||||||
|
|
||||||
|
/// The JID of the entity receiving this stream opening.
|
||||||
|
to: Option<BareJid> = "to",
|
||||||
|
|
||||||
|
/// The id of the stream, used for authentication challenges.
|
||||||
|
id: Option<String> = "id",
|
||||||
|
|
||||||
|
/// The XMPP version used during this stream.
|
||||||
|
version: Option<String> = "version",
|
||||||
|
|
||||||
|
/// The default human language for all subsequent stanzas, which will
|
||||||
|
/// be transmitted to other entities for better localisation.
|
||||||
|
xml_lang: Option<String> = "xml:lang",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
impl Open {
|
||||||
|
/// Creates a simple client→server `<open/>` element.
|
||||||
|
pub fn new(to: BareJid) -> Open {
|
||||||
|
Open {
|
||||||
|
from: None,
|
||||||
|
to: Some(to),
|
||||||
|
id: None,
|
||||||
|
version: Some(String::from("1.0")),
|
||||||
|
xml_lang: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the [@from](#structfield.from) attribute on this `<open/>`
|
||||||
|
/// element.
|
||||||
|
pub fn with_from(mut self, from: BareJid) -> Open {
|
||||||
|
self.from = Some(from);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the [@id](#structfield.id) attribute on this `<open/>` element.
|
||||||
|
pub fn with_id(mut self, id: String) -> Open {
|
||||||
|
self.id = Some(id);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the [@xml:lang](#structfield.xml_lang) attribute on this `<open/>`
|
||||||
|
/// element.
|
||||||
|
pub fn with_lang(mut self, xml_lang: String) -> Open {
|
||||||
|
self.xml_lang = Some(xml_lang);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks whether the version matches the expected one.
|
||||||
|
pub fn is_version(&self, version: &str) -> bool {
|
||||||
|
match self.version {
|
||||||
|
None => false,
|
||||||
|
Some(ref self_version) => self_version == &String::from(version),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::Element;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "32")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(Open, 84);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "64")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(Open, 168);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_simple() {
|
||||||
|
let elem: Element = "<open xmlns='urn:ietf:params:xml:ns:xmpp-framing'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let open = Open::try_from(elem).unwrap();
|
||||||
|
assert_eq!(open.from, None);
|
||||||
|
assert_eq!(open.to, None);
|
||||||
|
assert_eq!(open.id, None);
|
||||||
|
assert_eq!(open.version, None);
|
||||||
|
assert_eq!(open.xml_lang, None);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,642 @@
|
||||||
|
// 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::ns;
|
||||||
|
use crate::util::error::Error;
|
||||||
|
use minidom::{Element, Node};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
// TODO: Use a proper lang type.
|
||||||
|
type Lang = String;
|
||||||
|
|
||||||
|
/// Container for formatted text.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct XhtmlIm {
|
||||||
|
/// Map of language to body element.
|
||||||
|
bodies: HashMap<Lang, Body>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl XhtmlIm {
|
||||||
|
/// Serialise formatted text to HTML.
|
||||||
|
pub fn to_html(self) -> String {
|
||||||
|
let mut html = Vec::new();
|
||||||
|
// TODO: use the best language instead.
|
||||||
|
for (lang, body) in self.bodies {
|
||||||
|
if lang.is_empty() {
|
||||||
|
assert!(body.xml_lang.is_none());
|
||||||
|
} else {
|
||||||
|
assert_eq!(Some(lang), body.xml_lang);
|
||||||
|
}
|
||||||
|
for tag in body.children {
|
||||||
|
html.push(tag.to_html());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
html.concat()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes all unknown elements.
|
||||||
|
fn flatten(self) -> XhtmlIm {
|
||||||
|
let mut bodies = HashMap::new();
|
||||||
|
for (lang, body) in self.bodies {
|
||||||
|
let children = body.children.into_iter().fold(vec![], |mut acc, child| {
|
||||||
|
match child {
|
||||||
|
Child::Tag(Tag::Unknown(children)) => acc.extend(children),
|
||||||
|
any => acc.push(any),
|
||||||
|
}
|
||||||
|
acc
|
||||||
|
});
|
||||||
|
let body = Body { children, ..body };
|
||||||
|
bodies.insert(lang, body);
|
||||||
|
}
|
||||||
|
XhtmlIm { bodies }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessagePayload for XhtmlIm {}
|
||||||
|
|
||||||
|
impl TryFrom<Element> for XhtmlIm {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(elem: Element) -> Result<XhtmlIm, Error> {
|
||||||
|
check_self!(elem, "html", XHTML_IM);
|
||||||
|
check_no_attributes!(elem, "html");
|
||||||
|
|
||||||
|
let mut bodies = HashMap::new();
|
||||||
|
for child in elem.children() {
|
||||||
|
if child.is("body", ns::XHTML) {
|
||||||
|
let child = child.clone();
|
||||||
|
let lang = match child.attr("xml:lang") {
|
||||||
|
Some(lang) => lang,
|
||||||
|
None => "",
|
||||||
|
}
|
||||||
|
.to_string();
|
||||||
|
let body = Body::try_from(child)?;
|
||||||
|
match bodies.insert(lang, body) {
|
||||||
|
None => (),
|
||||||
|
Some(_) => {
|
||||||
|
return Err(Error::ParseError(
|
||||||
|
"Two identical language bodies found in XHTML-IM.",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(Error::ParseError("Unknown element in XHTML-IM."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(XhtmlIm { bodies }.flatten())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<XhtmlIm> for Element {
|
||||||
|
fn from(wrapper: XhtmlIm) -> Element {
|
||||||
|
Element::builder("html", ns::XHTML_IM)
|
||||||
|
.append_all(wrapper.bodies.into_iter().map(|(lang, body)| {
|
||||||
|
if lang.is_empty() {
|
||||||
|
assert!(body.xml_lang.is_none());
|
||||||
|
} else {
|
||||||
|
assert_eq!(Some(lang), body.xml_lang);
|
||||||
|
}
|
||||||
|
Element::from(body)
|
||||||
|
}))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
enum Child {
|
||||||
|
Tag(Tag),
|
||||||
|
Text(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Child {
|
||||||
|
fn to_html(self) -> String {
|
||||||
|
match self {
|
||||||
|
Child::Tag(tag) => tag.to_html(),
|
||||||
|
Child::Text(text) => text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct Property {
|
||||||
|
key: String,
|
||||||
|
value: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
type Css = Vec<Property>;
|
||||||
|
|
||||||
|
fn get_style_string(style: Css) -> Option<String> {
|
||||||
|
let mut result = vec![];
|
||||||
|
for Property { key, value } in style {
|
||||||
|
result.push(format!("{}: {}", key, value));
|
||||||
|
}
|
||||||
|
if result.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(result.join("; "))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct Body {
|
||||||
|
style: Css,
|
||||||
|
xml_lang: Option<String>,
|
||||||
|
children: Vec<Child>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<Element> for Body {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(elem: Element) -> Result<Body, Error> {
|
||||||
|
let mut children = vec![];
|
||||||
|
for child in elem.nodes() {
|
||||||
|
match child {
|
||||||
|
Node::Element(child) => children.push(Child::Tag(Tag::try_from(child.clone())?)),
|
||||||
|
Node::Text(text) => children.push(Child::Text(text.clone())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Body {
|
||||||
|
style: parse_css(elem.attr("style")),
|
||||||
|
xml_lang: elem.attr("xml:lang").map(|xml_lang| xml_lang.to_string()),
|
||||||
|
children,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Body> for Element {
|
||||||
|
fn from(body: Body) -> Element {
|
||||||
|
Element::builder("body", ns::XHTML)
|
||||||
|
.attr("style", get_style_string(body.style))
|
||||||
|
.attr("xml:lang", body.xml_lang)
|
||||||
|
.append_all(children_to_nodes(body.children))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
enum Tag {
|
||||||
|
A {
|
||||||
|
href: Option<String>,
|
||||||
|
style: Css,
|
||||||
|
type_: Option<String>,
|
||||||
|
children: Vec<Child>,
|
||||||
|
},
|
||||||
|
Blockquote {
|
||||||
|
style: Css,
|
||||||
|
children: Vec<Child>,
|
||||||
|
},
|
||||||
|
Br,
|
||||||
|
Cite {
|
||||||
|
style: Css,
|
||||||
|
children: Vec<Child>,
|
||||||
|
},
|
||||||
|
Em {
|
||||||
|
children: Vec<Child>,
|
||||||
|
},
|
||||||
|
Img {
|
||||||
|
src: Option<String>,
|
||||||
|
alt: Option<String>,
|
||||||
|
}, // TODO: height, width, style
|
||||||
|
Li {
|
||||||
|
style: Css,
|
||||||
|
children: Vec<Child>,
|
||||||
|
},
|
||||||
|
Ol {
|
||||||
|
style: Css,
|
||||||
|
children: Vec<Child>,
|
||||||
|
},
|
||||||
|
P {
|
||||||
|
style: Css,
|
||||||
|
children: Vec<Child>,
|
||||||
|
},
|
||||||
|
Span {
|
||||||
|
style: Css,
|
||||||
|
children: Vec<Child>,
|
||||||
|
},
|
||||||
|
Strong {
|
||||||
|
children: Vec<Child>,
|
||||||
|
},
|
||||||
|
Ul {
|
||||||
|
style: Css,
|
||||||
|
children: Vec<Child>,
|
||||||
|
},
|
||||||
|
Unknown(Vec<Child>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Tag {
|
||||||
|
fn to_html(self) -> String {
|
||||||
|
match self {
|
||||||
|
Tag::A {
|
||||||
|
href,
|
||||||
|
style,
|
||||||
|
type_,
|
||||||
|
children,
|
||||||
|
} => {
|
||||||
|
let href = write_attr(href, "href");
|
||||||
|
let style = write_attr(get_style_string(style), "style");
|
||||||
|
let type_ = write_attr(type_, "type");
|
||||||
|
format!(
|
||||||
|
"<a{}{}{}>{}</a>",
|
||||||
|
href,
|
||||||
|
style,
|
||||||
|
type_,
|
||||||
|
children_to_html(children)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Tag::Blockquote { style, children } => {
|
||||||
|
let style = write_attr(get_style_string(style), "style");
|
||||||
|
format!(
|
||||||
|
"<blockquote{}>{}</blockquote>",
|
||||||
|
style,
|
||||||
|
children_to_html(children)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Tag::Br => String::from("<br>"),
|
||||||
|
Tag::Cite { style, children } => {
|
||||||
|
let style = write_attr(get_style_string(style), "style");
|
||||||
|
format!("<cite{}>{}</cite>", style, children_to_html(children))
|
||||||
|
}
|
||||||
|
Tag::Em { children } => format!("<em>{}</em>", children_to_html(children)),
|
||||||
|
Tag::Img { src, alt } => {
|
||||||
|
let src = write_attr(src, "src");
|
||||||
|
let alt = write_attr(alt, "alt");
|
||||||
|
format!("<img{}{}>", src, alt)
|
||||||
|
}
|
||||||
|
Tag::Li { style, children } => {
|
||||||
|
let style = write_attr(get_style_string(style), "style");
|
||||||
|
format!("<li{}>{}</li>", style, children_to_html(children))
|
||||||
|
}
|
||||||
|
Tag::Ol { style, children } => {
|
||||||
|
let style = write_attr(get_style_string(style), "style");
|
||||||
|
format!("<ol{}>{}</ol>", style, children_to_html(children))
|
||||||
|
}
|
||||||
|
Tag::P { style, children } => {
|
||||||
|
let style = write_attr(get_style_string(style), "style");
|
||||||
|
format!("<p{}>{}</p>", style, children_to_html(children))
|
||||||
|
}
|
||||||
|
Tag::Span { style, children } => {
|
||||||
|
let style = write_attr(get_style_string(style), "style");
|
||||||
|
format!("<span{}>{}</span>", style, children_to_html(children))
|
||||||
|
}
|
||||||
|
Tag::Strong { children } => format!("<strong>{}</strong>", children_to_html(children)),
|
||||||
|
Tag::Ul { style, children } => {
|
||||||
|
let style = write_attr(get_style_string(style), "style");
|
||||||
|
format!("<ul{}>{}</ul>", style, children_to_html(children))
|
||||||
|
}
|
||||||
|
Tag::Unknown(_) => {
|
||||||
|
panic!("No unknown element should be present in XHTML-IM after parsing.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<Element> for Tag {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(elem: Element) -> Result<Tag, Error> {
|
||||||
|
let mut children = vec![];
|
||||||
|
for child in elem.nodes() {
|
||||||
|
match child {
|
||||||
|
Node::Element(child) => children.push(Child::Tag(Tag::try_from(child.clone())?)),
|
||||||
|
Node::Text(text) => children.push(Child::Text(text.clone())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(match elem.name() {
|
||||||
|
"a" => Tag::A {
|
||||||
|
href: elem.attr("href").map(|href| href.to_string()),
|
||||||
|
style: parse_css(elem.attr("style")),
|
||||||
|
type_: elem.attr("type").map(|type_| type_.to_string()),
|
||||||
|
children,
|
||||||
|
},
|
||||||
|
"blockquote" => Tag::Blockquote {
|
||||||
|
style: parse_css(elem.attr("style")),
|
||||||
|
children,
|
||||||
|
},
|
||||||
|
"br" => Tag::Br,
|
||||||
|
"cite" => Tag::Cite {
|
||||||
|
style: parse_css(elem.attr("style")),
|
||||||
|
children,
|
||||||
|
},
|
||||||
|
"em" => Tag::Em { children },
|
||||||
|
"img" => Tag::Img {
|
||||||
|
src: elem.attr("src").map(|src| src.to_string()),
|
||||||
|
alt: elem.attr("alt").map(|alt| alt.to_string()),
|
||||||
|
},
|
||||||
|
"li" => Tag::Li {
|
||||||
|
style: parse_css(elem.attr("style")),
|
||||||
|
children,
|
||||||
|
},
|
||||||
|
"ol" => Tag::Ol {
|
||||||
|
style: parse_css(elem.attr("style")),
|
||||||
|
children,
|
||||||
|
},
|
||||||
|
"p" => Tag::P {
|
||||||
|
style: parse_css(elem.attr("style")),
|
||||||
|
children,
|
||||||
|
},
|
||||||
|
"span" => Tag::Span {
|
||||||
|
style: parse_css(elem.attr("style")),
|
||||||
|
children,
|
||||||
|
},
|
||||||
|
"strong" => Tag::Strong { children },
|
||||||
|
"ul" => Tag::Ul {
|
||||||
|
style: parse_css(elem.attr("style")),
|
||||||
|
children,
|
||||||
|
},
|
||||||
|
_ => Tag::Unknown(children),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Tag> for Element {
|
||||||
|
fn from(tag: Tag) -> Element {
|
||||||
|
let (name, attrs, children) = match tag {
|
||||||
|
Tag::A {
|
||||||
|
href,
|
||||||
|
style,
|
||||||
|
type_,
|
||||||
|
children,
|
||||||
|
} => (
|
||||||
|
"a",
|
||||||
|
{
|
||||||
|
let mut attrs = vec![];
|
||||||
|
if let Some(href) = href {
|
||||||
|
attrs.push(("href", href));
|
||||||
|
}
|
||||||
|
if let Some(style) = get_style_string(style) {
|
||||||
|
attrs.push(("style", style));
|
||||||
|
}
|
||||||
|
if let Some(type_) = type_ {
|
||||||
|
attrs.push(("type", type_));
|
||||||
|
}
|
||||||
|
attrs
|
||||||
|
},
|
||||||
|
children,
|
||||||
|
),
|
||||||
|
Tag::Blockquote { style, children } => (
|
||||||
|
"blockquote",
|
||||||
|
match get_style_string(style) {
|
||||||
|
Some(style) => vec![("style", style)],
|
||||||
|
None => vec![],
|
||||||
|
},
|
||||||
|
children,
|
||||||
|
),
|
||||||
|
Tag::Br => ("br", vec![], vec![]),
|
||||||
|
Tag::Cite { style, children } => (
|
||||||
|
"cite",
|
||||||
|
match get_style_string(style) {
|
||||||
|
Some(style) => vec![("style", style)],
|
||||||
|
None => vec![],
|
||||||
|
},
|
||||||
|
children,
|
||||||
|
),
|
||||||
|
Tag::Em { children } => ("em", vec![], children),
|
||||||
|
Tag::Img { src, alt } => {
|
||||||
|
let mut attrs = vec![];
|
||||||
|
if let Some(src) = src {
|
||||||
|
attrs.push(("src", src));
|
||||||
|
}
|
||||||
|
if let Some(alt) = alt {
|
||||||
|
attrs.push(("alt", alt));
|
||||||
|
}
|
||||||
|
("img", attrs, vec![])
|
||||||
|
}
|
||||||
|
Tag::Li { style, children } => (
|
||||||
|
"li",
|
||||||
|
match get_style_string(style) {
|
||||||
|
Some(style) => vec![("style", style)],
|
||||||
|
None => vec![],
|
||||||
|
},
|
||||||
|
children,
|
||||||
|
),
|
||||||
|
Tag::Ol { style, children } => (
|
||||||
|
"ol",
|
||||||
|
match get_style_string(style) {
|
||||||
|
Some(style) => vec![("style", style)],
|
||||||
|
None => vec![],
|
||||||
|
},
|
||||||
|
children,
|
||||||
|
),
|
||||||
|
Tag::P { style, children } => (
|
||||||
|
"p",
|
||||||
|
match get_style_string(style) {
|
||||||
|
Some(style) => vec![("style", style)],
|
||||||
|
None => vec![],
|
||||||
|
},
|
||||||
|
children,
|
||||||
|
),
|
||||||
|
Tag::Span { style, children } => (
|
||||||
|
"span",
|
||||||
|
match get_style_string(style) {
|
||||||
|
Some(style) => vec![("style", style)],
|
||||||
|
None => vec![],
|
||||||
|
},
|
||||||
|
children,
|
||||||
|
),
|
||||||
|
Tag::Strong { children } => ("strong", vec![], children),
|
||||||
|
Tag::Ul { style, children } => (
|
||||||
|
"ul",
|
||||||
|
match get_style_string(style) {
|
||||||
|
Some(style) => vec![("style", style)],
|
||||||
|
None => vec![],
|
||||||
|
},
|
||||||
|
children,
|
||||||
|
),
|
||||||
|
Tag::Unknown(_) => {
|
||||||
|
panic!("No unknown element should be present in XHTML-IM after parsing.")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut builder = Element::builder(name, ns::XHTML).append_all(children_to_nodes(children));
|
||||||
|
for (key, value) in attrs {
|
||||||
|
builder = builder.attr(key, value);
|
||||||
|
}
|
||||||
|
builder.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn children_to_nodes(children: Vec<Child>) -> impl IntoIterator<Item = Node> {
|
||||||
|
children.into_iter().map(|child| match child {
|
||||||
|
Child::Tag(tag) => Node::Element(Element::from(tag)),
|
||||||
|
Child::Text(text) => Node::Text(text),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn children_to_html(children: Vec<Child>) -> String {
|
||||||
|
children
|
||||||
|
.into_iter()
|
||||||
|
.map(|child| child.to_html())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.concat()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_attr(attr: Option<String>, name: &str) -> String {
|
||||||
|
match attr {
|
||||||
|
Some(attr) => format!(" {}='{}'", name, attr),
|
||||||
|
None => String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_css(style: Option<&str>) -> Css {
|
||||||
|
let mut properties = vec![];
|
||||||
|
if let Some(style) = style {
|
||||||
|
// TODO: make that parser a bit more resilient to things.
|
||||||
|
for part in style.split(";") {
|
||||||
|
let mut part = part
|
||||||
|
.splitn(2, ":")
|
||||||
|
.map(|a| a.to_string())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let key = part.pop().unwrap();
|
||||||
|
let value = part.pop().unwrap();
|
||||||
|
properties.push(Property { key, value });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
properties
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "32")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(XhtmlIm, 32);
|
||||||
|
assert_size!(Child, 56);
|
||||||
|
assert_size!(Tag, 52);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_pointer_width = "64")]
|
||||||
|
#[test]
|
||||||
|
fn test_size() {
|
||||||
|
assert_size!(XhtmlIm, 48);
|
||||||
|
assert_size!(Child, 112);
|
||||||
|
assert_size!(Tag, 104);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_empty() {
|
||||||
|
let elem: Element = "<html xmlns='http://jabber.org/protocol/xhtml-im'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let xhtml = XhtmlIm::try_from(elem).unwrap();
|
||||||
|
assert_eq!(xhtml.bodies.len(), 0);
|
||||||
|
|
||||||
|
let elem: Element = "<html xmlns='http://jabber.org/protocol/xhtml-im'><body xmlns='http://www.w3.org/1999/xhtml'/></html>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let xhtml = XhtmlIm::try_from(elem).unwrap();
|
||||||
|
assert_eq!(xhtml.bodies.len(), 1);
|
||||||
|
|
||||||
|
let elem: Element = "<html xmlns='http://jabber.org/protocol/xhtml-im' xmlns:html='http://www.w3.org/1999/xhtml'><html:body xml:lang='fr'/><html:body xml:lang='en'/></html>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let xhtml = XhtmlIm::try_from(elem).unwrap();
|
||||||
|
assert_eq!(xhtml.bodies.len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_two_same_langs() {
|
||||||
|
let elem: Element = "<html xmlns='http://jabber.org/protocol/xhtml-im' xmlns:html='http://www.w3.org/1999/xhtml'><html:body/><html:body/></html>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let error = XhtmlIm::try_from(elem).unwrap_err();
|
||||||
|
let message = match error {
|
||||||
|
Error::ParseError(string) => string,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(message, "Two identical language bodies found in XHTML-IM.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tag() {
|
||||||
|
let elem: Element = "<body xmlns='http://www.w3.org/1999/xhtml'/>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let body = Body::try_from(elem).unwrap();
|
||||||
|
assert_eq!(body.children.len(), 0);
|
||||||
|
|
||||||
|
let elem: Element = "<body xmlns='http://www.w3.org/1999/xhtml'><p>Hello world!</p></body>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let mut body = Body::try_from(elem).unwrap();
|
||||||
|
assert_eq!(body.style.len(), 0);
|
||||||
|
assert_eq!(body.xml_lang, None);
|
||||||
|
assert_eq!(body.children.len(), 1);
|
||||||
|
let p = match body.children.pop() {
|
||||||
|
Some(Child::Tag(tag)) => tag,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
let mut children = match p {
|
||||||
|
Tag::P { style, children } => {
|
||||||
|
assert_eq!(style.len(), 0);
|
||||||
|
assert_eq!(children.len(), 1);
|
||||||
|
children
|
||||||
|
}
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
let text = match children.pop() {
|
||||||
|
Some(Child::Text(text)) => text,
|
||||||
|
_ => panic!(),
|
||||||
|
};
|
||||||
|
assert_eq!(text, "Hello world!");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_unknown_element() {
|
||||||
|
let elem: Element = "<html xmlns='http://jabber.org/protocol/xhtml-im'><body xmlns='http://www.w3.org/1999/xhtml'><coucou>Hello world!</coucou></body></html>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let parsed = XhtmlIm::try_from(elem).unwrap();
|
||||||
|
let parsed2 = parsed.clone();
|
||||||
|
let html = parsed.to_html();
|
||||||
|
assert_eq!(html, "Hello world!");
|
||||||
|
|
||||||
|
let elem = Element::from(parsed2);
|
||||||
|
assert_eq!(String::from(&elem), "<html xmlns=\"http://jabber.org/protocol/xhtml-im\"><body xmlns=\"http://www.w3.org/1999/xhtml\">Hello world!</body></html>");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_generate_html() {
|
||||||
|
let elem: Element = "<html xmlns='http://jabber.org/protocol/xhtml-im'><body xmlns='http://www.w3.org/1999/xhtml'><p>Hello world!</p></body></html>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let xhtml_im = XhtmlIm::try_from(elem).unwrap();
|
||||||
|
let html = xhtml_im.to_html();
|
||||||
|
assert_eq!(html, "<p>Hello world!</p>");
|
||||||
|
|
||||||
|
let elem: Element = "<html xmlns='http://jabber.org/protocol/xhtml-im'><body xmlns='http://www.w3.org/1999/xhtml'><p>Hello <strong>world</strong>!</p></body></html>"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
let xhtml_im = XhtmlIm::try_from(elem).unwrap();
|
||||||
|
let html = xhtml_im.to_html();
|
||||||
|
assert_eq!(html, "<p>Hello <strong>world</strong>!</p>");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generate_tree() {
|
||||||
|
let world = "world".to_string();
|
||||||
|
|
||||||
|
Body {
|
||||||
|
style: vec![],
|
||||||
|
xml_lang: Some("en".to_string()),
|
||||||
|
children: vec![Child::Tag(Tag::P {
|
||||||
|
style: vec![],
|
||||||
|
children: vec![
|
||||||
|
Child::Text("Hello ".to_string()),
|
||||||
|
Child::Tag(Tag::Strong {
|
||||||
|
children: vec![Child::Text(world)],
|
||||||
|
}),
|
||||||
|
Child::Text("!".to_string()),
|
||||||
|
],
|
||||||
|
})],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue