From 44050e9c004c88f4970787952f3ef85696674363 Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Tue, 18 Feb 2025 16:34:52 -0500 Subject: [PATCH 1/7] Use lazy encoding in utf-8 encoded string comparison --- .../firebase/firestore/FirestoreTest.java | 160 +++++++++++++----- .../google/firebase/firestore/util/Util.java | 28 ++- 2 files changed, 145 insertions(+), 43 deletions(-) diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/FirestoreTest.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/FirestoreTest.java index 796632e192e..58d3b4782df 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/FirestoreTest.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/FirestoreTest.java @@ -1658,17 +1658,33 @@ public void sdkOrdersQueryByDocumentIdTheSameWayOnlineAndOffline() { public void snapshotListenerSortsUnicodeStringsAsServer() { Map> testDocs = map( - "a", map("value", "Łukasiewicz"), - "b", map("value", "Sierpiński"), - "c", map("value", "岩澤"), - "d", map("value", "🄟"), - "e", map("value", "P"), - "f", map("value", "︒"), - "g", map("value", "🐵")); + "a", + map("value", "Łukasiewicz"), + "b", + map("value", "Sierpiński"), + "c", + map("value", "岩澤"), + "d", + map("value", "🄟"), + "e", + map("value", "P"), + "f", + map("value", "︒"), + "g", + map("value", "🐵"), + "h", + map("value", "你好"), + "i", + map("value", "你顥"), + "j", + map("value", "😁"), + "k", + map("value", "😀")); CollectionReference colRef = testCollectionWithDocs(testDocs); Query orderedQuery = colRef.orderBy("value"); - List expectedDocIds = Arrays.asList("b", "a", "c", "f", "e", "d", "g"); + List expectedDocIds = + Arrays.asList("b", "a", "h", "i", "c", "f", "e", "d", "g", "k", "j"); QuerySnapshot getSnapshot = waitFor(orderedQuery.get()); List getSnapshotDocIds = @@ -1699,17 +1715,33 @@ public void snapshotListenerSortsUnicodeStringsAsServer() { public void snapshotListenerSortsUnicodeStringsInArrayAsServer() { Map> testDocs = map( - "a", map("value", Arrays.asList("Łukasiewicz")), - "b", map("value", Arrays.asList("Sierpiński")), - "c", map("value", Arrays.asList("岩澤")), - "d", map("value", Arrays.asList("🄟")), - "e", map("value", Arrays.asList("P")), - "f", map("value", Arrays.asList("︒")), - "g", map("value", Arrays.asList("🐵"))); + "a", + map("value", Arrays.asList("Łukasiewicz")), + "b", + map("value", Arrays.asList("Sierpiński")), + "c", + map("value", Arrays.asList("岩澤")), + "d", + map("value", Arrays.asList("🄟")), + "e", + map("value", Arrays.asList("P")), + "f", + map("value", Arrays.asList("︒")), + "g", + map("value", Arrays.asList("🐵")), + "h", + map("value", Arrays.asList("你好")), + "i", + map("value", Arrays.asList("你顥")), + "j", + map("value", Arrays.asList("😁")), + "k", + map("value", Arrays.asList("😀"))); CollectionReference colRef = testCollectionWithDocs(testDocs); Query orderedQuery = colRef.orderBy("value"); - List expectedDocIds = Arrays.asList("b", "a", "c", "f", "e", "d", "g"); + List expectedDocIds = + Arrays.asList("b", "a", "h", "i", "c", "f", "e", "d", "g", "k", "j"); QuerySnapshot getSnapshot = waitFor(orderedQuery.get()); List getSnapshotDocIds = @@ -1740,17 +1772,33 @@ public void snapshotListenerSortsUnicodeStringsInArrayAsServer() { public void snapshotListenerSortsUnicodeStringsInMapAsServer() { Map> testDocs = map( - "a", map("value", map("foo", "Łukasiewicz")), - "b", map("value", map("foo", "Sierpiński")), - "c", map("value", map("foo", "岩澤")), - "d", map("value", map("foo", "🄟")), - "e", map("value", map("foo", "P")), - "f", map("value", map("foo", "︒")), - "g", map("value", map("foo", "🐵"))); + "a", + map("value", map("foo", "Łukasiewicz")), + "b", + map("value", map("foo", "Sierpiński")), + "c", + map("value", map("foo", "岩澤")), + "d", + map("value", map("foo", "🄟")), + "e", + map("value", map("foo", "P")), + "f", + map("value", map("foo", "︒")), + "g", + map("value", map("foo", "🐵")), + "h", + map("value", map("foo", "你好")), + "i", + map("value", map("foo", "你顥")), + "j", + map("value", map("foo", "😁")), + "k", + map("value", map("foo", "😀"))); CollectionReference colRef = testCollectionWithDocs(testDocs); Query orderedQuery = colRef.orderBy("value"); - List expectedDocIds = Arrays.asList("b", "a", "c", "f", "e", "d", "g"); + List expectedDocIds = + Arrays.asList("b", "a", "h", "i", "c", "f", "e", "d", "g", "k", "j"); QuerySnapshot getSnapshot = waitFor(orderedQuery.get()); List getSnapshotDocIds = @@ -1781,17 +1829,33 @@ public void snapshotListenerSortsUnicodeStringsInMapAsServer() { public void snapshotListenerSortsUnicodeStringsInMapKeyAsServer() { Map> testDocs = map( - "a", map("value", map("Łukasiewicz", "foo")), - "b", map("value", map("Sierpiński", "foo")), - "c", map("value", map("岩澤", "foo")), - "d", map("value", map("🄟", "foo")), - "e", map("value", map("P", "foo")), - "f", map("value", map("︒", "foo")), - "g", map("value", map("🐵", "foo"))); + "a", + map("value", map("Łukasiewicz", "foo")), + "b", + map("value", map("Sierpiński", "foo")), + "c", + map("value", map("岩澤", "foo")), + "d", + map("value", map("🄟", "foo")), + "e", + map("value", map("P", "foo")), + "f", + map("value", map("︒", "foo")), + "g", + map("value", map("🐵", "foo")), + "h", + map("value", map("你好", "foo")), + "i", + map("value", map("你顥", "foo")), + "j", + map("value", map("😁", "foo")), + "k", + map("value", map("😀", "foo"))); CollectionReference colRef = testCollectionWithDocs(testDocs); Query orderedQuery = colRef.orderBy("value"); - List expectedDocIds = Arrays.asList("b", "a", "c", "f", "e", "d", "g"); + List expectedDocIds = + Arrays.asList("b", "a", "h", "i", "c", "f", "e", "d", "g", "k", "j"); QuerySnapshot getSnapshot = waitFor(orderedQuery.get()); List getSnapshotDocIds = @@ -1822,18 +1886,34 @@ public void snapshotListenerSortsUnicodeStringsInMapKeyAsServer() { public void snapshotListenerSortsUnicodeStringsInDocumentKeyAsServer() { Map> testDocs = map( - "Łukasiewicz", map("value", "foo"), - "Sierpiński", map("value", "foo"), - "岩澤", map("value", "foo"), - "🄟", map("value", "foo"), - "P", map("value", "foo"), - "︒", map("value", "foo"), - "🐵", map("value", "foo")); + "Łukasiewicz", + map("value", "foo"), + "Sierpiński", + map("value", "foo"), + "岩澤", + map("value", "foo"), + "🄟", + map("value", "foo"), + "P", + map("value", "foo"), + "︒", + map("value", "foo"), + "🐵", + map("value", "foo"), + "你好", + map("value", "foo"), + "你顥", + map("value", "foo"), + "😁", + map("value", "foo"), + "😀", + map("value", "foo")); CollectionReference colRef = testCollectionWithDocs(testDocs); Query orderedQuery = colRef.orderBy(FieldPath.documentId()); List expectedDocIds = - Arrays.asList("Sierpiński", "Łukasiewicz", "岩澤", "︒", "P", "🄟", "🐵"); + Arrays.asList( + "Sierpiński", "Łukasiewicz", "你好", "你顥", "岩澤", "︒", "P", "🄟", "🐵", "😀", "😁"); QuerySnapshot getSnapshot = waitFor(orderedQuery.get()); List getSnapshotDocIds = diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/Util.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/Util.java index 543da11e7d3..76fa5552eed 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/Util.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/Util.java @@ -87,9 +87,31 @@ public static int compareIntegers(int i1, int i2) { /** Compare strings in UTF-8 encoded byte order */ public static int compareUtf8Strings(String left, String right) { - ByteString leftBytes = ByteString.copyFromUtf8(left); - ByteString rightBytes = ByteString.copyFromUtf8(right); - return compareByteStrings(leftBytes, rightBytes); + int i = 0; + while (i < left.length() && i < right.length()) { + int leftCodePoint = left.codePointAt(i); + int rightCodePoint = right.codePointAt(i); + + if (leftCodePoint != rightCodePoint) { + if (leftCodePoint < 128 && rightCodePoint < 128) { + // ASCII comparison + return Integer.compare(leftCodePoint, rightCodePoint); + } else { + // UTF-8 encoded byte comparison, substring 2 indexes to cover surrogate pairs + ByteString leftBytes = + ByteString.copyFromUtf8(left.substring(i, Math.min(i + 2, left.length()))); + ByteString rightBytes = + ByteString.copyFromUtf8(right.substring(i, Math.min(i + 2, right.length()))); + return compareByteStrings(leftBytes, rightBytes); + } + } + + // Increment by 2 for surrogate pairs, 1 otherwise + i += Character.charCount(leftCodePoint); + } + + // Compare lengths if all characters are equal + return Integer.compare(left.length(), right.length()); } /** From 2ca341ca9cd096a4121b4124021250caf9b413e5 Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Wed, 19 Feb 2025 10:21:01 -0500 Subject: [PATCH 2/7] Update CHANGELOG.md --- firebase-firestore/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/firebase-firestore/CHANGELOG.md b/firebase-firestore/CHANGELOG.md index 66fce5b35ce..097951bdc8c 100644 --- a/firebase-firestore/CHANGELOG.md +++ b/firebase-firestore/CHANGELOG.md @@ -1,4 +1,5 @@ # Unreleased +* [fixed] Use lazy encoding in UTF-8 encoded byte comparison for strings to solve performance issues. [#6706](//github.com/firebase/firebase-android-sdk/pull/6706) # 25.1.2 From b740bccd36d55b7e8ed1a47650fcd25604955ef2 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Wed, 19 Feb 2025 19:35:38 +0000 Subject: [PATCH 3/7] UtilTest.java: add test for compareUtf8Strings(), which fails and needs to be fixed --- .../firebase/firestore/util/UtilTest.java | 179 ++++++++++++++++++ 1 file changed, 179 insertions(+) diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/util/UtilTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/util/UtilTest.java index 6ff424ef994..042fdbb6b0c 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/util/UtilTest.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/util/UtilTest.java @@ -17,6 +17,7 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.firebase.firestore.util.Util.firstNEntries; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; import com.google.firebase.firestore.testutil.TestUtil; import com.google.protobuf.ByteString; @@ -26,6 +27,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Random; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -87,4 +89,181 @@ private void validateDiffCollection(List before, List after) { Util.diffCollections(before, after, String::compareTo, result::add, result::remove); assertThat(result).containsExactlyElementsIn(after); } + + @Test + public void compareUtf8StringsShouldReturnCorrectValue() { + ArrayList errors = new ArrayList<>(); + int seed = new Random().nextInt(); + int passCount = 0; + StringGenerator stringGenerator = new StringGenerator(seed); + for (int i = 0; i < 1_000_000 && errors.size() < 10; i++) { + String s1 = stringGenerator.next(); + String s2 = stringGenerator.next(); + + int actual = Util.compareUtf8Strings(s1, s2); + + ByteString b1 = ByteString.copyFromUtf8(s1); + ByteString b2 = ByteString.copyFromUtf8(s2); + int expected = Util.compareByteStrings(b1, b2); + + if (actual == expected) { + passCount++; + } else { + errors.add( + "compareUtf8Strings(s1=\"" + + s1 + + "\", s2=\"" + + s2 + + "\") returned " + + actual + + ", but expected " + + expected + + " (i=" + + i + + ", s1.length=" + + s1.length() + + ", s2.length=" + + s2.length() + + ")"); + } + } + + if (!errors.isEmpty()) { + StringBuilder sb = new StringBuilder(); + sb.append(errors.size()).append(" test cases failed, "); + sb.append(passCount).append(" test cases passed, "); + sb.append("seed=").append(seed).append(";"); + for (int i = 0; i < errors.size(); i++) { + sb.append("\nerrors[").append(i).append("]: ").append(errors.get(i)); + } + fail(sb.toString()); + } + } + + private static class StringGenerator { + + private static final float DEFAULT_SURROGATE_PAIR_PROBABILITY = 0.33f; + private static final int DEFAULT_MAX_LENGTH = 20; + + // The first Unicode code point that is in the basic multilingual plane ("BMP") and, + // therefore requires 1 UTF-16 code unit to be represented in UTF-16. + private static final int MIN_BMP_CODE_POINT = 0x00000000; + + // The last Unicode code point that is in the basic multilingual plane ("BMP") and, + // therefore requires 1 UTF-16 code unit to be represented in UTF-16. + private static final int MAX_BMP_CODE_POINT = 0x0000FFFF; + + // The first Unicode code point that is outside of the basic multilingual plane ("BMP") and, + // therefore requires 2 UTF-16 code units, a surrogate pair, to be represented in UTF-16. + private static final int MIN_SUPPLEMENTARY_CODE_POINT = 0x00010000; + + // The last Unicode code point that is outside of the basic multilingual plane ("BMP") and, + // therefore requires 2 UTF-16 code units, a surrogate pair, to be represented in UTF-16. + private static final int MAX_SUPPLEMENTARY_CODE_POINT = 0x0010FFFF; + + private final Random rnd; + private final float surrogatePairProbability; + private final int maxLength; + + public StringGenerator(int seed) { + this(new Random(seed), DEFAULT_SURROGATE_PAIR_PROBABILITY, DEFAULT_MAX_LENGTH); + } + + public StringGenerator(Random rnd, float surrogatePairProbability, int maxLength) { + this.rnd = rnd; + this.surrogatePairProbability = + validateProbability("surrogate pair", surrogatePairProbability); + this.maxLength = validateLength("maximum string", maxLength); + } + + private static float validateProbability(String name, float probability) { + if (!Float.isFinite(probability)) { + throw new IllegalArgumentException( + "invalid " + + name + + " probability: " + + probability + + " (must be between 0.0 and 1.0, inclusive)"); + } else if (probability < 0.0f) { + throw new IllegalArgumentException( + "invalid " + + name + + " probability: " + + probability + + " (must be greater than or equal to zero)"); + } else if (probability > 1.0f) { + throw new IllegalArgumentException( + "invalid " + + name + + " probability: " + + probability + + " (must be less than or equal to 1)"); + } + return probability; + } + + private static int validateLength(String name, int length) { + if (length < 0) { + throw new IllegalArgumentException( + "invalid " + name + " length: " + length + " (must be greater than or equal to zero)"); + } + return length; + } + + public String next() { + final int length = rnd.nextInt(maxLength + 1); + final StringBuilder sb = new StringBuilder(); + while (sb.length() < length) { + sb.appendCodePoint(nextCodePoint()); + } + return sb.toString(); + } + + private boolean isNextSurrogatePair() { + return nextBoolean(rnd, surrogatePairProbability); + } + + private static boolean nextBoolean(Random rnd, float probability) { + if (probability == 0.0f) { + return false; + } else if (probability == 1.0f) { + return true; + } else { + return rnd.nextFloat() < probability; + } + } + + private int nextCodePoint() { + if (isNextSurrogatePair()) { + return nextSurrogateCodePoint(); + } else { + return nextNonSurrogateCodePoint(); + } + } + + private int nextSurrogateCodePoint() { + return nextCodePoint(rnd, MIN_SUPPLEMENTARY_CODE_POINT, MAX_SUPPLEMENTARY_CODE_POINT, 2); + } + + private int nextNonSurrogateCodePoint() { + return nextCodePoint(rnd, MIN_BMP_CODE_POINT, MAX_BMP_CODE_POINT, 1); + } + + private int nextCodePoint(Random rnd, int min, int max, int expectedCharCount) { + int rangeSize = max - min; + int offset = rnd.nextInt(rangeSize); + int codePoint = min + offset; + if (Character.charCount(codePoint) != expectedCharCount) { + throw new RuntimeException( + "internal error vqgqnxcy97: " + + "Character.charCount(" + + codePoint + + ") returned " + + Character.charCount(codePoint) + + ", but expected " + + expectedCharCount); + } + return codePoint; + } + } } From 4c4d1a74a071924135d63f8ffd09c4cedf734efe Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Wed, 19 Feb 2025 19:58:46 +0000 Subject: [PATCH 4/7] UtilTest.java: make sure that most generated strings have a common prefix --- .../firebase/firestore/util/UtilTest.java | 41 ++++++++++++++++--- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/util/UtilTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/util/UtilTest.java index 042fdbb6b0c..6009f076caf 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/util/UtilTest.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/util/UtilTest.java @@ -93,12 +93,17 @@ private void validateDiffCollection(List before, List after) { @Test public void compareUtf8StringsShouldReturnCorrectValue() { ArrayList errors = new ArrayList<>(); - int seed = new Random().nextInt(); + int seed = new Random().nextInt(Integer.MAX_VALUE); int passCount = 0; - StringGenerator stringGenerator = new StringGenerator(seed); + StringGenerator stringGenerator = new StringGenerator(29750468); + StringPairGenerator stringPairGenerator = new StringPairGenerator(stringGenerator); for (int i = 0; i < 1_000_000 && errors.size() < 10; i++) { - String s1 = stringGenerator.next(); - String s2 = stringGenerator.next(); + final String s1, s2; + { + StringPairGenerator.StringPair stringPair = stringPairGenerator.next(); + s1 = stringPair.s1; + s2 = stringPair.s2; + } int actual = Util.compareUtf8Strings(s1, s2); @@ -140,6 +145,31 @@ public void compareUtf8StringsShouldReturnCorrectValue() { } } + private static class StringPairGenerator { + + private final StringGenerator stringGenerator; + + public StringPairGenerator(StringGenerator stringGenerator) { + this.stringGenerator = stringGenerator; + } + + public StringPair next() { + String prefix = stringGenerator.next(); + String s1 = prefix + stringGenerator.next(); + String s2 = prefix + stringGenerator.next(); + return new StringPair(s1, s2); + } + + public static class StringPair { + public final String s1, s2; + + public StringPair(String s1, String s2) { + this.s1 = s1; + this.s2 = s2; + } + } + } + private static class StringGenerator { private static final float DEFAULT_SURROGATE_PAIR_PROBABILITY = 0.33f; @@ -214,7 +244,8 @@ public String next() { final int length = rnd.nextInt(maxLength + 1); final StringBuilder sb = new StringBuilder(); while (sb.length() < length) { - sb.appendCodePoint(nextCodePoint()); + int codePoint = nextCodePoint(); + sb.appendCodePoint(codePoint); } return sb.toString(); } From ec37ebdc7bac12a8c6605e49ac94f579e3f8caad Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Wed, 19 Feb 2025 16:10:29 -0500 Subject: [PATCH 5/7] fix the failing test --- .../google/firebase/firestore/util/Util.java | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/Util.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/Util.java index 76fa5552eed..f612c6fe307 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/Util.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/Util.java @@ -27,7 +27,10 @@ import io.grpc.Status; import io.grpc.StatusException; import io.grpc.StatusRuntimeException; + +import java.nio.charset.StandardCharsets; import java.security.SecureRandom; +import java.sql.SQLOutput; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -97,12 +100,13 @@ public static int compareUtf8Strings(String left, String right) { // ASCII comparison return Integer.compare(leftCodePoint, rightCodePoint); } else { - // UTF-8 encoded byte comparison, substring 2 indexes to cover surrogate pairs - ByteString leftBytes = - ByteString.copyFromUtf8(left.substring(i, Math.min(i + 2, left.length()))); - ByteString rightBytes = - ByteString.copyFromUtf8(right.substring(i, Math.min(i + 2, right.length()))); - return compareByteStrings(leftBytes, rightBytes); + // substring and do UTF-8 encoded byte comparison + byte[] leftBytes = getUtf8SafeBytes(left, i); + byte[] rightBytes = getUtf8SafeBytes(right, i); + int comp = compareByteArrays(leftBytes,rightBytes); + if(comp !=0) { + return comp; + } } } @@ -114,6 +118,19 @@ public static int compareUtf8Strings(String left, String right) { return Integer.compare(left.length(), right.length()); } + private static byte[] getUtf8SafeBytes(String str, int index) { + int firstCodePoint = str.codePointAt(index); + String sub; + if (firstCodePoint > 0xffff) { + // It's a surrogate pair, return the whole pair + sub = str.substring(index, index + 2); + } else { + // It's a single code point, return it + sub = str.substring(index, index + 1); + } + return sub.getBytes(StandardCharsets.UTF_8); + } + /** * Utility function to compare longs. Note that we can't use Long.compare because it's only * available after Android 19. From 391867a5f8d79a840e63d0162f3ac1be85e2e26e Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Wed, 19 Feb 2025 16:37:37 -0500 Subject: [PATCH 6/7] simplify the code --- .../com/google/firebase/firestore/util/Util.java | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/Util.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/Util.java index f612c6fe307..8376efb1b7a 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/Util.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/Util.java @@ -27,10 +27,8 @@ import io.grpc.Status; import io.grpc.StatusException; import io.grpc.StatusRuntimeException; - import java.nio.charset.StandardCharsets; import java.security.SecureRandom; -import java.sql.SQLOutput; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -90,8 +88,7 @@ public static int compareIntegers(int i1, int i2) { /** Compare strings in UTF-8 encoded byte order */ public static int compareUtf8Strings(String left, String right) { - int i = 0; - while (i < left.length() && i < right.length()) { + for (int i = 0; i < left.length() && i < right.length(); i++) { int leftCodePoint = left.codePointAt(i); int rightCodePoint = right.codePointAt(i); @@ -103,15 +100,12 @@ public static int compareUtf8Strings(String left, String right) { // substring and do UTF-8 encoded byte comparison byte[] leftBytes = getUtf8SafeBytes(left, i); byte[] rightBytes = getUtf8SafeBytes(right, i); - int comp = compareByteArrays(leftBytes,rightBytes); - if(comp !=0) { + int comp = compareByteArrays(leftBytes, rightBytes); + if (comp != 0) { return comp; } } } - - // Increment by 2 for surrogate pairs, 1 otherwise - i += Character.charCount(leftCodePoint); } // Compare lengths if all characters are equal From 8159f25812411e3d534c24f8feebd819d07ed782 Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Thu, 20 Feb 2025 11:32:17 -0500 Subject: [PATCH 7/7] format --- .../com/google/firebase/firestore/util/Util.java | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/Util.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/Util.java index 8376efb1b7a..c671411262b 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/Util.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/Util.java @@ -27,7 +27,6 @@ import io.grpc.Status; import io.grpc.StatusException; import io.grpc.StatusRuntimeException; -import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import java.util.ArrayList; import java.util.Collection; @@ -98,9 +97,9 @@ public static int compareUtf8Strings(String left, String right) { return Integer.compare(leftCodePoint, rightCodePoint); } else { // substring and do UTF-8 encoded byte comparison - byte[] leftBytes = getUtf8SafeBytes(left, i); - byte[] rightBytes = getUtf8SafeBytes(right, i); - int comp = compareByteArrays(leftBytes, rightBytes); + ByteString leftBytes = ByteString.copyFromUtf8(getUtf8SafeBytes(left, i)); + ByteString rightBytes = ByteString.copyFromUtf8(getUtf8SafeBytes(right, i)); + int comp = compareByteStrings(leftBytes, rightBytes); if (comp != 0) { return comp; } @@ -112,17 +111,15 @@ public static int compareUtf8Strings(String left, String right) { return Integer.compare(left.length(), right.length()); } - private static byte[] getUtf8SafeBytes(String str, int index) { + private static String getUtf8SafeBytes(String str, int index) { int firstCodePoint = str.codePointAt(index); - String sub; if (firstCodePoint > 0xffff) { // It's a surrogate pair, return the whole pair - sub = str.substring(index, index + 2); + return str.substring(index, index + 2); } else { // It's a single code point, return it - sub = str.substring(index, index + 1); + return str.substring(index, index + 1); } - return sub.getBytes(StandardCharsets.UTF_8); } /**