From ab76112f5f518fb20e8d350201ba208acbdc0c0f Mon Sep 17 00:00:00 2001 From: Alan Evans Date: Thu, 23 Jul 2020 12:25:37 -0300 Subject: [PATCH] Prevent leading and trailing whitespace in group names. --- .../profiles/edit/EditProfileViewModel.java | 16 +++--- .../securesms/util/StringUtil.java | 37 +++++++++++-- .../StringUtilTest_whitespace_handling.java | 54 +++++++++++++++++++ .../api/groupsv2/GroupsV2Operations.java | 2 +- 4 files changed, 97 insertions(+), 12 deletions(-) create mode 100644 app/src/test/java/org/thoughtcrime/securesms/util/StringUtilTest_whitespace_handling.java diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileViewModel.java index c2de06b4ff..b03f440e97 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/edit/EditProfileViewModel.java @@ -13,7 +13,7 @@ import androidx.lifecycle.ViewModelProvider; import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.profiles.ProfileName; import org.thoughtcrime.securesms.util.StringUtil; -import org.thoughtcrime.securesms.util.livedata.LiveDataPair; +import org.thoughtcrime.securesms.util.livedata.LiveDataUtil; import org.whispersystems.libsignal.util.guava.Optional; import java.util.Objects; @@ -22,19 +22,20 @@ class EditProfileViewModel extends ViewModel { private final MutableLiveData givenName = new MutableLiveData<>(); private final MutableLiveData familyName = new MutableLiveData<>(); - private final LiveData internalProfileName = Transformations.map(new LiveDataPair<>(givenName, familyName), - pair -> ProfileName.fromParts(pair.first(), pair.second())); + private final LiveData trimmedGivenName = Transformations.map(givenName, StringUtil::trimToVisualBounds); + private final LiveData trimmedFamilyName = Transformations.map(familyName, StringUtil::trimToVisualBounds); + private final LiveData internalProfileName = LiveDataUtil.combineLatest(trimmedGivenName, trimmedFamilyName, ProfileName::fromParts); private final MutableLiveData internalAvatar = new MutableLiveData<>(); private final MutableLiveData originalAvatar = new MutableLiveData<>(); private final MutableLiveData> internalUsername = new MutableLiveData<>(); private final MutableLiveData originalDisplayName = new MutableLiveData<>(); - private final LiveData isFormValid = Transformations.map(givenName, name -> !StringUtil.isVisuallyEmpty(name)); + private final LiveData isFormValid = Transformations.map(trimmedGivenName, s -> s.length() > 0); private final EditProfileRepository repository; private final GroupId groupId; private EditProfileViewModel(@NonNull EditProfileRepository repository, boolean hasInstanceState, @Nullable GroupId groupId) { - this.repository = repository; - this.groupId = groupId; + this.repository = repository; + this.groupId = groupId; repository.getCurrentUsername(internalUsername::postValue); @@ -141,9 +142,8 @@ class EditProfileViewModel extends ViewModel { this.groupId = groupId; } - @NonNull @Override - public T create(@NonNull Class modelClass) { + public @NonNull T create(@NonNull Class modelClass) { //noinspection unchecked return (T) new EditProfileViewModel(repository, hasInstanceState, groupId); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/StringUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/StringUtil.java index 15bbf6f337..a3ae093ce9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/StringUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/StringUtil.java @@ -45,13 +45,44 @@ public final class StringUtil { return true; } - for (int i = 0; i < value.length(); i++) { + return indexOfFirstNonEmptyChar(value) == -1; + } + + /** + * @return String without any leading or trailing whitespace. + * Accounts for various unicode whitespace characters. + */ + public static String trimToVisualBounds(@NonNull String value) { + int start = indexOfFirstNonEmptyChar(value); + + if (start == -1) { + return ""; + } + + int end = indexOfLastNonEmptyChar(value); + + return value.substring(start, end + 1); + } + + private static int indexOfFirstNonEmptyChar(@NonNull String value) { + int length = value.length(); + + for (int i = 0; i < length; i++) { if (!isVisuallyEmpty(value.charAt(i))) { - return false; + return i; } } - return true; + return -1; + } + + private static int indexOfLastNonEmptyChar(@NonNull String value) { + for (int i = value.length() - 1; i >= 0; i--) { + if (!isVisuallyEmpty(value.charAt(i))) { + return i; + } + } + return -1; } /** diff --git a/app/src/test/java/org/thoughtcrime/securesms/util/StringUtilTest_whitespace_handling.java b/app/src/test/java/org/thoughtcrime/securesms/util/StringUtilTest_whitespace_handling.java new file mode 100644 index 0000000000..dbaae6ad61 --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/util/StringUtilTest_whitespace_handling.java @@ -0,0 +1,54 @@ +package org.thoughtcrime.securesms.util; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.Arrays; +import java.util.Collection; + +import static org.junit.Assert.assertEquals; + +@RunWith(Parameterized.class) +public final class StringUtilTest_whitespace_handling { + + private final String input; + private final String expectedTrimmed; + private final boolean isVisuallyEmpty; + + @Parameterized.Parameters + public static Collection data() { + return Arrays.asList(new Object[][]{ + + { "", "", true }, + { " ", "", true }, + { "A", "A", false }, + { " B", "B", false }, + { "C ", "C", false }, + + /* Unicode whitespace */ + { "\u200E", "", true }, + { "\u200F", "", true }, + { "\u2007", "", true }, + { "\u2007\u200FA\tB\u200EC\u200E\u200F", "A\tB\u200EC", false }, + + }); + } + + public StringUtilTest_whitespace_handling(String input, String expectedTrimmed, boolean isVisuallyEmpty) { + this.input = input; + this.expectedTrimmed = expectedTrimmed; + this.isVisuallyEmpty = isVisuallyEmpty; + } + + @Test + public void isVisuallyEmpty() { + assertEquals(isVisuallyEmpty, StringUtil.isVisuallyEmpty(input)); + } + + @Test + public void trim() { + assertEquals(expectedTrimmed, StringUtil.trimToVisualBounds(input)); + } + +} \ No newline at end of file diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations.java index 0e7ce59e34..2d4eca5bb8 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations.java @@ -545,7 +545,7 @@ public final class GroupsV2Operations { } private String decryptTitle(ByteString cipherText) { - return decryptBlob(cipherText).getTitle(); + return decryptBlob(cipherText).getTitle().trim(); } private int decryptDisappearingMessagesTimer(ByteString encryptedTimerMessage) {