From bc88134c41e522e444fe067a831d105b08c88e9a Mon Sep 17 00:00:00 2001 From: Link Mauve Date: Sat, 21 Feb 2026 20:34:36 +0100 Subject: [PATCH] xmpp-parsers: Make DiscoInfoResult::features a BTreeSet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Features must never be duplicated, so we can use a BTreeSet instead of a Vec to be nicer for users. It makes the internal API for computing caps and ecaps2 a bit worse, because it was expecting a slice directly, so for now let’s collect the BTreeSet into a Vec when computing the thing. A refactor to use Iterator might make it better eventually, but I won’t work on that before profiling it. --- parsers/ChangeLog | 3 ++- parsers/src/caps.rs | 5 ++++- parsers/src/disco.rs | 3 ++- parsers/src/ecaps2.rs | 5 ++++- tokio-xmpp/examples/download_avatars.rs | 5 +++-- xmpp/src/builder.rs | 8 +++++--- 6 files changed, 20 insertions(+), 9 deletions(-) diff --git a/parsers/ChangeLog b/parsers/ChangeLog index 2c02f4bc..3ea4f2a2 100644 --- a/parsers/ChangeLog +++ b/parsers/ChangeLog @@ -13,7 +13,8 @@ XXXX-YY-ZZ RELEASER - Remove the unnecessary SaslMechanisms struct, to directly extract SASL mechanisms in a Vec in StreamFeatures. - The type of disco::DiscoInfoResult::features changed from - Vec to Vec, this lets us remove Feature as well. + Vec to BTreeSet, this lets us remove Feature as + well. - bind::BindFeature::required is now a bool, thanks to xso’s flag. - jingle_rtp::Description::rtcp_mux is now a bool flag too. - stream_limits::Limits now extract directly to NonZeroU32. diff --git a/parsers/src/caps.rs b/parsers/src/caps.rs index 1e3804ea..05a3b405 100644 --- a/parsers/src/caps.rs +++ b/parsers/src/caps.rs @@ -125,8 +125,11 @@ fn compute_extensions(extensions: &[DataForm]) -> Vec { /// [ecaps2](../ecaps2/index.html) instead, see [this /// email](https://mail.jabber.org/pipermail/security/2009-July/000812.html). pub fn compute_disco(disco: &DiscoInfoResult) -> Vec { + // TODO: Figure out a way to remove the clones here. + let features: Vec<_> = disco.features.iter().cloned().collect(); + let identities_string = compute_identities(&disco.identities); - let features_string = compute_features(&disco.features); + let features_string = compute_features(&features); let extensions_string = compute_extensions(&disco.extensions); let mut final_string = vec![]; diff --git a/parsers/src/disco.rs b/parsers/src/disco.rs index bb9d2904..ff704743 100644 --- a/parsers/src/disco.rs +++ b/parsers/src/disco.rs @@ -10,6 +10,7 @@ use crate::data_forms::DataForm; use crate::iq::{IqGetPayload, IqResultPayload}; use crate::ns; use crate::rsm::{SetQuery, SetResult}; +use alloc::collections::BTreeSet; use jid::Jid; /// Structure representing a `` element. @@ -98,7 +99,7 @@ pub struct DiscoInfoResult { /// List of features supported by this entity. #[xml(extract(n = .., name = "feature", fields(attribute(name = "var", type_ = String))))] - pub features: Vec, + pub features: BTreeSet, /// List of extensions reported by this entity. #[xml(child(n = ..))] diff --git a/parsers/src/ecaps2.rs b/parsers/src/ecaps2.rs index d1a8c5cd..7669db39 100644 --- a/parsers/src/ecaps2.rs +++ b/parsers/src/ecaps2.rs @@ -124,7 +124,10 @@ fn compute_extensions(extensions: &[DataForm]) -> Result, Error> { /// XEP-0390](https://xmpp.org/extensions/xep-0390.html#algorithm-input) on a /// [disco#info query element](../disco/struct.DiscoInfoResult.html). pub fn compute_disco(disco: &DiscoInfoResult) -> Result, Error> { - let features_string = compute_features(&disco.features); + // TODO: Figure out a way to remove the clones here. + let features: Vec<_> = disco.features.iter().cloned().collect(); + + let features_string = compute_features(&features); let identities_string = compute_identities(&disco.identities); let extensions_string = compute_extensions(&disco.extensions)?; diff --git a/tokio-xmpp/examples/download_avatars.rs b/tokio-xmpp/examples/download_avatars.rs index 47a80b05..954927cc 100644 --- a/tokio-xmpp/examples/download_avatars.rs +++ b/tokio-xmpp/examples/download_avatars.rs @@ -1,4 +1,5 @@ use futures::stream::StreamExt; +use std::collections::BTreeSet; use std::env::args; use std::fs::{create_dir_all, File}; use std::io::{self, Write}; @@ -184,10 +185,10 @@ fn make_error( fn make_disco() -> DiscoInfoResult { let identities = vec![Identity::new("client", "bot", "en", "tokio-xmpp")]; - let features = vec![ + let features = BTreeSet::from([ String::from(ns::DISCO_INFO), format!("{}+notify", ns::AVATAR_METADATA), - ]; + ]); DiscoInfoResult { node: None, identities, diff --git a/xmpp/src/builder.rs b/xmpp/src/builder.rs index 23909791..7334b9ed 100644 --- a/xmpp/src/builder.rs +++ b/xmpp/src/builder.rs @@ -16,6 +16,7 @@ use crate::{ }, tokio_xmpp::{Client as TokioXmppClient, connect::ServerConnector, xmlstream::Timeouts}, }; +use std::collections::BTreeSet; pub struct ClientBuilder<'a, C: ServerConnector> { jid: BareJid, @@ -107,15 +108,16 @@ impl ClientBuilder<'_, C> { "en", self.config.disco.1.to_string(), )]; - let mut features = vec![String::from(ns::DISCO_INFO)]; + let mut features = BTreeSet::new(); + features.insert(String::from(ns::DISCO_INFO)); #[cfg(feature = "avatars")] { if self.features.contains(&ClientFeature::Avatars) { - features.push(format!("{}+notify", ns::AVATAR_METADATA)); + features.insert(format!("{}+notify", ns::AVATAR_METADATA)); } } if self.features.contains(&ClientFeature::JoinRooms) { - features.push(format!("{}+notify", ns::BOOKMARKS2)); + features.insert(format!("{}+notify", ns::BOOKMARKS2)); } DiscoInfoResult { node: None,