vendored xmpp-parsers into the repository for now

This commit is contained in:
Jasper Hugo 2021-10-06 12:05:46 +07:00
parent 55aa89010c
commit a6089293a6
83 changed files with 20287 additions and 0 deletions

35
xmpp-parsers/Cargo.toml Normal file
View File

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

365
xmpp-parsers/ChangeLog Normal file
View File

@ -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 Jingles 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 chronos DateTime for JingleFTs date element.
- Use Jid for JingleS5Bs jid attribute.
* Improvements:
- Use more macros for common tasks.
- Add a constructor for Message and Presence.
- Implement std::fmt::Display and std::error::Error on our
error type.
- Fix DataForms serialisation.
- Fix roster group serialisation.
- Update libraries, notably chrono whose version 0.3.1 got
yanked.
Version 0.5.0:
2017-06-11 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
* New parsers/serialisers:
- Implementation of the roster management protocol defined in
RFC 6121 §2.
- Implementation of PubSub events (except collections).
- Early implementation of MUC.
* Breaking changes:
- Rename presence enums to make them easier to use.
* Improvements:
- Make hashes comparable and hashable.
- Make data forms embeddable easily into minidom
Element::builder.
Version 0.4.0:
2017-05-28 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
* Incompatible changes:
- Receipts now make the id optional, as per the specification.
- Hashes now expose their raw binary value, instead of staying
base64-encoded.
- Parse dates (XEP-0082) in delayed delivery (XEP-0203) and
last user interaction (XEP-0319), using the chrono crate.
* Improvements:
- Removal of most of the remaining clones, the only ones left
are due to minidom not exposing a draining iterator over the
children.
- Finish to parse all of the attributes using get_attr!().
- More attribute checks.
- Split more parsers into one parser per element.
- Rely on minidom 0.4.3 to serialise more standard types
automatically.
- Implement forgotten serialisation for data forms (XEP-0004).
- Implement legacy capabilities (XEP-0115) for compatibility
with older software.
Version 0.3.0:
2017-05-23 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
* Big changes:
- All parsers and serialisers now consume their argument, this
makes the API way more efficient, but you will have to clone
before passing your structs in it if you want to keep them.
- Payloads of stanzas are not parsed automatically anymore, to
let applications which want to forward them as-is do so more
easily. Parsing now always succeeds on unknown payloads, it
just puts them into an Unknown value containing the existing
minidom Element.
* New parsers/serialisers:
- Last User Interaction in Presence, XEP-0319.
* Improved parsers/serialisers:
- Message now supports subject, bodies and threads as per
RFC 6121 §5.2.
- Replace most attribute reads with a nice macro.
- Use enums for more enum-like things, for example Algo in
Hash, or FieldType in DataForm.
- Wire up stanza-id and origin-id to MessagePayload.
- Wire up MAM elements to message and iq payloads.
- Changes in the RSM API.
- Add support for more data forms elements, but still not the
complete set.
- Thanks to minidom 0.3.1, check for explicitly disallowed
extra attributes in some elements.
* Crate updates:
- minidom 0.4.1
Version 0.2.0:
2017-05-06 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
* New parsers/serialisers:
- Stanza error, as per RFC 6120 §8.3.
- Jingle SOCKS5 Transport, XEP-0260.
* Incompatible changes:
- Parsers and serialisers now all implement TryFrom<Element>
and Into<Element>, instead of the old parse_* and serialise_*
functions.
- Presence has got an overhaul, it now hosts show, statuses and
priority in its struct. The status module has also been
dropped.
- Message now supports multiple bodies, each in a different
language. The body module has also been dropped.
- Iq now gets a proper StanzaError when the type is error.
- Fix bogus Jingle payload, which was requiring both
description and transport.
* Crate updates:
- minidom 0.3.0
Version 0.1.0:
2017-04-29 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
* Implement many extensions.

373
xmpp-parsers/LICENSE Normal file
View File

@ -0,0 +1,373 @@
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

684
xmpp-parsers/doap.xml Normal file
View File

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

View File

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

View File

@ -0,0 +1,72 @@
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
use crate::message::MessagePayload;
generate_empty_element!(
/// Requests the attention of the recipient.
Attention,
"attention",
ATTENTION
);
impl MessagePayload for Attention {}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(not(feature = "disable-validation"))]
use crate::util::error::Error;
use crate::Element;
use std::convert::TryFrom;
#[test]
fn test_size() {
assert_size!(Attention, 0);
}
#[test]
fn test_simple() {
let elem: Element = "<attention xmlns='urn:xmpp:attention:0'/>".parse().unwrap();
Attention::try_from(elem).unwrap();
}
#[cfg(not(feature = "disable-validation"))]
#[test]
fn test_invalid_child() {
let elem: Element = "<attention xmlns='urn:xmpp:attention:0'><coucou/></attention>"
.parse()
.unwrap();
let error = Attention::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown child in attention element.");
}
#[cfg(not(feature = "disable-validation"))]
#[test]
fn test_invalid_attribute() {
let elem: Element = "<attention xmlns='urn:xmpp:attention:0' coucou=''/>"
.parse()
.unwrap();
let error = Attention::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown attribute in attention element.");
}
#[test]
fn test_serialise() {
let elem: Element = "<attention xmlns='urn:xmpp:attention:0'/>".parse().unwrap();
let attention = Attention;
let elem2: Element = attention.into();
assert_eq!(elem, elem2);
}
}

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

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

203
xmpp-parsers/src/bind.rs Normal file
View File

@ -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 clients full JID.
///
/// See https://xmpp.org/rfcs/rfc6120.html#bind
#[derive(Debug, Clone, PartialEq)]
pub struct BindResponse {
/// The full JID returned by the server for this client.
jid: FullJid,
}
impl IqResultPayload for BindResponse {}
impl From<BindResponse> for FullJid {
fn from(bind: BindResponse) -> FullJid {
bind.jid
}
}
impl From<BindResponse> for Jid {
fn from(bind: BindResponse) -> Jid {
Jid::Full(bind.jid)
}
}
impl TryFrom<Element> for BindResponse {
type Error = Error;
fn try_from(elem: Element) -> Result<BindResponse, Error> {
check_self!(elem, "bind", BIND);
check_no_attributes!(elem, "bind");
let mut jid = None;
for child in elem.children() {
if jid.is_some() {
return Err(Error::ParseError("Bind can only have one child."));
}
if child.is("jid", ns::BIND) {
check_no_attributes!(child, "jid");
check_no_children!(child, "jid");
jid = Some(FullJid::from_str(&child.text())?);
} else {
return Err(Error::ParseError("Unknown element in bind response."));
}
}
Ok(BindResponse {
jid: match jid {
None => {
return Err(Error::ParseError(
"Bind response must contain a jid element.",
))
}
Some(jid) => jid,
},
})
}
}
impl From<BindResponse> for Element {
fn from(bind: BindResponse) -> Element {
Element::builder("bind", ns::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 resourceprepd into “TM” here…
//assert_eq!(bind.resource.unwrap(), "HelloTM");
assert_eq!(bind.resource.unwrap(), "Hello™");
let elem: Element = "<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'><jid>coucou@linkmauve.fr/HelloTM</jid></bind>"
.parse()
.unwrap();
let bind = BindResponse::try_from(elem).unwrap();
assert_eq!(bind.jid, FullJid::new("coucou", "linkmauve.fr", "HelloTM"));
}
#[cfg(not(feature = "disable-validation"))]
#[test]
fn test_invalid_resource() {
let elem: Element = "<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'><resource attr='coucou'>resource</resource></bind>"
.parse()
.unwrap();
let error = BindQuery::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown attribute in resource element.");
let elem: Element = "<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'><resource><hello-world/>resource</resource></bind>"
.parse()
.unwrap();
let error = BindQuery::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown child in resource element.");
}
}

View File

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

182
xmpp-parsers/src/bob.rs Normal file
View File

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

View File

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

View File

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

342
xmpp-parsers/src/caps.rs Normal file
View File

@ -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 applications
/// [disco#info](../disco/struct.DiscoInfoResult.html).
///
/// Warning: This protocol is insecure, you may want to switch to
/// [ecaps2](../ecaps2/index.html) instead, see [this
/// email](https://mail.jabber.org/pipermail/security/2009-July/000812.html).
pub hash: Hash,
}
impl PresencePayload for Caps {}
impl TryFrom<Element> for Caps {
type Error = Error;
fn try_from(elem: Element) -> Result<Caps, Error> {
check_self!(elem, "c", CAPS, "caps");
check_no_children!(elem, "caps");
check_no_unknown_attributes!(elem, "caps", ["hash", "ver", "ext", "node"]);
let ver: String = get_attr!(elem, "ver", Required);
let hash = Hash {
algo: get_attr!(elem, "hash", Required),
hash: base64::decode(&ver)?,
};
Ok(Caps {
ext: get_attr!(elem, "ext", Option),
node: get_attr!(elem, "node", Required),
hash,
})
}
}
impl From<Caps> for Element {
fn from(caps: Caps) -> Element {
Element::builder("c", ns::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()
);
}
}

165
xmpp-parsers/src/carbons.rs Normal file
View File

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

View File

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

View File

@ -0,0 +1,100 @@
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
use crate::message::MessagePayload;
generate_element_enum!(
/// Enum representing chatstate elements part of the
/// `http://jabber.org/protocol/chatstates` namespace.
ChatState, "chatstate", CHATSTATES, {
/// `<active xmlns='http://jabber.org/protocol/chatstates'/>`
Active => "active",
/// `<composing xmlns='http://jabber.org/protocol/chatstates'/>`
Composing => "composing",
/// `<gone xmlns='http://jabber.org/protocol/chatstates'/>`
Gone => "gone",
/// `<inactive xmlns='http://jabber.org/protocol/chatstates'/>`
Inactive => "inactive",
/// `<paused xmlns='http://jabber.org/protocol/chatstates'/>`
Paused => "paused",
}
);
impl MessagePayload for ChatState {}
#[cfg(test)]
mod tests {
use super::*;
use crate::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));
}
}

View File

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

65
xmpp-parsers/src/csi.rs Normal file
View File

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

View File

@ -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 isnt present.
pub required: bool,
/// A list of allowed values.
pub options: Vec<Option_>,
/// The values provided for this field.
pub values: Vec<String>,
/// A list of media related to this field.
pub media: Vec<MediaElement>,
}
impl Field {
fn is_list(&self) -> bool {
self.type_ == FieldType::ListSingle || self.type_ == FieldType::ListMulti
}
}
impl TryFrom<Element> for Field {
type Error = Error;
fn try_from(elem: Element) -> Result<Field, Error> {
check_self!(elem, "field", DATA_FORMS);
check_no_unknown_attributes!(elem, "field", ["label", "type", "var"]);
let mut field = Field {
var: get_attr!(elem, "var", Required),
type_: get_attr!(elem, "type", Default),
label: get_attr!(elem, "label", Option),
required: false,
options: vec![],
values: vec![],
media: vec![],
};
for element in elem.children() {
if element.is("value", ns::DATA_FORMS) {
check_no_children!(element, "value");
check_no_attributes!(element, "value");
field.values.push(element.text());
} else if element.is("required", ns::DATA_FORMS) {
if field.required {
return Err(Error::ParseError("More than one required element."));
}
check_no_children!(element, "required");
check_no_attributes!(element, "required");
field.required = true;
} else if element.is("option", ns::DATA_FORMS) {
if !field.is_list() {
return Err(Error::ParseError("Option element found in non-list field."));
}
let option = Option_::try_from(element.clone())?;
field.options.push(option);
} else if element.is("media", ns::MEDIA_ELEMENT) {
let media_element = MediaElement::try_from(element.clone())?;
field.media.push(media_element);
} else {
return Err(Error::ParseError(
"Field child isnt a value, option or media element.",
));
}
}
Ok(field)
}
}
impl From<Field> for Element {
fn from(field: Field) -> Element {
Element::builder("field", ns::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."
);
}
}

137
xmpp-parsers/src/date.rs Normal file
View File

@ -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};
// DateTimes size doesnt depend on the architecture.
#[test]
fn test_size() {
assert_size!(DateTime, 16);
}
#[test]
fn test_simple() {
let date: DateTime = "2002-09-10T23:08:25Z".parse().unwrap();
assert_eq!(date.0.year(), 2002);
assert_eq!(date.0.month(), 9);
assert_eq!(date.0.day(), 10);
assert_eq!(date.0.hour(), 23);
assert_eq!(date.0.minute(), 08);
assert_eq!(date.0.second(), 25);
assert_eq!(date.0.nanosecond(), 0);
assert_eq!(date.0.timezone(), FixedOffset::east(0));
}
#[test]
fn test_invalid_date() {
// There is no thirteenth month.
let error = DateTime::from_str("2017-13-01T12:23:34Z").unwrap_err();
let message = match error {
Error::ChronoParseError(string) => string,
_ => panic!(),
};
assert_eq!(message.to_string(), "input is out of range");
// Timezone ≥24:00 arent allowed.
let error = DateTime::from_str("2017-05-27T12:11:02+25:00").unwrap_err();
let message = match error {
Error::ChronoParseError(string) => string,
_ => panic!(),
};
assert_eq!(message.to_string(), "input is out of range");
// Timezone without the : separator arent allowed.
let error = DateTime::from_str("2017-05-27T12:11:02+0100").unwrap_err();
let message = match error {
Error::ChronoParseError(string) => string,
_ => panic!(),
};
assert_eq!(message.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 well want to support this one, as per XEP-0082 §4.
let error = DateTime::from_str("20170527T12:11:02+01:00").unwrap_err();
let message = match error {
Error::ChronoParseError(string) => string,
_ => panic!(),
};
assert_eq!(message.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")));
}
}

119
xmpp-parsers/src/delay.rs Normal file
View File

@ -0,0 +1,119 @@
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
use crate::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);
}
}

452
xmpp-parsers/src/disco.rs Normal file
View File

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

481
xmpp-parsers/src/ecaps2.rs Normal file
View File

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

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

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

View File

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

274
xmpp-parsers/src/hashes.rs Normal file
View File

@ -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 cant be empty.")),
"sha-1" => Algo::Sha_1,
"sha-256" => Algo::Sha_256,
"sha-512" => Algo::Sha_512,
"sha3-256" => Algo::Sha3_256,
"sha3-512" => Algo::Sha3_512,
"blake2b-256" => Algo::Blake2b_256,
"blake2b-512" => Algo::Blake2b_512,
value => Algo::Unknown(value.to_owned()),
})
}
}
impl From<Algo> for String {
fn from(algo: Algo) -> String {
String::from(match algo {
Algo::Sha_1 => "sha-1",
Algo::Sha_256 => "sha-256",
Algo::Sha_512 => "sha-512",
Algo::Sha3_256 => "sha3-256",
Algo::Sha3_512 => "sha3-512",
Algo::Blake2b_256 => "blake2b-256",
Algo::Blake2b_512 => "blake2b-512",
Algo::Unknown(text) => return text,
})
}
}
impl IntoAttributeValue for Algo {
fn into_attribute_value(self) -> Option<String> {
Some(String::from(self))
}
}
generate_element!(
/// This element represents a hash of some data, defined by the hash
/// algorithm used and the computed value.
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.");
}
}

171
xmpp-parsers/src/ibb.rs Normal file
View File

@ -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 doesnt require any feedback, but in
/// practice it will be throttled by the servers on the way.
Message => "message",
}, Default = Iq);
generate_element!(
/// Starts an In-Band Bytestream session with the given parameters.
Open, "open", IBB,
attributes: [
/// Maximum size in bytes for each chunk.
block_size: Required<u16> = "block-size",
/// The identifier to be used to create a stream.
sid: Required<StreamId> = "sid",
/// Which stanza type to use to exchange data.
stanza: Default<Stanza> = "stanza",
]);
impl IqSetPayload for Open {}
generate_element!(
/// Exchange a chunk of data in an open stream.
Data, "data", IBB,
attributes: [
/// Sequence number of this chunk, must wraparound after 65535.
seq: Required<u16> = "seq",
/// The identifier of the stream on which data is being exchanged.
sid: Required<StreamId> = "sid"
],
text: (
/// Vector of bytes to be exchanged.
data: Base64<Vec<u8>>
)
);
impl IqSetPayload for Data {}
generate_element!(
/// Close an open stream.
Close, "close", IBB,
attributes: [
/// The identifier of the stream to be closed.
sid: Required<StreamId> = "sid",
]);
impl IqSetPayload for Close {}
#[cfg(test)]
mod tests {
use super::*;
use crate::util::error::Error;
use crate::Element;
use std::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.");
}
}

195
xmpp-parsers/src/ibr.rs Normal file
View File

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

146
xmpp-parsers/src/idle.rs Normal file
View File

@ -0,0 +1,146 @@
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
use crate::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 arent allowed.
let elem: Element = "<idle xmlns='urn:xmpp:idle:1' since='2017-05-27T12:11:02+25:00'/>"
.parse()
.unwrap();
let error = Idle::try_from(elem).unwrap_err();
let message = match error {
Error::ChronoParseError(string) => string,
_ => panic!(),
};
assert_eq!(message.to_string(), "input is out of range");
// Timezone without the : separator arent allowed.
let elem: Element = "<idle xmlns='urn:xmpp:idle:1' since='2017-05-27T12:11:02+0100'/>"
.parse()
.unwrap();
let error = Idle::try_from(elem).unwrap_err();
let message = match error {
Error::ChronoParseError(string) => string,
_ => panic!(),
};
assert_eq!(message.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 well want to support this one, as per XEP-0082 §4.
let elem: Element = "<idle xmlns='urn:xmpp:idle:1' since='20170527T12:11:02+01:00'/>"
.parse()
.unwrap();
let error = Idle::try_from(elem).unwrap_err();
let message = match error {
Error::ChronoParseError(string) => string,
_ => panic!(),
};
assert_eq!(message.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);
}
}

461
xmpp-parsers/src/iq.rs Normal file
View File

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

View File

@ -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 stringprepd/PRECISd JID.
JidPrepResponse, "jid", JID_PREP,
text: (
/// The JID.
jid: JidCodec<Jid>
)
);
impl IqResultPayload for JidPrepResponse {}
#[cfg(test)]
mod tests {
use super::*;
use crate::Element;
use 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")
);
}
}

911
xmpp-parsers/src/jingle.rs Normal file
View File

@ -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 isnt known at compile-time.
Unknown(Element),
}
impl TryFrom<Element> for Description {
type Error = Error;
fn try_from(elem: Element) -> Result<Description, Error> {
Ok(if elem.is("description", ns::JINGLE_RTP) {
Description::Rtp(RtpDescription::try_from(elem)?)
} else {
Description::Unknown(elem)
})
}
}
impl From<RtpDescription> for Description {
fn from(desc: RtpDescription) -> Description {
Description::Rtp(desc)
}
}
impl From<Description> for Element {
fn from(desc: Description) -> Element {
match desc {
Description::Rtp(desc) => desc.into(),
Description::Unknown(elem) => elem,
}
}
}
/// 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 isnt known at compile-time.
Unknown(Element),
}
impl TryFrom<Element> for Transport {
type Error = Error;
fn try_from(elem: Element) -> Result<Transport, Error> {
Ok(if elem.is("transport", ns::JINGLE_ICE_UDP) {
Transport::IceUdp(IceUdpTransport::try_from(elem)?)
} else if elem.is("transport", ns::JINGLE_IBB) {
Transport::Ibb(IbbTransport::try_from(elem)?)
} else if elem.is("transport", ns::JINGLE_S5B) {
Transport::Socks5(Socks5Transport::try_from(elem)?)
} else {
Transport::Unknown(elem)
})
}
}
impl From<IceUdpTransport> for Transport {
fn from(transport: IceUdpTransport) -> Transport {
Transport::IceUdp(transport)
}
}
impl From<IbbTransport> for Transport {
fn from(transport: IbbTransport) -> Transport {
Transport::Ibb(transport)
}
}
impl From<Socks5Transport> for Transport {
fn from(transport: Socks5Transport) -> Transport {
Transport::Socks5(transport)
}
}
impl From<Transport> for Element {
fn from(transport: Transport) -> Element {
match transport {
Transport::IceUdp(transport) => transport.into(),
Transport::Ibb(transport) => transport.into(),
Transport::Socks5(transport) => transport.into(),
Transport::Unknown(elem) => elem,
}
}
}
generate_element!(
/// Describes a sessions 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 doesnt 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 initiators JID.
pub fn with_initiator(mut self, initiator: Jid) -> Jingle {
self.initiator = Some(initiator);
self
}
/// Set the responders JID.
pub fn with_responder(mut self, responder: Jid) -> Jingle {
self.responder = Some(responder);
self
}
/// Add a content to this Jingle container.
pub fn add_content(mut self, content: Content) -> Jingle {
self.contents.push(content);
self
}
/// Set the reason in this Jingle container.
pub fn set_reason(mut self, 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 doesnt contain a valid reason.");
let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><a/></reason></jingle>".parse().unwrap();
let error = Jingle::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown reason.");
let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><a xmlns='http://www.w3.org/1999/xhtml'/></reason></jingle>".parse().unwrap();
let error = Jingle::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Reason contains a foreign element.");
let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><decline/></reason><reason/></jingle>".parse().unwrap();
let error = Jingle::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Jingle must not have more than one reason.");
let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><decline/><text/><text/></reason></jingle>".parse().unwrap();
let error = Jingle::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Text element present twice for the same xml:lang.");
}
#[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);
}
}

View File

@ -0,0 +1,112 @@
// Copyright (c) 2019 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
use 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 isnt used, as per the XEP.
Holdconn => "holdconn",
*/
}
);
// TODO: use a hashes::Hash instead of two different fields here.
generate_element!(
/// Fingerprint of the key used for a DTLS handshake.
Fingerprint, "fingerprint", JINGLE_DTLS,
attributes: [
/// The hash algorithm used for this fingerprint.
hash: Required<Algo> = "hash",
/// Indicates which of the end points should initiate the TCP connection establishment.
setup: 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
]
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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 doesnt contain a description.",
))?,
}
}
"retract" => JingleMI::Retract(check_empty_and_get_sid(elem)?),
"accept" => JingleMI::Accept(check_empty_and_get_sid(elem)?),
"proceed" => JingleMI::Proceed(check_empty_and_get_sid(elem)?),
"reject" => JingleMI::Reject(check_empty_and_get_sid(elem)?),
_ => return Err(Error::ParseError("This is not a Jingle message element.")),
})
}
}
impl From<JingleMI> for Element {
fn from(jingle_mi: JingleMI) -> Element {
match jingle_mi {
JingleMI::Propose { sid, description } => {
Element::builder("propose", ns::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.");
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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 cant connect to the SOCKS5 proxy.
ProxyError,
/// XXX: Invalid, should not be found in the wild.
None,
}
/// Describes a Jingle transport using a direct or proxied connection.
#[derive(Debug, Clone, 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);
}
}

View File

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

229
xmpp-parsers/src/lib.rs Normal file
View File

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

296
xmpp-parsers/src/mam.rs Normal file
View File

@ -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 nodes archive.
node: Option<NodeName> = "node"
],
children: [
/// Used for filtering the results.
form: Option<DataForm> = ("x", DATA_FORMS) => DataForm,
/// Used for paging through results.
set: Option<SetQuery> = ("set", RSM) => SetQuery
]
);
impl IqGetPayload for Query {}
impl IqSetPayload for Query {}
impl IqResultPayload for Query {}
generate_element!(
/// The wrapper around forwarded stanzas.
Result_, "result", MAM,
attributes: [
/// The stanza-id under which the archive stored this stanza.
id: Required<String> = "id",
/// The same queryid as the one requested in the
/// [query](struct.Query.html).
queryid: Option<QueryId> = "queryid",
],
children: [
/// The actual stanza being forwarded.
forwarded: Required<Forwarded> = ("forwarded", FORWARD) => Forwarded
]
);
impl MessagePayload for Result_ {}
generate_attribute!(
/// True when the end of a MAM query has been reached.
Complete,
"complete",
bool
);
generate_element!(
/// Notes the end of a page in a query.
Fin, "fin", MAM,
attributes: [
/// True when the end of a MAM query has been reached.
complete: Default<Complete> = "complete",
],
children: [
/// Describes the current page, it should contain at least [first]
/// (with an [index]) and [last], and generally [count].
///
/// [first]: ../rsm/struct.SetResult.html#structfield.first
/// [index]: ../rsm/struct.SetResult.html#structfield.first_index
/// [last]: ../rsm/struct.SetResult.html#structfield.last
/// [count]: ../rsm/struct.SetResult.html#structfield.count
set: Required<SetResult> = ("set", RSM) => SetResult
]
);
impl IqResultPayload for Fin {}
#[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);
}
}

View File

@ -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 users [roster](../roster/index.html).
Roster => "roster",
}
);
/// Controls the archiving preferences of the user.
#[derive(Debug, Clone)]
pub struct Prefs {
/// The default preference for JIDs in neither
/// [always](#structfield.always) or [never](#structfield.never) lists.
pub default_: DefaultPrefs,
/// The set of JIDs for which to always store messages in the archive.
pub always: Vec<Jid>,
/// The set of JIDs for which to never store messages in the archive.
pub never: Vec<Jid>,
}
impl IqGetPayload for Prefs {}
impl IqSetPayload for Prefs {}
impl IqResultPayload for Prefs {}
impl TryFrom<Element> for Prefs {
type Error = Error;
fn try_from(elem: Element) -> Result<Prefs, Error> {
check_self!(elem, "prefs", MAM);
check_no_unknown_attributes!(elem, "prefs", ["default"]);
let mut always = vec![];
let mut never = vec![];
for child in elem.children() {
if child.is("always", ns::MAM) {
for jid_elem in child.children() {
if !jid_elem.is("jid", ns::MAM) {
return Err(Error::ParseError("Invalid jid element in always."));
}
always.push(jid_elem.text().parse()?);
}
} else if child.is("never", ns::MAM) {
for jid_elem in child.children() {
if !jid_elem.is("jid", ns::MAM) {
return Err(Error::ParseError("Invalid jid element in never."));
}
never.push(jid_elem.text().parse()?);
}
} else {
return Err(Error::ParseError("Unknown child in prefs element."));
}
}
let default_ = get_attr!(elem, "default", Required);
Ok(Prefs {
default_,
always,
never,
})
}
}
fn serialise_jid_list(name: &str, jids: Vec<Jid>) -> ::std::option::IntoIter<Node> {
if jids.is_empty() {
None.into_iter()
} else {
Some(
Element::builder(name, ns::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);
}
}

View File

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

407
xmpp-parsers/src/message.rs Normal file
View File

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

View File

@ -0,0 +1,99 @@
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
use crate::message::MessagePayload;
generate_element!(
/// Defines that the message containing this payload should replace a
/// previous message, identified by the id.
Replace, "replace", MESSAGE_CORRECT,
attributes: [
/// The 'id' attribute of the message getting corrected.
id: Required<String> = "id",
]
);
impl MessagePayload for Replace {}
#[cfg(test)]
mod tests {
use super::*;
use crate::util::error::Error;
use crate::Element;
use std::convert::TryFrom;
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(Replace, 12);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(Replace, 24);
}
#[test]
fn test_simple() {
let elem: Element = "<replace xmlns='urn:xmpp:message-correct:0' id='coucou'/>"
.parse()
.unwrap();
Replace::try_from(elem).unwrap();
}
#[cfg(not(feature = "disable-validation"))]
#[test]
fn test_invalid_attribute() {
let elem: Element = "<replace xmlns='urn:xmpp:message-correct:0' coucou=''/>"
.parse()
.unwrap();
let error = Replace::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown attribute in replace element.");
}
#[test]
fn test_invalid_child() {
let elem: Element = "<replace xmlns='urn:xmpp:message-correct:0'><coucou/></replace>"
.parse()
.unwrap();
let error = Replace::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown child in replace element.");
}
#[test]
fn test_invalid_id() {
let elem: Element = "<replace xmlns='urn:xmpp:message-correct:0'/>"
.parse()
.unwrap();
let error = Replace::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Required attribute 'id' missing.");
}
#[test]
fn test_serialise() {
let elem: Element = "<replace xmlns='urn:xmpp:message-correct:0' id='coucou'/>"
.parse()
.unwrap();
let replace = Replace {
id: String::from("coucou"),
};
let elem2 = replace.into();
assert_eq!(elem, elem2);
}
}

398
xmpp-parsers/src/mix.rs Normal file
View File

@ -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 users 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 users 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 channels 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, doesnt 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\"/>"
);
}
}

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

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

View File

@ -0,0 +1,14 @@
// Copyright (c) 2017 Maxime “pep” Buquet <pep@bouah.net>
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
/// The http://jabber.org/protocol/muc protocol.
pub mod muc;
/// The http://jabber.org/protocol/muc#user protocol.
pub mod user;
pub use self::muc::Muc;
pub use self::user::MucUser;

194
xmpp-parsers/src/muc/muc.rs Normal file
View File

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

View File

@ -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 isnt tied to its
/// presence in it.
Affiliation, "affiliation", {
/// The user who created the room, or who got appointed by its creator
/// to be their equal.
Owner => "owner",
/// A user who has been empowered by an owner to do administrative
/// operations.
Admin => "admin",
/// A user who is whitelisted to speak in moderated rooms, or to join a
/// member-only room.
Member => "member",
/// A user who has been banned from this room.
Outcast => "outcast",
/// A normal participant.
None => "none",
}, Default = None
);
generate_attribute!(
/// The current role of an entity in a room, it can be changed by an owner
/// or an administrator but will be lost once they leave the room.
Role, "role", {
/// This user can kick other participants, as well as grant and revoke
/// them voice.
Moderator => "moderator",
/// A user who can speak in this room.
Participant => "participant",
/// A user who cannot speak in this room, and must request voice before
/// doing so.
Visitor => "visitor",
/// A user who is absent from the room.
None => "none",
}, Default = None
);
generate_element!(
/// An item representing a user in a room.
Item, "item", MUC_USER, attributes: [
/// The affiliation of this user with the room.
affiliation: Required<Affiliation> = "affiliation",
/// The real JID of this user, if you are allowed to see it.
jid: Option<FullJid> = "jid",
/// The current nickname of this user.
nick: Option<String> = "nick",
/// The current role of this user.
role: Required<Role> = "role",
], children: [
/// The actor affected by this item.
actor: Option<Actor> = ("actor", MUC_USER) => Actor,
/// Whether this continues a one-to-one discussion.
continue_: Option<Continue> = ("continue", MUC_USER) => Continue,
/// A reason for this item.
reason: Option<Reason> = ("reason", MUC_USER) => Reason
]
);
impl Item {
/// Creates a new item with the given affiliation and role.
pub fn new(affiliation: Affiliation, role: Role) -> Item {
Item {
affiliation,
role,
jid: None,
nick: None,
actor: None,
continue_: None,
reason: None,
}
}
}
generate_element!(
/// The main muc#user element.
MucUser, "x", MUC_USER, children: [
/// List of statuses applying to this item.
status: Vec<Status> = ("status", MUC_USER) => Status,
/// List of items.
items: Vec<Item> = ("item", MUC_USER) => Item
]
);
#[cfg(test)]
mod tests {
use super::*;
#[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);
}
}

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

@ -0,0 +1,79 @@
// Copyright (c) 2018 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
generate_elem_id!(
/// Represents a global, memorable, friendly or informal name chosen by a user.
Nick,
"nick",
NICK
);
#[cfg(test)]
mod tests {
use super::*;
#[cfg(not(feature = "disable-validation"))]
use crate::util::error::Error;
use crate::Element;
use std::convert::TryFrom;
#[cfg(target_pointer_width = "32")]
#[test]
fn test_size() {
assert_size!(Nick, 12);
}
#[cfg(target_pointer_width = "64")]
#[test]
fn test_size() {
assert_size!(Nick, 24);
}
#[test]
fn test_simple() {
let elem: Element = "<nick xmlns='http://jabber.org/protocol/nick'>Link Mauve</nick>"
.parse()
.unwrap();
let nick = Nick::try_from(elem).unwrap();
assert_eq!(&nick.0, "Link Mauve");
}
#[test]
fn test_serialise() {
let elem1 = Element::from(Nick(String::from("Link Mauve")));
let elem2: Element = "<nick xmlns='http://jabber.org/protocol/nick'>Link Mauve</nick>"
.parse()
.unwrap();
assert_eq!(elem1, elem2);
}
#[cfg(not(feature = "disable-validation"))]
#[test]
fn test_invalid() {
let elem: Element = "<nick xmlns='http://jabber.org/protocol/nick'><coucou/></nick>"
.parse()
.unwrap();
let error = Nick::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown child in nick element.");
}
#[cfg(not(feature = "disable-validation"))]
#[test]
fn test_invalid_attribute() {
let elem: Element = "<nick xmlns='http://jabber.org/protocol/nick' coucou=''/>"
.parse()
.unwrap();
let error = Nick::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown attribute in nick element.");
}
}

271
xmpp-parsers/src/ns.rs Normal file
View File

@ -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 isnt enabled.
#[cfg(not(feature = "component"))]
pub const DEFAULT_NS: &str = JABBER_CLIENT;
/// Alias for the main namespace of the stream, that is
/// "jabber:component:accept" when the component feature is enabled.
#[cfg(feature = "component")]
pub const DEFAULT_NS: &str = COMPONENT_ACCEPT;
/// 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";

View File

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

119
xmpp-parsers/src/openpgp.rs Normal file
View File

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

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

@ -0,0 +1,71 @@
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
// Copyright (c) 2017 Maxime “pep” Buquet <pep@bouah.net>
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
use crate::iq::IqGetPayload;
generate_empty_element!(
/// Represents a ping to the recipient, which must be answered with an
/// empty `<iq/>` or with an error.
Ping,
"ping",
PING
);
impl IqGetPayload for Ping {}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(not(feature = "disable-validation"))]
use crate::util::error::Error;
use crate::Element;
use std::convert::TryFrom;
#[test]
fn test_size() {
assert_size!(Ping, 0);
}
#[test]
fn test_simple() {
let elem: Element = "<ping xmlns='urn:xmpp:ping'/>".parse().unwrap();
Ping::try_from(elem).unwrap();
}
#[test]
fn test_serialise() {
let elem1 = Element::from(Ping);
let elem2: Element = "<ping xmlns='urn:xmpp:ping'/>".parse().unwrap();
assert_eq!(elem1, elem2);
}
#[cfg(not(feature = "disable-validation"))]
#[test]
fn test_invalid() {
let elem: Element = "<ping xmlns='urn:xmpp:ping'><coucou/></ping>"
.parse()
.unwrap();
let error = Ping::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown child in ping element.");
}
#[cfg(not(feature = "disable-validation"))]
#[test]
fn test_invalid_attribute() {
let elem: Element = "<ping xmlns='urn:xmpp:ping' coucou=''/>".parse().unwrap();
let error = Ping::try_from(elem).unwrap_err();
let message = match error {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Unknown attribute in ping element.");
}
}

View File

@ -0,0 +1,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 [XMPPCORE]).
Error,
/// A request for an entity's current presence; SHOULD be generated only by
/// a server on behalf of a user.
Probe,
/// The sender wishes to subscribe to the recipient's presence.
Subscribe,
/// The sender has allowed the recipient to receive their presence.
Subscribed,
/// The sender is no longer available for communication.
Unavailable,
/// The sender is unsubscribing from the receiver's presence.
Unsubscribe,
/// The subscription request has been denied or a previously granted
/// subscription has been canceled.
Unsubscribed,
}
impl Default for Type {
fn default() -> Type {
Type::None
}
}
impl FromStr for Type {
type Err = Error;
fn from_str(s: &str) -> Result<Type, Error> {
Ok(match s {
"error" => Type::Error,
"probe" => Type::Probe,
"subscribe" => Type::Subscribe,
"subscribed" => Type::Subscribed,
"unavailable" => Type::Unavailable,
"unsubscribe" => Type::Unsubscribe,
"unsubscribed" => Type::Unsubscribed,
_ => {
return Err(Error::ParseError(
"Invalid 'type' attribute on presence element.",
));
}
})
}
}
impl IntoAttributeValue for Type {
fn into_attribute_value(self) -> Option<String> {
Some(
match self {
Type::None => return None,
Type::Error => "error",
Type::Probe => "probe",
Type::Subscribe => "subscribe",
Type::Subscribed => "subscribed",
Type::Unavailable => "unavailable",
Type::Unsubscribe => "unsubscribe",
Type::Unsubscribed => "unsubscribed",
}
.to_owned(),
)
}
}
/// The main structure representing the `<presence/>` stanza.
#[derive(Debug, Clone)]
pub struct Presence {
/// The sender of this presence.
pub from: Option<Jid>,
/// The recipient of this presence.
pub to: Option<Jid>,
/// The identifier, unique on this stream, of this stanza.
pub id: Option<String>,
/// The type of this presence stanza.
pub type_: Type,
/// The availability of the sender of this presence.
pub show: Option<Show>,
/// A localised list of statuses defined in this presence.
pub statuses: BTreeMap<Lang, Status>,
/// The senders resource priority, if negative it wont receive messages
/// that havent been directed to it.
pub priority: Priority,
/// A list of payloads contained in this presence.
pub payloads: Vec<Element>,
}
impl Presence {
/// Create a new presence of this type.
pub fn new(type_: Type) -> Presence {
Presence {
from: None,
to: None,
id: None,
type_,
show: None,
statuses: BTreeMap::new(),
priority: 0i8,
payloads: vec![],
}
}
/// Set the emitter of this presence, this should only be useful for
/// servers and components, as clients can only send presences from their
/// own resource (which is implicit).
pub fn with_from<J: Into<Jid>>(mut self, from: J) -> Presence {
self.from = Some(from.into());
self
}
/// Set the recipient of this presence, this is only useful for directed
/// presences.
pub fn with_to<J: Into<Jid>>(mut self, to: J) -> Presence {
self.to = Some(to.into());
self
}
/// Set the identifier for this presence.
pub fn with_id(mut self, id: String) -> Presence {
self.id = Some(id);
self
}
/// Set the availability information of this presence.
pub fn with_show(mut self, show: Show) -> Presence {
self.show = Some(show);
self
}
/// Set the priority of this presence.
pub fn with_priority(mut self, priority: i8) -> Presence {
self.priority = priority;
self
}
/// Set the payloads of this presence.
pub fn with_payloads(mut self, payloads: Vec<Element>) -> Presence {
self.payloads = payloads;
self
}
/// Set the availability information of this presence.
pub fn set_status<L, S>(&mut self, lang: L, status: S)
where
L: Into<Lang>,
S: Into<Status>,
{
self.statuses.insert(lang.into(), status.into());
}
/// Add a payload to this presence.
pub fn add_payload<P: PresencePayload>(&mut self, payload: P) {
self.payloads.push(payload.into());
}
}
impl TryFrom<Element> for Presence {
type Error = Error;
fn try_from(root: Element) -> Result<Presence, Error> {
check_self!(root, "presence", DEFAULT_NS);
let mut show = None;
let mut priority = None;
let mut presence = Presence {
from: get_attr!(root, "from", Option),
to: get_attr!(root, "to", Option),
id: get_attr!(root, "id", Option),
type_: get_attr!(root, "type", Default),
show: None,
statuses: BTreeMap::new(),
priority: 0i8,
payloads: vec![],
};
for elem in root.children() {
if elem.is("show", ns::DEFAULT_NS) {
if show.is_some() {
return Err(Error::ParseError(
"More than one show element in a presence.",
));
}
check_no_attributes!(elem, "show");
check_no_children!(elem, "show");
show = Some(Show::from_str(elem.text().as_ref())?);
} else if elem.is("status", ns::DEFAULT_NS) {
check_no_unknown_attributes!(elem, "status", ["xml:lang"]);
check_no_children!(elem, "status");
let lang = get_attr!(elem, "xml:lang", Default);
if presence.statuses.insert(lang, elem.text()).is_some() {
return Err(Error::ParseError(
"Status element present twice for the same xml:lang.",
));
}
} else if elem.is("priority", ns::DEFAULT_NS) {
if priority.is_some() {
return Err(Error::ParseError(
"More than one priority element in a presence.",
));
}
check_no_attributes!(elem, "priority");
check_no_children!(elem, "priority");
priority = Some(Priority::from_str(elem.text().as_ref())?);
} else {
presence.payloads.push(elem.clone());
}
}
presence.show = show;
if let Some(priority) = priority {
presence.priority = priority;
}
Ok(presence)
}
}
impl From<Presence> for Element {
fn from(presence: Presence) -> Element {
Element::builder("presence", ns::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"));
}
}

View File

@ -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 nodes configuration changed.
Configuration {
/// The node affected.
node: NodeName,
/// The new configuration of this node.
form: Option<DataForm>,
},
/// This node has been deleted, with an optional redirect to another node.
Delete {
/// The node affected.
node: NodeName,
/// The xmpp: URI of another node replacing this one.
redirect: Option<String>,
},
/// Some items have been published on this node.
PublishedItems {
/// The node affected.
node: NodeName,
/// The list of published items.
items: Vec<Item>,
},
/// Some items have been removed from this node.
RetractedItems {
/// The node affected.
node: NodeName,
/// The list of retracted items.
items: Vec<ItemId>,
},
/// All items of this node just got removed at once.
Purge {
/// The node affected.
node: NodeName,
},
/// The users subscription to this node has changed.
Subscription {
/// The node affected.
node: NodeName,
/// The time at which this subscription will expire.
expiry: Option<DateTime>,
/// The JID of the user affected.
jid: Option<Jid>,
/// An identifier for this subscription.
subid: Option<SubscriptionId>,
/// The state of this subscription.
subscription: Option<Subscription>,
},
}
fn parse_items(elem: Element, node: NodeName) -> Result<PubSubEvent, Error> {
let mut is_retract = None;
let mut items = vec![];
let mut retracts = vec![];
for child in elem.children() {
if child.is("item", ns::PUBSUB_EVENT) {
match is_retract {
None => is_retract = Some(false),
Some(false) => (),
Some(true) => {
return Err(Error::ParseError(
"Mix of item and retract in items element.",
));
}
}
items.push(Item::try_from(child.clone())?);
} else if child.is("retract", ns::PUBSUB_EVENT) {
match is_retract {
None => is_retract = Some(true),
Some(true) => (),
Some(false) => {
return Err(Error::ParseError(
"Mix of item and retract in items element.",
));
}
}
check_no_children!(child, "retract");
check_no_unknown_attributes!(child, "retract", ["id"]);
let id = get_attr!(child, "id", Required);
retracts.push(id);
} else {
return Err(Error::ParseError("Invalid child in items element."));
}
}
Ok(match is_retract {
Some(false) => PubSubEvent::PublishedItems { node, items },
Some(true) => PubSubEvent::RetractedItems {
node,
items: retracts,
},
None => return Err(Error::ParseError("Missing children in items element.")),
})
}
impl TryFrom<Element> for PubSubEvent {
type Error = Error;
fn try_from(elem: Element) -> Result<PubSubEvent, Error> {
check_self!(elem, "event", PUBSUB_EVENT);
check_no_attributes!(elem, "event");
let mut payload = None;
for child in elem.children() {
let node = get_attr!(child, "node", Required);
if child.is("configuration", ns::PUBSUB_EVENT) {
let mut payloads = child.children().cloned().collect::<Vec<_>>();
let item = payloads.pop();
if !payloads.is_empty() {
return Err(Error::ParseError(
"More than a single payload in configuration element.",
));
}
let form = match item {
None => None,
Some(payload) => Some(DataForm::try_from(payload)?),
};
payload = Some(PubSubEvent::Configuration { node, form });
} else if child.is("delete", ns::PUBSUB_EVENT) {
let mut redirect = None;
for item in child.children() {
if item.is("redirect", ns::PUBSUB_EVENT) {
if redirect.is_some() {
return Err(Error::ParseError(
"More than one redirect in delete element.",
));
}
let uri = get_attr!(item, "uri", Required);
redirect = Some(uri);
} else {
return Err(Error::ParseError("Unknown child in delete element."));
}
}
payload = Some(PubSubEvent::Delete { node, redirect });
} else if child.is("items", ns::PUBSUB_EVENT) {
payload = Some(parse_items(child.clone(), node)?);
} else if child.is("purge", ns::PUBSUB_EVENT) {
check_no_children!(child, "purge");
payload = Some(PubSubEvent::Purge { node });
} else if child.is("subscription", ns::PUBSUB_EVENT) {
check_no_children!(child, "subscription");
payload = Some(PubSubEvent::Subscription {
node,
expiry: get_attr!(child, "expiry", Option),
jid: get_attr!(child, "jid", Option),
subid: get_attr!(child, "subid", Option),
subscription: get_attr!(child, "subscription", Option),
});
} else {
return Err(Error::ParseError("Unknown child in event element."));
}
}
Ok(payload.ok_or(Error::ParseError("No payload in event element."))?)
}
}
impl From<PubSubEvent> for Element {
fn from(event: PubSubEvent) -> Element {
let payload = match event {
PubSubEvent::Configuration { node, form } => {
Element::builder("configuration", ns::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);
}
}

View File

@ -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 users subscription to this node is still pending.
Pending => "pending",
/// The user is subscribed to this node.
Subscribed => "subscribed",
/// The users subscription to this node will only be valid once
/// configured.
Unconfigured => "unconfigured",
}, Default = None
);
generate_attribute!(
/// A list of possible affiliations to a node.
AffiliationAttribute, "affiliation", {
/// You are a member of this node, you can subscribe and retrieve items.
Member => "member",
/// You dont have a specific affiliation with this node, you can only subscribe to it.
None => "none",
/// You are banned from this node.
Outcast => "outcast",
/// You are an owner of this node, and can do anything with it.
Owner => "owner",
/// You are a publisher on this node, you can publish and retract items to it.
Publisher => "publisher",
/// You can publish and retract items on this node, but not subscribe or retrieve items.
PublishOnly => "publish-only",
}
);
/// 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> {}

View File

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

View File

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

View File

@ -0,0 +1,89 @@
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
use crate::message::MessagePayload;
generate_empty_element!(
/// Requests that this message is acked by the final recipient once
/// received.
Request,
"request",
RECEIPTS
);
impl MessagePayload for Request {}
generate_element!(
/// Notes that a previous message has correctly been received, it is
/// referenced by its 'id' attribute.
Received, "received", RECEIPTS,
attributes: [
/// The 'id' attribute of the received message.
id: Required<String> = "id",
]
);
impl MessagePayload for Received {}
#[cfg(test)]
mod tests {
use super::*;
use crate::ns;
use crate::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"));
}
}

313
xmpp-parsers/src/roster.rs Normal file
View File

@ -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 doesnt have any subscription to this contacts presence,
/// and neither does this contact.
None => "none",
/// Only this contact has a subscription with you, not the opposite.
From => "from",
/// Only you have a subscription with this contact, not the opposite.
To => "to",
/// Both you and your contact are subscribed to each others presence.
Both => "both",
/// In a roster set, this asks the server to remove this contact item
/// from your roster.
Remove => "remove",
}, Default = None
);
generate_attribute!(
/// The sub-state of subscription with a contact.
Ask, "ask", (
/// Pending sub-state of the 'none' subscription state.
Subscribe => "subscribe"
)
);
generate_element!(
/// Contact from the users contact list.
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.");
}
}

302
xmpp-parsers/src/rsm.rs Normal file
View File

@ -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 recipients defaults if None.
pub max: Option<usize>,
/// The UID after which to give results, or if None it is the element
/// “before” the first item, effectively an index of negative one.
pub after: Option<String>,
/// The UID before which to give results, or if None it starts with the
/// last page of the full set.
pub before: Option<String>,
/// Numerical index of the page (deprecated).
pub index: Option<usize>,
}
impl TryFrom<Element> for SetQuery {
type Error = Error;
fn try_from(elem: Element) -> Result<SetQuery, Error> {
check_self!(elem, "set", RSM, "RSM set");
let mut set = SetQuery {
max: None,
after: None,
before: None,
index: None,
};
for child in elem.children() {
if child.is("max", ns::RSM) {
if set.max.is_some() {
return Err(Error::ParseError("Set cant have more than one max."));
}
set.max = Some(child.text().parse()?);
} else if child.is("after", ns::RSM) {
if set.after.is_some() {
return Err(Error::ParseError("Set cant have more than one after."));
}
set.after = Some(child.text());
} else if child.is("before", ns::RSM) {
if set.before.is_some() {
return Err(Error::ParseError("Set cant have more than one before."));
}
set.before = Some(child.text());
} else if child.is("index", ns::RSM) {
if set.index.is_some() {
return Err(Error::ParseError("Set cant have more than one index."));
}
set.index = Some(child.text().parse()?);
} else {
return Err(Error::ParseError("Unknown child in set element."));
}
}
Ok(set)
}
}
impl From<SetQuery> for Element {
fn from(set: SetQuery) -> Element {
Element::builder("set", ns::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 cant have more than one first."));
}
set.first_index = get_attr!(child, "index", Option);
set.first = Some(child.text());
} else if child.is("last", ns::RSM) {
if set.last.is_some() {
return Err(Error::ParseError("Set cant have more than one last."));
}
set.last = Some(child.text());
} else if child.is("count", ns::RSM) {
if set.count.is_some() {
return Err(Error::ParseError("Set cant have more than one count."));
}
set.count = Some(child.text().parse()?);
} else {
return Err(Error::ParseError("Unknown child in set element."));
}
}
Ok(set)
}
}
impl From<SetResult> for Element {
fn from(set: SetResult) -> Element {
let first = set.first.clone().map(|first| {
Element::builder("first", ns::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);
}
}

301
xmpp-parsers/src/sasl.rs Normal file
View File

@ -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 clients response to the
/// servers [challenge](struct.Challenge.html).
Response, "response", SASL,
text: (
/// The response data.
data: Base64<Vec<u8>>
)
);
generate_empty_element!(
/// Sent by the client at any point after [auth](struct.Auth.html) if it
/// wants to cancel the current authentication process.
Abort,
"abort",
SASL
);
generate_element!(
/// Sent by the server on SASL success.
Success, "success", SASL,
text: (
/// Possible data sent on success.
data: Base64<Vec<u8>>
)
);
generate_element_enum!(
/// List of possible failure conditions for SASL.
DefinedCondition, "defined-condition", SASL, {
/// The client aborted the authentication with
/// [abort](struct.Abort.html).
Aborted => "aborted",
/// The account the client is trying to authenticate against has been
/// disabled.
AccountDisabled => "account-disabled",
/// The credentials for this account have expired.
CredentialsExpired => "credentials-expired",
/// You must enable StartTLS or use direct TLS before using this
/// authentication mechanism.
EncryptionRequired => "encryption-required",
/// The base64 data sent by the client is invalid.
IncorrectEncoding => "incorrect-encoding",
/// The authzid provided by the client is invalid.
InvalidAuthzid => "invalid-authzid",
/// The client tried to use an invalid mechanism, or none.
InvalidMechanism => "invalid-mechanism",
/// The client sent a bad request.
MalformedRequest => "malformed-request",
/// The mechanism selected is weaker than what the server allows.
MechanismTooWeak => "mechanism-too-weak",
/// The credentials provided are invalid.
NotAuthorized => "not-authorized",
/// The server encountered an issue which may be fixed later, the
/// client should retry at some point.
TemporaryAuthFailure => "temporary-auth-failure",
}
);
type Lang = String;
/// Sent by the server on SASL failure.
#[derive(Debug, Clone)]
pub struct Failure {
/// One of the allowed defined-conditions for SASL.
pub defined_condition: DefinedCondition,
/// A human-readable explanation for the failure.
pub texts: BTreeMap<Lang, String>,
}
impl TryFrom<Element> for Failure {
type Error = Error;
fn try_from(root: Element) -> Result<Failure, Error> {
check_self!(root, "failure", SASL);
check_no_attributes!(root, "failure");
let mut defined_condition = None;
let mut texts = BTreeMap::new();
for child in root.children() {
if child.is("text", ns::SASL) {
check_no_unknown_attributes!(child, "text", ["xml:lang"]);
check_no_children!(child, "text");
let lang = get_attr!(child, "xml:lang", Default);
if texts.insert(lang, child.text()).is_some() {
return Err(Error::ParseError(
"Text element present twice for the same xml:lang in failure element.",
));
}
} else if child.has_ns(ns::SASL) {
if defined_condition.is_some() {
return Err(Error::ParseError(
"Failure must not have more than one defined-condition.",
));
}
check_no_attributes!(child, "defined-condition");
check_no_children!(child, "defined-condition");
let condition = match DefinedCondition::try_from(child.clone()) {
Ok(condition) => condition,
// TODO: do we really want to eat this error?
Err(_) => DefinedCondition::NotAuthorized,
};
defined_condition = Some(condition);
} else {
return Err(Error::ParseError("Unknown element in Failure."));
}
}
let defined_condition =
defined_condition.ok_or(Error::ParseError("Failure must have a defined-condition."))?;
Ok(Failure {
defined_condition,
texts,
})
}
}
impl From<Failure> for Element {
fn from(failure: Failure) -> Element {
Element::builder("failure", ns::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")
);
}
}

View File

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

247
xmpp-parsers/src/sm.rs Normal file
View File

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

View File

@ -0,0 +1,392 @@
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
use crate::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 [XMPPURI]).
Gone => "gone",
/// The server has experienced a misconfiguration or other internal
/// error that prevents it from processing the stanza; the associated
/// error type SHOULD be "cancel".
InternalServerError => "internal-server-error",
/// The addressed JID or item requested cannot be found; the associated
/// error type SHOULD be "cancel".
ItemNotFound => "item-not-found",
/// The sending entity has provided (e.g., during resource binding) or
/// communicated (e.g., in the 'to' address of a stanza) an XMPP
/// address or aspect thereof that violates the rules defined in
/// [XMPPADDR]; the associated error type SHOULD be "modify".
JidMalformed => "jid-malformed",
/// The recipient or server understands the request but cannot process
/// it because the request does not meet criteria defined by the
/// recipient or server (e.g., a request to subscribe to information
/// that does not simultaneously include configuration parameters
/// needed by the recipient); the associated error type SHOULD be
/// "modify".
NotAcceptable => "not-acceptable",
/// The recipient or server does not allow any entity to perform the
/// action (e.g., sending to entities at a blacklisted domain); the
/// associated error type SHOULD be "cancel".
NotAllowed => "not-allowed",
/// The sender needs to provide credentials before being allowed to
/// perform the action, or has provided improper credentials (the name
/// "not-authorized", which was borrowed from the "401 Unauthorized"
/// error of [HTTP], might lead the reader to think that this condition
/// relates to authorization, but instead it is typically used in
/// relation to authentication); the associated error type SHOULD be
/// "auth".
NotAuthorized => "not-authorized",
/// The entity has violated some local service policy (e.g., a message
/// contains words that are prohibited by the service) and the server
/// MAY choose to specify the policy in the <text/> element or in an
/// application-specific condition element; the associated error type
/// SHOULD be "modify" or "wait" depending on the policy being
/// violated.
PolicyViolation => "policy-violation",
/// The intended recipient is temporarily unavailable, undergoing
/// maintenance, etc.; the associated error type SHOULD be "wait".
RecipientUnavailable => "recipient-unavailable",
/// The recipient or server is redirecting requests for this
/// information to another entity, typically in a temporary fashion (as
/// opposed to the <gone/> error condition, which is used for permanent
/// addressing failures); the associated error type SHOULD be "modify"
/// and the error stanza SHOULD contain the alternate address in the
/// XML character data of the <redirect/> element (which MUST be a URI
/// or IRI with which the sender can communicate, typically an XMPP IRI
/// as specified in [XMPPURI]).
Redirect => "redirect",
/// The requesting entity is not authorized to access the requested
/// service because prior registration is necessary (examples of prior
/// registration include members-only rooms in XMPP multi-user chat
/// [XEP0045] and gateways to non-XMPP instant messaging services,
/// which traditionally required registration in order to use the
/// gateway [XEP0100]); the associated error type SHOULD be "auth".
RegistrationRequired => "registration-required",
/// A remote server or service specified as part or all of the JID of
/// the intended recipient does not exist or cannot be resolved (e.g.,
/// there is no _xmpp-server._tcp DNS SRV record, the A or AAAA
/// fallback resolution fails, or A/AAAA lookups succeed but there is
/// no response on the IANA-registered port 5269); the associated error
/// type SHOULD be "cancel".
RemoteServerNotFound => "remote-server-not-found",
/// A remote server or service specified as part or all of the JID of
/// the intended recipient (or needed to fulfill a request) was
/// resolved but communications could not be established within a
/// reasonable amount of time (e.g., an XML stream cannot be
/// established at the resolved IP address and port, or an XML stream
/// can be established but stream negotiation fails because of problems
/// with TLS, SASL, Server Dialback, etc.); the associated error type
/// SHOULD be "wait" (unless the error is of a more permanent nature,
/// e.g., the remote server is found but it cannot be authenticated or
/// it violates security policies).
RemoteServerTimeout => "remote-server-timeout",
/// The server or recipient is busy or lacks the system resources
/// necessary to service the request; the associated error type SHOULD
/// be "wait".
ResourceConstraint => "resource-constraint",
/// The server or recipient does not currently provide the requested
/// service; the associated error type SHOULD be "cancel".
ServiceUnavailable => "service-unavailable",
/// The requesting entity is not authorized to access the requested
/// service because a prior subscription is necessary (examples of
/// prior subscription include authorization to receive presence
/// information as defined in [XMPPIM] and opt-in data feeds for XMPP
/// publish-subscribe as defined in [XEP0060]); the associated error
/// type SHOULD be "auth".
SubscriptionRequired => "subscription-required",
/// The error condition is not one of those defined by the other
/// conditions in this list; any error type can be associated with this
/// condition, and it SHOULD NOT be used except in conjunction with an
/// application-specific condition.
UndefinedCondition => "undefined-condition",
/// The recipient or server understood the request but was not
/// expecting it at this time (e.g., the request was out of order); the
/// associated error type SHOULD be "wait" or "modify".
UnexpectedRequest => "unexpected-request",
}
);
type Lang = String;
/// The representation of a stanza error.
#[derive(Debug, Clone)]
pub struct StanzaError {
/// The type of this error.
pub type_: ErrorType,
/// The JID of the entity who set this error.
pub by: Option<Jid>,
/// One of the defined conditions for this error to happen.
pub defined_condition: DefinedCondition,
/// Human-readable description of this error.
pub texts: BTreeMap<Lang, String>,
/// A protocol-specific extension for this error.
pub other: Option<Element>,
}
impl MessagePayload for StanzaError {}
impl PresencePayload for StanzaError {}
impl StanzaError {
/// Create a new `<error/>` with the according content.
pub fn new<L, T>(
type_: ErrorType,
defined_condition: DefinedCondition,
lang: L,
text: T,
) -> StanzaError
where
L: Into<Lang>,
T: Into<String>,
{
StanzaError {
type_,
by: None,
defined_condition,
texts: {
let mut map = BTreeMap::new();
map.insert(lang.into(), text.into());
map
},
other: None,
}
}
}
impl TryFrom<Element> for StanzaError {
type Error = Error;
fn try_from(elem: Element) -> Result<StanzaError, Error> {
check_self!(elem, "error", DEFAULT_NS);
check_no_unknown_attributes!(elem, "error", ["type", "by"]);
let mut stanza_error = StanzaError {
type_: get_attr!(elem, "type", Required),
by: get_attr!(elem, "by", Option),
defined_condition: DefinedCondition::UndefinedCondition,
texts: BTreeMap::new(),
other: None,
};
let mut defined_condition = None;
for child in elem.children() {
if child.is("text", ns::XMPP_STANZAS) {
check_no_children!(child, "text");
check_no_unknown_attributes!(child, "text", ["xml:lang"]);
let lang = get_attr!(elem, "xml:lang", Default);
if stanza_error.texts.insert(lang, child.text()).is_some() {
return Err(Error::ParseError(
"Text element present twice for the same xml:lang.",
));
}
} else if child.has_ns(ns::XMPP_STANZAS) {
if defined_condition.is_some() {
return Err(Error::ParseError(
"Error must not have more than one defined-condition.",
));
}
check_no_children!(child, "defined-condition");
check_no_attributes!(child, "defined-condition");
let condition = DefinedCondition::try_from(child.clone())?;
defined_condition = Some(condition);
} else {
if stanza_error.other.is_some() {
return Err(Error::ParseError(
"Error must not have more than one other element.",
));
}
stanza_error.other = Some(child.clone());
}
}
stanza_error.defined_condition =
defined_condition.ok_or(Error::ParseError("Error must have a defined-condition."))?;
Ok(stanza_error)
}
}
impl From<StanzaError> for Element {
fn from(err: StanzaError) -> Element {
Element::builder("error", ns::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.");
}
}

View File

@ -0,0 +1,124 @@
// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
use crate::message::MessagePayload;
use jid::Jid;
generate_element!(
/// Gives the identifier a service has stamped on this stanza, often in
/// order to identify it inside of [an archive](../mam/index.html).
StanzaId, "stanza-id", SID,
attributes: [
/// The id associated to this stanza by another entity.
id: Required<String> = "id",
/// The entity who stamped this stanza-id.
by: Required<Jid> = "by",
]
);
impl MessagePayload for StanzaId {}
generate_element!(
/// A hack for MUC before version 1.31 to track a message which may have
/// its 'id' attribute changed.
OriginId, "origin-id", SID,
attributes: [
/// The id this client set for this stanza.
id: Required<String> = "id",
]
);
impl MessagePayload for OriginId {}
#[cfg(test)]
mod tests {
use super::*;
use crate::util::error::Error;
use crate::Element;
use 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);
}
}

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

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

115
xmpp-parsers/src/time.rs Normal file
View File

@ -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::*;
// DateTimes size doesnt depend on the architecture.
#[test]
fn test_size() {
assert_size!(TimeQuery, 0);
assert_size!(TimeResult, 16);
}
#[test]
fn parse_response() {
let elem: Element =
"<time xmlns='urn:xmpp:time'><tzo>-06:00</tzo><utc>2006-12-19T17:58:35Z</utc></time>"
.parse()
.unwrap();
let elem1 = elem.clone();
let time = TimeResult::try_from(elem).unwrap();
assert_eq!(time.0.timezone(), FixedOffset::west(6 * 3600));
assert_eq!(
time.0,
DateTime::from_str("2006-12-19T12:58:35-05:00").unwrap()
);
let elem2 = Element::from(time);
assert_eq!(elem1, elem2);
}
}

246
xmpp-parsers/src/tune.rs Normal file
View File

@ -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 cant have more than one artist."));
}
tune.artist = Some(Artist::try_from(child.clone())?);
} else if child.is("length", ns::TUNE) {
if tune.length.is_some() {
return Err(Error::ParseError("Tune cant have more than one length."));
}
tune.length = Some(Length::try_from(child.clone())?);
} else if child.is("rating", ns::TUNE) {
if tune.rating.is_some() {
return Err(Error::ParseError("Tune cant have more than one rating."));
}
tune.rating = Some(Rating::try_from(child.clone())?);
} else if child.is("source", ns::TUNE) {
if tune.source.is_some() {
return Err(Error::ParseError("Tune cant have more than one source."));
}
tune.source = Some(Source::try_from(child.clone())?);
} else if child.is("title", ns::TUNE) {
if tune.title.is_some() {
return Err(Error::ParseError("Tune cant have more than one title."));
}
tune.title = Some(Title::try_from(child.clone())?);
} else if child.is("track", ns::TUNE) {
if tune.track.is_some() {
return Err(Error::ParseError("Tune cant have more than one track."));
}
tune.track = Some(Track::try_from(child.clone())?);
} else if child.is("uri", ns::TUNE) {
if tune.uri.is_some() {
return Err(Error::ParseError("Tune cant have more than one uri."));
}
tune.uri = Some(Uri::try_from(child.clone())?);
} else {
return Err(Error::ParseError("Unknown element in User Tune."));
}
}
Ok(tune)
}
}
impl From<Tune> for Element {
fn from(tune: Tune) -> Element {
Element::builder("tune", ns::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())
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

642
xmpp-parsers/src/xhtml.rs Normal file
View File

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