From 495c96a469d480b2b6111ff37cd4929bd67d4555 Mon Sep 17 00:00:00 2001 From: Kierre Date: Tue, 10 Feb 2026 11:49:46 -0500 Subject: [PATCH 1/6] events: fix ACLs being case sensitive --- crates/ruma-events/src/room/server_acl.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/crates/ruma-events/src/room/server_acl.rs b/crates/ruma-events/src/room/server_acl.rs index a1c49f8cfd..dc59091d0c 100644 --- a/crates/ruma-events/src/room/server_acl.rs +++ b/crates/ruma-events/src/room/server_acl.rs @@ -102,11 +102,11 @@ impl RoomServerAclEventContent { } fn matches(a: &[String], s: &str) -> bool { - a.iter().map(String::as_str).any(|a| WildMatch::new(a).matches(s)) + a.iter().map(String::as_str).any(|a| WildMatch::new_case_insensitive(a).matches(s)) } fn contains(a: &[String], s: &str) -> bool { - a.iter().map(String::as_str).any(|a| a == s) + a.iter().map(String::as_str).any(|a| a.to_lowercase().to_str() == s.to_lowercase().to_str()) } } @@ -222,4 +222,19 @@ mod tests { assert!(!acl_event.is_allowed(server_name!("[2001:db8:1234::2]"))); assert!(acl_event.is_allowed(server_name!("[2001:db8:1234::1]"))); } + + #[test] + fn acl_case_insensitive() { + let acl_event = RoomServerAclEventContent { + allow_ip_literals: false, + allow: vec!["good.ServEr".to_owned()], + deny: vec!["bad.ServeR".to_owned()], + }; + assert!(!acl_event.is_allowed(server_name!("Bad.ServeR"))); + assert!(!acl_event.is_allowed(server_name!("bAD.sERvER"))); + assert!(!acl_event.is_allowed(server_name!("bAd.server"))); + assert!(acl_event.is_allowed(server_name!("good.ServEr"))); + assert!(acl_event.is_allowed(server_name!("good.server"))); + assert!(acl_event.is_allowed(server_name!("GOOD.SERVER"))); + } } From e8038f1c06a9554468e28a29a1e9d5458fdc58a9 Mon Sep 17 00:00:00 2001 From: Jason Volk Date: Thu, 12 Feb 2026 08:31:02 +0000 Subject: [PATCH 2/6] Optimize case-insensitive comparisons. Signed-off-by: Jason Volk --- crates/ruma-common/src/push/condition.rs | 11 +++++++---- crates/ruma-events/src/room/server_acl.rs | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/crates/ruma-common/src/push/condition.rs b/crates/ruma-common/src/push/condition.rs index 234daa5057..57e1a73def 100644 --- a/crates/ruma-common/src/push/condition.rs +++ b/crates/ruma-common/src/push/condition.rs @@ -503,13 +503,16 @@ impl StrExt for str { } fn matches_pattern(&self, pattern: &str, match_words: bool) -> bool { - let value = &self.to_lowercase(); - let pattern = &pattern.to_lowercase(); - if match_words { + if self.eq_ignore_ascii_case(pattern) { + return true; + } + + let value = &self.to_lowercase(); + let pattern = &pattern.to_lowercase(); value.matches_word(pattern) } else { - WildMatch::new(pattern).matches(value) + WildMatch::new_case_insensitive(pattern).matches(self) } } diff --git a/crates/ruma-events/src/room/server_acl.rs b/crates/ruma-events/src/room/server_acl.rs index dc59091d0c..ffd58ffa68 100644 --- a/crates/ruma-events/src/room/server_acl.rs +++ b/crates/ruma-events/src/room/server_acl.rs @@ -106,7 +106,7 @@ impl RoomServerAclEventContent { } fn contains(a: &[String], s: &str) -> bool { - a.iter().map(String::as_str).any(|a| a.to_lowercase().to_str() == s.to_lowercase().to_str()) + a.iter().map(String::as_str).any(|a| a.eq_ignore_ascii_case(s)) } } From 2f53677f4d621cb4560119c5938888c9ce983e56 Mon Sep 17 00:00:00 2001 From: Jason Volk Date: Fri, 13 Feb 2026 23:22:29 +0000 Subject: [PATCH 3/6] Replace version discovery Strings with SmallStrings. Signed-off-by: Jason Volk --- Cargo.lock | 1 + .../src/discovery/get_supported_versions.rs | 18 ++++++++++++++---- crates/ruma-common/src/api/metadata.rs | 16 +++++++++++----- crates/ruma-identity-service-api/Cargo.toml | 1 + .../src/discovery/get_supported_versions.rs | 16 +++++++++++++--- 5 files changed, 40 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7cf90e10a1..943a320d80 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2047,6 +2047,7 @@ dependencies = [ "ruma-events", "serde", "serde_json", + "smallstr", ] [[package]] diff --git a/crates/ruma-client-api/src/discovery/get_supported_versions.rs b/crates/ruma-client-api/src/discovery/get_supported_versions.rs index 8fe8f050a9..0b0be02ada 100644 --- a/crates/ruma-client-api/src/discovery/get_supported_versions.rs +++ b/crates/ruma-client-api/src/discovery/get_supported_versions.rs @@ -10,6 +10,7 @@ use ruma_common::{ api::{request, response, Metadata, SupportedVersions}, metadata, }; +use smallstr::SmallString; const METADATA: Metadata = metadata! { method: GET, @@ -29,16 +30,22 @@ pub struct Request {} #[response(error = crate::Error)] pub struct Response { /// A list of Matrix client API protocol versions supported by the homeserver. - pub versions: Vec, + pub versions: Vec, /// Experimental features supported by the server. /// /// Servers can enable some unstable features only for some users, so this /// list might differ when an access token is provided. #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - pub unstable_features: BTreeMap, + pub unstable_features: BTreeMap, } +/// Opinionated optimized Version String type. +pub type Version = SmallString<[u8; 16]>; + +/// Opinionated optimized Feature String type. +pub type Feature = SmallString<[u8; 48]>; + impl Request { /// Creates an empty `Request`. pub fn new() -> Self { @@ -48,7 +55,7 @@ impl Request { impl Response { /// Creates a new `Response` with the given `versions`. - pub fn new(versions: Vec) -> Self { + pub fn new(versions: Vec) -> Self { Self { versions, unstable_features: BTreeMap::new() } } @@ -58,6 +65,9 @@ impl Response { /// Matrix versions that can't be parsed to a `MatrixVersion`, and features with the boolean /// value set to `false` are discarded. pub fn as_supported_versions(&self) -> SupportedVersions { - SupportedVersions::from_parts(&self.versions, &self.unstable_features) + SupportedVersions::from_parts( + self.versions.iter().map(Version::as_str), + self.unstable_features.iter().map(|(k, v)| (k.as_str(), v)), + ) } } diff --git a/crates/ruma-common/src/api/metadata.rs b/crates/ruma-common/src/api/metadata.rs index 1e125da0df..eb600cf35d 100644 --- a/crates/ruma-common/src/api/metadata.rs +++ b/crates/ruma-common/src/api/metadata.rs @@ -1,6 +1,6 @@ use std::{ cmp::Ordering, - collections::{BTreeMap, BTreeSet}, + collections::BTreeSet, fmt::{Display, Write}, str::FromStr, }; @@ -1144,13 +1144,19 @@ impl SupportedVersions { /// /// Matrix versions that can't be parsed to a `MatrixVersion`, and features with the boolean /// value set to `false` are discarded. - pub fn from_parts(versions: &[String], unstable_features: &BTreeMap) -> Self { + pub fn from_parts<'a, Versions, Features>( + versions: Versions, + unstable_features: Features, + ) -> Self + where + Versions: Iterator, + Features: Iterator, + { Self { - versions: versions.iter().flat_map(|s| s.parse::()).collect(), + versions: versions.flat_map(|s| s.parse::()).collect(), features: unstable_features - .iter() .filter(|(_, enabled)| **enabled) - .map(|(feature, _)| feature.as_str().into()) + .map(|(feature, _)| feature.into()) .collect(), } } diff --git a/crates/ruma-identity-service-api/Cargo.toml b/crates/ruma-identity-service-api/Cargo.toml index 0ccc20b375..499f23c5ca 100644 --- a/crates/ruma-identity-service-api/Cargo.toml +++ b/crates/ruma-identity-service-api/Cargo.toml @@ -22,6 +22,7 @@ js_int = { workspace = true, features = ["serde"] } ruma-common = { workspace = true, features = ["api"] } ruma-events = { workspace = true } serde = { workspace = true } +smallstr = { workspace = true } [dev-dependencies] serde_json = { workspace = true } diff --git a/crates/ruma-identity-service-api/src/discovery/get_supported_versions.rs b/crates/ruma-identity-service-api/src/discovery/get_supported_versions.rs index 7e4c5dcf00..437cf71c9d 100644 --- a/crates/ruma-identity-service-api/src/discovery/get_supported_versions.rs +++ b/crates/ruma-identity-service-api/src/discovery/get_supported_versions.rs @@ -16,6 +16,7 @@ use ruma_common::{ api::{request, response, Metadata, SupportedVersions}, metadata, }; +use smallstr::SmallString; const METADATA: Metadata = metadata! { method: GET, @@ -35,9 +36,15 @@ pub struct Request {} #[response] pub struct Response { /// A list of Matrix client API protocol versions supported by the endpoint. - pub versions: Vec, + pub versions: Vec, } +/// Opinionated optimized Version String type. +pub type Version = SmallString<[u8; 16]>; + +/// Opinionated optimized Feature String type. +pub type Feature = SmallString<[u8; 48]>; + impl Request { /// Creates an empty `Request`. pub fn new() -> Self { @@ -47,7 +54,7 @@ impl Request { impl Response { /// Creates a new `Response` with the given `versions`. - pub fn new(versions: Vec) -> Self { + pub fn new(versions: Vec) -> Self { Self { versions } } @@ -57,6 +64,9 @@ impl Response { /// Matrix versions that can't be parsed to a `MatrixVersion`, and features with the boolean /// value set to `false` are discarded. pub fn as_supported_versions(&self) -> SupportedVersions { - SupportedVersions::from_parts(&self.versions, &BTreeMap::new()) + SupportedVersions::from_parts( + self.versions.iter().map(Version::as_str), + BTreeMap::::new().iter().map(|(k, v)| (k.as_str(), v)), + ) } } From 3cb939f5c8a67197433cbb3dc7e256f0ddaee978 Mon Sep 17 00:00:00 2001 From: dasha_uwu Date: Fri, 20 Feb 2026 00:41:04 +0500 Subject: [PATCH 4/6] Add function to iterate over aliases in RoomCanonicalAliasEventContent --- crates/ruma-events/src/room/canonical_alias.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/ruma-events/src/room/canonical_alias.rs b/crates/ruma-events/src/room/canonical_alias.rs index cb4597c48b..37500251ff 100644 --- a/crates/ruma-events/src/room/canonical_alias.rs +++ b/crates/ruma-events/src/room/canonical_alias.rs @@ -36,6 +36,11 @@ impl RoomCanonicalAliasEventContent { pub fn new() -> Self { Self { alias: None, alt_aliases: Vec::new() } } + + /// Returns an iterator over the canonical alias and alt aliases + pub fn aliases(&self) -> impl Iterator { + self.alias.iter().chain(self.alt_aliases.iter()) + } } #[cfg(test)] From 30d063c4503c3b630cdd55eda71a0bc3504518a2 Mon Sep 17 00:00:00 2001 From: June Strawberry Date: Sun, 15 Feb 2026 15:43:00 -0500 Subject: [PATCH 5/6] remove skip_serializing_if on empty display_name and avatar_url for /joined_members Signed-off-by: June Strawberry --- crates/ruma-client-api/src/membership/joined_members.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/ruma-client-api/src/membership/joined_members.rs b/crates/ruma-client-api/src/membership/joined_members.rs index 13fcc1cfc7..878fbd4f23 100644 --- a/crates/ruma-client-api/src/membership/joined_members.rs +++ b/crates/ruma-client-api/src/membership/joined_members.rs @@ -61,14 +61,12 @@ pub mod v3 { #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)] pub struct RoomMember { /// The display name of the user. - #[serde(skip_serializing_if = "Option::is_none")] pub display_name: Option, /// The mxc avatar url of the user. /// /// If you activate the `compat-empty-string-null` feature, this field being an empty /// string in JSON will result in `None` here during deserialization. - #[serde(skip_serializing_if = "Option::is_none")] #[cfg_attr( feature = "compat-empty-string-null", serde(default, deserialize_with = "ruma_common::serde::empty_string_as_none") From 45ff72e486a14d7224a426414c47fa66f25a4f05 Mon Sep 17 00:00:00 2001 From: ltoenjes Date: Sat, 14 Mar 2026 14:44:31 +0100 Subject: [PATCH 6/6] fix(msc4186): make timeline_limit optional in sliding sync request MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `#[serde(default)]` to `ListConfig::timeline_limit` so that clients which omit `timeline_limit` from their sliding sync list requests don't get a 400 M_BAD_JSON deserialization error. While MSC4186 marks `timeline_limit` as required, older matrix-rust-sdk builds (e.g. matrix-rust-components-swift ≤ 26.01.04) omit it from list configurations. Synapse resolved the same issue (element-hq/synapse#17704) by accepting requests without `timeline_limit` and defaulting to 0. The `ListConfig` struct already derives `Default`, so `UInt::default()` (0) is the natural fallback — no timeline events returned unless the client explicitly requests them. Ref: ruma/ruma#1914, element-hq/synapse#17704 --- crates/ruma-client-api/src/sync/sync_events/v5.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/ruma-client-api/src/sync/sync_events/v5.rs b/crates/ruma-client-api/src/sync/sync_events/v5.rs index 32271f2ba4..b9785da8f9 100644 --- a/crates/ruma-client-api/src/sync/sync_events/v5.rs +++ b/crates/ruma-client-api/src/sync/sync_events/v5.rs @@ -218,6 +218,7 @@ pub mod request { pub required_state: Vec<(StateEventType, StateKey)>, /// The maximum number of timeline events to return per room. + #[serde(default)] pub timeline_limit: UInt, }