Skip to content

Commit 89975d7

Browse files
committed
JavaScriptCore is very strict about invalid UTF symbols.
So if you pass an invalid UTF-8 string to it the string will be decoded as an empty string. The current implementation of progressive downloading for Android blindly cuts the response in 8KB chunks. That could cause a problem in case the last symbol in the chunk is multi-byte. To prevent it I added a class which determines if this is the case and cut the string in the appropriate place. A remainder is prepended to the next chunk of data.
1 parent 1954438 commit 89975d7

File tree

4 files changed

+274
-13
lines changed

4 files changed

+274
-13
lines changed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* Copyright (c) 2017-present, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*/
9+
package com.facebook.react.common;
10+
11+
import java.nio.charset.Charset;
12+
13+
/**
14+
* Not all versions of Android SDK have this class in nio package.
15+
* This is the reason to have it around.
16+
*/
17+
public class StandardCharsets {
18+
19+
/**
20+
* Eight-bit UCS Transformation Format
21+
*/
22+
public static final Charset UTF_8 = Charset.forName("UTF-8");
23+
24+
/**
25+
* Sixteen-bit UCS Transformation Format, byte order identified by an
26+
* optional byte-order mark
27+
*/
28+
public static final Charset UTF_16 = Charset.forName("UTF-16");
29+
30+
/**
31+
* Sixteen-bit UCS Transformation Format, big-endian byte order
32+
*/
33+
public static final Charset UTF_16BE = Charset.forName("UTF-16BE");
34+
/**
35+
* Sixteen-bit UCS Transformation Format, little-endian byte order
36+
*/
37+
public static final Charset UTF_16LE = Charset.forName("UTF-16LE");
38+
}

ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.java

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import java.io.IOException;
1515
import java.io.InputStream;
1616
import java.io.Reader;
17+
import java.nio.charset.Charset;
1718
import java.util.HashSet;
1819
import java.util.List;
1920
import java.util.Set;
@@ -29,6 +30,7 @@
2930
import com.facebook.react.bridge.ReadableArray;
3031
import com.facebook.react.bridge.ReadableMap;
3132
import com.facebook.react.bridge.WritableMap;
33+
import com.facebook.react.common.StandardCharsets;
3234
import com.facebook.react.common.network.OkHttpCallUtil;
3335
import com.facebook.react.module.annotations.ReactModule;
3436
import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter;
@@ -408,20 +410,45 @@ private void readWithProgress(
408410
// Ignore
409411
}
410412

411-
Reader reader = responseBody.charStream();
412-
try {
413-
char[] buffer = new char[MAX_CHUNK_SIZE_BETWEEN_FLUSHES];
414-
int read;
415-
while ((read = reader.read(buffer)) != -1) {
416-
ResponseUtil.onIncrementalDataReceived(
417-
eventEmitter,
418-
requestId,
419-
new String(buffer, 0, read),
420-
totalBytesRead,
421-
contentLength);
413+
Charset charset = responseBody.contentType() == null ? StandardCharsets.UTF_8 :
414+
responseBody.contentType().charset(StandardCharsets.UTF_8);
415+
416+
if (StandardCharsets.UTF_8.equals(charset)) {
417+
ProgressiveUTF8StreamDecoder streamDecoder = new ProgressiveUTF8StreamDecoder();
418+
InputStream inputStream = responseBody.byteStream();
419+
try {
420+
byte[] buffer = new byte[MAX_CHUNK_SIZE_BETWEEN_FLUSHES];
421+
int read;
422+
while ((read = inputStream.read(buffer)) != -1) {
423+
ResponseUtil.onIncrementalDataReceived(
424+
eventEmitter,
425+
requestId,
426+
streamDecoder.decodeNext(buffer, read),
427+
totalBytesRead,
428+
contentLength);
429+
}
430+
} finally {
431+
inputStream.close();
432+
}
433+
} else {
434+
// TODO: in UTF-16 some symbols took 4 bytes or 2 chars (HIGH and LOW surrogates)
435+
// Ideally we need to take care of this but it's way more complex task as it involves handling
436+
// of Byte Order Mark and little/big endian of UTF-16. Let's keep it in sync with iOS for now.
437+
Reader reader = responseBody.charStream();
438+
try {
439+
char[] buffer = new char[MAX_CHUNK_SIZE_BETWEEN_FLUSHES];
440+
int read;
441+
while ((read = reader.read(buffer)) != -1) {
442+
ResponseUtil.onIncrementalDataReceived(
443+
eventEmitter,
444+
requestId,
445+
new String(buffer, 0, read),
446+
totalBytesRead,
447+
contentLength);
448+
}
449+
} finally {
450+
reader.close();
422451
}
423-
} finally {
424-
reader.close();
425452
}
426453
}
427454

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/**
2+
* Copyright (c) 2017-present, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*/
9+
package com.facebook.react.modules.network;
10+
11+
import com.facebook.react.common.StandardCharsets;
12+
13+
/**
14+
* Class to decode UTF-8 strings from byte array chunks.
15+
* UTF-8 could have symbol size from 1 to 4 bytes.
16+
* In case of progressive decoding we could accidentally break the original string.
17+
*
18+
* Use this class to make sure that we extract Strings from byte stream correctly.
19+
*/
20+
public class ProgressiveUTF8StreamDecoder {
21+
22+
private byte[] mRemainder = null;
23+
24+
/**
25+
* Bit mask implementation performed 1.5x worse than this one
26+
*
27+
* @param firstByte - first byte of the symbol
28+
* @return count of bytes in the symbol
29+
*/
30+
private int symbolSize(byte firstByte) {
31+
int code = firstByte & 0XFF;
32+
if (code >= 240) {
33+
return 4;
34+
} else if (code >= 224 ) {
35+
return 3;
36+
} else if (code >= 192 ) {
37+
return 2;
38+
}
39+
40+
return 1;
41+
}
42+
43+
/**
44+
* Parses data to UTF-8 String
45+
* If last symbol is partial we save it to mRemainder and concatenate it to the next chunk
46+
* @param data
47+
* @param length length of data to decode
48+
* @return
49+
*/
50+
public String decodeNext(byte[] data, int length) {
51+
int i = 0;
52+
int lastSymbolSize = 0;
53+
if (mRemainder != null) {
54+
i = symbolSize(mRemainder[0]) - mRemainder.length;
55+
}
56+
while (i < length) {
57+
lastSymbolSize = symbolSize(data[i]);
58+
i += lastSymbolSize;
59+
60+
}
61+
62+
byte[] result;
63+
int symbolsToCopy = length;
64+
boolean hasNewReminder = false;
65+
if (i > length) {
66+
hasNewReminder = true;
67+
symbolsToCopy = i - lastSymbolSize;
68+
}
69+
70+
if (mRemainder == null) {
71+
result = data;
72+
} else {
73+
result = new byte[symbolsToCopy + mRemainder.length];
74+
System.arraycopy(mRemainder, 0, result, 0, mRemainder.length);
75+
System.arraycopy(data, 0, result, mRemainder.length, symbolsToCopy);
76+
mRemainder = null;
77+
symbolsToCopy = result.length;
78+
}
79+
80+
if (hasNewReminder) {
81+
int reminderSize = lastSymbolSize - i + length;
82+
mRemainder = new byte[reminderSize];
83+
System.arraycopy(data, length - reminderSize, mRemainder, 0, reminderSize );
84+
}
85+
86+
return new String(result, 0, symbolsToCopy, StandardCharsets.UTF_8);
87+
}
88+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/**
2+
* Copyright (c) 2017-present, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*/
9+
package com.facebook.react.modules.network;
10+
11+
import org.junit.Assert;
12+
import org.junit.Test;
13+
import org.junit.runner.RunWith;
14+
import org.robolectric.RobolectricTestRunner;
15+
16+
import java.nio.charset.Charset;
17+
18+
19+
@RunWith(RobolectricTestRunner.class)
20+
public class ProgressiveUTF8StreamDecoderTest {
21+
22+
private static String TEST_DATA_1_BYTE = "Lorem ipsum dolor sit amet, ea ius viris laoreet gloriatur, ea enim illud mel. Ea eligendi erroribus inciderint sea, id nemore sensibus contentiones qui. Eos et nulla abhorreant, noluisse adipiscing reprehendunt an sit. Harum iriure meliore ne nec, clita semper voluptaria at sea. Ius civibus vituperata reprehendunt ut.\n" +
23+
"\n" +
24+
"Sed nisl postea maiorum ex, mea eros verterem ea. Ne usu brute debitis appareat. Ad quem reprimique dissentias duo. Sit an labitur eleifend, illud zril audiam nam ex, epicuri luptatum ne usu. Lorem mundi utinam vix ea.\n" +
25+
"\n" +
26+
"Te eam nominati qualisque. Ut praesent consetetur pro. Soleat vivendum vim ea. Altera dolores eam in. Eum at praesent complectitur. Nec ea inani definitiones, tantas vivendum mei an, mea an ubique omnium latine. Has mundi ocurreret ei, nam ea iuvaret gloriatur.\n" +
27+
"\n" +
28+
"Ad omnes malorum vim, no latine facilisi mel, dicant salutandi conclusionemque ei est. Nam cu partem alterum minimum. Et quo iriure deleniti accommodare, ad impetus perfecto liberavisse pri. Instructior necessitatibus ut mel, ex cum sumo atqui comprehensam, ei nullam oporteat sed. Ius meliore placerat cu.\n" +
29+
"\n" +
30+
"Eum in ferri nobis, eam eu verear facilisis referrentur. Veniam epicuri referrentur at nam. Vel congue diceret fabulas te, ei fabellas temporibus mei. Nemore corrumpit quo ex, et vis soluta reprehendunt. Et eos eripuit atomorum.\n" +
31+
"\n" +
32+
"Eum no novum tantas decore. Indoctum definiebas intellegam ut vel. Cu per ipsum graeco, in nam dico dolore, usu id ludus consulatu. Vis an clita commune, cu quot quaeque cum. In eos semper aperiri. Ne mea probo inermis, no vis audiam volutpat.\n" +
33+
"\n" +
34+
"Cu quaeque scaevola vis. Civibus commune scriptorem vim an, vim ea vocent petentium consequuntur, meis propriae invidunt eam ex. Pro et ponderum recusabo sapientem. Vel legere possim ornatus ne, saepe commodo scaevola an quo. An scaevola repudiandae sed. Eam ei veri nemore.\n" +
35+
"\n" +
36+
"Ullum deleniti cum at. An has soleat docendi, epicuri erroribus inciderint pro ea. Noluisse invidunt splendide quo in, eam odio invenire ea. Eu hinc definiebas scripserit duo, has cu equidem ponderum expetenda, eum vulputate intellegat id. Pri eu natum semper pertinax, ei vel inani aliquip habemus, sit an facer dicam. Et graeci abhorreant contentiones duo, et summo partiendo conclusionemque per.\n" +
37+
"\n" +
38+
"Sed ei etiam iudico abhorreant. Pri an regione fastidii, clita discere eu nec. Torquatos percipitur inciderint eos in, id per prompta blandit. Sit et epicuri deleniti. Per labores corpora no.\n" +
39+
"\n" +
40+
"Quodsi melius facilis pri ei, has adhuc recusabo reprimique ut. Laoreet definitionem cum cu, amet nonumes ut vis, qui ut sonet ancillae. Vim no doctus efficiantur, ancillae indoctum ex sea, vel eu fabulas volumus argumentum. Ex eum aeque commune placerat, nam choro tamquam luptatum et. Ne sea vero idque liberavisse";
41+
42+
private static String TEST_DATA_2_BYTES = "Лорем ипсум долор сит амет, доминг дисцере ад вих, велит игнота ратионибус мел цу. Не вирис малорум яуаеяуе хас, еу либрис доцтус хис. Моллис садипсцинг ан цум, семпер молестие репрехендунт усу те. Цасе аетерно оффендит ан еос. При ан толлит опортере оцурререт, ан яуот мутат трацтатос вих.\n" +
43+
"\n" +
44+
"Нец фалли харум ратионибус еа. Магна адмодум ат нам, яуи еа рецусабо мандамус, аццусам цонсеяуунтур цу хис. Импедит цотидиеяуе улламцорпер еа мел, усу ет долорес аргументум. Веро торяуатос ех нам, цибо либерависсе ест еи. Вис долор омниум сплендиде ад, велит рецусабо цонсететур иус цу.\n" +
45+
"\n" +
46+
"Еи дуо меис атоморум сигниферумяуе, аугуе аццусам мел ет. Ут ностро легендос хонестатис пер, ут яуас мовет сеа. Меа цу продессет аппеллантур. Вис еа яуод оффендит, дебет видерер ет нам.\n" +
47+
"\n" +
48+
"Еам еа дебитис иудицабит, не хас иллуд цивибус. Усу ет алии уллум утамур. Поссит цонституто те яуи, хас ет лаудем аудире, нам еи епицури салутанди. Лудус делицатиссими цум еу, либер адиписцинг еи нец. Ид ерипуит лобортис антиопам хис, санцтус елигенди неглегентур сед ут, вел сентентиае инструцтиор еи. Ан про унум яуалисяуе.\n" +
49+
"\n" +
50+
"Ат еррор алтера сит, пер еу яуот номинави. Пертинах репудиаре цум еу. Еа фуиссет антиопам вим, пробатус реферрентур ут иус. Еум ад модус утрояуе диспутандо.\n" +
51+
"\n" +
52+
"Ехерци бландит ут меа. Солет импедит сед ад. Дуо порро тимеам аудире не, алии ерант номинави цу нец, сит ферри веритус адиписци те. Те меи синт адверсариум, ад феугаит инвидунт луцилиус сед, дицунт нумяуам нам те. Еум дицант елеифенд цонсецтетуер ет, суммо вереар епицуреи не про. Не лудус сцрипта опортере вим, еи дуо идяуе алияуам сигниферумяуе. Цум еу лабитур инвенире, про ессе губергрен темпорибус еи, ад хис минимум пертинах.\n" +
53+
"\n" +
54+
"Дуо ад вери евертитур интеллегат, демоцритум еффициенди дуо ет. Нец но доценди демоцритум сцрипторем, витуперата цонституам нецесситатибус ут вим. Яуи виде санцтус мандамус ан, нонумес принципес вел ат, ех дуо инани нулла. Петентиум маиестатис еам ин, те ерант дебитис еурипидис вис. Но вел антиопам цотидиеяуе еффициантур, сеа еи нибх нонумы инцидеринт.\n" +
55+
"\n" +
56+
"Одио омнес но яуо, популо ноструд иус ад. Инани хонестатис но вис. Хис еу лудус партем персиус, пурто малис витуперата при ан, еи елаборарет ассуеверит вим. Цу бруте утинам тинцидунт вих, цум ад дицтас лобортис лаборамус. Нец хабемус рецусабо ат, ех фацилис денияуе ест. При те велит алияуам аццусамус, юсто утамур антиопам но нам.\n" +
57+
"\n" +
58+
"Про не еррем иудицо мелиоре, еи цибо ерудити санцтус хас. Яуод еяуидем еу вис, вих яуидам легимус ад, ид сеа солум легере мандамус. Аеяуе детрахит ех иус, суас вертерем еум цу. Еи вим алиа ехерци пхаедрум, хас не лаборес цоррумпит. Ат граеци сцрипта вим.\n" +
59+
"\n" +
60+
"Иус ат менандри персеяуерис. Про модус дицта еу, ин граеци доценди фиерент при, еи хас аугуе мандамус дефинитионем. Ет путент интерпретарис сит, перицула сентентиае ат ест. При ут сумо видит волуптатибус, нобис деленити еа.";
61+
private static String TEST_DATA_3_BYTES = "案のづよド捕毎エオ文疑ろめた今宮レ秋像とが供持属ょー真場中ホサヒ不箱らご著質ーぼンろ保6年読さ系蔵べるル緩参フシセタ鮮県フずッ歳民ナセ楽飲匹恒桜ぱ。要電ネソメ嘉負向ス援中ぜく界党フネ属平ぎ象越容レ書95争効99争効7翌テ売約わこよッ紙点発事9入そさ補綱のラず他亭匠ぞ。\n" +
62+
"\n" +
63+
"天レ供内ソ愛7読でぽせ回書ほごしな浅月企設潟せぐり裂個ホヌヤ局題制エ柏央ざぽ。外くにさ下格か終所あ硬当ワ着少選とけリへ康件終にぎ季規らおず給測トユテ考毎サトス事版にーご文8忙チ深暮タヲムラ度6応しぞぎぐ装速て続際ぞ発准揮包孤てい。制はたちき合南む乙甲ゅさと捕4球任条こでン頭広セスモウ月夜エス面陽ヨネ力京ウリ紙聞ト印2火映ラ基頭スフ点愛伎協ねド。\n" +
64+
"\n" +
65+
"属と共代みむもず以監すい者新ス田政家ヱス使校音刑トホ則上ゅぐ一未ヌ意40芸標んは学必強ゅ帝歯没牧具もか。58新イシレ正米ニユ負皇っぐせの必容キソタコ公3容ーつぶべ年然検ざ整賞ニチ注興ぐ放約えあ野夜磨やゃフよ。柳ソシアテ申1科ル舗紀深むぜ競供とび室全ハネ測高エラク権暮ヲクオト館暮ヌ黒杯クリぴぽ火竹ねる種4帰替やあい北問クルゃン登壌粉つどべ。";
66+
67+
private static final String TEST_DATA_4_BYTES ="\uD800\uDE55\uD800\uDE55\uD800\uDE55 \uD800\uDE55\uD800\uDE55\uD800\uDE55\uD800\uDE55\uD800\uDE55\uD800\uDE55\uD800\uDE55\uD800\uDE55\uD800\uDE55\uD800\uDE55\uD800\uDE55\uD800\uDE55\uD800\uDE55\uD800\uDE55\uD800\uDE55\uD800\uDE55\uD800\uDE55\uD800\uDE55\uD800\uDE55\uD800\uDE55\uD800\uDE55" +
68+
"\uD800\uDE55\uD800\uDE55\uD800\uDE55 \uD800\uDE55\uD800\uDE55\uD800\uDE55\uD800\uDE80\uD800\uDE80\uD800\uDE80\uD800\uDE80\uD800\uDE80\uD800\uDE80\uD800\uDE80\uD800\uDE80\uD800\uDE80\uD800\uDE80\uD800\uDE80\uD800\uDE80\uD800\uDE80\uD800\uDE80\uD800\uDE80\uD800\uDE80\uD800\uDE80\uD800\uDE80\uD800\uDE80" +
69+
"\uD800\uDE55\uD800\uDE55\uD800\uDE55\uD800\uDE55\uD800\uDE55\uD800\uDE55\uD800\uDE80\uD800\uDE80\uD800\uDE80\uD800\uDE80\uD800\uDE80\uD800\uDE80\uD800\uDE80\uD800\uDE80\uD800\uDE80\uD800\uDE80\uD800\uDE80\uD800\uDE80\uD800\uDE80\uD800\uDE80\uD800\uDE80\uD800\uDE80\uD800\uDE80\uD800\uDE80\uD800\uDE80" +
70+
"\uD800\uDE55\uD800\uDE55\uD800\uDE55\uD800\uDE55\uD800\uDE55\uD800\uDE55\uD800\uDE80\uD800\uDE80\uD800\uDE80\uD800\uDE80\uD800\uDE80\uD800\uDE80\uD800\uDE80\uD800\uDE80\uD800\uDE80\uD800\uDE80\uD800\uDE80\uD800\uDE80\uD800\uDE80\uD800\uDE80\uD800\uDE80\uD800\uDE80\uD800\uDE80\uD800\uDE80\uD800\uDE80" +
71+
"\uD800\uDE80\uD800\uDE80\uD800\uDE80";
72+
73+
@Test
74+
public void testUnicode1Byte() {
75+
chunkString(TEST_DATA_1_BYTE, 64);
76+
}
77+
78+
@Test
79+
public void testUnicode2Bytes() {
80+
chunkString(TEST_DATA_2_BYTES, 63);
81+
}
82+
83+
@Test
84+
public void testUnicode3Bytes() throws Exception {
85+
chunkString(TEST_DATA_3_BYTES, 64);
86+
}
87+
88+
@Test
89+
public void testUnicode4Bytes() throws Exception {
90+
chunkString(TEST_DATA_4_BYTES, 111);
91+
}
92+
93+
private void chunkString(String originalString, int chunkSize) {
94+
byte data [] = originalString.getBytes(Charset.forName("UTF-8"));
95+
96+
StringBuilder builder = new StringBuilder();
97+
ProgressiveUTF8StreamDecoder collector = new ProgressiveUTF8StreamDecoder();
98+
byte[] buffer = new byte[chunkSize];
99+
for (int i = 0; i < data.length; i+= chunkSize) {
100+
int bytesRead = Math.min(chunkSize, data.length - i);
101+
System.arraycopy(data, i, buffer, 0, bytesRead );
102+
builder.append(collector.decodeNext(buffer, bytesRead ));
103+
}
104+
105+
String actualString = builder.toString();
106+
Assert.assertEquals(originalString, actualString);
107+
}
108+
}

0 commit comments

Comments
 (0)