Skip to content

Commit 76b029c

Browse files
authored
Fix an issue with loading bundles with multi-byte character (#2820)
1 parent 4f1cdc8 commit 76b029c

File tree

5 files changed

+91
-61
lines changed

5 files changed

+91
-61
lines changed

firebase-firestore/CHANGELOG.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
1-
# Unreleased
2-
- [changed] Increases the aggressiveness of network retires when an app's
1+
# 23.0.3
2+
- [fixed] Fixed an issue when loading a data bundle with
3+
multi-byte Unicode characters leads to failures.
4+
5+
# 23.0.2
6+
- [changed] Increases the aggressiveness of network retries when an app's
37
foreground status changes.
48

59
# 23.0.1
610
- [changed] The SDK now tries to immediately establish a connection to the
711
backend when the app enters the foreground.
12+
13+
# 23.0.0
14+
- [changed] Internal infrastructure improvements.
15+
- [changed] Internal changes to support dynamic feature modules.
816

917
# 22.1.1
1018
- [fixed] Fixed an issue that dropped the limit for queries loaded from

firebase-firestore/src/androidTest/java/com/google/firebase/firestore/BundleTest.java

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import java.io.InputStream;
3333
import java.io.UnsupportedEncodingException;
3434
import java.nio.ByteBuffer;
35+
import java.nio.charset.Charset;
3536
import java.util.ArrayList;
3637
import java.util.List;
3738
import java.util.concurrent.ExecutionException;
@@ -43,6 +44,7 @@
4344

4445
@RunWith(AndroidJUnit4.class)
4546
public class BundleTest {
47+
private static Charset UTF8_CHARSET = Charset.forName("UTF-8");
4648
private static String[] BUNDLE_TEMPLATES =
4749
new String[] {
4850
"{\"metadata\":{\"id\":\"test-bundle\",\"createTime\":{\"seconds\":1001,\"nanos\":9999},"
@@ -71,7 +73,7 @@ public class BundleTest {
7173
+ "{\"seconds\":1000,\"nanos\":9999},\"exists\":true}}",
7274
"{\"document\":{\"name\":\"projects/{projectId}/databases/(default)/documents/coll-1/b\","
7375
+ "\"createTime\":{\"seconds\":1,\"nanos\":9},\"updateTime\":{\"seconds\":1,"
74-
+ "\"nanos\":9},\"fields\":{\"k\":{\"stringValue\":\"b\"},\"bar\":"
76+
+ "\"nanos\":9},\"fields\":{\"k\":{\"stringValue\":\"\uD83D\uDE0A\"},\"bar\":"
7577
+ "{\"integerValue\":2}}}}"
7678
};
7779

@@ -238,18 +240,22 @@ private void verifyQueryResults() throws ExecutionException, InterruptedExceptio
238240
CollectionReference collectionQuery = db.collection("coll-1");
239241
QuerySnapshot collectionSnapshot = Tasks.await(collectionQuery.get(Source.CACHE));
240242
assertEquals(
241-
asList(map("bar", 1L, "k", "a"), map("bar", 2L, "k", "b")),
243+
asList(map("bar", 1L, "k", "a"), map("bar", 2L, "k", "\uD83D\uDE0A")),
242244
querySnapshotToValues(collectionSnapshot));
243245

244246
Query limitQuery = Tasks.await(db.getNamedQuery("limit"));
245247
QuerySnapshot limitSnapshot = Tasks.await(limitQuery.get(Source.CACHE));
246-
assertEquals(asList(map("bar", 2L, "k", "b")), querySnapshotToValues(limitSnapshot));
248+
assertEquals(asList(map("bar", 2L, "k", "\uD83D\uDE0A")), querySnapshotToValues(limitSnapshot));
247249

248250
Query limitToLastQuery = Tasks.await(db.getNamedQuery("limit-to-last"));
249251
QuerySnapshot limitToLastSnapshot = Tasks.await(limitToLastQuery.get(Source.CACHE));
250252
assertEquals(asList(map("bar", 1L, "k", "a")), querySnapshotToValues(limitToLastSnapshot));
251253
}
252254

255+
private int getUTF8ByteCount(String s) {
256+
return s.getBytes(UTF8_CHARSET).length;
257+
}
258+
253259
/**
254260
* Returns a valid bundle by replacing project id in `BUNDLE_TEMPLATES` with the given db project
255261
* id (also recalculates length prefixes).
@@ -262,7 +268,7 @@ private byte[] createBundle(String projectId) throws UnsupportedEncodingExceptio
262268
for (int i = 1; i < BUNDLE_TEMPLATES.length; ++i) {
263269
// Extract elements from BUNDLE_TEMPLATE and replace the project ID.
264270
String element = BUNDLE_TEMPLATES[i].replaceAll("\\{projectId\\}", projectId);
265-
bundle.append(element.length());
271+
bundle.append(getUTF8ByteCount(element));
266272
bundle.append(element);
267273
}
268274

@@ -273,7 +279,8 @@ private byte[] createBundle(String projectId) throws UnsupportedEncodingExceptio
273279
BUNDLE_TEMPLATES[0].replace(
274280
"{totalBytes}", Integer.toString(bundleString.getBytes("UTF-8").length));
275281

276-
return (metadata.length() + metadata + bundleString).getBytes("UTF-8");
282+
String fullBundle = getUTF8ByteCount(metadata) + metadata + bundleString;
283+
return fullBundle.getBytes("UTF-8");
277284
}
278285

279286
/**

firebase-firestore/src/main/java/com/google/firebase/firestore/bundle/BundleReader.java

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@
1616

1717
import androidx.annotation.Nullable;
1818
import com.google.firebase.firestore.util.Logger;
19+
import java.io.ByteArrayOutputStream;
1920
import java.io.IOException;
2021
import java.io.InputStream;
2122
import java.io.InputStreamReader;
22-
import java.nio.CharBuffer;
23+
import java.nio.ByteBuffer;
2324
import java.nio.charset.Charset;
2425
import org.json.JSONException;
2526
import org.json.JSONObject;
@@ -34,18 +35,21 @@ public class BundleReader {
3435
/** The capacity for the internal char buffer. */
3536
protected static final int BUFFER_CAPACITY = 1024;
3637

38+
private static final Charset UTF8_CHARSET = Charset.forName("UTF-8");
39+
3740
private final BundleSerializer serializer;
41+
private final InputStream bundleInputStream;
3842
private final InputStreamReader dataReader;
39-
private final Charset charset = Charset.forName("UTF-8");
4043

4144
@Nullable BundleMetadata metadata;
42-
private CharBuffer buffer;
45+
private ByteBuffer buffer;
4346
long bytesRead;
4447

45-
public BundleReader(BundleSerializer serializer, InputStream data) {
48+
public BundleReader(BundleSerializer serializer, InputStream bundleInputStream) {
4649
this.serializer = serializer;
47-
dataReader = new InputStreamReader(data, charset);
48-
buffer = CharBuffer.allocate(BUFFER_CAPACITY);
50+
this.bundleInputStream = bundleInputStream;
51+
this.dataReader = new InputStreamReader(this.bundleInputStream);
52+
buffer = ByteBuffer.allocate(BUFFER_CAPACITY);
4953

5054
buffer.flip(); // Start the buffer in "reading mode"
5155
}
@@ -83,7 +87,7 @@ public long getBytesRead() {
8387
}
8488

8589
public void close() throws IOException {
86-
dataReader.close();
90+
bundleInputStream.close();
8791
}
8892

8993
/**
@@ -101,8 +105,9 @@ private BundleElement readNextElement() throws IOException, JSONException {
101105
return null;
102106
}
103107

104-
String json = readJsonString(Integer.parseInt(lengthPrefix));
105-
bytesRead += lengthPrefix.length() + json.getBytes(charset).length;
108+
int jsonStringByteCount = Integer.parseInt(lengthPrefix);
109+
String json = readJsonString(jsonStringByteCount);
110+
bytesRead += lengthPrefix.getBytes(UTF8_CHARSET).length + jsonStringByteCount;
106111
return decodeBundleElement(json);
107112
}
108113

@@ -133,9 +138,9 @@ private BundleElement readNextElement() throws IOException, JSONException {
133138
throw abort("Reached the end of bundle when a length string is expected.");
134139
}
135140

136-
char[] c = new char[nextOpenBracket];
137-
buffer.get(c);
138-
return new String(c);
141+
byte[] b = new byte[nextOpenBracket];
142+
buffer.get(b);
143+
return UTF8_CHARSET.decode(ByteBuffer.wrap(b)).toString();
139144
}
140145

141146
/** Returns the index of the first open bracket, or -1 if none is found. */
@@ -157,25 +162,30 @@ private int indexOfOpenBracket() {
157162
* Reads from a specified position from the internal buffer, for a specified number of bytes,
158163
* pulling more data from the underlying stream if needed.
159164
*
160-
* <p>Returns a string decoded from the read bytes.
165+
* <p>Returns an object containing the Json string and its UTF8 byte count.
161166
*/
162-
private String readJsonString(int length) throws IOException {
163-
StringBuilder json = new StringBuilder(length);
167+
private String readJsonString(int bytesToRead) throws IOException {
168+
ByteArrayOutputStream jsonBytes = new ByteArrayOutputStream();
164169

165-
int remaining = length;
170+
// Read at least `bytesToRead` number of bytes from the bundle into `this.buffer`, pulling more
171+
// data if necessary.
172+
// Exactly `bytesToRead` number of bytes will be put in `jsonBytes` after the loop completes.
173+
int remaining = bytesToRead;
166174
while (remaining > 0) {
167175
if (buffer.remaining() == 0 && !pullMoreData()) {
168176
throw abort("Reached the end of bundle when more data was expected.");
169177
}
170178

179+
// `read` is the number of bytes guaranteed to exist in `this.buffer` after the above
180+
// call to `pullMoreData`. Copy them to `jsonBytes` and advance `this.buffer`'s position.
171181
int read = Math.min(remaining, buffer.remaining());
172-
json.append(buffer, 0, read);
182+
jsonBytes.write(buffer.array(), buffer.arrayOffset() + buffer.position(), read);
173183
buffer.position(buffer.position() + read);
174184

175185
remaining -= read;
176186
}
177187

178-
return json.toString();
188+
return jsonBytes.toString(UTF8_CHARSET.name());
179189
}
180190

181191
/**
@@ -185,9 +195,17 @@ private String readJsonString(int length) throws IOException {
185195
*/
186196
private boolean pullMoreData() throws IOException {
187197
buffer.compact();
188-
int read = dataReader.read(buffer);
198+
199+
int bytesRead =
200+
bundleInputStream.read(
201+
buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining());
202+
boolean readSuccess = bytesRead > 0;
203+
if (readSuccess) {
204+
buffer.position(buffer.position() + bytesRead);
205+
}
206+
189207
buffer.flip();
190-
return read > 0;
208+
return readSuccess;
191209
}
192210

193211
/** Converts a JSON-encoded bundle element into its model class. */

firebase-firestore/src/main/java/com/google/firebase/firestore/util/ByteBufferInputStream.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ public ByteBufferInputStream(ByteBuffer buf) {
2727
this.buffer = buf;
2828
}
2929

30+
@Override
31+
public int available() throws IOException {
32+
return this.buffer.remaining();
33+
}
34+
3035
@Override
3136
public int read() {
3237
if (!buffer.hasRemaining()) {

0 commit comments

Comments
 (0)