diff --git a/bindings/matrix-sdk-ffi/src/room_alias.rs b/bindings/matrix-sdk-ffi/src/room_alias.rs index 491820c5799..1ab5e5311c3 100644 --- a/bindings/matrix-sdk-ffi/src/room_alias.rs +++ b/bindings/matrix-sdk-ffi/src/room_alias.rs @@ -1,10 +1,13 @@ use matrix_sdk::RoomDisplayName; -use ruma::RoomAliasId; -/// Verifies the passed `String` matches the expected room alias format. +/// Verifies the passed `String` matches the expected room alias format: +/// +/// This means it's lowercase, with no whitespace chars, has a single leading +/// `#` char and a single `:` separator between the local and domain parts, and +/// the local part only contains characters that can't be percent encoded. #[matrix_sdk_ffi_macros::export] fn is_room_alias_format_valid(alias: String) -> bool { - RoomAliasId::parse(alias).is_ok() + matrix_sdk::utils::is_room_alias_format_valid(alias) } /// Transforms a Room's display name into a valid room alias name. diff --git a/crates/matrix-sdk-base/src/rooms/mod.rs b/crates/matrix-sdk-base/src/rooms/mod.rs index b0aafcb87ce..cd884e9c898 100644 --- a/crates/matrix-sdk-base/src/rooms/mod.rs +++ b/crates/matrix-sdk-base/src/rooms/mod.rs @@ -66,7 +66,7 @@ pub enum RoomDisplayName { } const WHITESPACE_REGEX: &str = r"\s+"; -const INVALID_SYMBOLS_REGEX: &str = r"[#,:]+"; +const INVALID_SYMBOLS_REGEX: &str = r"[#,:\{\}\\]+"; impl RoomDisplayName { /// Transforms the current display name into the name part of a @@ -636,7 +636,7 @@ mod tests { fn test_room_alias_from_room_display_name_removes_invalid_ascii_symbols() { assert_eq!( "roomalias", - RoomDisplayName::Named("#Room,Alias:".to_owned()).to_room_alias_name() + RoomDisplayName::Named("#Room,{Alias}:".to_owned()).to_room_alias_name() ); } } diff --git a/crates/matrix-sdk/src/utils.rs b/crates/matrix-sdk/src/utils.rs index bef0b63b042..327c0410176 100644 --- a/crates/matrix-sdk/src/utils.rs +++ b/crates/matrix-sdk/src/utils.rs @@ -24,6 +24,7 @@ use futures_util::StreamExt; use ruma::{ events::{AnyMessageLikeEventContent, AnyStateEventContent}, serde::Raw, + RoomAliasId, }; use serde_json::value::{RawValue as RawJsonValue, Value as JsonValue}; #[cfg(feature = "e2e-encryption")] @@ -190,8 +191,37 @@ impl IntoRawStateEventContent for &Box { } } +const INVALID_ROOM_ALIAS_NAME_CHARS: &str = "#,:{}\\"; + +/// Verifies the passed `String` matches the expected room alias format: +/// +/// This means it's lowercase, with no whitespace chars, has a single leading +/// `#` char and a single `:` separator between the local and domain parts, and +/// the local part only contains characters that can't be percent encoded. +pub fn is_room_alias_format_valid(alias: String) -> bool { + let alias_parts: Vec<&str> = alias.split(':').collect(); + if alias_parts.len() != 2 { + return false; + } + + let local_part = alias_parts[0]; + let has_valid_format = local_part.chars().skip(1).all(|c| { + c.is_ascii() + && !c.is_whitespace() + && !c.is_control() + && !INVALID_ROOM_ALIAS_NAME_CHARS.contains(c) + }); + + let is_lowercase = alias.to_lowercase() == alias; + + // Checks both the local part and the domain part + has_valid_format && is_lowercase && RoomAliasId::parse(alias).is_ok() +} + #[cfg(test)] mod test { + use crate::utils::is_room_alias_format_valid; + #[cfg(feature = "e2e-encryption")] #[test] fn test_channel_observable_get_set() { @@ -202,4 +232,54 @@ mod test { assert_eq!(observable.set(10), 1); assert_eq!(observable.get(), 10); } + + #[test] + fn test_is_room_alias_format_valid_when_it_has_no_leading_hash_char_is_not_valid() { + assert!(!is_room_alias_format_valid("alias:domain.org".to_owned())) + } + + #[test] + fn test_is_room_alias_format_valid_when_it_has_several_colon_chars_is_not_valid() { + assert!(!is_room_alias_format_valid("#alias:something:domain.org".to_owned())) + } + + #[test] + fn test_is_room_alias_format_valid_when_it_has_no_colon_chars_is_not_valid() { + assert!(!is_room_alias_format_valid("#alias.domain.org".to_owned())) + } + + #[test] + fn test_is_room_alias_format_valid_when_server_part_is_not_valid() { + assert!(!is_room_alias_format_valid("#alias:".to_owned())) + } + + #[test] + fn test_is_room_alias_format_valid_when_name_part_has_whitespace_is_not_valid() { + assert!(!is_room_alias_format_valid("#alias with whitespace:domain.org".to_owned())) + } + + #[test] + fn test_is_room_alias_format_valid_when_name_part_has_control_char_is_not_valid() { + assert!(!is_room_alias_format_valid("#alias\u{0009}:domain.org".to_owned())) + } + + #[test] + fn test_is_room_alias_format_valid_when_name_part_has_invalid_char_is_not_valid() { + assert!(!is_room_alias_format_valid("#a#lias,{t\\est}:domain.org".to_owned())) + } + + #[test] + fn test_is_room_alias_format_valid_when_name_part_is_not_lowercase_is_not_valid() { + assert!(!is_room_alias_format_valid("#Alias:domain.org".to_owned())) + } + + #[test] + fn test_is_room_alias_format_valid_when_server_part_is_not_lowercase_is_not_valid() { + assert!(!is_room_alias_format_valid("#alias:Domain.org".to_owned())) + } + + #[test] + fn test_is_room_alias_format_valid_when_has_valid_format() { + assert!(is_room_alias_format_valid("#alias.test:domain.org".to_owned())) + } }