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