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}