From 3c77ee0cc7955cdb3352a32b12c42a5b80cde044 Mon Sep 17 00:00:00 2001 From: Emmanuel Gil Peyrot Date: Wed, 18 Dec 2024 15:39:30 +0100 Subject: [PATCH] xmpp-parsers: Add support for This was previously unimplemented due to how the enum was treated, now that we moved the reason element to xso we can describe it properly and thus implement everything it supports. This causes one regression on xml:lang validation, but we can live with that until xso implements such verification. --- parsers/src/jingle.rs | 188 +++++++++++------------------------------- 1 file changed, 49 insertions(+), 139 deletions(-) diff --git a/parsers/src/jingle.rs b/parsers/src/jingle.rs index 36bd2f618057ef1091a513cb10cccb5e345dd5a9..8ce033c0569f2a64ecec908077d34a8e1be9b561 100644 --- a/parsers/src/jingle.rs +++ b/parsers/src/jingle.rs @@ -17,8 +17,7 @@ use jid::Jid; use minidom::Element; use std::collections::BTreeMap; use std::fmt; -use std::str::FromStr; -use xso::error::{Error, FromElementError}; +use xso::error::Error; generate_attribute!( /// The action attribute. @@ -376,134 +375,106 @@ impl Content { } /// Lists the possible reasons to be included in a Jingle iq. -#[derive(Debug, Clone, PartialEq)] +#[derive(FromXml, AsXml, Debug, Clone, PartialEq)] +#[xml(namespace = ns::JINGLE)] pub enum Reason { /// The party prefers to use an existing session with the peer rather than /// initiate a new session; the Jingle session ID of the alternative /// session SHOULD be provided as the XML character data of the \ /// child. - AlternativeSession, //(String), + #[xml(name = "alternative-session")] + AlternativeSession { + /// Session ID of the alternative session. + #[xml(extract(namespace = ns::JINGLE, name = "sid", default, fields(text(type_ = String))))] + sid: Option, + }, /// The party is busy and cannot accept a session. + #[xml(name = "busy")] Busy, /// The initiator wishes to formally cancel the session initiation request. + #[xml(name = "cancel")] Cancel, /// The action is related to connectivity problems. + #[xml(name = "connectivity-error")] ConnectivityError, /// The party wishes to formally decline the session. + #[xml(name = "decline")] Decline, /// The session length has exceeded a pre-defined time limit (e.g., a /// meeting hosted at a conference service). + #[xml(name = "expired")] Expired, /// The party has been unable to initialize processing related to the /// application type. + #[xml(name = "failed-application")] FailedApplication, /// The party has been unable to establish connectivity for the transport /// method. + #[xml(name = "failed-transport")] FailedTransport, /// The action is related to a non-specific application error. + #[xml(name = "general-error")] GeneralError, /// The entity is going offline or is no longer available. + #[xml(name = "gone")] Gone, /// The party supports the offered application type but does not support /// the offered or negotiated parameters. + #[xml(name = "incompatible-parameters")] IncompatibleParameters, /// The action is related to media processing problems. + #[xml(name = "media-error")] MediaError, /// The action is related to a violation of local security policies. + #[xml(name = "security-error")] SecurityError, /// The action is generated during the normal course of state management /// and does not reflect any error. + #[xml(name = "success")] Success, /// A request has not been answered so the sender is timing out the /// request. + #[xml(name = "timeout")] Timeout, /// The party supports none of the offered application types. + #[xml(name = "unsupported-applications")] UnsupportedApplications, /// The party supports none of the offered transport methods. + #[xml(name = "unsupported-transports")] UnsupportedTransports, } -impl FromStr for Reason { - type Err = Error; - - fn from_str(s: &str) -> Result { - Ok(match s { - "alternative-session" => Reason::AlternativeSession, - "busy" => Reason::Busy, - "cancel" => Reason::Cancel, - "connectivity-error" => Reason::ConnectivityError, - "decline" => Reason::Decline, - "expired" => Reason::Expired, - "failed-application" => Reason::FailedApplication, - "failed-transport" => Reason::FailedTransport, - "general-error" => Reason::GeneralError, - "gone" => Reason::Gone, - "incompatible-parameters" => Reason::IncompatibleParameters, - "media-error" => Reason::MediaError, - "security-error" => Reason::SecurityError, - "success" => Reason::Success, - "timeout" => Reason::Timeout, - "unsupported-applications" => Reason::UnsupportedApplications, - "unsupported-transports" => Reason::UnsupportedTransports, - - _ => return Err(Error::Other("Unknown reason.")), - }) - } -} - -impl From for Element { - fn from(reason: Reason) -> Element { - Element::builder( - match reason { - Reason::AlternativeSession => "alternative-session", - Reason::Busy => "busy", - Reason::Cancel => "cancel", - Reason::ConnectivityError => "connectivity-error", - Reason::Decline => "decline", - Reason::Expired => "expired", - Reason::FailedApplication => "failed-application", - Reason::FailedTransport => "failed-transport", - Reason::GeneralError => "general-error", - Reason::Gone => "gone", - Reason::IncompatibleParameters => "incompatible-parameters", - Reason::MediaError => "media-error", - Reason::SecurityError => "security-error", - Reason::Success => "success", - Reason::Timeout => "timeout", - Reason::UnsupportedApplications => "unsupported-applications", - Reason::UnsupportedTransports => "unsupported-transports", - }, - ns::JINGLE, - ) - .build() - } -} - type Lang = String; /// Informs the recipient of something. -#[derive(Debug, Clone, PartialEq)] +#[derive(FromXml, AsXml, Debug, Clone, PartialEq)] +#[xml(namespace = ns::JINGLE, name = "reason")] pub struct ReasonElement { /// The list of possible reasons to be included in a Jingle iq. + #[xml(child)] pub reason: Reason, /// A human-readable description of this reason. + #[xml(extract(n = .., namespace = ns::JINGLE, name = "text", fields( + attribute(type_ = String, name = "xml:lang", default), + text(type_ = String), + )))] pub texts: BTreeMap, } @@ -519,75 +490,6 @@ impl fmt::Display for ReasonElement { } } -impl TryFrom for ReasonElement { - type Error = FromElementError; - - fn try_from(elem: Element) -> Result { - check_self!(elem, "reason", JINGLE); - check_no_attributes!(elem, "reason"); - let mut reason = None; - let mut texts = BTreeMap::new(); - for child in elem.children() { - if child.is("text", ns::JINGLE) { - check_no_children!(child, "text"); - check_no_unknown_attributes!(child, "text", ["xml:lang"]); - let lang = get_attr!(elem, "xml:lang", Default); - if texts.insert(lang, child.text()).is_some() { - return Err( - Error::Other("Text element present twice for the same xml:lang.").into(), - ); - } - } else if child.has_ns(ns::JINGLE) { - if reason.is_some() { - return Err(Error::Other("Reason must not have more than one reason.").into()); - } - check_no_children!(child, "reason"); - check_no_attributes!(child, "reason"); - reason = Some(child.name().parse()?); - } else { - return Err(Error::Other("Reason contains a foreign element.").into()); - } - } - let reason = reason.ok_or(Error::Other("Reason doesn’t contain a valid reason."))?; - Ok(ReasonElement { reason, texts }) - } -} - -impl From for Element { - fn from(reason: ReasonElement) -> Element { - Element::builder("reason", ns::JINGLE) - .append(Element::from(reason.reason)) - .append_all(reason.texts.into_iter().map(|(lang, text)| { - Element::builder("text", ns::JINGLE) - .attr("xml:lang", lang) - .append(text) - })) - .build() - } -} - -impl ::xso::FromXml for ReasonElement { - type Builder = ::xso::minidom_compat::FromEventsViaElement; - - fn from_events( - qname: ::xso::exports::rxml::QName, - attrs: ::xso::exports::rxml::AttrMap, - ) -> Result { - if qname.0 != crate::ns::JINGLE || qname.1 != "reason" { - return Err(::xso::error::FromEventsError::Mismatch { name: qname, attrs }); - } - Self::Builder::new(qname, attrs) - } -} - -impl ::xso::AsXml for ReasonElement { - type ItemIter<'x> = ::xso::minidom_compat::AsItemsViaElement<'x>; - - fn as_xml_iter(&self) -> Result, ::xso::error::Error> { - ::xso::minidom_compat::AsItemsViaElement::new(self.clone()) - } -} - generate_id!( /// Unique identifier for a session between two JIDs. SessionId @@ -681,6 +583,7 @@ impl Jingle { #[cfg(test)] mod tests { use super::*; + use xso::error::FromElementError; #[cfg(target_pointer_width = "32")] #[test] @@ -691,10 +594,10 @@ mod tests { assert_size!(Disposition, 1); assert_size!(ContentId, 12); assert_size!(Content, 156); - assert_size!(Reason, 1); - assert_size!(ReasonElement, 16); + assert_size!(Reason, 12); + assert_size!(ReasonElement, 24); assert_size!(SessionId, 12); - assert_size!(Jingle, 104); + assert_size!(Jingle, 112); } #[cfg(target_pointer_width = "64")] @@ -706,10 +609,10 @@ mod tests { assert_size!(Disposition, 1); assert_size!(ContentId, 24); assert_size!(Content, 312); - assert_size!(Reason, 1); - assert_size!(ReasonElement, 32); + assert_size!(Reason, 24); + assert_size!(ReasonElement, 48); assert_size!(SessionId, 24); - assert_size!(Jingle, 208); + assert_size!(Jingle, 224); } #[test] @@ -859,7 +762,10 @@ mod tests { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; - assert_eq!(message, "Reason doesn’t contain a valid reason."); + assert_eq!( + message, + "Missing child field 'reason' in ReasonElement element." + ); let elem: Element = "".parse().unwrap(); let error = Jingle::try_from(elem).unwrap_err(); @@ -867,7 +773,7 @@ mod tests { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; - assert_eq!(message, "Unknown reason."); + assert_eq!(message, "Unknown child in ReasonElement element."); let elem: Element = "".parse().unwrap(); let error = Jingle::try_from(elem).unwrap_err(); @@ -875,7 +781,7 @@ mod tests { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; - assert_eq!(message, "Reason contains a foreign element."); + assert_eq!(message, "Unknown child in ReasonElement element."); let elem: Element = "".parse().unwrap(); let error = Jingle::try_from(elem).unwrap_err(); @@ -888,6 +794,9 @@ mod tests { "Jingle element must not have more than one child in field 'reason'." ); + // TODO: Reenable this test once xso is able to validate that no more than one text is + // there for every xml:lang. + /* let elem: Element = "".parse().unwrap(); let error = Jingle::try_from(elem).unwrap_err(); let message = match error { @@ -895,6 +804,7 @@ mod tests { _ => panic!(), }; assert_eq!(message, "Text element present twice for the same xml:lang."); + */ } #[test]