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