bind2.rs

  1// Copyright (c) 2024 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
  2//
  3// This Source Code Form is subject to the terms of the Mozilla Public
  4// License, v. 2.0. If a copy of the MPL was not distributed with this
  5// file, You can obtain one at http://mozilla.org/MPL/2.0/.
  6
  7use crate::mam;
  8use crate::ns;
  9use minidom::Element;
 10use xso::error::{Error, FromElementError};
 11
 12/// Represents the `<bind/>` element, as sent by the server in SASL 2 to advertise which features
 13/// can be enabled during the binding step.
 14#[derive(Debug, Clone, PartialEq)]
 15pub struct BindFeature {
 16    /// The features that can be enabled by the client.
 17    pub inline_features: Vec<String>,
 18}
 19
 20impl TryFrom<Element> for BindFeature {
 21    type Error = FromElementError;
 22
 23    fn try_from(root: Element) -> Result<BindFeature, Self::Error> {
 24        check_self!(root, "bind", BIND2);
 25        check_no_attributes!(root, "bind");
 26
 27        let mut inline = None;
 28        for child in root.children() {
 29            if child.is("inline", ns::BIND2) {
 30                if inline.is_some() {
 31                    return Err(
 32                        Error::Other("Bind must not have more than one inline element.").into(),
 33                    );
 34                }
 35                check_no_attributes!(child, "inline");
 36                inline = Some(child);
 37            } else {
 38                return Err(Error::Other("Unknown element in Bind.").into());
 39            }
 40        }
 41
 42        let mut inline_features = Vec::new();
 43        if let Some(inline) = inline {
 44            for child in inline.children() {
 45                if child.is("feature", ns::BIND2) {
 46                    check_no_children!(child, "feature");
 47                    check_no_unknown_attributes!(child, "feature", ["var"]);
 48                    let var = get_attr!(child, "var", Required);
 49                    inline_features.push(var);
 50                } else {
 51                    return Err(Error::Other("Unknown element in Inline.").into());
 52                }
 53            }
 54        }
 55
 56        Ok(BindFeature { inline_features })
 57    }
 58}
 59
 60impl From<BindFeature> for Element {
 61    fn from(bind: BindFeature) -> Element {
 62        Element::builder("bind", ns::BIND2)
 63            .append_all(if bind.inline_features.is_empty() {
 64                None
 65            } else {
 66                Some(
 67                    Element::builder("inline", ns::BIND2).append_all(
 68                        bind.inline_features
 69                            .into_iter()
 70                            .map(|var| Element::builder("feature", ns::BIND2).attr("var", var)),
 71                    ),
 72                )
 73            })
 74            .build()
 75    }
 76}
 77
 78/// Represents a `<bind/>` element, as sent by the client inline in the `<authenticate/>` SASL 2
 79/// element, to perform the binding at the same time as the authentication.
 80#[derive(Debug, Clone, PartialEq)]
 81pub struct BindQuery {
 82    /// Short text string that typically identifies the software the user is using, mostly useful
 83    /// for diagnostic purposes for users, operators and developers.  This tag may be visible to
 84    /// other entities on the XMPP network.
 85    pub tag: Option<String>,
 86
 87    /// Features that the client requests to be automatically enabled for its new session.
 88    pub payloads: Vec<Element>,
 89}
 90
 91impl TryFrom<Element> for BindQuery {
 92    type Error = FromElementError;
 93
 94    fn try_from(root: Element) -> Result<BindQuery, Self::Error> {
 95        check_self!(root, "bind", BIND2);
 96        check_no_attributes!(root, "bind");
 97
 98        let mut tag = None;
 99        let mut payloads = Vec::new();
100        for child in root.children() {
101            if child.is("tag", ns::BIND2) {
102                if tag.is_some() {
103                    return Err(
104                        Error::Other("Bind must not have more than one tag element.").into(),
105                    );
106                }
107                check_no_attributes!(child, "tag");
108                check_no_children!(child, "tag");
109                tag = Some(child.text());
110            } else {
111                payloads.push(child.clone());
112            }
113        }
114
115        Ok(BindQuery { tag, payloads })
116    }
117}
118
119impl From<BindQuery> for Element {
120    fn from(bind: BindQuery) -> Element {
121        Element::builder("bind", ns::BIND2)
122            .append_all(
123                bind.tag
124                    .map(|tag| Element::builder("tag", ns::BIND2).append(tag)),
125            )
126            .append_all(bind.payloads)
127            .build()
128    }
129}
130
131/// Represents a `<bound/>` element, which tells the client its resource is bound, alongside other
132/// requests.
133#[derive(Debug, Clone, PartialEq)]
134pub struct Bound {
135    /// Indicates which messages got missed by this particular device, start is the oldest message
136    /// and end is the newest, before this connection.
137    pub mam_metadata: Option<mam::MetadataResponse>,
138
139    /// Additional payloads which happened during the binding process.
140    pub payloads: Vec<Element>,
141}
142
143impl TryFrom<Element> for Bound {
144    type Error = FromElementError;
145
146    fn try_from(root: Element) -> Result<Bound, Self::Error> {
147        check_self!(root, "bound", BIND2);
148        check_no_attributes!(root, "bound");
149
150        let mut mam_metadata = None;
151        let mut payloads = Vec::new();
152        for child in root.children() {
153            if child.is("metadata", ns::MAM) {
154                if mam_metadata.is_some() {
155                    return Err(
156                        Error::Other("Bind must not have more than one metadata element.").into(),
157                    );
158                }
159                mam_metadata = Some(mam::MetadataResponse::try_from(child.clone())?);
160            } else {
161                payloads.push(child.clone());
162            }
163        }
164
165        Ok(Bound {
166            mam_metadata,
167            payloads,
168        })
169    }
170}
171
172impl From<Bound> for Element {
173    fn from(bound: Bound) -> Element {
174        Element::builder("bound", ns::BIND2)
175            .append_all(bound.mam_metadata)
176            .build()
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    #[cfg(target_pointer_width = "32")]
185    #[test]
186    fn test_size() {
187        assert_size!(BindFeature, 12);
188        assert_size!(BindQuery, 24);
189        assert_size!(Bound, 68);
190    }
191
192    #[cfg(target_pointer_width = "64")]
193    #[test]
194    fn test_size() {
195        assert_size!(BindFeature, 24);
196        assert_size!(BindQuery, 48);
197        assert_size!(Bound, 104);
198    }
199
200    #[test]
201    fn test_simple() {
202        // Example 1
203        let elem: Element =
204            "<bind xmlns='urn:xmpp:bind:0'><inline><feature var='urn:xmpp:carbons:2'/><feature var='urn:xmpp:csi:0'/><feature var='urn:xmpp:sm:3'/></inline></bind>"
205                .parse()
206                .unwrap();
207        let bind = BindFeature::try_from(elem.clone()).unwrap();
208        assert_eq!(bind.inline_features.len(), 3);
209        assert_eq!(bind.inline_features[0], "urn:xmpp:carbons:2");
210        assert_eq!(bind.inline_features[1], "urn:xmpp:csi:0");
211        assert_eq!(bind.inline_features[2], "urn:xmpp:sm:3");
212        let elem2 = bind.into();
213        assert_eq!(elem, elem2);
214
215        // Example 2
216        let elem: Element = "<bind xmlns='urn:xmpp:bind:0'><tag>AwesomeXMPP</tag></bind>"
217            .parse()
218            .unwrap();
219        let bind = BindQuery::try_from(elem).unwrap();
220        assert_eq!(bind.tag.unwrap(), "AwesomeXMPP");
221        assert_eq!(bind.payloads.len(), 0);
222
223        // Example 3
224        let elem: Element = "<bind xmlns='urn:xmpp:bind:0'><tag>AwesomeXMPP</tag><enable xmlns='urn:xmpp:carbons:2'/><enable xmlns='urn:xmpp:sm:3'/><inactive xmlns='urn:xmpp:csi:0'/></bind>".parse().unwrap();
225        let bind = BindQuery::try_from(elem).unwrap();
226        assert_eq!(bind.tag.unwrap(), "AwesomeXMPP");
227        assert_eq!(bind.payloads.len(), 3);
228
229        // Example 4
230        let elem: Element = "<bound xmlns='urn:xmpp:bind:0'><metadata xmlns='urn:xmpp:mam:2'><start id='YWxwaGEg' timestamp='2008-08-22T21:09:04Z'/><end id='b21lZ2Eg' timestamp='2020-04-20T14:34:21Z'/></metadata></bound>".parse().unwrap();
231        let bound = Bound::try_from(elem).unwrap();
232        assert!(bound.mam_metadata.is_some());
233        assert_eq!(bound.payloads.len(), 0);
234    }
235}