Skip to main content

tokio_xmpp/xmlstream/
xmpp.rs

1// Copyright (c) 2024 Jonas Schäfer <jonas@zombofant.net>
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 std::fmt;
8use std::io;
9
10use rxml::NcNameStr;
11
12use xso::{error::Error, fromxml::FallibleBuilder, AsXml, FromEventsBuilder, FromXml};
13
14use xmpp_parsers::{component, sasl, sm, starttls, stream_error::ReceivedStreamError};
15
16use crate::Stanza;
17
18use super::ReadError;
19
20/// Any valid XMPP stream-level element.
21#[derive(FromXml, AsXml, Debug)]
22#[xml()]
23pub enum XmppStreamElement {
24    /// Stanza
25    #[xml(transparent)]
26    Stanza(Stanza),
27
28    /// SASL-related nonza
29    #[xml(transparent)]
30    Sasl(sasl::Nonza),
31
32    /// STARTTLS-related nonza
33    #[xml(transparent)]
34    Starttls(starttls::Nonza),
35
36    /// Component protocol nonzas
37    #[xml(transparent)]
38    ComponentHandshake(component::Handshake),
39
40    /// Stream error received
41    #[xml(transparent)]
42    StreamError(ReceivedStreamError),
43
44    /// XEP-0198 nonzas
45    #[xml(transparent)]
46    SM(sm::Nonza),
47}
48
49#[derive(Debug)]
50pub enum PartialStanza {
51    Presence,
52    Iq,
53    Message,
54}
55
56impl fmt::Display for PartialStanza {
57    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
58        match self {
59            Self::Presence => f.write_str("presence"),
60            Self::Message => f.write_str("message"),
61            Self::Iq => f.write_str("iq"),
62        }
63    }
64}
65
66impl PartialStanza {
67    pub fn to_ncname(&self) -> &'static NcNameStr {
68        match self {
69            Self::Presence => rxml::xml_ncname!("presence"),
70            Self::Message => rxml::xml_ncname!("message"),
71            Self::Iq => rxml::xml_ncname!("iq"),
72        }
73    }
74}
75
76enum CapturedMetadata {
77    Nonza {
78        qname: rxml::QName,
79    },
80    Stanza {
81        ns: rxml::Namespace<'static>,
82        name: PartialStanza,
83        from: Option<String>,
84        to: Option<String>,
85        type_: Option<String>,
86        id: Option<String>,
87    },
88}
89
90impl CapturedMetadata {
91    pub fn new(qname: &rxml::QName, attrs: &rxml::AttrMap) -> Self {
92        let kind = match qname.1.as_str() {
93            "presence" => Some(PartialStanza::Presence),
94            "iq" => Some(PartialStanza::Iq),
95            "message" => Some(PartialStanza::Message),
96            _ => None,
97        };
98        if let Some(kind) = kind {
99            CapturedMetadata::Stanza {
100                ns: qname.0.clone(),
101                name: kind,
102                from: attrs.get(&rxml::Namespace::NONE, "from").cloned(),
103                to: attrs.get(&rxml::Namespace::NONE, "to").cloned(),
104                id: attrs.get(&rxml::Namespace::NONE, "id").cloned(),
105                type_: attrs.get(&rxml::Namespace::NONE, "type").cloned(),
106            }
107        } else {
108            CapturedMetadata::Nonza {
109                qname: qname.clone(),
110            }
111        }
112    }
113}
114
115pub struct FallibleStreamElementBuilder {
116    metadata: Option<CapturedMetadata>,
117    builder: FallibleBuilder<<XmppStreamElement as FromXml>::Builder, Error>,
118}
119
120impl FromEventsBuilder for FallibleStreamElementBuilder {
121    type Output = FallibleStreamElement;
122
123    fn feed(&mut self, ev: rxml::Event, ctx: &xso::Context) -> Result<Option<Self::Output>, Error> {
124        match self.builder.feed(ev, ctx) {
125            Ok(Some(output)) => Ok(Some(match output {
126                Ok(v) => FallibleStreamElement::Ok(v),
127
128                // The FallibleBuilder should never ever emit an rxml::Error,
129                // because it cannot *receive* rxml::Error via `feed`.
130                Err(Error::XmlError(e)) => unreachable!("feed somehow saw an rxml error: {e}"),
131
132                // This error condition can not be emitted from feed.
133                Err(Error::TypeMismatch) => unreachable!("feed somehow saw a TypeMismatch"),
134
135                Err(error) => FallibleStreamElement::Err(
136                    match self.metadata.take().expect("feed called after completion") {
137                        CapturedMetadata::Nonza { qname } => {
138                            StreamElementError::InvalidNonza { qname, error }
139                        }
140                        CapturedMetadata::Stanza {
141                            ns,
142                            name,
143                            from,
144                            to,
145                            type_,
146                            id,
147                        } => StreamElementError::InvalidStanza {
148                            ns,
149                            name,
150                            header: RawStanzaHeader {
151                                from,
152                                to,
153                                type_,
154                                id,
155                            },
156                            error,
157                        },
158                    },
159                ),
160            })),
161            Ok(None) => Ok(None),
162            Err(e) => Err(e),
163        }
164    }
165}
166
167/// Container for unparsed stanza attributes.
168#[derive(Debug)]
169pub struct RawStanzaHeader {
170    /// The unaltered `from` attribute, if present.
171    pub from: Option<String>,
172
173    /// The unaltered `to` attribute, if present.
174    pub to: Option<String>,
175
176    /// The unaltered `type` attribute, if present.
177    pub type_: Option<String>,
178
179    /// The unaltered `id` attribute, if present.
180    pub id: Option<String>,
181}
182
183/// Error condition arising from failing to convert a stream-level element
184/// into a [`xmpp_parsers`] struct.
185#[derive(Debug)]
186pub enum StreamElementError {
187    /// Failed to convert an expected `<iq/>`, `<presence/>` or `<message/>`
188    /// element into a struct.
189    InvalidStanza {
190        /// Namespace of the element.
191        ns: rxml::Namespace<'static>,
192
193        /// Name of the element.
194        name: PartialStanza,
195
196        /// Header attributes of the invalid stanza.
197        header: RawStanzaHeader,
198
199        /// The error which caused the stanza to fail to parse.
200        ///
201        /// Note that this is never `xso::error::Error::TypeMismatch`, because
202        /// type mismatches do not even start to parse with `FromXml`.
203        error: xso::error::Error,
204    },
205
206    /// Invalid top-level stream element.
207    ///
208    /// This is reported if the element header matched the
209    /// [`XmppStreamElement`] type, but the payload failed to parse and it was
210    /// not a stanza.
211    InvalidNonza {
212        /// Qualified name of the element which failed to parse.
213        qname: rxml::QName,
214
215        /// The error which caused the stanza to fail to parse.
216        ///
217        /// Note that this is never `xso::error::Error::TypeMismatch`, because
218        /// type mismatches do not even start to parse with `FromXml`.
219        error: xso::error::Error,
220    },
221}
222
223impl fmt::Display for StreamElementError {
224    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
225        match self {
226            Self::InvalidNonza { qname, error, .. } => write!(
227                f,
228                "invalid nonza received: <{{{}}}{}/> ({error})",
229                qname.0, qname.1
230            ),
231            Self::InvalidStanza {
232                ns, name, error, ..
233            } => write!(
234                f,
235                "invalid stanza received: <{{{}}}{}/> ({error})",
236                ns, name
237            ),
238        }
239    }
240}
241
242impl core::error::Error for StreamElementError {}
243
244/// Wrapper type to catch parse errors of [`XmppStreamElement`] items.
245#[derive(Debug)]
246pub enum FallibleStreamElement {
247    /// Parsing succeeded.
248    Ok(XmppStreamElement),
249
250    /// Parsing failed.
251    Err(StreamElementError),
252}
253
254impl FallibleStreamElement {
255    /// Convert the contained error condition (if any) to a [`ReadError`].
256    ///
257    /// This can be used in places where you do not care to handle the various
258    /// error conditions separately.
259    pub fn into_read_error(self) -> Result<XmppStreamElement, ReadError> {
260        match self {
261            Self::Ok(v) => Ok(v),
262            Self::Err(e) => Err(ReadError::HardError(io::Error::new(
263                io::ErrorKind::InvalidData,
264                e,
265            ))),
266        }
267    }
268}
269
270impl FromXml for FallibleStreamElement {
271    type Builder = FallibleStreamElementBuilder;
272
273    fn from_events(
274        qname: rxml::QName,
275        attrs: rxml::AttrMap,
276        ctx: &xso::Context,
277    ) -> Result<Self::Builder, xso::error::FromEventsError> {
278        let metadata = Some(CapturedMetadata::new(&qname, &attrs));
279        let builder =
280            <Result<XmppStreamElement, Error> as FromXml>::from_events(qname, attrs, ctx)?;
281        Ok(FallibleStreamElementBuilder { metadata, builder })
282    }
283}