diff --git a/parsers/ChangeLog b/parsers/ChangeLog
index 9768f2d2..39b83bac 100644
--- a/parsers/ChangeLog
+++ b/parsers/ChangeLog
@@ -1,6 +1,7 @@
Version xxx:
0000-00-00 Authors
* New parsers/serialisers:
+ - Bind 2 (XEP-0386)
- Fast Authentication Streamlining Tokens (XEP-0484)
* Improvements:
- Re-export the jid module entirely.
diff --git a/parsers/doap.xml b/parsers/doap.xml
index 5fa44830..07be4332 100644
--- a/parsers/doap.xml
+++ b/parsers/doap.xml
@@ -568,6 +568,14 @@
0.19.1
+
+
+
+ complete
+ 1.0.0
+ NEXT
+
+
diff --git a/parsers/src/bind2.rs b/parsers/src/bind2.rs
new file mode 100644
index 00000000..dd2a6b2d
--- /dev/null
+++ b/parsers/src/bind2.rs
@@ -0,0 +1,236 @@
+// Copyright (c) 2024 Emmanuel Gil Peyrot
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+use crate::mam;
+use crate::ns;
+use crate::Element;
+use xso::error::{Error, FromElementError};
+
+/// Represents the `` element, as sent by the server in SASL 2 to advertise which features
+/// can be enabled during the binding step.
+#[derive(Debug, Clone, PartialEq)]
+pub struct BindFeature {
+ /// The features that can be enabled by the client.
+ pub inline_features: Vec,
+}
+
+impl TryFrom for BindFeature {
+ type Error = FromElementError;
+
+ fn try_from(root: Element) -> Result {
+ check_self!(root, "bind", BIND2);
+ check_no_attributes!(root, "bind");
+
+ let mut inline = None;
+ for child in root.children() {
+ if child.is("inline", ns::BIND2) {
+ if inline.is_some() {
+ return Err(
+ Error::Other("Bind must not have more than one inline element.").into(),
+ );
+ }
+ check_no_attributes!(child, "inline");
+ inline = Some(child);
+ } else {
+ return Err(Error::Other("Unknown element in Bind.").into());
+ }
+ }
+
+ let mut inline_features = Vec::new();
+ if let Some(inline) = inline {
+ for child in inline.children() {
+ if child.is("feature", ns::BIND2) {
+ check_no_children!(child, "feature");
+ check_no_unknown_attributes!(child, "feature", ["var"]);
+ let var = get_attr!(child, "var", Required);
+ inline_features.push(var);
+ } else {
+ return Err(Error::Other("Unknown element in Inline.").into());
+ }
+ }
+ }
+
+ Ok(BindFeature { inline_features })
+ }
+}
+
+impl From for Element {
+ fn from(bind: BindFeature) -> Element {
+ Element::builder("bind", ns::BIND2)
+ .append_all(if bind.inline_features.is_empty() {
+ None
+ } else {
+ Some(
+ Element::builder("inline", ns::BIND2).append_all(
+ bind.inline_features
+ .into_iter()
+ .map(|var| Element::builder("feature", ns::BIND2).attr("var", var)),
+ ),
+ )
+ })
+ .build()
+ }
+}
+
+/// Represents a `` element, as sent by the client inline in the `` SASL 2
+/// element, to perform the binding at the same time as the authentication.
+#[derive(Debug, Clone, PartialEq)]
+pub struct BindQuery {
+ /// Short text string that typically identifies the software the user is using, mostly useful
+ /// for diagnostic purposes for users, operators and developers. This tag may be visible to
+ /// other entities on the XMPP network.
+ pub tag: Option,
+
+ /// Features that the client requests to be automatically enabled for its new session.
+ pub payloads: Vec,
+}
+
+impl TryFrom for BindQuery {
+ type Error = FromElementError;
+
+ fn try_from(root: Element) -> Result {
+ check_self!(root, "bind", BIND2);
+ check_no_attributes!(root, "bind");
+
+ let mut tag = None;
+ let mut payloads = Vec::new();
+ for child in root.children() {
+ if child.is("tag", ns::BIND2) {
+ if tag.is_some() {
+ return Err(
+ Error::Other("Bind must not have more than one tag element.").into(),
+ );
+ }
+ check_no_attributes!(child, "tag");
+ check_no_children!(child, "tag");
+ tag = Some(child.text());
+ } else {
+ payloads.push(child.clone());
+ }
+ }
+
+ Ok(BindQuery { tag, payloads })
+ }
+}
+
+impl From for Element {
+ fn from(bind: BindQuery) -> Element {
+ Element::builder("bind", ns::BIND2)
+ .append_all(
+ bind.tag
+ .map(|tag| Element::builder("tag", ns::BIND2).append(tag)),
+ )
+ .append_all(bind.payloads)
+ .build()
+ }
+}
+
+/// Represents a `` element, which tells the client its resource is bound, alongside other
+/// requests.
+#[derive(Debug, Clone, PartialEq)]
+pub struct Bound {
+ /// Indicates which messages got missed by this particuliar device, start is the oldest message
+ /// and end is the newest, before this connection.
+ pub mam_metadata: Option,
+
+ /// Additional payloads which happened during the binding process.
+ pub payloads: Vec,
+}
+
+impl TryFrom for Bound {
+ type Error = FromElementError;
+
+ fn try_from(root: Element) -> Result {
+ check_self!(root, "bound", BIND2);
+ check_no_attributes!(root, "bound");
+
+ let mut mam_metadata = None;
+ let mut payloads = Vec::new();
+ for child in root.children() {
+ if child.is("metadata", ns::MAM) {
+ if mam_metadata.is_some() {
+ return Err(
+ Error::Other("Bind must not have more than one metadata element.").into(),
+ );
+ }
+ mam_metadata = Some(mam::MetadataResponse::try_from(child.clone())?);
+ } else {
+ payloads.push(child.clone());
+ }
+ }
+
+ Ok(Bound {
+ mam_metadata,
+ payloads,
+ })
+ }
+}
+
+impl From for Element {
+ fn from(bound: Bound) -> Element {
+ Element::builder("bound", ns::BIND2)
+ .append_all(bound.mam_metadata)
+ .build()
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use minidom::Element;
+
+ #[cfg(target_pointer_width = "32")]
+ #[test]
+ fn test_size() {
+ assert_size!(BindFeature, 12);
+ assert_size!(BindQuery, 24);
+ assert_size!(Bound, 68);
+ }
+
+ #[cfg(target_pointer_width = "64")]
+ #[test]
+ fn test_size() {
+ assert_size!(BindFeature, 24);
+ assert_size!(BindQuery, 48);
+ assert_size!(Bound, 104);
+ }
+
+ #[test]
+ fn test_simple() {
+ // Example 1
+ let elem: Element =
+ ""
+ .parse()
+ .unwrap();
+ let bind = BindFeature::try_from(elem.clone()).unwrap();
+ assert_eq!(bind.inline_features.len(), 3);
+ assert_eq!(bind.inline_features[0], "urn:xmpp:carbons:2");
+ assert_eq!(bind.inline_features[1], "urn:xmpp:csi:0");
+ assert_eq!(bind.inline_features[2], "urn:xmpp:sm:3");
+ let elem2 = bind.into();
+ assert_eq!(elem, elem2);
+
+ // Example 2
+ let elem: Element = "AwesomeXMPP"
+ .parse()
+ .unwrap();
+ let bind = BindQuery::try_from(elem).unwrap();
+ assert_eq!(bind.tag.unwrap(), "AwesomeXMPP");
+ assert_eq!(bind.payloads.len(), 0);
+
+ // Example 3
+ let elem: Element = "AwesomeXMPP".parse().unwrap();
+ let bind = BindQuery::try_from(elem).unwrap();
+ assert_eq!(bind.tag.unwrap(), "AwesomeXMPP");
+ assert_eq!(bind.payloads.len(), 3);
+
+ // Example 4
+ let elem: Element = "".parse().unwrap();
+ let bound = Bound::try_from(elem).unwrap();
+ assert!(bound.mam_metadata.is_some());
+ assert_eq!(bound.payloads.len(), 0);
+ }
+}
diff --git a/parsers/src/lib.rs b/parsers/src/lib.rs
index cf2fd3b5..ac232538 100644
--- a/parsers/src/lib.rs
+++ b/parsers/src/lib.rs
@@ -252,6 +252,9 @@ pub mod eme;
/// XEP-0380: OMEMO Encryption (experimental version 0.3.0)
pub mod legacy_omemo;
+/// XEP-0386: Bind 2
+pub mod bind2;
+
/// XEP-0390: Entity Capabilities 2.0
pub mod ecaps2;
diff --git a/parsers/src/ns.rs b/parsers/src/ns.rs
index c279103c..1dd12111 100644
--- a/parsers/src/ns.rs
+++ b/parsers/src/ns.rs
@@ -275,6 +275,9 @@ pub const LEGACY_OMEMO_DEVICELIST: &str = "eu.siacs.conversations.axolotl.device
/// XEP-0384: OMEMO Encryption (experimental version 0.3.0)
pub const LEGACY_OMEMO_BUNDLES: &str = "eu.siacs.conversations.axolotl.bundles";
+/// XEP-0386: Bind 2
+pub const BIND2: &str = "urn:xmpp:bind:0";
+
/// XEP-0390: Entity Capabilities 2.0
pub const ECAPS2: &str = "urn:xmpp:caps";
/// XEP-0390: Entity Capabilities 2.0