diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditFragment.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditFragment.java index 8a56a1ef05..5c82d09622 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditFragment.java @@ -182,7 +182,7 @@ public class UsernameEditFragment extends LoggingFragment { case DISCRIMINATOR_TOO_LONG -> getString(R.string.UsernameEditFragment__invalid_username_enter_a_maximum_of_d_digits, UsernameUtil.MAX_DISCRIMINATOR_LENGTH); case DISCRIMINATOR_TOO_SHORT -> getString(R.string.UsernameEditFragment__invalid_username_enter_a_minimum_of_d_digits, UsernameUtil.MIN_DISCRIMINATOR_LENGTH); case DISCRIMINATOR_CANNOT_BE_00 -> getString(R.string.UsernameEditFragment__this_number_cant_be_00); - case DISCRIMINATOR_CANNOT_START_WITH_00 -> getString(R.string.UsernameEditFragment__this_number_cant_start_with_00); + case DISCRIMINATOR_CANNOT_START_WITH_0 -> getString(R.string.UsernameEditFragment__this_number_cant_start_with_0); }; int colorRes = error != null ? R.color.signal_colorError : R.color.signal_colorPrimary; diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditViewModel.kt index d5d4cb2bac..dbe059e317 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/UsernameEditViewModel.kt @@ -22,7 +22,7 @@ import org.thoughtcrime.securesms.profiles.manage.UsernameRepository.UsernameSet import org.thoughtcrime.securesms.util.NetworkUtil import org.thoughtcrime.securesms.util.UsernameUtil.InvalidReason import org.thoughtcrime.securesms.util.UsernameUtil.checkDiscriminator -import org.thoughtcrime.securesms.util.UsernameUtil.checkUsername +import org.thoughtcrime.securesms.util.UsernameUtil.checkNickname import org.thoughtcrime.securesms.util.rx.RxStore import org.whispersystems.signalservice.api.util.Usernames import java.util.concurrent.TimeUnit @@ -159,7 +159,7 @@ internal class UsernameEditViewModel private constructor(private val mode: Usern return } - val invalidReason: InvalidReason? = checkUsername(usernameState.getNickname()) + val invalidReason: InvalidReason? = checkNickname(usernameState.getNickname()) if (invalidReason != null) { Log.w(TAG, "Username was submitted, but did not pass validity checks. Reason: $invalidReason") uiState.update { it.copy(buttonState = ButtonState.SUBMIT_DISABLED, usernameStatus = mapNicknameError(invalidReason)) } @@ -257,7 +257,7 @@ internal class UsernameEditViewModel private constructor(private val mode: Usern return } - val invalidReason: InvalidReason? = checkUsername(nickname) + val invalidReason: InvalidReason? = checkNickname(nickname) if (invalidReason != null) { uiState.update { uiState -> uiState.copy( @@ -366,7 +366,7 @@ internal class UsernameEditViewModel private constructor(private val mode: Usern DISCRIMINATOR_TOO_LONG, DISCRIMINATOR_HAS_INVALID_CHARACTERS, DISCRIMINATOR_CANNOT_BE_00, - DISCRIMINATOR_CANNOT_START_WITH_00 + DISCRIMINATOR_CANNOT_START_WITH_0 } enum class ButtonState { @@ -394,7 +394,9 @@ internal class UsernameEditViewModel private constructor(private val mode: Usern InvalidReason.TOO_LONG -> UsernameStatus.TOO_LONG InvalidReason.STARTS_WITH_NUMBER -> UsernameStatus.CANNOT_START_WITH_NUMBER InvalidReason.INVALID_CHARACTERS -> UsernameStatus.INVALID_CHARACTERS - InvalidReason.INVALID_NUMBER, InvalidReason.INVALID_NUMBER_PREFIX -> error("Unexpected reason $invalidReason") + InvalidReason.INVALID_NUMBER, + InvalidReason.INVALID_NUMBER_00, + InvalidReason.INVALID_NUMBER_PREFIX_0 -> error("Unexpected reason $invalidReason") } } @@ -403,8 +405,8 @@ internal class UsernameEditViewModel private constructor(private val mode: Usern InvalidReason.TOO_SHORT -> UsernameStatus.DISCRIMINATOR_TOO_SHORT InvalidReason.TOO_LONG -> UsernameStatus.DISCRIMINATOR_TOO_LONG InvalidReason.INVALID_CHARACTERS -> UsernameStatus.DISCRIMINATOR_HAS_INVALID_CHARACTERS - InvalidReason.INVALID_NUMBER -> UsernameStatus.DISCRIMINATOR_CANNOT_BE_00 - InvalidReason.INVALID_NUMBER_PREFIX -> UsernameStatus.DISCRIMINATOR_CANNOT_START_WITH_00 + InvalidReason.INVALID_NUMBER_00 -> UsernameStatus.DISCRIMINATOR_CANNOT_BE_00 + InvalidReason.INVALID_NUMBER_PREFIX_0 -> UsernameStatus.DISCRIMINATOR_CANNOT_START_WITH_0 else -> UsernameStatus.INVALID_GENERIC } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/UsernameUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/util/UsernameUtil.kt index 5ac6dc053b..b636a6c200 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/UsernameUtil.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/UsernameUtil.kt @@ -1,6 +1,19 @@ package org.thoughtcrime.securesms.util import org.signal.core.util.logging.Log +import org.signal.libsignal.usernames.BadDiscriminatorCharacterException +import org.signal.libsignal.usernames.BadNicknameCharacterException +import org.signal.libsignal.usernames.BaseUsernameException +import org.signal.libsignal.usernames.CannotBeEmptyException +import org.signal.libsignal.usernames.CannotStartWithDigitException +import org.signal.libsignal.usernames.DiscriminatorCannotBeEmptyException +import org.signal.libsignal.usernames.DiscriminatorCannotBeSingleDigitException +import org.signal.libsignal.usernames.DiscriminatorCannotBeZeroException +import org.signal.libsignal.usernames.DiscriminatorCannotHaveLeadingZerosException +import org.signal.libsignal.usernames.DiscriminatorTooLargeException +import org.signal.libsignal.usernames.NicknameTooLongException +import org.signal.libsignal.usernames.NicknameTooShortException +import org.signal.libsignal.usernames.Username import java.util.Locale import java.util.regex.Pattern @@ -36,52 +49,75 @@ object UsernameUtil { } @JvmStatic - fun checkUsername(value: String?): InvalidReason? { - return when { - value == null -> { - InvalidReason.TOO_SHORT - } - value.length < MIN_NICKNAME_LENGTH -> { - InvalidReason.TOO_SHORT - } - value.length > MAX_NICKNAME_LENGTH -> { - InvalidReason.TOO_LONG - } - DIGIT_START_PATTERN.matcher(value).matches() -> { - InvalidReason.STARTS_WITH_NUMBER - } - !FULL_PATTERN.matcher(value).matches() -> { - InvalidReason.INVALID_CHARACTERS - } - else -> { - null - } + fun checkNickname(value: String?): InvalidReason? { + if (value == null) { + return InvalidReason.TOO_SHORT + } + + return try { + // We only want to check the nickname, so we pass in a known-valid discriminator + Username.fromParts(value, "01", MIN_NICKNAME_LENGTH, MAX_NICKNAME_LENGTH) + null + } catch (e: BadNicknameCharacterException) { + InvalidReason.INVALID_CHARACTERS + } catch (e: CannotBeEmptyException) { + InvalidReason.TOO_SHORT + } catch (e: CannotStartWithDigitException) { + InvalidReason.STARTS_WITH_NUMBER + } catch (e: NicknameTooLongException) { + InvalidReason.TOO_LONG + } catch (e: NicknameTooShortException) { + InvalidReason.TOO_SHORT + } catch (e: BaseUsernameException) { + Log.w(TAG, "Unhandled verification exception!", e) + InvalidReason.INVALID_CHARACTERS } } fun checkDiscriminator(value: String?): InvalidReason? { - return when { - value == null -> { - null - } - value == "00" -> { - InvalidReason.INVALID_NUMBER - } - value.startsWith("00") -> { - InvalidReason.INVALID_NUMBER_PREFIX - } - value.length < MIN_DISCRIMINATOR_LENGTH -> { + if (value == null) { + return null + } + + if (value.length < MIN_DISCRIMINATOR_LENGTH) { + return InvalidReason.TOO_SHORT + } + + if (value.length > MAX_DISCRIMINATOR_LENGTH) { + return InvalidReason.TOO_LONG + } + + return try { + // We only want to check the discriminator, so we pass in a known-valid nickname + Username.fromParts("spiderman", value, MIN_NICKNAME_LENGTH, MAX_NICKNAME_LENGTH) + null + } catch (e: BadDiscriminatorCharacterException) { + InvalidReason.INVALID_CHARACTERS + } catch (e: DiscriminatorCannotBeEmptyException) { + InvalidReason.TOO_SHORT + } catch (e: DiscriminatorCannotBeSingleDigitException) { + InvalidReason.TOO_SHORT + } catch (e: DiscriminatorCannotBeZeroException) { + if (value.length < 2) { InvalidReason.TOO_SHORT + } else if (value == "00") { + InvalidReason.INVALID_NUMBER_00 + } else { + InvalidReason.INVALID_NUMBER_PREFIX_0 } - value.length > MAX_DISCRIMINATOR_LENGTH -> { - InvalidReason.TOO_LONG - } - value.toIntOrNull() == null -> { - InvalidReason.INVALID_CHARACTERS - } - else -> { - null + } catch (e: DiscriminatorCannotHaveLeadingZerosException) { + if (value.length < 2) { + InvalidReason.TOO_SHORT + } else if (value == "00") { + InvalidReason.INVALID_NUMBER_00 + } else { + InvalidReason.INVALID_NUMBER_PREFIX_0 } + } catch (e: DiscriminatorTooLargeException) { + InvalidReason.TOO_LONG + } catch (e: BaseUsernameException) { + Log.w(TAG, "Unhandled verification exception!", e) + InvalidReason.INVALID_CHARACTERS } } @@ -91,6 +127,7 @@ object UsernameUtil { INVALID_CHARACTERS, STARTS_WITH_NUMBER, INVALID_NUMBER, - INVALID_NUMBER_PREFIX + INVALID_NUMBER_00, + INVALID_NUMBER_PREFIX_0 } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c88c4e89e8..682b1daea3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2320,8 +2320,8 @@ Invalid username, enter a maximum of %1$d digits. This number can\'t be 00. Enter a digit between 1–9 - - This number can\'t start with 00. Enter a digit between 1–9 + + Numbers with more than 2 digits can\'t start with 0 Recovering your username will reset your existing QR code and link. Are you sure? diff --git a/app/src/test/java/org/thoughtcrime/securesms/util/UsernameUtilTest.kt b/app/src/test/java/org/thoughtcrime/securesms/util/UsernameUtilTest.kt index 6197945dfc..e49c90e495 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/util/UsernameUtilTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/util/UsernameUtilTest.kt @@ -3,45 +3,81 @@ package org.thoughtcrime.securesms.util import org.junit.Test import org.thoughtcrime.securesms.assertIs import org.thoughtcrime.securesms.assertIsNull -import org.thoughtcrime.securesms.util.UsernameUtil.checkUsername +import org.thoughtcrime.securesms.util.UsernameUtil.checkDiscriminator +import org.thoughtcrime.securesms.util.UsernameUtil.checkNickname class UsernameUtilTest { @Test fun checkUsername_tooShort() { - checkUsername(null) assertIs UsernameUtil.InvalidReason.TOO_SHORT - checkUsername("") assertIs UsernameUtil.InvalidReason.TOO_SHORT - checkUsername("ab") assertIs UsernameUtil.InvalidReason.TOO_SHORT + checkNickname(null) assertIs UsernameUtil.InvalidReason.TOO_SHORT + checkNickname("") assertIs UsernameUtil.InvalidReason.TOO_SHORT + checkNickname("ab") assertIs UsernameUtil.InvalidReason.TOO_SHORT } @Test fun checkUsername_tooLong() { - checkUsername("abcdefghijklmnopqrstuvwxyz1234567") assertIs UsernameUtil.InvalidReason.TOO_LONG + checkNickname("abcdefghijklmnopqrstuvwxyz1234567") assertIs UsernameUtil.InvalidReason.TOO_LONG } @Test fun checkUsername_startsWithNumber() { - checkUsername("0abcdefg") assertIs UsernameUtil.InvalidReason.STARTS_WITH_NUMBER - checkUsername("9abcdefg") assertIs UsernameUtil.InvalidReason.STARTS_WITH_NUMBER - checkUsername("8675309") assertIs UsernameUtil.InvalidReason.STARTS_WITH_NUMBER + checkNickname("0abcdefg") assertIs UsernameUtil.InvalidReason.STARTS_WITH_NUMBER + checkNickname("9abcdefg") assertIs UsernameUtil.InvalidReason.STARTS_WITH_NUMBER + checkNickname("8675309") assertIs UsernameUtil.InvalidReason.STARTS_WITH_NUMBER } @Test fun checkUsername_invalidCharacters() { - checkUsername("\$abcd") assertIs UsernameUtil.InvalidReason.INVALID_CHARACTERS - checkUsername(" abcd") assertIs UsernameUtil.InvalidReason.INVALID_CHARACTERS - checkUsername("ab cde") assertIs UsernameUtil.InvalidReason.INVALID_CHARACTERS - checkUsername("%%%%%") assertIs UsernameUtil.InvalidReason.INVALID_CHARACTERS - checkUsername("-----") assertIs UsernameUtil.InvalidReason.INVALID_CHARACTERS - checkUsername("asĸ_me") assertIs UsernameUtil.InvalidReason.INVALID_CHARACTERS - checkUsername("+18675309") assertIs UsernameUtil.InvalidReason.INVALID_CHARACTERS + checkNickname("\$abcd") assertIs UsernameUtil.InvalidReason.INVALID_CHARACTERS + checkNickname(" abcd") assertIs UsernameUtil.InvalidReason.INVALID_CHARACTERS + checkNickname("ab cde") assertIs UsernameUtil.InvalidReason.INVALID_CHARACTERS + checkNickname("%%%%%") assertIs UsernameUtil.InvalidReason.INVALID_CHARACTERS + checkNickname("-----") assertIs UsernameUtil.InvalidReason.INVALID_CHARACTERS + checkNickname("asĸ_me") assertIs UsernameUtil.InvalidReason.INVALID_CHARACTERS + checkNickname("+18675309") assertIs UsernameUtil.InvalidReason.INVALID_CHARACTERS } @Test fun checkUsername_validUsernames() { - checkUsername("abcd").assertIsNull() - checkUsername("abcdefghijklmnopqrstuvwxyz").assertIsNull() - checkUsername("ABCDEFGHIJKLMNOPQRSTUVWXYZ").assertIsNull() - checkUsername("web_head").assertIsNull() - checkUsername("Spider_Fan_1991").assertIsNull() + checkNickname("abcd").assertIsNull() + checkNickname("abcdefghijklmnopqrstuvwxyz").assertIsNull() + checkNickname("ABCDEFGHIJKLMNOPQRSTUVWXYZ").assertIsNull() + checkNickname("web_head").assertIsNull() + checkNickname("Spider_Fan_1991").assertIsNull() + } + + @Test + fun checkDiscriminator_valid() { + checkDiscriminator(null).assertIsNull() + checkDiscriminator("01").assertIsNull() + checkDiscriminator("111111111").assertIsNull() + } + + @Test + fun checkDiscriminator_tooShort() { + checkDiscriminator("0") assertIs UsernameUtil.InvalidReason.TOO_SHORT + checkDiscriminator("") assertIs UsernameUtil.InvalidReason.TOO_SHORT + } + + @Test + fun checkDiscriminator_tooLong() { + checkDiscriminator("1111111111") assertIs UsernameUtil.InvalidReason.TOO_LONG + } + + @Test + fun checkDiscriminator_00() { + checkDiscriminator("00") assertIs UsernameUtil.InvalidReason.INVALID_NUMBER_00 + } + + @Test + fun checkDiscriminator_prefixZero() { + checkDiscriminator("001") assertIs UsernameUtil.InvalidReason.INVALID_NUMBER_PREFIX_0 + checkDiscriminator("0001") assertIs UsernameUtil.InvalidReason.INVALID_NUMBER_PREFIX_0 + checkDiscriminator("011") assertIs UsernameUtil.InvalidReason.INVALID_NUMBER_PREFIX_0 + } + + fun checkDiscriminator_invalidChars() { + checkDiscriminator("a1") assertIs UsernameUtil.InvalidReason.INVALID_CHARACTERS + checkDiscriminator("1x") assertIs UsernameUtil.InvalidReason.INVALID_CHARACTERS } }