Add alternate_address to StanzaError

mb created

[gone](https://datatracker.ietf.org/doc/html/rfc6120#section-8.3.3.5) and [redirect](https://datatracker.ietf.org/doc/html/rfc6120#section-8.3.3.14) errors may include an alternative address.

> gone
>
> 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 [XMPP-URI]).

—

> redirect
>
> 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
> [XMPP-URI](https://datatracker.ietf.org/doc/html/rfc6120#ref-XMPP-URI)).

Change summary

parsers/src/iq.rs           |  8 +-
parsers/src/stanza_error.rs | 91 ++++++++++++++++++++++++++++++++++++++
2 files changed, 93 insertions(+), 6 deletions(-)

Detailed changes

parsers/src/iq.rs 🔗

@@ -232,15 +232,15 @@ mod tests {
     #[cfg(target_pointer_width = "32")]
     #[test]
     fn test_size() {
-        assert_size!(IqType, 92);
-        assert_size!(Iq, 156);
+        assert_size!(IqType, 104);
+        assert_size!(Iq, 168);
     }
 
     #[cfg(target_pointer_width = "64")]
     #[test]
     fn test_size() {
-        assert_size!(IqType, 184);
-        assert_size!(Iq, 272);
+        assert_size!(IqType, 208);
+        assert_size!(Iq, 296);
     }
 
     #[test]

parsers/src/stanza_error.rs 🔗

@@ -10,7 +10,9 @@ use crate::presence::PresencePayload;
 use crate::util::error::Error;
 use crate::Element;
 use jid::Jid;
+use minidom::Node;
 use std::collections::BTreeMap;
+use std::convert::TryFrom;
 
 generate_attribute!(
     /// The type of the error.
@@ -209,6 +211,11 @@ pub struct StanzaError {
 
     /// A protocol-specific extension for this error.
     pub other: Option<Element>,
+
+    /// May include an alternate address if `defined_condition` is `Gone` or `Redirect`. It is
+    /// a Uniform Resource Identifier [URI] or Internationalized Resource Identifier [IRI] at
+    /// which the entity can be contacted, typically an XMPP IRI as specified in [XMPP‑URI]
+    pub alternate_address: Option<String>,
 }
 
 impl MessagePayload for StanzaError {}
@@ -236,6 +243,7 @@ impl StanzaError {
                 map
             },
             other: None,
+            alternate_address: None,
         }
     }
 }
@@ -255,6 +263,7 @@ impl TryFrom<Element> for StanzaError {
             defined_condition: DefinedCondition::UndefinedCondition,
             texts: BTreeMap::new(),
             other: None,
+            alternate_address: None,
         };
         let mut defined_condition = None;
 
@@ -277,6 +286,14 @@ impl TryFrom<Element> for StanzaError {
                 check_no_children!(child, "defined-condition");
                 check_no_attributes!(child, "defined-condition");
                 let condition = DefinedCondition::try_from(child.clone())?;
+
+                if condition == DefinedCondition::Gone || condition == DefinedCondition::Redirect {
+                    stanza_error.alternate_address = child.nodes().find_map(|node| {
+                        let Node::Text(text) = node else { return None };
+                        return Some(text.to_string());
+                    });
+                }
+
                 defined_condition = Some(condition);
             } else {
                 if stanza_error.other.is_some() {
@@ -319,7 +336,7 @@ mod tests {
     fn test_size() {
         assert_size!(ErrorType, 1);
         assert_size!(DefinedCondition, 1);
-        assert_size!(StanzaError, 92);
+        assert_size!(StanzaError, 104);
     }
 
     #[cfg(target_pointer_width = "64")]
@@ -327,7 +344,7 @@ mod tests {
     fn test_size() {
         assert_size!(ErrorType, 1);
         assert_size!(DefinedCondition, 1);
-        assert_size!(StanzaError, 184);
+        assert_size!(StanzaError, 208);
     }
 
     #[test]
@@ -415,4 +432,74 @@ mod tests {
         let stanza_error = StanzaError::try_from(elem).unwrap();
         assert_eq!(stanza_error.type_, ErrorType::Cancel);
     }
+
+    #[test]
+    fn test_gone_with_new_address() {
+        #[cfg(not(feature = "component"))]
+            let elem: Element = "<error xmlns='jabber:client' type='cancel'><gone xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'>xmpp:room@muc.example.org?join</gone></error>"
+            .parse()
+            .unwrap();
+        #[cfg(feature = "component")]
+            let elem: Element = "<error xmlns='jabber:component:accept' type='cancel'><gone xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'>xmpp:room@muc.example.org?join</gone></error>"
+            .parse()
+            .unwrap();
+        let error = StanzaError::try_from(elem).unwrap();
+        assert_eq!(error.type_, ErrorType::Cancel);
+        assert_eq!(error.defined_condition, DefinedCondition::Gone);
+        assert_eq!(
+            error.alternate_address,
+            Some("xmpp:room@muc.example.org?join".to_string())
+        );
+    }
+
+    #[test]
+    fn test_gone_without_new_address() {
+        #[cfg(not(feature = "component"))]
+            let elem: Element = "<error xmlns='jabber:client' type='cancel'><gone xmlns='urn:ietf:params:xml:ns:xmpp-stanzas' /></error>"
+            .parse()
+            .unwrap();
+        #[cfg(feature = "component")]
+            let elem: Element = "<error xmlns='jabber:component:accept' type='cancel'><gone 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::Gone);
+        assert_eq!(error.alternate_address, None);
+    }
+
+    #[test]
+    fn test_redirect_with_alternate_address() {
+        #[cfg(not(feature = "component"))]
+            let elem: Element = "<error xmlns='jabber:client' type='modify'><redirect xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'>xmpp:characters@conference.example.org</redirect></error>"
+            .parse()
+            .unwrap();
+        #[cfg(feature = "component")]
+            let elem: Element = "<error xmlns='jabber:component:accept' type='modify'><redirect xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'>xmpp:characters@conference.example.org</redirect></error>"
+            .parse()
+            .unwrap();
+        let error = StanzaError::try_from(elem).unwrap();
+        assert_eq!(error.type_, ErrorType::Modify);
+        assert_eq!(error.defined_condition, DefinedCondition::Redirect);
+        assert_eq!(
+            error.alternate_address,
+            Some("xmpp:characters@conference.example.org".to_string())
+        );
+    }
+
+    #[test]
+    fn test_redirect_without_alternate_address() {
+        #[cfg(not(feature = "component"))]
+            let elem: Element = "<error xmlns='jabber:client' type='modify'><redirect xmlns='urn:ietf:params:xml:ns:xmpp-stanzas' /></error>"
+            .parse()
+            .unwrap();
+        #[cfg(feature = "component")]
+            let elem: Element = "<error xmlns='jabber:component:accept' type='modify'><redirect xmlns='urn:ietf:params:xml:ns:xmpp-stanzas' /></error>"
+            .parse()
+            .unwrap();
+        let error = StanzaError::try_from(elem).unwrap();
+        assert_eq!(error.type_, ErrorType::Modify);
+        assert_eq!(error.defined_condition, DefinedCondition::Redirect);
+        assert_eq!(error.alternate_address, None);
+    }
 }