Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-30 15:16:50 +02:00
parent 066ab573b3
commit 3fefb7bf52
21 changed files with 944 additions and 56 deletions

View file

@ -3,6 +3,7 @@ package com.tangem.domain.addressbook.model
import arrow.core.Either
import arrow.core.raise.either
import arrow.core.raise.ensure
import com.tangem.domain.addressbook.model.ContactName.Companion.invoke
import kotlinx.serialization.Serializable
/**
@ -31,11 +32,24 @@ data class ContactName private constructor(val value: String) {
companion object {
const val MIN_LENGTH = 1
private const val MIN_LENGTH = 1
const val MAX_LENGTH = 50
/** Letters, numbers and spaces only — forbids emoji, new lines, tabs, special symbols and html/scripts. */
private val allowedPattern = Regex("^[\\p{L}\\p{N} ]+$")
/**
* Allows letters of any locale (`\p{L}`) and their combining marks (`\p{M}`, which also covers emoji
* variation selectors and keycap marks), digits (`\p{N}`), a regular space, and emoji symbols (`\p{So}`,
* including flags / regional indicators), emoji skin-tone modifiers (`\p{Sk}`) and the zero-width joiner
* (U+200D) used in emoji sequences.
*
* Everything else is rejected, which covers the forbidden set: line breaks, tabs and other control
* characters, invisible/format unicode (zero-width spaces, BOM, ), exotic spaces, and HTML/script symbols.
*
* The leading lookahead requires at least one visible "base" character (letter / digit / emoji symbol), so a
* name made up only of zero-width joiners, combining marks, modifiers or spaces (i.e. effectively invisible)
* is rejected.
*/
private val allowedPattern =
Regex("^(?=.*[\\p{L}\\p{N}\\p{So}])[\\p{L}\\p{M}\\p{N}\\p{So}\\p{Sk}\\u0020\\u200D]+$")
operator fun invoke(value: String): Either<Error, ContactName> = either {
val trimmed = value.trim()

View file

@ -39,8 +39,33 @@ class ContactNameTest {
}
@Test
fun `emoji is rejected`() {
assertThat(ContactName("Alice 😀").leftOrNull()).isEqualTo(ContactName.Error.InvalidCharacters)
fun `simple emoji is accepted`() {
assertThat(ContactName("Alice 😀").isRight()).isTrue()
}
@Test
fun `emoji-only name is accepted`() {
assertThat(ContactName("😀").isRight()).isTrue()
}
@Test
fun `flag emoji is accepted`() {
assertThat(ContactName("Team 🇺🇸").isRight()).isTrue()
}
@Test
fun `zwj emoji sequence is accepted`() {
assertThat(ContactName("Family 👨‍👩‍👧").isRight()).isTrue()
}
@Test
fun `emoji with variation selector is accepted`() {
assertThat(ContactName("Love ❤️").isRight()).isTrue()
}
@Test
fun `non-latin letters are accepted`() {
assertThat(ContactName("Алёша 大阪").isRight()).isTrue()
}
@Test
@ -53,6 +78,16 @@ class ContactNameTest {
assertThat(ContactName("Ali\tce").leftOrNull()).isEqualTo(ContactName.Error.InvalidCharacters)
}
@Test
fun `zero-width space is rejected`() {
assertThat(ContactName("Ali\u200Bce").leftOrNull()).isEqualTo(ContactName.Error.InvalidCharacters)
}
@Test
fun `non-breaking space is rejected`() {
assertThat(ContactName("Ali\u00A0ce").leftOrNull()).isEqualTo(ContactName.Error.InvalidCharacters)
}
@Test
fun `html script is rejected`() {
assertThat(ContactName("<script>alert(1)</script>").leftOrNull())