matrix_sdk_base/
deserialized_responses.rs

1// Copyright 2022 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! SDK-specific variations of response types from Ruma.
16
17use std::{collections::BTreeMap, fmt, hash::Hash, iter};
18
19pub use matrix_sdk_common::deserialized_responses::*;
20use once_cell::sync::Lazy;
21use regex::Regex;
22use ruma::{
23    EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedUserId, UInt, UserId,
24    events::{
25        AnyStrippedStateEvent, AnySyncStateEvent, AnySyncTimelineEvent, EventContentFromType,
26        PossiblyRedactedStateEventContent, RedactContent, RedactedStateEventContent,
27        StateEventContent, StaticStateEventContent, StrippedStateEvent, SyncStateEvent,
28        room::{
29            member::{MembershipState, RoomMemberEvent, RoomMemberEventContent},
30            power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
31        },
32    },
33    room_version_rules::AuthorizationRules,
34    serde::Raw,
35};
36use serde::Serialize;
37use unicode_normalization::UnicodeNormalization;
38
39/// A change in ambiguity of room members that an `m.room.member` event
40/// triggers.
41#[derive(Clone, Debug)]
42#[non_exhaustive]
43pub struct AmbiguityChange {
44    /// The user ID of the member that is contained in the state key of the
45    /// `m.room.member` event.
46    pub member_id: OwnedUserId,
47    /// Is the member that is contained in the state key of the `m.room.member`
48    /// event itself ambiguous because of the event.
49    pub member_ambiguous: bool,
50    /// Has another user been disambiguated because of this event.
51    pub disambiguated_member: Option<OwnedUserId>,
52    /// Has another user become ambiguous because of this event.
53    pub ambiguated_member: Option<OwnedUserId>,
54}
55
56impl AmbiguityChange {
57    /// Get an iterator over the user IDs listed in this `AmbiguityChange`.
58    pub fn user_ids(&self) -> impl Iterator<Item = &UserId> {
59        iter::once(&*self.member_id)
60            .chain(self.disambiguated_member.as_deref())
61            .chain(self.ambiguated_member.as_deref())
62    }
63}
64
65/// Collection of ambiguity changes that room member events trigger.
66#[derive(Clone, Debug, Default)]
67#[non_exhaustive]
68pub struct AmbiguityChanges {
69    /// A map from room id to a map of an event id to the `AmbiguityChange` that
70    /// the event with the given id caused.
71    pub changes: BTreeMap<OwnedRoomId, BTreeMap<OwnedEventId, AmbiguityChange>>,
72}
73
74static MXID_REGEX: Lazy<Regex> = Lazy::new(|| {
75    Regex::new(DisplayName::MXID_PATTERN)
76        .expect("We should be able to create a regex from our static MXID pattern")
77});
78static LEFT_TO_RIGHT_REGEX: Lazy<Regex> = Lazy::new(|| {
79    Regex::new(DisplayName::LEFT_TO_RIGHT_PATTERN)
80        .expect("We should be able to create a regex from our static left-to-right pattern")
81});
82static HIDDEN_CHARACTERS_REGEX: Lazy<Regex> = Lazy::new(|| {
83    Regex::new(DisplayName::HIDDEN_CHARACTERS_PATTERN)
84        .expect("We should be able to create a regex from our static hidden characters pattern")
85});
86
87/// Regex to match `i` characters.
88///
89/// This is used to replace an `i` with a lowercase `l`, i.e. to mark "Hello"
90/// and "HeIlo" as ambiguous. Decancer will lowercase an `I` for us.
91static I_REGEX: Lazy<Regex> = Lazy::new(|| {
92    Regex::new("[i]").expect("We should be able to create a regex from our uppercase I pattern")
93});
94
95/// Regex to match `0` characters.
96///
97/// This is used to replace an `0` with a lowercase `o`, i.e. to mark "HellO"
98/// and "Hell0" as ambiguous. Decancer will lowercase an `O` for us.
99static ZERO_REGEX: Lazy<Regex> = Lazy::new(|| {
100    Regex::new("[0]").expect("We should be able to create a regex from our zero pattern")
101});
102
103/// Regex to match a couple of dot-like characters, also matches an actual dot.
104///
105/// This is used to replace a `.` with a `:`, i.e. to mark "@mxid.domain.tld" as
106/// ambiguous.
107static DOT_REGEX: Lazy<Regex> = Lazy::new(|| {
108    Regex::new("[.\u{1d16d}]").expect("We should be able to create a regex from our dot pattern")
109});
110
111/// A high-level wrapper for strings representing display names.
112///
113/// This wrapper provides attempts to determine whether a display name
114/// contains characters that could make it ambiguous or easily confused
115/// with similar names.
116///
117///
118/// # Examples
119///
120/// ```
121/// use matrix_sdk_base::deserialized_responses::DisplayName;
122///
123/// let display_name = DisplayName::new("๐’ฎ๐’ถ๐’ฝ๐’ถ๐“ˆ๐“‡๐’ถ๐’ฝ๐“๐’ถ");
124///
125/// // The normalized and sanitized string will be returned by DisplayName.as_normalized_str().
126/// assert_eq!(display_name.as_normalized_str(), Some("sahasrahla"));
127/// ```
128///
129/// ```
130/// # use matrix_sdk_base::deserialized_responses::DisplayName;
131/// let display_name = DisplayName::new("@alice:localhost");
132///
133/// // The display name looks like an MXID, which makes it ambiguous.
134/// assert!(display_name.is_inherently_ambiguous());
135/// ```
136#[derive(Debug, Clone, Eq)]
137pub struct DisplayName {
138    raw: String,
139    decancered: Option<String>,
140}
141
142impl Hash for DisplayName {
143    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
144        if let Some(decancered) = &self.decancered {
145            decancered.hash(state);
146        } else {
147            self.raw.hash(state);
148        }
149    }
150}
151
152impl PartialEq for DisplayName {
153    fn eq(&self, other: &Self) -> bool {
154        match (self.decancered.as_deref(), other.decancered.as_deref()) {
155            (None, None) => self.raw == other.raw,
156            (None, Some(_)) | (Some(_), None) => false,
157            (Some(this), Some(other)) => this == other,
158        }
159    }
160}
161
162impl DisplayName {
163    /// Regex pattern matching an MXID.
164    const MXID_PATTERN: &'static str = "@.+[:.].+";
165
166    /// Regex pattern matching some left-to-right formatting marks:
167    ///     * LTR and RTL marks U+200E and U+200F
168    ///     * LTR/RTL and other directional formatting marks U+202A - U+202F
169    const LEFT_TO_RIGHT_PATTERN: &'static str = "[\u{202a}-\u{202f}\u{200e}\u{200f}]";
170
171    /// Regex pattern matching bunch of unicode control characters and otherwise
172    /// misleading/invisible characters.
173    ///
174    /// This includes:
175    ///     * various width spaces U+2000 - U+200D
176    ///     * Combining characters U+0300 - U+036F
177    ///     * Blank/invisible characters (U2800, U2062-U2063)
178    ///     * Arabic Letter RTL mark U+061C
179    ///     * Zero width no-break space (BOM) U+FEFF
180    const HIDDEN_CHARACTERS_PATTERN: &'static str =
181        "[\u{2000}-\u{200D}\u{300}-\u{036f}\u{2062}-\u{2063}\u{2800}\u{061c}\u{feff}]";
182
183    /// Creates a new [`DisplayName`] from the given raw string.
184    ///
185    /// The raw display name is transformed into a Unicode-normalized form, with
186    /// common confusable characters removed to reduce ambiguity.
187    ///
188    /// **Note**: If removing confusable characters fails,
189    /// [`DisplayName::is_inherently_ambiguous`] will return `true`, and
190    /// [`DisplayName::as_normalized_str()`] will return `None.
191    pub fn new(raw: &str) -> Self {
192        let normalized = raw.nfd().collect::<String>();
193        let replaced = DOT_REGEX.replace_all(&normalized, ":");
194        let replaced = HIDDEN_CHARACTERS_REGEX.replace_all(&replaced, "");
195
196        let decancered = decancer::cure!(&replaced).ok().map(|cured| {
197            let removed_left_to_right = LEFT_TO_RIGHT_REGEX.replace_all(cured.as_ref(), "");
198            let replaced = I_REGEX.replace_all(&removed_left_to_right, "l");
199            // We re-run the dot replacement because decancer normalized a lot of weird
200            // characets into a `.`, it just doesn't do that for /u{1d16d}.
201            let replaced = DOT_REGEX.replace_all(&replaced, ":");
202            let replaced = ZERO_REGEX.replace_all(&replaced, "o");
203
204            replaced.to_string()
205        });
206
207        Self { raw: raw.to_owned(), decancered }
208    }
209
210    /// Is this display name considered to be ambiguous?
211    ///
212    /// If the display name has cancer (i.e. fails normalisation or has a
213    /// different normalised form) or looks like an MXID, then it's ambiguous.
214    pub fn is_inherently_ambiguous(&self) -> bool {
215        // If we look like an MXID or have hidden characters then we're ambiguous.
216        self.looks_like_an_mxid() || self.has_hidden_characters() || self.decancered.is_none()
217    }
218
219    /// Returns the underlying raw and and unsanitized string of this
220    /// [`DisplayName`].
221    pub fn as_raw_str(&self) -> &str {
222        &self.raw
223    }
224
225    /// Returns the underlying normalized and and sanitized string of this
226    /// [`DisplayName`].
227    ///
228    /// Returns `None` if normalization failed during construction of this
229    /// [`DisplayName`].
230    pub fn as_normalized_str(&self) -> Option<&str> {
231        self.decancered.as_deref()
232    }
233
234    fn has_hidden_characters(&self) -> bool {
235        HIDDEN_CHARACTERS_REGEX.is_match(&self.raw)
236    }
237
238    fn looks_like_an_mxid(&self) -> bool {
239        self.decancered
240            .as_deref()
241            .map(|d| MXID_REGEX.is_match(d))
242            .unwrap_or_else(|| MXID_REGEX.is_match(&self.raw))
243    }
244}
245
246/// A deserialized response for the rooms members API call.
247///
248/// [`GET /_matrix/client/r0/rooms/{roomId}/members`](https://spec.matrix.org/v1.5/client-server-api/#get_matrixclientv3roomsroomidmembers)
249#[derive(Clone, Debug, Default)]
250pub struct MembersResponse {
251    /// The list of members events.
252    pub chunk: Vec<RoomMemberEvent>,
253    /// Collection of ambiguity changes that room member events trigger.
254    pub ambiguity_changes: AmbiguityChanges,
255}
256
257/// Wrapper around both versions of any event received via sync.
258#[derive(Clone, Debug, Serialize)]
259#[serde(untagged)]
260pub enum RawAnySyncOrStrippedTimelineEvent {
261    /// An event from a room in joined or left state.
262    Sync(Raw<AnySyncTimelineEvent>),
263    /// An event from a room in invited state.
264    Stripped(Raw<AnyStrippedStateEvent>),
265}
266
267impl From<Raw<AnySyncTimelineEvent>> for RawAnySyncOrStrippedTimelineEvent {
268    fn from(event: Raw<AnySyncTimelineEvent>) -> Self {
269        Self::Sync(event)
270    }
271}
272
273impl From<Raw<AnyStrippedStateEvent>> for RawAnySyncOrStrippedTimelineEvent {
274    fn from(event: Raw<AnyStrippedStateEvent>) -> Self {
275        Self::Stripped(event)
276    }
277}
278
279/// Wrapper around both versions of any raw state event.
280#[derive(Clone, Debug, Serialize)]
281#[serde(untagged)]
282pub enum RawAnySyncOrStrippedState {
283    /// An event from a room in joined or left state.
284    Sync(Raw<AnySyncStateEvent>),
285    /// An event from a room in invited state.
286    Stripped(Raw<AnyStrippedStateEvent>),
287}
288
289impl RawAnySyncOrStrippedState {
290    /// Try to deserialize the inner JSON as the expected type.
291    pub fn deserialize(&self) -> serde_json::Result<AnySyncOrStrippedState> {
292        match self {
293            Self::Sync(raw) => Ok(AnySyncOrStrippedState::Sync(Box::new(raw.deserialize()?))),
294            Self::Stripped(raw) => {
295                Ok(AnySyncOrStrippedState::Stripped(Box::new(raw.deserialize()?)))
296            }
297        }
298    }
299
300    /// Turns this `RawAnySyncOrStrippedState` into `RawSyncOrStrippedState<C>`
301    /// without changing the underlying JSON.
302    pub fn cast<C>(self) -> RawSyncOrStrippedState<C>
303    where
304        C: StaticStateEventContent + RedactContent,
305        C::Redacted: RedactedStateEventContent,
306    {
307        match self {
308            Self::Sync(raw) => RawSyncOrStrippedState::Sync(raw.cast_unchecked()),
309            Self::Stripped(raw) => RawSyncOrStrippedState::Stripped(raw.cast_unchecked()),
310        }
311    }
312}
313
314/// Wrapper around both versions of any state event.
315#[derive(Clone, Debug)]
316pub enum AnySyncOrStrippedState {
317    /// An event from a room in joined or left state.
318    ///
319    /// The value is `Box`ed because it is quite large. Let's keep the size of
320    /// `Self` as small as possible.
321    Sync(Box<AnySyncStateEvent>),
322    /// An event from a room in invited state.
323    ///
324    /// The value is `Box`ed because it is quite large. Let's keep the size of
325    /// `Self` as small as possible.
326    Stripped(Box<AnyStrippedStateEvent>),
327}
328
329impl AnySyncOrStrippedState {
330    /// If this is an `AnySyncStateEvent`, return a reference to the inner
331    /// event.
332    pub fn as_sync(&self) -> Option<&AnySyncStateEvent> {
333        match self {
334            Self::Sync(ev) => Some(ev),
335            Self::Stripped(_) => None,
336        }
337    }
338
339    /// If this is an `AnyStrippedStateEvent`, return a reference to the inner
340    /// event.
341    pub fn as_stripped(&self) -> Option<&AnyStrippedStateEvent> {
342        match self {
343            Self::Sync(_) => None,
344            Self::Stripped(ev) => Some(ev),
345        }
346    }
347}
348
349/// Wrapper around both versions of a raw state event.
350#[derive(Clone, Debug, Serialize)]
351#[serde(untagged)]
352pub enum RawSyncOrStrippedState<C>
353where
354    C: StaticStateEventContent + RedactContent,
355    C::Redacted: RedactedStateEventContent,
356{
357    /// An event from a room in joined or left state.
358    Sync(Raw<SyncStateEvent<C>>),
359    /// An event from a room in invited state.
360    Stripped(Raw<StrippedStateEvent<C::PossiblyRedacted>>),
361}
362
363impl<C> RawSyncOrStrippedState<C>
364where
365    C: StaticStateEventContent + RedactContent,
366    C::Redacted: RedactedStateEventContent + fmt::Debug + Clone,
367{
368    /// Try to deserialize the inner JSON as the expected type.
369    pub fn deserialize(&self) -> serde_json::Result<SyncOrStrippedState<C>>
370    where
371        C: StaticStateEventContent + EventContentFromType + RedactContent,
372        C::Redacted: RedactedStateEventContent<StateKey = C::StateKey> + EventContentFromType,
373        C::PossiblyRedacted: PossiblyRedactedStateEventContent + EventContentFromType,
374    {
375        match self {
376            Self::Sync(ev) => Ok(SyncOrStrippedState::Sync(ev.deserialize()?)),
377            Self::Stripped(ev) => Ok(SyncOrStrippedState::Stripped(ev.deserialize()?)),
378        }
379    }
380}
381
382/// Raw version of [`MemberEvent`].
383pub type RawMemberEvent = RawSyncOrStrippedState<RoomMemberEventContent>;
384
385/// Wrapper around both versions of a state event.
386#[derive(Clone, Debug)]
387pub enum SyncOrStrippedState<C>
388where
389    C: StaticStateEventContent + RedactContent,
390    C::Redacted: RedactedStateEventContent + fmt::Debug + Clone,
391{
392    /// An event from a room in joined or left state.
393    Sync(SyncStateEvent<C>),
394    /// An event from a room in invited state.
395    Stripped(StrippedStateEvent<C::PossiblyRedacted>),
396}
397
398impl<C> SyncOrStrippedState<C>
399where
400    C: StaticStateEventContent + RedactContent,
401    C::Redacted: RedactedStateEventContent<StateKey = C::StateKey> + fmt::Debug + Clone,
402    C::PossiblyRedacted: PossiblyRedactedStateEventContent<StateKey = C::StateKey>,
403{
404    /// If this is a `SyncStateEvent`, return a reference to the inner event.
405    pub fn as_sync(&self) -> Option<&SyncStateEvent<C>> {
406        match self {
407            Self::Sync(ev) => Some(ev),
408            Self::Stripped(_) => None,
409        }
410    }
411
412    /// If this is a `StrippedStateEvent`, return a reference to the inner
413    /// event.
414    pub fn as_stripped(&self) -> Option<&StrippedStateEvent<C::PossiblyRedacted>> {
415        match self {
416            Self::Sync(_) => None,
417            Self::Stripped(ev) => Some(ev),
418        }
419    }
420
421    /// The sender of this event.
422    pub fn sender(&self) -> &UserId {
423        match self {
424            Self::Sync(e) => e.sender(),
425            Self::Stripped(e) => &e.sender,
426        }
427    }
428
429    /// The ID of this event.
430    pub fn event_id(&self) -> Option<&EventId> {
431        match self {
432            Self::Sync(e) => Some(e.event_id()),
433            Self::Stripped(_) => None,
434        }
435    }
436
437    /// The server timestamp of this event.
438    pub fn origin_server_ts(&self) -> Option<MilliSecondsSinceUnixEpoch> {
439        match self {
440            Self::Sync(e) => Some(e.origin_server_ts()),
441            Self::Stripped(_) => None,
442        }
443    }
444
445    /// The state key associated to this state event.
446    pub fn state_key(&self) -> &C::StateKey {
447        match self {
448            Self::Sync(e) => e.state_key(),
449            Self::Stripped(e) => &e.state_key,
450        }
451    }
452}
453
454impl<C> SyncOrStrippedState<C>
455where
456    C: StaticStateEventContent<PossiblyRedacted = C>
457        + RedactContent
458        + PossiblyRedactedStateEventContent,
459    C::Redacted: RedactedStateEventContent<StateKey = <C as StateEventContent>::StateKey>
460        + fmt::Debug
461        + Clone,
462{
463    /// The inner content of the wrapped event.
464    pub fn original_content(&self) -> Option<&C> {
465        match self {
466            Self::Sync(e) => e.as_original().map(|e| &e.content),
467            Self::Stripped(e) => Some(&e.content),
468        }
469    }
470}
471
472/// Wrapper around both MemberEvent-Types
473pub type MemberEvent = SyncOrStrippedState<RoomMemberEventContent>;
474
475impl MemberEvent {
476    /// The membership state of the user.
477    pub fn membership(&self) -> &MembershipState {
478        match self {
479            MemberEvent::Sync(e) => e.membership(),
480            MemberEvent::Stripped(e) => &e.content.membership,
481        }
482    }
483
484    /// The user id associated to this member event.
485    pub fn user_id(&self) -> &UserId {
486        self.state_key()
487    }
488
489    /// The name that should be displayed for this member event.
490    ///
491    /// It there is no `displayname` in the event's content, the localpart or
492    /// the user ID is returned.
493    pub fn display_name(&self) -> DisplayName {
494        DisplayName::new(
495            self.original_content()
496                .and_then(|c| c.displayname.as_deref())
497                .unwrap_or_else(|| self.user_id().localpart()),
498        )
499    }
500
501    /// The optional reason why the membership changed.
502    pub fn reason(&self) -> Option<&str> {
503        match self {
504            MemberEvent::Sync(SyncStateEvent::Original(c)) => c.content.reason.as_deref(),
505            MemberEvent::Stripped(e) => e.content.reason.as_deref(),
506            _ => None,
507        }
508    }
509
510    /// The optional timestamp for this member event.
511    pub fn timestamp(&self) -> Option<UInt> {
512        match self {
513            MemberEvent::Sync(SyncStateEvent::Original(c)) => Some(c.origin_server_ts.0),
514            _ => None,
515        }
516    }
517}
518
519impl SyncOrStrippedState<RoomPowerLevelsEventContent> {
520    /// The power levels of the event.
521    pub fn power_levels(
522        &self,
523        rules: &AuthorizationRules,
524        creators: Vec<OwnedUserId>,
525    ) -> RoomPowerLevels {
526        match self {
527            Self::Sync(e) => e.power_levels(rules, creators),
528            Self::Stripped(e) => e.power_levels(rules, creators),
529        }
530    }
531}
532
533#[cfg(test)]
534mod test {
535    macro_rules! assert_display_name_eq {
536        ($left:expr, $right:expr $(, $desc:expr)?) => {{
537            let left = crate::deserialized_responses::DisplayName::new($left);
538            let right = crate::deserialized_responses::DisplayName::new($right);
539
540            similar_asserts::assert_eq!(
541                left,
542                right
543                $(, $desc)?
544            );
545        }};
546    }
547
548    macro_rules! assert_display_name_ne {
549        ($left:expr, $right:expr $(, $desc:expr)?) => {{
550            let left = crate::deserialized_responses::DisplayName::new($left);
551            let right = crate::deserialized_responses::DisplayName::new($right);
552
553            assert_ne!(
554                left,
555                right
556                $(, $desc)?
557            );
558        }};
559    }
560
561    macro_rules! assert_ambiguous {
562        ($name:expr) => {
563            let name = crate::deserialized_responses::DisplayName::new($name);
564
565            assert!(
566                name.is_inherently_ambiguous(),
567                "The display {:?} should be considered amgibuous",
568                name
569            );
570        };
571    }
572
573    macro_rules! assert_not_ambiguous {
574        ($name:expr) => {
575            let name = crate::deserialized_responses::DisplayName::new($name);
576
577            assert!(
578                !name.is_inherently_ambiguous(),
579                "The display {:?} should not be considered amgibuous",
580                name
581            );
582        };
583    }
584
585    #[test]
586    fn test_display_name_inherently_ambiguous() {
587        // These should not be inherently ambiguous, only if another similarly looking
588        // display name appears should they be considered to be ambiguous.
589        assert_not_ambiguous!("Alice");
590        assert_not_ambiguous!("Carol");
591        assert_not_ambiguous!("Car0l");
592        assert_not_ambiguous!("Ivan");
593        assert_not_ambiguous!("๐’ฎ๐’ถ๐’ฝ๐’ถ๐“ˆ๐“‡๐’ถ๐’ฝ๐“๐’ถ");
594        assert_not_ambiguous!("โ“ˆโ“โ“—โ“โ“ขโ“กโ“โ“—โ“›โ“");
595        assert_not_ambiguous!("๐Ÿ…‚๐Ÿ„ฐ๐Ÿ„ท๐Ÿ„ฐ๐Ÿ…‚๐Ÿ…๐Ÿ„ฐ๐Ÿ„ท๐Ÿ„ป๐Ÿ„ฐ");
596        assert_not_ambiguous!("๏ผณ๏ฝ๏ฝˆ๏ฝ๏ฝ“๏ฝ’๏ฝ๏ฝˆ๏ฝŒ๏ฝ");
597        // Left to right is fine, if it's the only one in the room.
598        assert_not_ambiguous!("\u{202e}alharsahas");
599
600        // These on the other hand contain invisible chars.
601        assert_ambiguous!("Saฬดhasrahla");
602        assert_ambiguous!("Sahas\u{200D}rahla");
603    }
604
605    #[test]
606    fn test_display_name_equality_capitalization() {
607        // Display name with different capitalization
608        assert_display_name_eq!("Alice", "alice");
609    }
610
611    #[test]
612    fn test_display_name_equality_different_names() {
613        // Different display names
614        assert_display_name_ne!("Alice", "Carol");
615    }
616
617    #[test]
618    fn test_display_name_equality_capital_l() {
619        // Different display names
620        assert_display_name_eq!("Hello", "HeIlo");
621    }
622
623    #[test]
624    fn test_display_name_equality_confusable_zero() {
625        // Different display names
626        assert_display_name_eq!("Carol", "Car0l");
627    }
628
629    #[test]
630    fn test_display_name_equality_cyrillic() {
631        // Display name with scritpure symbols
632        assert_display_name_eq!("alice", "ะฐlice");
633    }
634
635    #[test]
636    fn test_display_name_equality_scriptures() {
637        // Display name with scritpure symbols
638        assert_display_name_eq!("Sahasrahla", "๐’ฎ๐’ถ๐’ฝ๐’ถ๐“ˆ๐“‡๐’ถ๐’ฝ๐“๐’ถ");
639    }
640
641    #[test]
642    fn test_display_name_equality_frakturs() {
643        // Display name with fraktur symbols
644        assert_display_name_eq!("Sahasrahla", "๐”–๐”ž๐”ฅ๐”ž๐”ฐ๐”ฏ๐”ž๐”ฅ๐”ฉ๐”ž");
645    }
646
647    #[test]
648    fn test_display_name_equality_circled() {
649        // Display name with circled symbols
650        assert_display_name_eq!("Sahasrahla", "โ“ˆโ“โ“—โ“โ“ขโ“กโ“โ“—โ“›โ“");
651    }
652
653    #[test]
654    fn test_display_name_equality_squared() {
655        // Display name with squared symbols
656        assert_display_name_eq!("Sahasrahla", "๐Ÿ…‚๐Ÿ„ฐ๐Ÿ„ท๐Ÿ„ฐ๐Ÿ…‚๐Ÿ…๐Ÿ„ฐ๐Ÿ„ท๐Ÿ„ป๐Ÿ„ฐ");
657    }
658
659    #[test]
660    fn test_display_name_equality_big_unicode() {
661        // Display name with big unicode letters
662        assert_display_name_eq!("Sahasrahla", "๏ผณ๏ฝ๏ฝˆ๏ฝ๏ฝ“๏ฝ’๏ฝ๏ฝˆ๏ฝŒ๏ฝ");
663    }
664
665    #[test]
666    fn test_display_name_equality_left_to_right() {
667        // Display name with a left-to-right character
668        assert_display_name_eq!("Sahasrahla", "\u{202e}alharsahas");
669    }
670
671    #[test]
672    fn test_display_name_equality_diacritical() {
673        // Display name with a diacritical mark.
674        assert_display_name_eq!("Sahasrahla", "Saฬดhasrahla");
675    }
676
677    #[test]
678    fn test_display_name_equality_zero_width_joiner() {
679        // Display name with a zero-width joiner
680        assert_display_name_eq!("Sahasrahla", "Sahas\u{200B}rahla");
681    }
682
683    #[test]
684    fn test_display_name_equality_zero_width_space() {
685        // Display name with zero-width space.
686        assert_display_name_eq!("Sahasrahla", "Sahas\u{200D}rahla");
687    }
688
689    #[test]
690    fn test_display_name_equality_ligatures() {
691        // Display name with a ligature.
692        assert_display_name_eq!("ff", "\u{FB00}");
693    }
694
695    #[test]
696    fn test_display_name_confusable_mxid_colon() {
697        assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{0589}domain.tld");
698        assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{05c3}domain.tld");
699        assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{0703}domain.tld");
700        assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{0a83}domain.tld");
701        assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{16ec}domain.tld");
702        assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{205a}domain.tld");
703        assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{2236}domain.tld");
704        assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{fe13}domain.tld");
705        assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{fe52}domain.tld");
706        assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{fe30}domain.tld");
707        assert_display_name_eq!("@mxid:domain.tld", "@mxid\u{ff1a}domain.tld");
708
709        // Additionally these should be considered to be ambiguous on their own.
710        assert_ambiguous!("@mxid\u{0589}domain.tld");
711        assert_ambiguous!("@mxid\u{05c3}domain.tld");
712        assert_ambiguous!("@mxid\u{0703}domain.tld");
713        assert_ambiguous!("@mxid\u{0a83}domain.tld");
714        assert_ambiguous!("@mxid\u{16ec}domain.tld");
715        assert_ambiguous!("@mxid\u{205a}domain.tld");
716        assert_ambiguous!("@mxid\u{2236}domain.tld");
717        assert_ambiguous!("@mxid\u{fe13}domain.tld");
718        assert_ambiguous!("@mxid\u{fe52}domain.tld");
719        assert_ambiguous!("@mxid\u{fe30}domain.tld");
720        assert_ambiguous!("@mxid\u{ff1a}domain.tld");
721    }
722
723    #[test]
724    fn test_display_name_confusable_mxid_dot() {
725        assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{0701}tld");
726        assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{0702}tld");
727        assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{2024}tld");
728        assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{fe52}tld");
729        assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{ff0e}tld");
730        assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain\u{1d16d}tld");
731
732        // Additionally these should be considered to be ambiguous on their own.
733        assert_ambiguous!("@mxid:domain\u{0701}tld");
734        assert_ambiguous!("@mxid:domain\u{0702}tld");
735        assert_ambiguous!("@mxid:domain\u{2024}tld");
736        assert_ambiguous!("@mxid:domain\u{fe52}tld");
737        assert_ambiguous!("@mxid:domain\u{ff0e}tld");
738        assert_ambiguous!("@mxid:domain\u{1d16d}tld");
739    }
740
741    #[test]
742    fn test_display_name_confusable_mxid_replacing_a() {
743        assert_display_name_eq!("@mxid:domain.tld", "@mxid:dom\u{1d44e}in.tld");
744        assert_display_name_eq!("@mxid:domain.tld", "@mxid:dom\u{0430}in.tld");
745
746        // Additionally these should be considered to be ambiguous on their own.
747        assert_ambiguous!("@mxid:dom\u{1d44e}in.tld");
748        assert_ambiguous!("@mxid:dom\u{0430}in.tld");
749    }
750
751    #[test]
752    fn test_display_name_confusable_mxid_replacing_l() {
753        assert_display_name_eq!("@mxid:domain.tld", "@mxid:domain.tId");
754        assert_display_name_eq!("mxid:domain.tld", "mxid:domain.t\u{217c}d");
755        assert_display_name_eq!("mxid:domain.tld", "mxid:domain.t\u{ff4c}d");
756        assert_display_name_eq!("mxid:domain.tld", "mxid:domain.t\u{1d5f9}d");
757        assert_display_name_eq!("mxid:domain.tld", "mxid:domain.t\u{1d695}d");
758        assert_display_name_eq!("mxid:domain.tld", "mxid:domain.t\u{2223}d");
759
760        // Additionally these should be considered to be ambiguous on their own.
761        assert_ambiguous!("@mxid:domain.tId");
762        assert_ambiguous!("@mxid:domain.t\u{217c}d");
763        assert_ambiguous!("@mxid:domain.t\u{ff4c}d");
764        assert_ambiguous!("@mxid:domain.t\u{1d5f9}d");
765        assert_ambiguous!("@mxid:domain.t\u{1d695}d");
766        assert_ambiguous!("@mxid:domain.t\u{2223}d");
767    }
768}