Compare commits

...

5 Commits

Author SHA1 Message Date
Jonas Schäfer 83dca24e13 Merge branch 'feature/fix-standard-deviance-in-jingle' into 'main'
Fix two places where we deviate from XEP-0166

See merge request xmpp-rs/xmpp-rs!305
2024-05-07 17:05:19 +00:00
Jonas Schäfer 384b366f5f Add Message::extract_payload function
This should simplify access to message payloads significantly.
2024-05-06 09:40:08 +00:00
Lucas Kent a291ab2e83 Remove an allocation in client::mechanisms::scram::Scram::initial 2024-05-06 08:25:24 +10:00
Jonas Schäfer b5de6cb82c xmpp_parsers::jingle: add ReasonElement::other
The Jingle XEP states [1] that the `<reason/>` element may contain up
to one element which further qualifies the error condition:

> The <reason/> element MAY contain an element qualified by some other
> namespace that provides more detailed machine-readable information
> about the reason for the action.

The schema agrees:

> ```
>   <xs:complexType name='reasonElementType'>
>     <xs:sequence>
>       <xs:choice>
>         <!-- … omitted … -->
>       </xs:choice>
>       <!-- … omitted … -->
>       <xs:any namespace='##other' minOccurs='0' maxOccurs='1'/>
>     </xs:sequence>
>   </xs:complexType>
> ```

   [1]: https://xmpp.org/extensions/xep-0166.html#def-reason
2024-04-17 17:13:56 +02:00
Jonas Schäfer ed0a1cd8cf xmpp_parsers::jingle: make ReasonElement::texts into an Option
The Jingle XEP says [1]:

> The <reason/> element MAY contain a <text/> element that provides
> human-readable information about the reason for the action.

It says nowhere that there may be more than one `<text/>` element in
there, or that they may be qualified by xml:lang. The schema [2] also
agrees with that:

> ```
>   <xs:complexType name='reasonElementType'>
>     <xs:sequence>
>       <xs:choice>
>         <!-- … omitted … -->
>       </xs:choice>
>       <xs:element name='text' type='xs:string' minOccurs='0' maxOccurs='1'/>
>       <!-- … omitted … -->
>     </xs:sequence>
>   </xs:complexType>
> ```

   [1]: https://xmpp.org/extensions/xep-0166.html#def-reason
   [2]: https://xmpp.org/extensions/xep-0166.html#schema-jingle
2024-04-17 17:13:56 +02:00
3 changed files with 92 additions and 28 deletions

View File

@ -14,7 +14,6 @@ use crate::ns;
use crate::util::error::Error;
use crate::Element;
use jid::Jid;
use std::collections::BTreeMap;
use std::fmt;
use std::str::FromStr;
@ -461,8 +460,6 @@ impl From<Reason> for Element {
}
}
type Lang = String;
/// Informs the recipient of something.
#[derive(Debug, Clone, PartialEq)]
pub struct ReasonElement {
@ -470,15 +467,18 @@ pub struct ReasonElement {
pub reason: Reason,
/// A human-readable description of this reason.
pub texts: BTreeMap<Lang, String>,
pub text: Option<String>,
/// A machine-readable extension of the reason.
// using a box here to save a significant amount of bytes in the common
// (absent) case.
pub other: Option<Box<Element>>,
}
impl fmt::Display for ReasonElement {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
write!(fmt, "{}", Element::from(self.reason.clone()).name())?;
if let Some(text) = self.texts.get("en") {
write!(fmt, ": {}", text)?;
} else if let Some(text) = self.texts.get("") {
if let Some(text) = self.text.as_ref() {
write!(fmt, ": {}", text)?;
}
Ok(())
@ -492,17 +492,16 @@ impl TryFrom<Element> for ReasonElement {
check_self!(elem, "reason", JINGLE);
check_no_attributes!(elem, "reason");
let mut reason = None;
let mut texts = BTreeMap::new();
let mut text = None;
let mut other = None;
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::ParseError(
"Text element present twice for the same xml:lang.",
));
if text.is_some() {
return Err(Error::ParseError("Multiple reason texts."));
}
text = Some(child.text());
} else if child.has_ns(ns::JINGLE) {
if reason.is_some() {
return Err(Error::ParseError(
@ -512,12 +511,18 @@ impl TryFrom<Element> for ReasonElement {
check_no_children!(child, "reason");
check_no_attributes!(child, "reason");
reason = Some(child.name().parse()?);
} else if other.is_none() {
other = Some(Box::new(child.clone()));
} else {
return Err(Error::ParseError("Reason contains a foreign element."));
}
}
let reason = reason.ok_or(Error::ParseError("Reason doesnt contain a valid reason."))?;
Ok(ReasonElement { reason, texts })
Ok(ReasonElement {
reason,
text,
other,
})
}
}
@ -525,11 +530,13 @@ impl From<ReasonElement> 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)
}))
.append_all(
reason
.text
.into_iter()
.map(|text| Element::builder("text", ns::JINGLE).append(text)),
)
.append_all(reason.other.into_iter().map(|other| *other))
.build()
}
}
@ -690,9 +697,9 @@ mod tests {
assert_size!(ContentId, 12);
assert_size!(Content, 216);
assert_size!(Reason, 1);
assert_size!(ReasonElement, 16);
assert_size!(ReasonElement, 20);
assert_size!(SessionId, 12);
assert_size!(Jingle, 104);
assert_size!(Jingle, 108);
}
#[cfg(target_pointer_width = "64")]
@ -709,9 +716,9 @@ mod tests {
#[cfg(feature = "stable")]
assert_size!(Content, 440);
assert_size!(Reason, 1);
assert_size!(ReasonElement, 32);
assert_size!(ReasonElement, 40);
assert_size!(SessionId, 24);
assert_size!(Jingle, 208);
assert_size!(Jingle, 216);
}
#[test]
@ -823,13 +830,13 @@ mod tests {
let jingle = Jingle::try_from(elem).unwrap();
let reason = jingle.reason.unwrap();
assert_eq!(reason.reason, Reason::Success);
assert_eq!(reason.texts, BTreeMap::new());
assert_eq!(reason.text, None);
let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><success/><text>coucou</text></reason></jingle>".parse().unwrap();
let jingle = Jingle::try_from(elem).unwrap();
let reason = jingle.reason.unwrap();
assert_eq!(reason.reason, Reason::Success);
assert_eq!(reason.texts.get(""), Some(&String::from("coucou")));
assert_eq!(reason.text, Some(String::from("coucou")));
}
#[test]
@ -856,7 +863,7 @@ mod tests {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Reason contains a foreign element.");
assert_eq!(message, "Reason doesnt contain a valid reason.");
let elem: Element = "<jingle xmlns='urn:xmpp:jingle:1' action='session-initiate' sid='coucou'><reason><decline/></reason><reason/></jingle>".parse().unwrap();
let error = Jingle::try_from(elem).unwrap_err();
@ -872,7 +879,7 @@ mod tests {
Error::ParseError(string) => string,
_ => panic!(),
};
assert_eq!(message, "Text element present twice for the same xml:lang.");
assert_eq!(message, "Multiple reason texts.");
}
#[test]

View File

@ -204,6 +204,40 @@ impl Message {
pub fn get_best_subject(&self, preferred_langs: Vec<&str>) -> Option<(Lang, &Subject)> {
Message::get_best::<Subject>(&self.subjects, preferred_langs)
}
/// Try to extract the given payload type from the message's payloads.
///
/// Returns the first matching payload element as parsed struct or its
/// parse error. If no element matches, `Ok(None)` is returned. If an
/// element matches, but fails to parse, it is nontheless removed from
/// the message.
///
/// Elements which do not match the given type are not removed.
pub fn extract_payload<T: TryFrom<Element, Error = Error>>(
&mut self,
) -> Result<Option<T>, Error> {
let mut buf = Vec::with_capacity(self.payloads.len());
let mut iter = self.payloads.drain(..);
let mut result = Ok(None);
for item in &mut iter {
match T::try_from(item) {
Ok(v) => {
result = Ok(Some(v));
break;
}
Err(Error::TypeMismatch(_, _, residual)) => {
buf.push(residual);
}
Err(other) => {
result = Err(other);
break;
}
}
}
buf.extend(iter);
std::mem::swap(&mut buf, &mut self.payloads);
result
}
}
impl TryFrom<Element> for Message {
@ -460,4 +494,27 @@ mod tests {
let elem2 = message.into();
assert_eq!(elem1, elem2);
}
#[test]
fn test_extract_payload() {
use super::super::attention::Attention;
use super::super::pubsub::event::PubSubEvent;
#[cfg(not(feature = "component"))]
let elem: Element = "<message xmlns='jabber:client' to='coucou@example.org' type='chat'><attention xmlns='urn:xmpp:attention:0'/></message>".parse().unwrap();
#[cfg(feature = "component")]
let elem: Element = "<message xmlns='jabber:component:accept' to='coucou@example.org' type='chat'><attention xmlns='urn:xmpp:attention:0'/></message>".parse().unwrap();
let mut message = Message::try_from(elem).unwrap();
assert_eq!(message.payloads.len(), 1);
match message.extract_payload::<PubSubEvent>() {
Ok(None) => (),
other => panic!("unexpected result: {:?}", other),
};
assert_eq!(message.payloads.len(), 1);
match message.extract_payload::<Attention>() {
Ok(Some(_)) => (),
other => panic!("unexpected result: {:?}", other),
};
assert_eq!(message.payloads.len(), 0);
}
}

View File

@ -109,7 +109,7 @@ impl<S: ScramProvider> Mechanism for Scram<S> {
bare.extend(self.client_nonce.bytes());
let mut data = Vec::new();
data.extend(&gs2_header);
data.extend(bare.clone());
data.extend(&bare);
self.state = ScramState::SentInitialMessage {
initial_message: bare,
gs2_header: gs2_header,